#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2008-2010 É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 """ from datetime import datetime, timedelta from django.utils.translation import ugettext_lazy as _ from django.contrib.gis.db import models from django.contrib.gis.gdal import SpatialReference from django.contrib import admin from chimere import settings from chimere.main.widgets import PointField, RouteField, SelectMultipleField class News(models.Model): """News of the site """ title = models.CharField(_("Name"), max_length=150) available = models.BooleanField(_("Available")) date = models.DateField(_("Date"), auto_now_add=True) content = models.TextField() def __unicode__(self): ordering = ["-date"] return self.title class Meta: verbose_name = _("News") verbose_name_plural = _("News") class TinyUrl(models.Model): """Tinyfied version of permalink parameters """ parameters = models.CharField(_("Parameters"), max_length=500) def __unicode__(self): return self.parameters class Meta: verbose_name = _("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(_("Name"), max_length=150) def __unicode__(self): return self.name class Meta: verbose_name = _("Color theme") class Color(models.Model): """Color """ code = models.CharField(_("Code"), max_length=6) order = models.IntegerField(_("Order")) color_theme = models.ForeignKey(ColorTheme, verbose_name=_("Color theme")) def __unicode__(self): return self.code class Meta: ordering = ["order"] verbose_name = _("Color") 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) def __unicode__(self): return self.name class Meta: ordering = ["order"] verbose_name = _("Category") 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")) def __unicode__(self): return self.name class Meta: verbose_name = _("Icon") class SubCategory(models.Model): '''Sub-category ''' category = models.ForeignKey(Category, verbose_name=_("Category")) name = models.CharField(_("Name"), max_length=150) available = models.BooleanField(_("Available")) areas = SelectMultipleField('Area', related_name='areas', blank=True) icon = models.ForeignKey(Icon, verbose_name=_("Icon")) color_theme = models.ForeignKey(ColorTheme, verbose_name=_("Color theme"), blank=True, null=True) order = models.IntegerField(_("Order")) TYPE = (('M', _('Marker')), ('R', _('Route')), ('B', _('Both')),) item_type = models.CharField(_("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 = _("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(_("Name"), max_length=150) categories = SelectMultipleField(SubCategory) point = PointField(_("Localisation"), srid=settings.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) STATUS = (('S', _('Submited')), ('A', _('Available')), ('D', _('Disabled')),) STATUS_DCT = {} for key, label in STATUS: STATUS_DCT[key] = label status = models.CharField(_("Status"), max_length=1, choices=STATUS) if settings.DAYS_BEFORE_EVENT: 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")) if 'chimere.rss' in settings.INSTALLED_APPS: available_date = models.DateTimeField(_("Available Date"), blank=True, null=True) route = models.ForeignKey("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 = _("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 jsons.append("""{"type":"Feature", "geometry":%(geometry)s, \ "properties":{"pk": %(id)d, "name": "%(name)s", \ "icon_path":"%(icon_path)s", "icon_width":%(icon_width)d, \ "icon_height":%(icon_height)d}}""" % {'id':self.id, 'name':self.name, 'icon_path':cat.icon.image, 'geometry':self.point.geojson, 'icon_width':cat.icon.image.width, 'icon_height':cat.icon.image.height,}) 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 Route(models.Model): '''Route on the map ''' name = models.CharField(_("Name"), max_length=150) categories = SelectMultipleField(SubCategory) route = RouteField(_("Route"), srid=settings.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) STATUS = (('S', _('Submited')), ('A', _('Available')), ('D', _('Disabled')),) STATUS_DCT = {} for key, label in STATUS: STATUS_DCT[key] = label if settings.DAYS_BEFORE_EVENT: 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")) status = models.CharField(_("Status"), max_length=1, choices=STATUS) objects = models.GeoManager() def __unicode__(self): return self.name class Meta: ordering = ('status', 'name') verbose_name = _("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 return """{"type":"Feature", "geometry":%(geometry)s, \ "properties":{"pk": %(id)d, "name": "%(name)s", \ "color":"%(color)s"}}""" % {'id':self.id, 'name':self.name, 'color':color, 'geometry':self.route.geojson,} 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 = "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 ) 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 main_subcategory subcat inner join main_category cat on cat.id=subcat.category_id''' sql = sql_main + ''' inner join main_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 main_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 main_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 main_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(_("Name"), max_length=150) urn = models.SlugField(_("Area urn"), max_length=50, blank=True, unique=True) order = models.IntegerField(_("Order")) available = models.BooleanField(_("Available")) upper_left_corner = models.PointField(_("Upper left corner"), default='POINT(0 0)', srid=settings.EPSG_DISPLAY_PROJECTION) lower_right_corner = models.PointField(_("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='"main_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(_("Name"), max_length=150) order = models.IntegerField(_("Order")) available = models.BooleanField(_("Available")) TYPE = (('T', _('Text')), ('L', _('Long text')), ('P', _('Password'))) TYPE_WIDGET = {'T':'forms.TextInput', 'L':'TextareaWidget', 'P':'forms.PasswordInput'} type = models.CharField(_("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=_("Point of interest")) propertymodel = models.ForeignKey(PropertyModel, verbose_name=_("Property model")) value = models.TextField(_("Value")) def __unicode__(self): return "%s : %s" % (str(self.propertymodel), self.value) class Meta: verbose_name = _("Property")