import logging from AccessControl import Unauthorized from Acquisition import aq_inner from OFS.Image import File from Products.Archetypes.atapi import DisplayList from Products.Archetypes.utils import contentDispositionHeader from Products.CMFCore.utils import getToolByName from Products.CMFPlone import PloneMessageFactory as PMF from Products.CMFPlone.utils import safe_unicode from Products.Five.browser import BrowserView from Products.statusmessages.interfaces import IStatusMessage from plone.memoize.view import memoize from zope.cachedescriptors.property import Lazy from zope.i18n import translate from zope.interface import implements from zope.lifecycleevent import modified try: from plone.i18n.normalizer.interfaces import \ IUserPreferredFileNameNormalizer FILE_NORMALIZER = True except ImportError: FILE_NORMALIZER = False from Products.Poi import PoiMessageFactory as _ from Products.Poi import permissions from Products.Poi.adapters import IResponseContainer from Products.Poi.adapters import Response from Products.Poi.browser.interfaces import IResponseAdder from Products.Poi.config import DEFAULT_ISSUE_MIME_TYPE logger = logging.getLogger('Poi') def pretty_size(size): if size <= 0: return "0 Kb" kb = size / 1024 size = "%d Kb" % kb if kb > 999: mb = kb / 1024 size = "%d Mb" % mb if mb > 999: gb = mb / 1024 size = "%d Gb" % gb return size def voc2dict(vocab, current=None): """Make a dictionary from a vocabulary. >>> from Products.Archetypes.atapi import DisplayList >>> vocab = DisplayList() >>> vocab.add('a', "The letter A") >>> voc2dict(vocab) [{'checked': '', 'value': 'a', 'label': 'The letter A'}] >>> vocab.add('b', "The letter B") >>> voc2dict(vocab) [{'checked': '', 'value': 'a', 'label': 'The letter A'}, {'checked': '', 'value': 'b', 'label': 'The letter B'}] >>> voc2dict(vocab, current='c') [{'checked': '', 'value': 'a', 'label': 'The letter A'}, {'checked': '', 'value': 'b', 'label': 'The letter B'}] >>> voc2dict(vocab, current='b') [{'checked': '', 'value': 'a', 'label': 'The letter A'}, {'checked': 'checked', 'value': 'b', 'label': 'The letter B'}] """ options = [] for value, label in vocab.items(): checked = (value == current) and "checked" or "" options.append(dict(value=value, label=label, checked=checked)) return options class Base(BrowserView): """Base view for PoiIssues. Mostly meant as helper for adding a response. """ def __init__(self, context, request): self.context = context self.request = request self.folder = IResponseContainer(context) self.mimetype = DEFAULT_ISSUE_MIME_TYPE self.use_wysiwyg = (self.mimetype == 'text/html') def responses(self): context = aq_inner(self.context) trans = context.portal_transforms items = [] linkDetection = context.linkDetection for id, response in enumerate(self.folder): if response is None: # Has been removed. continue # Use the already rendered response when available if response.rendered_text is None: rendering_success = True if response.mimetype in ('text/html', 'text/x-html-safe'): html = response.text else: html = trans.convertTo('text/html', response.text, mimetype=response.mimetype) if html is None: logger.debug("Conversion to text/html failed for " "response id %s of %s", id, context.absolute_url()) html = u'' rendering_success = False else: html = html.getData() if rendering_success: # Detect links like #1 and r1234 html = linkDetection(html) response.rendered_text = html html = response.rendered_text or u'' info = dict(id=id, response=response, attachment=self.attachment_info(id), html=html) items.append(info) return items @property @memoize def portal_url(self): context = aq_inner(self.context) plone = context.restrictedTraverse('@@plone_portal_state') return plone.portal_url() def attachment_info(self, id): """Get icon and other info for attachment Taken partly from Archetypes/skins/archetypes/getBestIcon.py """ context = aq_inner(self.context) response = self.folder[id] attachment = response.attachment if attachment is None: return None from zExceptions import NotFound icon = None mtr = getToolByName(context, 'mimetypes_registry', None) if mtr is None: icon = context.getIcon() lookup = mtr.lookup(attachment.content_type) if lookup: mti = lookup[0] try: context.restrictedTraverse(mti.icon_path) icon = mti.icon_path except (NotFound, KeyError, AttributeError): pass if icon is None: icon = context.getIcon() filename = getattr(attachment, 'filename', attachment.getId()) info = dict( icon=self.portal_url + '/' + icon, url=context.absolute_url() + '/@@poi_response_attachment?response_id=' + str(id), content_type=attachment.content_type, size=pretty_size(attachment.size), filename=filename, ) return info @Lazy def memship(self): context = aq_inner(self.context) return getToolByName(context, 'portal_membership') @property @memoize def can_edit_response(self): context = aq_inner(self.context) return self.memship.checkPermission('Poi: Edit response', context) @property @memoize def can_delete_response(self): context = aq_inner(self.context) return self.memship.checkPermission('Delete objects', context) def validate_response_id(self): """Validate the response id from the request. Return -1 if for example the response id does not exist. Return the response id otherwise. Side effect: an informative status message is set. """ status = IStatusMessage(self.request) response_id = self.request.form.get('response_id', None) if response_id is None: msg = _(u"No response selected.") msg = translate(msg, 'Poi', context=self.request) status.addStatusMessage(msg, type='error') return -1 else: try: response_id = int(response_id) except ValueError: msg = _(u"Response id ${response_id} is no integer.", mapping=dict(response_id=response_id)) msg = translate(msg, 'Poi', context=self.request) status.addStatusMessage(msg, type='error') return -1 if response_id >= len(self.folder): msg = _(u"Response id ${response_id} does not exist.", mapping=dict(response_id=response_id)) msg = translate(msg, 'Poi', context=self.request) status.addStatusMessage(msg, type='error') return -1 else: return response_id # fallback return -1 @property def severity(self): context = aq_inner(self.context) return context.getSeverity() @property def targetRelease(self): context = aq_inner(self.context) return context.getTargetRelease() @property def responsibleManager(self): context = aq_inner(self.context) return context.getResponsibleManager() @property @memoize def transitions_for_display(self): """Display the available transitions for this issue. """ context = aq_inner(self.context) if not self.memship.checkPermission(permissions.ModifyIssueState, context): return [] wftool = getToolByName(context, 'portal_workflow') transitions = [] transitions.append(dict(value='', label=PMF(u'No change'), checked="checked")) for tdef in wftool.getTransitionsFor(context): transitions.append(dict(value=tdef['id'], label=tdef['title_or_id'], checked='')) return transitions @property def available_transitions(self): """Get the available transitions for this issue. """ return [x['value'] for x in self.transitions_for_display] @property def severities_for_display(self): """Get the available severities for this issue. """ vocab = self.available_severities options = [] for value in vocab: checked = (value == self.severity) and "checked" or "" options.append(dict(value=value, label=value, checked=checked)) return options @property @memoize def available_severities(self): """Get the available severities for this issue. """ # get vocab from tracker so use aq_inner context = aq_inner(self.context) if not self.memship.checkPermission( permissions.ModifyIssueSeverity, context): return [] return context.getAvailableSeverities() @property def releases_for_display(self): """Get the releases from the project. Usually nothing, unless you use Poi in combination with PloneSoftwareCenter. """ vocab = self.available_releases current = self.targetRelease return voc2dict(vocab, current) @property @memoize def available_releases(self): """Get the releases from the project. Usually nothing, unless you use Poi in combination with PloneSoftwareCenter. """ # get vocab from issue context = aq_inner(self.context) if not self.memship.checkPermission( permissions.ModifyIssueTargetRelease, context): return DisplayList() return context.getReleasesVocab() @property def show_target_releases(self): """Should the option for selecting a target release be shown? There is always at least one option: None. So only show when there is more than one option. """ return len(self.available_releases) > 1 @property def managers_for_display(self): """Get the tracker managers. """ vocab = self.available_managers return voc2dict(vocab, self.responsibleManager) @property @memoize def available_managers(self): """Get the tracker managers. """ # get vocab from issue context = aq_inner(self.context) if not self.memship.checkPermission( permissions.ModifyIssueAssignment, context): return DisplayList() return context.getManagersVocab() @property @memoize def upload_allowed(self): """Is the user allowed to upload on attachment? """ context = aq_inner(self.context) return self.memship.checkPermission( permissions.UploadAttachment, context) class AddForm(Base): implements(IResponseAdder) #template = ViewPageTemplateFile('response.pt') def __init__(self, context, request, view): super(AddForm, self).__init__(context, request) self.__parent__ = view def update(self): pass def render(self): # self.template is defined in zcml return self.template() class Create(Base): def determine_response_type(self, response): """Return a string indicating the type of response this is. """ responseCreator = response.creator if responseCreator == '(anonymous)': return 'additional' issue = aq_inner(self.context) if responseCreator == issue.Creator(): return 'clarification' if responseCreator in self.available_managers: return 'reply' # default: return 'additional' def __call__(self): form = self.request.form context = aq_inner(self.context) if not self.memship.checkPermission('Poi: Add Response', context): raise Unauthorized response_text = form.get('response', u'') new_response = Response(response_text) new_response.mimetype = self.mimetype new_response.type = self.determine_response_type(new_response) issue_has_changed = False transition = form.get('transition', u'') if transition and transition in self.available_transitions: wftool = getToolByName(context, 'portal_workflow') before = wftool.getInfoFor(context, 'review_state') before = wftool.getTitleForStateOnType(before, 'PoiIssue') wftool.doActionFor(context, transition) after = wftool.getInfoFor(context, 'review_state') after = wftool.getTitleForStateOnType(after, 'PoiIssue') new_response.add_change('review_state', _(u'Issue state'), before, after) issue_has_changed = True options = [ ('severity', _(u'Severity'), 'available_severities'), ('responsibleManager', _(u'Responsible manager'), 'available_managers'), ] # Changes that need to be applied to the issue (apart from # workflow changes that need to be handled separately). changes = {} for option, title, vocab in options: new = form.get(option, u'') if new and new in self.__getattribute__(vocab): current = self.__getattribute__(option) if current != new: changes[option] = new new_response.add_change(option, title, current, new) issue_has_changed = True #('targetRelease', 'Target release', 'available_releases'), new = form.get('targetRelease', u'') if new and new in self.available_releases: current = self.targetRelease if current != new: # from value (uid) to key (id) new_label = self.available_releases.getValue(new) current_label = self.available_releases.getValue(current) changes['targetRelease'] = new new_response.add_change('targetRelease', _(u'Target release'), current_label, new_label) issue_has_changed = True attachment = form.get('attachment') if attachment: # File(id, title, file) data = File(attachment.filename, attachment.filename, attachment) new_response.attachment = data issue_has_changed = True if len(response_text) == 0 and not issue_has_changed: status = IStatusMessage(self.request) msg = _(u"No response text added and no issue changes made.") msg = translate(msg, 'Poi', context=self.request) status.addStatusMessage(msg, type='error') else: # Apply changes to issue context.update(**changes) # Add response self.folder.add(new_response) self.request.response.redirect(context.absolute_url()) class Edit(Base): @property @memoize def response(self): form = self.request.form response_id = form.get('response_id', None) if response_id is None: return None try: response_id = int(response_id) except ValueError: return None if response_id >= len(self.folder): return None return self.folder[response_id] @property def response_found(self): return self.response is not None class Save(Base): def __call__(self): form = self.request.form context = aq_inner(self.context) status = IStatusMessage(self.request) if not self.can_edit_response: msg = _(u"You are not allowed to edit responses.") msg = translate(msg, 'Poi', context=self.request) status.addStatusMessage(msg, type='error') else: response_id = form.get('response_id', None) if response_id is None: msg = _(u"No response selected for saving.") msg = translate(msg, 'Poi', context=self.request) status.addStatusMessage(msg, type='error') elif self.folder[response_id] is None: msg = _(u"Response does not exist anymore; perhaps it was " "removed by another user.") msg = translate(msg, 'Poi', context=self.request) status.addStatusMessage(msg, type='error') else: response = self.folder[response_id] response_text = form.get('response', u'') response.text = response_text # Remove cached rendered response. response.rendered_text = None msg = _(u"Changes saved to response id ${response_id}.", mapping=dict(response_id=response_id)) msg = translate(msg, 'Poi', context=self.request) status.addStatusMessage(msg, type='info') # Fire event. We put the context in the descriptions # so event handlers can use this fully acquisition # wrapped object to do their thing. Feels like # cheating, but it gets the job done. Arguably we # could turn the two arguments around and signal that # the issue has changed, with the response in the # event descriptions. modified(response, context) self.request.response.redirect(context.absolute_url()) class Delete(Base): def __call__(self): context = aq_inner(self.context) status = IStatusMessage(self.request) if not self.can_delete_response: msg = _(u"You are not allowed to delete responses.") msg = translate(msg, 'Poi', context=self.request) status.addStatusMessage(msg, type='error') else: response_id = self.request.form.get('response_id', None) if response_id is None: msg = _(u"No response selected for removal.") msg = translate(msg, 'Poi', context=self.request) status.addStatusMessage(msg, type='error') else: try: response_id = int(response_id) except ValueError: msg = _(u"Response id ${response_id} is no integer so it " "cannot be removed.", mapping=dict(response_id=response_id)) msg = translate(msg, 'Poi', context=self.request) status.addStatusMessage(msg, type='error') self.request.response.redirect(context.absolute_url()) return if response_id >= len(self.folder): msg = _(u"Response id ${response_id} does not exist so it " "cannot be removed.", mapping=dict(response_id=response_id)) msg = translate(msg, 'Poi', context=self.request) status.addStatusMessage(msg, type='error') else: self.folder.delete(response_id) msg = _(u"Removed response id ${response_id}.", mapping=dict(response_id=response_id)) msg = translate(msg, 'Poi', context=self.request) status.addStatusMessage(msg, type='info') self.request.response.redirect(context.absolute_url()) class Download(Base): """Download the attachment of a response. """ def __call__(self): context = aq_inner(self.context) request = self.request response_id = self.validate_response_id() file = None if response_id != -1: response = self.folder[response_id] file = response.attachment if file is None: status = IStatusMessage(request) msg = _(u"Response id ${response_id} has no attachment.", mapping=dict(response_id=response_id)) msg = translate(msg, 'Poi', context=context) status.addStatusMessage(msg, type='error') if file is None: request.response.redirect(context.absolute_url()) # From now on file exists. # Code mostly taken from Archetypes/Field.py:FileField.download filename = getattr(file, 'filename', file.getId()) if filename is not None: if FILE_NORMALIZER: filename = IUserPreferredFileNameNormalizer(request).normalize( safe_unicode(filename, context.getCharset())) else: filename = safe_unicode(filename, context.getCharset()) header_value = contentDispositionHeader( disposition='attachment', filename=filename) request.response.setHeader("Content-disposition", header_value) return file.index_html(request, request.response)