diff options
Diffstat (limited to 'chimere/models.py')
| -rw-r--r-- | chimere/models.py | 1123 | 
1 files changed, 758 insertions, 365 deletions
diff --git a/chimere/models.py b/chimere/models.py index 122d61f..9e7e52f 100644 --- a/chimere/models.py +++ b/chimere/models.py @@ -20,7 +20,11 @@  """  Models description  """ -import os, datetime, pyexiv2, re, string, copy +import os +import datetime +import pyexiv2 +import re +import copy  import simplejson as json  from lxml import etree  from PIL import Image @@ -29,22 +33,19 @@ from BeautifulSoup import BeautifulSoup  from django import forms  from django.conf import settings -from django.contrib import admin  from django.contrib.auth.models import User, Permission, ContentType, Group  from django.contrib.gis.db import models -from django.contrib.gis.gdal import SpatialReference  from django.core.files import File -from django.core.exceptions import ValidationError, ObjectDoesNotExist +from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned  from django.core.urlresolvers import reverse -from django.db.models import Q +from django.db.models import Q, Count  from django.db.models.signals import post_save, pre_save, m2m_changed  from django.template import defaultfilters  from django.utils.translation import ugettext_lazy as _  from chimere.widgets import HiddenPointChooserWidget, PointField, RouteField, \ -                            SelectMultipleField, TextareaWidget, \ -                            DatePickerWidget -from chimere.managers import BaseGeoManager +    SelectMultipleField, TextareaWidget, DatePickerWidget, PolygonField, \ +    JQueryAutoComplete  from chimere.utils import KMLManager, OSMManager, ShapefileManager, \      GeoRSSManager, CSVManager, HtmlXsltManager, XMLXsltManager, JsonManager, \      IcalManager @@ -61,13 +62,16 @@ class Page(models.Model):      template_path = models.CharField(_(u"Template path"), max_length=150,                                       blank=True, null=True)      content = models.TextField(blank=True, null=True) -    def __unicode__(self): -        ordering = ["order"] -        return self.title +      class Meta: +        ordering = ["order"]          verbose_name = _(u"Page")          verbose_name_plural = _(u"Page") +    def __unicode__(self): +        return self.title + +  def page_post_save(sender, **kwargs):      if not kwargs['instance']:          return @@ -77,6 +81,7 @@ def page_post_save(sender, **kwargs):          page.save()  post_save.connect(page_post_save, sender=Page) +  def shortify(text):      if not text:          return '' @@ -89,15 +94,16 @@ def shortify(text):          if c == '>':              break          if c == '<': -            short_desc = desc[:-(idx+1)] +            short_desc = desc[:-(idx + 1)]              break      if not short_desc:          for idx, c in enumerate(reversed(desc)):              if c == ' ' or c == '\n': -                short_desc = desc[:-(idx+1)] +                short_desc = desc[:-(idx + 1)]                  break      return BeautifulSoup(short_desc).prettify() +  class News(models.Model):      """News of the site      """ @@ -110,33 +116,39 @@ class News(models.Model):      url = models.URLField(_(u"Url"), max_length=200, blank=True, null=True)      areas = SelectMultipleField('Area', verbose_name=_(u"Associated areas"),                                  blank=True, null=True) -    def __unicode__(self): -        ordering = ["-date"] -        return self.title +      class Meta: +        ordering = ["-date"]          verbose_name = _(u"News")          verbose_name_plural = _(u"News") +    def __unicode__(self): +        return self.title +      @property      def short_desc(self):          return shortify(self.content) +  class TinyUrl(models.Model):      """Tinyfied version of permalink parameters      """ -    parameters = models.CharField(_(u"Parameters"), max_length=500, unique=True) -    def __unicode__(self): -        return self.parameters -    class Meta: -        verbose_name = _(u"TinyUrl") +    parameters = models.CharField(_(u"Parameters"), max_length=500, +                                  unique=True)      digits = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"      base = len(digits) +    class Meta: +        verbose_name = _(u"TinyUrl") + +    def __unicode__(self): +        return self.parameters +      @classmethod      def getParametersByUrn(cls, urn):          c_id = 0          for idx, char in enumerate(reversed(urn)): -            c_id += cls.digits.index(char)*pow(cls.base, idx) +            c_id += cls.digits.index(char) * pow(cls.base, idx)          try:              params = cls.objects.get(id=c_id).parameters          except cls.DoesNotExist: @@ -160,27 +172,38 @@ class TinyUrl(models.Model):                  break          return urn +  class ColorTheme(models.Model):      """Color theme      """      name = models.CharField(_(u"Name"), max_length=150) -    def __unicode__(self): -        return self.name +      class Meta:          verbose_name = _(u"Color theme") +    def __unicode__(self): +        return self.name + +  class Color(models.Model):      """Color      """ -    code = models.CharField(_(u"Code"), max_length=6) +    code = models.CharField(_(u"Code/name"), max_length=200, +                            help_text=_(u"HTML code/name")) +    inner_code = models.CharField(_(u"Code/name (inner)"), max_length=200, +                                  help_text=_(u"HTML code/name"), +                                  blank=True, null=True)      order = models.IntegerField(_(u"Order"))      color_theme = models.ForeignKey(ColorTheme, verbose_name=_(u"Color theme")) -    def __unicode__(self): -        return self.code +      class Meta:          ordering = ["order"]          verbose_name = _(u"Color") +    def __unicode__(self): +        return self.code + +  class Category(models.Model):      """Category of Point Of Interest (POI)      """ @@ -188,12 +211,15 @@ class Category(models.Model):      available = models.BooleanField(_(u"Available"))      order = models.IntegerField(_(u"Order"))      description = models.TextField(blank=True, null=True) -    def __unicode__(self): -        return self.name +      class Meta:          ordering = ["order"]          verbose_name = _(u"Category") +    def __unicode__(self): +        return self.name + +  class Icon(models.Model):      '''Icon      ''' @@ -202,11 +228,25 @@ class Icon(models.Model):                                height_field='height', width_field='width')      height = models.IntegerField(_(u"Height"))      width = models.IntegerField(_(u"Width")) +    offset_x = models.IntegerField( +        _(u"Offset x"), default=10, +        help_text=_(u"Common value is half the icon width")) +    offset_y = models.IntegerField( +        _(u"Offset y"), default=20, +        help_text=_(u"Common value is icon height")) +    popup_offset_x = models.IntegerField(_(u"Popup offset x"), default=0, +                                         help_text=_(u"Common value is 0")) +    popup_offset_y = models.IntegerField( +        _(u"Popup offset y"), default=20, +        help_text=_(u"Common value is icon height")) +      def __unicode__(self):          return self.name +      class Meta:          verbose_name = _(u"Icon") +  class SubCategory(models.Model):      '''Sub-category      ''' @@ -218,28 +258,34 @@ class SubCategory(models.Model):                                       default=True)      TYPE = (('M', _(u'Marker')),              ('R', _(u'Route')), +            ('P', _(u'Polygon')),              ('B', _(u'Both')),)      item_type = models.CharField(_(u"Item type"), max_length=1, choices=TYPE)      dated = models.BooleanField(_(u"Is dated"), default=False)      description = models.TextField(blank=True, null=True)      icon = models.ForeignKey(Icon, verbose_name=_(u"Icon")) -    hover_icon = models.ForeignKey(Icon, verbose_name=_(u"Hover icon"), -                          blank=True, null=True, related_name='subcat_hovered') +    hover_icon = models.ForeignKey( +        Icon, verbose_name=_(u"Hover icon"), blank=True, null=True, +        related_name='subcat_hovered')      color_theme = models.ForeignKey(ColorTheme, verbose_name=_(u"Color theme"),                                      blank=True, null=True)      as_layer = models.BooleanField(_(u"Displayed in the layer menu"),                                     default=False) +    weight_formula = models.TextField(_(u"Weight formula"), default="", +                                      blank=True, null=True)      routing_warn = models.BooleanField(_(u"Routing warn"), default=False)      order = models.IntegerField(_(u"Order"), default=1000)      keywords = models.TextField(_(u"Keywords"), max_length=200, -                                        blank=True, null=True) -    def __unicode__(self): -        return u"%s / %s" % (self.category.name, self.name) +                                blank=True, null=True) +      class Meta:          ordering = ["category", "order"]          verbose_name = _(u"Sub-category")          verbose_name_plural = _(u"Sub-categories") +    def __unicode__(self): +        return u"%s / %s" % (self.category.name, self.name) +      @classmethod      def getAvailable(cls, item_types=None, area_name=None, public=False):          '''Get list of tuples with first the category and second the associated @@ -270,8 +316,8 @@ class SubCategory(models.Model):                  sub_category.category.selected = True              sub_categories[sub_category.category].append(sub_category) -        subcategories = [(cat, subcats) \ -                           for cat, subcats in sub_categories.items()] +        subcategories = [(cat, subcats) +                         for cat, subcats in sub_categories.items()]          get_cat_order = lambda cat_tuple: cat_tuple[0].order          subcategories = sorted(subcategories, key=get_cat_order)          return subcategories @@ -286,15 +332,20 @@ class SubCategory(models.Model):          return cats      def getJSONDict(self): -        items = {'id':self.pk, 'name':self.name, -                 'description':self.description if self.description\ -                               else '', -                 'icon':{'url':self.icon.image.url, -                         'width':self.icon.image.width, -                         'height':self.icon.image.height} -                } +        items = {'id': self.pk, 'name': self.name, +                 'description': self.description if self.description +                                else '', +                 'icon': {'url': self.icon.image.url, +                          'width': self.icon.image.width, +                          'height': self.icon.image.height, +                          'offset_x': self.icon.offset_x, +                          'offset_y': self.icon.offset_y, +                          'popup_offset_x': self.icon.popup_offset_x, +                          'popup_offset_y': self.icon.popup_offset_y} +                 } +          if self.hover_icon: -            items['icon_hover'] = {'url':self.hover_icon.image.url} +            items['icon_hover'] = {'url': self.hover_icon.image.url}          return items      def getJSON(self, categories_id=[]): @@ -370,10 +421,10 @@ class Importer(models.Model):      source = models.CharField(_(u"Web address"), max_length=200,                                blank=True, null=True,                                help_text=_(u"Don't forget the trailing slash")) -    source_file = models.FileField(_(u"Source file"), -                        upload_to='import_files', blank=True, null=True) -    source_file_alt = models.FileField(_(u"Alt source file"), -                        upload_to='import_files', blank=True, null=True) +    source_file = models.FileField( +        _(u"Source file"), upload_to='import_files', blank=True, null=True) +    source_file_alt = models.FileField( +        _(u"Alt source file"), upload_to='import_files', blank=True, null=True)      default_name = models.CharField(_(u"Name by default"), max_length=200,                                      blank=True, null=True)      srid = models.IntegerField(_(u"SRID"), blank=True, null=True) @@ -388,19 +439,20 @@ class Importer(models.Model):                                blank=True, null=True)      license = models.CharField(_(u"License"), max_length=1000,                                 blank=True, null=True) -    categories = SelectMultipleField(SubCategory, blank=True, null=True, -                      verbose_name=_(u"Associated subcategories")) +    categories = SelectMultipleField( +        SubCategory, blank=True, null=True, +        verbose_name=_(u"Associated subcategories"))      state = models.TextField(_(u"State"), blank=True, null=True) -    associate_marker_to_way = models.BooleanField(_(u"Automatically associate "\ -                                           u"a marker to a way"), default=False) +    associate_marker_to_way = models.BooleanField( +        _(u"Automatically associate a marker to a way"), default=False)      automatic_update = models.BooleanField(_(u"Automatically updated"),                                             default=False)      default_status = models.CharField(_(u"Default status"), max_length=1,                                        choices=STATUS, default='I') -    default_localisation = PointField(_(u"Default localisation"), -                               srid=settings.CHIMERE_EPSG_DISPLAY_PROJECTION, -                               blank=True, null=True, -                               widget=HiddenPointChooserWidget) +    default_localisation = PointField( +        _(u"Default localisation"), +        srid=settings.CHIMERE_EPSG_DISPLAY_PROJECTION, blank=True, null=True, +        widget=HiddenPointChooserWidget)      class Meta:          verbose_name = _(u"Importer") @@ -446,47 +498,166 @@ class ImporterKeyCategories(models.Model):      class Meta:          verbose_name = _(u"Importer - Key categories") +  class GeographicItem(models.Model): -    name = models.TextField(_(u"Name"))      categories = SelectMultipleField(SubCategory) -    submiter_session_key = models.CharField(_(u"Submitter session key"), -                                        blank=True, null=True, max_length=40) +    name = models.TextField(_(u"Name")) +    submiter_session_key = models.CharField( +        _(u"Submitter session key"), blank=True, null=True, max_length=40)      submiter_name = models.CharField(_(u"Submitter name or nickname"),                                       blank=True, null=True, max_length=40)      submiter_email = models.EmailField(_(u"Submitter email"), blank=True,                                         null=True) -    submiter_comment = models.TextField(_(u"Submitter comment"), max_length=200, -                                        blank=True, null=True) +    submiter_comment = models.TextField(_(u"Submitter comment"), +                                        max_length=200, blank=True, null=True)      status = models.CharField(_(u"Status"), max_length=1, choices=STATUS) -    keywords = models.TextField(_(u"Keywords"), max_length=200, -                                        blank=True, null=True) +    keywords = models.TextField(_(u"Keywords"), max_length=200, blank=True, +                                null=True)      import_key = models.CharField(_(u"Import key"), max_length=200,                                    blank=True, null=True)      import_version = models.IntegerField(_(u"Import version"),                                           blank=True, null=True)      import_source = models.CharField(_(u"Source"), max_length=200,                                       blank=True, null=True) -    modified_since_import = models.BooleanField(_(u"Modified since last import"), -                                                default=True) +    modified_since_import = models.BooleanField( +        _(u"Modified since last import"), default=True)      not_for_osm = models.BooleanField(_(u"Not to be exported to OSM"),                                        default=False)      origin = models.CharField(_(u"Origin"), max_length=1000,                                blank=True, null=True)      license = models.CharField(_(u"License"), max_length=1000,                                 blank=True, null=True) -    start_date = models.DateField(_(u"Start date"), blank=True, null=True, -        help_text=_(u"Not mandatory. Set it for dated item such as event. "\ +    start_date = models.DateField( +        _(u"Start date"), blank=True, null=True, +        help_text=_(u"Not mandatory. Set it for dated item such as event. "                      u"Format YYYY-MM-DD")) -    end_date = models.DateField(_(u"End date"), blank=True, null=True, -        help_text=_(u"Not mandatory. Set it only if you have a multi-day "\ +    end_date = models.DateField( +        _(u"End date"), blank=True, null=True, +        help_text=_(u"Not mandatory. Set it only if you have a multi-day "                      u"event. Format YYYY-MM-DD")) +    weight = models.FloatField( +        _(u"Weight"), blank=True, null=True, +        help_text=_( +            u"Weight are used for heatmap and clustering. A formula must " +            u"defined in the associated category.")) +    normalised_weight = models.FloatField( +        _(u"Normalised weight"), blank=True, null=True, +        help_text=_(u"The weight normalised to be between 0 and 1. " +                    u"Automatically recalculated.")) +      class Meta:          abstract = True +    def __unicode__(self): +        return self.name + +    def __init__(self, *args, **kwargs): +        super(GeographicItem, self).__init__(*args, **kwargs) +        # add read attributes for properties +        for pm in self.all_properties(): +            attr_name = pm.getAttrName() +            if not hasattr(self, attr_name): +                val = '' +                property = self.getProperty(pm) +                if property: +                    val = property.python_value +                setattr(self, attr_name, val) +            if not hasattr(self, attr_name + '_set'): +                setattr(self, attr_name + '_set', +                        property_setter(self.__class__, pm)) + +    def get_geometry(self): +        return getattr(self, self.geom_attr) + +    @property +    def geometry(self): +        return getattr(self, self.geom_attr).wkt + +    def _get_geom_item_fk_name(self): +        geom_attr = self.geom_attr +        if self.geom_attr == 'route': +            # # TODO v3 - backport routes +            geom_attr = 'point' +        return GEOM_TO_GEOM_ITEM[geom_attr].lower() + +    def getProperty(self, propertymodel, safe=None): +        """Get the property of an associated property model. +        If safe set to True, verify if the property is available +        """ +        if safe and not propertymodel.available: +            return +        geom_item = self._get_geom_item_fk_name() +        try: +            d = {'propertymodel': propertymodel, +                 geom_item: self} +            property = Property.objects.get(**d) +        except Property.DoesNotExist: +            return +        return property + +    def getProperties(self, area_name=None): +        """Get all the property availables +        """ +        properties = [] +        querys = PropertyModel.getAvailable(area_name=area_name) +        for query in querys: +            for pm in query.all(): +                property = self.getProperty(pm) +                if property: +                    properties.append(property) +        return properties + +    def setProperty(self, pm, value): +        u""" +        Set a property +        """ +        if not hasattr(pm, 'pk'): +            pm = PropertyModel.objects.get(slug=pm) +        geom_item = self._get_geom_item_fk_name() +        d = {'propertymodel': pm, geom_item: self} +        q = Property.objects.filter(**d) +        properties = q.all() +        # in case of multiple edition as the same time delete arbitrary +        # the others +        if len(properties) > 1: +            for property in properties[1:]: +                property.delete() +        if pm.type == 'C' and value: +            try: +                value = str(int(value)) +            except ValueError: +                choice = PropertyModelChoice.objects.filter(propertymodel=pm, +                                                            value=value) +                if choice.count(): +                    value = choice.all()[0].pk +                else: +                    choice = PropertyModelChoice.objects.create( +                        value=value, propertymodel=pm) +                    value = choice.pk +        # new property +        if not properties: +            d = {'propertymodel': pm, geom_item: self, 'value': value} +            new_property = Property.objects.create(**d) +            new_property.save() +        else: +            property = properties[0] +            property.value = value +            property.save() + +    def saveProperties(self, values): +        """ +        Save properties +        """ +        for propertymodel in PropertyModel.objects.filter(available=True): +            val = u"" +            if unicode(propertymodel.id) in values: +                val = values[unicode(propertymodel.id)] +            self.setProperty(propertymodel, val) +      def get_key(self, key):          key_vals = self.import_key.split(';')          for k_v in key_vals: -            if k_v.startswith(key+':'): +            if k_v.startswith(key + ':'):                  return k_v.split(':')[1]      def set_key(self, key, value): @@ -513,8 +684,8 @@ class GeographicItem(models.Model):      def has_modified(self):          if (self.ref_item and self.ref_item != self) \ -           or self.__class__.objects.filter(ref_item=self -                                ).exclude(pk=self.pk).count(): +           or self.__class__.objects.filter( +                ref_item=self).exclude(pk=self.pk).count():              return True          return False @@ -526,25 +697,103 @@ class GeographicItem(models.Model):      def all_properties(cls):          return [pm for pm in PropertyModel.objects.all()] +    def get_init_multi(self): +        multis = [forms.model_to_dict(multi) +                  for multi in self.multimedia_files.all()] +        return multis + +    def get_init_picture(self): +        picts = [forms.model_to_dict(pict) +                 for pict in self.pictures.all()] +        return picts + +    def get_full_dict(self): +        dct = {} +        # get all property even the one not displayed +        for pm in PropertyModel.objects.all(): +            dct[pm.slug] = unicode(self.getProperty(pm)) +        return dct + +    def calculate_weight(self, formula): +        try: +            # try to eval the formula +            # safe because open to admin only +            return round(eval(formula.format(**self.get_full_dict())), 10) +        except: +            return 0 + +    def get_weight_formula(self, get_associated_cat=False): +        for sub in self.categories.order_by('order').all(): +            if sub.weight_formula: +                if get_associated_cat: +                    return sub.weight_formula, sub +                return sub.weight_formula +        if get_associated_cat: +            return None, None +        return None + +    def normalise_weight(self): +        formula, cat = self.get_weight_formula(get_associated_cat=True) +        if not formula: +            return +        q = self.__class__.objects.filter( +            categories__pk=cat.pk, weight__isnull=False) +        if not q.count(): +            return 0 +        min_weight = q.order_by('weight')[0].weight or 0 +        max_weight = q.order_by('-weight')[0].weight or 0 +        return 1 - round( +            (max_weight - self.weight or 0) / +            (float((max_weight - min_weight)) or 1), 5) + + +def weighted_post_save(sender, **kwargs): +    if not kwargs['instance']: +        return +    obj = kwargs['instance'] +    formula = obj.get_weight_formula() +    weight, normalised_weight = None, None +    if formula: +        weight = obj.calculate_weight(formula) +        if weight != obj.weight: +            obj.weight = weight +            obj.save() +            return +        normalised_weight = obj.normalise_weight() +    if weight != obj.weight or normalised_weight != obj.normalised_weight: +        obj.weight = weight +        obj.normalised_weight = normalised_weight +        obj.save() + +  def property_setter(cls, propertymodel):      def setter(self, value): -        marker = self +        item = self          if cls == Route: +            # TODO v3              if not self.associated_marker.objects.count():                  return -            marker = self.associated_marker.objects.all()[0] -        marker.setProperty(propertymodel, value) +            item = self.associated_marker.objects.all()[0] +        item.setProperty(propertymodel, value)      return setter +GEOM_TO_GEOM_ITEM = { +    "point": "Marker", +    "route": "Route", +    "polygon": "Polygon" +} + +  class Marker(GeographicItem):      '''Marker for a POI      ''' -    ref_item = models.ForeignKey("Marker", blank=True, null=True, -            verbose_name=_(u"Reference marker"), related_name='submited_marker') +    ref_item = models.ForeignKey( +        "Marker", blank=True, null=True, verbose_name=_(u"Reference marker"), +        related_name='submited_marker')      point = PointField(_(u"Localisation"),                         srid=settings.CHIMERE_EPSG_DISPLAY_PROJECTION)      available_date = models.DateTimeField(_(u"Available Date"), blank=True, -                                          null=True) # used by feeds +                                          null=True)  # used by feeds      route = models.ForeignKey(u"Route", blank=True, null=True,                                related_name='associated_marker')      description = models.TextField(_(u"Description"), blank=True, null=True) @@ -552,33 +801,9 @@ class Marker(GeographicItem):                                              null=True)      objects = models.GeoManager() -    def __unicode__(self): -        return self.name - -    def __init__(self, *args, **kwargs): -        super(Marker, self).__init__(*args, **kwargs) -        # add read attributes for properties -        for pm in self.all_properties(): -            attr_name = pm.getAttrName() -            if not hasattr(self, attr_name): -                val = '' -                property = self.getProperty(pm) -                if property: -                    val = property.python_value -                setattr(self, attr_name, val) -            if not hasattr(self, attr_name + '_set'): -                setattr(self, attr_name + '_set', -                              property_setter(self.__class__, pm)) - -    def get_init_multi(self): -        multis = [forms.model_to_dict(multi) -                  for multi in self.multimedia_files.all()] -        return multis - -    def get_init_picture(self): -        picts = [forms.model_to_dict(pict) -                 for pict in self.pictures.all()] -        return picts +    class Meta: +        ordering = ('status', 'name') +        verbose_name = _(u"Point of interest")      @property      def multimedia_items(self): @@ -609,17 +834,9 @@ class Marker(GeographicItem):          return shortify(self.description)      @property -    def geometry(self): -        return self.point.wkt - -    @property      def geom_attr(self):          return 'point' -    class Meta: -        ordering = ('status', 'name') -        verbose_name = _(u"Point of interest") -      def getLatitude(self):          '''Return the latitude          ''' @@ -630,93 +847,33 @@ class Marker(GeographicItem):          '''          return self.point.x -    def getProperty(self, propertymodel, safe=None): -        """Get the property of an associated property model. -        If safe set to True, verify if the property is available -        """ -        if safe and not propertymodel.available: -            return -        try: -            property = Property.objects.get(propertymodel=propertymodel, -                                            marker=self) -        except Property.DoesNotExist: -            return -        return property - -    def getProperties(self): -        """Get all the property availables -        """ -        properties = [] -        for pm in PropertyModel.objects.filter(available=True): -            property = self.getProperty(pm) -            if property: -                properties.append(property) -        return properties - -    def setProperty(self, pm, value): -        u""" -        Set a property -        """ -        properties = Property.objects.filter(marker=self, -                                             propertymodel=pm).all() -        # in case of multiple edition as the same time delete arbitrary -        # the others -        if len(properties) > 1: -            for property in properties[1:]: -                property.delete() -        if pm.type == 'C' and value: -            try: -                value = str(int(value)) -            except ValueError: -                choice = PropertyModelChoice.objects.filter(propertymodel=pm, -                                                            value=value) -                if choice.count(): -                    value = choice.all()[0].pk -                else: -                    choice = PropertyModelChoice.objects.create(value=value, -                                                            propertymodel=pm) -                    value = choice.pk -        # new property -        if not properties: -            new_property = Property.objects.create(marker=self, -                                propertymodel=pm, -                                value=value) -            new_property.save() -        else: -            property = properties[0] -            property.value = value -            property.save() - -    def saveProperties(self, values): -        """ -        Save properties -        """ -        for propertymodel in PropertyModel.objects.filter(available=True): -            val = u"" -            if unicode(propertymodel.id) in values: -                val = values[unicode(propertymodel.id)] -            self.setProperty(propertymodel, val) -      def getGeoJSON(self, categories_id=[]):          '''Return a GeoJSON string          '''          jsons = [] -        json_tpl = {"type":"Feature", "properties":{}} +        json_tpl = {"type": "Feature", "properties": {}}          for cat in self.categories.all():              if categories_id and cat.id not in categories_id:                  continue              items = copy.deepcopy(json_tpl)              items['geometry'] = json.loads(self.point.geojson)              items['properties'].update({ -                     'pk':self.id, -                     'name':self.name, -                     'icon_path':unicode(cat.icon.image), -                     'icon_hover_path':unicode(cat.hover_icon.image) \ -                                       if cat.hover_icon else '', -                     'category_name':cat.name}) +                'pk': self.id, +                'key': "marker-{}".format(self.id), +                'name': self.name, +                'icon_path': unicode(cat.icon.image), +                'icon_hover_path': unicode(cat.hover_icon.image) +                if cat.hover_icon else '', +                'icon_offset_x': cat.icon.offset_x, +                'icon_offset_y': cat.icon.offset_y, +                'icon_popup_offset_x': cat.icon.popup_offset_x, +                'icon_popup_offset_y': cat.icon.popup_offset_y, +                'category_name': cat.name})              try: -                items['properties'].update({'icon_width':cat.icon.image.width, -                                          'icon_height':cat.icon.image.height,}) +                items['properties'].update( +                    {'icon_width': cat.icon.image.width, +                     'icon_height': cat.icon.image.height, +                     })              except IOError:                  pass @@ -742,11 +899,13 @@ class Marker(GeographicItem):          return url  PRE_ATTRS = { -    'Marker':('name', 'description', 'start_date', 'geometry', 'import_version', -              'modified_since_import'), -    'Route':('name', 'geometry', 'import_version', 'modified_since_import'), -    'Area':('urn', 'name'), -    } +    'Marker': ('name', 'description', 'start_date', 'geometry', +               'import_version', 'modified_since_import'), +    'Route': ('name', 'geometry', 'import_version', 'modified_since_import'), +    'Area': ('urn', 'name'), +} + +  def geometry_pre_save(cls, pre_save_geom_values):      def geom_pre_save(sender, **kwargs):          if not kwargs['instance'] or not kwargs['instance'].pk: @@ -762,12 +921,15 @@ def geometry_pre_save(cls, pre_save_geom_values):      return geom_pre_save  pre_save_marker_values = {} + +  def marker_pre_save(sender, **kwargs):      if not kwargs['instance']:          return      geometry_pre_save(Marker, pre_save_marker_values)(sender, **kwargs)  pre_save.connect(marker_pre_save, sender=Marker) +  def geometry_post_save(pre_save_geom_values):      def geom_post_save(sender, **kwargs):          if not kwargs['instance'] \ @@ -785,18 +947,147 @@ def geometry_post_save(pre_save_geom_values):              return          if instance.modified_since_import:              return -        if [key for key in pre if pre not in ('import_version', -                                        'modified_since_import') and -                           getattr(instance, key) != pre[key]]: +        if [key for key in pre if pre not in ( +            'import_version', 'modified_since_import') and +                getattr(instance, key) != pre[key]]:              instance.modified_since_import = True              instance.save()      return geom_post_save + +  def marker_post_save(sender, **kwargs): +    weighted_post_save(sender, **kwargs)      if not kwargs['instance'] or kwargs['created']:          return      geometry_post_save(pre_save_marker_values)(sender, **kwargs)  post_save.connect(marker_post_save, sender=Marker) + +class Polygon(GeographicItem): +    '''Polygon on the map +    ''' +    ref_item = models.ForeignKey( +        "Polygon", blank=True, null=True, verbose_name=_(u"Reference polygon"), +        related_name='submited_polygon') +    polygon = PolygonField( +        _(u"Polygon"), srid=settings.CHIMERE_EPSG_DISPLAY_PROJECTION) +    picture = models.ImageField( +        _(u"Image"), upload_to='upload', blank=True, null=True, +        height_field='height', width_field='width') +    height = models.IntegerField(_(u"Height"), blank=True, null=True) +    width = models.IntegerField(_(u"Width"), blank=True, null=True) +    color = models.CharField( +        _(u"Color"), max_length=200, help_text=_(u"HTML code/name"), +        blank=True, null=True) +    inner_color = models.CharField( +        _(u"Inner color"), max_length=200, +        help_text=_(u"HTML code/name"), blank=True, null=True) +    objects = models.GeoManager() + +    class Meta: +        ordering = ('status', 'name') +        verbose_name = _(u"Polygon") + +    @property +    def geom_attr(self): +        return 'polygon' + +    def getGeoJSON(self, color="#000", inner_color='#0F0'): +        '''Return a GeoJSON string +        ''' +        attributes = {"type": "Feature", +                      "geometry": json.loads(self.polygon.geojson), +                      "properties": {"pk": self.id, "name": self.name, +                                     'key': "polygon-{}".format(self.pk), +                                     "color": self.color or color, +                                     "inner_color": self.inner_color +                                     or inner_color}} +        return json.dumps(attributes) + +    @classmethod +    def getGeoJSONs(self, queryset, color="#000", +                    inner_color='rgba(180, 180, 180, 0.3)', +                    limit_to_categories=[]): +        vals, default_color, default_inner_color = [], color, inner_color +        q = queryset.select_related('categories').extra( +            select={'json': 'ST_AsGeoJSON(polygon)'}).values( +                'json', 'name', 'pk', 'inner_color', 'color', 'categories__pk') +        added = [] +        current_categories = {} +        for polygon in q.all(): +            if polygon['pk'] in added: +                continue +            if limit_to_categories and \ +                    polygon["categories__pk"] not in limit_to_categories: +                continue +            color = default_color +            if polygon["color"]: +                color = polygon['color'] +            elif polygon["categories__pk"]: +                if polygon["categories__pk"] not in current_categories: +                    cat = SubCategory.objects.get(pk=polygon["categories__pk"]) +                    # [index, color list] +                    current_categories[polygon["categories__pk"]] = \ +                        [0, list(Color.objects.filter( +                            color_theme=cat.color_theme))] +                idx, colors = current_categories[polygon["categories__pk"]] +                # category have a color theme +                if colors: +                    c = colors[idx % len(colors)] +                    color = c.code +                    if c.inner_code: +                        inner_color = c.inner_code +                    # index += 1 +                    current_categories[polygon["categories__pk"]][0] += 1 +            if polygon["inner_color"]: +                inner_color = polygon["inner_color"] +            elif not inner_color: +                inner_color = default_inner_color +            vals.append({ +                "type": "Feature", +                "geometry": json.loads(polygon['json']), +                "properties": {"pk": polygon['pk'], "name": polygon['name'], +                               'key': "polygon-{}".format(polygon['pk']), +                               'color': color, +                               'inner_color': inner_color}}) +            added.append(polygon['pk']) +        return vals + +    def get_full_dict(self): +        dct = super(Polygon, self).get_full_dict() +        # to be done - use local unity +        dct['area'] = self.polygon.area +        dct['length'] = self.polygon.length +        return dct + +post_save.connect(weighted_post_save, sender=Polygon) + + +class AggregatedPolygon(models.Model): +    ''' +    Database view for aggregated polygons +    ''' +    polygon = models.MultiPolygonField() +    subcategory = models.ForeignKey(SubCategory) +    status = models.CharField(_(u"Status"), max_length=1, choices=STATUS) + +    class Meta: +        managed = False +        db_table = 'chimere_aggregated_polygons' + +    def getGeoJSON(self, color="#000"): +        '''Return a GeoJSON string +        ''' +        if '#' not in color: +            color = '#' + color +        attributes = { +            'color': color, 'geometry': json.loads(self.polygon.geojson), +            'type': "Feature", "properties": { +                'key': "aggpoly-{}".format(self.pk), +                "pk": self.id, "name": u'Aggregated polygon'}} +        return json.dumps(attributes) + +  class MultimediaType(models.Model):      MEDIA_TYPES = (('A', _(u"Audio")),                     ('V', _(u"Video")), @@ -832,25 +1123,27 @@ class MultimediaType(models.Model):          return tuples  IFRAME_LINKS = { -    'youtube':((re.compile(r'youtube.com\/watch\?[A-Za-z0-9_\-\=\&]*v=([A-Za-z0-9_-]*)[A-Za-z0-9_\-\=\&]*'), -                re.compile(r'youtu.be\/([A-Za-z0-9_-]*)'), -                re.compile(r'youtube.com\/embed\/([A-Za-z0-9_-]*)')), -                 "http://www.youtube.com/embed/%s"), -    'dailymotion':( +    'youtube': ((re.compile(r'youtube.com\/watch\?[A-Za-z0-9_\-\=\&]*v=' +                            r'([A-Za-z0-9_-]*)[A-Za-z0-9_\-\=\&]*'), +                 re.compile(r'youtu.be\/([A-Za-z0-9_-]*)'), +                 re.compile(r'youtube.com\/embed\/([A-Za-z0-9_-]*)')), +                "http://www.youtube.com/embed/%s"), +    'dailymotion': (          (re.compile(r'dailymotion.com/video/([A-Za-z0-9]*)_[A-Za-z0-9_-]*'),           re.compile(r'dailymotion.com/embed/video/([A-Za-z0-9]*)'),           re.compile("http://www.dailymotion.com/embed/video/%s")), -         'http://www.dailymotion.com/embed/video/%s'), -    'vimeo':((re.compile(r'vimeo.com/video/([A-Za-z0-9]*)'), -              re.compile(r'vimeo.com/([A-Za-z0-9]*)'),), +        'http://www.dailymotion.com/embed/video/%s'), +    'vimeo': ((re.compile(r'vimeo.com/video/([A-Za-z0-9]*)'), +               re.compile(r'vimeo.com/([A-Za-z0-9]*)'),),                "http://player.vimeo.com/video/%s")  } +  class MultimediaExtension(models.Model):      name = models.CharField(_(u"Extension name"), max_length=6) -    multimedia_type = models.ForeignKey(MultimediaType, -                        verbose_name=_(u"Associated multimedia type"), -                        related_name='extensions') +    multimedia_type = models.ForeignKey( +        MultimediaType, verbose_name=_(u"Associated multimedia type"), +        related_name='extensions')      class Meta:          verbose_name = _(u"Multimedia extension") @@ -859,14 +1152,19 @@ class MultimediaExtension(models.Model):      def __unicode__(self):          return self.name +  class MultimediaFile(models.Model):      name = models.CharField(_(u"Name"), max_length=150)      url = models.URLField(_(u"Url"), max_length=200)      order = models.IntegerField(_(u"Order"), default=1)      multimedia_type = models.ForeignKey(MultimediaType, blank=True, null=True) -    miniature = models.BooleanField(_(u"Display inside the description?"), -                                  default=settings.CHIMERE_MINIATURE_BY_DEFAULT) -    marker = models.ForeignKey(Marker, related_name='multimedia_files') +    miniature = models.BooleanField( +        _(u"Display inside the description?"), +        default=settings.CHIMERE_MINIATURE_BY_DEFAULT) +    marker = models.ForeignKey(Marker, related_name='multimedia_files', +                               blank=True, null=True) +    polygon = models.ForeignKey(Polygon, related_name='multimedia_files', +                                blank=True, null=True)      class Meta:          verbose_name = _(u"Multimedia file") @@ -875,6 +1173,7 @@ class MultimediaFile(models.Model):      def __unicode__(self):          return self.name or u"" +  def multimediafile_post_save(sender, **kwargs):      if not kwargs['instance'] or not kwargs['created']:          return @@ -886,7 +1185,7 @@ def multimediafile_post_save(sender, **kwargs):              res, embeded_url = IFRAME_LINKS[mm_type]              if [r for r in res if r.search(url)]:                  multimedia_type = MultimediaType.objects.get( -                                    name__iexact=mm_type) +                    name__iexact=mm_type)                  multimediafile.multimedia_type = multimedia_type          if not multimediafile.multimedia_type:              ext = url.split(".")[-1] @@ -896,10 +1195,12 @@ def multimediafile_post_save(sender, **kwargs):              else:                  # default to an iframe                  multimediafile.multimedia_type = \ -                   MultimediaType.objects.filter(name__iexact='iframe').all()[0] +                    MultimediaType.objects.filter(name__iexact='iframe')\ +                                          .all()[0]      # manage iframe of video providers      if multimediafile.multimedia_type.name.lower() in IFRAME_LINKS: -        regexps, lnk = IFRAME_LINKS[multimediafile.multimedia_type.name.lower()] +        regexps, lnk = IFRAME_LINKS[ +            multimediafile.multimedia_type.name.lower()]          key = None          for regexp in regexps:              key = regexp.findall(multimediafile.url) @@ -909,8 +1210,9 @@ def multimediafile_post_save(sender, **kwargs):          if key:              multimediafile.url = lnk % key -    mfs = MultimediaFile.objects.filter(marker=multimediafile.marker).exclude( -                                        pk=multimediafile.pk).order_by('order') +    mfs = MultimediaFile.objects.filter(marker=multimediafile.marker)\ +                                .exclude(pk=multimediafile.pk)\ +                                .order_by('order')      for idx, mf in enumerate(mfs.all()):          mf.order = idx + 1          mf.save() @@ -918,24 +1220,28 @@ def multimediafile_post_save(sender, **kwargs):      multimediafile.save()  post_save.connect(multimediafile_post_save, sender=MultimediaFile) +  class PictureFile(models.Model):      name = models.CharField(_(u"Name"), max_length=150)      picture = models.ImageField(_(u"Image"), upload_to='pictures',                                  height_field='height', width_field='width')      height = models.IntegerField(_(u"Height"), blank=True, null=True)      width = models.IntegerField(_(u"Width"), blank=True, null=True) -    miniature = models.BooleanField(_(u"Display inside the description?"), -                                  default=settings.CHIMERE_MINIATURE_BY_DEFAULT) -    thumbnailfile = models.ImageField(_(u"Thumbnail"), upload_to='pictures', -                        blank=True, null=True, -                        height_field='thumbnailfile_height', -                        width_field='thumbnailfile_width') +    miniature = models.BooleanField( +        _(u"Display inside the description?"), +        default=settings.CHIMERE_MINIATURE_BY_DEFAULT) +    thumbnailfile = models.ImageField( +        _(u"Thumbnail"), upload_to='pictures', blank=True, null=True, +        height_field='thumbnailfile_height', width_field='thumbnailfile_width')      thumbnailfile_height = models.IntegerField(_(u"Thumbnail height"),                                                 blank=True, null=True)      thumbnailfile_width = models.IntegerField(_(u"Thumbnail width"),                                                blank=True, null=True)      order = models.IntegerField(_(u"Order"), default=1) -    marker = models.ForeignKey(Marker, related_name='pictures') +    marker = models.ForeignKey(Marker, related_name='pictures', blank=True, +                               null=True) +    polygon = models.ForeignKey(Polygon, related_name='pictures', blank=True, +                                null=True)      def __unicode__(self):          return self.name or u"" @@ -944,6 +1250,7 @@ class PictureFile(models.Model):          verbose_name = _(u"Picture file")          verbose_name_plural = _(u"Picture files") +  def scale_image(max_x, pair):      x, y = pair      new_y = (float(max_x) / x) * y @@ -958,6 +1265,7 @@ IMAGE_EXIF_ORIENTATION_MAP = {  PYEXIV2_OLD_API = not hasattr(pyexiv2, 'ImageMetadata') +  def picturefile_post_save(sender, **kwargs):      if not kwargs['instance']:          return @@ -970,12 +1278,12 @@ def picturefile_post_save(sender, **kwargs):              metadata = pyexiv2.Image(filename)              metadata.readMetadata()              orientation = metadata['Exif.Image.Orientation'] \ -                   if 'Exif.Image.Orientation' in metadata.exifKeys() else None +                if 'Exif.Image.Orientation' in metadata.exifKeys() else None          else:              metadata = pyexiv2.ImageMetadata(filename)              metadata.read()              orientation = metadata['Exif.Image.Orientation'].value \ -                      if 'Exif.Image.Orientation' in metadata else None +                if 'Exif.Image.Orientation' in metadata else None          if orientation and orientation in IMAGE_EXIF_ORIENTATION_MAP \             and orientation > 1:              metadata['Exif.Image.Orientation'] = 1 @@ -993,7 +1301,7 @@ def picturefile_post_save(sender, **kwargs):          filehead, filetail = os.path.split(os.path.abspath(file.path))          basename, format = os.path.splitext(filetail)          basename = defaultfilters.slugify(basename) -        basename = re.sub(r'-','_', basename) +        basename = re.sub(r'-', '_', basename)          miniature = basename + '_thumb.jpg'          filename = file.path          miniature_filename = os.path.join(filehead, miniature) @@ -1005,12 +1313,10 @@ def picturefile_post_save(sender, **kwargs):              image_x, image_y = image.size              if settings.CHIMERE_THUMBS_SCALE_HEIGHT:                  image_y, image_x = scale_image( -                                          settings.CHIMERE_THUMBS_SCALE_HEIGHT, -                                          (image_y, image_x)) +                    settings.CHIMERE_THUMBS_SCALE_HEIGHT, (image_y, image_x))              elif settings.CHIMERE_THUMBS_SCALE_WIDTH:                  image_x, image_y = scale_image( -                                          settings.CHIMERE_THUMBS_SCALE_WIDTH, -                                          (image_x, image_y)) +                    settings.CHIMERE_THUMBS_SCALE_WIDTH, (image_x, image_y))              image.thumbnail([image_x, image_y], Image.ANTIALIAS)              temp_image = open(miniature_filename, 'w') @@ -1027,8 +1333,8 @@ def picturefile_post_save(sender, **kwargs):      if not kwargs['created']:          return -    pfs = PictureFile.objects.filter(marker=picturefile.marker).exclude( -                                     pk=picturefile.pk).order_by('order') +    pfs = PictureFile.objects.filter(marker=picturefile.marker)\ +                             .exclude(pk=picturefile.pk).order_by('order')      for idx, pf in enumerate(pfs.all()):          pf.order = idx + 1          pf.save() @@ -1036,12 +1342,13 @@ def picturefile_post_save(sender, **kwargs):      picturefile.save()  post_save.connect(picturefile_post_save, sender=PictureFile) +  class RouteFile(models.Model):      name = models.CharField(_(u"Name"), max_length=150)      raw_file = models.FileField(_(u"Raw file (gpx or kml)"),                                  upload_to='route_files') -    simplified_file = models.FileField(_(u"Simplified file"), -                        upload_to='route_files', blank=True, null=True) +    simplified_file = models.FileField( +        _(u"Simplified file"), upload_to='route_files', blank=True, null=True)      TYPE = (('K', _(u'KML')), ('G', _(u'GPX')))      file_type = models.CharField(max_length=1, choices=TYPE) @@ -1058,7 +1365,7 @@ class RouteFile(models.Model):              return          input_name = settings.MEDIA_ROOT + self.raw_file.name          output_name = settings.MEDIA_ROOT + self.raw_file.name[:-4] + \ -                      "_simplified" + ".gpx" +            "_simplified" + ".gpx"          cli_args = [settings.GPSBABEL, '-i']          if self.file_type == 'K':              cli_args.append('kml') @@ -1070,7 +1377,7 @@ class RouteFile(models.Model):          p.wait()          if p.returncode:              print p.stderr.read() -            #logger.error(p.stderr.read()) +            # logger.error(p.stderr.read())          else:              self.simplified_file = File(open(output_name))              self.save() @@ -1088,32 +1395,33 @@ class RouteFile(models.Model):              if not pt.tag.endswith('trkpt'):                  continue              pts.append((pt.get("lon"), pt.get("lat"))) -        geojson_tpl = u'{"type":"Feature", "geometry":{ "type": "LineString", '\ -                       '"coordinates":[%s]}}'          wkt_tpl = u'LINESTRING(%s)' -        return wkt_tpl % u','.join([u'%s %s' % (pt[0], pt[1]) \ -                                           for pt in pts]) +        return wkt_tpl % u','.join([u'%s %s' % (pt[0], pt[1]) +                                    for pt in pts]) +  class Route(GeographicItem):      '''Route on the map      ''' -    ref_item = models.ForeignKey("Route", blank=True, null=True, -              verbose_name=_(u"Reference route"), related_name='submited_route') +    ref_item = models.ForeignKey( +        "Route", blank=True, null=True, verbose_name=_(u"Reference route"), +        related_name='submited_route')      route = RouteField(_(u"Route"),                         srid=settings.CHIMERE_EPSG_DISPLAY_PROJECTION)      associated_file = models.ForeignKey(RouteFile, blank=True, null=True,                                          verbose_name=_(u"Associated file")) -    picture = models.ImageField(_(u"Image"), upload_to='upload', blank=True, -                          null=True, height_field='height', width_field='width') +    picture = models.ImageField( +        _(u"Image"), upload_to='upload', blank=True, null=True, +        height_field='height', width_field='width')      height = models.IntegerField(_(u"Height"), blank=True, null=True)      width = models.IntegerField(_(u"Width"), blank=True, null=True)      has_associated_marker = models.BooleanField(_(u"Has an associated marker"),                                                  default=True) +    color = models.CharField( +        _(u"Color"), max_length=200, help_text=_(u"HTML code/name"), +        blank=True, null=True)      objects = models.GeoManager() -    def __unicode__(self): -        return self.name -      class Meta:          ordering = ('status', 'name')          verbose_name = _(u"Route") @@ -1138,11 +1446,7 @@ class Route(GeographicItem):                  setattr(self, attr_name, val)              if not hasattr(self, attr_name + '_set'):                  setattr(self, attr_name + '_set', -                              property_setter(self.__class__, pm)) - -    @property -    def geometry(self): -        return self.route.wkt +                        property_setter(self.__class__, pm))      @property      def geom_attr(self): @@ -1151,8 +1455,10 @@ class Route(GeographicItem):      def get_init_multi(self):          if not self.associated_marker.count():              return [] -        multis = [forms.model_to_dict(multi) -            for multi in self.associated_marker.all()[0].multimedia_files.all()] +        multis = [ +            forms.model_to_dict(multi) +            for multi in self.associated_marker.all()[0].multimedia_files.all() +        ]          return multis      def get_init_picture(self): @@ -1162,68 +1468,55 @@ class Route(GeographicItem):                   for pict in self.associated_marker.all()[0].pictures.all()]          return picts -    def getProperty(self, propertymodel, safe=None): -        """Get the property of an associated property model. -        If safe set to True, verify if the property is available -        """ -        if safe and not propertymodel.available: -            return -        try: -            property = Property.objects.get(propertymodel=propertymodel, -                                            marker=self) -        except Property.DoesNotExist: -            return -        return property - -    def getProperties(self): -        """Get all the property availables -        """ -        properties = [] -        for pm in PropertyModel.objects.filter(available=True): -            property = self.getProperty(pm) -            if property: -                properties.append(property) -        return properties -      def getGeoJSON(self, color="#000"):          '''Return a GeoJSON string          ''' -        if '#' not in color: -            color = '#' + color -        attributes = {"type":"Feature", -                      "geometry":json.loads(self.route.geojson), -                      "properties":{"pk":self.id, "name":self.name, -                      "color":color}} +        attributes = {"type": "Feature", +                      "geometry": json.loads(self.route.geojson), +                      "properties": {"pk": self.id, "name": self.name, +                                     'key': "route-{}".format(self.pk), +                                     "color": color}}          return json.dumps(attributes) -    def getTinyUrl(self): -        parameters = 'current_feature=%d&checked_categories=%s' % (self.id, -                                                          self.categories[0].id) -        return TinyUrl.getUrnByParameters(parameters) +    def get_full_dict(self): +        dct = super(Route, self).get_full_dict() +        # to be done - use local unity +        dct['length'] = self.route.length +        return dct  pre_save_route_values = {} + +  def route_pre_save(sender, **kwargs):      if not kwargs['instance']:          return      geometry_pre_save(Route, pre_save_route_values)(sender, **kwargs)  pre_save.connect(route_pre_save, sender=Route) +  def route_post_save(sender, **kwargs):      if not kwargs['instance']:          return      geometry_post_save(pre_save_route_values)(sender, **kwargs) +    weighted_post_save(sender, **kwargs)      instance = kwargs['instance']      # manage associated marker      if instance.has_associated_marker:          marker_fields = [f.attname for f in Marker._meta.fields]          route_fields = [f.attname for f in Route._meta.fields] -        marker_dct = dict([(k, getattr(instance, k)) for k in marker_fields -                       if k in route_fields and k not in ('id', 'ref_item_id')]) -        marker_dct['point'] = "SRID=%d;POINT(%f %f)" % (instance.route.srid, -                                  instance.route[0][0], instance.route[0][1]) -        marker, created = Marker.objects.get_or_create(route=instance, -                                                       defaults=marker_dct) +        marker_dct = dict( +            [(k, getattr(instance, k)) for k in marker_fields +             if k in route_fields and k not in ('id', 'ref_item_id')]) +        marker_dct['point'] = "SRID=%d;POINT(%f %f)" % ( +            instance.route.srid, instance.route[0][0], instance.route[0][1]) +        try: +            marker, created = Marker.objects.get_or_create(route=instance, +                                                           defaults=marker_dct) +        except MultipleObjectsReturned: +            # db error - trying to continue... +            marker = Marker.objects.filter(route=instance).all()[0] +            created = False          if not created:              marker.status = instance.status              marker.point = marker_dct['point'] @@ -1241,6 +1534,7 @@ def route_post_save(sender, **kwargs):  post_save.connect(route_post_save, sender=Route) +  def sync_m2m_route(sender, **kwargs):      if kwargs['action'] not in ('post_add', 'post_clear', 'post_remove'):          return @@ -1256,6 +1550,7 @@ def sync_m2m_route(sender, **kwargs):          marker.categories.add(cat)  m2m_changed.connect(sync_m2m_route, sender=Route.categories.through) +  def getDateCondition():      '''      Return an SQL condition for apparition of dates @@ -1263,9 +1558,9 @@ def getDateCondition():      if not settings.CHIMERE_DAYS_BEFORE_EVENT:          return ""      now = datetime.datetime.now().strftime('%Y-%m-%d') -    after = (datetime.datetime.now() + \ -             datetime.timedelta(settings.CHIMERE_DAYS_BEFORE_EVENT) -                                                  ).strftime('%Y-%m-%d') +    after = (datetime.datetime.now() + +             datetime.timedelta(settings.CHIMERE_DAYS_BEFORE_EVENT)).strftime( +        '%Y-%m-%d')      date_condition = " and (%(alias)s.start_date is null or "      date_condition += "(%(alias)s.start_date >= '" + now + "' and "      date_condition += "%(alias)s.start_date <='" + after + "')" @@ -1273,6 +1568,7 @@ def getDateCondition():      date_condition += "%(alias)s.end_date >='" + now + "')) "      return date_condition +  class AggregatedRoute(models.Model):      '''      Database view for aggregated routes @@ -1280,6 +1576,7 @@ class AggregatedRoute(models.Model):      route = models.MultiLineStringField()      subcategory = models.ForeignKey(SubCategory)      status = models.CharField(_(u"Status"), max_length=1, choices=STATUS) +      class Meta:          managed = False          db_table = 'chimere_aggregated_routes' @@ -1289,11 +1586,14 @@ class AggregatedRoute(models.Model):          '''          if '#' not in color:              color = '#' + color -        attributes = {'color':color, 'geometry':json.loads(self.route.geojson), -                      'type':"Feature", "properties":{"pk": self.id, -                                "name": u'Aggregated route',}} +        attributes = { +            'color': color, 'geometry': json.loads(self.route.geojson), +            'type': "Feature", "properties": { +                'key': "aggroute-{}".format(self.pk), +                "pk": self.id, "name": u'Aggregated route'}}          return json.dumps(attributes) +  class SimplePoint:      """      Point in the map (not in the database) @@ -1301,6 +1601,7 @@ class SimplePoint:      def __init__(self, x, y):          self.x, self.y = x, y +  class SimpleArea:      """      Rectangular area of a map (not in the database) @@ -1336,7 +1637,7 @@ class SimpleArea:                  for subcat in subcats:                      subcategory_pks.append(unicode(subcat.pk))              if filter_available: -                wheres += ['subcat.available = TRUE',  'cat.available = TRUE'] +                wheres += ['subcat.available = TRUE', 'cat.available = TRUE']              wheres += ['subcat.id in (%s)' % ",".join(subcategory_pks)]          where = " where " + " and ".join(wheres) if wheres else "" @@ -1346,13 +1647,13 @@ class SimpleArea:          elif status:              equal_status = " in ('%s')" % "','".join(status)          area = u"ST_GeometryFromText('POLYGON((%f %f,%f %f,%f %f,%f %f, %f %f"\ -              u"))', %d)" % (self.upper_left_corner.x, self.upper_left_corner.y, -                          self.lower_right_corner.x, self.upper_left_corner.y, -                          self.lower_right_corner.x, self.lower_right_corner.y, -                          self.upper_left_corner.x, self.lower_right_corner.y, -                          self.upper_left_corner.x, self.upper_left_corner.y, -                          settings.CHIMERE_EPSG_DISPLAY_PROJECTION -                          ) +            u"))', %d)" % ( +                self.upper_left_corner.x, self.upper_left_corner.y, +                self.lower_right_corner.x, self.upper_left_corner.y, +                self.lower_right_corner.x, self.lower_right_corner.y, +                self.upper_left_corner.x, self.lower_right_corner.y, +                self.upper_left_corner.x, self.upper_left_corner.y, +                settings.CHIMERE_EPSG_DISPLAY_PROJECTION)          date_condition = getDateCondition()          sql_main = '''select subcat.id as id, subcat.category_id as category_id,          subcat.name as name, subcat.available as available, @@ -1360,35 +1661,63 @@ class SimpleArea:          subcat.order as order, subcat.item_type as item_type          from chimere_subcategory subcat          inner join chimere_category cat on cat.id=subcat.category_id''' +          sql = sql_main + '''          inner join chimere_marker mark on ST_Contains(%s, mark.point)''' % area          if equal_status:              sql += ' and mark.status' + equal_status -        sql += date_condition % {'alias':'mark'} +        sql += date_condition % {'alias': 'mark'}          sql += ''' -        inner join chimere_marker_categories mc on mc.subcategory_id=subcat.id and -        mc.marker_id=mark.id''' +        inner join chimere_marker_categories mc on mc.subcategory_id=subcat.id +        and mc.marker_id=mark.id'''          sql += where          subcats = set(SubCategory.objects.raw(sql)) +          sql = sql_main + '''          inner join chimere_route rt on (ST_Intersects(%s, rt.route) or          ST_Contains(%s, rt.route))''' % (area, area)          if equal_status:              sql += ' and rt.status' + equal_status -        sql += date_condition % {'alias':'rt'} +        sql += date_condition % {'alias': 'rt'}          sql += ''' -        inner join chimere_route_categories rc on rc.subcategory_id=subcat.id and -        rc.route_id=rt.id''' +        inner join chimere_route_categories rc on rc.subcategory_id=subcat.id +        and rc.route_id=rt.id'''          sql += where -        # subcats.union(set(SubCategory.objects.raw(sql))) +        # subcats.union(set(SubCategory.objects.raw(sql)))          # set union behave strangely. Doing it manualy...          for c in set(SubCategory.objects.raw(sql)):              subcats.add(c) + +        sql = sql_main + ''' +        inner join chimere_polygon pol on (ST_Intersects(%s, pol.polygon) or +        ST_Contains(%s, pol.polygon))''' % (area, area) +        if equal_status: +            sql += ' and pol.status' + equal_status +        sql += date_condition % {'alias': 'pol'} +        sql += ''' +        inner join chimere_polygon_categories pc on pc.subcategory_id=subcat.id +        and pc.polygon_id=pol.id''' +        sql += where +        # subcats.union(set(SubCategory.objects.raw(sql))) +        # set union behave strangely. Doing it manualy... +        for c in set(SubCategory.objects.raw(sql)): +            subcats.add(c) +          return subcats +    def getExtent(self): +        return (unicode(self.upper_left_corner.x), +                unicode(self.upper_left_corner.y), +                unicode(self.lower_right_corner.x), +                unicode(self.lower_right_corner.y)) + +  class Layer(models.Model):      name = models.CharField(_(u"Name"), max_length=150) -    layer_code = models.TextField(_(u"Layer code"), max_length=300) +    layer_code = models.TextField(_(u"Layer code")) +    extra_js_code = models.TextField( +        _(u"Extra JS code"), blank=True, null=True, default='', +        help_text=_(u"This code is loaded before the layer code."))      def __unicode__(self):          return self.name @@ -1396,6 +1725,7 @@ class Layer(models.Model):      class Meta:          verbose_name = _("Layer") +  class Area(models.Model, SimpleArea):      """Rectangular area of the map      """ @@ -1406,29 +1736,47 @@ class Area(models.Model, SimpleArea):                                         null=True)      order = models.IntegerField(_(u"Order"), unique=True)      available = models.BooleanField(_(u"Available")) -    upper_left_corner = models.PointField(_(u"Upper left corner"), -            default='POINT(0 0)', srid=settings.CHIMERE_EPSG_DISPLAY_PROJECTION) -    lower_right_corner = models.PointField(_(u"Lower right corner"), -            default='POINT(0 0)', srid=settings.CHIMERE_EPSG_DISPLAY_PROJECTION) -    default = models.NullBooleanField(_(u"Default area"), -                    help_text=_(u"Only one area is set by default")) +    upper_left_corner = models.PointField( +        _(u"Upper left corner"), default='POINT(0 0)', +        srid=settings.CHIMERE_EPSG_DISPLAY_PROJECTION) +    lower_right_corner = models.PointField( +        _(u"Lower right corner"), default='POINT(0 0)', +        srid=settings.CHIMERE_EPSG_DISPLAY_PROJECTION) +    default = models.NullBooleanField( +        _(u"Default area"), help_text=_(u"Only one area is set by default"))      layers = SelectMultipleField(Layer, related_name='areas',                                   through='AreaLayers', blank=True) -    default_subcategories = SelectMultipleField(SubCategory, blank=True, -                           verbose_name=_(u"Sub-categories checked by default")) +    default_subcategories = SelectMultipleField( +        SubCategory, blank=True, +        verbose_name=_(u"Sub-categories checked by default"))      dynamic_categories = models.NullBooleanField(          _(u"Sub-categories dynamicaly displayed"), -        help_text=_(u"If checked, categories are only displayed in the menu if " -                    u"they are available on the current extent.")) -    subcategories = SelectMultipleField(SubCategory, related_name='areas', -       blank=True, db_table='chimere_subcategory_areas', -       verbose_name=_(u"Restricted to theses sub-categories"), -       help_text=_(u"If no sub-category is set all sub-categories are " -                   u"available")) +        help_text=_(u"If checked, categories are only displayed in the menu " +                    u"if they are available on the current extent.")) +    subcategories = SelectMultipleField( +        SubCategory, related_name='areas', +        blank=True, db_table='chimere_subcategory_areas', +        verbose_name=_(u"Restricted to theses sub-categories"), +        help_text=_(u"If no sub-category is set all sub-categories are " +                    u"available")) +    display_category_menu = models.BooleanField( +        _(u"Display category menu"), default=True, +        help_text=_(u"If set to False, category menu will be hide and all " +                    u"categories will be always displayed."))      external_css = models.URLField(_(u"Link to an external CSS"), blank=True,                                     null=True)      restrict_to_extent = models.BooleanField(_(u"Restrict to the area extent"),                                               default=False) +    allow_point_edition = models.BooleanField(_(u"Allow point edition"), +                                              default=True) +    allow_route_edition = models.BooleanField(_(u"Allow route edition"), +                                              default=True) +    allow_polygon_edition = models.BooleanField(_(u"Allow polygon edition"), +                                                default=True) +    extra_map_def = models.TextField( +        _(u"Extra map definition"), blank=True, null=True, +        help_text=_(u"Extra javascript script loaded for this area. " +                    u"Carreful! To prevent breaking the map must be valid."))      objects = models.GeoManager()      def __unicode__(self): @@ -1446,13 +1794,13 @@ class Area(models.Model, SimpleArea):      def getWkt(self):          return "SRID=%d;POLYGON((%f %f,%f %f,%f %f,%f %f, %f %f))" % ( -          settings.CHIMERE_EPSG_DISPLAY_PROJECTION, -          self.upper_left_corner.x, self.upper_left_corner.y, -          self.lower_right_corner.x, self.upper_left_corner.y, -          self.lower_right_corner.x, self.lower_right_corner.y, -          self.upper_left_corner.x, self.lower_right_corner.y, -          self.upper_left_corner.x, self.upper_left_corner.y, -          ) +            settings.CHIMERE_EPSG_DISPLAY_PROJECTION, +            self.upper_left_corner.x, self.upper_left_corner.y, +            self.lower_right_corner.x, self.upper_left_corner.y, +            self.lower_right_corner.x, self.lower_right_corner.y, +            self.upper_left_corner.x, self.lower_right_corner.y, +            self.upper_left_corner.x, self.upper_left_corner.y, +        )      def getIncludeMarker(self):          """ @@ -1466,13 +1814,22 @@ class Area(models.Model, SimpleArea):          """          return Q(route__contained=self.getWkt()) +    def getIncludePolygon(self): +        """ +        Get the sql statement for the test if the route is included in the area +        """ +        return Q(polygon__contained=self.getWkt()) +  pre_save_area_values = {} + +  def area_pre_save(sender, **kwargs):      if not kwargs['instance']:          return      geometry_pre_save(Area, pre_save_area_values)(sender, **kwargs)  pre_save.connect(area_pre_save, sender=Area) +  def area_post_save(sender, **kwargs):      if not kwargs['instance']:          return @@ -1486,7 +1843,7 @@ def area_post_save(sender, **kwargs):      old_urn, old_name = area.urn, area.name      if area.pk in pre_save_area_values:          old_urn, old_name = pre_save_area_values[area.pk] -    perm, old_groups, old_users = None, [], [] +    perm = None      if area.urn != old_urn:          oldmnemo = 'change_area_' + old_urn          old_perm = Permission.objects.filter(codename=oldmnemo) @@ -1502,7 +1859,7 @@ def area_post_save(sender, **kwargs):      lbl = "Can change " + area.name      if not perm.count():          content_type, created = ContentType.objects.get_or_create( -                                        app_label="chimere", model="area") +            app_label="chimere", model="area")          perm = Permission(name=lbl, content_type_id=content_type.id,                            codename=mnemo)          perm.save() @@ -1526,16 +1883,18 @@ def area_post_save(sender, **kwargs):          group.permissions.add(perm)          for app_label, model in (('chimere', 'marker'),                                   ('chimere', 'route'), +                                 ('chimere', 'polygon'),                                   ('chimere', 'multimediafile'),                                   ('chimere', 'picturefile'),                                   ('chimere', 'routefile')): -            ct, created = ContentType.objects.get_or_create(app_label=app_label, -                                                            model=model) +            ct, created = ContentType.objects.get_or_create( +                app_label=app_label, model=model)              for p in Permission.objects.filter(content_type=ct).all():                  group.permissions.add(p)  post_save.connect(area_post_save, sender=Area) +  def get_areas_for_user(user):      """      Getting subcats for a specific user @@ -1552,12 +1911,14 @@ def get_areas_for_user(user):                  pass      return areas +  def get_users_by_area(area):      if not area:          return [] -    perm = 'change_area_'+area.urn -    return User.objects.filter(Q(groups__permissions__codename=perm)| -                                Q(user_permissions__codename=perm)).all() +    perm = 'change_area_' + area.urn +    return User.objects.filter(Q(groups__permissions__codename=perm) | +                               Q(user_permissions__codename=perm)).all() +  class AreaLayers(models.Model):      area = models.ForeignKey(Area) @@ -1570,48 +1931,74 @@ class AreaLayers(models.Model):          verbose_name = _("Layers")          verbose_name_plural = _("Layers") +  class PropertyModel(models.Model):      '''Model for a property      '''      name = models.CharField(_(u"Name"), max_length=150) +    slug = models.SlugField(_(u"Slug"), blank=True, null=True)      order = models.IntegerField(_(u"Order"))      available = models.BooleanField(_(u"Available"))      mandatory = models.BooleanField(_(u"Mandatory")) -    subcategories = SelectMultipleField(SubCategory, related_name='properties', -       blank=True, verbose_name=_(u"Restricted to theses sub-categories"), -       help_text=_(u"If no sub-category is set all the property applies to all " -                   u"sub-categories")) +    subcategories = SelectMultipleField( +        SubCategory, related_name='properties', +        blank=True, verbose_name=_(u"Restricted to theses sub-categories"), +        help_text=_(u"If no sub-category is set all the property applies to " +                    u"all sub-categories")) +    areas = SelectMultipleField( +        'Area', verbose_name=_(u"Restrict to theses areas"), blank=True, +        null=True, +        help_text=_(u"If no area is set the property apply to " +                    u"all areas"))      TYPE = (('T', _('Text')),              ('L', _('Long text')),              ('P', _('Password')),              ('D', _("Date")),              ('C', _("Choices")), +            ('A', _("Choices (autocomplete)")),              ('B', _("Boolean")),              ) -    TYPE_WIDGET = {'T':forms.TextInput, -                   'L':TextareaWidget, -                   'P':forms.PasswordInput, -                   'D':DatePickerWidget, -                   'C':forms.Select, -                   'B':forms.CheckboxInput, +    TYPE_WIDGET = {'T': forms.TextInput, +                   'L': TextareaWidget, +                   'P': forms.PasswordInput, +                   'D': DatePickerWidget, +                   'C': forms.Select, +                   'A': JQueryAutoComplete, +                   'B': forms.CheckboxInput,                     }      type = models.CharField(_(u"Type"), max_length=1, choices=TYPE) -    def __unicode__(self): -        return self.name +      class Meta:          ordering = ('order',)          verbose_name = _("Property model") +    def __unicode__(self): +        return self.name +      def getAttrName(self): -        attr_name = defaultfilters.slugify(self.name) -        attr_name = re.sub(r'-','_', attr_name) -        return attr_name +        return self.slug      def getNamedId(self):          '''Get the name used as named id (easily sortable)          '''          return 'property_%d_%d' % (self.order, self.id) +    @classmethod +    def getAvailable(cls, area_name=None): +        if area_name and area_name.endswith('/'): +            area_name = area_name[:-1] +        base_q = cls.objects.filter(available=True).annotate(Count('areas')) +        q1 = base_q.filter(areas__count=0) +        if not area_name: +            return [q1] +        # areas__count__gt=0 necessary to prevent Django bug +        q2 = base_q.filter(Q(areas__urn=area_name) & Q(areas__count__gt=0)) +        # if made it a single queryset the condition on 'count' is +        # wrong - hope this will be fixed on higher Django version (>=1.4) +        # to make a single query +        return [q1, q2] + +  class PropertyModelChoice(models.Model):      '''Choices for property model      ''' @@ -1619,26 +2006,32 @@ class PropertyModelChoice(models.Model):                                        verbose_name=_(u"Property model"))      value = models.CharField(_(u"Value"), max_length=150)      available = models.BooleanField(_(u"Available"), default=True) -    def __unicode__(self): -        return unicode(self.value)      class Meta:          verbose_name = _(u"Model property choice") +    def __unicode__(self): +        return unicode(self.value) + +  class Property(models.Model):      '''Property for a POI      ''' -    marker = models.ForeignKey(Marker, verbose_name=_(u"Point of interest")) +    marker = models.ForeignKey( +        Marker, verbose_name=_(u"Point of interest"), blank=True, null=True) +    polygon = models.ForeignKey( +        Polygon, verbose_name=_(u"Polygon"), blank=True, null=True)      propertymodel = models.ForeignKey(PropertyModel,                                        verbose_name=_(u"Property model"))      value = models.TextField(_(u"Value")) +      def __unicode__(self):          if self.propertymodel.type == 'C':              if not self.value:                  return ''              try:                  return unicode(PropertyModelChoice.objects.get( -                                                    pk=self.value).value) +                    pk=self.value).value)              except (self.DoesNotExist, ValueError):                  return ""          return unicode(self.value) @@ -1650,7 +2043,8 @@ class Property(models.Model):      def python_value(self):          if self.propertymodel.type == 'D':              try: -                return datetime.date(*[int(val) for val in self.value.split('-')]) +                return datetime.date(*[int(val) +                                       for val in self.value.split('-')])              except:                  return ""          if self.propertymodel.type == 'C' and self.value: @@ -1660,4 +2054,3 @@ class Property(models.Model):                  return None          else:              return self.value -  | 
