diff options
Diffstat (limited to 'ishtar_common/models.py')
| -rw-r--r-- | ishtar_common/models.py | 205 | 
1 files changed, 198 insertions, 7 deletions
| diff --git a/ishtar_common/models.py b/ishtar_common/models.py index 28a24115b..c3ba4fdd0 100644 --- a/ishtar_common/models.py +++ b/ishtar_common/models.py @@ -35,6 +35,8 @@ import tempfile  import time  from django.conf import settings +from django.contrib.postgres.fields import JSONField +from django.contrib.postgres.search import SearchVectorField, SearchVector  from django.core.cache import cache  from django.core.exceptions import ObjectDoesNotExist, ValidationError  from django.core.files.uploadedfile import SimpleUploadedFile @@ -58,7 +60,7 @@ from simple_history.models import HistoricalRecords as BaseHistoricalRecords  from ishtar_common.model_merging import merge_model_objects  from ishtar_common.utils import get_cache, disable_for_loaddata, create_slug,\ -    get_all_field_names +    get_all_field_names, merge_tsvectors, cached_label_changed  from ishtar_common.models_imports import ImporterModel, ImporterType, \      ImporterDefault, ImporterDefaultValues, ImporterColumn, \ @@ -908,6 +910,96 @@ class BulkUpdatedItem(object):          return transaction_id, False +class JsonDataSection(models.Model): +    content_type = models.ForeignKey(ContentType) +    name = models.CharField(_(u"Name"), max_length=200) +    order = models.IntegerField(_(u"Order"), default=10) + +    class Meta: +        verbose_name = _(u"Json data - Menu") +        verbose_name_plural = _(u"Json data - Menus") +        ordering = ['order', 'name'] + +    def __unicode__(self): +        return u"{} - {}".format(self.content_type, self.name) + + +class JsonDataField(models.Model): +    name = models.CharField(_(u"Name"), max_length=200) +    content_type = models.ForeignKey(ContentType) +    key = models.CharField( +        _(u"Key"), max_length=200, +        help_text=_(u"Value of the key in the JSON schema. For hierarchical " +                    u"key use \"__\" to explain it. For instance the key " +                    u"'my_subkey' with data such as {'my_key': {'my_subkey': " +                    u"'value'}} will be reached with my_key__my_subkey.")) +    display = models.BooleanField(_(u"Display"), default=True) +    order = models.IntegerField(_(u"Order"), default=10) +    section = models.ForeignKey(JsonDataSection, blank=True, null=True) + +    class Meta: +        verbose_name = _(u"Json data - Field") +        verbose_name_plural = _(u"Json data - Fields") +        ordering = ['order', 'name'] + +    def __unicode__(self): +        return u"{} - {}".format(self.content_type, self.name) + +    def clean(self): +        if not self.section: +            return +        if self.section.content_type != self.content_type: +            raise ValidationError( +                _(u"Content type of the field and of the menu do not match")) + + +class JsonData(models.Model): +    data = JSONField(default={}, db_index=True, blank=True) + +    class Meta: +        abstract = True + +    def pre_save(self): +        if not self.data: +            self.data = {} + +    @property +    def json_sections(self): +        sections = [] +        try: +            content_type = ContentType.objects.get_for_model(self) +        except ContentType.DoesNotExists: +            return sections +        fields = list(JsonDataField.objects.filter( +                content_type=content_type, display=True, section__isnull=True +        ).all())  # no section fields + +        fields += list(JsonDataField.objects.filter( +            content_type=content_type, display=True, section__isnull=False +        ).order_by('section__order', 'order').all()) + +        for field in fields: +            value = None +            data = self.data.copy() +            for key in field.key.split('__'): +                if key in data: +                    value = copy.copy(data[key]) +                    data = data[key] +                else: +                    value = None +                    break +            if not value: +                continue +            if type(value) in (list, tuple): +                value = u" ; ".join([unicode(v) for v in value]) +            section_name = field.section.name if field.section else None +            if not sections or section_name != sections[-1][0]: +                # if section name is identical it is the same +                sections.append((section_name, [])) +            sections[-1][1].append((field.name, value)) +        return sections + +  class Imported(models.Model):      imports = models.ManyToManyField(          Import, blank=True, @@ -917,9 +1009,85 @@ class Imported(models.Model):          abstract = True -class BaseHistorizedItem(Imported): +class FullSearch(models.Model): +    search_vector = SearchVectorField(_("Search vector"), blank=True, null=True, +                                      help_text=_("Auto filled at save")) +    BASE_SEARCH_VECTORS = [] +    INT_SEARCH_VECTORS = [] +    M2M_SEARCH_VECTORS = [] +    PARENT_SEARCH_VECTORS = [] + +    class Meta: +        abstract = True + +    def update_search_vector(self, save=True): +        """ +        Update the search vector +        :param save: True if you want to save the object immediately +        :return: True if modified +        """ +        if not self.BASE_SEARCH_VECTORS and not self.M2M_SEARCH_VECTORS: +            logger.warning("No search_vectors defined for {}".format( +                self.__class__)) +            return +        if getattr(self, '_search_updated', None): +            return +        self._search_updated = True + +        old_search = "" +        if self.search_vector: +            old_search = self.search_vector[:] +        search_vectors = [] +        base_q = self.__class__.objects.filter(pk=self.pk) + +        # many to many have to be queried one by one otherwise only one is fetch +        for M2M_SEARCH_VECTOR in self.M2M_SEARCH_VECTORS: +            key = M2M_SEARCH_VECTOR.split('__')[0] +            rel_key = getattr(self, key) +            for item in rel_key.values('pk').all(): +                query_dct = {key + "__pk": item['pk']} +                q = copy.copy(base_q).filter(**query_dct) +                q = q.annotate( +                    search=SearchVector( +                        M2M_SEARCH_VECTOR, +                        config=settings.ISHTAR_SEARCH_LANGUAGE) +                ).values('search') +                search_vectors.append(q.all()[0]['search']) + +        # int/float are not well managed by the SearchVector +        for INT_SEARCH_VECTOR in self.INT_SEARCH_VECTORS: +            q = base_q.values(INT_SEARCH_VECTOR) +            search_vectors.append( +                "'{}':1".format(q.all()[0][INT_SEARCH_VECTOR])) + +        # copy parent vector fields +        for PARENT_SEARCH_VECTOR in self.PARENT_SEARCH_VECTORS: +            parent = getattr(self, PARENT_SEARCH_VECTOR) +            if hasattr(parent, 'all'):  # m2m +                for p in parent.all(): +                    search_vectors.append(p.search_vector) +            else: +                search_vectors.append(parent.search_vector) + +        # query "simple" fields +        q = base_q.annotate( +            search=SearchVector( +                *self.BASE_SEARCH_VECTORS, +                config=settings.ISHTAR_SEARCH_LANGUAGE +            )).values('search') +        search_vectors.append(q.all()[0]['search']) +        self.search_vector = merge_tsvectors(search_vectors) +        changed = old_search != self.search_vector +        if save and changed: +            self.skip_history_when_saving = True +            self.save() +        return changed + + +class BaseHistorizedItem(FullSearch, Imported, JsonData):      """ -    Historized item with external ID management +    Historized item with external ID management. +    All historized items are searcheable and have a data json field      """      IS_BASKET = False      EXTERNAL_ID_KEY = '' @@ -1187,6 +1355,7 @@ class LightHistorizedItem(BaseHistorizedItem):          super(LightHistorizedItem, self).save(*args, **kwargs)          return True +  PARSE_FORMULA = re.compile("{([^}]*)}")  FORMULA_FILTERS = { @@ -1409,6 +1578,7 @@ def get_current_profile(force=False):  def cached_site_changed(sender, **kwargs):      get_current_profile(force=True) +  post_save.connect(cached_site_changed, sender=IshtarSiteProfile)  post_delete.connect(cached_site_changed, sender=IshtarSiteProfile) @@ -2490,12 +2660,20 @@ class Town(Imported, models.Model):      center = models.PointField(_(u"Localisation"), srid=settings.SRID,                                 blank=True, null=True)      if settings.COUNTRY == 'fr': -        numero_insee = models.CharField(u"Numéro INSEE", max_length=6, -                                        unique=True) +        numero_insee = models.CharField(u"Numéro INSEE", max_length=6)          departement = models.ForeignKey(              Department, verbose_name=u"Département", null=True, blank=True)          canton = models.ForeignKey(Canton, verbose_name=u"Canton", null=True,                                     blank=True) +    year = models.IntegerField( +        _("Year of creation"), null=True, blank=True, +        help_text=_(u"Filling this field is relevant to distinguish old towns " +                    u"to new towns.")) +    children = models.ManyToManyField( +        'Town', verbose_name=_(u"Town children"), blank=True, +        related_name='parents') +    cached_label = models.CharField(_(u"Cached name"), max_length=500, +                                    null=True, blank=True, db_index=True)      objects = models.GeoManager()      class Meta: @@ -2503,11 +2681,24 @@ class Town(Imported, models.Model):          verbose_name_plural = _(u"Towns")          if settings.COUNTRY == 'fr':              ordering = ['numero_insee'] +            unique_together = (('numero_insee', 'year'),)      def __unicode__(self): +        if self.cached_label: +            return self.cached_label +        self.save() +        return self.cached_label + +    def _generate_cached_label(self): +        cached_label = self.name          if settings.COUNTRY == "fr": -            return u"%s (%s)" % (self.name, self.numero_insee[:2]) -        return self.name +            cached_label = u"%s - %s" % (self.name, self.numero_insee[:2]) +        if self.year: +            cached_label += " ({})".format(self.year) +        return cached_label + + +post_save.connect(cached_label_changed, sender=Town)  class OperationType(GeneralType): | 
