diff options
Diffstat (limited to 'chimere/models.py')
-rw-r--r-- | chimere/models.py | 302 |
1 files changed, 277 insertions, 25 deletions
diff --git a/chimere/models.py b/chimere/models.py index 5d0b93c..d16d20a 100644 --- a/chimere/models.py +++ b/chimere/models.py @@ -36,9 +36,9 @@ from django.conf import settings from django.contrib.auth.models import User, Permission, ContentType, Group from django.contrib.gis.db import models from django.core.files import File -from django.core.exceptions import 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 _ @@ -189,6 +189,9 @@ class Color(models.Model): """ 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")) @@ -267,6 +270,8 @@ class SubCategory(models.Model): 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, @@ -529,6 +534,15 @@ class GeographicItem(models.Model): _(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 @@ -551,39 +565,57 @@ class GeographicItem(models.Model): 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: - property = Property.objects.get(propertymodel=propertymodel, - marker=self) + d = {'propertymodel': propertymodel, + geom_item: self} + property = Property.objects.get(**d) except Property.DoesNotExist: return return property - def getProperties(self): + def getProperties(self, area_name=None): """Get all the property availables """ properties = [] - for pm in PropertyModel.objects.filter(available=True): - property = self.getProperty(pm) - if property: - properties.append(property) + 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 """ - properties = Property.objects.filter(marker=self, - propertymodel=pm).all() + 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: @@ -603,8 +635,8 @@ class GeographicItem(models.Model): value = choice.pk # new property if not properties: - new_property = Property.objects.create( - marker=self, propertymodel=pm, value=value) + d = {'propertymodel': pm, geom_item: self, 'value': value} + new_property = Property.objects.create(**d) new_property.save() else: property = properties[0] @@ -674,17 +706,82 @@ class GeographicItem(models.Model): 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 @@ -858,6 +955,7 @@ def geometry_post_save(pre_save_geom_values): 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) @@ -900,8 +998,92 @@ class Polygon(GeographicItem): "geometry": json.loads(self.polygon.geojson), "properties": {"pk": self.id, "name": self.name, 'key': "polygon-{}".format(self.pk), - "color": color, - "inner_color": inner_color}} + "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) @@ -1295,6 +1477,12 @@ class Route(GeographicItem): "color": color}} return json.dumps(attributes) + 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 = {} @@ -1309,6 +1497,7 @@ 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 @@ -1320,8 +1509,13 @@ def route_post_save(sender, **kwargs): 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) + 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'] @@ -1466,6 +1660,7 @@ 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: @@ -1476,6 +1671,7 @@ class SimpleArea: 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) @@ -1490,12 +1686,37 @@ class SimpleArea: # 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 @@ -1537,6 +1758,10 @@ class Area(models.Model, SimpleArea): 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"), @@ -1547,6 +1772,10 @@ class Area(models.Model, SimpleArea): 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): @@ -1653,6 +1882,7 @@ 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')): @@ -1705,6 +1935,7 @@ 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")) @@ -1713,6 +1944,11 @@ class PropertyModel(models.Model): 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')), @@ -1737,15 +1973,28 @@ class PropertyModel(models.Model): 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 @@ -1765,7 +2014,10 @@ class PropertyModelChoice(models.Model): 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")) |