summaryrefslogtreecommitdiff
path: root/chimere/models.py
diff options
context:
space:
mode:
Diffstat (limited to 'chimere/models.py')
-rw-r--r--chimere/models.py2118
1 files changed, 2118 insertions, 0 deletions
diff --git a/chimere/models.py b/chimere/models.py
new file mode 100644
index 0000000..a1e00f9
--- /dev/null
+++ b/chimere/models.py
@@ -0,0 +1,2118 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright (C) 2008-2016 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# See the file COPYING for details.
+
+"""
+Models description
+"""
+import os
+import datetime
+import pyexiv2
+import re
+import copy
+import simplejson as json
+from lxml import etree
+from PIL import Image
+from subprocess import Popen, PIPE
+from BeautifulSoup import BeautifulSoup
+
+from django import forms
+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, MultipleObjectsReturned
+from django.core.urlresolvers import reverse
+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, PolygonField, \
+ JQueryAutoComplete
+from chimere.utils import KMLManager, OSMManager, ShapefileManager, \
+ GeoRSSManager, CSVManager, HtmlXsltManager, XMLXsltManager, JsonManager, \
+ IcalManager
+
+
+class Page(models.Model):
+ """Simple extra pages
+ """
+ title = models.CharField(_(u"Name"), max_length=150)
+ mnemonic = models.CharField(_(u"Mnemonic"), max_length=10, blank=True,
+ null=True)
+ available = models.BooleanField(_(u"Available"), default=True)
+ order = models.IntegerField(_(u"Order"), default=10, blank=True, null=True)
+ template_path = models.CharField(_(u"Template path"), max_length=150,
+ blank=True, null=True)
+ content = models.TextField(blank=True, null=True)
+
+ 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
+ page = kwargs['instance']
+ if not page.mnemonic:
+ page.mnemonic = defaultfilters.slugify(page.title)
+ page.save()
+post_save.connect(page_post_save, sender=Page)
+
+
+def shortify(text):
+ if not text:
+ return ''
+ if len(text) <= settings.CHIMERE_SHORT_DESC_LENGTH:
+ return text
+ desc = text[:settings.CHIMERE_SHORT_DESC_LENGTH]
+ short_desc = ""
+ # find a correct opportunity to cut
+ for idx, c in enumerate(reversed(desc)):
+ if c == '>':
+ break
+ if c == '<':
+ 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)]
+ break
+ return BeautifulSoup(short_desc).prettify()
+
+
+class News(models.Model):
+ """News of the site
+ """
+ title = models.CharField(_(u"Name"), max_length=150)
+ available = models.BooleanField(_(u"Available"))
+ is_front_page = models.NullBooleanField(_(u"Is front page"), blank=True,
+ null=True)
+ date = models.DateField(_(u"Date"))
+ content = models.TextField()
+ url = models.URLField(_(u"Url"), max_length=200, blank=True, null=True)
+ areas = SelectMultipleField('Area', verbose_name=_(u"Associated areas"),
+ blank=True, null=True)
+
+ 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)
+ 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)
+ try:
+ params = cls.objects.get(id=c_id).parameters
+ except cls.DoesNotExist:
+ return ''
+ return params
+
+ @classmethod
+ def getUrnByParameters(cls, parameters):
+ try:
+ obj = cls.objects.get(parameters=parameters)
+ except cls.DoesNotExist:
+ obj = cls(parameters=parameters)
+ obj.save()
+ n = obj.id
+ urn = ''
+ while 1:
+ idx = n % cls.base
+ urn = cls.digits[idx] + urn
+ n = n / cls.base
+ if n == 0:
+ break
+ return urn
+
+
+class ColorTheme(models.Model):
+ """Color theme
+ """
+ name = models.CharField(_(u"Name"), max_length=150)
+
+ class Meta:
+ verbose_name = _(u"Color theme")
+
+ def __unicode__(self):
+ return self.name
+
+
+class Color(models.Model):
+ """Color
+ """
+ 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"))
+
+ class Meta:
+ ordering = ["order"]
+ verbose_name = _(u"Color")
+
+ def __unicode__(self):
+ return self.code
+
+ @property
+ def color(self):
+ return self.code
+
+ @property
+ def inner_color(self):
+ return self.inner_code
+
+
+class Category(models.Model):
+ """Category of Point Of Interest (POI)
+ """
+ name = models.CharField(_(u"Name"), max_length=150)
+ available = models.BooleanField(_(u"Available"))
+ order = models.IntegerField(_(u"Order"))
+ description = models.TextField(blank=True, null=True)
+
+ class Meta:
+ ordering = ["order"]
+ verbose_name = _(u"Category")
+
+ def __unicode__(self):
+ return self.name
+
+
+class Icon(models.Model):
+ '''Icon
+ '''
+ name = models.CharField(_(u"Name"), max_length=150)
+ image = models.ImageField(_(u"Image"), upload_to='icons',
+ 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
+ '''
+ category = models.ForeignKey(Category, verbose_name=_(u"Category"),
+ related_name='subcategories')
+ name = models.CharField(_(u"Name"), max_length=150)
+ available = models.BooleanField(_(u"Available"), default=True)
+ submission = models.BooleanField(_(u"Available for submission"),
+ 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')
+ color_theme = models.ForeignKey(ColorTheme, verbose_name=_(u"Color theme"),
+ blank=True, null=True,
+ related_name='subcategories')
+ 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)
+ min_zoom = models.IntegerField(
+ _(u"Minimum zoom for loading details"), blank=True, null=True,
+ help_text=_(u"Optimization when too many data have to be displayed. "
+ u"Currently available only for route and polygon."))
+ simplify_tolerance = models.FloatField(
+ _(u"Simplify tolerance for lower zoom"), blank=True, null=True,
+ help_text=_(u"Only relevant when Minimum zoom is set. Use the "
+ u"Douglas-Peucker algorythm to simplify the geometry when "
+ u"details is not alvailable. Adjust to your data volume "
+ u"and your performance need. 0.0003 is a good starting "
+ u"point. Note: typology is not preserved."))
+
+ 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,
+ instance=False):
+ '''Get list of tuples with first the category and second the associated
+ subcategories
+ '''
+ sub_categories = {}
+ subcategories = cls.objects.filter(category__available=True)
+ if not item_types:
+ subcategories = subcategories.filter(available=True)
+ else:
+ subcategories = subcategories.filter(item_type__in=item_types)
+ if public:
+ subcategories = subcategories.filter(submission=True)
+ selected_cats = []
+ if area_name:
+ area = Area.objects.get(urn=area_name)
+ # if there some restrictions with categories limit them
+ if area.subcategories.count():
+ sub_ids = [sub.id for sub in area.subcategories.all()]
+ subcategories = subcategories.filter(id__in=sub_ids)
+ selected_cats = [subcat.pk
+ for subcat in area.default_subcategories.all()]
+
+ if instance:
+ return subcategories.order_by('order')
+
+ for sub_category in subcategories.order_by('order'):
+ if sub_category.category not in sub_categories:
+ sub_categories[sub_category.category] = []
+ if sub_category.id in selected_cats:
+ sub_category.selected = True
+ sub_category.category.selected = True
+ sub_categories[sub_category.category].append(sub_category)
+ 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
+
+ @classmethod
+ def getAvailableTuples(cls, item_types=None, area_name=None):
+ cats = []
+ for cat, subcats in cls.getAvailable(item_types=item_types,
+ area_name=area_name):
+ cats.append((unicode(cat),
+ [(subcat.pk, subcat.name) for subcat in subcats]))
+ 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,
+ '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}
+ return items
+
+ def getJSON(self, categories_id=[]):
+ '''Return a JSON string - mainly used to get description
+ '''
+ json_string = json.dumps(self.getJSONDict())
+ return json_string
+
+ @property
+ def slug(self):
+ return defaultfilters.slugify(self.name)
+
+ @property
+ def item_nb(self):
+ return Marker.objects.filter(categories=self).count()
+
+
+class SubCategoryUserLimit(models.Model):
+ """
+ Moderation limit for user by category
+ """
+ subcategory = models.ForeignKey(
+ SubCategory, related_name='limited_for_user')
+ user = models.ForeignKey(User, related_name='subcategory_limit_to')
+
+ def __unicode__(self):
+ return u"{} / {}".format(self.user, self.subcategory)
+
+ class Meta:
+ verbose_name = _(u"Sub-category limit for user")
+ verbose_name_plural = _(u"Sub-category limits for users")
+
+
+STATUS = (('S', _(u'Submited')),
+ ('A', _(u'Available')),
+ ('M', _(u'Modified')),
+ ('D', _(u'Disabled')),
+ ('I', _(u'Imported')))
+STATUS_DCT = dict(STATUS)
+
+IMPORTERS = {'KML': KMLManager,
+ 'OSM': OSMManager,
+ 'SHP': ShapefileManager,
+ 'RSS': GeoRSSManager,
+ 'CSV': CSVManager,
+ 'JSON': JsonManager,
+ 'ICAL': IcalManager,
+ 'XSLT': HtmlXsltManager,
+ 'XXLT': XMLXsltManager
+ }
+
+IMPORTER_CHOICES = (('KML', 'KML'),
+ ('OSM', 'OSM'),
+ ('SHP', 'Shapefile'),
+ ('RSS', 'GeoRSS'),
+ ('CSV', 'CSV'),
+ ('JSON', 'JSON'),
+ ('ICAL', 'ICAL'),
+ ('XSLT', 'HTML-XSLT'),
+ ('XXLT', 'XML-XSLT'),
+ )
+
+IMPORTER_CHOICES_DICT = dict(IMPORTER_CHOICES)
+
+
+class Importer(models.Model):
+ '''
+ Data importer for a specific subcategory
+ '''
+ importer_type = models.CharField(_(u"Importer type"), max_length=4,
+ choices=IMPORTER_CHOICES)
+ filtr = models.TextField(_(u"Filter"), blank=True, null=True)
+ 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)
+ default_name = models.CharField(_(u"Name by default"), max_length=200,
+ blank=True, null=True)
+ srid = models.IntegerField(_(u"SRID"), blank=True, null=True)
+ zipped = models.BooleanField(_(u"Zipped file"), default=False)
+ overwrite = models.BooleanField(_(u"Overwrite existing data"),
+ default=False)
+ get_description = models.BooleanField(_(u"Get description from source"),
+ default=False)
+ default_description = models.TextField(_(u"Default description"),
+ blank=True, null=True)
+ origin = models.CharField(_(u"Origin"), max_length=1000,
+ 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"))
+ state = models.TextField(_(u"State"), blank=True, null=True)
+ 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)
+
+ class Meta:
+ verbose_name = _(u"Importer")
+
+ def __unicode__(self):
+ vals = [IMPORTER_CHOICES_DICT[self.importer_type],
+ self.source, self.source_file.name,
+ u", ".join([unicode(cat) for cat in self.categories.all()]),
+ self.default_name]
+ return u' %d: %s' % (self.pk, u" - ".join([unicode(v)
+ for v in vals if v]))
+
+ @property
+ def manager(self):
+ return IMPORTERS[self.importer_type](self)
+
+ def display_categories(self):
+ return u"\n".join([cat.name for cat in self.categories.all()])
+
+ def get_key_category_dict(self):
+ dct = {}
+ # if no category provided: all category are considered
+ q = SubCategory.objects.all()
+ if self.categories.count():
+ q = self.categories.all()
+ for cat in q.all():
+ dct[defaultfilters.slugify(cat.name)] = cat
+
+ for key_cat in self.key_categories.all():
+ dct[key_cat.key] = key_cat.category
+ return dct
+
+
+class ImporterKeyCategories(models.Model):
+ """
+ Association between key and categories
+ """
+ importer = models.ForeignKey(Importer, verbose_name=_(u"Importer"),
+ related_name='key_categories')
+ category = models.ForeignKey(SubCategory, verbose_name=_(u"Category"))
+ key = models.CharField(_(u"Import key"), max_length=200)
+
+ class Meta:
+ verbose_name = _(u"Importer - Key categories")
+
+
+class GeographicItem(models.Model):
+ categories = SelectMultipleField(SubCategory)
+ 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)
+ status = models.CharField(_(u"Status"), max_length=1, choices=STATUS)
+ 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)
+ 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. "
+ 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 "
+ 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 + ':'):
+ return k_v.split(':')[1]
+
+ def set_key(self, key, value):
+ new_keys, _set = '', None
+ key_vals = self.import_key.split(';') if self.import_key else []
+ for k_v in key_vals:
+ if ':' not in k_v:
+ continue
+ k, v = k_v.split(':')
+ if k == key:
+ _set = True
+ new_keys += '%s:%s;' % (k, value)
+ else:
+ new_keys += '%s:%s;' % (k, v)
+ if not _set:
+ new_keys += '%s:%s;' % (key, value)
+ self.import_key = new_keys
+ modified_since_import = self.modified_since_import
+ self.save()
+ # preserve modified_since_import
+ if modified_since_import != self.modified_since_import:
+ self.modified_since_import = modified_since_import
+ self.save()
+
+ 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():
+ return True
+ return False
+
+ @classmethod
+ def properties(cls):
+ return [pm for pm in PropertyModel.objects.filter(available=True)]
+
+ @classmethod
+ 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):
+ item = self
+ if cls == Route:
+ # TODO v3
+ if not self.associated_marker.objects.count():
+ return
+ 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')
+ point = PointField(_(u"Localisation"),
+ srid=settings.CHIMERE_EPSG_DISPLAY_PROJECTION)
+ available_date = models.DateTimeField(_(u"Available Date"), blank=True,
+ 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)
+ is_front_page = models.NullBooleanField(_(u"Is front page"), blank=True,
+ null=True)
+ objects = models.GeoManager()
+
+ class Meta:
+ ordering = ('status', 'name')
+ verbose_name = _(u"Point of interest")
+
+ @property
+ def multimedia_items(self):
+ pict = list(self.pictures.filter(miniature=False).all())
+ mm = list(self.multimedia_files.filter(miniature=False).all())
+ items = [(item.order, item) for item in pict + mm]
+ return [item for order, item in sorted(items)]
+
+ @property
+ def default_pictures(self):
+ return list(self.pictures.filter(miniature=True).order_by('order'))
+
+ @property
+ def default_multimedia_items(self):
+ return list(self.multimedia_files.filter(miniature=True
+ ).order_by('order'))
+
+ @property
+ def date(self):
+ if settings.CHIMERE_DAYS_BEFORE_EVENT and self.start_date:
+ today = datetime.date.today()
+ if self.end_date and self.start_date < today:
+ return self.end_date
+ return self.start_date
+
+ @property
+ def short_desc(self):
+ return shortify(self.description)
+
+ @property
+ def geom_attr(self):
+ return 'point'
+
+ def getLatitude(self):
+ '''Return the latitude
+ '''
+ return self.point.y
+
+ def getLongitude(self):
+ '''Return the longitude
+ '''
+ return self.point.x
+
+ def getGeoJSON(self, categories_id=[]):
+ '''Return a GeoJSON string
+ '''
+ jsons = []
+ 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)
+ try:
+ items['geometry'] = json.loads(self.point.geojson)
+ except json.JSONDecodeError:
+ continue
+ items['properties'].update({
+ '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,
+ })
+ except IOError:
+ pass
+
+ jsons.append(items)
+
+ return json.dumps(jsons)
+
+ @property
+ def default_category(self):
+ # Should we select only available ones ?
+ # Should we catch if not exists ?
+ cats = self.categories.filter(available=True, category__available=True)
+ if cats.count():
+ return cats.all()[0]
+
+ def get_absolute_url(self, area_name=''):
+ parameters = 'current_feature=%d' % self.id
+ if self.default_category:
+ parameters += '&checked_categories=%s' % self.default_category.pk
+ urn = TinyUrl.getUrnByParameters(parameters)
+ area_name = area_name + '/' if area_name else ''
+ url = reverse('chimere:tiny', args=[area_name, urn])
+ 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'),
+}
+
+
+def geometry_pre_save(cls, pre_save_geom_values):
+ def geom_pre_save(sender, **kwargs):
+ if not kwargs['instance'] or not kwargs['instance'].pk:
+ return
+ instance = kwargs['instance']
+ try:
+ instance = cls.objects.get(pk=instance.pk)
+ pre_save_geom_values[instance.pk] = dict(
+ [(attr, getattr(instance, attr))
+ for attr in PRE_ATTRS[cls.__name__]])
+ except ObjectDoesNotExist:
+ pass
+ 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'] \
+ or kwargs['instance'].pk not in pre_save_geom_values:
+ return
+ instance = kwargs['instance']
+ pre = pre_save_geom_values[instance.pk]
+ # force the reinit of modified_since_import
+ if pre['modified_since_import'] != instance.modified_since_import:
+ return
+ if (instance.import_version != pre['import_version']
+ and instance.modified_since_import):
+ instance.modified_since_import = False
+ instance.save()
+ 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]]:
+ 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
+ '''
+ try:
+ geom = json.loads(self.polygon.geojson)
+ except json.JSONDecodeError:
+ return json.dumps('{}')
+ attributes = {"type": "Feature",
+ "geometry": geom,
+ "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="", inner_color=''):
+ '''Return a GeoJSON string
+ '''
+ # get colors
+ if not color or not inner_color:
+ q = Color.objects.filter(
+ color_theme__subcategories=self.subcategory)
+ if not q.count():
+ if not color:
+ color = "rgba(0, 0, 255, 1)"
+ if not inner_color:
+ inner_color = 'rgba(255, 125, 0, 0.6)'
+ else:
+ # get the first
+ c = q.order_by('order').all()[0]
+ if not color:
+ color = c.color
+ if not inner_color:
+ inner_color = c.inner_color
+
+ geom = self.polygon
+ if self.subcategory.simplify_tolerance:
+ geom = self.polygon.simplify(
+ self.subcategory.simplify_tolerance).json
+ else:
+ geom = geom.json
+ attributes = {
+ 'color': color, 'geometry': json.loads(geom),
+ 'type': "Feature", "properties": {
+ 'color': color,
+ 'inner_color': inner_color,
+ '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")),
+ ('I', _(u"Image")),
+ ('O', _(u"Other")),)
+ media_type = models.CharField(_(u"Media type"), max_length=1,
+ choices=MEDIA_TYPES)
+ name = models.CharField(_(u"Name"), max_length=150)
+ mime_type = models.CharField(_(u"Mime type"), max_length=50, blank=True,
+ null=True)
+ iframe = models.BooleanField(_(u"Inside an iframe"), default=False)
+ available = models.BooleanField(_(u"Available"), default=True)
+
+ class Meta:
+ verbose_name = _(u"Multimedia type")
+ verbose_name_plural = _(u"Multimedia types")
+
+ def __unicode__(self):
+ return self.name
+
+ @classmethod
+ def get_tuples(cls):
+ vals = cls.objects.filter(available=True).order_by('media_type',
+ 'name')
+ tuples, c_tpe = [('', _("Automatic recognition"))], None
+ media_type_dct = dict(cls.MEDIA_TYPES)
+ for tpe, pk, name in [(media_type_dct[v.media_type], v.pk, v.name)
+ for v in vals]:
+ if not c_tpe or c_tpe != tpe:
+ c_tpe = tpe
+ tuples.append([tpe, []])
+ tuples[-1][1].append((pk, name))
+ return tuples
+
+IFRAME_LINKS = {
+ '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://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')
+
+ class Meta:
+ verbose_name = _(u"Multimedia extension")
+ verbose_name_plural = _(u"Multimedia extensions")
+
+ 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',
+ blank=True, null=True)
+ polygon = models.ForeignKey(Polygon, related_name='multimedia_files',
+ blank=True, null=True)
+
+ class Meta:
+ verbose_name = _(u"Multimedia file")
+ verbose_name_plural = _(u"Multimedia files")
+
+ def __unicode__(self):
+ return self.name or u""
+
+
+def multimediafile_post_save(sender, **kwargs):
+ if not kwargs['instance'] or not kwargs['created']:
+ return
+ multimediafile = kwargs['instance']
+ # auto recognition of file types
+ if not multimediafile.multimedia_type:
+ url = multimediafile.url
+ for mm_type in IFRAME_LINKS:
+ 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)
+ multimediafile.multimedia_type = multimedia_type
+ if not multimediafile.multimedia_type:
+ ext = url.split(".")[-1]
+ q = MultimediaExtension.objects.filter(name__iendswith=ext)
+ if q.count():
+ multimediafile.multimedia_type = q.all()[0].multimedia_type
+ else:
+ # default to an iframe
+ multimediafile.multimedia_type = \
+ 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()]
+ key = None
+ for regexp in regexps:
+ key = regexp.findall(multimediafile.url)
+ if key:
+ key = key[0]
+ break
+ if key:
+ multimediafile.url = lnk % key
+
+ 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()
+ multimediafile.order = mfs.count() + 1
+ 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')
+ 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', blank=True,
+ null=True)
+ polygon = models.ForeignKey(Polygon, related_name='pictures', blank=True,
+ null=True)
+
+ def __unicode__(self):
+ return self.name or u""
+
+ class Meta:
+ 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
+ return (int(max_x), int(new_y))
+
+IMAGE_EXIF_ORIENTATION_MAP = {
+ 1: 0,
+ 8: 2,
+ 3: 3,
+ 6: 4,
+}
+
+PYEXIV2_OLD_API = not hasattr(pyexiv2, 'ImageMetadata')
+
+
+def picturefile_post_save(sender, **kwargs):
+ if not kwargs['instance']:
+ return
+ picturefile = kwargs['instance']
+
+ if kwargs['created']:
+ filename = picturefile.picture.path
+ metadata, orientation = None, None
+ if PYEXIV2_OLD_API:
+ metadata = pyexiv2.Image(filename)
+ metadata.readMetadata()
+ orientation = metadata['Exif.Image.Orientation'] \
+ 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 orientation and orientation in IMAGE_EXIF_ORIENTATION_MAP \
+ and orientation > 1:
+ metadata['Exif.Image.Orientation'] = 1
+ if PYEXIV2_OLD_API:
+ metadata.writeMetadata()
+ else:
+ metadata.write()
+ im = Image.open(filename)
+ im = im.transpose(IMAGE_EXIF_ORIENTATION_MAP[orientation])
+ im.save(filename)
+
+ if not picturefile.thumbnailfile:
+ file = picturefile.picture
+ # defining the filename and the thumbnail filename
+ 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)
+ miniature = basename + '_thumb.jpg'
+ filename = file.path
+ miniature_filename = os.path.join(filehead, miniature)
+ try:
+ image = Image.open(filename)
+ except:
+ image = None
+ if image:
+ 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))
+ elif settings.CHIMERE_THUMBS_SCALE_WIDTH:
+ image_x, image_y = scale_image(
+ settings.CHIMERE_THUMBS_SCALE_WIDTH, (image_x, image_y))
+ image.thumbnail([image_x, image_y], Image.ANTIALIAS)
+
+ temp_image = open(miniature_filename, 'w')
+ if image.mode != "RGB":
+ image = image.convert('RGB')
+ try:
+ image.save(temp_image, 'JPEG', quality=90, optimize=1)
+ except:
+ image.save(temp_image, 'JPEG', quality=90)
+
+ short_name = miniature_filename[len(settings.MEDIA_ROOT):]
+ picturefile.thumbnailfile = short_name
+ picturefile.save()
+
+ if not kwargs['created']:
+ return
+ 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()
+ picturefile.order = pfs.count() + 1
+ 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)
+ TYPE = (('K', _(u'KML')), ('G', _(u'GPX')))
+ file_type = models.CharField(max_length=1, choices=TYPE)
+
+ class Meta:
+ ordering = ('name',)
+ verbose_name = _(u"Route file")
+ verbose_name_plural = _(u"Route files")
+
+ def __unicode__(self):
+ return self.name
+
+ def process(self):
+ if self.simplified_file:
+ return
+ input_name = settings.MEDIA_ROOT + self.raw_file.name
+ output_name = settings.MEDIA_ROOT + self.raw_file.name[:-4] + \
+ "_simplified" + ".gpx"
+ cli_args = [settings.GPSBABEL, '-i']
+ if self.file_type == 'K':
+ cli_args.append('kml')
+ elif self.file_type == 'G':
+ cli_args.append('gpx')
+ cli_args += ['-f', input_name, '-x', settings.GPSBABEL_OPTIONS,
+ '-o', 'gpx', '-F', output_name]
+ p = Popen(cli_args, stderr=PIPE)
+ p.wait()
+ if p.returncode:
+ print p.stderr.read()
+ # logger.error(p.stderr.read())
+ else:
+ self.simplified_file = File(open(output_name))
+ self.save()
+ os.remove(output_name)
+
+ @property
+ def route(self):
+ if not self.simplified_file:
+ return
+
+ file_name = settings.MEDIA_ROOT + self.simplified_file.name
+ tree = etree.parse(file_name)
+ pts = []
+ for pt in tree.getiterator():
+ if not pt.tag.endswith('trkpt'):
+ continue
+ pts.append((pt.get("lon"), pt.get("lat")))
+ wkt_tpl = u'LINESTRING(%s)'
+ 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')
+ 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')
+ 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()
+
+ class Meta:
+ ordering = ('status', 'name')
+ verbose_name = _(u"Route")
+
+ def __init__(self, *args, **kwargs):
+ super(Route, self).__init__(*args, **kwargs)
+ self.description = ''
+ try:
+ associated_marker = Marker.objects.get(route=self)
+ self.description = associated_marker.description
+ except:
+ associated_marker = None
+ # add read attributes for properties
+ for pm in self.properties():
+ attr_name = pm.getAttrName()
+ if not hasattr(self, attr_name):
+ val = ''
+ if associated_marker:
+ property = associated_marker.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))
+
+ @property
+ def geom_attr(self):
+ return 'route'
+
+ 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()
+ ]
+ return multis
+
+ def get_init_picture(self):
+ if not self.associated_marker.count():
+ return []
+ picts = [forms.model_to_dict(pict)
+ for pict in self.associated_marker.all()[0].pictures.all()]
+ return picts
+
+ def getGeoJSON(self, color="#000"):
+ '''Return a GeoJSON string
+ '''
+ try:
+ geom = json.loads(self.route.geojson)
+ except json.JSONDecodeError:
+ return json.dumps('{}')
+ attributes = {"type": "Feature",
+ "geometry": geom,
+ "properties": {"pk": self.id, "name": self.name,
+ 'key': "route-{}".format(self.pk),
+ "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 = {}
+
+
+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])
+ 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']
+ marker.save()
+ properties = {}
+ for pm in instance.properties():
+ prop = instance.getProperty(pm)
+ if prop:
+ properties[pm.pk] = prop.python_value
+ # fix mis-initialized markers
+ if created:
+ for cat in instance.categories.all():
+ marker.categories.add(cat)
+ marker.saveProperties(properties)
+
+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
+ route = kwargs['instance']
+ marker = route.associated_marker
+ if not marker.count():
+ return
+ marker = marker.all()[0]
+ marker.categories.clear()
+ if kwargs['action'] == 'post_clear':
+ return
+ for cat in route.categories.all():
+ marker.categories.add(cat)
+m2m_changed.connect(sync_m2m_route, sender=Route.categories.through)
+
+
+def getDateCondition():
+ '''
+ Return an SQL condition for apparition of dates
+ '''
+ 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')
+ 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 + "')"
+ date_condition += " or (%(alias)s.start_date <='" + now + "' and "
+ date_condition += "%(alias)s.end_date >='" + now + "')) "
+ return date_condition
+
+
+class AggregatedRoute(models.Model):
+ '''
+ Database view for aggregated routes
+ '''
+ 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'
+
+ def getGeoJSON(self, color="#000"):
+ '''Return a GeoJSON string
+ '''
+ if '#' not in color:
+ color = '#' + color
+ try:
+ geom = json.loads(self.route.geojson)
+ except json.JSONDecodeError:
+ return json.dumps('{}')
+ attributes = {
+ 'color': color, 'geometry': geom,
+ '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)
+ """
+ def __init__(self, x, y):
+ self.x, self.y = x, y
+
+
+class SimpleArea:
+ """
+ Rectangular area of a map (not in the database)
+ """
+ def __init__(self, area):
+ """
+ Defining upper left corner ans lower right corner from a tuple
+ """
+ assert len(area) == 4
+ x1, y1, x2, y2 = area
+ self.upper_left_corner = SimplePoint(x1, y1)
+ self.lower_right_corner = SimplePoint(x2, y2)
+
+ def isIn(self, area):
+ """
+ Verify if the current area is in the designated area
+ """
+ if self.upper_left_corner.x >= area.upper_left_corner.x and \
+ self.upper_left_corner.y <= area.upper_left_corner.x and \
+ self.lower_right_corner.x <= area.lower_right_corner.x and \
+ self.lower_right_corner.y >= area.lower_right_corner.y:
+ return True
+ return False
+
+ def getCategories(self, status='A', filter_available=True, area_name=None):
+ """
+ Get categories for this area
+ """
+ wheres = []
+ if area_name:
+ subcategory_pks = []
+ for cat, subcats in SubCategory.getAvailable(area_name=area_name):
+ for subcat in subcats:
+ subcategory_pks.append(unicode(subcat.pk))
+ if filter_available:
+ wheres += ['subcat.available = TRUE', 'cat.available = TRUE']
+ wheres += ['subcat.id in (%s)' % ",".join(subcategory_pks)]
+ where = " where " + " and ".join(wheres) if wheres else ""
+
+ equal_status = ''
+ if len(status) == 1:
+ equal_status = "='%s'" % status[0]
+ 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)
+ date_condition = getDateCondition()
+ sql_main = '''select subcat.id as id, subcat.category_id as category_id,
+ subcat.name as name, subcat.available as available,
+ subcat.icon_id as icon_id, subcat.color_theme_id as color_theme_id,
+ 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 += '''
+ 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 += '''
+ 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)))
+ # 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"))
+ 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
+
+ class Meta:
+ verbose_name = _("Layer")
+
+
+class Area(models.Model, SimpleArea):
+ """Rectangular area of the map
+ """
+ name = models.CharField(_(u"Name"), max_length=150)
+ urn = models.SlugField(_(u"Area urn"), max_length=50, blank=True,
+ unique=True)
+ welcome_message = models.TextField(_(u"Welcome message"), blank=True,
+ 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"))
+ layers = SelectMultipleField(Layer, related_name='areas',
+ through='AreaLayers', blank=True)
+ 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 "
+ 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):
+ return self.name
+
+ class Meta:
+ ordering = ('order', 'name')
+ verbose_name = _("Area")
+
+ @classmethod
+ def getAvailable(cls):
+ '''Get available areas
+ '''
+ return cls.objects.filter(available=True)
+
+ 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,
+ )
+
+ def getIncludeMarker(self):
+ """
+ Get the sql statement for the test if the point is included in the area
+ """
+ return Q(point__contained=self.getWkt())
+
+ def getIncludeRoute(self):
+ """
+ Get the sql statement for the test if the route is included in the area
+ """
+ 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
+ area = kwargs['instance']
+ if area.default:
+ defaults = Area.objects.filter(default=True).exclude(pk=area.pk)
+ for default in defaults:
+ default.default = False
+ default.save()
+ # manage permissions
+ 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 = None
+ if area.urn != old_urn:
+ oldmnemo = 'change_area_' + old_urn
+ old_perm = Permission.objects.filter(codename=oldmnemo)
+ if old_perm.count():
+ perm = old_perm.all()[0]
+ perm.codename = 'change_area_' + area.urn
+ perm.save()
+ if not area.urn:
+ area.urn = defaultfilters.slugify(area.name)
+ area.save()
+ mnemo = 'change_area_' + area.urn
+ perm = Permission.objects.filter(codename=mnemo)
+ lbl = "Can change " + area.name
+ if not perm.count():
+ content_type, created = ContentType.objects.get_or_create(
+ app_label="chimere", model="area")
+ perm = Permission(name=lbl, content_type_id=content_type.id,
+ codename=mnemo)
+ perm.save()
+ else:
+ perm = perm.all()[0]
+ if old_name != area.name:
+ perm.name = lbl
+ perm.save()
+ # manage moderation group
+ groupname = area.name + " moderation"
+ if old_name != area.name:
+ old_groupname = old_name + " moderation"
+ old_gp = Group.objects.filter(name=old_groupname)
+ if old_gp.count():
+ old_gp = old_gp.all()[0]
+ old_gp.name = groupname
+ old_gp.save()
+ group = Group.objects.filter(name=groupname)
+ if not group.count():
+ group = Group.objects.create(name=groupname)
+ 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)
+ 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
+ """
+ perms = user.get_all_permissions()
+ areas = set()
+ prefix = 'chimere.change_area_'
+ for perm in perms:
+ if perm.startswith(prefix):
+ try:
+ area = Area.objects.get(urn=perm[len(prefix):])
+ areas.add(area)
+ except ObjectDoesNotExist:
+ 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()
+
+
+class AreaLayers(models.Model):
+ area = models.ForeignKey(Area)
+ layer = models.ForeignKey(Layer)
+ order = models.IntegerField(_(u"Order"))
+ default = models.NullBooleanField(_(u"Default layer"))
+
+ class Meta:
+ ordering = ('order',)
+ 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 "
+ 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,
+ 'A': JQueryAutoComplete,
+ 'B': forms.CheckboxInput,
+ }
+ type = models.CharField(_(u"Type"), max_length=1, choices=TYPE)
+
+ class Meta:
+ ordering = ('order',)
+ verbose_name = _("Property model")
+
+ def __unicode__(self):
+ return self.name
+
+ def getAttrName(self):
+ 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
+ '''
+ propertymodel = models.ForeignKey(PropertyModel, related_name='choices',
+ verbose_name=_(u"Property model"))
+ value = models.CharField(_(u"Value"), max_length=150)
+ available = models.BooleanField(_(u"Available"), default=True)
+
+ 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"), 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)
+ except (self.DoesNotExist, ValueError):
+ return ""
+ return unicode(self.value)
+
+ class Meta:
+ verbose_name = _(u"Property")
+
+ @property
+ def python_value(self):
+ if self.propertymodel.type == 'D':
+ try:
+ return datetime.date(*[int(val)
+ for val in self.value.split('-')])
+ except:
+ return ""
+ if self.propertymodel.type == 'C' and self.value:
+ try:
+ return PropertyModelChoice.objects.get(pk=self.value)
+ except (self.DoesNotExist, ValueError):
+ return None
+ else:
+ return self.value