class ItemLocationController(TIMWorkspaceContentRestController, BaseController): @tg.require(current_user_is_content_manager()) @tg.expose() def get_one(self, item_id): item_id = int(item_id) user = tmpl_context.current_user workspace = tmpl_context.workspace item = ContentApi(user).get_one(item_id, ContentType.Any, workspace) raise NotImplementedError return item @tg.require(current_user_is_content_manager()) @tg.expose('tracim.templates.folder.move') def edit(self, item_id): """ Show the edit form (do not really edit the data) :param item_id: :return: """ current_user_content = \ Context(CTX.CURRENT_USER).toDict(tmpl_context.current_user) fake_api = \ Context(CTX.FOLDER) \ .toDict(DictLikeClass(current_user=current_user_content)) 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, ContentType.Any, workspace) dictified_item = Context(CTX.DEFAULT).toDict(item, 'item') return DictLikeClass(result=dictified_item, fake_api=fake_api) @tg.require(current_user_is_content_manager()) @tg.expose() def put(self, item_id, folder_id='0'): """ :param item_id: :param folder_id: id of the folder, in a style like 'workspace_14__content_1586' :return: """ # TODO - SECURE THIS workspace = tmpl_context.workspace item_id = int(item_id) new_workspace, new_parent = convert_id_into_instances(folder_id) if new_workspace != workspace: # check that user is at least # - content manager in current workspace # - content manager in new workspace user = tmpl_context.current_user if user.get_role(workspace) < UserRoleInWorkspace.CONTENT_MANAGER: tg.flash(_('You are not allowed ' 'to move this folder'), CST.STATUS_ERROR) tg.redirect(self.parent_controller.url(item_id)) if user.get_role( new_workspace) < UserRoleInWorkspace.CONTENT_MANAGER: tg.flash( _('You are not allowed to move ' 'this folder to this workspace'), CST.STATUS_ERROR) tg.redirect(self.parent_controller.url(item_id)) api = ContentApi(tmpl_context.current_user) item = api.get_one(item_id, ContentType.Any, workspace) with new_revision(item): api.move_recursively(item, new_parent, new_workspace) next_url = tg.url('/workspaces/{}/folders/{}'.format( new_workspace.workspace_id, item_id)) if new_parent: tg.flash( _('Item moved to {} (workspace {})').format( new_parent.label, new_workspace.label), CST.STATUS_OK) else: tg.flash( _('Item moved to workspace {}').format( new_workspace.label)) tg.redirect(next_url) else: # Default move inside same workspace api = ContentApi(tmpl_context.current_user) item = api.get_one(item_id, ContentType.Any, workspace) with new_revision(item): api.move(item, new_parent) next_url = self.parent_controller.url(item_id) if new_parent: tg.flash( _('Item moved to {}').format(new_parent.label), CST.STATUS_OK) else: tg.flash(_('Item moved to workspace root')) tg.redirect(next_url)
class UserWorkspaceFolderRestController(TIMRestControllerWithBreadcrumb): TEMPLATE_NEW = 'mako:tracim.templates.folder.new' location = ItemLocationController() files = UserWorkspaceFolderFileRestController() pages = UserWorkspaceFolderPageRestController() threads = UserWorkspaceFolderThreadRestController() def _before(self, *args, **kw): TIMRestPathContextSetup.current_user() try: TIMRestPathContextSetup.current_workspace() except NoResultFound: abort(404) @tg.require(current_user_is_content_manager()) @tg.expose('tracim.templates.folder.edit') def edit(self, folder_id): """ Show the edit form (do not really edit the data) :param item_id: :return: """ folder_id = int(folder_id) user = tmpl_context.current_user workspace = tmpl_context.workspace content_api = ContentApi(user) folder = content_api.get_one(folder_id, ContentType.Folder, workspace) dictified_folder = Context(CTX.FOLDER).toDict(folder, 'folder') return DictLikeClass(result=dictified_folder) @tg.require(current_user_is_reader()) @tg.expose('tracim.templates.folder.getone') def get_one(self, folder_id, **kwargs): """ :param folder_id: Displayed folder 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', '')) show_archived = str_as_bool(kwargs.get('show_archived', '')) folder_id = int(folder_id) 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_deleted=show_deleted, show_archived=show_archived, ) with content_api.show(show_deleted=True, show_archived=True): folder = content_api.get_one( folder_id, ContentType.Folder, workspace, ) fake_api_breadcrumb = self.get_breadcrumb(folder_id) fake_api_subfolders = self.get_all_fake(workspace, folder.content_id).result fake_api_pages = self.pages.get_all_fake(workspace, folder).result fake_api_files = self.files.get_all_fake(workspace, folder).result fake_api_threads = self.threads.get_all_fake(workspace, folder).result fake_api_content = DictLikeClass( current_user=current_user_content, breadcrumb=fake_api_breadcrumb, current_folder_subfolders=fake_api_subfolders, current_folder_pages=fake_api_pages, current_folder_files=fake_api_files, current_folder_threads=fake_api_threads, ) fake_api = Context(CTX.FOLDER).toDict(fake_api_content) sub_items = content_api.get_children( parent_id=folder.content_id, content_types=[ ContentType.Folder, ContentType.File, ContentType.Page, ContentType.Thread, ], ) fake_api.sub_items = Context(CTX.FOLDER_CONTENT_LIST).toDict(sub_items) fake_api.content_types = Context(CTX.DEFAULT).toDict( content_api.get_all_types()) dictified_folder = Context(CTX.FOLDER).toDict(folder, 'folder') return DictLikeClass( result=dictified_folder, fake_api=fake_api, show_deleted=show_deleted, show_archived=show_archived, ) def get_all_fake(self, context_workspace: Workspace, parent_id=None): """ 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) with content_api.show(show_deleted=True, show_archived=True): parent_folder = content_api.get_one(parent_id, ContentType.Folder) folders = content_api.get_child_folders(parent_folder, workspace) folders = Context(CTX.FOLDERS).toDict(folders) return DictLikeClass(result=folders) @tg.require(current_user_is_content_manager()) @tg.expose() def post(self, label, parent_id=None, can_contain_folders=False, can_contain_threads=False, can_contain_files=False, can_contain_pages=False): # TODO - SECURE THIS workspace = tmpl_context.workspace api = ContentApi(tmpl_context.current_user) redirect_url_tmpl = '/workspaces/{}/folders/{}' redirect_url = '' try: parent = None if parent_id: parent = api.get_one(int(parent_id), ContentType.Folder, workspace) with DBSession.no_autoflush: folder = api.create(ContentType.Folder, workspace, parent, label) subcontent = dict( folder=True if can_contain_folders == 'on' else False, thread=True if can_contain_threads == 'on' else False, file=True if can_contain_files == 'on' else False, page=True if can_contain_pages == 'on' else False) api.set_allowed_content(folder, subcontent) if not self._path_validation.validate_new_content(folder): return render_invalid_integrity_chosen_path( folder.get_label(), ) api.save(folder) tg.flash(_('Folder created'), CST.STATUS_OK) redirect_url = redirect_url_tmpl.format(tmpl_context.workspace_id, folder.content_id) except Exception as e: error_msg = 'An unexpected exception has been catched. ' \ 'Look at the traceback below.' logger.error(self, error_msg) traceback.print_exc() tb = sys.exc_info()[2] tg.flash( _('Folder not created: {}').format(e.with_traceback(tb)), CST.STATUS_ERROR) if parent_id: redirect_url = \ redirect_url_tmpl.format(tmpl_context.workspace_id, parent_id) else: redirect_url = \ '/workspaces/{}'.format(tmpl_context.workspace_id) #### # # INFO - D.A. - 2014-10-22 - Do not put redirect in a # try/except block as redirect is using exceptions! # tg.redirect(tg.url(redirect_url)) @tg.require(current_user_is_content_manager()) @tg.expose() def put(self, folder_id, label, can_contain_folders=False, can_contain_threads=False, can_contain_files=False, can_contain_pages=False): # TODO - SECURE THIS workspace = tmpl_context.workspace api = ContentApi(tmpl_context.current_user) next_url = '' try: folder = api.get_one(int(folder_id), ContentType.Folder, workspace) subcontent = dict( folder=True if can_contain_folders == 'on' else False, thread=True if can_contain_threads == 'on' else False, file=True if can_contain_files == 'on' else False, page=True if can_contain_pages == 'on' else False) with new_revision(folder): if label != folder.label: # TODO - D.A. - 2015-05-25 # Allow to set folder description api.update_content(folder, label, folder.description) api.set_allowed_content(folder, subcontent) if not self._path_validation.validate_new_content(folder): return render_invalid_integrity_chosen_path( folder.get_label(), ) api.save(folder) tg.flash(_('Folder updated'), CST.STATUS_OK) next_url = self.url(folder.content_id) except Exception as e: tg.flash( _('Folder not updated: {}').format(str(e)), CST.STATUS_ERROR) next_url = self.url(int(folder_id)) tg.redirect(next_url) @property def _std_url(self): return tg.url('/workspaces/{}/folders/{}') @property def _parent_url(self): return tg.url('/workspaces/{}') @property def _item_type_label(self): return _('Folder') @property def _item_type(self): return ContentType.Folder @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) tmp_url = self._std_url.format(item.workspace_id, item.content_id) undo_url = tmp_url + '/put_archive_undo' archived_msg = '{} archived. ' \ '<a class="alert-link" href="{}">Cancel action</a>' msg = _(archived_msg).format(self._item_type_label, undo_url) with new_revision(item): content_api.archive(item) content_api.save(item, ActionDescription.ARCHIVING) # TODO allow to come back tg.flash(msg, CST.STATUS_OK, no_escape=True) 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) # 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) try: next_url = self._std_url.format(item.workspace_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.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) tmp_url = self._std_url.format(item.workspace_id, item.content_id) undo_url = tmp_url + '/put_delete_undo' deleted_msg = '{} deleted. ' \ '<a class="alert-link" href="{}">Cancel action</a>' msg = _(deleted_msg).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.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) # 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) try: next_url = self._std_url.format(item.workspace_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)
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)