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 - |