"""
open/dulcinea/lib/ui/attachment.qpy
"""
from dulcinea.attachable import Attachment
from dulcinea.common import format_user, format_date_time, format_action_link
from qp.lib.stored_file import new_file
from dulcinea.ui.browse import ImageArchiveDirectory
from qp.fill.stored_file import thumbnail_response
from dulcinea.ui.user.util import ensure_signed_in
from dulcinea.ui.util import boxtitle
from mimetypes import guess_type
from qp.fill.directory import Directory
from qp.fill.form import Form
from qp.fill.html import href, htmltag
from qp.fill.static import FileStream
from qp.fill.stored_file import format_file_size
from qp.fill.widget import StringWidget, TextWidget, FileWidget, CompositeWidget
from qp.pub.common import get_user, redirect, get_response, get_session
from qp.pub.common import page, get_publisher, not_found
from qpy import xml
def default_decorate:xml(attachable, body, title=None):
page(title, '
', body, '
')
def attachments_link:xml(prefix=''):
''
format_action_link('%sfile' % prefix, 'Details', title='Attachments')
'
'
def files_action_link:xml(attachable, url):
plural = "s"
if len(attachable.get_attachments()) == 1:
plural = ""
format_action_link(
url, '%d File%s' % (len(attachable.get_attachments()), plural),
title=', '.join([attachment.get_mime_type()
for attachment in attachable.get_attachments()]))
class AttachmentUI (Directory):
def __init__(self, attachable, decorate=default_decorate, multiple=5, allow_change=None):
"""(attachable:Attachable)"""
self.attachable = attachable
self.decorate = decorate
self.multiple = multiple
if allow_change is None:
self.allow_change = bool(get_user())
else:
self.allow_change = allow_change
def get_exports(self):
yield ('', '_q_index', 'Files', 'Attached files')
if self.allow_change:
yield ('upload', 'upload', 'Upload', 'Upload new file attachment')
if get_session().get_attachments():
yield ('attach', 'attach', 'Paste',
'Attach file attachments from clipboard here')
yield ('clear', 'clear', 'Clear',
'Clear file attachments on clipboard')
def _q_index:xml(self):
title = 'Attached files'
self.decorate(self.attachable,
self.render_attachments(),
title=title)
def render_attachments:xml(self):
if self.allow_change:
_format_clipboard_attachments(self.attachable)
user = get_user()
attachments = self.attachable.get_attachments()
''
if len(attachments) == 0:
'
No attached files.
'
else:
format_attachments(self.attachable, '',
show_action_links=self.allow_change)
'
'
def clear(self):
get_session().clear_attachments()
redirect('.')
def upload(self):
ensure_signed_in()
return upload_form(self.attachable, decorate=self.decorate,
multiple=self.multiple)
def attach(self):
for attachment in get_session().get_attachments():
new_attachment = Attachment(attachment.get_file(), get_user())
new_attachment.set_filename(attachment.get_filename())
new_attachment.set_description(attachment.get_description())
self.attachable.add_attachment(new_attachment)
get_session().clear_attachments()
redirect('.')
def get_data_ui(self):
return DataUI
def _q_lookup(self, component):
attachment = (self.attachable.get_attachment(component) or
get_session().get_attachment(component))
if attachment is not None:
return self.get_data_ui()(self.attachable, attachment,
decorate=self.decorate)
def _format_clipboard_attachments:xml(attachable):
if get_session().get_attachments():
''
boxtitle('Clipboard')
format_attachments(get_session(), '')
format_action_link('attach', 'Paste')
format_action_link('clear', 'Clear')
'
'
class UploadWidget(CompositeWidget):
def __init__(self, *args, **kwargs):
CompositeWidget.__init__(self, *args, **kwargs)
self.add(FileWidget, 'upload_file', size=60)
self.add(TextWidget, 'description', title='Description',
cols=70, rows=2)
def _parse(self, request):
if self['upload_file'] is not None:
self.value = (self['upload_file'], self['description'])
else:
self.value = None
class MultipleUploadWidget(CompositeWidget):
def __init__(self, name, multiple=3, **kwargs):
CompositeWidget.__init__(self, name, **kwargs)
for k in range(multiple):
self.add(UploadWidget, str(k))
def _parse(self, request):
values = [widget.parse() for widget in self.get_widgets()]
self.value = None
for value in values:
if value:
self.value = values
break
def render_content:xml(self):
'
'.join([widget.render() for widget in self.get_widgets()])
def upload_form(attachable, decorate=default_decorate, multiple=1):
form = Form(enctype='multipart/form-data')
for k in range(multiple):
form.add(UploadWidget, str(k))
if multiple > 1:
uploading = "files"
else:
uploading = "file"
hint = xml(
"When you press this, the your browser will start sending "
"the %s you have selected, and "
"it may take a while. "
"Please wait for the upload to complete, "
"or else it will be cancelled. ") % uploading
form.add_submit('upload', 'Upload', hint=hint)
form.add_submit('cancel', 'Cancel')
if form.get('cancel'):
redirect('.')
if form.has_errors() or not form.get('upload'):
def render_body:xml():
if multiple > 1:
'''
You can upload up to %s files at a time.
''' % multiple
form.render()
return decorate(attachable, render_body(), title='Upload File')
for k in range(multiple):
stored_file = create_stored_file_from_upload(
form.get(str(k)), attachable.get_allowed_mime_types(get_user()))
if stored_file:
attachable.attach_file(stored_file, get_user())
redirect('.')
def create_stored_file_from_upload(upload_description, allowed_mime_types):
stored_file = None
if upload_description is not None:
upload, description = upload_description
mime_type = (guess_type(upload.get_base_filename())[0] or
'application/octet-stream')
if allowed_mime_types and mime_type not in allowed_mime_types:
get_publisher().respond('Not allowed',
"Uploading %r files is not allowed." % mime_type)
stored_file = new_file(upload)
stored_file.set_mime_type(mime_type)
stored_file.set_filename(upload.get_base_filename())
stored_file.set_owner(get_user())
stored_file.set_description(description)
return stored_file
browser_map = {}
browser_map["application/x-gtar"] = ImageArchiveDirectory
browser_map["application/x-tar"] = ImageArchiveDirectory
browser_map["application/zip"] = ImageArchiveDirectory
browser_map[".tgz"] = ImageArchiveDirectory
browser_map[".tar.gz"] = ImageArchiveDirectory
browser_map[".zip"] = ImageArchiveDirectory
browser_map[".apk"] = ImageArchiveDirectory
browser_map[".jar"] = ImageArchiveDirectory
def is_browsable(mimetype, filename):
if mimetype in browser_map:
return browser_map[mimetype]
for key in browser_map:
if '/' not in key and filename.endswith(key):
return browser_map[key]
return None
def format_file:xml(file_obj, url, index=None, show_thumbnail=True, show_name=True,
show_details=True, thumbnail_size=None, show_browse=True, show_action_links=False, **extra):
if file_obj.get_hidden() and not file_obj.has_manage_access(get_user()):
return ''
'\n'
ui_available = isinstance(file_obj, Attachment)
if ui_available:
if not url.endswith(str('/')):
url += str('/')
filename = file_obj.get_filename()
# Add an unused query string so that renaming the file will invalidate
# the attachment in the browser cache. Browsers don't make it easy to
# force a reload of files that cannot be displayed (e.g. Word documents,
# PDFs).
if ui_available and show_thumbnail:
if file_obj.get_mime_type().startswith(str('image/')):
thumbnail = url + 'thumbnail'
if thumbnail_size:
thumbnail += '?%s' % thumbnail_size
extra = dict(width="%s" % thumbnail_size)
href(url + 'view',
htmltag('img',
src=thumbnail,
alt='[Thumbnail]',
css_class="thumbnail",
xml_end=True,
**extra))
if show_name:
if ui_available:
href(url + filename, '%s
' % filename)
else:
href(url, '%s
' % filename)
if show_details:
size = format_file_size(file_obj.get_size())
' '
'(%s, %s)' % (size, file_obj.get_mime_type())
''
def get_actions:xml():
if show_browse and is_browsable(file_obj.get_mime_type(), filename):
format_action_link('%sbrowse/' % url, 'Browse')
if show_action_links:
format_action_link('%scopy' % url, 'Copy',
css_class='button attachment_copy')
if file_obj.has_manage_access(get_user()):
format_action_link('%sdetach' % url, 'Detach')
format_action_link('%sedit' % url, 'Edit Properties')
if file_obj.get_hidden():
format_action_link('%sunhide' % url, 'Unhide')
else:
format_action_link('%shide' % url, 'Hide')
if index:
format_action_link('%sup' % url, 'Move up')
if ui_available and get_actions():
' %s' % get_actions()
''
if show_details:
' attached '
if file_obj.get_owner():
'by %s ' % format_user(file_obj.get_owner(), email=False)
'on %s' % format_date_time(file_obj.get_date())
''
if file_obj.get_description():
'\n%s' % file_obj.get_description()
def format_attachment_list:xml(attachments, path, show_action_links=False,
show_name=True, show_details=True, thumbnail_size=50, show_browse=True):
if attachments:
''
for index, attachment in enumerate(attachments):
format_file(attachment, '%s%s/' % (path, attachment.get_file_id()),
index=index,
thumbnail_size=thumbnail_size,
show_name=show_name, show_details=show_details,
show_thumbnail=True, show_action_links=show_action_links,
show_browse=show_browse)
'
'
def format_attachments:xml(attachable, path, show_action_links=False,
show_name=True, show_details=True, thumbnail_size=50, show_browse=True):
if attachable.get_attachments():
return format_attachment_list(
reversed(attachable.get_attachments()),
path, show_action_links=show_action_links, show_name=show_name, show_details=show_details,
thumbnail_size=thumbnail_size)
class DataUI (Directory):
cache_time = 24*3600 # seconds to cache _q_index and thumbnail
def __init__(self, attachable, attachment, decorate=default_decorate):
self.attachable = attachable
self.attachment = attachment
self.decorate = decorate
def get_exports(self):
yield ('', '_q_index', self.attachment.get_filename(),
self.attachment.get_description())
yield ('view', 'view', 'View', None)
yield ('view_full', 'view_full', None, None)
yield ('copy', 'copy', None, None)
yield ('thumbnail', 'thumbnail' , None, None)
if self.attachment.has_manage_access(get_user()):
yield ('detach', 'detach', 'Detach', 'Detach file')
yield ('edit', 'edit', 'Edit', 'Edit file properties')
yield ('unhide', 'unhide', 'Unhide', 'Unhide this file')
yield ('hide', 'hide', 'Hide', 'Hide this file')
yield ('up', 'up', 'Move up', None)
if self.attachment.get_mime_type() in ("application/x-gtar",
"application/x-tar"
"application/zip"):
yield ('browse', None, 'Browse', 'Browse into this archive')
def _q_index(self):
response = get_response()
response.set_expires(seconds=self.cache_time)
try:
fp = self.attachment.open()
except IOError:
not_found()
response.set_content_type(
self.attachment.get_mime_type(), None)
response.set_header('Content-Disposition',
'inline; filename="%s"'
% self.attachment.get_filename())
return FileStream(fp, length=self.attachment.get_size())
def _q_lookup(self, name):
if name == self.attachment.get_filename():
return self._q_index()
if name == 'browse':
directory = is_browsable(
self.attachment.get_mime_type(),
self.attachment.get_filename())
if directory:
return directory(self.attachment.get_file().get_full_path(),
decorate=self.decorate, obj=self.attachable)
def __call__(self):
"""
Use _q_index after the last component is traversed,
even if the last component is not empty.
This makes it so that the URL for a file within a
tar archive does not need to end with a slash.
"""
return self._q_index()
def copy(self):
get_session().add_attachment(self.attachment)
redirect('..')
def detach(self):
return detach_confirm_form(self.attachable,
self.attachment,
decorate=self.decorate)
def edit(self):
return edit_properties_form(self.attachable,
self.attachment,
decorate=self.decorate)
def unhide(self):
self.attachment.set_hidden(False)
redirect('..')
def hide(self):
self.attachment.set_hidden(True)
redirect('..')
def up(self):
for index, attachment in enumerate(self.attachable.get_attachments()):
if attachment is self.attachment:
self.attachable.move_attachment(attachment, index+1)
redirect('..')
def thumbnail(self):
if not self.attachment.is_image():
return None
try:
fp = self.attachment.open()
except IOError:
# attachment file is probably missing
not_found('attached file not found')
# Manufacture a thumbnail image
return thumbnail_response(fp, cache_time=self.cache_time, mime_type=self.attachment.get_mime_type())
def view_full(self):
def body:xml():
''
self.attachment.get_description()
'
'
if self.attachment.is_image():
href('./?%s' % self.attachment.get_filename(),
'',
title="Click for original version.")
return self.decorate(self.attachable, body(),
self.attachment.get_filename())
def view(self):
def body:xml():
''
self.attachment.get_description()
'
'
if self.attachment.is_image():
href('./view_full',
'',
title="Click to view full sized version.")
return self.decorate(self.attachable, body(),
self.attachment.get_filename())
def detach_confirm_form(attachable, attachment,
decorate=default_decorate):
if not attachment.has_manage_access(get_user()):
not_found()
form = Form()
redirect_path = '..'
form.add_submit('detach', 'Detach')
if form['detach']:
attachable.detach_attachment(attachment)
redirect(redirect_path)
form.add_submit('cancel', 'Cancel')
if form['cancel']:
redirect(redirect_path)
def render:xml():
'Detach the file below?
'
''
format_file(attachment, ".", show_thumbnail=True)
'
'
form.render()
return decorate(attachable, render(),
title='Confirm: Detach file?')
def edit_properties_form(attachable, attachment,
decorate=default_decorate):
if not attachment.has_manage_access(get_user()):
not_found()
form = Form()
redirect_path = '..'
form.add_submit('update', 'Update')
form.add_submit('cancel', 'Cancel')
if form['cancel']:
redirect(redirect_path)
form.add(StringWidget, 'filename',
value=attachment.get_filename(),
title='Filename',
required=1)
form.add(TextWidget, 'description',
value=attachment.get_description(),
title='Description',
cols=40, rows=2)
if not form.is_submitted() or form.has_errors():
def render:xml():
'''
Note that the file properties are stored as part of the file
object and will be the same wherever this file is attached.
'''
form.render()
return decorate(attachable, render(),
title='Edit file properties')
# XXX perhaps this modification should not happen in-place
attachment_modified = False
file_name = form['filename']
if attachment.get_filename() != file_name:
attachment.set_filename(file_name)
attachment_modified = True
description = form['description']
if attachment.get_description() != description:
attachment.set_description(description)
attachment_modified = True
# If the attachment has been modified, notify the attachable
if attachment_modified:
attachable.attachment_modified(attachment, get_user())
redirect(redirect_path)
class ClipboardUI (DataUI):
"""Show object currently on clipboard.
"""
def get_exports(self):
yield ('thumbnail', 'thumbnail', None, None)
cache_time = -1 # don't cache since it changes
def format_attachment_css:str():
"""
div.clipboard {
margin: 1em 0 1em 0;
background-color: #ffd865;
color: black;
}
div.clipboard a {
color: blue;
}
div.clipboard div.boxtitle {
color: #00008B;
margin-bottom: 1em;
}
dt.attachment {
clear: left;
}
dt.attachment span.sub-title {
font-weight: normal;
font-size: 75%;
}
dd.attachment {
margin-top: 0.5ex;
margin-bottom: 0.5ex;
font-size: smaller;
}
"""