#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2008-2012 É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 os, string, json, re import lxml.etree as ElementTree from datetime import datetime, timedelta from subprocess import Popen, PIPE from django.conf import settings from django.contrib.gis.db import models from django.contrib.gis.gdal import SpatialReference from django.contrib import admin from django.core.files import File from django.core.exceptions import ValidationError from django import forms from django.utils.translation import ugettext_lazy as _ from chimere.widgets import PointField, RouteField, SelectMultipleField, \ TextareaWidget from chimere.managers import BaseGeoManager class News(models.Model): """News of the site """ title = models.CharField(_(u"Name"), max_length=150) available = models.BooleanField(_(u"Available")) date = models.DateField(_(u"Date"), auto_now_add=True) content = models.TextField() def __unicode__(self): ordering = ["-date"] return self.title class Meta: verbose_name = _(u"News") verbose_name_plural = _(u"News") class TinyUrl(models.Model): """Tinyfied version of permalink parameters """ parameters = models.CharField(_(u"Parameters"), max_length=500) def __unicode__(self): return self.parameters class Meta: verbose_name = _(u"TinyUrl") digits = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" base = len(digits) @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) def __unicode__(self): return self.name class Meta: verbose_name = _(u"Color theme") class Color(models.Model): """Color """ code = models.CharField(_(u"Code"), max_length=6) order = models.IntegerField(_(u"Order")) color_theme = models.ForeignKey(ColorTheme, verbose_name=_(u"Color theme")) def __unicode__(self): return self.code class Meta: ordering = ["order"] verbose_name = _(u"Color") 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) def __unicode__(self): return self.name class Meta: ordering = ["order"] verbose_name = _(u"Category") 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")) 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")) name = models.CharField(_(u"Name"), max_length=150) available = models.BooleanField(_(u"Available")) areas = SelectMultipleField('Area', related_name='areas', blank=True) icon = models.ForeignKey(Icon, verbose_name=_(u"Icon")) color_theme = models.ForeignKey(ColorTheme, verbose_name=_(u"Color theme"), blank=True, null=True) order = models.IntegerField(_(u"Order")) TYPE = (('M', _(u'Marker')), ('R', _(u'Route')), ('B', _(u'Both')),) item_type = models.CharField(_(u"Item type"), max_length=1, choices=TYPE) def __unicode__(self): return u"%s / %s" % (self.category.name, self.name) class Meta: ordering = ["category", "order"] verbose_name = _(u"Subcategory") @classmethod def getAvailable(cls, item_types=None, area_name=None): '''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 area_name: area = Area.objects.get(urn=area_name) # if there some restrictions with categories limit them if area.subcategory_set.count(): sub_ids = [sub.id for sub in area.subcategory_set.all()] # if no area is defined for a category don't filter it sub_ids += [sub.id for sub in subcategories if not sub.areas.count()] subcategories = subcategories.filter(id__in=sub_ids) for sub_category in subcategories: if sub_category.category not in sub_categories: sub_categories[sub_category.category] = [] if sub_category.id in settings.CHIMERE_DEFAULT_CATEGORIES: sub_category.selected = True sub_category.category.selected = True sub_categories[sub_category.category].append(sub_category) return [(category, sub_cats) for category, sub_cats \ in sub_categories.items()] class GeographicItem(models.Model): name = models.CharField(_(u"Name"), max_length=150) categories = SelectMultipleField(SubCategory) submiter_session_key = models.CharField(_(u"Submitter session key"), blank=True, null=True, max_length=40) submiter_email = models.EmailField(_(u"Submitter email"), blank=True, null=True) submiter_comment = models.CharField(_(u"Submitter comment"), max_length=200, blank=True, null=True) STATUS = (('S', _(u'Submited')), ('A', _(u'Available')), ('M', _(u'Modified')), ('D', _(u'Disabled')),) STATUS_DCT = {} for key, label in STATUS: STATUS_DCT[key] = label status = models.CharField(_(u"Status"), max_length=1, choices=STATUS) if settings.CHIMERE_DAYS_BEFORE_EVENT: start_date = models.DateField(_(u"Start date"), blank=True, null=True, help_text=_(u"Not mandatory. Set it for dated item such as event. "\ u"Format YYYY-MM-DD")) end_date = models.DateField(_(u"End date"), blank=True, null=True, help_text=_(u"Not mandatory. Set it only if you have a multi-day "\ u"event. Format YYYY-MM-DD")) class Meta: abstract = True 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) if 'chimere_rss' in settings.INSTALLED_APPS: available_date = models.DateTimeField(_(u"Available Date"), blank=True, null=True) route = models.ForeignKey(u"Route", blank=True, null=True, related_name='associated_marker') pictures = models.ManyToManyField(u"PictureFile", verbose_name='Pictures', blank=True, null=True, related_name='marker') multimedia_files = models.ManyToManyField(u"MultimediaFile", verbose_name='Multimedia files', blank=True, null=True, related_name='marker') objects = BaseGeoManager() def __unicode__(self): return self.name 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 multimedia_items(self): pict = list(self.pictures.filter(miniature=False).all()) mm = list(self.multimedia_files.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)) @property def date(self): if settings.CHIMERE_DAYS_BEFORE_EVENT: return self.start_date class Meta: ordering = ('status', 'name') verbose_name = _(u"Point of interest") def getLatitude(self): '''Return the latitude ''' return self.point.y def getLongitude(self): '''Return the longitude ''' return self.point.x def getProperty(self, propertymodel, safe=None): """Get the property of an associated property model. If safe set to True, verify if the property is available """ if safe and not propertymodel.available: return try: property = Property.objects.get(propertymodel=propertymodel, marker=self) except Property.DoesNotExist: return return property def getProperties(self): """Get all the property availables """ properties = [] for pm in PropertyModel.objects.filter(available=True): property = self.getProperty(pm) if property: properties.append(property) return properties def saveProperties(self, values): """ Save properties """ for propertymodel in PropertyModel.objects.filter(available=True): properties = Property.objects.filter(marker=self, propertymodel=propertymodel).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() val = u"" if unicode(propertymodel.id) in values: val = values[unicode(propertymodel.id)] # new property if not properties: new_property = Property.objects.create(marker=self, propertymodel=propertymodel, value=val) new_property.save() else: property = properties[0] property.value = val property.save() def getGeoJSON(self, categories_id=[]): '''Return a GeoJSON string ''' jsons = [] for cat in self.categories.all(): if categories_id and cat.id not in categories_id: continue items = {'id':self.id, 'name':json.dumps(self.name), 'geometry':self.point.geojson, 'icon_path':cat.icon.image, 'icon_width':cat.icon.image.width, 'icon_height':cat.icon.image.height,} jsons.append(u'{"type":"Feature", "geometry":%(geometry)s, '\ u'"properties":{"pk": %(id)d, "name": %(name)s, '\ u'"icon_path":"%(icon_path)s", "icon_width":%(icon_width)d, '\ u'"icon_height":%(icon_height)d}}' % items) return ",".join(jsons) @property def default_category(self): # Should we select only available ones ? # Should we catch if not exists ? return self.categories.all()[0] def get_absolute_url(self): parameters = 'current_feature=%d&checked_categories=%s' % (self.id, self.default_category.pk) return settings.BASE_URL + 'ty/' + TinyUrl.getUrnByParameters(parameters) 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) 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 = [('', '--')], 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\?v=([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/([A-Za-z0-9]*)'), re.compile(r'vimeo.com/video/([A-Za-z0-9]*)')), "http://player.vimeo.com/video/%s") } class MultimediaFile(models.Model): name = models.CharField(_(u"Name"), max_length=150, blank=True, null=True) url = models.CharField(_(u"Url"), max_length=200) order = models.IntegerField(_(u"Order"), default=1) multimedia_type = models.ForeignKey(MultimediaType) def __unicode__(self): return self.name or u"" def save(self, *args, **kwargs): # manage iframe of video providers if self.multimedia_type.name.lower() in IFRAME_LINKS: regexps, lnk = IFRAME_LINKS[self.multimedia_type.name.lower()] key = None for regexp in regexps: key = regexp.findall(self.url) if key: key = key[0] break if key: self.url = lnk % key super(MultimediaFile, self).save(*args, **kwargs) class PictureFile(models.Model): name = models.CharField(_(u"Name"), max_length=150, blank=True, null=True) picture = models.ImageField(_(u"Image"), upload_to='upload', height_field='height', width_field='width') height = models.IntegerField(_(u"Height")) width = models.IntegerField(_(u"Width")) miniature = models.BooleanField(_(u"Display inside the description?")) order = models.IntegerField(_(u"Order"), default=1) def __unicode__(self): return self.name or u"" class RouteFile(models.Model): name = models.CharField(_(u"Name"), max_length=150) raw_file = models.FileField(_(u"Raw file (gpx or kml)"), upload_to='upload') simplified_file = models.FileField(_(u"Simplified file"), upload_to='upload', 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") 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 mainNS = string.Template("{http://www.topografix.com/GPX/1/0}$tag") trkpt = mainNS.substitute(tag="trkpt") file_name = settings.MEDIA_ROOT + self.simplified_file.name et = ElementTree.parse(open(file_name)) pts = [] for pt in et.findall("//" + trkpt): pts.append((pt.get("lon"), pt.get("lat"))) geojson_tpl = u'{"type":"Feature", "geometry":{ "type": "LineString", '\ '"coordinates":[%s]}}' wkt_tpl = u'LINESTRING(%s)' return wkt_tpl % u','.join([u'%s %s' % (pt[0], pt[1]) \ for pt in pts]) 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) objects = BaseGeoManager() def __unicode__(self): return self.name class Meta: ordering = ('status', 'name') verbose_name = _(u"Route") def get_init_multi(self): if not self.associated_file: return [] multis = [forms.model_to_dict(multi) for multi in self.associated_file.multimedia_files.all()] return multis def get_init_picture(self): if not self.associated_file: return [] picts = [forms.model_to_dict(pict) for pict in self.associated_file.pictures.all()] return picts def getProperty(self, propertymodel, safe=None): """Get the property of an associated property model. If safe set to True, verify if the property is available """ if safe and not propertymodel.available: return try: property = Property.objects.get(propertymodel=propertymodel, marker=self) except Property.DoesNotExist: return return property def getProperties(self): """Get all the property availables """ properties = [] for pm in PropertyModel.objects.filter(available=True): property = self.getProperty(pm) if property: properties.append(property) return properties def getGeoJSON(self, color="#000"): '''Return a GeoJSON string ''' if '#' not in color: color = '#' + color attributes = {'id':self.id, 'name':json.dumps(self.name), 'color':color, 'geometry':self.route.geojson,} return u'{"type":"Feature", "geometry":%(geometry)s, '\ u'"properties":{"pk": %(id)d, "name": %(name)s, '\ u'"color":"%(color)s"}}' % attributes def getTinyUrl(self): parameters = 'current_feature=%d&checked_categories=%s' % (self.id, self.categories[0].id) return TinyUrl.getUrnByParameters(parameters) def getDateCondition(): ''' Return an SQL condition for apparition of dates ''' if not settings.CHIMERE_DAYS_BEFORE_EVENT: return "" now = datetime.now().strftime('%Y-%m-%d') after = (datetime.now() + 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 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): """ Get categories for this area """ 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''' if filter_available: sql += ' where subcat.available = TRUE and cat.available = TRUE' 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''' if filter_available: sql += ' where subcat.available = TRUE and cat.available = TRUE' subcats.union(SubCategory.objects.raw(sql)) return subcats 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) order = models.IntegerField(_(u"Order")) 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) 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 getIncludeSql(self, geometry='"chimere_marker".point'): """ Get the sql statement for the test if the point is included in the area """ 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.EPSG_DISPLAY_PROJECTION ) sql = "ST_Contains(" + area + ", " + geometry + ")" return sql class PropertyModel(models.Model): '''Model for a property ''' name = models.CharField(_(u"Name"), max_length=150) order = models.IntegerField(_(u"Order")) available = models.BooleanField(_(u"Available")) TYPE = (('T', _('Text')), ('L', _('Long text')), ('P', _('Password'))) TYPE_WIDGET = {'T':forms.TextInput, 'L':TextareaWidget, 'P':forms.PasswordInput} type = models.CharField(_(u"Type"), max_length=1, choices=TYPE) def __unicode__(self): return self.name class Meta: ordering = ('order',) verbose_name = _("Property model") def getNamedId(self): '''Get the name used as named id (easily sortable) ''' return 'property_%d_%d' % (self.order, self.id) class Property(models.Model): '''Property for a POI ''' marker = models.ForeignKey(Marker, verbose_name=_(u"Point of interest")) propertymodel = models.ForeignKey(PropertyModel, verbose_name=_(u"Property model")) value = models.TextField(_(u"Value")) def __unicode__(self): return "%s : %s" % (str(self.propertymodel), self.value) class Meta: verbose_name = _(u"Property")