Coverage for lino/core/store.py : 65%

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
# Copyright 2009-2016 Luc Saffre # License: BSD (see file COPYING for details)
During startup, Lino instantiates a "store" and its "fields" (aka "atomizers") for every table. These were used originally for dealing with Sencha ExtJS GridPanels and FormPanels, but the concept turned out useful for other features.
Some usages specific to Sencha ExtJS:
- for generating the JS code of the GridPanel definition - for generating an "atomized" JSON representation when rendering data for that GridPanel - for parsing the JSON sent by GridPanel and FormPanel
Other usages:
- remote fields (:class:`lino.core.fields.RemoteField`)
- render tables as text (:meth:`lino.core.renderer.TextRenderer.show_table` and :meth:`lino.core.tablerequest.TableRequest.row2text`)
"""
# import six # str = six.text_type
"""Base class for the fields of a :class:`Store`.
.. attribute:: field
The database field (a subclass of `django.db.models.fields.Field`)
.. attribute:: options
A `dict` with options to be used by :meth:`as_js`.
Note: `value_from_object` and `full_value_from_object` are similar, but for ForeignKeyStoreField and GenericForeignKeyField one returns the primary key while the other returns the full instance.
"""
"because checkboxes are not submitted when they are off"
"Necessary to compute :attr:`Store.pk_index`."
""" Return a Javascript string which defines this atomizer as an object. This is used by :mod:`lino.modlib.extjs.ext_renderer`. and in case of virtual remote fields they use the virtual field's delegate as_js method but with their own name.
""" self.options.update(name=name) return py2js(self.options)
return "%s '%s'" % (self.__class__.__name__, self.name)
#~ if not self.options.has_key('name'): #~ raise Exception("20130719 %s has no option 'name'" % self) #~ yield self.options['name'] yield self.name
"""
""" return self.full_value_from_object(obj, ar)
#~ def value2odt(self,ar,v,tc,**params): #~ """ #~ Add the necessary :term:`odfpy` element(s) to the containing element `tc`. #~ """ #~ params.update(text=force_text(v)) #~ tc.addElement(odf.text.P(**params))
#~ if v == '' and not self.field.empty_strings_allowed: #~ return None return self.field.to_python(v)
#~ logger.info("20130128 StoreField.extract_form_data %s",self.name) return post_data.get(self.name, None)
""" Test cases: - setting a CharField to '' - sales.Invoice.number may be blank """ v = self.extract_form_data(post_data) #~ logger.info("20130128 %s.form2obj() %s = %r",self.__class__.__name__,self.name,v) if v is None: # means that the field wasn't part of the submitted form. don't # touch it. return if v == '': #~ print 20130125, self.field.empty_strings_allowed, self.field.name, self.form2obj_default if self.field.empty_strings_allowed: v = self.parse_form_value(v, instance)
# If a field has been posted with empty string, we # don't want it to get the field's default value! # Otherwise checkboxes with default value True can # never be unset!
# Charfields have empty_strings_allowed (e.g. id field # may be empty) but don't do this for other cases. else: v = self.form2obj_default else: v = self.parse_form_value(v, instance) if not is_new and self.field.primary_key and instance.pk is not None: if instance.pk == v: return raise exceptions.ValidationError({ self.field.name: _( "Existing primary key value %r " "may not be modified.") % instance.pk})
return self.set_value_in_object(ar, instance, v)
old_value = self.value_from_object(instance, ar.request) #~ old_value = getattr(instance,self.field.attname) if old_value != v: setattr(instance, self.name, v) return True
""" Return a plain textual representation as a unicode string of the given value `v`. Note that `v` might be `None`. """
"""Common methods for :class:`ForeignKeyStoreField` and :class:`OneToOneField`.
""" #~ if self.field.rel is None: #~ return None
# here we don't want the pk (stored in field's attname) # but the full object this field refers to #~ logger.warning("%s get_rel_to returned None",self.field) return None except relto_model.DoesNotExist: return None
"""An atomizer for all kinds of fields which use a ComboBox."""
s = StoreField.as_js(self, name) #~ s += "," + repr(self.field.name+constants.CHOICES_HIDDEN_SUFFIX) s += ", '%s'" % (name + constants.CHOICES_HIDDEN_SUFFIX) return s
#~ yield self.options['name'] #~ yield self.options['name'] + constants.CHOICES_HIDDEN_SUFFIX yield self.name yield self.name + constants.CHOICES_HIDDEN_SUFFIX
#~ logger.info("20130128 ComboStoreField.extract_form_data %s",self.name) return post_data.get(self.name + constants.CHOICES_HIDDEN_SUFFIX, None)
#~ def obj2list(self,request,obj): value, text = self.get_value_text(v, row) l += [text, value]
#~ def obj2dict(self,request,obj,d):
#~ v = self.full_value_from_object(None,obj) if v is None or v == '': return (None, None) if obj is not None: ch = obj.__class__.get_chooser_for_field(self.field.name) if ch is not None: return (v, ch.get_text_for_value(v, obj)) for i in self.field.choices: if i[0] == v: # return (v, i[1].encode('utf8')) return (v, i[1]) return (v, _("%r (invalid choice)") % v)
"""An atomizer used for all ForeignKey fields.""" #~ def cell_html(self,req,row): #~ obj = self.full_value_from_object(req,row) #~ if obj is None: #~ return '' #~ return req.ui.obj2html(obj)
#~ v = self.full_value_from_object(None,obj) #~ if isinstance(v,basestring): #~ logger.info("20120109 %s -> %s -> %r",obj,self,v) else:
"""Convert the form field value (expected to contain a primary key) into the corresponding database object. If it is an invalid primary key, return None.
If this comes from a *learning* ExtJS ComboBox (i.e. :attr:`can_create_choice <lino.core.choosers.Chooser.can_create_choice>` is True) the value will be the text entered by the user. In that case, call :meth:`create_choice <lino.core.choosers.Chooser.create_choice>`.
""" relto_model = self.get_rel_to(obj) if not relto_model: #~ logger.info("20111209 get_value_text: no relto_model") return try: return relto_model.objects.get(pk=v) except ValueError: pass except relto_model.DoesNotExist: pass
if obj is not None: ch = obj.__class__.get_chooser_for_field(self.field.name) if ch and ch.can_create_choice: return ch.create_choice(obj, v) return None
#~ class LinkedForeignKeyField(ForeignKeyStoreField):
#~ def get_rel_to(self,obj): #~ ct = self.field.get_content_type(obj) #~ if ct is None: #~ return None #~ return ct.model_class()
#~ 20130130 self.value2num = delegate.value2num #~ 20130130 self.value2html = delegate.value2html #~ 20130130 self.format_sum = delegate.format_sum #~ 20130130 self.sum2html = delegate.sum2html #~ self.form2obj = delegate.form2obj # as long as http://code.djangoproject.com/ticket/15497 is open: #~ 20130130 self.apply_cell_format = delegate.apply_cell_format #~ self.value_from_object = vf.value_from_object
return '(virtual)' + self.delegate.__class__.__name__ + ' ' + self.name
# 20150218 : added new rule that virtual fields are never # computed for unsaved instances. This is because # `InsertRow.get_status` otherwise generated lots of useless # slave summaries which furthermore caused an endless # recursion problem. See test case in # :ref:`welfare.tested.pcsw`. Note that `obj` does not need to # be a database object. See # e.g. :doc:`/tutorials/vtables/index`. # if isinstance(obj, models.Model) and not obj.pk: return None
""" StoreField for :class:`lino.core.fields.RequestField`. """
#~ self.editable = False
return self.vf.value_from_object(obj, ar)
return l.append(self.format_value(ar, v))
d[self.name] = self.format_value(None, v) #~ d[self.options['name']] = self.format_value(ui,v) #~ d[self.field.name] = v
if v is None: return '' return str(v.get_total_count())
#~ def sum2html(self,ar,sums,i,**cellattrs): #~ cellattrs.update(align="right") #~ return super(RequestStoreField,self).sum2html(ar,sums,i,**cellattrs)
#~ def value2odt(self,ar,v,tc,**params): #~ params.update(text=self.format_value(ar,v)) #~ tc.addElement(odf.text.P(**params))
v = super(PasswordStoreField, self).value_from_object(obj, request) if v: return "*" * len(v) return v
pass
pass #~ raise NotImplementedError #~ return instance
"""See also blog entries 20100803, 20111003, 20120901
Note some special cases:
- :attr:`lino.modlib.vat.mixins.VatDocument.total_incl` (readonly virtual PriceField) must be disabled and may not get submitted. ExtJS requires us to set this dynamically each time.
- JobsOverview.body (a virtual HtmlBox) or Model.workflow_buttons (a displayfield) must *not* have the 'disabled' css class -
"""
f.vf.return_type, fields.DisplayField): #~ print "20121010 always disabled:", f
if name is not None: d[name] = True
d[name] = True
# disable the primary key field of a saved instance. Note that # pk might be set also on an unsaved instance and that
and self.store.pk is not None: # if self.store.pk.attname is None: # raise Exception('20130322b') # d[self.store.pk.attname] = True # # MTI children have an additional "primary key" for every # # parent: # pk = self.store.pk # while isinstance(pk, models.OneToOneField): # if pk.rel.field_name is None: # raise Exception('20130322c') # d[pk.rel.field_name] = True # pk = None
""" """
#~ class RecnoStoreField(SpecialStoreField): #~ name = 'recno' #~ def full_value_from_object(self,request,obj): #~ return
return ' '.join([ar.renderer.row_classes_map.get(s, '') for s in self.store.actor.get_row_classes(obj, ar)]) #~ return ar.renderer.row_classes_map.get('x-grid3-row-%s' % s
""" A field whose value is the result of the `get_row_permission` method on that row. New feature since `/blog/2011/0830` """
#~ print 20120601, self.store.actor, "update_action is None" return True # disable editing if there's no update_action obj, ar, actor.get_row_state(obj), actor.update_action) # if str(actor).startswith('aids.'): # logger.info("20141128 store.py %s %s value=%s", # actor, actor.update_action, v)
"""A :class:`StoreField` for `BooleanField <https://docs.djangoproject.com/en/dev/ref/models/fields/#booleanfield>`__.
"""
kw['type'] = 'boolean' StoreField.__init__(self, field, name, **kw) if not field.editable: def full_value_from_object(self, obj, ar): #~ return self.value2html(ar,self.field.value_from_object(obj)) return self.format_value(ar, self.field.value_from_object(obj)) self.full_value_from_object = curry(full_value_from_object, self)
""" Ext.ensible CalendarPanel sends boolean values as """ return constants.parse_boolean(v)
return force_text(iif(v, _("Yes"), _("No")))
v = getattr(obj, self.name, None) #~ logger.info("20130611 full_value_from_object() %s",v) if v is None: return '' if ar is None: return str(v) if ar.renderer is None: return str(v) return ar.obj2html(v)
#~ def __init__(self,field,name,**kw): #~ kw['type'] = 'float' #~ StoreField.__init__(self,field,name,**kw)
return parse_decimal(v)
#~ def value2num(self,v): # ~ # print "20120426 %s value2num(%s)" % (self,v) #~ return v
if not v: return '' return settings.SITE.decfmt(v, places=self.field.decimal_places)
#~ def value2html(self,ar,v,**cellattrs): #~ cellattrs.update(align="right") #~ return E.td(self.format_value(ar,v),**cellattrs)
"""A :class:`StoreField` for `AutoField <https://docs.djangoproject.com/en/dev/ref/models/fields/#autofield>`__
"""
#~ logger.info("20121022 AutoStoreField.form2obj(%r)",ar.bound_action.full_name()) if isinstance(ar.bound_action.action, actions.InsertRow): return super(AutoStoreField, self).form2obj( ar, obj, post_data, is_new)
# date_format # 'Y-m-d'
if v: v = datetime.date(*settings.SITE.parse_date(v)) else: v = None return v
"""Return a plain textual representation of this value as a unicode string.
""" return fds(v)
if v: v = IncompleteDate(*settings.SITE.parse_date(v)) #~ v = datetime.date(*settings.SITE.parse_date(v)) return v
if v: return settings.SITE.parse_datetime(v) return None
if v: return settings.SITE.parse_time(v) return None
ff = self.field.value_from_object(obj) return ff.name
"Deprecated. See `/blog/2012/0327`."
unbound_meth = self.field._return_type_for_method assert unbound_meth.__code__.co_argcount >= 2, (self.name, unbound_meth.__code__.co_varnames) #~ print self.field.name return unbound_meth(obj, request)
unbound_meth = self.field._return_type_for_method assert unbound_meth.__code__.co_argcount >= 2, (self.name, unbound_meth.__code__.co_varnames) #~ print self.field.name return unbound_meth(obj, request)
#~ def obj2list(self,request,obj): #~ return [self.value_from_object(request,obj)]
#~ def obj2dict(self,request,obj,d): # logger.debug('MethodStoreField.obj2dict() %s',self.field.name) #~ d[self.field.name] = self.value_from_object(request,obj)
#~ def get_from_form(self,instance,post_data): #~ pass
pass #~ return instance #raise Exception("Cannot update a virtual field")
#~ class ComputedColumnField(StoreField):
#~ def value_from_object(self,ar,obj): #~ m = self.field.func # ~ # assert m.func_code.co_argcount >= 2, (self.field.name, m.func_code.co_varnames) # ~ # print self.field.name #~ return m(obj,ar)[0]
#~ def form2obj(self,request,instance,post_data,is_new): #~ pass
#~ class SlaveSummaryField(MethodStoreField): #~ def obj2dict(self,request,obj,d): #~ meth = getattr(obj,self.field.name) # ~ #logger.debug('MethodStoreField.obj2dict() %s',self.field.name) #~ d[self.field.name] = self.slave_report.()
v = self.full_value_from_object(obj, request) #~ try: #~ v = getattr(obj,self.field.name) #~ except self.field.rel.model.DoesNotExist,e: #~ v = None if v is None: return None return v.pk
#~ def obj2list(self,request,obj): #~ return [self.value_from_object(request,obj)]
#~ def obj2dict(self,request,obj,d): #~ d[self.field.name] = self.value_from_object(request,obj)
"""Return the atomizer for this database field. The atomizer is an instance of a subclass of :class:`StoreField`.
"""
""" Hack: we create a StoreField based on the remote field, then modify its behaviour. """ sf = create_atomizer(model, fld.field, fld.name)
def value_from_object(sf, obj, ar): m = fld.func return m(obj, ar)
def full_value_from_object(sf, obj, ar): m = fld.func v = m(obj, ar) return v
sf.value_from_object = curry(value_from_object, sf) sf.full_value_from_object = curry(full_value_from_object, sf) return sf # uh, this is tricky... return MethodStoreField(fld, name)
return sf_class(fld, name)
# e.g. VirtualField with DummyField as return_type return None # raise Exception("No atomizer for %s %s %s" % ( # model, fld.return_type, fld.name)) return FileFieldStoreField(fld, name) return StoreField(fld, name) return OneToOneStoreField(fld, name)
return GenericForeignKeyField(fld, name) return ComboStoreField(fld, name)
return TimeStoreField(fld, name) return IncompleteDateStoreField(fld, name) return BooleanStoreField(fld, name) return DecimalStoreField(fld, name) #~ kw.update(type='int') return IntegerStoreField(fld, name) else:
# instantiated in `lino.core.layouts`
return "%s of %s" % ( self.__class__.__name__, self.params_layout_handle)
data = getrqdata(request) pv = data.getlist(self.url_param) #~ logger.info("20120221 ParameterStore.parse_params(%s) --> %s",self.url_param,pv)
def parse(sf, form_value): if form_value == '' and not sf.field.empty_strings_allowed: return sf.form2obj_default # When a field has been posted with empty string, we # don't want it to get the field's default value # because otherwise checkboxes with default value True # can never be unset. charfields have # empty_strings_allowed e.g. id field may be empty. # But don't do this for other cases. else: return sf.parse_form_value(form_value, None)
if len(pv) > 0: if len(self.param_fields) != len(pv): raise Exception( "%s expects a list of %d values but got %d: %s" % ( self, len(self.param_fields), len(pv), pv)) for i, f in enumerate(self.param_fields): kw[f.field.name] = parse(f, pv[i]) return kw
"""A Store is the collection of StoreFields for a given actor.
"""
# temporary dict used by collect_fields and add_field_for
""" Django's Field.__cmp__() does::
return cmp(self.creation_counter, other.creation_counter)
which causes an exception when trying to compare a field with an object of other type. """ and fld.field == self.pk: #~ self.pk = fld.field raise Exception("Primary key %s not found in list_fields %s" % (self.pk, self.list_fields))
#~ if not issubclass(rh.report,dbtables.Table): #~ addfield(RecnoStoreField(self))
addfield(RowClassStoreField(self))
# virtual fields must come last so that Store.form2obj() # processes "real" fields first. f for f in self.all_fields if not isinstance(f, VirtStoreField) ] + [ f for f in self.all_fields if isinstance(f, VirtStoreField) ]
"""`fields` is a pointer to either `self.detail_fields` or `self.list_fields`. Each of these must contain a primary key field.
"""
#~ sf = self.df2sf.get(df,None) #~ if sf is None: #~ sf = self.create_atomizer(df,df.name) #~ self.all_fields.append(sf) #~ self.df2sf[df] = sf
"""Store the `form_values` into the `instance` by calling :meth:`form2obj` for every store field.
""" disabled_fields = set(self.actor.disabled_fields(instance, ar)) changed_triggers = [] for f in self.all_fields: if f.name not in disabled_fields: try: if f.form2obj(ar, instance, form_values, is_new): m = getattr(instance, f.name + "_changed", None) if m is not None: changed_triggers.append(m) except exceptions.ValidationError as e: # logger.warning("20150127 store (field %s) : %s", # f.name, e) raise exceptions.ValidationError({f.name: e.messages}) except ValueError as e: # logger.warning("20150127 store (field %s) : %s", # f.name, e) raise exceptions.ValidationError( {f.name: _("Invalid value for this field (%s).") % e}) except Exception as e: logger.warning( "Exception during Store.form2obj (field %s) : %s", f.name, e) logger.exception(e) raise # logger.info("20120228 Store.form2obj %s -> %s", # f, dd.obj2str(instance)) for m in changed_triggers: m(ar)
l = [] for fld in self.list_fields: l += fld.column_names() return l
""" Used to set `disabled_actions_index`. Was used to write definition of Ext.ensible.cal.CalendarMappings and Ext.ensible.cal.EventMappings """ #~ logger.info("20111214 column_names: %s",list(self.column_names())) return list(self.column_names()).index(name)
#~ assert isinstance(request,dbtables.AbstractTableRequest) #~ if not isinstance(request,dbtables.ListActionRequest): #~ raise Exception() #~ logger.info("20120107 Store %s row2list(%s)", self.report.model, dd.obj2str(row)) #~ elif isinstance(row,actions.VirtualRow): #~ for fld in self.list_fields: #~ fld.value2list(request,None,l,row) else: #~ logger.info("20130611 Store row2list() --> %r", l)
#~ assert isinstance(ar,dbtables.AbstractTableRequest) #~ logger.info("20111209 Store.row2dict(%s)", dd.obj2str(row)) # logger.info("20140429 Store.row2dict %s", fld) # logger.info("20140429 Store.row2dict %s -> %s", fld, v)
#~ def row2odt(self,request,fields,row,sums): #~ for i,fld in enumerate(fields): #~ if fld.field is not None: #~ v = fld.full_value_from_object(request,row) #~ if v is None: #~ yield '' #~ else: #~ sums[i] += fld.value2num(v) #~ yield fld.value2odt(request,v) |