summaryrefslogtreecommitdiff
path: root/chimere/models.py
diff options
context:
space:
mode:
Diffstat (limited to 'chimere/models.py')
-rw-r--r--chimere/models.py615
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")
+