Source code for kotti.views.edit.actions

"""
Action views
"""
from pyramid.exceptions import Forbidden
from pyramid.httpexceptions import HTTPFound
from pyramid.url import resource_url
from pyramid.view import view_config
from pyramid.view import view_defaults

from kotti import DBSession
from kotti import get_settings
from kotti.fanstatic import contents_view_js
from kotti.interfaces import IContent
from kotti.resources import Node
from kotti.util import ActionButton
from kotti.util import _
from kotti.util import get_paste_items
from kotti.util import title_to_name
from kotti.views.edit import _state_info
from kotti.views.edit import _states
from kotti.views.form import EditFormView
from kotti.views.util import nodes_tree
from kotti.workflow import get_workflow


[docs]@view_defaults(permission="edit") class NodeActions: """Actions related to content nodes.""" def __init__(self, context, request): self.context = context self.request = request self.flash = self.request.session.flash def _selected_children(self, add_context=True): """ Get the selected children of the given context. These are either the selected nodes of the contents view or the context itself. :result: List with select children. :rtype: list """ ids = self.request.session.pop("kotti.selected-children") if ids is None and add_context: ids = [self.context.id] return ids def _all_children(self, context, permission="view"): """ Recursively get all children of the given context. :result: List with all children of a given context. :rtype: list """ tree = nodes_tree(self.request, context=context, permission=permission) return tree.tolist()[1:]
[docs] def back(self, view=None): """ Redirect to the given view of the context, the referrer of the request or the default_view of the context. :rtype: pyramid.httpexceptions.HTTPFound """ url = self.request.resource_url(self.context) if view is not None: url += view elif self.request.referrer: url = self.request.referrer return HTTPFound(location=url)
[docs] @view_config(name="workflow-change", permission="state_change") def workflow_change(self): """ Handle workflow change requests from workflow dropdown. :result: Redirect response to the referrer of the request. :rtype: pyramid.httpexceptions.HTTPFound """ new_state = self.request.params["new_state"] wf = get_workflow(self.context) wf.transition_to_state(self.context, self.request, new_state) self.flash(EditFormView.success_message, "success") return self.back()
[docs] @view_config(name="copy") def copy_node(self): """ Copy nodes view. Copy the current node or the selected nodes in the contents view and save the result in the session of the request. :result: Redirect response to the referrer of the request. :rtype: pyramid.httpexceptions.HTTPFound """ return self._do_copy_or_cut("copy", "copied")
[docs] @view_config(name="cut") def cut_nodes(self): """ Cut nodes view. Cut the current node or the selected nodes in the contents view and save the result in the session of the request. :result: Redirect response to the referrer of the request. :rtype: pyramid.httpexceptions.HTTPFound """ return self._do_copy_or_cut("cut", "cut")
def _do_copy_or_cut(self, action, action_title): ids = self._selected_children() self.request.session["kotti.paste"] = (ids, action) for id in ids: item = DBSession.query(Node).get(id) self.flash( _("${title} was %s." % action_title, mapping=dict(title=item.title)), "success", ) if not self.request.is_xhr: return self.back()
[docs] @view_config(name="paste") def paste_nodes(self): """ Paste nodes view. Paste formerly copied or cutted nodes into the current context. Note that a cutted node can not be pasted into itself. :result: Redirect response to the referrer of the request. :rtype: pyramid.httpexceptions.HTTPFound """ ids, action = self.request.session["kotti.paste"] for count, id in enumerate(ids): item = DBSession.query(Node).get(id) if item is not None: if action == "cut": if not self.request.has_permission("edit", item): raise Forbidden() item.__parent__.children.remove(item) item.name = title_to_name(item.name, blacklist=self.context.keys()) self.context[item.name] = item if count is len(ids) - 1: del self.request.session["kotti.paste"] elif action == "copy": copy = item.copy() name = copy.name if not name: # for root name = copy.title name = title_to_name(name, blacklist=self.context.keys()) copy.name = name self.context[name] = copy self.flash( _("${title} was pasted.", mapping=dict(title=item.title)), "success" ) else: self.flash(_("Could not paste node. It no longer exists."), "error") DBSession.flush() if not self.request.is_xhr: return self.back()
[docs] def move(self, move): """ Do the real work to move the selected nodes up or down. Called by the up and the down view. :result: Redirect response to the referrer of the request. :rtype: pyramid.httpexceptions.HTTPFound """ ids = self._selected_children() if move == 1: ids.reverse() for id in ids: child = DBSession.query(Node).get(id) index = self.context.children.index(child) self.context.children.pop(index) self.context.children.insert(index + move, child) self.flash( _("${title} was moved.", mapping=dict(title=child.title)), "success" ) if not self.request.is_xhr: return self.back()
[docs] @view_config(name="up") def up(self): """ Move up nodes view. Move the selected nodes up by 1 position and get back to the referrer of the request. :result: Redirect response to the referrer of the request. :rtype: pyramid.httpexceptions.HTTPFound """ return self.move(-1)
[docs] @view_config(name="down") def down(self): """ Move down nodes view. Move the selected nodes down by 1 position and get back to the referrer of the request. :result: Redirect response to the referrer of the request. :rtype: pyramid.httpexceptions.HTTPFound """ return self.move(1)
[docs] def set_visibility(self, show): """ Do the real work to set the visibility of nodes in the menu. Called by the show and the hide view. :result: Redirect response to the referrer of the request. :rtype: pyramid.httpexceptions.HTTPFound """ ids = self._selected_children() for id in ids: child = DBSession.query(Node).get(id) if child.in_navigation != show: child.in_navigation = show mapping = dict(title=child.title) if show: mg = _( "${title} is now visible in the navigation.", mapping=mapping ) else: mg = _( "${title} is no longer visible in the navigation.", mapping=mapping, ) self.flash(mg, "success") if not self.request.is_xhr: return self.back()
[docs] @view_config(name="show") def show(self): """ Show nodes view. Switch the in_navigation attribute of selected nodes to ``True`` and get back to the referrer of the request. :result: Redirect response to the referrer of the request. :rtype: pyramid.httpexceptions.HTTPFound """ return self.set_visibility(True)
[docs] @view_config(name="hide") def hide(self): """ Hide nodes view. Switch the in_navigation attribute of selected nodes to ``False`` and get back to the referrer of the request. :result: Redirect response to the referrer of the request. :rtype: pyramid.httpexceptions.HTTPFound """ return self.set_visibility(False)
[docs] @view_config( name="delete", permission="delete", renderer="kotti:templates/edit/delete.pt" ) def delete_node(self): """ Delete node view. Renders either a view to delete the current node or handle the deletion of the current node and get back to the default view of the node. :result: Either a redirect response or a dictionary passed to the template for rendering. :rtype: pyramid.httpexceptions.HTTPFound or dict """ action = self.request.POST.get("delete") if action is not None: parent = self.context.__parent__ if action == "delete": location = resource_url(parent, self.request) self.flash( _("${title} was deleted.", mapping=dict(title=self.context.title)), "success", ) del parent[self.context.name] else: location = resource_url(self.context, self.request) return HTTPFound(location=location) return {}
[docs] @view_config( name="delete_nodes", permission="delete", renderer="kotti:templates/edit/delete-nodes.pt", ) def delete_nodes(self): """ Delete nodes view. Renders either a view to delete multiple nodes or delete the selected nodes and get back to the referrer of the request. :result: Either a redirect response or a dictionary passed to the template for rendering. :rtype: pyramid.httpexceptions.HTTPFound or dict """ if "delete_nodes" in self.request.POST: ids = self.request.POST.getall("children-to-delete") if not ids: self.flash(_("Nothing was deleted."), "info") for id in ids: item = DBSession.query(Node).get(id) self.flash( _("${title} was deleted.", mapping=dict(title=item.title)), "success", ) del self.context[item.name] return self.back("@@contents") if "cancel" in self.request.POST: self.flash(_("No changes were made."), "info") return self.back("@@contents") ids = self._selected_children(add_context=False) items = [] if ids is not None: items = ( DBSession.query(Node) .filter(Node.id.in_(ids)) .order_by(Node.position) .all() ) return {"items": items, "states": _states(self.context, self.request)}
[docs] @view_config(name="rename", renderer="kotti:templates/edit/rename.pt") def rename_node(self): """ Rename node view. Renders either a view to change the title and name for the current node or handle the changes and get back to the default view of the node. :result: Either a redirect response or a dictionary passed to the template for rendering. :rtype: pyramid.httpexceptions.HTTPFound or dict """ if "rename" in self.request.POST: name = self.request.POST["name"] title = self.request.POST["title"] if not name or not title: self.flash(_("Name and title are required."), "error") else: self.context.name = name.replace("/", "") self.context.title = title self.flash(_("Item was renamed."), "success") return self.back("") return {}
[docs] @view_config(name="rename_nodes", renderer="kotti:templates/edit/rename-nodes.pt") def rename_nodes(self): """ Rename nodes view. Renders either a view to change the titles and names for multiple nodes or handle the changes and get back to the referrer of the request. :result: Either a redirect response or a dictionary passed to the template for rendering. :rtype: pyramid.httpexceptions.HTTPFound or dict """ if "rename_nodes" in self.request.POST: ids = self.request.POST.getall("children-to-rename") for id in ids: item = DBSession.query(Node).get(id) name = self.request.POST[id + "-name"] title = self.request.POST[id + "-title"] if not name or not title: self.flash(_("Name and title are required."), "error") location = resource_url( self.context, self.request, "@@rename_nodes" ) return HTTPFound(location=location) else: item.name = title_to_name(name, blacklist=self.context.keys()) item.title = title self.flash(_("Your changes have been saved."), "success") return self.back("@@contents") if "cancel" in self.request.POST: self.flash(_("No changes were made."), "info") return self.back("@@contents") ids = self._selected_children(add_context=False) items = [] if ids is not None: items = DBSession.query(Node).filter(Node.id.in_(ids)).all() return {"items": items}
[docs] @view_config(name="change_state", renderer="kotti:templates/edit/change-state.pt") def change_state(self): """ Change state view. Renders either a view to handle workflow changes for multiple nodes or handle the selected workflow changes and get back to the referrer of the request. :result: Either a redirect response or a dictionary passed to the template for rendering. :rtype: pyramid.httpexceptions.HTTPFound or dict """ if "change_state" in self.request.POST: ids = self.request.POST.getall("children-to-change-state") to_state = self.request.POST.get("to-state", "no-change") include_children = self.request.POST.get("include-children") if to_state != "no-change": items = DBSession.query(Node).filter(Node.id.in_(ids)).all() for item in items: wf = get_workflow(item) if wf is not None: wf.transition_to_state(item, self.request, to_state) if include_children: childs = self._all_children(item, permission="state_change") for child in childs: wf = get_workflow(child) if wf is not None: wf.transition_to_state(child, self.request, to_state) self.flash(_("Your changes have been saved."), "success") else: self.flash(_("No changes were made."), "info") return self.back("@@contents") if "cancel" in self.request.POST: self.flash(_("No changes were made."), "info") return self.back("@@contents") ids = self._selected_children(add_context=False) items = transitions = [] if ids is not None: wf = get_workflow(self.context) if wf is not None: items = DBSession.query(Node).filter(Node.id.in_(ids)).all() for item in items: trans_info = wf.get_transitions(item, self.request) for tran_info in trans_info: if tran_info not in transitions: transitions.append(tran_info) return { "items": items, "states": _states(self.context, self.request), "transitions": transitions, }
[docs]def contents_buttons(context, request): """ Build the action buttons for the contents view based on the current state and the persmissions of the user. :result: List of ActionButtons. :rtype: list """ buttons = [] if get_paste_items(context, request): buttons.append(ActionButton("paste", title=_("Paste"), no_children=True)) if context.children: buttons.append(ActionButton("copy", title=_("Copy"))) buttons.append(ActionButton("cut", title=_("Cut"))) buttons.append( ActionButton("rename_nodes", title=_("Rename"), css_class="btn btn-warning") ) buttons.append( ActionButton("delete_nodes", title=_("Delete"), css_class="btn btn-danger") ) if get_workflow(context) is not None: buttons.append(ActionButton("change_state", title=_("Change State"))) buttons.append(ActionButton("up", title=_("Move up"))) buttons.append(ActionButton("down", title=_("Move down"))) buttons.append(ActionButton("show", title=_("Show"))) buttons.append(ActionButton("hide", title=_("Hide"))) return [button for button in buttons if button.permitted(context, request)]
[docs]@view_config(name="add-dropdown", renderer="kotti:templates/add-dropdown.pt") def content_type_factories(context, request): """ Renders the drop down menu for Add button in editor bar. :result: Dictionary passed to the template for rendering. :rtype: pyramid.httpexceptions.HTTPFound or dict """ all_types = get_settings()["kotti.available_types"] factories = [] for factory in all_types: if factory.type_info.addable(context, request): factories.append(factory) return {"factories": factories}
[docs]@view_config( context=IContent, name="contents", permission="view", renderer="kotti:templates/edit/contents.pt", ) def contents(context, request): """ Contents view. Renders either the contents view or handle the action button actions of the view. :result: Either a redirect response or a dictionary passed to the template for rendering. :rtype: pyramid.httpexceptions.HTTPFound or dict """ contents_view_js.need() buttons = contents_buttons(context, request) for button in buttons: if button.name in request.POST: children = request.POST.getall("children") if not children and button.name != "paste": request.session.flash( _("You have to select items to perform an action."), "info" ) location = resource_url(context, request) + "@@contents" return HTTPFound(location=location) request.session["kotti.selected-children"] = children location = button.url(context, request) return HTTPFound(location, request=request) return {"children": context.children_with_permission(request), "buttons": buttons}
[docs]@view_config( name="move-child-position", permission="edit", request_method="POST", renderer="json", ) def move_child_position(context, request): """ Move the child from one position to another. :param context: "Container" node in which the child changes its position. :type context: :class:kotti.resources.Node or descendant :param request: Current request (of method POST). Must contain either "from" and "to" params or a json_body that contain(s) the 0-based old (i.e. the current index of the child to be moved) and new position (its new index) values. :type request: :result: JSON serializable object with a single attribute ("result") that is either "success" or "error". :rtype: dict """ data = request.POST or request.json_body if ("from" in data) and ("to" in data): max_pos = len(context.children) - 1 try: old_position = int(data["from"]) new_position = int(data["to"]) if not ((0 <= old_position <= max_pos) and (0 <= new_position <= max_pos)): raise ValueError except ValueError: return {"result": "error"} # sqlalchemy.ext.orderinglist takes care of the "right" sequence # numbers (immediately consecutive, starting with 0) for us. context.children.insert(new_position, context.children.pop(old_position)) result = "success" else: result = "error" return {"result": result}
[docs]@view_config( name="workflow-dropdown", permission="view", renderer="kotti:templates/workflow-dropdown.pt", ) def workflow(context, request): """ Renders the drop down menu for workflow actions. :result: Dictionary passed to the template for rendering. :rtype: dict """ wf = get_workflow(context) if wf is not None: state_info = _state_info(context, request) curr_state = [i for i in state_info if i["current"]][0] trans_info = [ trans for trans in wf.get_transitions(context, request) if request.has_permission(trans["permission"], context) ] return { "states": _states(context, request), "transitions": trans_info, "current_state": curr_state, } return {"current_state": None}
[docs]@view_config( name="actions-dropdown", permission="edit", renderer="kotti:templates/actions-dropdown.pt", ) def actions(context, request): """ Renders the drop down menu for Actions button in editor bar. :result: Dictionary passed to the template for rendering. :rtype: dict """ actions = [] if hasattr(context, "type_info"): actions = [ a for a in context.type_info.action_links if a.visible(context, request) ] return {"actions": actions}
[docs]def includeme(config): """ Pyramid includeme hook. :param config: app config :type config: :class:`pyramid.config.Configurator` """ import warnings with warnings.catch_warnings(): warnings.simplefilter("ignore") config.scan(__name__)