1. Marcelo Gonçalves
  2. difutils

Commits

Marcelo Gonçalves  committed 02b4d60

Implementada edição de Atributos e valores no EAV, pequenos ajustes e correções de bugs
- Criados templates para a edição de atributos em Tags e Entidades
- Adicionado método em Entity para dar valor a um atributo
- Adicionado método no AttributeValue para interpretar valores recebidos via json
- URLs do CRUD movidos para o próprio projeto
- O modo de listar um modelo foi movido para o próprio modelo, permitindo customizações futuras
- Corrigido bug no Entity.all_values, agora ele funciona como um generator
- Criado um TagSingleton, para denotar tags importantes para o sistema
- A carga dos modelos no EAV agora é feita usando models.get_model
- Atributos e valores possuem um método jsonFriendly, para retornar um dict pronto para ser serializado
- Attribute, Tag e Entity podem ser recuperados via CRUD
- Criado método global para informar os tipos de atributo suportados por uma instalação (não necessita autenticação, e é comum a todo tenant)
- Integração mínima entre campoReais com Knockout.js implementada
- Corrigido bug no listener do activeRecords - os argumentos do _.bind estavam invertidos

  • Participants
  • Parent commits 3bdbd51
  • Branches default

Comments (0)

Files changed (11)

File core/models.py

View file
  • Ignore whitespace
     @classmethod
     def get_fields(cls,tags=None):
         return cls._meta.fields
+    # CRUD logic
+    @classmethod
+    def crud_list(cls,tenant,kwargs):
+        if tenant is not None:
+            kwargs = tenant.filter_tenant(cls,kwargs)
+        return cls.objects.filter(**kwargs)
         
 class ModeloTenant(Modelo):
     tenant = models.PositiveIntegerField()

File crud/urls.py

View file
  • Ignore whitespace
 from django.conf.urls.defaults import *
 
 urlpatterns = patterns('difutils.crud.views',
+    (r'^(?P<module>[^/]+)/(?P<entity>\w+)/count/$', 'crud_count'),
+    (r'^(?P<module>[^/]+)/(?P<entity>\w+)/$', 'crud_list'),
+    (r'^(?P<module>[^/]+)/(?P<entity>\w+)/(?P<oid>\d+)/$', 'crud_read'),
+    (r'^(?P<module>[^/]+)/(?P<entity>\w+)/new/$', 'crud_create'),
+    (r'^(?P<module>[^/]+)/(?P<entity>\w+)/(?P<oid>\d+)/update/$', 'crud_update'),
+    (r'^(?P<module>[^/]+)/(?P<entity>\w+)/(?P<oid>\d+)/related/$', 'crud_all_related'),
+    (r'^(?P<module>[^/]+)/(?P<entity>\w+)/(?P<oid>\d+)/delete/$', 'crud_delete'),
+    (r'^transaction/$', 'crud_transacao'),
+    (r'^multibusca/$', 'crud_multibusca'),
 )
 

File crud/views.py

View file
  • Ignore whitespace
 def crud_count(request,model,tenant,listener=(lambda x:x)):
     campos_extra = simplejson.loads(request.GET['campos_extra']) if 'campos_extra' in request.GET else {}
     kwargs = util_param_dict(request.GET,'filtros')
-    if tenant is not None:
-        kwargs = tenant.filter_tenant(kwargs)
-    count = model.filter_extra_fields(model.objects.filter(**kwargs), campos_extra).count()
+    count = model.filter_extra_fields(model.crud_list(tenant,kwargs), campos_extra).count()
     listener(count)
     return HttpResponse('{"ok":true, "count":' + count + '}', mimetype="application/json")
         
     limite = int(request.GET['limit']) if 'limit' in request.GET else sys.maxint
     campos_extra = simplejson.loads(request.GET['campos_extra']) if 'campos_extra' in request.GET else {}
     kwargs = util_param_dict(request.GET,'filtros')
-    if tenant is not None:
-        kwargs = tenant.filter_tenant(model,kwargs)
-    qs = model.filter_extra_fields(model.objects.filter(**kwargs), campos_extra)
+    qs = model.filter_extra_fields(model.crud_list(tenant,kwargs), campos_extra)
     if 'order' in request.GET and len(request.GET['order']) > 0:
         qs = qs.order_by(*request.GET['order'].split(','))
     if 'related' in request.GET and len(request.GET['related']) > 0:

File eav/models.py

View file
  • Ignore whitespace
     def all_attributes(self):
         return Attribute.objects.filter(tag__dset__descendant__taggedentity__entity__aset__ancestor=self).distinct()
     def all_values(self):
-        types = set(TYPE_MODELS[a.type] for a in self.all_attributes())
-        return chain(unique_attributes(m.objects.filter(entity__aset__ancestor=self).select_related('attribute').order_by('entity__aset__depth')) for m in types)
+        types = set(model_for_type(a.type) for a in self.all_attributes())
+        for m in types:
+            for attr in unique_attributes(m.objects.filter(entity__aset__ancestor=self).select_related('attribute').order_by('entity__aset__depth')):
+                yield attr  
     def to_dict(self):
         return { av.attribute.name:av.value for av in self.all_values() }
+    def set_attr(self,attr_pk,value):
+        attr = Attribute.objects.get(pk=attr_pk, tenant=self.tenant)
+        model = model_for_type(attr.type)
+        if value is not None:
+            model.objects.get_or_create(entity=self, attribute=attr, **model.parse_value(value))
+        elif not attr.nullable:
+            raise ValidationError('Setting a value of NULL to a non-nullable attribute')
+        else:
+            model.objects.filter(entity=self, attribute=attr).delete()
     @classmethod
     def filter_extra_fields(cls,qs,params={}):
         # TODO filter by tag or by attribute, if params specify that
     @classmethod
     def campo_principal(cls):
         return ("entity", Entity)
+        
+class TagSingleton(ModeloTenant):
+    tag = models.ForeignKey(Tag)
+    class Meta:
+        abstract = True
+        unique_together = (('tenant',),)
+    @classmethod
+    def crud_list(cls,tenant,kwargs):
+        """List tags that descend from the singleton"""
+        self = cls.objects.get(**tenant.filter_tenant({}))
+        return Tag.objects.filter(**tenant.filter_tenant({'aset__ancestor':self.tag_id}))
     
-class Attribute(NamedModel):
+class Attribute(NamedModel): # FIXME should be a Modelo, not a ModeloTenant
     tag = models.ForeignKey(Tag)
     type = models.CharField(max_length=3, choices=TYPE_CHOICES, default="txt")
     nullable = models.BooleanField(default=False)
     class Meta:
         unique_together = (('tag', 'name'),)
     def get_value(self,entity):
-        m = TYPE_MODELS[self.type]
+        m = model_for_type(self.type)
         ret = unique_attributes(m.objects.filter(entity__aset__ancestor=entity).order_by('entity__aset__depth'))
         return ret[0].value if ret else None
     def validate_value(self,ref):
         attr.blank = attr.nullable
         attr.has_default = lambda: False
         return attr
+    def jsonFriendly(self):
+        return {
+            'pk':self.pk,
+            'name':self.name,
+            'tag':self.tag_id,
+            'type':self.type,
+            'nullable':self.nullable,
+        }
     
 class AttributeValue(Modelo):
     entity = models.ForeignKey(Entity, related_name="+")
         # Type validation
         if not Attribute.objects.filter(pk=self.attribute.pk, tag__dset__taggedentity__entity__aset__ancestor=self.entity):
             raise ValidationError('This entity does not have this attribute')
-        if TYPE_MODELS[self.attribute.type] != self.__class__:
+        if model_for_type(self.attribute.type) != self.__class__:
             raise ValidationError('Wrong type for attribute value')
         # Attribute validation
         self.attribute.validate_value(self)
     @classmethod
     def campo_principal(cls):
         return ("entity", Entity)
+    def jsonFriendly(self):
+        meta = self.attribute.__class__._meta
+        return {
+            'pk':self.pk,
+            'model':u"{}.{}".format(meta.app_label, meta.module_name),
+            'entity':self.entity_id,
+            'attribute':self.attribute.jsonFriendly(),
+            'value':self.jsonFriendlyValue(),
+        }
+    def jsonFriendlyValue(self):
+        return unicode(self.value)
+    @classmethod
+    def parse_value(cls,value):
+        return { 'value':value }
 
 # Attribute types        
 
         # Tenant validation
         if self.entity.tenant != self.value.tenant:
             raise ValidationError('Entity and attribute value from different tenants')
+    def jsonFriendlyValue(self):
+        return unicode(self.value_id)
+    @classmethod
+    def parse_value(cls,value):
+        return { 'value_id':value }
     
 class SetAttribute(Attribute):
     set = models.ForeignKey(Set)
     def validate_value(self,ref):
         if not SetItem.objects.filter(element=ref.value, container=self.set).count():
             raise ValidationError('Entity is not element of set')
+    def jsonFriendly(self):
+        ret = super(Attribute,self).jsonFriendly()
+        ret['set'] = set_id
+        return ret
     
 class TagAttribute(Attribute):
     prototype = models.ForeignKey(Tag)
     def validate_value(self,ref):
         if not ref.value.all_tags().filter(pk=self.prototype.pk).count():
             raise ValidationError('Entity does not have tag')
+    def jsonFriendly(self):
+        ret = super(Attribute,self).jsonFriendly()
+        ret['prototype'] = prototype_id
+        return ret
     
 class IntegerAttribute(AttributeValue):
     value = models.IntegerField()
+    def jsonFriendlyValue(self):
+        return self.value
     
 class Currency62Attribute(AttributeValue):
     value = models.DecimalField(max_digits=6+2, decimal_places=2)
     
 class FloatAttribute(AttributeValue):
     value = models.FloatField()
+    def jsonFriendlyValue(self):
+        return self.value
     
 class BooleanAttribute(AttributeValue):
     value = models.BooleanField()
+    def jsonFriendlyValue(self):
+        return self.value
     
 class DateAttribute(AttributeValue):
     value = models.DateField()
     value = models.TimeField()
 
 TYPE_MODELS = {
-    'txt':TextAttribute,
-    'ref':RefAttribute,
-    'set':RefAttribute,
-    'int':IntegerAttribute,
-    '$62':Currency62Attribute,
-    '$E2':CurrencyE2Attribute,
-    '$I2':CurrencyI2Attribute,
-    '$88':Currency88Attribute,
-    'flo':FloatAttribute,
-    'boo':BooleanAttribute,
-    'dat':DateAttribute,
-    'dt':DateTimeAttribute,
-    'tim':TimeAttribute,
+    'txt':'eav.TextAttribute',
+    'ref':'eav.RefAttribute',
+    'set':'eav.RefAttribute',
+    'tag':'eav.RefAttribute',
+    'int':'eav.IntegerAttribute',
+    '$62':'eav.Currency62Attribute',
+    '$E2':'eav.CurrencyE2Attribute',
+    '$I2':'eav.CurrencyI2Attribute',
+    '$88':'eav.Currency88Attribute',
+    'flo':'eav.FloatAttribute',
+    'boo':'eav.BooleanAttribute',
+    'dat':'eav.DateAttribute',
+    'dt':'eav.DateTimeAttribute',
+    'tim':'eav.TimeAttribute',
 }
+def model_for_type(type):
+    return models.get_model(*TYPE_MODELS[type].split('.'))
+    
 # Customizations
 if hasattr(settings, 'DIFUTILS') and 'EAV' in settings.DIFUTILS:
     import sys
     if 'TYPE_MODELS' in settings.DIFUTILS['EAV']:
-        TYPE_MODELS = settings.DIFUTILS['EAV']['TYPE_MODELS'](sys.modules[__name__])
+        TYPE_MODELS = settings.DIFUTILS['EAV']['TYPE_MODELS']
     if 'ADD_TYPE_MODELS' in settings.DIFUTILS['EAV']:
-        TYPE_MODELS.update(settings.DIFUTILS['EAV']['ADD_TYPE_MODELS'](sys.modules[__name__]))
+        TYPE_MODELS.update(settings.DIFUTILS['EAV']['ADD_TYPE_MODELS'])
 
+CRUD_Enabled = [Attribute,Tag,Entity]
+MultiTenant_Required = True
+

File eav/urls.py

View file
  • Ignore whitespace
 from django.conf.urls.defaults import *
 
 urlpatterns = patterns('difutils.eav.views',
+    (r'^available_attr_types/$', 'available_attr_types'),
 )
 

File eav/views.py

View file
  • Ignore whitespace
    
     
 """
+from difutils.eav.models import TYPE_CHOICES
+from django.http import HttpResponse
+from django.utils import simplejson
 
+def available_attr_types(request):
+    return HttpResponse(simplejson.dumps(TYPE_CHOICES), mimetype="application/json")
+    

File models.py

View file
  • Ignore whitespace
         return qs
     def add_extra_fields(self,params=None):
         return self
+    # CRUD logic
+    @classmethod
+    def crud_list(cls,tenant,kwargs):
+        if tenant is not None:
+            kwargs = tenant.filter_tenant(cls,kwargs)
+        return cls.objects.filter(**kwargs)
         
 class ModeloTenant(Modelo):
     tenant = models.PositiveIntegerField()

File static/difutils/js/difutils.js

View file
  • Ignore whitespace
                     var saldoEmCentavos = parseInt(saldo.replace(/\./g,"").replace(/\,/g,""),10);
                     $(element).data("emCentavos",saldoEmCentavos).data("emTexto",saldo).data("estaValido",true);
                     options.saldo_valido(saldo,saldoEmCentavos);
+                    if ( options.ko )
+                        options.ko(difutils.formatarDecimal(saldoEmCentavos));
                 }
                 else {
                     $(element).data("estaValido",false);
                     options.saldo_invalido();
+                    if ( options.ko )
+                        options.ko(null);
                 }
             }
             $(element).data("options",options).keypress(function(e) {
                 if ( validos.indexOf(e.which) < 0 && (e.which < 48 || 57 < e.which) && e.which != 0 )
                     e.preventDefault();
             }).keyup(validate).change(validate);
+            
+            // Valor inicial
+            if ( options.ko )
+                campoReaisMethods.emTexto.apply($(element), ["emTexto", options.ko()]);
         });
     };
     var campoReaisMethods = {
             }
             return this.each(function() {
                 // TODO se negativo for false e o valor em centavos for negativo, não aceitar
-                $(this)
+                var options = $(this)
                     .data("emCentavos",centavos)
                     .data("estaValido",true)
-                    .val(difutils.formatarDecimal(centavos).replace(".",","));
+                    .val(difutils.formatarDecimal(centavos).replace(".",","))
+                    .data("options");
+                if ( options.ko )
+                    options.ko(difutils.formatarDecimal(centavos));
             });
         },
         emTexto:function(method,texto) {
             }
             return this.each(function() {
                 var centavos = difutils.parseReais(texto);
-                $(this)
+                var options = $(this)
                     .data("emCentavos",centavos)
                     .data("estaValido",true)
-                    .val(difutils.formatarDecimal(centavos).replace(".",","));
+                    .val(difutils.formatarDecimal(centavos).replace(".",","))
+                    .data("options");
+                if ( options.ko )
+                    options.ko(difutils.formatarDecimal(centavos));
             });
         },
         paraAjax:function(method) {
                                 div.difutils().activeRecords("atualizar");
                             }
                             posOperacao(data);
-                            listeners.forEach(function(f) { _.bind(f,f[posOperacao2])(data); });
+                            listeners.forEach(function(f) { _.bind(f[posOperacao2],f)(data); });
                         }
                         // Salva o objeto
                         if ( div.data("editando") == null )

File templates/difutils/eav_entity_edit.pt.html

View file
  • Ignore whitespace
+<div class="eav_entity_edit" data-bind="visible: attrs().length > 0">
+  <p data-bind="text: label"></p>
+  <span data-bind="foreach: { data:attrs, afterRender: attrAdded }"><p style="margin-left: 20px">
+        <span data-bind="text: name" type="text"></span>: 
+        <span data-bind="if: type.code == 'txt'"> <!-- Simple text field -->
+            <input data-bind="value: value" type="text" />
+        </span>
+        <span data-bind="if: type.code == 'num'"> <!-- Simple numeric field -->
+            <input data-bind="value: value" type="number" />
+        </span>
+        <span data-bind="if: type.code == 'int'"> <!-- Simple numeric field, but with "integer" class -->
+            <input class="integer" data-bind="value: value" type="number" />
+        </span>
+        <span data-bind="if: type.code == 'brl'"> <!-- difutils.campoReais -->
+            <input class="campoReais" type="text" />
+        </span>
+        <span data-bind="if: type.code == 'dat'"> <!-- jQuery.DatePicker -->
+            <input class="datepicker" type="text" />
+        </span>
+        <span data-bind="if: type.code == 'dt'"> <!-- jQuery.DateTimePicker -->
+            <input class="datetimepicker" type="text" />
+        </span>
+        <span data-bind="if: type.code == 'boo'"> <!-- Simple checkbox -->
+            <input data-bind="checked: value" type="checkbox" />
+        </span>
+        <!-- TODO add an input for each reasonable type -->
+        <span class="placeholder"></span>
+  </p></span>
+</div>

File templates/difutils/eav_tag_edit.pt.html

View file
  • Ignore whitespace
+<div class="eav_tag_edit">
+  <p data-bind="text: label"></p>
+  <span data-bind="foreach: { data:attrs, afterAdd: attrAdded }">
+    <p style="margin-left:20px;">
+        Nome: <input data-bind="value: name, hasfocus: focus" type="text" />
+        Tipo: <select data-bind="options: attrTypes, optionsText: 'name', value: type"></select>
+        <input type="checkbox" data-bind="checked: nullable" /><label data-bind="text: labelNullable"></label>
+        <button data-bind="click: remove">Excluir</button>
+        <span style="color:red" data-bind="visible: !focus() && name().length == 0">Escolha um nome</span>
+    </p>
+  </span>
+  <button data-bind="click: add">Novo</button>
+</div>

File urls.py

View file
  • Ignore whitespace
     Licença dual: MIT ou GPL versão 3 ou superior
 """
 from django.conf.urls.defaults import *
+import difutils.crud.urls, difutils.eav.urls
 
 urlpatterns = patterns('difutils.views',
     (r'^versao_aplicativo/$', 'versao_aplicativo'),
     #(r'^processes/(?P<id>\d+)/$', 'process_detail'),
     #(r'^processes/(?P<id>\d+)/stop/$', 'stop_process'),
     # CRUD
-    (r'^crud/(?P<module>[^/]+)/(?P<entity>\w+)/count/$', 'crud_count'),
-    (r'^crud/(?P<module>[^/]+)/(?P<entity>\w+)/$', 'crud_list'),
-    (r'^crud/(?P<module>[^/]+)/(?P<entity>\w+)/(?P<oid>\d+)/$', 'crud_read'),
-    (r'^crud/(?P<module>[^/]+)/(?P<entity>\w+)/new/$', 'crud_create'),
-    (r'^crud/(?P<module>[^/]+)/(?P<entity>\w+)/(?P<oid>\d+)/update/$', 'crud_update'),
-    (r'^crud/(?P<module>[^/]+)/(?P<entity>\w+)/(?P<oid>\d+)/related/$', 'crud_all_related'),
-    (r'^crud/(?P<module>[^/]+)/(?P<entity>\w+)/(?P<oid>\d+)/delete/$', 'crud_delete'),
-    (r'^crud/transaction/$', 'crud_transacao'),
-    (r'^crud/multibusca/$', 'crud_multibusca'),
+    (r'^crud/', include(difutils.crud.urls)),
+    # EAV
+    (r'^eav/', include(difutils.eav.urls)),
     # Informacoes pessoais
     (r'^info_pessoais/all/(?P<id>\d+)/$', 'todas_informacoes_pessoais'),
     # Etc