class MailRoleEditForm(EditForm): """ An edit form for the mail action """ schema = IMailRoleAction label = _plone(u"Edit Mail Role Action") description = _plone(u"A mail action that can mail plone users who have " u"a role on the object") form_name = _plone(u"Configure element") template = ViewPageTemplateFile('templates/mail.pt') if not IS_PLONE_5: form_fields = form.FormFields(IMailRoleAction)
class MailSubscriptionsEditForm(EditForm): """ An edit form for the mail action """ form_fields = form.FormFields(IMailSubscriptionsAction) label = _plone(u"Edit Mail Subscriptions Action") description = _( u'form_description', default=u"A mail action that can mail users subscribed to this rule") form_name = _plone(u"Configure element") # custom template will allow us to add help text template = ViewPageTemplateFile('templates/mail.pt')
class IMailSubscriptionsAction(Interface): """Definition of the configuration available for a mail action """ subject = schema.TextLine(title=_plone(u"Subject"), description=_plone(u"Subject of the message"), required=True) source = schema.TextLine( title=_plone(u"Email source"), description=_plone("The email address that sends the \ email. If no email is provided here, it will use the portal from address."), required=False) message = schema.Text(title=_(u"Message"), description=_(u"The message that you want to mail."), required=True)
def toPloneboardTime(context, request, time_=None): """Return time formatted for Ploneboard""" ploneboard_time=None ts = getToolByName(context, 'translation_service') format = '%Y;%m;%d;%w;%H;%M;%S' # fallback formats, english young_format_en = '%A %H:%M' old_format_en = '%B %d. %Y' if not time_: return 'Unknown date' try: if isinstance(time_, DateTime): time_ = datetime.fromtimestamp(time_.timeTime()) else: time_ = dateparse(str(time_)) (year, month, day, hours, minutes, seconds, wday, _, dst) = time_.timetuple() translated_date_elements = { 'year' : year , 'month' : unicode(defer(translate, _locales(ts.month_msgid(month)), context=request)) , 'day' : day , 'wday' : unicode(defer(translate, _locales(ts.day_msgid((wday+1)%7)), context=request)) , 'hours' : hours , 'minutes': minutes , 'seconds': seconds } if time.time() - time.mktime(time_.timetuple()) < 604800: # 60*60*24*7 ploneboard_time = translate(_plone( 'young_date_format: ${wday} ${hours}:${minutes}' , default = unicode(time_.strftime(young_format_en)) , mapping=translated_date_elements) , context=request ) else: ploneboard_time = translate( _plone( 'old_date_format: ${year} ${month} ${day} ${hours}:${minutes}' , default = unicode(time_.strftime(old_format_en)) , mapping = translated_date_elements) , context=request ) except IndexError: pass return ploneboard_time
class MailSubscriptionsAddForm(AddForm): """ An add form for the mail action """ form_fields = form.FormFields(IMailSubscriptionsAction) label = _plone(u"Add Mail Action") description = _( u'form_description', default=u"A mail action that can mail users subscribed to this rule") form_name = _plone(u"Configure element") # custom template will allow us to add help text template = ViewPageTemplateFile('templates/mail.pt') def create(self, data): a = MailSubscriptionsAction() form.applyChanges(a, self.form_fields, data) return a
class IMailRoleAction(Interface): """Definition of the configuration available for a mail action """ subject = schema.TextLine(title=_plone(u"Subject"), description=_plone(u"Subject of the message"), required=True) source = schema.TextLine( title=_plone(u"Email source"), description=_plone("The email address that sends the \ email. If no email is provided here, it will use the portal from address."), required=False) role = schema.Choice(title=_(u'field_role_title', default=u"Role"), description=_(u'field_role_description', default="Select a role. \ The action will look up the all Plone site users who explicitly have this \ role on the object and send a message to their email address."), vocabulary="collective.contentrules.mailtorole.roles", required=True) acquired = schema.Bool(title=_(u'field_acquired_title', default=u"Acquired Roles"), description=_( u'field_acquired_description', default=u"Should users that have this \ role as an acquired role also receive this email?"), required=False) global_roles = schema.Bool(title=_(u'field_global_roles_title', default=u"Global Roles"), description=_( u'field_global_roles_description', default=u"Should users that have this \ role as a role in the whole site also receive this email?"), required=False) message = schema.Text(title=_plone(u"Message"), description=_( u'field_message_description', default=u"Type in here the message that you \ want to mail. Some defined content can be replaced: ${title} will be replaced \ by the title of the newly created item. ${url} will be replaced by the \ URL of the newly created item."), required=True)
class MailRoleAddForm(AddForm): """ An add form for the mail action """ schema = IMailRoleAction label = _plone(u"Add Mail Action") description = _( u'form_description', default=u"A mail action that can mail plone users who have " u"a role on the object") form_name = _plone(u"Configure element") Type = MailRoleAction template = ViewPageTemplateFile('templates/mail.pt') if not IS_PLONE_5: form_fields = form.FormFields(IMailRoleAction) def create(self, data): if IS_PLONE_5: return super(MailRoleAddForm, self).create(data) a = MailRoleAction() form.applyChanges(a, self.form_fields, data) return a
def toPloneboardTime(context, request, time_=None): """Return time formatted for Ploneboard""" ploneboard_time = None ts = getToolByName(context, 'translation_service') # format = '%Y;%m;%d;%w;%H;%M;%S' # fallback formats, english # young_format_en = '%A %H:%M' old_format_en = '%B %d. %Y' if not time_: return 'Unknown date' if callable(time_): time_ = time_() try: if isinstance(time_, DateTime): time_ = datetime.fromtimestamp(time_.timeTime()) else: time_ = dateparse(str(time_)) (year, month, day, hours, minutes, seconds, wday, _, dst) = time_.timetuple() translated_date_elements = { 'year': year, 'month': unicode( defer(translate, _locales(ts.month_msgid(month)), context=request)), 'day': day, 'wday': unicode( defer(translate, _locales(ts.day_msgid((wday + 1) % 7)), context=request)), 'hours': "%02i" % hours, 'minutes': "%02i" % minutes, 'seconds': "%02i" % seconds } if time.time() - time.mktime(time_.timetuple()) < 604800: # 60*60*24*7 ploneboard_time = translate('${wday} ${hours}:${minutes}', mapping=translated_date_elements, context=request) else: ploneboard_time = translate(_plone( 'old_date_format: ${year} ${month} ${day} ' '${hours}:${minutes}', default=unicode(time_.strftime(old_format_en).decode('utf-8')), mapping=translated_date_elements), context=request) except IndexError: pass return ploneboard_time
class ActionsPanelView(BrowserView): """ This manage the view displaying actions on context. """ def __init__(self, context, request): super(ActionsPanelView, self).__init__(context, request) self.context = context self.request = request self.parent = self.context.getParentNode() self.member = self.request.get('imio.actionspanel_member_cachekey', None) if not self.member: self.member = api.user.get_current() self.request.set('imio.actionspanel_member_cachekey', self.member) self.portal_url = self.request.get('imio.actionspanel_portal_url_cachekey', None) self.portal = self.request.get('imio.actionspanel_portal_cachekey', None) if not self.portal_url or not self.portal: self.portal = api.portal.get() self.portal_url = self.portal.absolute_url() self.request.set('imio.actionspanel_portal_url_cachekey', self.portal_url) self.request.set('imio.actionspanel_portal_cachekey', self.portal) self.SECTIONS_TO_RENDER = ('renderFolderContents', 'renderEdit', 'renderExtEdit', 'renderTransitions', 'renderArrows', 'renderOwnDelete', 'renderActions', 'renderAddContent', 'renderHistory') # portal_actions.object_buttons action ids not to keep # every actions will be kept except actions listed here self.IGNORABLE_ACTIONS = () # portal_actions.object_buttons action ids to keep # if you define some here, only these actions will be kept self.ACCEPTABLE_ACTIONS = () def __call__(self, useIcons=True, showTransitions=True, appendTypeNameToTransitionLabel=False, showEdit=True, showExtEdit=False, showOwnDelete=True, showActions=True, showAddContent=False, showHistory=False, showHistoryLastEventHasComments=True, showArrows=False, showFolderContents=False, arrowsPortalTypeAware=False, markingInterface=None, **kwargs): """ Master method that will render the content. This is not supposed to be overrided. """ self.useIcons = useIcons self.showTransitions = showTransitions self.appendTypeNameToTransitionLabel = appendTypeNameToTransitionLabel self.showEdit = showEdit self.showExtEdit = showExtEdit self.showOwnDelete = showOwnDelete # if 'delete' is in acceptable actions, it takes precedence on showOwnDelete if showActions and 'delete' in self.ACCEPTABLE_ACTIONS: self.showOwnDelete = False # if we manage our own delete, do not use Plone default one elif self.showOwnDelete and 'delete' not in self.IGNORABLE_ACTIONS: self.IGNORABLE_ACTIONS = self.IGNORABLE_ACTIONS + ('delete', ) self.showActions = showActions self.showAddContent = showAddContent self.showHistory = showHistory self.showHistoryLastEventHasComments = showHistoryLastEventHasComments self.showArrows = showArrows self.showFolderContents = showFolderContents # arrowsPortalTypeAware will change the script used to # change object position. It is used when several elements of # various portal_type are in the same container but we want to # order the elements of same portal_type together self.arrowsPortalTypeAware = arrowsPortalTypeAware self.markingInterface = markingInterface self.kwargs = kwargs self.hasActions = False return self.index() def isInFacetedNavigation(self): """Is the actions panel displayed in a faceted navigation?""" return bool(self.request['URL'].endswith('@@faceted_query')) def _renderSections(self): """ This will check what sections need to be rendered. This is not supposed to be overrided. """ res = '' for section in self.SECTIONS_TO_RENDER: renderedSection = getattr(self, section)() or '' res += renderedSection return res def renderArrows(self): """ Render arrows if user may change order of elements. """ if not self.useIcons: return '' if self.showArrows and self.member.has_permission(ManageProperties, self.parent): self.parentObjectIds = [ ob.id for ob in self.parent.objectValues() if (not self.arrowsPortalTypeAware or ob.portal_type == self.context.portal_type)] self.objId = self.context.getId() self.moveUrl = self._moveUrl() return ViewPageTemplateFile("actions_panel_arrows.pt")(self) return '' def _moveUrl(self): """ """ script_name = 'folder_position' if self.arrowsPortalTypeAware: script_name = 'folder_position_typeaware' return "{0}/{1}?position=%s&id=%s&template_id={2}".format( self.parent.absolute_url(), script_name, self._returnTo()) def _returnTo(self, ): """What URL should I return to after moving the element and page is refreshed.""" return self.request.getURL() def renderTransitions(self): """ Render the current context available workflow transitions. """ if self.showTransitions: return ViewPageTemplateFile("actions_panel_transitions.pt")(self) return '' def renderFolderContents(self): """ Render a 'folder_contents' action. """ if self.showFolderContents and \ (not self.markingInterface or self.isMarked(self.markingInterface, self.getCurrentFolder())) and \ self.mayFolderContents(): return ViewPageTemplateFile("actions_panel_folder_contents.pt")(self) return '' def renderEdit(self): """ Render a 'edit' action. By default, only available when actions are displayed as icons because when it is not the case, we already have a 'edit' tab and that would be redundant. """ if self.showEdit and self.mayEdit(): return ViewPageTemplateFile("actions_panel_edit.pt")(self) return '' def renderExtEdit(self): """ Render a 'external_edit' action. By default, only available when actions are displayed as icons because when it is not the case, we already have a 'external_edit' viewlet and that would be redundant. """ if self.showExtEdit and self.useIcons and self.mayExtEdit(): return ViewPageTemplateFile("actions_panel_ext_edit.pt")(self) return '' def renderOwnDelete(self): """ Render our own version of the 'delete' action. """ if self.showOwnDelete and \ IContentDeletable(self.context).mayDelete(): return ViewPageTemplateFile("actions_panel_own_delete.pt")(self) return '' def renderActions(self): """ Render actions coming from portal_actions.object_buttons and available on the context. """ if self.showActions: return ViewPageTemplateFile("actions_panel_actions.pt")(self) def renderAddContent(self): """ Render allowed_content_types coming from portal_type. """ if self.showAddContent: return ViewPageTemplateFile("actions_panel_add_content.pt")(self) def renderHistory(self): """ Render a link to the object's history (@@historyview). """ if self.showHistory and self.useIcons and self.showHistoryForContext(): return ViewPageTemplateFile("actions_panel_history.pt")(self) def showHistoryForContext(self): """ Method to control access to the @@historyview view and so to the action icon. We rely on view 'contenthistory' overrided in imio.history. """ contenthistory = getMultiAdapter((self.context, self.request), name='contenthistory') return contenthistory.show_history() def historyLastEventHasComments(self): """ Returns True if the last event of the object's history has a comment. """ adapter = getAdapter(self.context, IImioHistory, 'workflow') return adapter.historyLastEventHasComments() def mayFolderContents(self): """ Method that check if folder_contents action has to be displayed. """ if self.member.has_permission('List folder contents', self.context): plone_view = getMultiAdapter((self.context, self.request), name='plone') return bool(plone_view.displayContentsTab()) return False def mayEdit(self): """ Method that check if special 'edit' action has to be displayed. """ return self.member.has_permission('Modify portal content', self.context) def mayExtEdit(self): """ Method that check if special 'external_edit' action has to be displayed. """ if not self.member.has_permission('Modify portal content', self.context): return False portal_quickinstaller = api.portal.get_tool('portal_quickinstaller') external_edit_installed = portal_quickinstaller.isProductInstalled('collective.externaleditor') if not external_edit_installed: return False # Can be to slow for a dashboard ? # view = getMultiAdapter((self.context, self.request), name='externalEditorEnabled') # return view.available() # The availability check can be simpler because external_edit view check also availability # with available method registry = getUtility(IRegistry) # check if enabled if not registry.get('externaleditor.ext_editor', False): return False # check portal type externaleditor_enabled_types = registry.get('externaleditor.externaleditor_enabled_types', []) return self.context.portal_type in externaleditor_enabled_types def saveHasActions(self): """ Save the fact that we have actions. """ self.hasActions = True def sortTransitions(self, lst): """ Sort the list of transitions by title """ lst.sort(key=itemgetter('title')) def getTransitions(self, caching=True): """ This method is similar to portal_workflow.getTransitionsFor, but with some improvements: - we retrieve transitions that the user can't trigger, but for which he needs to know for what reason he can't trigger it; - for every transition, we know if we need to display a confirm popup or not; If caching=True, we will stored result in _transitions and use it if method is called again. """ if caching: if getattr(self, '_transitions', None): return self._transitions res = [] # Get the workflow definition for p_obj. workflow = self.request.get('imio.actionspanel_workflow_%s_cachekey' % self.context.portal_type, None) if not workflow: wfTool = api.portal.get_tool('portal_workflow') workflows = wfTool.getWorkflowsFor(self.context) if not workflows: return res workflow = workflows[0] self.request.set('imio.actionspanel_workflow_%s_cachekey' % self.context.portal_type, workflow) # What is the current state for self.context? currentState = workflow._getWorkflowStateOf(self.context) if not currentState: return res # Get the transitions to confirm from the config. toConfirm = self._transitionsToConfirmInfos() # Analyse all the transitions that start from this state. for transitionId in currentState.transitions: transition = workflow.transitions.get(transitionId, None) if transition and (transition.trigger_type == TRIGGER_USER_ACTION) \ and transition.actbox_name: # We have a possible candidate for a user-triggerable transition if transition.guard is None: mayTrigger = True else: mayTrigger = self._checkTransitionGuard(transition.guard, self.member, workflow, self.context) if mayTrigger or isinstance(mayTrigger, No): # Information about this transition must be part of result. # check if the transition have to be confirmed regarding # current object meta_type/portal_type and transition to trigger preNameMetaType = '%s.%s' % (self.context.meta_type, transition.id) preNamePortalType = '%s.%s' % (self.context.portal_type, transition.id) confirmation_view = toConfirm.get(preNameMetaType, '') or toConfirm.get(preNamePortalType, '') tInfo = { 'id': transition.id, # if the transition.id is not translated, use translated transition.title... 'title': translate(safe_unicode(transition.title), domain="plone", context=self.request), 'description': transition.description, 'name': transition.actbox_name, 'may_trigger': True, 'confirm': bool(confirmation_view), 'confirmation_view': confirmation_view or DEFAULT_CONFIRM_VIEW, 'url': transition.actbox_url % {'content_url': self.context.absolute_url(), 'portal_url': '', 'folder_url': ''}, 'icon': transition.actbox_icon % {'content_url': self.context.absolute_url(), 'portal_url': self.portal_url, 'folder_url': ''}, } if not mayTrigger: tInfo['may_trigger'] = False # mayTrigger.msg is a 'zope.i18nmessageid.message.Message', translate it now tInfo['reason'] = translate(mayTrigger.msg, context=self.request) res.append(tInfo) self.sortTransitions(res) if caching: # store transitions in case getTransitions is called several times setattr(self, '_transitions', res) return res def _transitionsToConfirmInfos(self): transitions = self._transitionsToConfirm() if type(transitions) is not dict: transitions = dict([(t, DEFAULT_CONFIRM_VIEW) for t in transitions]) else: for name, confirm_view in transitions.iteritems(): if not confirm_view: transitions[name] = DEFAULT_CONFIRM_VIEW return transitions def _transitionsToConfirm(self): """ Return the list of transitions the user will have to confirm, aka the user will be able to enter a comment for. This is a per meta_type or portal_type list of transitions to confirm. So for example, this could be : ('ATDocument.reject', 'Document.publish', 'Collection.publish', ) --> ATDocument is a meta_type and Document is a portal_type for example The list can also be a dict with the key being the transition name to confirm and the value being the name of the view to call to confirm the transition. eg: {'Document.reject': 'simpleconfirmview', 'Mytype.cancel': 'messageconfirmview'} If no confirmation view is provided (empty string) imio.actionspanel confirmation default view is used instead. """ values = api.portal.get_registry_record( 'imio.actionspanel.browser.registry.IImioActionsPanelConfig.transitions') if values is None: return () return dict([val.split('|') for val in values]) def _checkTransitionGuard(self, guard, sm, wf_def, ob): """ This method is similar to DCWorkflow.Guard.check, but allows to retrieve the truth value as a appy.gen.No instance, not simply "1" or "0". """ u_roles = None if wf_def.manager_bypass: # Possibly bypass. u_roles = sm.getRolesInContext(ob) if 'Manager' in u_roles: return 1 if guard.permissions: for p in guard.permissions: if _checkPermission(p, ob): break else: return 0 if guard.roles: # Require at least one of the given roles. if u_roles is None: u_roles = sm.getRolesInContext(ob) for role in guard.roles: if role in u_roles: break else: return 0 if guard.groups: # Require at least one of the specified groups. u = sm.getUser() b = aq_base(u) if hasattr(b, 'getGroupsInContext'): u_groups = u.getGroupsInContext(ob) elif hasattr(b, 'getGroups'): u_groups = u.getGroups() else: u_groups = () for group in guard.groups: if group in u_groups: break else: return 0 expr = guard.expr if expr is not None: econtext = createExprContext(StateChangeInfo(ob, wf_def)) res = expr(econtext) return res return 1 def getTransitionTitle(self, transition): '''Render the transition title including portal_type title if necessary.''' transition_title = transition['title'] if self.appendTypeNameToTransitionLabel: typesTool = api.portal.get_tool('portal_types') type_info = typesTool.getTypeInfo(self.context) transition_title = u"{0} {1}".format(safe_unicode(transition_title), translate(type_info.title, domain=type_info.i18n_domain, context=self.request)) return transition_title def computeTriggerTransitionLink(self, transition): """ """ return "{0}/{1}?transition={2}&actionspanel_view_name={3}{4}".format( self.context.absolute_url(), transition['confirmation_view'], transition['id'], self.__name__, not transition['confirm'] and '&form.submitted=1' or '') def computeTriggerTransitionOnClick(self, transition): """ """ transition_ids = [tr['id'] for tr in self.getTransitions()] # if transition is no more available, this means that element's transition # was already triggered by another user or in another tab, we just refresh the page if not transition or transition['id'] not in transition_ids: return 'window.location.href=window.location.href;' if not transition['confirm']: return "triggerTransition(baseUrl='{0}', viewName='@@triggertransition', transition='{1}', this);".format( self.context.absolute_url(), transition['id']) else: return '' def computeDeleteGivenUIDOnClick(self): """ """ return "deleteElement(baseUrl='{0}', viewName='@@delete_givenuid', object_uid='{1}');".format( self.context.absolute_url(), self.context.UID()) def addableContents(self): """ Return addable content types. """ if not self.context.isPrincipiaFolderish: return [] factories_view = getMultiAdapter((self.context, self.request), name='folder_factories') return factories_view.addable_types() def listObjectButtonsActions(self): """ Return a list of object_buttons actions coming from portal_actions/portal_types. """ actionsTool = api.portal.get_tool('portal_actions') typesTool = api.portal.get_tool('portal_types') # filter acceptable/ignorable actions before evaluating the TAL expression actions = [act for act in (actionsTool.listActions(categories=['object_buttons']) + tuple(typesTool.listActions(object=self.context, category='object_buttons'))) if (self.ACCEPTABLE_ACTIONS and act.id in self.ACCEPTABLE_ACTIONS) or (not self.ACCEPTABLE_ACTIONS and act.id not in self.IGNORABLE_ACTIONS)] ec = actionsTool._getExprContext(self.context) actions = [ActionInfo(action, ec) for action in actions] objectButtonActions = [act for act in actions if act['visible'] and act['allowed'] and act['available']] res = [] for action in objectButtonActions: act = action.copy() # We try to append the url of the icon of the action # look on the action itself if act['icon']: # make sure we only have the action icon name not a complete # path including portal_url or so, just take care that we do not have # an image in a static resource folder splittedIconPath = act['icon'].split('/') if len(splittedIconPath) > 1 and '++resource++' in splittedIconPath[-2]: # keep last 2 parts of the path act['icon'] = '/'.join((splittedIconPath[-2], splittedIconPath[-1], )) else: act['icon'] = splittedIconPath[-1] res.append(act) return res def triggerTransition(self, transition, comment, redirect=True): """ Triggers a p_transition on self.context. """ wfTool = api.portal.get_tool('portal_workflow') plone_utils = api.portal.get_tool('plone_utils') try: wfTool.doActionFor(self.context, transition, comment=comment) except Exception, exc: # abort because element state was changed transaction.abort() plone_utils.addPortalMessage(exc.message, type='warning') return # use transition title to translate so if several transitions have the same title, # we manage only one translation transition_title = wfTool.getWorkflowsFor(self.context)[0].transitions[transition].title or \ transition # add a portal message, we try to translate a specific one or add 'Item state changed.' as default msg = _(u'%s_done_descr' % safe_unicode(transition_title), default=_plone("Item state changed.")) plone_utils.addPortalMessage(msg) http_referer = self.request.get('HTTP_REFERER') # After having triggered a wfchange, it the current user # can not access the obj anymore, try to find a place viewable by the user redirectToUrl = self._redirectToViewableUrl() if redirectToUrl != http_referer: return redirectToUrl else: # in some cases, redirection is managed at another level, by jQuery for example if not redirect: return return http_referer