Example #1
0
    def get_one(self, workspace_id):
        user = tmpl_context.current_user

        current_user_content = Context(CTX.CURRENT_USER).toDict(user)
        current_user_content.roles.sort(key=lambda role: role.workspace.name)

        workspace_api = WorkspaceApi(user)
        workspace = workspace_api.get_one(workspace_id)

        dictified_current_user = Context(CTX.CURRENT_USER).toDict(user)
        dictified_folders = self.folders.get_all_fake(workspace).result
        fake_api = DictLikeClass(
            current_user=dictified_current_user,
            current_workspace_folders=dictified_folders,
            current_user_workspace_role=workspace.get_user_role(user)
        )

        fake_api.sub_items = Context(CTX.FOLDER_CONTENT_LIST).toDict(
            # TODO BS 20161209: Is the correct way to grab folders? No use API?
            workspace.get_valid_children(ContentApi.DISPLAYABLE_CONTENTS)
        )

        dictified_workspace = Context(CTX.WORKSPACE).toDict(workspace, 'workspace')
        webdav_url = CFG.get_instance().WSGIDAV_CLIENT_BASE_URL

        return DictLikeClass(
            result=dictified_workspace,
            fake_api=fake_api,
            webdav_url=webdav_url,
        )
Example #2
0
def send_email_through(
        sendmail_callable: typing.Callable[[Message], None],
        message: Message,
) -> None:
    """
    Send mail encapsulation to send it in async or sync mode.

    TODO BS 20170126: A global mail/sender management should be a good
                      thing. Actually, this method is an fast solution.
    :param sendmail_callable: A callable who get message on first parameter
    :param message: The message who have to be sent
    """
    from tracim.config.app_cfg import CFG
    cfg = CFG.get_instance()

    if cfg.EMAIL_PROCESSING_MODE == CFG.CST.SYNC:
        sendmail_callable(message)
    elif cfg.EMAIL_PROCESSING_MODE == CFG.CST.ASYNC:
        queue = get_rq_queue('mail_sender')
        queue.enqueue(sendmail_callable, message)
    else:
        raise NotImplementedError(
            'Mail sender processing mode {} is not implemented'.format(
                cfg.EMAIL_PROCESSING_MODE,
            )
        )
Example #3
0
def send_email_through(
        sendmail_callable: typing.Callable[[Message], None],
        message: Message,
) -> None:
    """
    Send mail encapsulation to send it in async or sync mode.
    TODO BS 20170126: A global mail/sender management should be a good
                      thing. Actually, this method is an fast solution.
    :param sendmail_callable: A callable who get message on first parameter
    :param message: The message who have to be sent
    """
    from tracim.config.app_cfg import CFG
    cfg = CFG.get_instance()

    if cfg.EMAIL_PROCESSING_MODE == CFG.CST.SYNC:
        sendmail_callable(message)
    elif cfg.EMAIL_PROCESSING_MODE == CFG.CST.ASYNC:
        queue = get_rq_queue('mail_sender')
        queue.enqueue(sendmail_callable, message)
    else:
        raise NotImplementedError(
            'Mail sender processing mode {} is not implemented'.format(
                cfg.EMAIL_PROCESSING_MODE,
            )
        )
Example #4
0
 def get_one(self,
             page_id: str = '-1',
             revision_id: str = None,
             size: int = 300,
             *args,
             **kwargs):
     file_id = int(tg.request.controller_state.routing_args.get('file_id'))
     page = int(page_id)
     revision_id = int(revision_id) if revision_id != 'latest' else None
     cache_path = CFG.get_instance().PREVIEW_CACHE_DIR
     preview_manager = PreviewManager(cache_path, create_folder=True)
     user = tmpl_context.current_user
     content_api = ContentApi(user, show_archived=True, show_deleted=True)
     if revision_id:
         file_path = content_api.get_one_revision_filepath(revision_id)
     else:
         file = content_api.get_one(file_id, self._item_type)
         file_path = content_api.get_one_revision_filepath(file.revision_id)
     try:
         path = preview_manager.get_jpeg_preview(file_path=file_path,
                                                 page=page,
                                                 height=size,
                                                 width=size)
         with open(path, 'rb') as large:
             result = large.read()
     except PreviewGeneratorException:
         result = None
     return result
Example #5
0
 def download_pdf_one(self,
                      page_id: str,
                      revision_id: str = None,
                      *args,
                      **kwargs):
     file_id = int(tg.request.controller_state.routing_args.get('file_id'))
     revision_id = int(revision_id) if revision_id != 'latest' else None
     page = int(page_id)
     cache_path = CFG.get_instance().PREVIEW_CACHE_DIR
     preview_manager = PreviewManager(cache_path, create_folder=True)
     user = tmpl_context.current_user
     content_api = ContentApi(user, show_archived=True, show_deleted=True)
     file = content_api.get_one(file_id, self._item_type)
     if revision_id:
         file_path = content_api.get_one_revision_filepath(revision_id)
     else:
         file = content_api.get_one(file_id, self._item_type)
         file_path = content_api.get_one_revision_filepath(file.revision_id)
     path = preview_manager.get_pdf_preview(file_path=file_path, page=page)
     file_suffix = ''
     if page > -1:
         file_suffix = '.page-{}'.format(page + 1)
     tg.response.headers['Content-Disposition'] = \
         'attachment; filename="{}{}.pdf"'.format(
             file.label,
             file_suffix,
         )
     with open(path, 'rb') as pdf:
         return pdf.read()
Example #6
0
    def get_deleted_calendar_file_path(
            self,
            calendar_class,
            related_object_id,
    ) -> str:
        from tracim.config.app_cfg import CFG
        cfg = CFG.get_instance()

        calendar_file_name = '{}.ics'.format(related_object_id)

        if calendar_class == WorkspaceCalendar:
            calendar_type_folder = 'workspace'
        elif calendar_class == UserCalendar:
            calendar_type_folder = 'user'
        else:
            raise NotImplementedError()

        deleted_calendar_file_path = os.path.join(
            cfg.RADICALE_SERVER_FILE_SYSTEM_FOLDER,
            calendar_type_folder,
            'deleted',
            calendar_file_name,
        )

        return deleted_calendar_file_path
Example #7
0
    def test_is_item_still_editable(self):
        config = CFG.get_instance()
        item = DictLikeClass()

        config.DATA_UPDATE_ALLOWED_DURATION = 0
        item.created = datetime.datetime.now() - datetime.timedelta(0, 10)

        item.type = DictLikeClass({'id': 5})
        eq_(False, h.is_item_still_editable(config, item))

        item.type.id = 'comment'
        eq_(False, h.is_item_still_editable(config, item))

        config.DATA_UPDATE_ALLOWED_DURATION = -1
        item.type.id = 'comment'
        item.created = datetime.datetime.now() - datetime.timedelta(0, 10)
        eq_(True, h.is_item_still_editable(config, item))

        config.DATA_UPDATE_ALLOWED_DURATION = 12
        item.created = datetime.datetime.now() - datetime.timedelta(0, 10)
        eq_(
            True, h.is_item_still_editable(config, item),
            'created: {}, now: {}'.format(item.created,
                                          datetime.datetime.now())
        )  # This test will pass only if the test duration is less than 120s !!!

        config.DATA_UPDATE_ALLOWED_DURATION = 8
        item.created = datetime.datetime.now() - datetime.timedelta(0, 10)
        eq_(False, h.is_item_still_editable(config, item))
Example #8
0
    def get_deleted_calendar_file_path(
        self,
        calendar_class,
        related_object_id,
    ) -> str:
        from tracim.config.app_cfg import CFG
        cfg = CFG.get_instance()

        calendar_file_name = '{}.ics'.format(related_object_id)

        if calendar_class == WorkspaceCalendar:
            calendar_type_folder = 'workspace'
        elif calendar_class == UserCalendar:
            calendar_type_folder = 'user'
        else:
            raise NotImplementedError()

        deleted_calendar_file_path = os.path.join(
            cfg.RADICALE_SERVER_FILE_SYSTEM_FOLDER,
            calendar_type_folder,
            'deleted',
            calendar_file_name,
        )

        return deleted_calendar_file_path
Example #9
0
    def index(self):
        from tracim.config.app_cfg import CFG
        cfg = CFG.get_instance()

        # TODO BS 20160720: S'assurer d'être identifié !
        user = tmpl_context.identity.get('user')
        dictified_current_user = Context(CTX.CURRENT_USER).toDict(user)

        fake_api = DictLikeClass(
            current_user=dictified_current_user,
        )
        user_base_url = CalendarManager.get_user_base_url()
        workspace_base_url = CalendarManager.get_workspace_base_url()
        workspace_calendar_urls = CalendarManager\
            .get_workspace_readable_calendars_urls_for_user(user)
        base_href_url = \
            re.sub(r"^http[s]?://", '', cfg.RADICALE_CLIENT_BASE_URL_HOST)

        # Template will use User.auth_token, ensure it's validity
        user.ensure_auth_token()

        return DictLikeClass(
            fake_api=fake_api,
            user_base_url=user_base_url,
            workspace_base_url=workspace_base_url,
            workspace_clendar_urls=workspace_calendar_urls,
            auth_token=user.auth_token,
            base_href_url=base_href_url,
        )
Example #10
0
    def test_is_item_still_editable(self):
        config = CFG.get_instance()
        item = DictLikeClass()

        config.DATA_UPDATE_ALLOWED_DURATION = 0
        item.created = datetime.datetime.now() - datetime.timedelta(0, 10)

        item.type = DictLikeClass({'id': 5})
        eq_(False, h.is_item_still_editable(config, item))

        item.type.id = 'comment'
        eq_(False, h.is_item_still_editable(config, item))

        config.DATA_UPDATE_ALLOWED_DURATION = -1
        item.type.id = 'comment'
        item.created = datetime.datetime.now() - datetime.timedelta(0, 10)
        eq_(True, h.is_item_still_editable(config, item))

        config.DATA_UPDATE_ALLOWED_DURATION = 12
        item.created = datetime.datetime.now() - datetime.timedelta(0, 10)
        eq_(True, h.is_item_still_editable(config, item), 'created: {}, now: {}'.format(item.created, datetime.datetime.now())) # This test will pass only if the test duration is less than 120s !!!

        config.DATA_UPDATE_ALLOWED_DURATION = 8
        item.created = datetime.datetime.now() - datetime.timedelta(0, 10)
        eq_(False, h.is_item_still_editable(config, item))
Example #11
0
 def download_pdf_one(self,
                      page_id: str,
                      revision_id: str=None,
                      *args, **kwargs):
     file_id = int(tg.request.controller_state.routing_args.get('file_id'))
     revision_id = int(revision_id) if revision_id != 'latest' else None
     page = int(page_id)
     cache_path = CFG.get_instance().PREVIEW_CACHE_DIR
     preview_manager = PreviewManager(cache_path, create_folder=True)
     user = tmpl_context.current_user
     content_api = ContentApi(user,
                              show_archived=True,
                              show_deleted=True)
     file = content_api.get_one(file_id, self._item_type)
     if revision_id:
         file_path = content_api.get_one_revision_filepath(revision_id)
     else:
         file = content_api.get_one(file_id, self._item_type)
         file_path = content_api.get_one_revision_filepath(file.revision_id)
     path = preview_manager.get_pdf_preview(file_path=file_path,
                                            page=page)
     file_suffix = ''
     if page > -1:
         file_suffix = '.page-{}'.format(page + 1)
     tg.response.headers['Content-Disposition'] = \
         'attachment; filename="{}{}.pdf"'.format(
             file.label,
             file_suffix,
         )
     with open(path, 'rb') as pdf:
         return pdf.read()
Example #12
0
 def get_one(self,
             page_id: str='-1',
             revision_id: str=None,
             size: int=300,
             *args, **kwargs):
     file_id = int(tg.request.controller_state.routing_args.get('file_id'))
     page = int(page_id)
     revision_id = int(revision_id) if revision_id != 'latest' else None
     cache_path = CFG.get_instance().PREVIEW_CACHE_DIR
     preview_manager = PreviewManager(cache_path, create_folder=True)
     user = tmpl_context.current_user
     content_api = ContentApi(user,
                              show_archived=True,
                              show_deleted=True)
     if revision_id:
         file_path = content_api.get_one_revision_filepath(revision_id)
     else:
         file = content_api.get_one(file_id, self._item_type)
         file_path = content_api.get_one_revision_filepath(file.revision_id)
     try:
         path = preview_manager.get_jpeg_preview(file_path=file_path,
                                                 page=page,
                                                 height=size,
                                                 width=size)
         with open(path, 'rb') as large:
             result = large.read()
     except PreviewGeneratorException:
         result = None
     return result
Example #13
0
    def create(cls, current_user: User=None) -> INotifier:
        # TODO: Find a way to import properly without cyclic import
        from tracim.config.app_cfg import CFG
        cfg = CFG.get_instance()
        if not cfg.EMAIL_NOTIFICATION_ACTIVATED:
            return DummyNotifier(current_user)

        return RealNotifier(current_user)
Example #14
0
    def test_email_notifier__build_name_with_no_user(self):
        config = CFG.get_instance()
        config.EMAIL_NOTIFICATION_FROM_DEFAULT_LABEL = 'Robot'
        config.EMAIL_NOTIFICATION_FROM_EMAIL = '*****@*****.**'

        notifier = EmailNotifier(smtp_config=None, global_config=config)
        email = notifier._get_sender()
        eq_('Robot <*****@*****.**>', email)
Example #15
0
    def create(cls, current_user: User = None) -> INotifier:
        # TODO: Find a way to import properly without cyclic import
        from tracim.config.app_cfg import CFG
        cfg = CFG.get_instance()
        if not cfg.EMAIL_NOTIFICATION_ACTIVATED:
            return DummyNotifier(current_user)

        return RealNotifier(current_user)
Example #16
0
    def test_email_notifier__build_name_with_no_user(self):
        config = CFG.get_instance()
        config.EMAIL_NOTIFICATION_FROM_DEFAULT_LABEL = 'Robot'
        config.EMAIL_NOTIFICATION_FROM_EMAIL = '*****@*****.**'

        notifier = EmailNotifier(smtp_config=None, global_config=config)
        email = notifier._get_sender()
        eq_('Robot <*****@*****.**>', email)
Example #17
0
 def _get_server(self):
     from tracim.config.app_cfg import CFG
     cfg = CFG.get_instance()
     return make_server(
         cfg.RADICALE_SERVER_HOST, cfg.RADICALE_SERVER_PORT,
         RadicaleApplication(), RadicaleHTTPSServer
         if cfg.RADICALE_SERVER_SSL else RadicaleHTTPServer,
         RadicaleRequestHandler)
Example #18
0
 def _get_server(self):
     from tracim.config.app_cfg import CFG
     cfg = CFG.get_instance()
     return make_server(
         cfg.RADICALE_SERVER_HOST,
         cfg.RADICALE_SERVER_PORT,
         RadicaleApplication(),
         RadicaleHTTPSServer if cfg.RADICALE_SERVER_SSL else RadicaleHTTPServer,
         RadicaleRequestHandler
     )
Example #19
0
    def _prepare_config(self):
        from tracim.config.app_cfg import CFG
        cfg = CFG.get_instance()

        tracim_auth = 'tracim.lib.radicale.auth'
        tracim_rights = 'tracim.lib.radicale.rights'
        tracim_storage = 'tracim.lib.radicale.storage'
        fs_path = cfg.RADICALE_SERVER_FILE_SYSTEM_FOLDER
        allow_origin = cfg.RADICALE_SERVER_ALLOW_ORIGIN
        realm_message = cfg.RADICALE_SERVER_REALM_MESSAGE

        radicale_config.set('auth', 'type', 'custom')
        radicale_config.set('auth', 'custom_handler', tracim_auth)

        radicale_config.set('rights', 'type', 'custom')
        radicale_config.set('rights', 'custom_handler', tracim_rights)

        radicale_config.set('storage', 'type', 'custom')
        radicale_config.set('storage', 'custom_handler', tracim_storage)
        radicale_config.set('storage', 'filesystem_folder', fs_path)

        radicale_config.set('server', 'realm', realm_message)
        radicale_config.set(
            'server',
            'base_prefix',
            cfg.RADICALE_CLIENT_BASE_URL_PREFIX,
        )

        try:
            radicale_config.add_section('headers')
        except DuplicateSectionError:
            pass  # It is not a problem, we just want it exist

        if allow_origin:
            radicale_config.set(
                'headers',
                'Access-Control-Allow-Origin',
                allow_origin,
            )

        # Radicale is not 100% CALDAV Compliant, we force some Allow-Methods
        radicale_config.set(
            'headers',
            'Access-Control-Allow-Methods',
            'DELETE, HEAD, GET, MKCALENDAR, MKCOL, MOVE, OPTIONS, PROPFIND, '
            'PROPPATCH, PUT, REPORT',
        )

        # Radicale is not 100% CALDAV Compliant, we force some Allow-Headers
        radicale_config.set(
            'headers',
            'Access-Control-Allow-Headers',
            'X-Requested-With,X-Auth-Token,Content-Type,Content-Length,'
            'X-Client,Authorization,depth,Prefer,If-None-Match,If-Match',
        )
Example #20
0
    def treeview_root(self, id='#',
                      current_id=None,
                      all_workspaces=True,
                      folder_allowed_content_types='',
                      ignore_id=None,
                      ignore_workspace_id=None):
        all_workspaces = bool(int(all_workspaces))

        # ignore_workspace_id is a string like 3,12,78,15
        ignored_ids = [int(id) for id in ignore_workspace_id.split(',')] if ignore_workspace_id else None

        if not current_id:
            # Default case is to return list of workspaces
            api = WorkspaceApi(tmpl_context.current_user)
            workspaces = api.get_all_for_user(tmpl_context.current_user,
                                              ignored_ids)
            dictified_workspaces = Context(CTX.MENU_API).toDict(workspaces, 'd')
            return dictified_workspaces

        allowed_content_types = ContentType.allowed_types_from_str(folder_allowed_content_types)
        ignored_item_ids = [int(ignore_id)] if ignore_id else []

        # Now complex case: we must return a structured tree
        # including the selected node, all parents (and their siblings)
        workspace, content = convert_id_into_instances(current_id)

        # The following step allow to select the parent folder when content itself is not visible in the treeview
        if content and content.type!=ContentType.Folder and CFG.CST.TREEVIEW_ALL!=CFG.get_instance().WEBSITE_TREEVIEW_CONTENT:
            content = content.parent if content.parent else None

        # This is the init of the recursive-like build of the tree
        content_parent = content
        tree_items = []

        # The first step allow to load child of selected item
        # (for example, when you select a folder in the windows explorer,
        # then the selected folder is expanded by default)
        content_api = ContentApi(tmpl_context.current_user)
        child_folders = content_api.get_child_folders(content_parent, workspace, allowed_content_types, ignored_item_ids)

        if len(child_folders)>0:
            first_child = child_folders[0]
            content_parent, tree_items = self._build_sibling_list_of_tree_items(workspace, first_child, tree_items, False, allowed_content_types, ignored_item_ids)

        content_parent, tree_items = self._build_sibling_list_of_tree_items(workspace, content_parent, tree_items, True, allowed_content_types, ignored_item_ids)
        while content_parent:
            # Do the same for the parent level
            content_parent, tree_items = self._build_sibling_list_of_tree_items(workspace, content_parent, tree_items)
        # Now, we have a tree_items list that is the root folders list,
        # so we now have to put it as a child of a list of workspaces
        should_select_workspace = not content

        full_tree = self._build_sibling_list_of_workspaces(workspace, tree_items, should_select_workspace, all_workspaces)

        return Context(CTX.MENU_API_BUILD_FROM_TREE_ITEM).toDict(full_tree, 'd')
Example #21
0
    def test_email_notifier__build_name_without_user_id(self):
        u = User()
        u.user_id = 3
        u.display_name = 'François Michâlié'

        config = CFG.get_instance()
        config.EMAIL_NOTIFICATION_FROM_EMAIL = '*****@*****.**'

        notifier = EmailNotifier(smtp_config=None, global_config=config)
        email = notifier._get_sender(user=u)
        eq_('=?utf-8?q?Fran=C3=A7ois_Mich=C3=A2li=C3=A9_via_Tracim?= <*****@*****.**>', email)  # nopep8
Example #22
0
    def test_notifier_factory_method(self):
        u = User()

        cfg = CFG.get_instance()
        cfg.EMAIL_NOTIFICATION_ACTIVATED = True
        notifier = NotifierFactory.create(u)
        eq_(RealNotifier, notifier.__class__)

        cfg.EMAIL_NOTIFICATION_ACTIVATED = False
        notifier = NotifierFactory.create(u)
        eq_(DummyNotifier, notifier.__class__)
Example #23
0
    def test_notifier_factory_method(self):
        u = User()

        cfg = CFG.get_instance()
        cfg.EMAIL_NOTIFICATION_ACTIVATED = True
        notifier = NotifierFactory.create(u)
        eq_(RealNotifier, notifier.__class__)

        cfg.EMAIL_NOTIFICATION_ACTIVATED = False
        notifier = NotifierFactory.create(u)
        eq_(DummyNotifier, notifier.__class__)
Example #24
0
    def get_one(self, workspace_id, **kwargs):
        """
        :param workspace_id: Displayed workspace id
        :param kwargs:
          * show_deleted: bool: Display deleted contents or hide them if False
          * show_archived: bool: Display archived contents or hide them
            if False
        """
        show_deleted = str_as_bool(kwargs.get('show_deleted', False))
        show_archived = str_as_bool(kwargs.get('show_archived', ''))
        user = tmpl_context.current_user

        workspace_api = WorkspaceApi(user)
        workspace = workspace_api.get_one(workspace_id)

        unread_contents = ContentApi(user).get_last_unread(None,
                                                           ContentType.Any,
                                                           workspace=workspace)
        current_user_content = Context(CTX.CURRENT_USER).toDict(user)
        current_user_content.roles.sort(key=lambda role: role.workspace.name)



        dictified_current_user = Context(CTX.CURRENT_USER).toDict(user)
        dictified_folders = self.folders.get_all_fake(workspace).result
        fake_api = DictLikeClass(
            last_unread=Context(CTX.CONTENT_LIST).toDict(unread_contents,
                                                         'contents',
                                                         'nb'),
            current_user=dictified_current_user,
            current_workspace_folders=dictified_folders,
            current_user_workspace_role=workspace.get_user_role(user)
        )

        fake_api.sub_items = Context(CTX.FOLDER_CONTENT_LIST).toDict(
            # TODO BS 20161209: Is the correct way to grab folders? No use API?
            workspace.get_valid_children(
                ContentApi.DISPLAYABLE_CONTENTS,
                show_deleted=show_deleted,
                show_archived=show_archived,
            )
        )

        dictified_workspace = Context(CTX.WORKSPACE).toDict(workspace, 'workspace')
        webdav_url = CFG.get_instance().WSGIDAV_CLIENT_BASE_URL

        return DictLikeClass(
            result=dictified_workspace,
            fake_api=fake_api,
            webdav_url=webdav_url,
            show_deleted=show_deleted,
            show_archived=show_archived,
        )
Example #25
0
    def get_base_url(cls, low_level: bool = False) -> str:
        """
        :param low_level: If True, use local ip address with radicale port.
        :return: Radical address base url.
        """
        from tracim.config.app_cfg import CFG
        cfg = CFG.get_instance()

        if not low_level:
            return cfg.RADICALE_CLIENT_BASE_URL_TEMPLATE

        return 'http://127.0.0.1:{0}'.format(cfg.RADICALE_SERVER_PORT)
Example #26
0
    def run(self) -> None:
        from tracim.config.app_cfg import CFG
        cfg = CFG.get_instance()

        with RQConnection(
                Redis(
                    host=cfg.EMAIL_SENDER_REDIS_HOST,
                    port=cfg.EMAIL_SENDER_REDIS_PORT,
                    db=cfg.EMAIL_SENDER_REDIS_DB,
                )):
            self.worker = RQWorker(['mail_sender'])
            self.worker.work()
Example #27
0
    def get_base_url(cls, low_level: bool=False) -> str:
        """
        :param low_level: If True, use local ip address with radicale port.
        :return: Radical address base url.
        """
        from tracim.config.app_cfg import CFG
        cfg = CFG.get_instance()

        if not low_level:
            return cfg.RADICALE_CLIENT_BASE_URL_TEMPLATE

        return '127.0.0.1:{0}'.format(cfg.RADICALE_SERVER_PORT)
Example #28
0
    def __init__(self, current_user: User=None):
        """
        :param current_user: the user that has triggered the notification
        :return:
        """
        logger.info(self, 'Instantiating Real Notifier')
        cfg = CFG.get_instance()

        self._user = current_user
        self._smtp_config = SmtpConfiguration(cfg.EMAIL_NOTIFICATION_SMTP_SERVER,
                                       cfg.EMAIL_NOTIFICATION_SMTP_PORT,
                                       cfg.EMAIL_NOTIFICATION_SMTP_USER,
                                       cfg.EMAIL_NOTIFICATION_SMTP_PASSWORD)
Example #29
0
File: utils.py Project: qyqx/tracim
def get_rq_queue(queue_name: str = 'default') -> Queue:
    """
    :param queue_name: name of queue
    :return: wanted queue
    """
    from tracim.config.app_cfg import CFG
    cfg = CFG.get_instance()

    return Queue(queue_name,
                 connection=Redis(
                     host=cfg.EMAIL_SENDER_REDIS_HOST,
                     port=cfg.EMAIL_SENDER_REDIS_PORT,
                     db=cfg.EMAIL_SENDER_REDIS_DB,
                 ))
Example #30
0
    def _prepare_config(self):
        from tracim.config.app_cfg import CFG
        cfg = CFG.get_instance()

        tracim_auth = 'tracim.lib.radicale.auth'
        tracim_rights = 'tracim.lib.radicale.rights'
        tracim_storage = 'tracim.lib.radicale.storage'
        fs_path = cfg.RADICALE_SERVER_FILE_SYSTEM_FOLDER
        allow_origin = cfg.RADICALE_SERVER_ALLOW_ORIGIN
        realm_message = cfg.RADICALE_SERVER_REALM_MESSAGE

        radicale_config.set('auth', 'type', 'custom')
        radicale_config.set('auth', 'custom_handler', tracim_auth)

        radicale_config.set('rights', 'type', 'custom')
        radicale_config.set('rights', 'custom_handler', tracim_rights)

        radicale_config.set('storage', 'type', 'custom')
        radicale_config.set('storage', 'custom_handler', tracim_storage)
        radicale_config.set('storage', 'filesystem_folder', fs_path)

        radicale_config.set('server', 'realm', realm_message)

        try:
            radicale_config.add_section('headers')
        except DuplicateSectionError:
            pass  # It is not a problem, we just want it exist

        if allow_origin:
            radicale_config.set(
                'headers',
                'Access-Control-Allow-Origin',
                allow_origin,
            )

        # Radicale is not 100% CALDAV Compliant, we force some Allow-Methods
        radicale_config.set(
            'headers',
            'Access-Control-Allow-Methods',
            'DELETE, HEAD, GET, MKCALENDAR, MKCOL, MOVE, OPTIONS, PROPFIND, '
            'PROPPATCH, PUT, REPORT',
        )

        # Radicale is not 100% CALDAV Compliant, we force some Allow-Headers
        radicale_config.set(
            'headers',
            'Access-Control-Allow-Headers',
            'X-Requested-With,X-Auth-Token,Content-Type,Content-Length,'
            'X-Client,Authorization,depth,Prefer,If-None-Match,If-Match',
        )
Example #31
0
    def __init__(self, current_user: User=None):
        """
        :param current_user: the user that has triggered the notification
        :return:
        """
        logger.info(self, 'Instantiating Real Notifier')
        # TODO: Find a way to import properly without cyclic import
        from tracim.config.app_cfg import CFG
        cfg = CFG.get_instance()

        self._user = current_user
        self._smtp_config = SmtpConfiguration(cfg.EMAIL_NOTIFICATION_SMTP_SERVER,
                                       cfg.EMAIL_NOTIFICATION_SMTP_PORT,
                                       cfg.EMAIL_NOTIFICATION_SMTP_USER,
                                       cfg.EMAIL_NOTIFICATION_SMTP_PASSWORD)
Example #32
0
def get_email_manager():
    """
    :return: EmailManager instance
    """
    #  TODO: Find a way to import properly without cyclic import
    from tracim.config.app_cfg import CFG

    global_config = CFG.get_instance()
    smtp_config = SmtpConfiguration(
        global_config.EMAIL_NOTIFICATION_SMTP_SERVER,
        global_config.EMAIL_NOTIFICATION_SMTP_PORT,
        global_config.EMAIL_NOTIFICATION_SMTP_USER,
        global_config.EMAIL_NOTIFICATION_SMTP_PASSWORD)

    return EmailManager(global_config=global_config, smtp_config=smtp_config)
Example #33
0
    def __init__(self, current_user: User = None):
        """
        :param current_user: the user that has triggered the notification
        :return:
        """
        logger.info(self, 'Instantiating Real Notifier')
        # TODO: Find a way to import properly without cyclic import
        from tracim.config.app_cfg import CFG
        cfg = CFG.get_instance()

        self._user = current_user
        self._smtp_config = SmtpConfiguration(
            cfg.EMAIL_NOTIFICATION_SMTP_SERVER,
            cfg.EMAIL_NOTIFICATION_SMTP_PORT, cfg.EMAIL_NOTIFICATION_SMTP_USER,
            cfg.EMAIL_NOTIFICATION_SMTP_PASSWORD)
Example #34
0
def get_email_manager():
    """
    :return: EmailManager instance
    """
    #  TODO: Find a way to import properly without cyclic import
    from tracim.config.app_cfg import CFG

    global_config = CFG.get_instance()
    smtp_config = SmtpConfiguration(
        global_config.EMAIL_NOTIFICATION_SMTP_SERVER,
        global_config.EMAIL_NOTIFICATION_SMTP_PORT,
        global_config.EMAIL_NOTIFICATION_SMTP_USER,
        global_config.EMAIL_NOTIFICATION_SMTP_PASSWORD
    )

    return EmailManager(global_config=global_config, smtp_config=smtp_config)
Example #35
0
    def _render(self, mako_template_filepath: str, context: dict):
        """
        Render mako template with all needed current variables
        :param mako_template_filepath: file path of mako template
        :param context: dict with template context
        :return: template rendered string
        """
        # TODO - D.A. - 2014-11-06 - move this
        # Import is here for circular import problem
        import tracim.lib.helpers as helpers
        from tracim.config.app_cfg import CFG

        template = Template(filename=mako_template_filepath)
        return template.render(base_url=self._global_config.WEBSITE_BASE_URL,
                               _=_,
                               h=helpers,
                               CFG=CFG.get_instance(),
                               **context)
Example #36
0
 def run(self) -> None:
     from tracim.config.app_cfg import CFG
     cfg = CFG.get_instance()
     self._fetcher = MailFetcher(
         host=cfg.EMAIL_REPLY_IMAP_SERVER,
         port=cfg.EMAIL_REPLY_IMAP_PORT,
         user=cfg.EMAIL_REPLY_IMAP_USER,
         password=cfg.EMAIL_REPLY_IMAP_PASSWORD,
         use_ssl=cfg.EMAIL_REPLY_IMAP_USE_SSL,
         folder=cfg.EMAIL_REPLY_IMAP_FOLDER,
         delay=cfg.EMAIL_REPLY_CHECK_HEARTBEAT,
         # FIXME - G.M - 2017-11-15 - proper tracim url formatting
         endpoint=cfg.WEBSITE_BASE_URL + "/events",
         token=cfg.EMAIL_REPLY_TOKEN,
         use_html_parsing=cfg.EMAIL_REPLY_USE_HTML_PARSING,
         use_txt_parsing=cfg.EMAIL_REPLY_USE_TXT_PARSING,
         lockfile_path=cfg.EMAIL_REPLY_LOCKFILE_PATH,
     )
     self._fetcher.run()
Example #37
0
    def _render(self, mako_template_filepath: str, context: dict):
        """
        Render mako template with all needed current variables
        :param mako_template_filepath: file path of mako template
        :param context: dict with template context
        :return: template rendered string
        """
        # TODO - D.A. - 2014-11-06 - move this
        # Import is here for circular import problem
        import tracim.lib.helpers as helpers
        from tracim.config.app_cfg import CFG

        template = Template(filename=mako_template_filepath)
        return template.render(
            base_url=self._global_config.WEBSITE_BASE_URL,
            _=_,
            h=helpers,
            CFG=CFG.get_instance(),
            **context
        )
Example #38
0
 def log_notification(
     action: str,
     recipient: typing.Optional[str],
     subject: typing.Optional[str],
 ) -> None:
     """Log notification metadata."""
     from tracim.config.app_cfg import CFG
     log_path = CFG.get_instance().EMAIL_NOTIFICATION_LOG_FILE_PATH
     if log_path:
         # TODO - A.P - 2017-09-06 - file logging inefficiency
         # Updating a document with 100 users to notify will leads to open
         # and close the file 100 times.
         with open(log_path, 'a') as log_file:
             print(
                 datetime.datetime.now(),
                 action,
                 recipient,
                 subject,
                 sep='|',
                 file=log_file,
             )
Example #39
0
def is_item_still_editable(item):
    if item.type.id != "comment":
        return False

    # HACK - D.A - 2014-12-24 - item contains a datetime object!!!
    # 'item' is a variable which is created by serialization and it should be an instance of DictLikeClass.
    # therefore, it contains strins, integers and booleans (something json-ready or almost json-ready)
    #
    # BUT, the property 'created' is still a datetime object
    #
    edit_duration = CFG.get_instance().DATA_UPDATE_ALLOWED_DURATION
    if edit_duration < 0:
        return True
    elif edit_duration == 0:
        return False
    else:
        time_limit = item.created + datetime.timedelta(0, edit_duration)
        logger.warning(is_item_still_editable, "limit is: {}".format(time_limit))
        if datetime.datetime.now() < time_limit:
            return True
    return False
Example #40
0
 def log_notification(
         action: str,
         recipient: typing.Optional[str],
         subject: typing.Optional[str],
 ) -> None:
     """Log notification metadata."""
     from tracim.config.app_cfg import CFG
     log_path = CFG.get_instance().EMAIL_NOTIFICATION_LOG_FILE_PATH
     if log_path:
         # TODO - A.P - 2017-09-06 - file logging inefficiency
         # Updating a document with 100 users to notify will leads to open
         # and close the file 100 times.
         with open(log_path, 'a') as log_file:
             print(
                 datetime.datetime.now(),
                 action,
                 recipient,
                 subject,
                 sep='|',
                 file=log_file,
             )
Example #41
0
    def ensure_auth_token(self) -> None:
        """
        Create auth_token if None, regenerate auth_token if too much old.
        auth_token validity is set in
        :return:
        """
        from tracim.config.app_cfg import CFG
        validity_seconds = CFG.get_instance().USER_AUTH_TOKEN_VALIDITY

        if not self.auth_token or not self.auth_token_created:
            self.auth_token = uuid.uuid4()
            self.auth_token_created = datetime.utcnow()
            DBSession.flush()
            return

        now_seconds = time.mktime(datetime.utcnow().timetuple())
        auth_token_seconds = time.mktime(self.auth_token_created.timetuple())
        difference = now_seconds - auth_token_seconds

        if difference > validity_seconds:
            self.auth_token = uuid.uuid4()
            self.auth_token_created = datetime.utcnow()
            DBSession.flush()
Example #42
0
    def ensure_auth_token(self) -> None:
        """
        Create auth_token if None, regenerate auth_token if too much old.
        auth_token validity is set in
        :return:
        """
        from tracim.config.app_cfg import CFG
        validity_seconds = CFG.get_instance().USER_AUTH_TOKEN_VALIDITY

        if not self.auth_token or not self.auth_token_created:
            self.auth_token = str(uuid.uuid4())
            self.auth_token_created = datetime.utcnow()
            DBSession.flush()
            return

        now_seconds = time.mktime(datetime.utcnow().timetuple())
        auth_token_seconds = time.mktime(self.auth_token_created.timetuple())
        difference = now_seconds - auth_token_seconds

        if difference > validity_seconds:
            self.auth_token = uuid.uuid4()
            self.auth_token_created = datetime.utcnow()
            DBSession.flush()
Example #43
0
    def get_one(self, file_id, revision_id=None):
        file_id = int(file_id)
        cache_path = CFG.get_instance().PREVIEW_CACHE
        preview_manager = PreviewManager(cache_path, create_folder=True)
        user = tmpl_context.current_user
        workspace = tmpl_context.workspace
        current_user_content = Context(CTX.CURRENT_USER,
                                       current_user=user).toDict(user)
        current_user_content.roles.sort(key=lambda role: role.workspace.name)
        content_api = ContentApi(user, show_archived=True, show_deleted=True)
        if revision_id:
            file = content_api.get_one_from_revision(file_id, self._item_type,
                                                     workspace, revision_id)
        else:
            file = content_api.get_one(file_id, self._item_type, workspace)
            revision_id = file.revision_id

        file_path = content_api.get_one_revision_filepath(revision_id)
        nb_page = preview_manager.get_nb_page(file_path=file_path)
        preview_urls = []
        for page in range(int(nb_page)):
            url_str = '/previews/{}/pages/{}?revision_id={}'
            url = url_str.format(file_id, page, revision_id)
            preview_urls.append(url)

        fake_api_breadcrumb = self.get_breadcrumb(file_id)
        fake_api_content = DictLikeClass(breadcrumb=fake_api_breadcrumb,
                                         current_user=current_user_content)
        fake_api = Context(CTX.FOLDER,
                           current_user=user).toDict(fake_api_content)
        dictified_file = Context(self._get_one_context,
                                 current_user=user).toDict(file, 'file')
        result = DictLikeClass(result=dictified_file,
                               fake_api=fake_api,
                               nb_page=nb_page,
                               url=preview_urls)
        return result
Example #44
0
 def test_unit__log_notification(self):
     """Check file and format of notification log."""
     log_path = CFG.get_instance().EMAIL_NOTIFICATION_LOG_FILE_PATH
     pattern = '\|{act}\|{rec}\|{subj}$\\n'
     line_1_act = 'CREATED'
     line_1_rec = 'user 1 <*****@*****.**>'
     line_1_subj = 'notification 1'
     line_1_pattern = pattern.format(
         act=line_1_act,
         rec=line_1_rec,
         subj=line_1_subj,
     )
     line_2_act = '   SENT'
     line_2_rec = 'user 2 <*****@*****.**>'
     line_2_subj = 'notification 2'
     line_2_pattern = pattern.format(
         act=line_2_act,
         rec=line_2_rec,
         subj=line_2_subj,
     )
     EmailNotifier.log_notification(
         action=line_1_act,
         recipient=line_1_rec,
         subject=line_1_subj,
     )
     EmailNotifier.log_notification(
         action=line_2_act,
         recipient=line_2_rec,
         subject=line_2_subj,
     )
     with open(log_path, 'rt') as log_file:
         line_1 = log_file.readline()
         line_2 = log_file.readline()
     os.remove(path=log_path)
     ok_(re.search(pattern=line_1_pattern, string=line_1))
     ok_(re.search(pattern=line_2_pattern, string=line_2))
Example #45
0
    # BUT, the property 'created' is still a datetime object
    #
    edit_duration = CFG.get_instance().DATA_UPDATE_ALLOWED_DURATION
    if edit_duration < 0:
        return True
    elif edit_duration == 0:
        return False
    else:
        time_limit = item.created + datetime.timedelta(0, edit_duration)
        logger.warning(is_item_still_editable, "limit is: {}".format(time_limit))
        if datetime.datetime.now() < time_limit:
            return True
    return False


def shorten(text: str, maxlength: int, add_three_points=True) -> str:

    result = text
    if len(text) > maxlength:
        result = text[:maxlength]

        if add_three_points:
            result += "…"

    return result


from tracim.config.app_cfg import CFG as CFG_ORI

CFG = CFG_ORI.get_instance()  # local CFG var is an instance of CFG class found in app_cfg
Example #46
0
    def notify_content_update(self, content: Content):
        # TODO: Find a way to import properly without cyclic import
        from tracim.config.app_cfg import CFG
        global_config = CFG.get_instance()

        if content.get_last_action().id not \
                in global_config.EMAIL_NOTIFICATION_NOTIFIED_EVENTS:
            logger.info(
                self,
                'Skip email notification for update of content {}'
                'by user {} (the action is {})'.format(
                    content.content_id,
                    # below: 0 means "no user"
                    self._user.user_id if self._user else 0,
                    content.get_last_action().id))
            return

        logger.info(
            self,
            'About to email-notify update'
            'of content {} by user {}'.format(
                content.content_id,
                # Below: 0 means "no user"
                self._user.user_id if self._user else 0))

        if content.type not \
                in global_config.EMAIL_NOTIFICATION_NOTIFIED_CONTENTS:
            logger.info(
                self,
                'Skip email notification for update of content {}'
                'by user {} (the content type is {})'.format(
                    content.type,
                    # below: 0 means "no user"
                    self._user.user_id if self._user else 0,
                    content.get_last_action().id))
            return

        logger.info(
            self,
            'About to email-notify update'
            'of content {} by user {}'.format(
                content.content_id,
                # Below: 0 means "no user"
                self._user.user_id if self._user else 0))

        ####
        #
        # INFO - D.A. - 2014-11-05 - Emails are sent through asynchronous jobs.
        # For that reason, we do not give SQLAlchemy objects but ids only
        # (SQLA objects are related to a given thread/session)
        #
        try:
            if global_config.EMAIL_NOTIFICATION_PROCESSING_MODE.lower(
            ) == global_config.CST.ASYNC.lower():
                logger.info(self, 'Sending email in ASYNC mode')
                # TODO - D.A - 2014-11-06
                # This feature must be implemented in order to be able to scale to large communities
                raise NotImplementedError(
                    'Sending emails through ASYNC mode is not working yet')
            else:
                logger.info(self, 'Sending email in SYNC mode')
                EmailNotifier(self._smtp_config,
                              global_config).notify_content_update(
                                  self._user.user_id, content.content_id)
        except Exception as e:
            logger.error(
                self, 'Exception catched during email notification: {}'.format(
                    e.__str__()))
Example #47
0
def tracker_js():
    return CFG.get_instance().TRACKER_JS_CONTENT
Example #48
0
    def get_one(self, file_id, revision_id=None):
        file_id = int(file_id)
        cache_path = CFG.get_instance().PREVIEW_CACHE_DIR
        preview_manager = PreviewManager(cache_path, create_folder=True)
        user = tmpl_context.current_user
        workspace = tmpl_context.workspace
        current_user_content = Context(CTX.CURRENT_USER,
                                       current_user=user).toDict(user)
        current_user_content.roles.sort(key=lambda role: role.workspace.name)
        content_api = ContentApi(user, show_archived=True, show_deleted=True)
        if revision_id:
            file = content_api.get_one_from_revision(file_id, self._item_type,
                                                     workspace, revision_id)
        else:
            file = content_api.get_one(file_id, self._item_type, workspace)
            revision_id = file.revision_id

        file_path = content_api.get_one_revision_filepath(revision_id)

        nb_page = 0
        enable_pdf_buttons = False  # type: bool
        preview_urls = []

        try:
            nb_page = preview_manager.get_page_nb(file_path=file_path)
            for page in range(int(nb_page)):
                url_str = '/previews/{}/pages/{}?revision_id={}'
                url = url_str.format(file_id, page, revision_id)
                preview_urls.append(url)

            enable_pdf_buttons = \
                preview_manager.has_pdf_preview(file_path=file_path)

        except PreviewGeneratorException as e:
            # INFO - A.P - Silently intercepts preview exception
            # As preview generation isn't mandatory, just register it
            logger.debug(self,
                         'Preview Generator Exception: {}'.format(e.__str__))
        except Exception as e:
            # INFO - D.A - 2017-08-11 - Make Tracim robust to pg exceptions
            # Preview generator may potentially raise any type of exception
            # so we prevent user interface crashes by catching all exceptions
            logger.error(
                self,
                'Preview Generator Generic Exception: {}'.format(e.__str__))

        pdf_available = 'true' if enable_pdf_buttons else 'false'  # type: str

        fake_api_breadcrumb = self.get_breadcrumb(file_id)
        fake_api_content = DictLikeClass(breadcrumb=fake_api_breadcrumb,
                                         current_user=current_user_content)
        fake_api = Context(CTX.FOLDER, current_user=user)\
            .toDict(fake_api_content)

        dictified_file = Context(self._get_one_context,
                                 current_user=user).toDict(file, 'file')
        result = DictLikeClass(result=dictified_file,
                               fake_api=fake_api,
                               nb_page=nb_page,
                               url=preview_urls,
                               pdf_available=pdf_available)
        return result
Example #49
0
 def get_workspace_base_url(cls):
     from tracim.config.app_cfg import CFG
     cfg = CFG.get_instance()
     return os.path.join(cfg.RADICALE_CLIENT_BASE_URL_TEMPLATE,
                         'workspace/')
Example #50
0
def show_email_stuff():
    """
    this function is used in order to show/hide link for sending password reset through email
    """
    return CFG.get_instance().EMAIL_NOTIFICATION_ACTIVATED
Example #51
0
    def create(cls, current_user: User=None) -> INotifier:
        cfg = CFG.get_instance()
        if not cfg.EMAIL_NOTIFICATION_ACTIVATED:
            return DummyNotifier(current_user)

        return RealNotifier(current_user)
Example #52
0
    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
Example #53
0
    def notify_content_update(self, content: Content):
        # TODO: Find a way to import properly without cyclic import
        from tracim.config.app_cfg import CFG
        global_config = CFG.get_instance()

        if content.get_last_action().id not \
                in global_config.EMAIL_NOTIFICATION_NOTIFIED_EVENTS:
            logger.info(
                self,
                'Skip email notification for update of content {}'
                'by user {} (the action is {})'.format(
                    content.content_id,
                    # below: 0 means "no user"
                    self._user.user_id if self._user else 0,
                    content.get_last_action().id
                )
            )
            return

        logger.info(self,
                    'About to email-notify update'
                    'of content {} by user {}'.format(
                        content.content_id,
                        # Below: 0 means "no user"
                        self._user.user_id if self._user else 0
                    )
        )

        if content.type not \
                in global_config.EMAIL_NOTIFICATION_NOTIFIED_CONTENTS:
            logger.info(
                self,
                'Skip email notification for update of content {}'
                'by user {} (the content type is {})'.format(
                    content.type,
                    # below: 0 means "no user"
                    self._user.user_id if self._user else 0,
                    content.get_last_action().id
                )
            )
            return

        logger.info(self,
                    'About to email-notify update'
                    'of content {} by user {}'.format(
                        content.content_id,
                        # Below: 0 means "no user"
                        self._user.user_id if self._user else 0
                    )
        )

        ####
        #
        # INFO - D.A. - 2014-11-05 - Emails are sent through asynchronous jobs.
        # For that reason, we do not give SQLAlchemy objects but ids only
        # (SQLA objects are related to a given thread/session)
        #
        try:
            if global_config.EMAIL_NOTIFICATION_PROCESSING_MODE.lower()==global_config.CST.ASYNC.lower():
                logger.info(self, 'Sending email in ASYNC mode')
                # TODO - D.A - 2014-11-06
                # This feature must be implemented in order to be able to scale to large communities
                raise NotImplementedError('Sending emails through ASYNC mode is not working yet')
                asyncjob_perform(EmailNotifier(self._smtp_config, global_config).notify_content_update, self._user.user_id, content.content_id)
            else:
                logger.info(self, 'Sending email in SYNC mode')
                EmailNotifier(self._smtp_config, global_config).notify_content_update(self._user.user_id, content.content_id)
        except Exception as e:
            logger.error(self, 'Exception catched during email notification: {}'.format(e.__str__()))
Example #54
0
 def _get_treeviewable_content_types_or_none(self):
     if CFG.get_instance().WEBSITE_TREEVIEW_CONTENT == CFG.CST.TREEVIEW_ALL:
         return ContentType.Any
     return None  # None means "workspaces and folders"
Example #55
0
    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 = _('<span id="content-intro-username">{}</span> added a comment:').format(actor.display_name)
            content_text = content.description
            call_to_action_text = _('Answer')

        elif ActionDescription.CREATION == action:

            # Default values (if not overriden)
            content_text = content.description
            call_to_action_text = _('View online')

            if ContentType.Thread == content.type:
                call_to_action_text = _('Answer')
                content_intro = _('<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 = _('<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 = _('<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 = _('View online')

            if ContentType.File == content.type:
                content_intro = _('<span id="content-intro-username">{}</span> uploaded a new revision.').format(actor.display_name)
                content_text = ''

            elif ContentType.Page == content.type:
                content_intro = _('<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 = _('<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 = _('<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 = _('<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 = _('<span id="content-intro-username">{}</span> updated this page.').format(actor.display_name)
            #     previous_revision = content.get_previous_revision()
            #     content_text = _('<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 = _('View online')

            if ContentType.File == content.type:
                content_intro = _('<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


        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,
            _=_,
            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