Coverage for lino/core/kernel.py : 61%

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)
# import six # str = six.text_type
"""This defines the :class:`Kernel` class.
The "kernel" of a Lino site is (like `SITE` itself) a "de facto singleton", available to application code as ``SITE.kernel`` (and its alias for backwards compatibility: ``SITE.ui``).
The kernel is instantiated at the end of the startup process, when the :settings`SITE` has been instantiated and models have been loaded. It encapsulates a bunch of functionality which becomes available only then.
TODO: Rename "kernel" to something else. Because "kernel" suggests something which is loaded *in first place*. That's not true for Lino's "kernel".
"""
pre_analyze, post_analyze) # from .utils import format_request
"""If the verbose_name of a ForeignKey was not set by user code, Django sets it to ``field.name.replace('_', ' ')``. We replace this default value by ``f.rel.model._meta.verbose_name``. This rule holds also for virtual FK fields.
"""
bound_action create_kw known_values param_values action_param_values""".split())
#~ def __init__(self,name,label,func):
self.name = name #~ self.index = index self.func = func self.label = label
"""A callback is a question that rose during an AJAX action. The original action is pending until we get a request that answers the question.
TODO: move all callback-related code out of :mod:`lino.core.kernel` into to a separate module and install it as a "kernel plugin" in a similar way as :mod:`lino.core.web` and :mod:`lino.utils.config`.
"""
self.message = message self.choices = [] self.choices_dict = {} self.ar = ar
return "Callback(%r)" % self.message
self.title = title
""" Add a possible answer to this callback. - name: "yes", "no", "ok" or "cancel" - func: a callable to be executed when user selects this choice - the label of the button """ assert not name in self.choices_dict allowed_names = ("yes", "no", "ok", "cancel") if not name in allowed_names: raise Exception("Sorry, name must be one of %s" % allowed_names) cbc = CallbackChoice(name, func, label) self.choices.append(cbc) self.choices_dict[name] = cbc return cbc
"""This is the class of the object stored in :attr:`Site.kernel <lino.core.site.Site.kernel>`.
"""
# _singleton_instance = None
# @classmethod # def instance(cls, site): # if cls._singleton_instance is None: # cls._singleton_instance = cls(site) # elif cls._singleton_instance.site is not site: # site.logger().info("Overriding SITE instance") # cls._singleton_instance.site = site # return cls._singleton_instance
# logger.info("20140227 Kernel.__init__() a")
# from importlib import import_module # # For every plugin, Lino checks whether the package contains a # # module named `ui` and, if yes, imports this module. The # # benefit of this is that all "Lino extensions" to the models # # can be moved out of :xfile:`models.py` into a separate file # # :xfile:`ui.py`. # # print '\n'.join([p.app_name for p in self.installed_plugins]) # for p in site.installed_plugins: # # fn = dirname(inspect.getfile(p.app_module)) # # fn = join(fn, 'ui.py') # try: # x = p.app_name + '.ui' # import_module(x) # logger.info("20150416 imported %s", x) # except Exception as e: # # except ImportError as e: # if str(e) != "No module named ui": # logger.warning("Failed to import %s : %s", x, e) # # raise Exception("Failed to import %s : %s" % (x, e))
# We set `code_mtime` only after kernel_startup() because # codetime watches only those modules which are already # imported.
url_text = s.split(None, 1) if len(url_text) == 1: url = text = url_text[0] else: url, text = url_text return '<a href="%s">%s</a>' % (url, text)
site.build_js_cache_on_startup = not ( settings.DEBUG or is_devserver())
# web.site_setup(site)
# site._welcome_actors.append(a) site.add_welcome_handler(a.get_welcome_messages)
for n in constants.URL_PARAMS]
raise Exception("Duplicate reserved name %r" % n)
raise Exception( "Invalid value %r for `default_ui` (must be one of %s)" % (self.site.default_ui, list(self.site.plugins.keys()))) else: for p in self.site.installed_plugins: if p.ui_handle_attr_name is not None: ui = p break # if ui is None: # raise Exception("No user interface in {0}".format( # [u.app_name for u in self.site.installed_plugins]))
# trigger creation of params_layout.params_store self.default_ui) # logger.info("20140227 Kernel.__init__() done")
"""This is a part of a Lino site startup. The Django Model definitions are done, now Lino analyzes them and does certain actions:
- Verify that there are no more pending injects Install a :class:`DisableDeleteHandler <lino.core.ddh.DisableDeleteHandler>` for each Model into `_lino_ddh`.
- Install :class:`lino.core.model.Model` attributes and methods into Models that don't inherit from it.
""" process_name = 'WSGI' else:
process_name, settings.SETTINGS_MODULE, os.getpid()) # puts(self.welcome_text())
# this also triggers django.db.models.loading.cache._populate()
# app_name_model is the full installed app module name + # the model name. It certainly contains at least one dot. m = '.'.join(app_name_model.split('.')[-2:]) resolve_model( m, strict="%s plugin tries to extend unresolved model '%%s'" % p.__class__.__module__)
#~ print 20130216, model #~ fix_field_cache(model)
# if hasattr(model, '_lino_ddh'): raise Exception("20150831 %s", model)
fields.fields_list(model, model.hidden_columns))
fields.fields_list(model, model.active_fields))
model.allow_cascaded_delete = frozenset( fields.fields_list(model, model.allow_cascaded_delete))
# Attention when inheriting this from from parent model. # qsf = model.__dict__.get('quick_search_fields', None) fields.fields_list(model, model.quick_search_fields)) else: raise ChangedAPI( "{0}.quick_search_fields must be None or a string " "of space-separated field names (not {1})".format( model, qsf))
raise Exception("Tiens?")
# self.modules.define(model._meta.app_label, model.__name__, model)
# vip_classes = (layouts.BaseLayout, fields.Dummy) # for a in models.get_apps(): # app_label = a.__name__.split('.')[-2]
# for k, v in a.__dict__.items(): # if isinstance(v, type) and issubclass(v, vip_classes): # self.modules.define(app_label, k, v)
# if k.startswith('setup_'): # self.modules.define(app_label, k, v)
continue # automatic intermediate models created by # ManyToManyField should not disable delete # for f, m in model._meta.get_fields_with_model():
# Refuse nullable CharFields, but don't trigger on # NullableCharField (which is a subclass of CharField).
msg = "Nullable CharField %s in %s" % (f.name, model) raise Exception(msg) raise Exception("Could not resolve target %r of " "ForeignKey '%s' in %s " "(models are %s)" % (f.rel.model, f.name, model, models_list))
""" If JobProvider is an MTI child of Company, then mti.delete_child(JobProvider) must not fail on a JobProvider being referred only by objects that can refer to a Company as well. """ raise Exception("20150824") # f.rel.model._lino_ddh.add_fk(f.model, f) # m = f.model._meta.concrete_model # f.rel.model._lino_ddh.add_fk(m, f)
# logger.info("20150429 Gonna send pre_analyze signal") # logger.info("20150429 pre_analyze signal done") # MergeActions are defined in pre_analyze. # And MergeAction needs the info in _lino_ddh to correctly find # keep_volatiles
"""Virtual fields declared on the model must have been attached before calling Model.site_setup(), e.g. because pcsw.Person.site_setup() declares `is_client` as imported field.
"""
#~ logger.info("20130817 attached model vfs")
#~ choosers.discover()
logger.info("Languages: %s. %d apps, %d models, %s actors.", ', '.join([li.django_code for li in self.languages]), len(self.modules), len(models_list), len(actors.actors_list))
#~ logger.info(settings.INSTALLED_APPS)
# Actor.after_site_setup() is called after the apps' # site_setup(). Example: pcsw.site_setup() adds a detail to # properties.Properties, the base class for # properties.PropsByGroup. The latter would not install a # `detail_action` during her after_site_setup() and also would # never get it later.
#~ self.on_site_startup()
#~ logger.info("20130827 startup_site done")
"""Change `on_delete` from CASCADE (Django's default value) to PROTECT for foreignkeys that need to be protected.
Basically we protect all FK fields that are not listed in their model's :attr:`allow_cascaded_delete <lino.core.model.Model.allow_cascaded_delete>`. With one exception: pointers to the MTI parent of a :class:`Polymorphic <lino.mixins.polymorphic.Polymorphic>` must not become protected (because Lino handles it automatically, see :meth:`lino.mixins.polymorphic.Polymorphic.disable_delete`).
"""
# Whether the given foreign key fk # if issubclass(m, model) or issubclass(model, m): # they have an MTI relation
"Setting {0}.{1}.on_delete to PROTECT because " "field is not specified in " "allow_cascaded_delete.").format(fmn(m), fk.name) else: msg = ("{0}.{1} specified in allow_cascaded_delete " "but on_delete is not CASCADE").format( fmn(m), fk.name) raise Exception(msg)
if not fk.null: msg = ("{0}.{1} has on_delete SET_NULL but " "is not nullable ") msg = msg.format(fmn(m), fk.name, fk.rel.model) raise Exception(msg)
else: fmn(m), fk.name, fk.rel.on_delete)
"""Yield a series of `(gfk, fk_field, queryset)` tuples which together will return all database objects for which the given GenericForeignKey gfk points to the object `obj`. See also :doc:`/dev/gfks`.
""" return # e.g. if contenttypes is not installed
# raise Exception("20150330 %s", obj._meta.pk) return # e.g. Country.iso_code is a CharField, cannot # point to a country using GFK # logger.info("20150330 ok %s", obj_ct) # fk_field, remote_model, direct, m2m = \ # gfk.model._meta.get_field_by_name(gfk.fk_field)
"""Yield all database objects of this model which have some broken GFK field.
This is a slow query which does an additional database request for each row. (Is there a possibility to do this in a single SQL query?)
Each yeld object has two special attributes:
- `_message` : a textual description of the problem - `_todo` : 'delete', 'clear' or 'manual'
Note: the "clear" action should not run automatically, at least not for :mod:`lino.modlib.changes`.
See also :ref:`lino.tutorial.watch`.
""" gfks = [f for f in self.GFK_LIST if f.model is model] if len(gfks): for gfk in gfks: fk_field = gfk.model._meta.get_field(gfk.fk_field) # fk_field, remote_model, direct, m2m = \ # gfk.model._meta.get_field_by_name(gfk.fk_field) kw = {gfk.ct_field+'__isnull': False} qs = model.objects.filter(**kw) for obj in qs: fk = getattr(obj, gfk.fk_field) ct = getattr(obj, gfk.ct_field) pointed_model = ct.model_class() # pointed_model = ContentType.objects.get_for_id(ct) try: pointed_model.objects.get(pk=fk) except pointed_model.DoesNotExist: msg = "Invalid primary key {1} for {2} in `{0}`" obj._message = msg.format( gfk.fk_field, fk, fmn(pointed_model)) if gfk.name in model.allow_cascaded_delete: obj._todo = 'delete' elif fk_field.null: obj._todo = 'clear' else: obj._todo = 'manual' yield obj
return self.success(_("User abandoned"))
pass
"""Continue the action which was started in a previous request and which asked for user interaction via a :class:`Callback`.
This is called from `lino.core.views.Callbacks`.
""" # logger.info("20131212 get_callback %s %s", thread_id, button_id)
# 20140304 Also set a renderer so that callbacks can use it # (feature needed by beid.FindByBeIdAction).
thread_id = int(thread_id) cb = self.pending_threads.pop(thread_id, None) if cb is None: ar = ActorRequest(request, renderer=self.default_renderer) logger.debug("No callback %r in %r" % ( thread_id, list(self.pending_threads.keys()))) ar.error("Unknown callback %r" % thread_id) return self.render_action_response(ar)
# e.g. SubmitInsertClient must set `data_record` in the # callback request ("ar2"), not the original request ("ar"), # i.e. the methods to create an instance and to fill # `data_record` must run on the callback request. So the # callback request must be a clone of the original request. # New since 20140421 ar = cb.ar.actor.request_from(cb.ar) for k in CLONEABLE_ATTRS: setattr(ar, k, getattr(cb.ar, k))
for c in cb.choices: if c.name == button_id: a = ar.bound_action.action if self.site.log_each_action_request and not a.readonly: logger.info("run_callback {0} {1} {2}".format( thread_id, cb.message, c.name)) c.func(ar) return self.render_action_response(ar)
ar.error("Invalid button %r for callback" % (button_id, thread_id)) return self.render_action_response(ar)
"""Returns an "action callback" which will initiate a dialog thread by asking a question to the user and suspending execution until the user's answer arrives in a next HTTP request.
Calling this from an Action's :meth:`run_from_ui <lino.core.actions.Action.run_from_ui>` method will interrupt the execution, send the specified message back to the user, adding the executables `yes` and optionally `no` to a queue of pending "dialog threads".
The client will display the prompt and will continue this thread by requesting :class:`lino.modlib.extjs3.views.Callbacks`.
""" if len(msgs) > 1: msg = '\n'.join([force_text(s) for s in msgs]) else: msg = msgs[0]
return Callback(ar, msg)
""" """ h = hash(cb) self.pending_threads[h] = cb # logger.info("20131212 Stored %r in %r" % ( # h, self.pending_threads))
buttons = dict() for c in cb.choices: buttons[c.name] = c.label
ar.success( cb.message, xcallback=dict( id=h, title=cb.title, buttons=buttons))
"""Run the action, catching some exceptions in order to report them in a user-friendly way.
"""
a = ar.bound_action.action if self.site.log_each_action_request and not a.readonly: flds = [] A = flds.append a = ar.bound_action.action # A(a.__class__.__module__+'.'+a.__class__.__name__) A(ar.get_user().username) A(ar.bound_action.full_name()) A(obj2str(ar.master_instance)) A(obj2str(ar.selected_rows)) # A(format_request(ar.request)) logger.info("run_action {0}".format(' '.join(flds))) # logger.info("run_action {0}".format(ar)) try: a.run_from_ui(ar) if a.parameters and not a.no_params_window: ar.set_response(close_window=True) except exceptions.ValidationError as e: # logger.info("20150127 run_action %r", e) ar.error(ar.ah.actor.error2str(e), alert=True) except Warning as e: ar.error(str(e), alert=True)
return self.render_action_response(ar)
""" ar is usually None, except for actors with dynamic handle """ #~ logger.info('20121010 ExtUI.setup_handle() %s',h.actor)
return
h.actor.get_column_names(ar), h.actor, hidden_elements=he) else: h.list_layout = None
"""Builds a JSON response from response information stored in given ActionRequest.
""" return views.json_response(ar.response, ar.content_type)
""" See :meth:`ExtRenderer.row_action_button` """ return self.default_renderer.row_action_button(*args, **kw)
self._must_build = True
"""Make the specified cache file. This is used internally at server startup.
""" # cachedir = self.site.cache_dir.child('static', 'cache', 'js') # if not exists(settings.STATIC_ROOT): # logger.info("STATIC_ROOT does not exist: %s", settings.STATIC_ROOT) # return 0 fn = join(settings.MEDIA_ROOT, fn) # fn = join(settings.STATIC_ROOT, fn) # fn = join(self.site.cache_dir, fn) if not force and not self._must_build and exists(fn): mtime = os.stat(fn).st_mtime if mtime > self.code_mtime: logger.debug("%s (%s) is up to date.", fn, time.ctime(mtime)) return 0
logger.info("Building %s ...", fn) self.site.makedirs_if_missing(dirname(fn)) f = codecs.open(fn, 'w', encoding='utf-8') try: write(f) f.close() return 1 except Exception: f.close() if not self.site.keep_erroneous_cache_files: # os.remove(fn) raise #~ logger.info("Wrote %s ...", fn)
# def setup_static_link(self, urlpatterns, short_name, # attr_name=None, source=None): |