summaryrefslogtreecommitdiff
path: root/chimere/models.py
diff options
context:
space:
mode:
Diffstat (limited to 'chimere/models.py')
-rw-r--r--chimere/models.py302
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"))