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") +  | 
