## Copyright 2009-2013 Luc Saffre
## This file is part of the Lino project.
## Lino is free software; you can redistribute it and/or modify 
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 3 of the License, or
## (at your option) any later version.
## Lino is distributed in the hope that it will be useful, 
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
## GNU General Public License for more details.
## You should have received a copy of the GNU General Public License
## along with Lino; if not, see <http://www.gnu.org/licenses/>.
"""

Defines the `Store` class and its fields 

"""

import logging
logger = logging.getLogger(__name__)

#~ import odf

import cgi
import datetime
#~ import decimal
#~ from dateutil import parser as dateparser

from django.conf import settings
from django.db import models
from django.db.models.fields import NOT_PROVIDED
from django.core import exceptions
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import force_unicode
from django.contrib.contenttypes import generic

from lino.utils import jsgen 
from lino.utils.jsgen import py2js, Component, id2js, js_code
from lino.utils.quantities import parse_decimal
#~ from lino.utils.xmlgen import etree
#~ from lino.utils.xmlgen.html import E

#~ from . import ext_requests
#~ from lino.ui import requests as ext_requests
from lino.core import constants as ext_requests

import lino
from lino.core import dbtables
from lino.core import fields
from lino.core import actions
from lino.core import frames
from lino import dd
#~ from lino.modlib.properties import models as properties
from lino.utils import choosers
from lino.utils import curry
from lino.utils import iif
from lino.core.requests import PhantomRow
from lino.utils import IncompleteDate
#~ from lino.core import tables
#~ from lino.utils import moneyfmt
from lino.mixins.printable import decfmt

from lino.utils.xmlgen import html as xghtml

class StoreField(object):
    """
    Base class for the fields of a :class:`Store`.
    
    Note: `value_from_object` and `full_value_from_object` are similar, 
    but for ForeignKeyStoreField one returns the primary key while the 
    other returns the full instance.
    
    """
    
    
    form2obj_default = None
    "because checkboxes are not submitted when they are off"
    
    list_values_count = 1
    "Necessary to compute :attr:`Store.pk_index`."
    
    
    def __init__(self,field,name,**options):
        self.field = field
        self.name = name
        #~ if isinstance(field,generic.GenericForeignKey):
            #~ self.editable = False
        #~ else:
            #~ self.editable = field.editable # VirtualField changes this
        #~ options.update(name=name or field.name)
        self.options = options
        
    #~ def __repr__(self):
        #~ return self.__class__.__name__ + ' ' + self.field.name
        
    def as_js(self,name):
        """
        possible side effect. but self.options is used only for as_js(),
        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)
        
    def __repr__(self):
        return "%s '%s'" % (self.__class__.__name__, self.name)
        
    def column_names(self):
        yield self.options['name']
        
    def value_from_object(self,obj,ar):
        return self.full_value_from_object(obj,ar)
        
    def full_value_from_object(self,obj,ar):
        return self.field.value_from_object(obj)
    
        
    def value2list(self,ar,v,l,row):
        return l.append(v)
        
    def value2dict(self,ui,v,d,row):
        d[self.name] = v

    #~ def value2odt(self,ar,v,tc,**params):
        #~ """
        #~ Add the necessary :term:`odfpy` element(s) to the containing element `tc`.
        #~ """
        #~ params.update(text=force_unicode(v))
        #~ tc.addElement(odf.text.P(**params))
      
    def parse_form_value(self,v,obj):
        #~ if v == '' and not self.field.empty_strings_allowed:
            #~ return None
        return self.field.to_python(v)
        
    def extract_form_data(self,post_data):
        #~ logger.info("20130128 StoreField.extract_form_data %s",self.name)
        return post_data.get(self.name,None)
        #~ v = post_data.get(self.field.name,self.form2obj_default)
        #~ if v is None:
            #~ # means that the field wasn't part of the submitted form. don't touch it.
            #~ # except for checkboxes (who unfortunately are not submitted when clear)
            #~ # whose form2obj_default is False instead of None
            #~ return
    #~ v = post_data.get(self.field.name,NOT_PROVIDED)
    #~ if v is NOT_PROVIDED:
        #~ return
        
    def form2obj(self,ar,instance,post_data,is_new):
        """
        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)
        
    def set_value_in_object(self,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
            
    def format_value(self,ar,v):
        """
        Return a plain textual representation of this value as a unicode string.
        """
        return force_unicode(v)
        

#~ class RemoteStoreField(StoreField):
    #~ def __init__(self,store,rf):
        #~ self.remote_field = rf
        #~ self.delegate = store.create_atomizer(rf.field)
        #~ StoreField.__init__
      
      
class RelatedMixin(object):
        
    def get_rel_to(self,obj):
        #~ if self.field.rel is None:
            #~ return None
        return self.field.rel.to
        
    def full_value_from_object(self,obj,ar):
        # here we don't want the pk (stored in field's attname) 
        # but the full object this field refers to
        relto_model = self.get_rel_to(obj)
        if not relto_model:
            logger.warning("%s get_rel_to returned None",self.field)
            return None
        try:
            return getattr(obj,self.name)
        except relto_model.DoesNotExist,e:
            return None
        


class ComboStoreField(StoreField):
  
    list_values_count = 2
    
    def as_js(self,name):
        s = StoreField.as_js(self,name)
        #~ s += "," + repr(self.field.name+ext_requests.CHOICES_HIDDEN_SUFFIX)
        s += ", '%s'" % (name+ext_requests.CHOICES_HIDDEN_SUFFIX)
        return s 
        
    def column_names(self):
        #~ yield self.options['name']
        #~ yield self.options['name'] + ext_requests.CHOICES_HIDDEN_SUFFIX
        yield self.name
        yield self.name + ext_requests.CHOICES_HIDDEN_SUFFIX
        
    def extract_form_data(self,post_data):
        #~ logger.info("20130128 ComboStoreField.extract_form_data %s",self.name)
        return post_data.get(self.name+ext_requests.CHOICES_HIDDEN_SUFFIX,None)
        
    #~ def obj2list(self,request,obj):
    def value2list(self,ar,v,l,row):
        value,text = self.get_value_text(v,row)
        l += [text,value]
        
    #~ def obj2dict(self,request,obj,d):
    def value2dict(self,request,v,d,row):
        value,text = self.get_value_text(v,row)
        d[self.name] = text
        d[self.name + ext_requests.CHOICES_HIDDEN_SUFFIX] = value
        
    def get_value_text(self,v,obj):
        #~ v = self.full_value_from_object(None,obj)
        if v is None or v == '':
            return (None, None)
        ch = choosers.get_for_field(self.field) 
        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, unicode(i[1]))
        return (v, _("%r (invalid choice)") % v)
        
class ForeignKeyStoreField(RelatedMixin,ComboStoreField):
        
    #~ def cell_html(self,req,row):
        #~ obj = self.full_value_from_object(req,row)
        #~ if obj is None:
            #~ return ''
        #~ return req.ui.href_to(obj)
        
        
    def get_value_text(self,v,obj):
        #~ v = self.full_value_from_object(None,obj)
        #~ if isinstance(v,basestring):
            #~ logger.info("20120109 %s -> %s -> %r",obj,self,v)
        if v is None:
            return (None, None)
        else:
            return (v.pk, unicode(v))
            
    def parse_form_value(self,v,obj):
        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,e:
            pass
        except relto_model.DoesNotExist,e:
            pass
            
        ch = choosers.get_for_field(self.field)
        #~ if ch and ch.meth.quick_insert_field:
        if ch and ch.can_create_choice:
            return ch.create_choice(obj,v)
            #~ if o is not None:
                #~ o.full_clean()
                #~ o.save()
                #~ logger.info(u"Auto-created %s %s",
                    #~ unicode(o._meta.verbose_name),unicode(o))
                #~ return o
        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()
        


class VirtStoreField(StoreField):
  
    def __init__(self,vf,delegate,name):
        self.vf = vf
        StoreField.__init__(self,vf.return_type,name)
        self.as_js = delegate.as_js
        self.column_names = delegate.column_names
        self.list_values_count = delegate.list_values_count
        self.form2obj_default = delegate.form2obj_default
        #~ 20130130 self.value2num = delegate.value2num
        #~ 20130130 self.value2html = delegate.value2html
        self.value2list = delegate.value2list
        self.value2dict = delegate.value2dict
        self.format_value = delegate.format_value
        #~ 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:
        self.parse_form_value = delegate.parse_form_value
        self.set_value_in_object = vf.set_value_in_object
        #~ 20130130 self.apply_cell_format = delegate.apply_cell_format
        #~ self.value_from_object = vf.value_from_object
        
        self.delegate = delegate

    def __repr__(self):
        return '(virtual)' + self.delegate.__class__.__name__ + ' ' + self.name
        
    def full_value_from_object(self,obj,ar):
        return self.vf.value_from_object(obj,ar)

        
class RequestStoreField(StoreField):
    """
    StoreField for :class:`lino.core.fields.RequestField`.
    """
    def __init__(self,vf,delegate,name):
        self.vf = vf 
        StoreField.__init__(self,vf.return_type,name)
        #~ self.editable = False
        self.as_js = delegate.as_js
        self.column_names = delegate.column_names
        self.list_values_count = delegate.list_values_count
        
    def full_value_from_object(self,obj,ar):
        return self.vf.value_from_object(obj,ar)

    def value2list(self,ar,v,l,row):
        return l.append(self.format_value(ar,v))
        
    def value2dict(self,ar,v,d,row):
        d[self.name] = self.format_value(ar,v)
        #~ d[self.options['name']] = self.format_value(ui,v)
        #~ d[self.field.name] = v

    def format_value(self,ar,v):
        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))





class PasswordStoreField(StoreField):
  
    def value_from_object(self,obj,request):
        v = super(PasswordStoreField,self).value_from_object(obj,request)
        if v:
            return "*" * len(v)
        return v
        
class GenericForeignKeyField(StoreField):
        
    def full_value_from_object(self,obj,ar):
        return getattr(obj,self.name,None)
        #~ owner = getattr(obj,self.name)
        #~ if owner is None: 
            #~ # owner_id = getattr(obj,self.field.fk_field)
            #~ # if owner_id is None:
                #~ # return ''
            #~ return ''
        #~ return ar.href_to(owner)
  
class SpecialStoreField(StoreField):
    field = None 
    name = None
    editable = False
  
    def __init__(self,store):
        self.options = dict(name=self.name)
        self.store = store
        
    #~ def value2dict(self,ui,v,d):
        #~ d[self.name] = v

    #~ def obj2dict(self,request,obj,d):
        #~ # d.update(disable_editing=self.value_from_object(request,obj))
        #~ d[self.name] = self.value_from_object(request,obj)

    #~ def __repr__(self):
        #~ return "%s '%s'" % (self.__class__.__name__, self.name)
        
    def parse_form_value(self,v,instance):
        pass
        
    #~ def obj2list(self,request,obj):
        #~ return [self.value_from_object(request,obj)]
      
    #~ def value2list(self,ui,v):
        #~ return [v]
      
    def form2obj(self,ar,instance,post_data,is_new):
        pass
        #~ raise NotImplementedError
        #~ return instance

class DisabledFieldsStoreField(SpecialStoreField):
    """
    See also blog entries 20100803, 20111003, 20120901
    
    Note some special cases:
    
    - vat.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
    - 
    """
    name = 'disabled_fields'
    
    def __init__(self,store):
        SpecialStoreField.__init__(self,store)
        self.disabled_fields = set()
        for f in self.store.all_fields:
            if f.field is not None:
                if isinstance(f,VirtStoreField):
                    if not f.vf.editable:
                        if not isinstance(f.vf.return_type,dd.DisplayField):
                            self.disabled_fields.add(f.name)
                            #~ print "20121010 always disabled:", f
                elif not isinstance(f.field,generic.GenericForeignKey):
                    if not f.field.editable:
                        self.disabled_fields.add(f.name)
        
    def full_value_from_object(self,obj,ar):
        #~ l = [ f.name for f in self.store.actor.disabled_fields(request,obj)]
        d = dict()
        #~ if self.store.actor.disabled_fields is not None:
        for name in self.store.actor.disabled_fields(obj,ar):
            if name is not None: 
                d[name] = True
        #~ l = list(self.store.actor.disabled_fields(obj,request))
        
        for name in self.disabled_fields:
            if name is None: raise Exception('20130322a')
            d[name] = True
        
        # disable the primary key field if pk is set (i.e. on saved instance):
        if self.store.pk is not None and obj.pk is not None:
            if self.store.pk.attname is None: raise Exception('20130322b')
            d[self.store.pk.attname] = True
            #~ l.append(self.store.pk.attname)
            # MTI children have two "primary keys":
            if isinstance(self.store.pk,models.OneToOneField):
                #~ l.append(self.store.pk.rel.field_name)
                if self.store.pk.rel.field_name is None: raise Exception('20130322c')
                d[self.store.pk.rel.field_name] = True
        return d
        
class DisabledActionsStoreField(SpecialStoreField):
    """
    """
    name = 'disabled_actions'
    
    def full_value_from_object(self,obj,ar):
        return self.store.actor.disabled_actions(ar,obj)
        
        
#~ class RecnoStoreField(SpecialStoreField):
    #~ name = 'recno'
    #~ def full_value_from_object(self,request,obj):
        #~ return 
        
class DisableEditingStoreField(SpecialStoreField):
    """
    A field whose value is the result of the `get_row_permission` 
    method on that row.
    New feature since `/blog/2011/0830`
    """
    name = 'disable_editing'
        
    def full_value_from_object(self,obj,ar):
        #~ return self.store.actor.disable_editing(obj,request)
        #~ a = self.store.actor.submit_action
        #~ m = getattr(obj,'get_permission',None)
        #~ u = ar.get_user()
        #~ if not self.store.actor.get_permission(a,u) or (m is not None and not m(a,u)):
        #~ if not self.store.actor.get_permission(a,u,obj):
        #~ return not self.store.actor.get_permission(actions.UPDATE,ar.get_user(),obj)
        #~ if self.store.actor.get_row_permission is None:
            #~ return self.store.actor.editable
        #~ return not self.store.actor.get_row_permission(actions.UPDATE,ar.get_user(),obj)
        actor = self.store.actor
        if actor.update_action is None:
            #~ print 20120601, self.store.actor, "update_action is None"
            return True # disable editing if there's no update_action
        #~ state = actor.get_row_state(obj)
        #~ print 20120621, obj.__class__
        #~ return not obj.get_row_permission(ar.get_user(),state,self.store.actor.update_action)
        #~ logger.info("20120622 ext_store gonna call get_action_permission")
        v = actor.get_row_permission(obj,ar,actor.get_row_state(obj),actor.update_action)
        #~ v = self.store.actor.update_action.get_action_permission(ar.get_user(),obj,state)
        #~ logger.info("ext_store 20120622 got %r", v)
        return not v
        #~ return not self.store.actor.get_row_permission(self.store.actor.update_action,ar.get_user(),obj)
        #~ if not self.store.actor.get_permission(actions.UPDATE,ar.get_user(),obj):
            #~ return True
        #~ return False
        #~ if not self.store.report.get_permission(ar.get_user(),):
            #~ return False
        

        
#~ class PropertiesStoreField(StoreField):
#~ class PropertyStoreField(StoreField):
    #~ def __init__(self,field,**kw):
        #~ kw['type'] = ...
        #~ StoreField.__init__(self,field,**kw)
    #~ def get_from_form(self,instance,post_data):
        #~ v = post_data.get(self.field.name)
        #~ if v == 'true':
            #~ v = True
        #~ else:
            #~ v = False
        #~ instance[self.field.name] = v

#~ from lino.utils.textfields import extract_summary

#~ class TextStoreField(StoreField):
  
    #~ def value_from_object(self,request,obj):
        #~ v = self.field.value_from_object(obj)
        #~ if request.expand_memos:
            #~ return v
        #~ return extract_summary(v)
  


class BooleanStoreField(StoreField):
    """
    This class wouldn't be necessary if Django's 
    `BooleanField.to_python` method would interpret 
    "on" as a valid `True` value. We had some interesting 
    discussion on this in :djangoticket:`#15497 (BooleanField
    should work for all PostgreSQL expressions)<15497>`. 
    """
  
    form2obj_default = False # 'off'
    
    def __init__(self,field,name,**kw):
        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)
        
        
    def parse_form_value(self,v,obj):
        """
        Ext.ensible CalendarPanel sends boolean values as 
        """
        #~ print "20110717 parse_form_value", self.field.name, v, obj
        return ext_requests.parse_boolean(v)

        
    def format_value(self,ar,v):
        return force_unicode(iif(v,_("Yes"),_("No")))
        
      
class DisplayStoreField(StoreField):
    pass
    #~ def value2html(self,ar,v,**cellattrs):
        #~ return E.td(v,**cellattrs)
  

class DecimalStoreField(StoreField):
    #~ def __init__(self,field,name,**kw):
        #~ kw['type'] = 'float'
        #~ StoreField.__init__(self,field,name,**kw)
        
    def parse_form_value(self,v,obj):
        return parse_decimal(v)

    #~ def value2num(self,v):
        #~ # print "20120426 %s value2num(%s)" % (self,v)
        #~ return v
        
    def format_value(self,ar,v):
        if not v:
            return ''
        return 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)
        
        
class IntegerStoreField(StoreField):
    def __init__(self,field,name,**kw):
        kw['type'] = 'int'
        StoreField.__init__(self,field,name,**kw)
        
        
        
class AutoStoreField(StoreField):
    """
    A :class:`StoreField` for 
    `AutoField <https://docs.djangoproject.com/en/dev/ref/models/fields/#autofield>`__
    """
    def __init__(self,field,name,**kw):
        kw['type'] = 'int'
        StoreField.__init__(self,field,name,**kw)
  
    def form2obj(self,ar,obj,post_data,is_new):
        #~ 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)
        
        
        

class DateStoreField(StoreField):
  
    def __init__(self,field,name,**kw):
        kw['type'] = 'date'
        kw['dateFormat'] = settings.SITE.date_format_extjs # date_format # 'Y-m-d'
        StoreField.__init__(self,field,name,**kw)
        
    def parse_form_value(self,v,obj):
        if v:
            v = datetime.date(*settings.SITE.parse_date(v))
        else:
            v = None
        return v
        
    def format_value(self,ar,v):
        """
        Return a plain textual representation of this value as a unicode string.
        """
        return dd.dtos(v)
        #~ return settings.SITE.format_date(v)
        #~ return force_unicode(v)
        

class IncompleteDateStoreField(StoreField):
  
    def parse_form_value(self,v,obj):
        if v:
            v = IncompleteDate(*settings.SITE.parse_date(v))
            #~ v = datetime.date(*settings.SITE.parse_date(v))
        return v

class DateTimeStoreField(StoreField):
  
    def parse_form_value(self,v,obj):
        if v:
            return settings.SITE.parse_datetime(v) 
        return None

class TimeStoreField(StoreField):
  
    def parse_form_value(self,v,obj):
        if v:
            return settings.SITE.parse_time(v) 
        return None


class FileFieldStoreField(StoreField):
  
    def full_value_from_object(self,obj,request):
        ff = self.field.value_from_object(obj)
        return ff.name
        
class MethodStoreField(StoreField):
    "Deprecated. See `/blog/2012/0327`."
    def full_value_from_object(self,obj,request):
        unbound_meth = self.field._return_type_for_method
        assert unbound_meth.func_code.co_argcount >= 2, (self.name, unbound_meth.func_code.co_varnames)
        #~ print self.field.name
        return unbound_meth(obj,request)
        
    def value_from_object(self,obj,request):
        unbound_meth = self.field._return_type_for_method
        assert unbound_meth.func_code.co_argcount >= 2, (self.name, unbound_meth.func_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
        
    def form2obj(self,request,instance,post_data,is_new):
        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.()

class OneToOneStoreField(RelatedMixin,StoreField):
        
    def value_from_object(self,obj,request):
        v = self.full_value_from_object(obj,request)
        #~ try:
            #~ v = getattr(obj,self.field.name)
        #~ except self.field.rel.to.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)
        

def get_atomizer(fld,name):
    sf = getattr(fld,'_lino_atomizer',None)
    if sf is None:
        sf = create_atomizer(fld,name)
        setattr(fld,'_lino_atomizer',sf)
    return sf

def create_atomizer(fld,name):
    if isinstance(fld,fields.RemoteField):
        """
        Hack: we create a StoreField based on the remote field,
        then modify its behaviour.
        """
        sf = create_atomizer(fld.field,fld.name)
        def value_from_object(sf,obj,ar):
            m = fld.func
            return m(obj)
            
        def full_value_from_object(sf,obj,ar):
            #~ logger.info("20120406 %s.full_value_from_object(%s)",sf.name,sf)
            m = fld.func
            return m(obj)
            
        sf.value_from_object = curry(value_from_object,sf)
        sf.full_value_from_object = curry(full_value_from_object,sf)
        #~ sf.field = fld.field
        #~ sf.value2list = curry(value2list,sf)
        return sf
    #~ if isinstance(fld,tables.ComputedColumn):
        #~ logger.info("20111230 Store.create_atomizer(%s)", fld)
        #~ return ComputedColumnField(fld)
    meth = getattr(fld,'_return_type_for_method',None)
    if meth is not None:
        # uh, this is tricky...
        return MethodStoreField(fld,name)
    #~ if isinstance(fld,fields.HtmlBox):
        #~ ...
    #~ if isinstance(fld,dd.LinkedForeignKey):
        #~ return LinkedForeignKeyField(fld,name)
    if isinstance(fld,dd.RequestField):
        delegate = create_atomizer(fld.return_type,fld.name)
        return RequestStoreField(fld,delegate,name)
    if isinstance(fld,dd.VirtualField):
        delegate = create_atomizer(fld.return_type,fld.name)
        return VirtStoreField(fld,delegate,name)
    if isinstance(fld,models.FileField):
        return FileFieldStoreField(fld,name)
    if isinstance(fld,models.ManyToManyField):
        return StoreField(fld,name)
    if isinstance(fld,dd.PasswordField):
        return PasswordStoreField(fld,name)
    if isinstance(fld,models.OneToOneField):
        return OneToOneStoreField(fld,name)
    if isinstance(fld,generic.GenericForeignKey):
        return GenericForeignKeyField(fld,name)
    if isinstance(fld,dd.GenericForeignKeyIdField):
        return ComboStoreField(fld,name)
    if isinstance(fld,models.ForeignKey):
        return ForeignKeyStoreField(fld,name)
    if isinstance(fld,models.TimeField):
        return TimeStoreField(fld,name)
    if isinstance(fld,models.DateTimeField):
        return DateTimeStoreField(fld,name)
    if isinstance(fld,dd.IncompleteDateField):
        return IncompleteDateStoreField(fld,name)
    if isinstance(fld,models.DateField):
        return DateStoreField(fld,name)
    if isinstance(fld,models.BooleanField):
        return BooleanStoreField(fld,name)
    if isinstance(fld,models.DecimalField):
        return DecimalStoreField(fld,name)
    if isinstance(fld,models.AutoField):
        return AutoStoreField(fld,name)
        #~ kw.update(type='int')
    if isinstance(fld,models.SmallIntegerField):
        return IntegerStoreField(fld,name)
    if isinstance(fld,dd.DisplayField):
        return DisplayStoreField(fld,name)
    if isinstance(fld,models.IntegerField):
        return IntegerStoreField(fld,name)
    kw = {}
    if choosers.uses_simple_values(fld):
        return StoreField(fld,name,**kw)
    else:
        return ComboStoreField(fld,name,**kw)


class BaseStore(object):
    pass

class ParameterStore(BaseStore):
        
    def __init__(self,params_layout_handle,url_param):
        self.param_fields = []
        
        for pf in params_layout_handle._store_fields:
        #~ for pf in rh.report.params:
            self.param_fields.append(create_atomizer(pf,pf.name))
        
        self.param_fields = tuple(self.param_fields)
        self.url_param = url_param
        self.params_layout_handle = params_layout_handle
        
    def __str__(self):
        return "%s of %s" % (self.__class__,self.params_layout_handle)

        
    def unused_pv2list(self,pv):
        l = []
        for f in self.param_fields:
            l.append(pv[f.field.name])
        return l
        
    def pv2dict(self,ui,pv,**d):
        for fld in self.param_fields:
            v = pv.get(fld.name,None)
            fld.value2dict(ui,v,d,None)
        return d

    def parse_params(self,request,**kw):
        pv = request.REQUEST.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
                # if 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 pv: 
            #~ if len(self.param_fields) != len(pv):
                #~ raise Exception("len(%r) != len(%r)" % (self.param_fields,pv))
        if len(pv) > 0:
            if len(self.param_fields) != len(pv):
                #~ logger.info('20121016 %s para_fields are %s',self,[sf.field.name for sf in self.param_fields])
                raise Exception("Expected a list of %d values, but got %s" % (len(self.param_fields),pv))
            for i,f in enumerate(self.param_fields):
                kw[f.field.name] = parse(f,pv[i])
        #~ else: removed 20120918
            #~ for i,f in enumerate(self.param_fields):
                #~ kw[f.field.name] = parse(f,'')
        #~ logger.info("20120221 2 parse_params(%s) --> %r",pv,kw)
        return kw
        



class Store(BaseStore):
    """
    A Store is the collection of StoreFields for a given Table.
    """
    
    pk = None
    
    def __init__(self,rh,**options):
        #~ assert isinstance(rh,tables.TableHandle)
        #~ Component.__init__(self,id2js(rh.report.actor_id),**options)
        self.rh = rh
        self.actor = rh.actor
        """
        MTI children have two primary keys. Example::
        >>> from lino.projects.pcsw.models import Person
        >>> [f for f in Person._meta.fields if f.primary_key]
        [<django.db.models.fields.AutoField: id>, <django.db.models.fields.related.OneToOneField: contact_ptr>]
        >>> Person._meta.pk
        <django.db.models.fields.related.OneToOneField: contact_ptr>
        >>> p = Person.objects.get(pk=118)
        >>> p
        <Person: ARENS Annette (118)>
        >>> p.contact_ptr_id = 117
        >>> print p.pk
        117
        >>> p.save()
        >>>        
        """
        #~ if issubclass(rh.report,dbtables.Table):
            #~ self.pk = self.report.model._meta.pk
            #~ assert self.pk is not None, "Cannot make Store for %s because %s has no pk" % (
              #~ self.report.actor_id,self.report.model)
          
        #~ fields = []
        
        self.df2sf = {} # temporary dict used by collect_fields and add_field_for
        self.all_fields = []
        self.list_fields = []
        self.detail_fields = []
        
        def addfield(sf):
            self.all_fields.append(sf)
            self.list_fields.append(sf)
            self.detail_fields.append(sf)
            
        if not issubclass(rh.actor,frames.Frame):
            self.collect_fields(self.list_fields,rh.get_list_layout())
            
        form = rh.actor.detail_layout
        if form:
            dh = form.get_layout_handle(rh.ui)
            self.collect_fields(self.detail_fields,dh)
            
        form = rh.actor.insert_layout
        if form:
            dh = form.get_layout_handle(rh.ui)
            self.collect_fields(self.detail_fields,dh)
        
        if issubclass(rh.actor,dbtables.Table):
            self.pk_index = 0
            found = False
            for fld in self.list_fields:
                """
                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.
                """
                #~ if type(fld.field) == type(self.pk) and fld.field == self.pk:
                if (fld.field.__class__ is self.pk.__class__) and fld.field == self.pk:
                    #~ self.pk = fld.field
                    found = True
                    break
                self.pk_index += fld.list_values_count
            if not found:
                raise Exception("Primary key %s not found in list_fields %s" % (self.pk,self.list_fields))
            
        del self.df2sf
        
        #~ if not issubclass(rh.report,dbtables.Table):
            #~ addfield(RecnoStoreField(self))
          
        #~ if rh.report.disabled_fields is not None:
        addfield(DisabledFieldsStoreField(self))
        addfield(DisabledActionsStoreField(self))
            
            #~ sf = DisabledFieldsStoreField(self)
            #~ self.fields.append(sf)
            #~ self.list_fields.append(sf)
            #~ self.detail_fields.append(sf)
        #~ if rh.report.disable_editing is not None:
        #~ if rh.report.submit_action is not None:
        if rh.actor.editable:
            addfield(DisableEditingStoreField(self))
            
        #~ self.fields.append(PropertiesStoreField)
        #~ self.fields_dict = dict([(f.field.name,f) for f in self.fields])
        
        # virtual fields must come last so that Store.form2obj() 
        # processes "real" fields first.
        self.all_fields = [
            f for f in self.all_fields if not isinstance(f,VirtStoreField)
            ] + [
            f for f in self.all_fields if isinstance(f,VirtStoreField)
            ]
        #~ if str(self.report) == "cal.PanelEvents":
            #~ logger.info("20120119 %s",self.all_fields)
        self.all_fields = tuple(self.all_fields)
        self.list_fields = tuple(self.list_fields)
        self.detail_fields = tuple(self.detail_fields)
        

    def add_field_for(self,fields,df):
        sf = get_atomizer(df,df.name)
            
        if not sf in self.all_fields:
            self.all_fields.append(sf)
            
        #~ 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
        fields.append(sf)
        
    def collect_fields(self,fields,*layouts):
        """
        `fields` is a pointer to either `self.detail_fields` or `self.list_fields`.
        Each of these must contain a primary key field.
        
        """
        pk_found = False
        for layout in layouts:
            for df in layout._store_fields:
                assert df is not None
                self.add_field_for(fields,df)
                if df.primary_key:
                    pk_found = True
                    if self.pk is None:
                        self.pk = df
                #~ fields.add(fld)
        #~ if not self.pk in fields:
        if issubclass(self.actor,dbtables.Table):
            if self.pk is None:
                self.pk = self.actor.model._meta.pk
            if not pk_found:
                self.add_field_for(fields,self.pk)
                

    def form2obj(self,ar,form_values,instance,is_new):
        disabled_fields = set(self.actor.disabled_fields(instance,ar))
        #~ logger.info("20130107 Store.form2obj(%s)", form_values)
        #~ logger.info("20120228 %s Store.form2obj(%s),\ndisabled %s\n all_fields %s", 
            #~ self.rh,form_values,disabled_fields,self.all_fields)
        #~ print 20110406, disabled_fields
        changed_triggers = []
        for f in self.all_fields:
          #~ if f.field is not None and f.field.editable:
          #~ if f.editable:
            if not f.name in disabled_fields:
                try:
                    #~ if f.form2obj(ar.request,instance,form_values,is_new):
                    if f.form2obj(ar,instance,form_values,is_new):
                        #~ logger.info("20130107 Store.form2obj %s changed", f)
                        m = getattr(instance,f.name + "_changed",None)
                        if m is not None:
                            changed_triggers.append(m)
                except exceptions.ValidationError as e:
                    raise exceptions.ValidationError({f.name:e})
                except ValueError as e:
                    raise exceptions.ValidationError({f.name:_("Invalid value for this field (%s).") % e})
                except Exception,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)
            #~ m()
            
        #~ m = getattr(instance,"_changed",None)
        #~ if m is not None:
            #~ m(request)
        #~ return instance
        #~ logger.info("20120603 Store.form2obj %s", instance.national_id)
            
    def column_names(self):
        l = []
        for fld in self.list_fields:
            l += fld.column_names()
        return l
        
    def column_index(self,name):
        """
        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)
        

    def row2list(self,request,row):
        #~ 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))
        l = []
        if isinstance(row,PhantomRow):
            for fld in self.list_fields:
                fld.value2list(request,None,l,row)
        #~ elif isinstance(row,actions.VirtualRow):
            #~ for fld in self.list_fields:
                #~ fld.value2list(request,None,l,row)
        else:
            for fld in self.list_fields:
                v = fld.full_value_from_object(row,request)
                #~ if fld.name == 'person__age':
                    #~ logger.info("20120406 Store.row2list %s -> %s", fld, v)
                fld.value2list(request,v,l,row)
                #~ if fld.name == 'person__age':
                    #~ logger.info("20120406 Store.row2list %s -> %s", fld, l)
        return l
      
    def row2dict(self,ar,row,fields=None,**d):
        #~ assert isinstance(ar,dbtables.AbstractTableRequest)
        #~ logger.info("20111209 Store.row2dict(%s)", dd.obj2str(row))
        if fields is None:
            fields = self.detail_fields
        for fld in fields:
            #~ logger.info("20111209 Store.row2dict %s %s", row,fld)
            v = fld.full_value_from_object(row,ar)
            fld.value2dict(ar.ui,v,d,row)
            #~ logger.info("20111209 Store.row2dict %s -> %s", f, d)
        return d

    #~ 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)
            
            
