diff options
Diffstat (limited to 'chimere/models.py')
| -rw-r--r-- | chimere/models.py | 615 |
1 files changed, 615 insertions, 0 deletions
diff --git a/chimere/models.py b/chimere/models.py new file mode 100644 index 0000000..8c80fab --- /dev/null +++ b/chimere/models.py @@ -0,0 +1,615 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2008-2011 É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, string, json +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.utils.translation import ugettext_lazy as _ + +from chimere.widgets import PointField, RouteField, SelectMultipleField + +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(_("uParameters"), 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.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 Marker(models.Model): + '''Marker for a POI + ''' + name = models.CharField(_(u"Name"), max_length=150) + categories = SelectMultipleField(SubCategory) + point = PointField(_(u"Localisation"), + srid=settings.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) + STATUS = (('S', _(u'Submited')), + ('A', _(u'Available')), + ('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.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")) + 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) + objects = models.GeoManager() + + def __unicode__(self): + return self.name + + @property + def date(self): + if settings.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) + + def get_absolute_url(self): + parameters = 'current_feature=%d&checked_categories=%s' % (self.id, + self.categories.all()[0].id) + return settings.BASE_URL + 'ty/' + TinyUrl.getUrnByParameters(parameters) + +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(models.Model): + '''Route on the map + ''' + name = models.CharField(_(u"Name"), max_length=150) + categories = SelectMultipleField(SubCategory) + route = RouteField(_(u"Route"), srid=settings.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) + STATUS = (('S', _(u'Submited')), + ('A', _(u'Available')), + ('D', _(u'Disabled')),) + STATUS_DCT = {} + for key, label in STATUS: + STATUS_DCT[key] = label + if settings.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")) + status = models.CharField(_(u"Status"), max_length=1, choices=STATUS) + objects = models.GeoManager() + + def __unicode__(self): + return self.name + + class Meta: + ordering = ('status', 'name') + verbose_name = _(u"Route") + + 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.DAYS_BEFORE_EVENT: + return "" + now = datetime.now().strftime('%Y-%m-%d') + after = (datetime.now() + timedelta(settings.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.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.EPSG_DISPLAY_PROJECTION) + lower_right_corner = models.PointField(_(u"Lower right corner"), + default='POINT(0 0)', srid=settings.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") + |
