""" 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(), 'thumbnail', 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', 'thumbnail', 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; } """