Coverage for lino/core/actions.py : 66%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
# -*- coding: UTF-8 -*- # Copyright 2009-2016 Luc Saffre # License: BSD (see file COPYING for details)
decorator, together with some of the predefined actions.
See also:
- :ref:`dev.actions`. - :doc:`/tutorials/actions/index`
""" # import six
else: from django.db.models.loading import get_models
# from lino.modlib.users.choicelists import SiteUser
# holder is either a Model, an Actor or an Action. return raise Exception("Redefinition of chooser %s" % field) # if field.name == 'city': # logger.info("20140822 chooser for %s.%s", holder, field.name)
#~ logger.debug("Instantiate model reports...") #~ n = 0 else: allfields = model._meta.fields + model._meta.virtual_fields #~ logger.debug("Discovered %d choosers in model %s.",n,model)
""" - `cls` is the actor (a class object) - `k` is one of 'detail_layout', 'insert_layout', 'params_layout' - `layout_class`
""" options.update(dl.options) setattr(cls, k, layout_class(dl.desc, cls, **options)) raise Exception( "Cannot reuse %s instance (%s of %r) for %r" % (dl.__class__, k, dl._datasource, cls))
"""Note that `cls` is either an actor or an action. And remember that actors are class objects while actions are instances.
"""
list(cls.parameters.keys()))
raise Exception("params_layout but no parameters ?!")
# Before Django 1.8: else: fld.rel.to = resolve_model(fld.rel.to) #~ if fld.verbose_name is None: #~ fld.verbose_name = fld.rel.model._meta.verbose_name
settings.SITE.kernel.default_ui)
""" Abstract base class for all actions. """
#~ __metaclass__ = ActionMetaClass
""" The text to appear on the button. """ """Whether primary key fields should be disabled when using this action. This is `True` for all actions except :class:`InsertRow`.
"""
"""The class name of an icon to be used for this action when rendered as toolbar button.
Allowed icon names are defined in :data:`lino.core.constants.ICON_NAMES`.
"""
""" The name of another action to which to "attach" this action. Both actions will then be rendered as a single combobutton.
"""
"See :attr:`Parametrizable.parameters`."
"""Used internally. This is True for window actions whose window use the parameter panel: grid and emptytable (but not showdetail)
"""
"""Set this to `True` if your action has :attr:`parameters` but you do *not* want it to open a window where the user can edit these parameters before calling the action.
Setting this attribute to `True` means that the calling code must explicitly set all parameter values. Usage example is the :attr:`lino.modlib.polls.models.AnswersByResponse.answer_buttons` virtual field.
"""
""" Determins the sort order in which the actions will be presented to the user.
List actions are negative and come first.
Predefined `sort_index` values are:
===== ================================= value action ===== ================================= -1 :class:`as_pdf <lino.utils.appy_pod.PrintTableAction>` 10 :class:`InsertRow`, :class:`SubmitDetail` 11 :attr:`duplicate <lino.mixins.duplicable.Duplicable.duplicate>` 20 :class:`detail <ShowDetailAction>` 30 :class:`delete <DeleteSelected>` 31 :class:`merge <lino.core.merge.MergeAction>` 50 :class:`Print <lino.mixins.printable.BasePrintAction>` 51 :class:`Clear Cache <lino.mixins.printable.ClearCacheAction>` 60 :class:`ShowSlaveTable` 90 default for all custom row actions ===== =================================
"""
"""A help text that shortly explains what this action does. :mod:`lino.modlib.extjs` shows this as tooltip text.
"""
""" What to do when this action is being called while the user is on a dirty record.
- `False` means: forget any changes in current record and run the action.
- `True` means: save any changes in current record before running the action. `None` means: ask the user.
"""
"""Used by :mod:`lino_xl.lib.extensible` and :mod:`lino.modlib.awesome_uploader`.
Example::
class CalendarAction(dd.Action): extjs_main_panel = "Lino.CalendarApp().get_main_panel()" ...
"""
""" This is usually `None`. Otherwise it is the name of a Javascript callable to be called without arguments. That callable must have been defined in a :attr:`lino.core.plugin.Plugin.site_js_snippets` of the plugin.
"""
"""Internally used to store the name of this action within the defining Actor's namespace.
""" """Internally used to store the :class:`lino.core.actors.Actor` who defined this action.
"""
""" The hotkey to associate to this action in a user interface. """
""" Used internally. """
"""Whether this action is readonly, i.e. does not change any data.
Setting this to `False` will make the action unavailable for `readonly` user profiles and will cause it to be logged when :attr:`log_each_action_request <lino.core.site.Site.log_each_action_request>` is set to `True`.
Note that Lino actually does not check whether it is true. When a readonly action actually does modify the database, Lino won't "notice" it.
Discussion
Maybe we should change the name `readonly` to `modifying` or `writing` (and set the default value `False`). Because for the application developer that looks more natural. Or --maybe better but probably even more consequences-- the default value should be `False`. Because being readonly, for actions, is a kind of "privilege": they don't get logged, they also exists for readonly users. It would be more "secure" when the developer must be explicit when granting that privilege.
"""
""" Used internally to say whether this action opens a window. """
"""Used internally if :attr:`opens_a_window` to say whether the window has a top toolbar.
"""
"""Used internally if :attr:`opens_a_window` to say whether the window has a navigator.
"""
"""Whether this action should be displayed as a button in the toolbar and the context menu.
For example the :class:`CheckinVisitor <lino_xl.lib.reception.models.CheckinVisitor>`, :class:`ReceiveVisitor <lino_xl.lib.reception.models.ReceiveVisitor>` and :class:`CheckoutVisitor <lino_xl.lib.reception.models.CheckoutVisitor>` actions have this attribute explicitly set to `False` because otherwise they would be visible in the toolbar.
"""
"""Used internally. Whether this action should be displayed as the :attr:`workflow_buttons <lino.core.model.Model.workflow_buttons>` column. If this is True, then Lino will automatically set :attr:`custom_handler` to True.
"""
""" Whether this action is implemented as Javascript function call. This is necessary if you want your action to be callable using an "action link" (html button).
"""
"""True if this action needs an object to act on.
Set this to `False` if this action is a list action, not a row action.
"""
""" HTTP method to use when this action is called using an AJAX call. """
""" Name of a Javascript function to be invoked on the web client when this action is called. """
"""The first argument is the optional `label`, other arguments should be specified as keywords and can be any of the existing class attributes.
"""
# if self.parameters is not None and self.select_rows: # self.show_in_bbar = False # # see ticket #105
raise Exception("Invalid action keyword %s" % k)
raise Exception( "Unkonwn icon_name '{0}'".format(self.icon_name))
""" When a model has an action "foo", then getting an attribute "foo" of a model instance will return an :class:`InstanceAction`. """ self, instance.get_default_table(), instance, owner)
#~ if self.select_rows: #~ return isinstance(caller,(GridEdit,ShowDetailAction)) #~ return isinstance(caller,GridEdit)
"""Return `True` if this is a "window action" (i.e. which opens a GUI window on the client before executin).
""" self.parameters and not self.no_params_window)
ar.action_param_values, **defaults)
return obj.get_choices_text(request, self, field)
return make_params_layout_handle(self, ui)
# same as in Actor but here it is an instance method return None
# same as in Actor but here it is an instance method return None
# same as in Actor but here it is an instance method
if actor is None or actor.default_action is None: return self.label if self is actor.default_action.action: return actor.label else: return self.label # since 20140923 return u"%s %s" % (self.label, actor.label)
raise Exception("Tried to full_name() on %r" % self) #~ return repr(self)
return ar.get_title()
if self.label is None: return "<%s %s>" % (self.__class__.__name__, self.action_name) return "<%s %s (%r)>" % ( self.__class__.__name__, self.action_name, str(self.label))
raise Exception("20121003 Must use full_name(actor)") if self.defining_actor is None: return repr(self) if self.action_name is None: return repr(self) return str(self.defining_actor) + ':' + self.action_name
#~ def set_permissions(self,*args,**kw) #~ self.permission = perms.factory(*args,**kw)
"""Called once per Actor per Action on startup before a BoundAction instance is being created. If this returns False, then the action won't be attached to the given actor.
""" # if not actor.editable and not self.readonly: # return False
# already defined in another actor raise Exception("tried to attach named action %s.%s" % (actor, self.action_name)) # setup_params_choosers(self.__class__)
# return force_text(self.label)
"""Return (True or False) whether the given :class:`ActionRequest <lino.core.requests.BaseRequest>` `ar` should get permission to execute on the given Model instance `obj` (which is in the given `state`).
Derived Action classes may override this to add vetos. E.g. the MoveUp action of a Sequenced is not available on the first row of given `ar`.
"""
""" Return True if this action is visible for users of given profile.
"""
"""Execute the action. `ar` is an :class:`ActionRequest <lino.core.requests.BaseRequest>` object representing the context in which the action is running. """ raise NotImplementedError( "%s has no run_from_ui() method" % self.__class__)
if len(args): obj = args[0] else: obj = None ia = InstanceAction(self, self.defining_actor, obj, None) return ia.run_from_session(ses, **kw)
"""Same as :meth:`lino.core.actors.Actor.param_defaults`, except that on an action it is a instance method.
Note that this method is not called for actions which are rendered in a toolbar (:srcref:`docs/tickets/105`)
"""
# print 20151203, pf.name, repr(pf.rel.to)
return ar.get_title()
raise NotImplementedError
"""Open a window with a grid editor on this table as main item.
"""
#~ self.label = actor.button_label or actor.label self.label = actor.label return super(GridEdit, self).attach_to_actor(actor, name)
#~ return self.actor.list_layout
return actor.window_size
"""Open the detail window on a row of this table.
"""
wl = self.get_window_layout(actor) return wl.window_size
#~ hide_top_toolbar = True
return isinstance(caller, GridEdit)
self.label = actor.label return super(ShowEmptyTable, self).attach_to_actor(actor, name)
return super(ShowEmptyTable, self).as_bootstrap_html(ar, '-99998')
"""Open the Insert window filled with a row of blank or default values. The new row will be actually created only when this window gets submitted.
"""
# required_roles = set([SiteUser])
wl = self.get_window_layout(actor) return wl.window_size
# see blog/2012/0726 # if settings.SITE.user_model and ar.get_user().profile.readonly: if ar.get_user().profile.readonly: return False return super(InsertRow, self).get_action_permission(ar, obj, state)
return kw return kw # raise Exception("20150218 %s" % self) # existing = getattr(ar, '_elem', None) # if existing is not None: # raise Exception("20150218 %s %s", elem, existing) # if existing == elem: # return kw # ar._elem = elem
# required_roles = set([SiteUser])
""" Called when user edited a cell of a non-phantom record in a grid. Installed as `update_action` on every :class:`Actor`.
"""
# logger.info("20140423 SubmitDetail") elem = ar.selected_rows[0] # ar.form2obj_and_save(ar.rqdata, elem, False) self.save_existing_instance(elem, ar)
watcher = ChangeWatcher(elem) ar.ah.store.form2obj(ar, ar.rqdata, elem, False) elem.full_clean()
if watcher.is_dirty(): pre_ui_save.send(sender=elem.__class__, instance=elem, ar=ar) elem.before_ui_save(ar) elem.save(force_update=True) watcher.send_update(ar.request) ar.success(_("%s has been updated.") % obj2unicode(elem)) else: ar.success(_("%s : nothing to save.") % obj2unicode(elem))
elem.after_ui_save(ar, watcher)
# TODO: in fact we need *either* `rows` (when this was called # from a Grid) *or* `goto_instance` (when this was called from a # form). But how to find out which one is needed? # if ar.edit_mode == constants.EDIT_MODE_GRID: ar.set_response(rows=[ar.ah.store.row2list(ar, elem)])
# this is a first attempt to solve the "cannot use active fields in # insert window" problem. not yet ready for use. the idea is that # active fields should not send a real "save" request (either POST or # PUT) in the background but a "validate_form" request which creates a # dummy instance from form content, calls it's full_clean() method to # have other fields filled in, and then return the modified form # content. Fails because the Record.phantom in ExtJS then still gets # lost.
# called by active_fields
elem = ar.create_instance_from_request() ar.ah.store.form2obj(ar, ar.rqdata, elem, False) elem.full_clean() ar.success() # ar.set_response(rows=[ar.ah.store.row2list(ar, elem)]) ar.goto_instance(elem)
"""The "Save" button of a :term:`detail window`.
Called when the OK button of a Detail Window was clicked. Installed as `submit_detail` on every actor.
"""
# logger.info("20140423 SubmitDetail") elem = ar.selected_rows[0] # ar.form2obj_and_save(ar.rqdata, elem, False) self.save_existing_instance(elem, ar) ar.goto_instance(elem)
"""Called when user edited a cell of a phantom record in a grid. """
elem = ar.create_instance_from_request() self.save_new_instance(ar, elem)
pre_ui_save.send(sender=elem.__class__, instance=elem, ar=ar) elem.before_ui_save(ar) elem.save(force_insert=True) # yes, `on_ui_created` comes *after* save() on_ui_created.send(elem, request=ar.request) elem.after_ui_create(ar) elem.after_ui_save(ar, None) ar.success(_("%s has been created.") % obj2unicode(elem))
if ar.actor.handle_uploaded_files is None: # The `rows` can contain complex strings which cause # decoding problems on the client when responding to a # file upload ar.set_response(rows=[ar.ah.store.row2list(ar, elem)]) else: # Must set text/html for file uploads, otherwise the # browser adds a <PRE></PRE> tag around the AJAX response. ar.set_content_type('text/html')
if ar.actor.stay_in_grid: return # No need to ask refresh_all since closing the window will # automatically refresh the underlying window.
ar.goto_instance(elem)
"""Called when the OK button of an Insert Window was clicked. Installed as `submit_insert` on every `dd.Model <lino.core.model.Model>`. """
ar.requesting_panel = None # must set this to None, otherwise javascript button actions # would try to refer the requesting panel which is going to be # closed (this disturbs at least in ticket #219) elem = ar.create_instance_from_request() self.save_new_instance(ar, elem) ar.set_response(close_window=True)
# class SubmitInsertAndStay(SubmitInsert): # sort_index = 11 # switch_to_detail = False # action_name = 'poststay' # label = _("Create without detail") # help_text = _("Don't open a detail window on the new record")
"""An action which opens a window showing the table specified when instantiating the action.
""" 'sort_index', 'required_roles')
self.slave_table = slave_table self.explicit_attribs = set(kw.keys()) super(ShowSlaveTable, self).__init__(**kw)
def get_actor_label(self): return self._label or self.slave_table.label
if isinstance(self.slave_table, basestring): T = settings.SITE.modules.resolve(self.slave_table) if T is None: raise Exception("No table named %s" % self.slave_table) self.slave_table = T for k in self.TABLE2ACTION_ATTRS: if not k in self.explicit_attribs: setattr(self, k, getattr(self.slave_table, k)) return super(ShowSlaveTable, self).attach_to_actor(actor, name)
obj = ar.selected_rows[0] sar = ar.spawn(self.slave_table, master_instance=obj) js = ar.renderer.request_handler(sar) ar.set_response(eval_js=js)
"""An action with a generic dialog window of three fields "Summary", "Description" and a checkbox "Don't send email notification". The default implementation calls the request's :meth:`add_system_note <lino.core.requests.BaseRequest.add_system_note>` method.
Screenshot of a notifying action:
.. image:: /images/screenshots/reception.CheckinVisitor.png :scale: 50
Dialog fields:
.. attribute:: subject .. attribute:: body .. attribute:: silent
"""
notify_subject=models.CharField( _("Summary"), blank=True, max_length=200), notify_body=fields.RichTextField(_("Description"), blank=True), notify_silent=models.BooleanField( _("Don't send email notification"), default=False), )
notify_subject notify_body notify_silent """, window_size=(50, 15))
""" Return the default value of the `notify_subject` field. """ return None
""" Return the default value of the `notify_body` field. """ return None
kw = super(NotifyingAction, self).action_param_defaults(ar, obj, **kw) if obj is not None: s = self.get_notify_subject(ar, obj) if s is not None: kw.update(notify_subject=s) s = self.get_notify_body(ar, obj) if s is not None: kw.update(notify_body=s) return kw
obj = ar.selected_rows[0] ar.set_response(message=ar.action_param_values.notify_subject) ar.set_response(refresh=True) ar.set_response(success=True) self.add_system_note(ar, obj)
#~ body = _("""%(user)s executed the following action:\n%(body)s #~ """) % dict(user=ar.get_user(),body=body) ar.add_system_note( owner, ar.action_param_values.notify_subject, ar.action_param_values.notify_body, ar.action_param_values.notify_silent, **kw)
"""Base class for actions that update something on every selected row. """
"""This is being called on every selected row. """ raise NotImplemented()
ar.success(**kw) n = 0 for obj in ar.selected_rows: if not ar.response.get('success'): ar.info("Aborting remaining rows") break ar.info("%s for %s...", str(self.label), str(obj)) n += self.run_on_row(obj, ar) ar.set_response(refresh_all=True)
msg = _("%d row(s) have been updated.") % n ar.info(msg) #~ ar.success(msg,**kw)
"""The action used to delete the selected row(s). Automatically installed on every editable actor.
"""
# required_roles = set([SiteUser]) #~ callable_from = (GridEdit,ShowDetailAction) #~ needs_selection = True #~ url_action_name = 'delete' #~ client_side = True
objects = [] for obj in ar.selected_rows: objects.append(str(obj)) msg = ar.actor.disable_delete(obj, ar) if msg is not None: ar.error(None, msg, alert=True) return
def ok(ar2): super(DeleteSelected, self).run_from_ui(ar, **kw) ar2.success(record_deleted=True) if ar2.actor.detail_action: ar2.set_response( detail_handler_name=ar2.actor.detail_action.full_name())
d = dict(num=len(objects), targets=', '.join(objects)) if len(objects) == 1: d.update(type=ar.actor.model._meta.verbose_name) else: d.update(type=ar.actor.model._meta.verbose_name_plural) ar.confirm( ok, string_concat( _("You are about to delete %(num)d %(type)s:\n" "%(targets)s") % d, '\n', _("Are you sure ?")))
pre_ui_delete.send(sender=obj, request=ar.request) obj.delete() return 1
"""Decorator to define custom actions.
The decorated function will be installed as the actions's :meth:`run_from_ui <Action.run_from_ui>` method.
Same signature as :meth:`Action.__init__`. In practice you'll possibly use: :attr:`label <Action.label>`, :attr:`help_text <Action.help_text>` and :attr:`required_roles <lino.core.permissions.Permittable.required_roles>`.
""" # print 20140422, fn.__name__
obj = ar.selected_rows[0] return fn(obj, ar)
from lino.utils import jsgen if isinstance(e, Permittable) and not e.get_view_permission( jsgen._for_user_profile): return False # e.g. pcsw.ClientDetail has a tab "Other", visible only to system # admins but the "Other" contains a GridElement RolesByPerson # which is not per se reserved for system admins. js of normal # users should not try to call on_master_changed() on it parent = e.parent while parent is not None: if isinstance(parent, Permittable) and not parent.get_view_permission( jsgen._for_user_profile): return False # bug 3 (bcss_summary) blog/2012/0927 parent = parent.parent return True
|