class TIMRestControllerWithBreadcrumb(TIMRestController): def get_breadcrumb(self, item_id=None) -> [BreadcrumbItem]: """ TODO - Remove this and factorize it with other get_breadcrumb_xxx methods :param item_id: an item id (item may be normal content or folder :return: """ return ContentApi( tmpl_context.current_user, show_archived=True, show_deleted=True, ).build_breadcrumb(tmpl_context.workspace, item_id) def _struct_new_serialized(self, workspace_id, parent_id): print('values are: ', workspace_id, parent_id) result = DictLikeClass( item=DictLikeClass(parent=DictLikeClass(id=parent_id), workspace=DictLikeClass(id=workspace_id))) return result @tg.require(current_user_is_contributor()) @tg.expose() def new(self, parent_id=None, workspace_id=None): """ Show the add form """ tg.override_template(self.new, self.TEMPLATE_NEW) workspace_id = tg.request.GET['workspace_id'] parent_id = tg.request.GET['parent_id'] if 'parent_id' in tg.request.GET else None return DictLikeClass(result=self._struct_new_serialized(workspace_id, parent_id))
class UserWorkspaceFolderThreadRestController(TIMWorkspaceContentRestController ): """ manage a path like this: /workspaces/1/folders/XXX/pages/4 """ TEMPLATE_NEW = 'mako:tracim.templates.thread.new' TEMPLATE_EDIT = 'mako:tracim.templates.thread.edit' comments = UserWorkspaceFolderThreadCommentRestController() def _before(self, *args, **kw): TIMRestPathContextSetup.current_user() TIMRestPathContextSetup.current_workspace() TIMRestPathContextSetup.current_folder() @property def _std_url(self): return tg.url('/workspaces/{}/folders/{}/threads/{}') @property def _err_url(self): return self._std_url @property def _parent_url(self): return tg.url('/workspaces/{}/folders/{}') @property def _item_type(self): return ContentType.Thread @property def _item_type_label(self): return _('Thread') @property def _get_one_context(self) -> str: return CTX.THREAD @property def _get_all_context(self) -> str: return CTX.THREADS @tg.require(current_user_is_contributor()) @tg.expose() def post(self, label='', content='', parent_id=None): """ Creates a new thread. Actually, on POST, the content will be included in a user comment instead of being the thread description :param label: :param content: :return: """ # TODO - SECURE THIS workspace = tmpl_context.workspace api = ContentApi(tmpl_context.current_user) with DBSession.no_autoflush: thread = api.create(ContentType.Thread, workspace, tmpl_context.folder, label) # FIXME - DO NOT DUPLCIATE FIRST MESSAGE # thread.description = content api.save(thread, ActionDescription.CREATION, do_notify=False) comment = api.create(ContentType.Comment, workspace, thread, label) comment.label = '' comment.description = content if not self._path_validation.validate_new_content(thread): return render_invalid_integrity_chosen_path( thread.get_label(), ) api.save(comment, ActionDescription.COMMENT, do_notify=False) api.do_notify(thread) tg.flash(_('Thread created'), CST.STATUS_OK) tg.redirect( self._std_url.format(tmpl_context.workspace_id, tmpl_context.folder_id, thread.content_id)) @tg.require(current_user_is_reader()) @tg.expose('tracim.templates.thread.getone') def get_one(self, thread_id, **kwargs): """ :param thread_id: content_id of Thread :param inverted: fill with True equivalent to invert order of comments NOTE: This parameter is in kwargs because prevent URL changes. """ inverted = kwargs.get('inverted') thread_id = int(thread_id) user = tmpl_context.current_user workspace = tmpl_context.workspace current_user_content = Context(CTX.CURRENT_USER).toDict(user) current_user_content.roles.sort(key=lambda role: role.workspace.name) content_api = ContentApi( user, show_deleted=True, show_archived=True, ) thread = content_api.get_one(thread_id, ContentType.Thread, workspace) fake_api_breadcrumb = self.get_breadcrumb(thread_id) fake_api_content = DictLikeClass(breadcrumb=fake_api_breadcrumb, current_user=current_user_content) fake_api = Context(CTX.FOLDER).toDict(fake_api_content) dictified_thread = Context(CTX.THREAD).toDict(thread, 'thread') if inverted: dictified_thread.thread.history = \ reversed(dictified_thread.thread.history) return DictLikeClass( result=dictified_thread, fake_api=fake_api, inverted=inverted, )
class UserWorkspaceFolderThreadCommentRestController(TIMRestController): @property def _item_type(self): return ContentType.Comment @property def _item_type_label(self): return _('Comment') def _before(self, *args, **kw): TIMRestPathContextSetup.current_user() TIMRestPathContextSetup.current_workspace() TIMRestPathContextSetup.current_folder() TIMRestPathContextSetup.current_thread() @tg.expose() @tg.require(current_user_is_contributor()) def post(self, content: str = ''): # TODO - SECURE THIS workspace = tmpl_context.workspace thread = tmpl_context.thread api = ContentApi(tmpl_context.current_user) comment = api.create_comment(workspace, thread, content, True) next_str = '/workspaces/{}/folders/{}/threads/{}' next_url = tg.url(next_str).format(tmpl_context.workspace_id, tmpl_context.folder_id, tmpl_context.thread_id) tg.flash(_('Comment added'), CST.STATUS_OK) tg.redirect(next_url) @tg.expose() @tg.require(not_anonymous()) def put_delete(self, item_id): require_current_user_is_owner(int(item_id)) # TODO - CHECK RIGHTS item_id = int(item_id) content_api = ContentApi(tmpl_context.current_user) item = content_api.get_one(item_id, self._item_type, tmpl_context.workspace) next_or_back = '/workspaces/{}/folders/{}/threads/{}' try: next_url = tg.url(next_or_back).format(tmpl_context.workspace_id, tmpl_context.folder_id, tmpl_context.thread_id) undo_str = '{}/comments/{}/put_delete_undo' undo_url = tg.url(undo_str).format(next_url, item_id) msg_str = ('{} deleted. ' '<a class="alert-link" href="{}">Cancel action</a>') msg = _(msg_str).format(self._item_type_label, undo_url) with new_revision(item): content_api.delete(item) content_api.save(item, ActionDescription.DELETION) tg.flash(msg, CST.STATUS_OK, no_escape=True) tg.redirect(next_url) except ValueError as e: back_url = tg.url(next_or_back).format(tmpl_context.workspace_id, tmpl_context.folder_id, tmpl_context.thread_id) msg = _('{} not deleted: {}').format(self._item_type_label, str(e)) tg.flash(msg, CST.STATUS_ERROR) tg.redirect(back_url) @tg.expose() @tg.require(not_anonymous()) def put_delete_undo(self, item_id): require_current_user_is_owner(int(item_id)) item_id = int(item_id) # Here we do not filter deleted items content_api = ContentApi(tmpl_context.current_user, True, True) item = content_api.get_one(item_id, self._item_type, tmpl_context.workspace) next_or_back = '/workspaces/{}/folders/{}/threads/{}' try: next_url = tg.url(next_or_back).format(tmpl_context.workspace_id, tmpl_context.folder_id, tmpl_context.thread_id) msg = _('{} undeleted.').format(self._item_type_label) with new_revision(item): content_api.undelete(item) content_api.save(item, ActionDescription.UNDELETION) tg.flash(msg, CST.STATUS_OK) tg.redirect(next_url) except ValueError as e: logger.debug(self, 'Exception: {}'.format(e.__str__)) back_url = tg.url(next_or_back).format(tmpl_context.workspace_id, tmpl_context.folder_id, tmpl_context.thread_id) msg = _('{} not un-deleted: {}').format(self._item_type_label, str(e)) tg.flash(msg, CST.STATUS_ERROR) tg.redirect(back_url)
class UserWorkspaceFolderPageRestController(TIMWorkspaceContentRestController): """ manage a path like this: /workspaces/1/folders/XXX/pages/4 """ TEMPLATE_NEW = 'mako:tracim.templates.page.new' TEMPLATE_EDIT = 'mako:tracim.templates.page.edit' @property def _std_url(self): return tg.url('/workspaces/{}/folders/{}/pages/{}') @property def _err_url(self): return tg.url('/workspaces/{}/folders/{}/pages/{}') @property def _parent_url(self): return tg.url('/workspaces/{}/folders/{}') @property def _item_type(self): return ContentType.Page @property def _item_type_label(self): return _('Page') @property def _get_one_context(self) -> str: return CTX.PAGE @property def _get_all_context(self) -> str: return CTX.PAGES @tg.require(current_user_is_reader()) @tg.expose('tracim.templates.page.getone') def get_one(self, page_id, revision_id=None): page_id = int(page_id) user = tmpl_context.current_user workspace = tmpl_context.workspace current_user_content = Context(CTX.CURRENT_USER).toDict(user) current_user_content.roles.sort(key=lambda role: role.workspace.name) content_api = ContentApi( user, show_deleted=True, show_archived=True, ) if revision_id: page = content_api.get_one_from_revision(page_id, ContentType.Page, workspace, revision_id) else: page = content_api.get_one(page_id, ContentType.Page, workspace) fake_api_breadcrumb = self.get_breadcrumb(page_id) fake_api_content = DictLikeClass(breadcrumb=fake_api_breadcrumb, current_user=current_user_content) fake_api = Context(CTX.FOLDER).toDict(fake_api_content) dictified_page = Context(CTX.PAGE).toDict(page, 'page') return DictLikeClass(result=dictified_page, fake_api=fake_api) def get_all_fake(self, context_workspace: Workspace, context_folder: Content): """ fake methods are used in other controllers in order to simulate a client/server api. the "client" controller method will include the result into its own fake_api object which will be available in the templates :param context_workspace: the workspace which would be taken from tmpl_context if we were in the normal behavior :return: """ workspace = context_workspace content_api = ContentApi(tmpl_context.current_user) pages = content_api.get_all(context_folder.content_id, ContentType.Page, workspace) dictified_pages = Context(CTX.PAGES).toDict(pages) return DictLikeClass(result=dictified_pages) @tg.require(current_user_is_contributor()) @tg.expose() def post(self, label='', content=''): workspace = tmpl_context.workspace api = ContentApi(tmpl_context.current_user) with DBSession.no_autoflush: page = api.create(ContentType.Page, workspace, tmpl_context.folder, label) page.description = content if not self._path_validation.validate_new_content(page): return render_invalid_integrity_chosen_path(page.get_label(), ) api.save(page, ActionDescription.CREATION, do_notify=True) tg.flash(_('Page created'), CST.STATUS_OK) redirect = '/workspaces/{}/folders/{}/pages/{}' tg.redirect( tg.url(redirect).format(tmpl_context.workspace_id, tmpl_context.folder_id, page.content_id)) @tg.require(current_user_is_contributor()) @tg.expose() def put(self, item_id, label='', content=''): # INFO - D.A. This method is a raw copy of # TODO - SECURE THIS workspace = tmpl_context.workspace try: api = ContentApi(tmpl_context.current_user) item = api.get_one(int(item_id), self._item_type, workspace) with new_revision(item): api.update_content(item, label, content) if not self._path_validation.validate_new_content(item): return render_invalid_integrity_chosen_path( item.get_label(), ) api.save(item, ActionDescription.REVISION) msg = _('{} updated').format(self._item_type_label) tg.flash(msg, CST.STATUS_OK) tg.redirect( self._std_url.format(tmpl_context.workspace_id, tmpl_context.folder_id, item.content_id)) except SameValueError as e: not_updated = '{} not updated: the content did not change' msg = _(not_updated).format(self._item_type_label) tg.flash(msg, CST.STATUS_WARNING) tg.redirect( self._err_url.format(tmpl_context.workspace_id, tmpl_context.folder_id, item_id)) except ValueError as e: not_updated = '{} not updated - error: {}' msg = _(not_updated).format(self._item_type_label, str(e)) tg.flash(msg, CST.STATUS_ERROR) tg.redirect( self._err_url.format(tmpl_context.workspace_id, tmpl_context.folder_id, item_id))
class UserWorkspaceFolderFileRestController(TIMWorkspaceContentRestController): """ manage a path like this: /workspaces/1/folders/XXX/files/4 """ TEMPLATE_NEW = 'mako:tracim.templates.file.new' TEMPLATE_EDIT = 'mako:tracim.templates.file.edit' @property def _std_url(self): return tg.url('/workspaces/{}/folders/{}/files/{}') @property def _parent_url(self): return tg.url('/workspaces/{}/folders/{}') @property def _err_url(self): return tg.url('/dashboard/workspaces/{}/folders/{}/file/{}') @property def _item_type(self): return ContentType.File @property def _item_type_label(self): return _('File') @property def _get_one_context(self) -> str: return CTX.FILE @property def _get_all_context(self) -> str: return CTX.FILES @tg.require(current_user_is_reader()) @tg.expose('tracim.templates.file.getone') 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 @tg.require(current_user_is_reader()) @tg.expose() def download(self, file_id, revision_id=None): file_id = int(file_id) revision_id = int(revision_id) if revision_id != 'latest' else None user = tmpl_context.current_user workspace = tmpl_context.workspace content_api = ContentApi(user) revision_to_send = None if revision_id: item = content_api.get_one_from_revision(file_id, self._item_type, workspace, revision_id) else: item = content_api.get_one(file_id, self._item_type, workspace) revision_to_send = None if item.revision_to_serialize <= 0: for revision in item.revisions: if not revision_to_send: revision_to_send = revision if revision.revision_id > revision_to_send.revision_id: revision_to_send = revision else: for revision in item.revisions: if revision.revision_id == item.revision_to_serialize: revision_to_send = revision break content_type = 'application/x-download' if revision_to_send.file_mimetype: content_type = str(revision_to_send.file_mimetype) tg.response.headers['Content-type'] = \ str(revision_to_send.file_mimetype) tg.response.headers['Content-Type'] = content_type file_name = get_valid_header_file_name(revision_to_send.file_name) tg.response.headers['Content-Disposition'] = \ str('attachment; filename="{}"'.format(file_name)) return DepotManager.get().get(revision_to_send.depot_file) def get_all_fake(self, context_workspace: Workspace, context_folder: Content): """ fake methods are used in other controllers in order to simulate a client/server api. the "client" controller method will include the result into its own fake_api object which will be available in the templates :param context_workspace: the workspace which would be taken from tmpl_context if we were in the normal behavior :return: """ workspace = context_workspace content_api = ContentApi(tmpl_context.current_user) files = content_api.get_all(context_folder.content_id, ContentType.File, workspace) dictified_files = Context(CTX.FILES).toDict(files) return DictLikeClass(result=dictified_files) @tg.require(current_user_is_contributor()) @tg.expose() def post(self, label='', file_data=None): # TODO - SECURE THIS workspace = tmpl_context.workspace folder = tmpl_context.folder api = ContentApi(tmpl_context.current_user) with DBSession.no_autoflush: file = api.create(ContentType.File, workspace, folder, label) api.update_file_data(file, file_data.filename, file_data.type, file_data.file.read()) # Display error page to user if chosen label is in conflict if not self._path_validation.validate_new_content(file): return render_invalid_integrity_chosen_path( file.get_label_as_file(), ) api.save(file, ActionDescription.CREATION) tg.flash(_('File created'), CST.STATUS_OK) redirect = '/workspaces/{}/folders/{}/files/{}' tg.redirect( tg.url(redirect).format(tmpl_context.workspace_id, tmpl_context.folder_id, file.content_id)) @tg.require(current_user_is_contributor()) @tg.expose() def put(self, item_id, file_data=None, comment=None, label=None): # TODO - SECURE THIS workspace = tmpl_context.workspace try: api = ContentApi(tmpl_context.current_user) item = api.get_one(int(item_id), self._item_type, workspace) label_changed = False if label is not None and label != item.label: label_changed = True if label is None: label = '' # TODO - D.A. - 2015-03-19 # refactor this method in order to make code easier to understand with new_revision(item): if (comment and label) or (not comment and label_changed): updated_item = api.update_content( item, label if label else item.label, comment if comment else '') # Display error page to user if chosen label is in conflict if not self._path_validation.validate_new_content( updated_item, ): return render_invalid_integrity_chosen_path( updated_item.get_label_as_file(), ) api.save(updated_item, ActionDescription.EDITION) # This case is the default "file title and description # update" In this case the file itself is not revisionned else: # So, now we may have a comment and/or a file revision if comment and '' == label: comment_item = api.create_comment(workspace, item, comment, do_save=False) if not isinstance(file_data, FieldStorage): api.save(comment_item, ActionDescription.COMMENT) else: # The notification is only sent # if the file is NOT updated # # If the file is also updated, # then a 'file revision' notification will be sent. api.save(comment_item, ActionDescription.COMMENT, do_notify=False) if isinstance(file_data, FieldStorage): api.update_file_data(item, file_data.filename, file_data.type, file_data.file.read()) # Display error page to user if chosen label is in # conflict if not self._path_validation.validate_new_content( item, ): return render_invalid_integrity_chosen_path( item.get_label_as_file(), ) api.save(item, ActionDescription.REVISION) msg = _('{} updated').format(self._item_type_label) tg.flash(msg, CST.STATUS_OK) tg.redirect( self._std_url.format(tmpl_context.workspace_id, tmpl_context.folder_id, item.content_id)) except ValueError as e: error = '{} not updated - error: {}' msg = _(error).format(self._item_type_label, str(e)) tg.flash(msg, CST.STATUS_ERROR) tg.redirect( self._err_url.format(tmpl_context.workspace_id, tmpl_context.folder_id, item_id))
class UserWorkspaceFolderThreadRestController(TIMWorkspaceContentRestController ): """ manage a path like this: /workspaces/1/folders/XXX/pages/4 """ TEMPLATE_NEW = 'mako:tracim.templates.thread.new' TEMPLATE_EDIT = 'mako:tracim.templates.thread.edit' comments = UserWorkspaceFolderThreadCommentRestController() def _before(self, *args, **kw): TIMRestPathContextSetup.current_user() TIMRestPathContextSetup.current_workspace() TIMRestPathContextSetup.current_folder() @property def _std_url(self): return tg.url('/workspaces/{}/folders/{}/threads/{}') @property def _err_url(self): return self._std_url @property def _parent_url(self): return tg.url('/workspaces/{}/folders/{}') @property def _item_type(self): return ContentType.Thread @property def _item_type_label(self): return _('Thread') @property def _get_one_context(self) -> str: return CTX.THREAD @property def _get_all_context(self) -> str: return CTX.THREADS @tg.require(current_user_is_contributor()) @tg.expose() def post(self, label='', content='', parent_id=None): """ Creates a new thread. Actually, on POST, the content will be included in a user comment instead of being the thread description :param label: :param content: :return: """ # TODO - SECURE THIS workspace = tmpl_context.workspace api = ContentApi(tmpl_context.current_user) thread = api.create(ContentType.Thread, workspace, tmpl_context.folder, label) # FIXME - DO NOT DUPLCIATE FIRST MESSAGE thread.description = content api.save(thread, ActionDescription.CREATION, do_notify=False) comment = api.create(ContentType.Comment, workspace, thread, label) comment.label = '' comment.description = content api.save(comment, ActionDescription.COMMENT, do_notify=False) api.do_notify(thread) tg.flash(_('Thread created'), CST.STATUS_OK) tg.redirect( self._std_url.format(tmpl_context.workspace_id, tmpl_context.folder_id, thread.content_id)) @tg.require(current_user_is_reader()) @tg.expose('tracim.templates.thread.getone') def get_one(self, thread_id): thread_id = int(thread_id) user = tmpl_context.current_user workspace = tmpl_context.workspace current_user_content = Context(CTX.CURRENT_USER).toDict(user) current_user_content.roles.sort(key=lambda role: role.workspace.name) content_api = ContentApi(user) thread = content_api.get_one(thread_id, ContentType.Thread, workspace) fake_api_breadcrumb = self.get_breadcrumb(thread_id) fake_api_content = DictLikeClass(breadcrumb=fake_api_breadcrumb, current_user=current_user_content) fake_api = Context(CTX.FOLDER).toDict(fake_api_content) dictified_thread = Context(CTX.THREAD).toDict(thread, 'thread') return DictLikeClass(result=dictified_thread, fake_api=fake_api)
class UserWorkspaceFolderFileRestController(TIMWorkspaceContentRestController): """ manage a path like this: /workspaces/1/folders/XXX/files/4 """ TEMPLATE_NEW = 'mako:tracim.templates.file.new' TEMPLATE_EDIT = 'mako:tracim.templates.file.edit' @property def _std_url(self): return tg.url('/workspaces/{}/folders/{}/files/{}') @property def _parent_url(self): return tg.url('/workspaces/{}/folders/{}') @property def _item_type(self): return ContentType.File @property def _item_type_label(self): return _('File') @property def _get_one_context(self) -> str: return CTX.FILE @property def _get_all_context(self) -> str: return CTX.FILES @tg.require(current_user_is_reader()) @tg.expose('tracim.templates.file.getone') def get_one(self, file_id, revision_id=None): file_id = int(file_id) user = tmpl_context.current_user workspace = tmpl_context.workspace workspace_id = tmpl_context.workspace_id 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) 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) 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') return DictLikeClass(result=dictified_file, fake_api=fake_api) @tg.require(current_user_is_reader()) @tg.expose() def download(self, file_id, revision_id=None): file_id = int(file_id) revision_id = int(revision_id) if revision_id != 'latest' else None user = tmpl_context.current_user workspace = tmpl_context.workspace content_api = ContentApi(user) revision_to_send = None if revision_id: item = content_api.get_one_from_revision(file_id, self._item_type, workspace, revision_id) else: item = content_api.get_one(file_id, self._item_type, workspace) revision_to_send = None if item.revision_to_serialize <= 0: for revision in item.revisions: if not revision_to_send: revision_to_send = revision if revision.revision_id > revision_to_send.revision_id: revision_to_send = revision else: for revision in item.revisions: if revision.revision_id == item.revision_to_serialize: revision_to_send = revision break content_type = 'application/x-download' if revision_to_send.file_mimetype: content_type = str(revision_to_send.file_mimetype) tg.response.headers['Content-type'] = str( revision_to_send.file_mimetype) tg.response.headers['Content-Type'] = content_type tg.response.headers['Content-Disposition'] = str( 'attachment; filename="{}"'.format(revision_to_send.file_name)) return revision_to_send.file_content def get_all_fake(self, context_workspace: Workspace, context_folder: Content): """ fake methods are used in other controllers in order to simulate a client/server api. the "client" controller method will include the result into its own fake_api object which will be available in the templates :param context_workspace: the workspace which would be taken from tmpl_context if we were in the normal behavior :return: """ workspace = context_workspace content_api = ContentApi(tmpl_context.current_user) files = content_api.get_all(context_folder.content_id, ContentType.File, workspace) dictified_files = Context(CTX.FILES).toDict(files) return DictLikeClass(result=dictified_files) @tg.require(current_user_is_contributor()) @tg.expose() def post(self, label='', file_data=None): # TODO - SECURE THIS workspace = tmpl_context.workspace api = ContentApi(tmpl_context.current_user) file = api.create(ContentType.File, workspace, tmpl_context.folder, label) api.update_file_data(file, file_data.filename, file_data.type, file_data.file.read()) api.save(file, ActionDescription.CREATION) tg.flash(_('File created'), CST.STATUS_OK) tg.redirect( tg.url('/workspaces/{}/folders/{}/files/{}').format( tmpl_context.workspace_id, tmpl_context.folder_id, file.content_id)) @tg.require(current_user_is_contributor()) @tg.expose() def put(self, item_id, file_data=None, comment=None, label=''): # TODO - SECURE THIS workspace = tmpl_context.workspace try: item_saved = False api = ContentApi(tmpl_context.current_user) item = api.get_one(int(item_id), self._item_type, workspace) # TODO - D.A. - 2015-03-19 # refactor this method in order to make code easier to understand with new_revision(item): if comment and label: updated_item = api.update_content( item, label if label else item.label, comment if comment else '') api.save(updated_item, ActionDescription.EDITION) # This case is the default "file title and description update" # In this case the file itself is not revisionned else: # So, now we may have a comment and/or a file revision if comment and '' == label: comment_item = api.create_comment(workspace, item, comment, do_save=False) if not isinstance(file_data, FieldStorage): api.save(comment_item, ActionDescription.COMMENT) else: # The notification is only sent # if the file is NOT updated # # If the file is also updated, # then a 'file revision' notification will be sent. api.save(comment_item, ActionDescription.COMMENT, do_notify=False) if isinstance(file_data, FieldStorage): api.update_file_data(item, file_data.filename, file_data.type, file_data.file.read()) api.save(item, ActionDescription.REVISION) msg = _('{} updated').format(self._item_type_label) tg.flash(msg, CST.STATUS_OK) tg.redirect( self._std_url.format(tmpl_context.workspace_id, tmpl_context.folder_id, item.content_id)) except ValueError as e: msg = _('{} not updated - error: {}').format( self._item_type_label, str(e)) tg.flash(msg, CST.STATUS_ERROR) tg.redirect( self._err_url.format(tmpl_context.workspace_id, tmpl_context.folder_id, item_id))
class TIMWorkspaceContentRestController(TIMRestControllerWithBreadcrumb): """ This class is intended to be parent class of controllers managing routes like /dashboard/workspaces/{}/folders/{}/someitems/{} """ def _before(self, *args, **kw): try: TIMRestPathContextSetup.current_user() TIMRestPathContextSetup.current_workspace() TIMRestPathContextSetup.current_folder() except NoResultFound: abort(404) @property def _std_url(self): raise NotImplementedError('You must implement this method in child controllers') ## # ## Example of result: ## return tg.url('/dashboard/workspaces/{}/folders/{}/threads/{}') @property def _err_url(self): raise NotImplementedError('You must implement this method in child controllers') ## # ## Example of result: ## return tg.url('/dashboard/workspaces/{}/folders/{}/threads/{}') def _parent_url(self): raise NotImplementedError('You must implement this method in child controllers') ## # ## Example of result: ## return tg.url('/dashboard/workspaces/{}/folders/{}') @property def _item_type(self): raise NotImplementedError('You must implement this method in child controllers') @property def _item_type_label(self): raise NotImplementedError('You must implement this method in child controllers') @property def _get_one_context(self) -> str: """ This method should return the get_all context associated to the given Node type example: CTX.THREAD """ raise NotImplementedError('You must implement this method in child controllers') @property def _get_all_context(self) -> str: """ This method should return the get_all context associated to the given Node type example: CTX.THREADS """ raise NotImplementedError('You must implement this method in child controllers') @tg.require(current_user_is_contributor()) @tg.expose() def new(self, parent_id=None, workspace_id=None): """ Show the add form Note: parent is the /folders/{parent_id} value When refactoring urls, this may be need somme update """ tg.override_template(self.new, self.TEMPLATE_NEW) workspace_id = tg.request.GET['workspace_id'] parent_id = tg.request.GET['parent_id'] if 'parent_id' in tg.request.GET else None return DictLikeClass(result=self._struct_new_serialized(workspace_id, parent_id)) @tg.require(current_user_is_contributor()) @tg.expose() def edit(self, item_id): """ Show the edit form (do not really edit the data) :param item_id: :return: """ # the follwing line allow to define the template to use in child classes. tg.override_template(self.edit, self.TEMPLATE_EDIT) item_id = int(item_id) user = tmpl_context.current_user workspace = tmpl_context.workspace content_api = ContentApi(user) item = content_api.get_one(item_id, self._item_type, workspace) dictified_item = Context(self._get_one_context).toDict(item, 'item') return DictLikeClass(result = dictified_item) @tg.require(current_user_is_contributor()) @tg.expose() def put(self, item_id, label='',content=''): # TODO - SECURE THIS workspace = tmpl_context.workspace try: api = ContentApi(tmpl_context.current_user) item = api.get_one(int(item_id), self._item_type, workspace) with new_revision(item): api.update_content(item, label, content) if not self._path_validation.validate_new_content(item): return render_invalid_integrity_chosen_path( item.get_label(), ) api.save(item, ActionDescription.REVISION) msg = _('{} updated').format(self._item_type_label) tg.flash(msg, CST.STATUS_OK) tg.redirect(self._std_url.format(tmpl_context.workspace_id, tmpl_context.folder_id, item.content_id)) except SameValueError as e: msg = _('{} not updated: the content did not change').format(self._item_type_label) tg.flash(msg, CST.STATUS_WARNING) tg.redirect(self._err_url.format(tmpl_context.workspace_id, tmpl_context.folder_id, item_id)) except ValueError as e: msg = _('{} not updated - error: {}').format(self._item_type_label, str(e)) tg.flash(msg, CST.STATUS_ERROR) tg.redirect(self._err_url.format(tmpl_context.workspace_id, tmpl_context.folder_id, item_id)) @tg.require(current_user_is_contributor()) @tg.expose() def put_status(self, item_id, status): item_id = int(item_id) content_api = ContentApi(tmpl_context.current_user) item = content_api.get_one(item_id, self._item_type, tmpl_context.workspace) try: with new_revision(item): content_api.set_status(item, status) content_api.save(item, ActionDescription.STATUS_UPDATE) msg = _('{} status updated').format(self._item_type_label) tg.flash(msg, CST.STATUS_OK) tg.redirect(self._std_url.format(item.workspace_id, item.parent_id, item.content_id)) except ValueError as e: msg = _('{} status not updated: {}').format(self._item_type_label, str(e)) tg.flash(msg, CST.STATUS_ERROR) tg.redirect(self._err_url.format(item.workspace_id, item.parent_id, item.content_id)) def get_all_fake(self, context_workspace: Workspace, context_folder: Content) -> [Content]: """ fake methods are used in other controllers in order to simulate a client/server api. the "client" controller method will include the result into its own fake_api object which will be available in the templates :param context_workspace: the workspace which would be taken from tmpl_context if we were in the normal behavior :return: """ workspace = context_workspace content_api = ContentApi(tmpl_context.current_user) items = content_api.get_all(context_folder.content_id, self._item_type, workspace) dictified_items = Context(self._get_all_context).toDict(items) return DictLikeClass(result = dictified_items) @tg.require(current_user_is_content_manager()) @tg.expose() def put_archive(self, item_id): # TODO - CHECK RIGHTS item_id = int(item_id) content_api = ContentApi(tmpl_context.current_user) item = content_api.get_one(item_id, self._item_type, tmpl_context.workspace) try: next_url = self._parent_url.format(item.workspace_id, item.parent_id) undo_url = self._std_url.format(item.workspace_id, item.parent_id, item.content_id)+'/put_archive_undo' msg = _('{} archived. <a class="alert-link" href="{}">Cancel action</a>').format(self._item_type_label, undo_url) with new_revision(item): content_api.archive(item) content_api.save(item, ActionDescription.ARCHIVING) tg.flash(msg, CST.STATUS_OK, no_escape=True) # TODO allow to come back tg.redirect(next_url) except ValueError as e: next_url = self._std_url.format(item.workspace_id, item.parent_id, item.content_id) msg = _('{} not archived: {}').format(self._item_type_label, str(e)) tg.flash(msg, CST.STATUS_ERROR) tg.redirect(next_url) @tg.require(current_user_is_content_manager()) @tg.expose() def put_archive_undo(self, item_id): # TODO - CHECK RIGHTS item_id = int(item_id) content_api = ContentApi(tmpl_context.current_user, True) # Here we do not filter archived items item = content_api.get_one(item_id, self._item_type, tmpl_context.workspace) try: next_url = self._std_url.format(item.workspace_id, item.parent_id, item.content_id) msg = _('{} unarchived.').format(self._item_type_label) with new_revision(item): content_api.unarchive(item) content_api.save(item, ActionDescription.UNARCHIVING) tg.flash(msg, CST.STATUS_OK) tg.redirect(next_url ) except ValueError as e: msg = _('{} not un-archived: {}').format(self._item_type_label, str(e)) next_url = self._std_url.format(item.workspace_id, item.parent_id, item.content_id) # We still use std url because the item has not been archived tg.flash(msg, CST.STATUS_ERROR) tg.redirect(next_url) @tg.require(current_user_is_content_manager()) @tg.expose() def put_delete(self, item_id): # TODO - CHECK RIGHTS item_id = int(item_id) content_api = ContentApi(tmpl_context.current_user) item = content_api.get_one(item_id, self._item_type, tmpl_context.workspace) try: next_url = self._parent_url.format(item.workspace_id, item.parent_id) undo_url = self._std_url.format(item.workspace_id, item.parent_id, item.content_id)+'/put_delete_undo' msg = _('{} deleted. <a class="alert-link" href="{}">Cancel action</a>').format(self._item_type_label, undo_url) with new_revision(item): content_api.delete(item) content_api.save(item, ActionDescription.DELETION) tg.flash(msg, CST.STATUS_OK, no_escape=True) tg.redirect(next_url) except ValueError as e: back_url = self._std_url.format(item.workspace_id, item.parent_id, item.content_id) msg = _('{} not deleted: {}').format(self._item_type_label, str(e)) tg.flash(msg, CST.STATUS_ERROR) tg.redirect(back_url) @tg.require(current_user_is_content_manager()) @tg.expose() def put_delete_undo(self, item_id): # TODO - CHECK RIGHTS item_id = int(item_id) content_api = ContentApi(tmpl_context.current_user, True, True) # Here we do not filter deleted items item = content_api.get_one(item_id, self._item_type, tmpl_context.workspace) try: next_url = self._std_url.format(item.workspace_id, item.parent_id, item.content_id) msg = _('{} undeleted.').format(self._item_type_label) with new_revision(item): content_api.undelete(item) content_api.save(item, ActionDescription.UNDELETION) tg.flash(msg, CST.STATUS_OK) tg.redirect(next_url) except ValueError as e: logger.debug(self, 'Exception: {}'.format(e.__str__)) back_url = self._parent_url.format(item.workspace_id, item.parent_id) msg = _('{} not un-deleted: {}').format(self._item_type_label, str(e)) tg.flash(msg, CST.STATUS_ERROR) tg.redirect(back_url) @tg.expose() @tg.require(not_anonymous()) def put_read(self, item_id): item_id = int(item_id) content_api = ContentApi(tmpl_context.current_user, True, True) # Here we do not filter deleted items item = content_api.get_one(item_id, self._item_type, tmpl_context.workspace) item_url = self._std_url.format(item.workspace_id, item.parent_id, item.content_id) try: msg = _('{} marked as read.').format(self._item_type_label) content_api.mark_read(item) tg.flash(msg, CST.STATUS_OK) tg.redirect(item_url) except ValueError as e: logger.debug(self, 'Exception: {}'.format(e.__str__)) msg = _('{} not marked as read: {}').format(self._item_type_label, str(e)) tg.flash(msg, CST.STATUS_ERROR) tg.redirect(item_url) @tg.expose() @tg.require(not_anonymous()) def put_unread(self, item_id): item_id = int(item_id) content_api = ContentApi(tmpl_context.current_user, True, True) # Here we do not filter deleted items item = content_api.get_one(item_id, self._item_type, tmpl_context.workspace) item_url = self._std_url.format(item.workspace_id, item.parent_id, item.content_id) try: msg = _('{} marked unread.').format(self._item_type_label) content_api.mark_unread(item) tg.flash(msg, CST.STATUS_OK) tg.redirect(item_url) except ValueError as e: logger.debug(self, 'Exception: {}'.format(e.__str__)) msg = _('{} not marked unread: {}').format(self._item_type_label, str(e)) tg.flash(msg, CST.STATUS_ERROR) tg.redirect(item_url)