#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2008-2016 Étienne Loks # 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 . # See the file COPYING for details. """ Models description """ import copy import datetime import json import os import pyexiv2 import re import tempfile import shutil from lxml import etree from PIL import Image from subprocess import Popen, PIPE from bs4 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.contrib.postgres.search import SearchVectorField, SearchVector from django.core.files import File from django.core.exceptions import ObjectDoesNotExist from django.core.urlresolvers import reverse from django.db.models import Q, Count from django.db.models.signals import post_save, pre_save 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(_("Name"), max_length=150) mnemonic = models.CharField(_("Mnemonic"), max_length=10, blank=True, null=True) available = models.BooleanField(_("Available"), default=True) order = models.IntegerField(_("Order"), default=10, blank=True, null=True) template_path = models.CharField(_("Template path"), max_length=150, blank=True, null=True) content = models.TextField(blank=True, null=True) class Meta: ordering = ["order"] verbose_name = _("Page") verbose_name_plural = _("Page") def __str__(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(_("Name"), max_length=150) available = models.BooleanField(_("Available")) is_front_page = models.NullBooleanField(_("Is front page"), blank=True, null=True) date = models.DateField(_("Date")) content = models.TextField() url = models.URLField(_("Url"), max_length=200, blank=True, null=True) areas = SelectMultipleField('Area', verbose_name=_("Associated areas"), blank=True) class Meta: ordering = ["-date"] verbose_name = _("News") verbose_name_plural = _("News") def __str__(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(_("Parameters"), max_length=500, unique=True) digits = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" base = len(digits) class Meta: verbose_name = _("TinyUrl") def __str__(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 = int(n / cls.base) if n == 0: break return urn class ColorTheme(models.Model): """Color theme """ name = models.CharField(_("Name"), max_length=150) class Meta: verbose_name = _("Color theme") def __str__(self): return self.name class Color(models.Model): """Color """ code = models.CharField(_("Code/name"), max_length=200, help_text=_("HTML code/name")) inner_code = models.CharField(_("Code/name (inner)"), max_length=200, help_text=_("HTML code/name"), blank=True, null=True) order = models.IntegerField(_("Order")) color_theme = models.ForeignKey(ColorTheme, verbose_name=_("Color theme")) class Meta: ordering = ["order"] verbose_name = _("Color") def __str__(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(_("Name"), max_length=150) available = models.BooleanField(_("Available")) order = models.IntegerField(_("Order")) description = models.TextField(blank=True, null=True) color = models.CharField(_("Color code/name"), max_length=200, help_text=_("HTML code/name"), blank=True, null=True) class Meta: ordering = ["order"] verbose_name = _("Category") def __str__(self): return self.name class Icon(models.Model): """ Icon """ name = models.CharField(_("Name"), max_length=150) image = models.ImageField(_("Image"), upload_to='icons', height_field='height', width_field='width') height = models.IntegerField(_("Height")) width = models.IntegerField(_("Width")) offset_x = models.IntegerField( _("Offset x"), default=10, help_text=_("Common value is half the icon width")) offset_y = models.IntegerField( _("Offset y"), default=20, help_text=_("Common value is icon height")) popup_offset_x = models.IntegerField(_("Popup offset x"), default=0, help_text=_("Common value is 0")) popup_offset_y = models.IntegerField( _("Popup offset y"), default=20, help_text=_("Common value is icon height")) def __str__(self): return self.name class Meta: verbose_name = _("Icon") class SubCategory(models.Model): '''Sub-category ''' category = models.ForeignKey(Category, verbose_name=_("Category"), related_name='subcategories') name = models.CharField(_("Name"), max_length=150) available = models.BooleanField(_("Available"), default=True) submission = models.BooleanField(_("Available for submission"), default=True) TYPE = (('M', _('Marker')), ('R', _('Route')), ('P', _('Polygon')), ('B', _('Both')),) item_type = models.CharField(_("Item type"), max_length=1, choices=TYPE) dated = models.BooleanField(_("Is dated"), default=False) description = models.TextField(blank=True, null=True) icon = models.ForeignKey(Icon, verbose_name=_("Icon")) hover_icon = models.ForeignKey( Icon, verbose_name=_("Hover icon"), blank=True, null=True, related_name='subcat_hovered') color_theme = models.ForeignKey(ColorTheme, verbose_name=_("Color theme"), blank=True, null=True, related_name='subcategories') as_layer = models.BooleanField(_("Displayed in the layer menu"), default=False) weight_formula = models.TextField(_("Weight formula"), default="", blank=True, null=True) routing_warn = models.BooleanField(_("Routing warn"), default=False) order = models.IntegerField(_("Order"), default=1000) keywords = models.TextField(_("Keywords"), max_length=200, blank=True, null=True) min_zoom = models.IntegerField( _("Minimum zoom for loading details"), blank=True, null=True, help_text=_("Optimization when too many data have to be displayed. " "Currently available only for route and polygon.")) simplify_tolerance = models.FloatField( _("Simplify tolerance for lower zoom"), blank=True, null=True, help_text=_("Only relevant when Minimum zoom is set. Use the " "Douglas-Peucker algorythm to simplify the geometry when " "details is not alvailable. Adjust to your data volume " "and your performance need. 0.0003 is a good starting " "point. Note: topology is not preserved.")) class Meta: ordering = ["category", "order"] verbose_name = _("Sub-category") verbose_name_plural = _("Sub-categories") def __str__(self): return "%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((str(cat), [(subcat.pk, subcat.name) for subcat in subcats])) return cats def getJSONDict(self): # don't crash if some image have disapears try: width = self.icon.image.width height = self.icon.image.height except IOError: width = 0 height = 0 items = {'id': self.pk, 'name': self.name, 'description': self.description if self.description else '', 'icon': {'url': self.icon.image.url, 'width': width, 'height': 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 __str__(self): return "{} / {}".format(self.user, self.subcategory) class Meta: verbose_name = _("Sub-category limit for user") verbose_name_plural = _("Sub-category limits for users") STATUS = (('S', _('Submited')), ('A', _('Available')), ('M', _('Modified')), ('D', _('Disabled')), ('I', _('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(_("Importer type"), max_length=4, choices=IMPORTER_CHOICES) filtr = models.TextField(_("Filter"), blank=True, null=True) source = models.CharField(_("Web address"), max_length=200, blank=True, null=True, help_text=_("Don't forget the trailing slash")) source_file = models.FileField( _("Source file"), upload_to='import_files', blank=True, null=True) source_file_alt = models.FileField( _("Alt source file"), upload_to='import_files', blank=True, null=True) default_name = models.CharField(_("Name by default"), max_length=200, blank=True, null=True) srid = models.IntegerField(_("SRID"), blank=True, null=True) zipped = models.BooleanField(_("Zipped file"), default=False) overwrite = models.BooleanField(_("Overwrite existing data"), default=False) get_description = models.BooleanField(_("Get description from source"), default=False) default_description = models.TextField(_("Default description"), blank=True, null=True) origin = models.CharField(_("Origin"), max_length=1000, blank=True, null=True) license = models.CharField(_("License"), max_length=1000, blank=True, null=True) categories = SelectMultipleField( SubCategory, blank=True, verbose_name=_("Associated subcategories")) state = models.TextField(_("State"), blank=True, null=True) automatic_update = models.BooleanField(_("Automatically updated"), default=False) default_status = models.CharField(_("Default status"), max_length=1, choices=STATUS, default='I') default_localisation = PointField( _("Default localisation"), srid=settings.CHIMERE_EPSG_DISPLAY_PROJECTION, blank=True, null=True, widget=HiddenPointChooserWidget) class Meta: verbose_name = _("Importer") def __str__(self): vals = [IMPORTER_CHOICES_DICT[self.importer_type], self.source, self.source_file.name, ", ".join([str(cat) for cat in self.categories.all()]), self.default_name] return ' %d: %s' % (self.pk, " - ".join([str(v) for v in vals if v])) @property def manager(self): return IMPORTERS[self.importer_type](self) def display_categories(self): return "\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=_("Importer"), related_name='key_categories') category = models.ForeignKey(SubCategory, verbose_name=_("Category")) key = models.CharField(_("Import key"), max_length=200) class Meta: verbose_name = _("Importer - Key categories") OVERLAY_CHOICES = ( ('JSON', 'GeoJSON'), ) class Overlay(models.Model): ''' Static overlay on the map ''' name = models.CharField(_("Name"), max_length=150) overlay_type = models.CharField(_("Importer type"), max_length=4, choices=OVERLAY_CHOICES) overlay_file = models.FileField(_("File")) style = models.TextField(_("Style definition"), blank=True, null=True, help_text=_("Javascript definition. Cf. to " "openlayers3 documentation.")) def __str__(self): return self.name or "" class Meta: verbose_name = _("Overlay file") verbose_name_plural = _("Overlay files") class GeographicItem(models.Model): categories = SelectMultipleField(SubCategory) name = models.TextField(_("Name")) description = models.TextField(_("Description"), blank=True, null=True) submiter_session_key = models.CharField( _("Submitter session key"), blank=True, null=True, max_length=40) submiter_name = models.CharField(_("Submitter name or nickname"), blank=True, null=True, max_length=40) submiter_email = models.EmailField(_("Submitter email"), blank=True, null=True) submiter_comment = models.TextField(_("Submitter comment"), max_length=200, blank=True, null=True) status = models.CharField(_("Status"), max_length=1, choices=STATUS) keywords = models.TextField(_("Keywords"), blank=True, null=True) import_key = models.CharField(_("Import key"), max_length=200, blank=True, null=True) import_version = models.IntegerField(_("Import version"), blank=True, null=True) import_source = models.CharField(_("Source"), max_length=200, blank=True, null=True) modified_since_import = models.BooleanField( _("Modified since last import"), default=True) not_for_osm = models.BooleanField(_("Not to be exported to OSM"), default=False) origin = models.CharField(_("Origin"), max_length=1000, blank=True, null=True) license = models.CharField(_("License"), max_length=1000, blank=True, null=True) start_date = models.DateField( _("Start date"), blank=True, null=True, help_text=_("Not mandatory. Set it for dated item such as event. " "Format YYYY-MM-DD")) end_date = models.DateField( _("End date"), blank=True, null=True, help_text=_("Not mandatory. Set it only if you have a multi-day " "event. Format YYYY-MM-DD")) weight = models.FloatField( _("Weight"), blank=True, null=True, help_text=_( "Weight are used for heatmap and clustering. A formula must be " "defined in the associated category.")) normalised_weight = models.FloatField( _("Normalised weight"), blank=True, null=True, help_text=_("The weight normalised to be between 0 and 1. " "Automatically recalculated.")) search_vector = SearchVectorField(_("Search vector"), blank=True, null=True, help_text=_("Auto filled at save")) class Meta: abstract = True def __str__(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)) @property def full_id(self): """ Get the full ID: "object type-primary key". For instance: "marker-1234" :return: full id of the object (string) """ return "{}-{}".format(str(self.__class__.__name__).lower(), self.pk) def get_geometry(self): return getattr(self, self.geom_attr) @property def geometry(self): return getattr(self, self.geom_attr).wkt def text_description(self): """ Convert the html description into a text description """ if not hasattr(self, 'description'): return "" soup = BeautifulSoup(self.description) # kill all script and style elements for script in soup(["script", "style"]): script.extract() # rip it out # get text text = soup.get_text() # break into lines and remove leading and trailing space on each lines = (line.strip() for line in text.splitlines()) # break multi-headlines into a line each chunks = (phrase.strip() for line in lines for phrase in line.split(" ")) # drop blank lines text = '\n'.join(chunk for chunk in chunks if chunk) # drop line breaks text = text.replace('\n', ' ') return text def _get_geom_item_fk_name(self): geom_attr = self.geom_attr 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): """ 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 = "" if str(propertymodel.id) in values: val = values[str(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 @property def image(self): if not self.pictures.count(): return return self.pictures.order_by('pk').all()[0] def get_full_dict(self): dct = {} # get all property even the one not displayed for pm in PropertyModel.objects.all(): dct[pm.slug] = str(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): self.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=_("Reference marker"), related_name='submited_marker') point = PointField(_("Localisation"), srid=settings.CHIMERE_EPSG_DISPLAY_PROJECTION) available_date = models.DateTimeField(_("Available Date"), blank=True, null=True) # used by feeds is_front_page = models.NullBooleanField(_("Is front page"), blank=True, null=True) objects = models.GeoManager() geom_attr = 'point' class Meta: ordering = ('status', 'name') verbose_name = _("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) 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': str(cat.icon.image), 'icon_hover_path': str(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) @classmethod def getGeoJSONs(self, queryset, limit_to_categories=[]): vals = [] q = queryset.select_related('categories').extra( select={'json': 'ST_AsGeoJSON(point)'}).values( 'json', 'name', 'pk', 'categories__pk') added, cats = [], {} for item in q.all(): if item['pk'] in added: continue if limit_to_categories and \ item["categories__pk"] not in limit_to_categories: continue if item['categories__pk'] not in cats: try: cat = SubCategory.objects.get( available=True, pk=item['categories__pk']) except SubCategory.DoesNotExist: continue cats[item['categories__pk']] = { 'icon_path': str(cat.icon.image), 'icon_hover_path': str(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: cats[item['categories__pk']].update( {'icon_width': cat.icon.image.width, 'icon_height': cat.icon.image.height, }) except IOError: pass dct = { "type": "Feature", "geometry": json.loads(item['json']), "properties": {"pk": item['pk'], "name": item['name'], 'key': "marker-{}".format(item['pk'])}} dct['properties'].update(cats[item['categories__pk']]) vals.append(dct) added.append(item['pk']) return vals @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'] changed = False pre = pre_save_geom_values[instance.pk] # force the reinit of modified_since_import if pre['modified_since_import'] == instance.modified_since_import: if (instance.import_version != pre['import_version'] and instance.modified_since_import): instance.modified_since_import = False changed = True elif not instance.modified_since_import and \ [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 changed = True if not getattr(instance, '_search_updated', None): instance._search_updated = True q = instance.__class__.objects.filter(pk=instance.pk) q = q.annotate( search=SearchVector( 'name', 'description', 'keywords', 'categories__keywords', 'properties__search_value', config=settings.CHIMERE_SEARCH_LANGUAGE )) instance.search_vector = q.all()[0].search changed = True if changed: 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=_("Reference polygon"), related_name='submited_polygon') polygon = PolygonField( _("Polygon"), srid=settings.CHIMERE_EPSG_DISPLAY_PROJECTION) picture = models.ImageField( _("Image"), upload_to='upload', blank=True, null=True, height_field='height', width_field='width') height = models.IntegerField(_("Height"), blank=True, null=True) width = models.IntegerField(_("Width"), blank=True, null=True) color = models.CharField( _("Color"), max_length=200, help_text=_("HTML code/name"), blank=True, null=True) inner_color = models.CharField( _("Inner color"), max_length=200, help_text=_("HTML code/name"), blank=True, null=True) objects = models.GeoManager() geom_attr = 'polygon' class Meta: ordering = ('status', 'name') verbose_name = _("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(_("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": 'Aggregated polygon'}} return json.dumps(attributes) class MultimediaType(models.Model): MEDIA_TYPES = (('A', _("Audio")), ('V', _("Video")), ('I', _("Image")), ('O', _("Other")),) media_type = models.CharField(_("Media type"), max_length=1, choices=MEDIA_TYPES) name = models.CharField(_("Name"), max_length=150) mime_type = models.CharField(_("Mime type"), max_length=50, blank=True, null=True) iframe = models.BooleanField(_("Inside an iframe"), default=False) available = models.BooleanField(_("Available"), default=True) class Meta: verbose_name = _("Multimedia type") verbose_name_plural = _("Multimedia types") def __str__(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(_("Extension name"), max_length=6) multimedia_type = models.ForeignKey( MultimediaType, verbose_name=_("Associated multimedia type"), related_name='extensions') class Meta: verbose_name = _("Multimedia extension") verbose_name_plural = _("Multimedia extensions") def __str__(self): return self.name class MultimediaFile(models.Model): name = models.CharField(_("Name"), max_length=150) url = models.URLField(_("Url"), max_length=200) order = models.IntegerField(_("Order"), default=1) multimedia_type = models.ForeignKey(MultimediaType, blank=True, null=True) miniature = models.BooleanField( _("Display inside the description?"), default=settings.CHIMERE_MINIATURE_BY_DEFAULT) marker = models.ForeignKey(Marker, related_name='multimedia_files', blank=True, null=True) route = models.ForeignKey('Route', related_name='multimedia_files', blank=True, null=True) polygon = models.ForeignKey(Polygon, related_name='multimedia_files', blank=True, null=True) class Meta: verbose_name = _("Multimedia file") verbose_name_plural = _("Multimedia files") def __str__(self): return self.name or "" 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(_("Name"), max_length=150) picture = models.ImageField(_("Image"), upload_to='pictures', height_field='height', width_field='width') height = models.IntegerField(_("Height"), blank=True, null=True) width = models.IntegerField(_("Width"), blank=True, null=True) miniature = models.BooleanField( _("Display inside the description?"), default=settings.CHIMERE_MINIATURE_BY_DEFAULT) thumbnailfile = models.ImageField( _("Thumbnail"), upload_to='pictures', blank=True, null=True, height_field='thumbnailfile_height', width_field='thumbnailfile_width') thumbnailfile_height = models.IntegerField(_("Thumbnail height"), blank=True, null=True) thumbnailfile_width = models.IntegerField(_("Thumbnail width"), blank=True, null=True) order = models.IntegerField(_("Order"), default=1) marker = models.ForeignKey(Marker, related_name='pictures', blank=True, null=True) route = models.ForeignKey('Route', related_name='pictures', blank=True, null=True) polygon = models.ForeignKey(Polygon, related_name='pictures', blank=True, null=True) def __str__(self): return self.name or "" class Meta: verbose_name = _("Picture file") verbose_name_plural = _("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, } 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 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 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(_("Name"), max_length=150) raw_file = models.FileField(_("Raw file (gpx or kml)"), upload_to='route_files') simplified_file = models.FileField( _("Simplified file"), upload_to='route_files', blank=True, null=True) TYPE = (('K', _('KML')), ('G', _('GPX'))) file_type = models.CharField(max_length=1, choices=TYPE) class Meta: ordering = ('name',) verbose_name = _("Route file") verbose_name_plural = _("Route files") def __str__(self): return self.name def process(self): if self.simplified_file: return input_name = settings.MEDIA_ROOT + self.raw_file.name temp_dir = tempfile.gettempdir() temp_path = os.path.join(temp_dir, 'temp_filename.gpx') temp_out_path = os.path.join(temp_dir, 'temp_out_filename.gpx') shutil.copy2(input_name, temp_path) 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', temp_path, '-x', settings.GPSBABEL_OPTIONS, '-o', 'gpx', '-F', temp_out_path] with Popen(cli_args, stderr=PIPE) as p: p.wait() if p.returncode: print(p.stderr.read()) # logger.error(p.stderr.read()) else: output_name = settings.MEDIA_ROOT + self.raw_file.name[:-4] + \ "_simplified" + ".gpx" shutil.copy2(temp_out_path, output_name) self.simplified_file = File(open(output_name)) self.save() os.remove(output_name) os.remove(temp_out_path) os.remove(temp_path) @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 = 'LINESTRING(%s)' return wkt_tpl % ','.join(['%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=_("Reference route"), related_name='submited_route') route = RouteField(_("Route"), srid=settings.CHIMERE_EPSG_DISPLAY_PROJECTION) associated_file = models.ForeignKey(RouteFile, blank=True, null=True, verbose_name=_("Associated file")) picture = models.ImageField( _("Image"), upload_to='upload', blank=True, null=True, height_field='height', width_field='width') height = models.IntegerField(_("Height"), blank=True, null=True) width = models.IntegerField(_("Width"), blank=True, null=True) color = models.CharField( _("Color"), max_length=200, help_text=_("HTML code/name"), blank=True, null=True) objects = models.GeoManager() geom_attr = 'route' class Meta: ordering = ('status', 'name') verbose_name = _("Route") 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 @classmethod def getGeoJSONs(self, queryset, color="#000", limit_to_categories=[]): vals, default_color = [], color q = queryset.select_related('categories').extra( select={'json': 'ST_AsGeoJSON(route)'}).values( 'json', 'name', 'pk', 'color', 'categories__pk') added = [] current_categories = {} for item in q.all(): if item['pk'] in added: continue if limit_to_categories and \ item["categories__pk"] not in limit_to_categories: continue color = default_color if item["color"]: color = item['color'] elif item["categories__pk"]: if item["categories__pk"] not in current_categories: cat = SubCategory.objects.get(pk=item["categories__pk"]) # [index, color list] current_categories[item["categories__pk"]] = \ [0, list(Color.objects.filter( color_theme=cat.color_theme))] idx, colors = current_categories[item["categories__pk"]] # category have a color theme if colors: c = colors[idx % len(colors)] color = c.code # index += 1 current_categories[item["categories__pk"]][0] += 1 vals.append({ "type": "Feature", "geometry": json.loads(item['json']), "properties": {"pk": item['pk'], "name": item['name'], 'key': "route-{}".format(item['pk']), 'color': color}}) added.append(item['pk']) return vals 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) post_save.connect(route_post_save, sender=Route) 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(_("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": '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=None): """ Defining upper left corner and lower right corner from a tuple """ if not area: return super(SimpleArea, self).__init__() assert len(area) == 4 x1, y1, x2, y2 = area self.upper_left_corner = SimplePoint(x1, y1) self.lower_right_corner = SimplePoint(x2, y2) return super(SimpleArea, self).__init__() 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(str(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 = "ST_GeometryFromText('POLYGON((%f %f,%f %f,%f %f,%f %f, %f %f"\ "))', %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 (str(self.upper_left_corner.x), str(self.upper_left_corner.y), str(self.lower_right_corner.x), str(self.lower_right_corner.y)) class Layer(models.Model): name = models.CharField(_("Name"), max_length=150) layer_code = models.TextField(_("Layer code")) extra_js_code = models.TextField( _("Extra JS code"), blank=True, null=True, default='', help_text=_("This code is loaded before the layer code.")) def __str__(self): return self.name class Meta: verbose_name = _("Layer") class Area(models.Model, SimpleArea): """Rectangular area of the map """ name = models.CharField(_("Name"), max_length=150) urn = models.SlugField(_("Area urn"), max_length=50, blank=True, unique=True) welcome_message = models.TextField(_("Welcome message"), blank=True, null=True) order = models.IntegerField(_("Order"), unique=True) available = models.BooleanField(_("Available")) # logo = models.ImageField(_("Logo"), upload_to='logos', blank=True, # null=True) upper_left_corner = models.PointField( _("Upper left corner"), default='POINT(0 0)', srid=settings.CHIMERE_EPSG_DISPLAY_PROJECTION) lower_right_corner = models.PointField( _("Lower right corner"), default='POINT(0 0)', srid=settings.CHIMERE_EPSG_DISPLAY_PROJECTION) default = models.NullBooleanField( _("Default area"), help_text=_("Only one area is set by default")) layers = models.ManyToManyField(Layer, related_name='areas', through='AreaLayers', blank=True) overlays = models.ManyToManyField(Overlay, related_name='overlays', through='AreaOverlays', blank=True) default_subcategories = models.ManyToManyField( SubCategory, blank=True, verbose_name=_("Sub-categories checked by default")) dynamic_categories = models.NullBooleanField( _("Sub-categories dynamicaly displayed"), help_text=_("If checked, categories are only displayed in the menu " "if they are available on the current extent.")) subcategories = models.ManyToManyField( SubCategory, related_name='areas', blank=True, db_table='chimere_subcategory_areas', verbose_name=_("Restricted to theses sub-categories"), help_text=_("If no sub-category is set all sub-categories are " "available")) display_category_menu = models.BooleanField( _("Display category menu"), default=True, help_text=_("If set to False, category menu will be hide and all " "categories will be always displayed.")) external_css = models.URLField(_("Link to an external CSS"), blank=True, null=True) restrict_to_extent = models.BooleanField(_("Restrict to the area extent"), default=False) allow_point_edition = models.BooleanField(_("Allow point edition"), default=True) allow_route_edition = models.BooleanField(_("Allow route edition"), default=True) allow_polygon_edition = models.BooleanField(_("Allow polygon edition"), default=True) use_search = models.BooleanField(_("Use search"), default=True) extra_map_def = models.TextField( _("Extra map definition"), blank=True, null=True, help_text=_("Extra javascript script loaded for this area. " "Carreful! To prevent breaking the map must be valid.")) objects = models.GeoManager() def __str__(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(_("Order")) default = models.NullBooleanField(_("Default layer")) class Meta: ordering = ('order',) verbose_name = _("Area - Layer") verbose_name_plural = _("Areas - Layers") class AreaOverlays(models.Model): area = models.ForeignKey(Area) overlay = models.ForeignKey(Overlay) order = models.IntegerField(_("Order")) class Meta: ordering = ('order',) verbose_name = _("Area - Overlay") verbose_name_plural = _("Areas - Overlays") class PropertyModel(models.Model): """ Model for a property """ name = models.CharField(_("Name"), max_length=150) slug = models.SlugField(_("Slug"), blank=True, null=True) order = models.IntegerField(_("Order")) available = models.BooleanField(_("Available")) mandatory = models.BooleanField(_("Mandatory")) subcategories = models.ManyToManyField( SubCategory, related_name='properties', blank=True, verbose_name=_("Restricted to theses sub-categories"), help_text=_("If no sub-category is set all the property applies to " "all sub-categories")) areas = models.ManyToManyField( 'Area', verbose_name=_("Restrict to theses areas"), blank=True, help_text=_("If no area is set the property apply to " "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(_("Type"), max_length=1, choices=TYPE) class Meta: ordering = ('order',) verbose_name = _("Property model") def __str__(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=_("Property model")) value = models.CharField(_("Value"), max_length=150) available = models.BooleanField(_("Available"), default=True) class Meta: verbose_name = _("Model property choice") def __str__(self): return str(self.value) class Property(models.Model): """ Property for a POI """ marker = models.ForeignKey( Marker, verbose_name=_("Point of interest"), blank=True, null=True, related_name='properties' ) route = models.ForeignKey( Route, verbose_name=_("Route"), blank=True, null=True, related_name='properties' ) polygon = models.ForeignKey( Polygon, verbose_name=_("Polygon"), blank=True, null=True, related_name='properties' ) propertymodel = models.ForeignKey(PropertyModel, verbose_name=_("Property model")) value = models.TextField(_("Value")) search_value = models.TextField(_("Search value"), blank=True, null=True, help_text=_("Auto filled at save")) def __str__(self): if self.propertymodel.type == 'C': if not self.value: return '' try: return str(PropertyModelChoice.objects.get( pk=self.value).value) except (self.DoesNotExist, ValueError): return "" return str(self.value) class Meta: verbose_name = _("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 def save(self, *args, **kwargs): self.search_value = str(self.python_value or "") super(Property, self).save(*args, **kwargs)