class ActionDescription(object): """ Allowed status are: - open - closed-validated - closed-invalidated - closed-deprecated """ ARCHIVING = 'archiving' COMMENT = 'content-comment' CREATION = 'creation' DELETION = 'deletion' EDITION = 'edition' # Default action if unknow REVISION = 'revision' STATUS_UPDATE = 'status-update' UNARCHIVING = 'unarchiving' UNDELETION = 'undeletion' MOVE = 'move' _ICONS = { 'archiving': 'fa fa-archive', 'content-comment': 'fa-comment-o', 'creation': 'fa-magic', 'deletion': 'fa-trash', 'edition': 'fa-edit', 'revision': 'fa-history', 'status-update': 'fa-random', 'unarchiving': 'fa-file-archive-o', 'undeletion': 'fa-trash-o', 'move': 'fa-arrows' } _LABELS = { 'archiving': l_('archive'), 'content-comment': l_('Item commented'), 'creation': l_('Item created'), 'deletion': l_('Item deleted'), 'edition': l_('item modified'), 'revision': l_('New revision'), 'status-update': l_('New status'), 'unarchiving': l_('Item unarchived'), 'undeletion': l_('Item undeleted'), 'move': l_('Item moved') } def __init__(self, id): assert id in ActionDescription.allowed_values() self.id = id self.label = ActionDescription._LABELS[id] self.icon = ActionDescription._ICONS[id] self.css = '' @classmethod def allowed_values(cls): return [ cls.ARCHIVING, cls.COMMENT, cls.CREATION, cls.DELETION, cls.EDITION, cls.REVISION, cls.STATUS_UPDATE, cls.UNARCHIVING, cls.UNDELETION, cls.MOVE ]
class UserRoleInWorkspace(DeclarativeBase): __tablename__ = 'user_workspace' user_id = Column(Integer, ForeignKey('users.user_id'), nullable=False, default=None, primary_key=True) workspace_id = Column(Integer, ForeignKey('workspaces.workspace_id'), nullable=False, default=None, primary_key=True) role = Column(Integer, nullable=False, default=0, primary_key=False) do_notify = Column(Boolean, unique=False, nullable=False, default=False) workspace = relationship('Workspace', remote_side=[Workspace.workspace_id], backref='roles', lazy='joined') user = relationship('User', remote_side=[User.user_id], backref='roles') NOT_APPLICABLE = 0 READER = 1 CONTRIBUTOR = 2 CONTENT_MANAGER = 4 WORKSPACE_MANAGER = 8 LABEL = dict() LABEL[0] = l_('N/A') LABEL[1] = l_('Reader') LABEL[2] = l_('Contributor') LABEL[4] = l_('Content Manager') LABEL[8] = l_('Workspace Manager') STYLE = dict() STYLE[0] = '' STYLE[1] = 'color: #1fdb11;' STYLE[2] = 'color: #759ac5;' STYLE[4] = 'color: #ea983d;' STYLE[8] = 'color: #F00;' ICON = dict() ICON[0] = '' ICON[1] = 'fa-eye' ICON[2] = 'fa-pencil' ICON[4] = 'fa-graduation-cap' ICON[8] = 'fa-legal' @property def icon(self): return UserRoleInWorkspace.ICON[self.role] @property def style(self): return UserRoleInWorkspace.STYLE[self.role] def role_as_label(self): return UserRoleInWorkspace.LABEL[self.role] @classmethod def get_all_role_values(self): return [ UserRoleInWorkspace.READER, UserRoleInWorkspace.CONTRIBUTOR, UserRoleInWorkspace.CONTENT_MANAGER, UserRoleInWorkspace.WORKSPACE_MANAGER ]
class Profile(object): """This model is the "max" group associated to a given user.""" _NAME = [ Group.TIM_NOBODY_GROUPNAME, Group.TIM_USER_GROUPNAME, Group.TIM_MANAGER_GROUPNAME, Group.TIM_ADMIN_GROUPNAME ] _LABEL = [ l_('Nobody'), l_('Users'), l_('Global managers'), l_('Administrators') ] def __init__(self, profile_id): assert isinstance(profile_id, int) self.id = profile_id self.name = Profile._NAME[profile_id] self.label = Profile._LABEL[profile_id]
class Globals(object): """Container for objects available throughout the life of the application. One instance of Globals is created during application initialization and is available during requests via the 'app_globals' variable. """ def __init__(self): """Do nothing, by default.""" pass VERSION_NUMBER = '1.0.3' SHORT_DATE_FORMAT = l_('%B %d at %I:%M%p')
# Don't start daemons if they are disabled if config.get('disable_daemons', False): return manager.run('radicale', RadicaleDaemon) manager.run('webdav', WsgiDavDaemon) if cfg.EMAIL_PROCESSING_MODE == CFG.CST.ASYNC: manager.run('mail_sender', MailSenderDaemon) environment_loaded.register(lambda: start_daemons(daemons)) interrupt_manager = InterruptManager(os.getpid(), daemons_manager=daemons) # Note: here are fake translatable strings that allow to translate messages for reset password email content duplicated_email_subject = l_('Password reset request') duplicated_email_body = l_(''' We've received a request to reset the password for this account. Please click this link to reset your password: %(password_reset_link)s If you no longer wish to make the above change, or if you did not initiate this request, please disregard and/or delete this e-mail. ''') ####### # # INFO - D.A. - 2014-10-31 # The following code is a dirty way to integrate translation for resetpassword tgapp in tracim # TODO - Integrate these translations into tgapp-resetpassword #
class ContentType(object): Any = 'any' Folder = 'folder' File = 'file' Comment = 'comment' Thread = 'thread' Page = 'page' Event = 'event' # Fake types, used for breadcrumb only FAKE_Dashboard = 'dashboard' FAKE_Workspace = 'workspace' _STRING_LIST_SEPARATOR = ',' _ICONS = { # Deprecated 'dashboard': 'fa-home', 'workspace': 'fa-bank', 'folder': 'fa fa-folder-open-o', 'file': 'fa fa-paperclip', 'page': 'fa fa-file-text-o', 'thread': 'fa fa-comments-o', 'comment': 'fa fa-comment-o', 'event': 'fa fa-calendar-o', } _CSS_ICONS = { 'dashboard': 'fa fa-home', 'workspace': 'fa fa-bank', 'folder': 'fa fa-folder-open-o', 'file': 'fa fa-paperclip', 'page': 'fa fa-file-text-o', 'thread': 'fa fa-comments-o', 'comment': 'fa fa-comment-o', 'event': 'fa fa-calendar-o', } _CSS_COLORS = { 'dashboard': 't-dashboard-color', 'workspace': 't-less-visible', 'folder': 't-folder-color', 'file': 't-file-color', 'page': 't-page-color', 'thread': 't-thread-color', 'comment': 't-thread-color', 'event': 't-event-color', } _ORDER_WEIGHT = { 'folder': 0, 'page': 1, 'thread': 2, 'file': 3, 'comment': 4, 'event': 5, } _LABEL = { 'dashboard': '', 'workspace': l_('workspace'), 'folder': l_('folder'), 'file': l_('file'), 'page': l_('page'), 'thread': l_('thread'), 'comment': l_('comment'), 'event': l_('event'), } _DELETE_LABEL = { 'dashboard': '', 'workspace': l_('Delete this workspace'), 'folder': l_('Delete this folder'), 'file': l_('Delete this file'), 'page': l_('Delete this page'), 'thread': l_('Delete this thread'), 'comment': l_('Delete this comment'), 'event': l_('Delete this event'), } @classmethod def get_icon(cls, type: str): assert (type in ContentType._ICONS) # DYN_REMOVE return ContentType._ICONS[type] @classmethod def all(cls): return cls.allowed_types() @classmethod def allowed_types(cls): return [ cls.Folder, cls.File, cls.Comment, cls.Thread, cls.Page, cls.Event ] @classmethod def allowed_types_for_folding(cls): # This method is used for showing only "main" types in the left-side treeview return [cls.Folder, cls.File, cls.Thread, cls.Page] @classmethod def allowed_types_from_str(cls, allowed_types_as_string: str): allowed_types = [] # HACK - THIS for item in allowed_types_as_string.split( ContentType._STRING_LIST_SEPARATOR): if item and item in ContentType.allowed_types_for_folding(): allowed_types.append(item) return allowed_types @classmethod def fill_url(cls, content: 'Content'): # TODO - DYNDATATYPE - D.A. - 2014-12-02 # Make this code dynamic loading data types if content.type == ContentType.Folder: return '/workspaces/{}/folders/{}'.format(content.workspace_id, content.content_id) elif content.type == ContentType.File: return '/workspaces/{}/folders/{}/files/{}'.format( content.workspace_id, content.parent_id, content.content_id) elif content.type == ContentType.Thread: return '/workspaces/{}/folders/{}/threads/{}'.format( content.workspace_id, content.parent_id, content.content_id) elif content.type == ContentType.Page: return '/workspaces/{}/folders/{}/pages/{}'.format( content.workspace_id, content.parent_id, content.content_id) @classmethod def fill_url_for_workspace(cls, workspace: Workspace): # TODO - DYNDATATYPE - D.A. - 2014-12-02 # Make this code dynamic loading data types return '/workspaces/{}'.format(workspace.workspace_id) @classmethod def sorted(cls, types: ['ContentType']) -> ['ContentType']: return sorted(types, key=lambda content_type: content_type.priority) @property def type(self): return self.id def __init__(self, type): self.id = type self.icon = ContentType._CSS_ICONS[type] self.color = ContentType._CSS_COLORS[type] # deprecated self.css = ContentType._CSS_COLORS[type] self.label = ContentType._LABEL[type] self.priority = ContentType._ORDER_WEIGHT[type] def toDict(self): return dict(id=self.type, type=self.type, icon=self.icon, color=self.color, label=self.label, priority=self.priority)
class ContentStatus(object): """ Allowed status are: - open - closed-validated - closed-invalidated - closed-deprecated """ OPEN = 'open' CLOSED_VALIDATED = 'closed-validated' CLOSED_UNVALIDATED = 'closed-unvalidated' CLOSED_DEPRECATED = 'closed-deprecated' _LABELS = { 'open': l_('work in progress'), 'closed-validated': l_('closed — validated'), 'closed-unvalidated': l_('closed — cancelled'), 'closed-deprecated': l_('deprecated') } _LABELS_THREAD = { 'open': l_('subject in progress'), 'closed-validated': l_('subject closed — resolved'), 'closed-unvalidated': l_('subject closed — cancelled'), 'closed-deprecated': l_('deprecated') } _LABELS_FILE = { 'open': l_('work in progress'), 'closed-validated': l_('closed — validated'), 'closed-unvalidated': l_('closed — cancelled'), 'closed-deprecated': l_('deprecated') } _ICONS = { 'open': 'fa fa-square-o', 'closed-validated': 'fa fa-check-square-o', 'closed-unvalidated': 'fa fa-close', 'closed-deprecated': 'fa fa-warning', } _CSS = { 'open': 'tracim-status-open', 'closed-validated': 'tracim-status-closed-validated', 'closed-unvalidated': 'tracim-status-closed-unvalidated', 'closed-deprecated': 'tracim-status-closed-deprecated', } def __init__(self, id, type=''): self.id = id self.icon = ContentStatus._ICONS[id] self.css = ContentStatus._CSS[id] if type == ContentType.Thread: self.label = ContentStatus._LABELS_THREAD[id] elif type == ContentType.File: self.label = ContentStatus._LABELS_FILE[id] else: self.label = ContentStatus._LABELS[id] @classmethod def all(cls, type='') -> ['ContentStatus']: all = [] all.append(ContentStatus('open', type)) all.append(ContentStatus('closed-validated', type)) all.append(ContentStatus('closed-unvalidated', type)) all.append(ContentStatus('closed-deprecated', type)) return all @classmethod def allowed_values(cls): return ContentStatus._LABELS.keys()
def _build_email_body(self, mako_template_filepath: str, role: UserRoleInWorkspace, content: Content, actor: User) -> str: """ Build an email body and return it as a string :param mako_template_filepath: the absolute path to the mako template to be used for email body building :param role: the role related to user to whom the email must be sent. The role is required (and not the user only) in order to show in the mail why the user receive the notification :param content: the content item related to the notification :param actor: the user at the origin of the action / notification (for example the one who wrote a comment :param config: the global configuration :return: the built email body as string. In case of multipart email, this method must be called one time for text and one time for html """ logger.debug( self, 'Building email content from MAKO template {}'.format( mako_template_filepath)) template = Template(filename=mako_template_filepath) # TODO - D.A. - 2014-11-06 - move this # Import is here for circular import problem import tracim.lib.helpers as helpers dictified_item = Context( CTX.EMAIL_NOTIFICATION, self._global_config.WEBSITE_BASE_URL).toDict(content) dictified_actor = Context(CTX.DEFAULT).toDict(actor) main_title = dictified_item.label content_intro = '' content_text = '' call_to_action_text = '' action = content.get_last_action().id if ActionDescription.COMMENT == action: content_intro = l_( '<span id="content-intro-username">{}</span> added a comment:' ).format(actor.display_name) content_text = content.description call_to_action_text = l_('Answer') elif ActionDescription.CREATION == action: # Default values (if not overriden) content_text = content.description call_to_action_text = l_('View online') if ContentType.Thread == content.type: call_to_action_text = l_('Answer') content_intro = l_( '<span id="content-intro-username">{}</span> started a thread entitled:' ).format(actor.display_name) content_text = '<p id="content-body-intro">{}</p>'.format(content.label) + \ content.get_last_comment_from(actor).description elif ContentType.File == content.type: content_intro = l_( '<span id="content-intro-username">{}</span> added a file entitled:' ).format(actor.display_name) if content.description: content_text = content.description else: content_text = '<span id="content-body-only-title">{}</span>'.format( content.label) elif ContentType.Page == content.type: content_intro = l_( '<span id="content-intro-username">{}</span> added a page entitled:' ).format(actor.display_name) content_text = '<span id="content-body-only-title">{}</span>'.format( content.label) elif ActionDescription.REVISION == action: content_text = content.description call_to_action_text = l_('View online') if ContentType.File == content.type: content_intro = l_( '<span id="content-intro-username">{}</span> uploaded a new revision.' ).format(actor.display_name) content_text = '' elif ContentType.Page == content.type: content_intro = l_( '<span id="content-intro-username">{}</span> updated this page.' ).format(actor.display_name) previous_revision = content.get_previous_revision() title_diff = '' if previous_revision.label != content.label: title_diff = htmldiff(previous_revision.label, content.label) content_text = str(l_('<p id="content-body-intro">Here is an overview of the changes:</p>'))+ \ title_diff + \ htmldiff(previous_revision.description, content.description) elif ContentType.Thread == content.type: content_intro = l_( '<span id="content-intro-username">{}</span> updated the thread description.' ).format(actor.display_name) previous_revision = content.get_previous_revision() title_diff = '' if previous_revision.label != content.label: title_diff = htmldiff(previous_revision.label, content.label) content_text = str(l_('<p id="content-body-intro">Here is an overview of the changes:</p>'))+ \ title_diff + \ htmldiff(previous_revision.description, content.description) # elif ContentType.Thread == content.type: # content_intro = l_('<span id="content-intro-username">{}</span> updated this page.').format(actor.display_name) # previous_revision = content.get_previous_revision() # content_text = l_('<p id="content-body-intro">Here is an overview of the changes:</p>')+ \ # htmldiff(previous_revision.description, content.description) elif ActionDescription.EDITION == action: call_to_action_text = l_('View online') if ContentType.File == content.type: content_intro = l_( '<span id="content-intro-username">{}</span> updated the file description.' ).format(actor.display_name) content_text = '<p id="content-body-intro">{}</p>'.format(content.get_label()) + \ content.description elif ActionDescription.STATUS_UPDATE == action: call_to_action_text = l_('View online') intro_user_msg = l_('<span id="content-intro-username">{}</span> ' 'updated the following status:') content_intro = intro_user_msg.format(actor.display_name) intro_body_msg = '<p id="content-body-intro">{}: {}</p>' content_text = intro_body_msg.format( content.get_label(), content.get_status().label, ) if '' == content_intro and content_text == '': # Skip notification, but it's not normal logger.error( self, 'A notification is being sent but no content. ' 'Here are some debug informations: [content_id: {cid}]' '[action: {act}][author: {actor}]'.format( cid=content.content_id, act=action, actor=actor)) raise ValueError('Unexpected empty notification') # Import done here because cyclic import from tracim.config.app_cfg import CFG body_content = template.render( base_url=self._global_config.WEBSITE_BASE_URL, _=l_, h=helpers, user_display_name=role.user.display_name, user_role_label=role.role_as_label(), workspace_label=role.workspace.label, content_intro=content_intro, content_text=content_text, main_title=main_title, call_to_action_text=call_to_action_text, result=DictLikeClass(item=dictified_item, actor=dictified_actor), CFG=CFG.get_instance(), ) return body_content
def _build_email_body(self, mako_template_filepath: str, role: UserRoleInWorkspace, content: Content, actor: User) -> str: """ Build an email body and return it as a string :param mako_template_filepath: the absolute path to the mako template to be used for email body building :param role: the role related to user to whom the email must be sent. The role is required (and not the user only) in order to show in the mail why the user receive the notification :param content: the content item related to the notification :param actor: the user at the origin of the action / notification (for example the one who wrote a comment :param config: the global configuration :return: the built email body as string. In case of multipart email, this method must be called one time for text and one time for html """ logger.debug(self, 'Building email content from MAKO template {}'.format(mako_template_filepath)) template = Template(filename=mako_template_filepath) # TODO - D.A. - 2014-11-06 - move this # Import is here for circular import problem import tracim.lib.helpers as helpers dictified_item = Context(CTX.EMAIL_NOTIFICATION, self._global_config.WEBSITE_BASE_URL).toDict(content) dictified_actor = Context(CTX.DEFAULT).toDict(actor) main_title = dictified_item.label content_intro = '' content_text = '' call_to_action_text = '' action = content.get_last_action().id if ActionDescription.COMMENT == action: content_intro = l_('<span id="content-intro-username">{}</span> added a comment:').format(actor.display_name) content_text = content.description call_to_action_text = l_('Answer') elif ActionDescription.CREATION == action: # Default values (if not overriden) content_text = content.description call_to_action_text = l_('View online') if ContentType.Thread == content.type: call_to_action_text = l_('Answer') content_intro = l_('<span id="content-intro-username">{}</span> started a thread entitled:').format(actor.display_name) content_text = '<p id="content-body-intro">{}</p>'.format(content.label) + \ content.get_last_comment_from(actor).description elif ContentType.File == content.type: content_intro = l_('<span id="content-intro-username">{}</span> added a file entitled:').format(actor.display_name) if content.description: content_text = content.description else: content_text = '<span id="content-body-only-title">{}</span>'.format(content.label) elif ContentType.Page == content.type: content_intro = l_('<span id="content-intro-username">{}</span> added a page entitled:').format(actor.display_name) content_text = '<span id="content-body-only-title">{}</span>'.format(content.label) elif ActionDescription.REVISION == action: content_text = content.description call_to_action_text = l_('View online') if ContentType.File == content.type: content_intro = l_('<span id="content-intro-username">{}</span> uploaded a new revision.').format(actor.display_name) content_text = '' elif ContentType.Page == content.type: content_intro = l_('<span id="content-intro-username">{}</span> updated this page.').format(actor.display_name) previous_revision = content.get_previous_revision() title_diff = '' if previous_revision.label != content.label: title_diff = htmldiff(previous_revision.label, content.label) content_text = str(l_('<p id="content-body-intro">Here is an overview of the changes:</p>'))+ \ title_diff + \ htmldiff(previous_revision.description, content.description) elif ContentType.Thread == content.type: content_intro = l_('<span id="content-intro-username">{}</span> updated the thread description.').format(actor.display_name) previous_revision = content.get_previous_revision() title_diff = '' if previous_revision.label != content.label: title_diff = htmldiff(previous_revision.label, content.label) content_text = str(l_('<p id="content-body-intro">Here is an overview of the changes:</p>'))+ \ title_diff + \ htmldiff(previous_revision.description, content.description) # elif ContentType.Thread == content.type: # content_intro = l_('<span id="content-intro-username">{}</span> updated this page.').format(actor.display_name) # previous_revision = content.get_previous_revision() # content_text = l_('<p id="content-body-intro">Here is an overview of the changes:</p>')+ \ # htmldiff(previous_revision.description, content.description) elif ActionDescription.EDITION == action: call_to_action_text = l_('View online') if ContentType.File == content.type: content_intro = l_('<span id="content-intro-username">{}</span> updated the file description.').format(actor.display_name) content_text = '<p id="content-body-intro">{}</p>'.format(content.get_label()) + \ content.description elif ActionDescription.STATUS_UPDATE == action: call_to_action_text = l_('View online') intro_user_msg = l_( '<span id="content-intro-username">{}</span> ' 'updated the following status:' ) content_intro = intro_user_msg.format(actor.display_name) intro_body_msg = '<p id="content-body-intro">{}: {}</p>' content_text = intro_body_msg.format( content.get_label(), content.get_status().label, ) if '' == content_intro and content_text == '': # Skip notification, but it's not normal logger.error( self, 'A notification is being sent but no content. ' 'Here are some debug informations: [content_id: {cid}]' '[action: {act}][author: {actor}]'.format( cid=content.content_id, act=action, actor=actor ) ) raise ValueError('Unexpected empty notification') # Import done here because cyclic import from tracim.config.app_cfg import CFG body_content = template.render( base_url=self._global_config.WEBSITE_BASE_URL, _=l_, h=helpers, user_display_name=role.user.display_name, user_role_label=role.role_as_label(), workspace_label=role.workspace.label, content_intro=content_intro, content_text=content_text, main_title=main_title, call_to_action_text=call_to_action_text, result = DictLikeClass(item=dictified_item, actor=dictified_actor), CFG=CFG.get_instance(), ) return body_content
def notify_content_update(self, event_actor_id: int, event_content_id: int): """ Look for all users to be notified about the new content and send them an individual email :param event_actor_id: id of the user that has triggered the event :param event_content_id: related content_id :return: """ # FIXME - D.A. - 2014-11-05 # Dirty import. It's here in order to avoid circular import from tracim.lib.content import ContentApi user = UserApi(None).get_one(event_actor_id) logger.debug(self, 'Content: {}'.format(event_content_id)) content = ContentApi( user, show_archived=True, show_deleted=True ).get_one( event_content_id, ContentType.Any ) # TODO - use a system user instead of the user that has triggered the event main_content = content.parent if content.type == ContentType.Comment else content notifiable_roles = WorkspaceApi(user).get_notifiable_roles( content.workspace) if len(notifiable_roles) <= 0: logger.info( self, 'Skipping notification as nobody subscribed to in workspace {}' .format(content.workspace.label)) return logger.info( self, 'Sending asynchronous emails to {} user(s)'.format( len(notifiable_roles))) # INFO - D.A. - 2014-11-06 # The following email sender will send emails in the async task queue # This allow to build all mails through current thread but really send them (including SMTP connection) # In the other thread. # # This way, the webserver will return sooner (actually before notification emails are sent async_email_sender = EmailSender( self._smtp_config, self._global_config.EMAIL_NOTIFICATION_ACTIVATED) for role in notifiable_roles: logger.info(self, 'Sending email to {}'.format(role.user.email)) to_addr = formataddr((role.user.display_name, role.user.email)) # # INFO - G.M - 2017-11-15 - set content_id in header to permit reply # references can have multiple values, but only one in this case. replyto_addr = self._global_config.EMAIL_NOTIFICATION_REPLY_TO_EMAIL.replace( # nopep8 '{content_id}', str(content.content_id)) reference_addr = self._global_config.EMAIL_NOTIFICATION_REFERENCES_EMAIL.replace( #nopep8 '{content_id}', str(content.content_id)) # # INFO - D.A. - 2014-11-06 # We do not use .format() here because the subject defined in the .ini file # may not include all required labels. In order to avoid partial format() (which result in an exception) # we do use replace and force the use of .__str__() in order to process LazyString objects # subject = self._global_config.EMAIL_NOTIFICATION_CONTENT_UPDATE_SUBJECT subject = subject.replace( EST.WEBSITE_TITLE, self._global_config.WEBSITE_TITLE.__str__()) subject = subject.replace(EST.WORKSPACE_LABEL, main_content.workspace.label.__str__()) subject = subject.replace(EST.CONTENT_LABEL, main_content.label.__str__()) subject = subject.replace( EST.CONTENT_STATUS_LABEL, main_content.get_status().label.__str__()) reply_to_label = l_( '{username} & all members of {workspace}').format( username=user.display_name, workspace=main_content.workspace.label) message = MIMEMultipart('alternative') message['Subject'] = subject message['From'] = self._get_sender(user) message['To'] = to_addr message['Reply-to'] = formataddr((reply_to_label, replyto_addr)) # INFO - G.M - 2017-11-15 # References can theorically have label, but in pratice, references # contains only message_id from parents post in thread. # To link this email to a content we create a virtual parent # in reference who contain the content_id. message['References'] = formataddr(('', reference_addr)) body_text = self._build_email_body( self._global_config. EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT, role, content, user) body_html = self._build_email_body( self._global_config. EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML, role, content, user) part1 = MIMEText(body_text, 'plain', 'utf-8') part2 = MIMEText(body_html, 'html', 'utf-8') # Attach parts into message container. # According to RFC 2046, the last part of a multipart message, in this case # the HTML message, is best and preferred. message.attach(part1) message.attach(part2) self.log_notification( action='CREATED', recipient=message['To'], subject=message['Subject'], ) send_email_through(async_email_sender.send_mail, message)
) environment_loaded.register(lambda: start_daemons(daemons)) environment_loaded.register(lambda: configure_depot()) interrupt_manager = InterruptManager(os.getpid(), daemons_manager=daemons) ####### # # INFO - D.A. - 2014-10-31 # fake strings allowing to translate resetpassword tgapp. # TODO - Integrate these translations into tgapp-resetpassword # l_('New password') l_('Confirm new password') l_('Save new password') l_('Email address') l_('Send Request') l_('Password reset request') l_('Password reset request sent') l_('Invalid password reset request') l_('Password reset request timed out') l_('Invalid password reset request') l_('Password changed successfully') l_(''' We've received a request to reset the password for this account.
from tg import abort from tg import request from tg import tmpl_context from tracim.lib.utils import lazy_ugettext as l_ from tg.i18n import ugettext as _ from tg.predicates import Predicate from tracim.model.data import ContentType from tracim.lib.base import logger from tracim.lib.content import ContentApi from tracim.model.data import UserRoleInWorkspace FORBIDDEN_MSG = l_('You are not authorized to access this resource') class WorkspaceRelatedPredicate(Predicate): def __init__(self, **kwargs): super(WorkspaceRelatedPredicate, self).__init__(**kwargs) self.message = FORBIDDEN_MSG def minimal_role_level(self) -> int: """ This method must be implemented in child classes. It defines the role of the user in the given workspace :return: required level associated to the predicate """ raise NotImplementedError def evaluate(self, environ, credentials):