#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2008-2015 É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, datetime, pyexiv2, re, string import simplejson as json from lxml import etree from PIL import Image from subprocess import Popen, PIPE from BeautifulSoup import BeautifulSoup from django import forms from django.conf import settings from django.contrib import admin from django.contrib.auth.models import User, Permission, ContentType, Group from django.contrib.gis.db import models from django.contrib.gis.gdal import SpatialReference from django.core.files import File from django.core.exceptions import ValidationError, ObjectDoesNotExist from django.core.urlresolvers import reverse from django.db.models import Q from django.db.models.signals import post_save, pre_save, m2m_changed from django.template import defaultfilters from django.utils.translation import ugettext_lazy as _ from chimere.widgets import HiddenPointChooserWidget, PointField, RouteField, \ SelectMultipleField, TextareaWidget, \ DatePickerWidget from chimere.managers import BaseGeoManager from chimere.utils import KMLManager, OSMManager, ShapefileManager, \ GeoRSSManager, CSVManager, HtmlXsltManager, XMLXsltManager class Page(models.Model): """Simple extra pages """ title = models.CharField(_(u"Name"), max_length=150) mnemonic = models.CharField(_(u"Mnemonic"), max_length=10, blank=True, null=True) available = models.BooleanField(_(u"Available"), default=True) order = models.IntegerField(_(u"Order"), default=10, blank=True, null=True) template_path = models.CharField(_(u"Template path"), max_length=150, blank=True, null=True) content = models.TextField(blank=True, null=True) def __unicode__(self): ordering = ["order"] return self.title class Meta: verbose_name = _(u"Page") verbose_name_plural = _(u"Page") def page_post_save(sender, **kwargs): if not kwargs['instance']: return page = kwargs['instance'] if not page.mnemonic: page.mnemonic = defaultfilters.slugify(page.title) page.save() post_save.connect(page_post_save, sender=Page) def shortify(text): if not text: return '' if len(text) <= settings.CHIMERE_SHORT_DESC_LENGTH: return text desc = text[:settings.CHIMERE_SHORT_DESC_LENGTH] short_desc = "" # find a correct opportunity to cut for idx, c in enumerate(reversed(desc)): if c == '>': break if c == '<': short_desc = desc[:-(idx+1)] break if not short_desc: for idx, c in enumerate(reversed(desc)): if c == ' ' or c == '\n': short_desc = desc[:-(idx+1)] break return BeautifulSoup(short_desc).prettify() class News(models.Model): """News of the site """ title = models.CharField(_(u"Name"), max_length=150) available = models.BooleanField(_(u"Available")) is_front_page = models.NullBooleanField(_(u"Is front page"), blank=True, null=True) date = models.DateField(_(u"Date")) content = models.TextField() url = models.URLField(_(u"Url"), max_length=200, blank=True, null=True) areas = SelectMultipleField('Area', verbose_name=_(u"Associated areas"), blank=True, null=True) def __unicode__(self): ordering = ["-date"] return self.title class Meta: verbose_name = _(u"News") verbose_name_plural = _(u"News") @property def short_desc(self): return shortify(self.content) 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"), related_name='subcategories') name = models.CharField(_(u"Name"), max_length=150) available = models.BooleanField(_(u"Available"), default=True) submission = models.BooleanField(_(u"Available for submission"), default=True) TYPE = (('M', _(u'Marker')), ('R', _(u'Route')), ('B', _(u'Both')),) item_type = models.CharField(_(u"Item type"), max_length=1, choices=TYPE) dated = models.BooleanField(_(u"Is dated"), default=False) description = models.TextField(blank=True, null=True) icon = models.ForeignKey(Icon, verbose_name=_(u"Icon")) hover_icon = models.ForeignKey(Icon, verbose_name=_(u"Hover icon"), blank=True, null=True, related_name='subcat_hovered') color_theme = models.ForeignKey(ColorTheme, verbose_name=_(u"Color theme"), blank=True, null=True) as_layer = models.BooleanField(_(u"Displayed in the layer menu"), default=False) routing_warn = models.BooleanField(_(u"Routing warn"), default=False) order = models.IntegerField(_(u"Order"), default=1000) def __unicode__(self): return u"%s / %s" % (self.category.name, self.name) class Meta: ordering = ["category", "order"] verbose_name = _(u"Sub-category") verbose_name_plural = _(u"Sub-categories") @classmethod def getAvailable(cls, item_types=None, area_name=None, public=False): '''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 public: subcategories = subcategories.filter(submission=True) selected_cats = [] if area_name: area = Area.objects.get(urn=area_name) # if there some restrictions with categories limit them if area.subcategories.count(): sub_ids = [sub.id for sub in area.subcategories.all()] subcategories = subcategories.filter(id__in=sub_ids) selected_cats = [subcat.pk for subcat in area.default_subcategories.all()] for sub_category in subcategories.order_by('order'): if sub_category.category not in sub_categories: sub_categories[sub_category.category] = [] if sub_category.id in selected_cats: sub_category.selected = True sub_category.category.selected = True sub_categories[sub_category.category].append(sub_category) subcategories = [(cat, subcats) \ for cat, subcats in sub_categories.items()] get_cat_order = lambda cat_tuple: cat_tuple[0].order subcategories = sorted(subcategories, key=get_cat_order) return subcategories @classmethod def getAvailableTuples(cls, item_types=None, area_name=None): cats = [] for cat, subcats in cls.getAvailable(item_types=item_types, area_name=area_name): cats.append((unicode(cat), [(subcat.pk, subcat.name) for subcat in subcats])) return cats def getJSONDict(self): items = {'id':self.pk, 'name':self.name, 'description':self.description if self.description\ else '', 'icon':{'url':self.icon.image.url, 'width':self.icon.image.width, 'height':self.icon.image.height} } if self.hover_icon: items['icon_hover'] = {'url':self.hover_icon.image.url} return items def getJSON(self, categories_id=[]): '''Return a JSON string - mainly used to get description ''' json_string = json.dumps(self.getJSONDict()) return json_string STATUS = (('S', _(u'Submited')), ('A', _(u'Available')), ('M', _(u'Modified')), ('D', _(u'Disabled')), ('I', _(u'Imported'))) STATUS_DCT = dict(STATUS) IMPORTERS = {'KML':KMLManager, 'OSM':OSMManager, 'SHP':ShapefileManager, 'RSS':GeoRSSManager, 'CSV':CSVManager, 'XSLT':HtmlXsltManager, 'XXLT':XMLXsltManager } IMPORTER_CHOICES = (('KML', 'KML'), ('OSM', 'OSM'), ('SHP', 'Shapefile'), ('RSS', 'GeoRSS'), ('CSV', 'CSV'), ('XSLT', 'HTML-XSLT'), ('XXLT', 'XML-XSLT'), ) IMPORTER_CHOICES_DICT = dict(IMPORTER_CHOICES) class Importer(models.Model): ''' Data importer for a specific subcategory ''' importer_type = models.CharField(_(u"Importer type"), max_length=4, choices=IMPORTER_CHOICES) filtr = models.CharField(_(u"Filter"), max_length=200, blank=True, null=True) source = models.CharField(_(u"Web address"), max_length=200, blank=True, null=True, help_text=_(u"Don't forget the trailing slash")) source_file = models.FileField(_(u"Source file"), upload_to='import_files', blank=True, null=True) source_file_alt = models.FileField(_(u"Alt source file"), upload_to='import_files', blank=True, null=True) default_name = models.CharField(_(u"Name by default"), max_length=200, blank=True, null=True) srid = models.IntegerField(_(u"SRID"), blank=True, null=True) zipped = models.BooleanField(_(u"Zipped file"), default=False) overwrite = models.BooleanField(_(u"Overwrite existing data"), default=False) get_description = models.BooleanField(_(u"Get description from source"), default=False) default_description = models.TextField(_(u"Default description"), blank=True, null=True) origin = models.CharField(_(u"Origin"), max_length=100, blank=True, null=True) license = models.CharField(_(u"License"), max_length=100, blank=True, null=True) categories = SelectMultipleField(SubCategory, blank=True, null=True, verbose_name=_(u"Associated subcategories")) state = models.TextField(_(u"State"), blank=True, null=True) associate_marker_to_way = models.BooleanField(_(u"Automatically associate "\ u"a marker to a way"), default=False) automatic_update = models.BooleanField(_(u"Automatically updated"), default=False) default_status = models.CharField(_(u"Default status"), max_length=1, choices=STATUS, default='I') default_localisation = PointField(_(u"Default localisation"), srid=settings.CHIMERE_EPSG_DISPLAY_PROJECTION, blank=True, null=True, widget=HiddenPointChooserWidget) class Meta: verbose_name = _(u"Importer") def __unicode__(self): vals = [IMPORTER_CHOICES_DICT[self.importer_type], self.source, self.source_file.name, u", ".join([unicode(cat) for cat in self.categories.all()]), self.default_name] return u' %d: %s' % (self.pk, u" - ".join([unicode(v) for v in vals if v])) @property def manager(self): return IMPORTERS[self.importer_type](self) def display_categories(self): return u"\n".join([cat.name for cat in self.categories.all()]) def get_key_category_dict(self): dct = {} # if no category provided: all category are considered q = SubCategory.objects.all() if self.categories.count(): q = self.categories.all() for cat in q.all(): dct[defaultfilters.slugify(cat.name)] = cat for key_cat in self.key_categories.all(): dct[key_cat.key] = key_cat.category return dct class ImporterKeyCategories(models.Model): """ Association between key and categories """ importer = models.ForeignKey(Importer, verbose_name=_(u"Importer"), related_name='key_categories') category = models.ForeignKey(SubCategory, verbose_name=_(u"Category")) key = models.CharField(_(u"Import key"), max_length=200) class Meta: verbose_name = _(u"Importer - Key categories") 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_name = models.CharField(_(u"Submitter name or nickname"), blank=True, null=True, max_length=40) submiter_email = models.EmailField(_(u"Submitter email"), blank=True, null=True) submiter_comment = models.TextField(_(u"Submitter comment"), max_length=200, blank=True, null=True) status = models.CharField(_(u"Status"), max_length=1, choices=STATUS) import_key = models.CharField(_(u"Import key"), max_length=200, blank=True, null=True) import_version = models.IntegerField(_(u"Import version"), blank=True, null=True) import_source = models.CharField(_(u"Source"), max_length=200, blank=True, null=True) modified_since_import = models.BooleanField(_(u"Modified since last import"), default=True) not_for_osm = models.BooleanField(_(u"Not to be exported to OSM"), default=False) origin = models.CharField(_(u"Origin"), max_length=100, blank=True, null=True) license = models.CharField(_(u"License"), max_length=100, blank=True, null=True) 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 def get_key(self, key): key_vals = self.import_key.split(';') for k_v in key_vals: if k_v.startswith(key+':'): return k_v.split(':')[1] def set_key(self, key, value): new_keys, _set = '', None key_vals = self.import_key.split(';') if self.import_key else [] for k_v in key_vals: if ':' not in k_v: continue k, v = k_v.split(':') if k == key: _set = True new_keys += '%s:%s;' % (k, value) else: new_keys += '%s:%s;' % (k, v) if not _set: new_keys += '%s:%s;' % (key, value) self.import_key = new_keys modified_since_import = self.modified_since_import self.save() # preserve modified_since_import if modified_since_import != self.modified_since_import: self.modified_since_import = modified_since_import self.save() def has_modified(self): if (self.ref_item and self.ref_item != self) \ or self.__class__.objects.filter(ref_item=self ).exclude(pk=self.pk).count(): return True return False @classmethod def properties(cls): return [pm for pm in PropertyModel.objects.filter(available=True)] @classmethod def all_properties(cls): return [pm for pm in PropertyModel.objects.all()] def property_setter(cls, propertymodel): def setter(self, value): marker = self if cls == Route: if not self.associated_marker.objects.count(): return marker = self.associated_marker.objects.all()[0] marker.setProperty(propertymodel, value) return setter 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) available_date = models.DateTimeField(_(u"Available Date"), blank=True, null=True) # used by feeds route = models.ForeignKey(u"Route", blank=True, null=True, related_name='associated_marker') description = models.TextField(_(u"Description"), blank=True, null=True) is_front_page = models.NullBooleanField(_(u"Is front page"), blank=True, null=True) objects = models.GeoManager() def __unicode__(self): return self.name def __init__(self, *args, **kwargs): super(Marker, self).__init__(*args, **kwargs) # add read attributes for properties for pm in self.all_properties(): attr_name = pm.getAttrName() if not hasattr(self, attr_name): val = '' property = self.getProperty(pm) if property: val = property.python_value setattr(self, attr_name, val) if not hasattr(self, attr_name + '_set'): setattr(self, attr_name + '_set', property_setter(self.__class__, pm)) 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.filter(miniature=False).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).order_by('order')) @property def default_multimedia_items(self): return list(self.multimedia_files.filter(miniature=True ).order_by('order')) @property def date(self): if settings.CHIMERE_DAYS_BEFORE_EVENT and self.start_date: today = datetime.date.today() if self.end_date and self.start_date < today: return self.end_date return self.start_date @property def short_desc(self): return shortify(self.description) @property def geometry(self): return self.point.wkt @property def geom_attr(self): return 'point' 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 setProperty(self, pm, value): u""" Set a property """ properties = Property.objects.filter(marker=self, propertymodel=pm).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() if pm.type == 'C' and value: try: value = str(int(value)) except ValueError: choice = PropertyModelChoice.objects.filter(propertymodel=pm, value=value) if choice.count(): value = choice.all()[0].pk else: choice = PropertyModelChoice.objects.create(value=value, propertymodel=pm) value = choice.pk # new property if not properties: new_property = Property.objects.create(marker=self, propertymodel=pm, value=value) new_property.save() else: property = properties[0] property.value = value property.save() def saveProperties(self, values): """ Save properties """ for propertymodel in PropertyModel.objects.filter(available=True): val = u"" if unicode(propertymodel.id) in values: val = values[unicode(propertymodel.id)] self.setProperty(propertymodel, val) 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_hover_path':cat.hover_icon.image \ if cat.hover_icon else '', 'icon_width':cat.icon.image.width, 'icon_height':cat.icon.image.height, 'category_name':json.dumps(cat.name)} jsons.append(u'{"type":"Feature", "geometry":%(geometry)s, '\ u'"properties":{"pk": %(id)d, "name": %(name)s, '\ u'"icon_path":"%(icon_path)s", '\ u'"icon_hover_path":"%(icon_hover_path)s", '\ u'"icon_width":%(icon_width)d, '\ u'"icon_height":%(icon_height)d, '\ u'"category_name":%(category_name)s}}' % items) return ",".join(jsons) @property def default_category(self): # Should we select only available ones ? # Should we catch if not exists ? cats = self.categories if cats.count(): return cats.all()[0] def get_absolute_url(self, area_name=''): parameters = 'current_feature=%d' % self.id if self.default_category: parameters += '&checked_categories=%s' % self.default_category.pk urn = TinyUrl.getUrnByParameters(parameters) area_name = area_name + '/' if area_name else '' url = reverse('chimere:tiny', args=[area_name, urn]) return url PRE_ATTRS = { 'Marker':('name', 'description', 'start_date', 'geometry', 'import_version', 'modified_since_import'), 'Route':('name', 'geometry', 'import_version', 'modified_since_import'), 'Area':('urn', 'name'), } def geometry_pre_save(cls, pre_save_geom_values): def geom_pre_save(sender, **kwargs): if not kwargs['instance'] or not kwargs['instance'].pk: return instance = kwargs['instance'] try: instance = cls.objects.get(pk=instance.pk) pre_save_geom_values[instance.pk] = dict( [(attr, getattr(instance, attr)) for attr in PRE_ATTRS[cls.__name__]]) except ObjectDoesNotExist: pass return geom_pre_save pre_save_marker_values = {} def marker_pre_save(sender, **kwargs): if not kwargs['instance']: return geometry_pre_save(Marker, pre_save_marker_values)(sender, **kwargs) pre_save.connect(marker_pre_save, sender=Marker) def geometry_post_save(pre_save_geom_values): def geom_post_save(sender, **kwargs): if not kwargs['instance'] \ or kwargs['instance'].pk not in pre_save_geom_values: return instance = kwargs['instance'] pre = pre_save_geom_values[instance.pk] # force the reinit of modified_since_import if pre['modified_since_import'] != instance.modified_since_import: return if (instance.import_version != pre['import_version'] and instance.modified_since_import): instance.modified_since_import = False instance.save() return if instance.modified_since_import: return if [key for key in pre if pre not in ('import_version', 'modified_since_import') and getattr(instance, key) != pre[key]]: instance.modified_since_import = True instance.save() return geom_post_save def marker_post_save(sender, **kwargs): if not kwargs['instance'] or kwargs['created']: return geometry_post_save(pre_save_marker_values)(sender, **kwargs) post_save.connect(marker_post_save, sender=Marker) 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) class Meta: verbose_name = _(u"Multimedia type") verbose_name_plural = _(u"Multimedia types") 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 = [('', _("Automatic recognition"))], 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\?[A-Za-z0-9_\-\=\&]*v=([A-Za-z0-9_-]*)[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/video/([A-Za-z0-9]*)'), re.compile(r'vimeo.com/([A-Za-z0-9]*)'),), "http://player.vimeo.com/video/%s") } class MultimediaExtension(models.Model): name = models.CharField(_(u"Extension name"), max_length=6) multimedia_type = models.ForeignKey(MultimediaType, verbose_name=_(u"Associated multimedia type"), related_name='extensions') class Meta: verbose_name = _(u"Multimedia extension") verbose_name_plural = _(u"Multimedia extensions") def __unicode__(self): return self.name class MultimediaFile(models.Model): name = models.CharField(_(u"Name"), max_length=150) url = models.URLField(_(u"Url"), max_length=200) order = models.IntegerField(_(u"Order"), default=1) multimedia_type = models.ForeignKey(MultimediaType, blank=True, null=True) miniature = models.BooleanField(_(u"Display inside the description?"), default=settings.CHIMERE_MINIATURE_BY_DEFAULT) marker = models.ForeignKey(Marker, related_name='multimedia_files') class Meta: verbose_name = _(u"Multimedia file") verbose_name_plural = _(u"Multimedia files") def __unicode__(self): return self.name or u"" def multimediafile_post_save(sender, **kwargs): if not kwargs['instance'] or not kwargs['created']: return multimediafile = kwargs['instance'] # auto recognition of file types if not multimediafile.multimedia_type: url = multimediafile.url for mm_type in IFRAME_LINKS: res, embeded_url = IFRAME_LINKS[mm_type] if [r for r in res if r.search(url)]: multimedia_type = MultimediaType.objects.get( name__iexact=mm_type) multimediafile.multimedia_type = multimedia_type if not multimediafile.multimedia_type: ext = url.split(".")[-1] q = MultimediaExtension.objects.filter(name__iendswith=ext) if q.count(): multimediafile.multimedia_type = q.all()[0].multimedia_type else: # default to an iframe multimediafile.multimedia_type = \ MultimediaType.objects.filter(name__iexact='iframe').all()[0] # manage iframe of video providers if multimediafile.multimedia_type.name.lower() in IFRAME_LINKS: regexps, lnk = IFRAME_LINKS[multimediafile.multimedia_type.name.lower()] key = None for regexp in regexps: key = regexp.findall(multimediafile.url) if key: key = key[0] break if key: multimediafile.url = lnk % key mfs = MultimediaFile.objects.filter(marker=multimediafile.marker).exclude( pk=multimediafile.pk).order_by('order') for idx, mf in enumerate(mfs.all()): mf.order = idx + 1 mf.save() multimediafile.order = mfs.count() + 1 multimediafile.save() post_save.connect(multimediafile_post_save, sender=MultimediaFile) class PictureFile(models.Model): name = models.CharField(_(u"Name"), max_length=150) picture = models.ImageField(_(u"Image"), upload_to='pictures', height_field='height', width_field='width') height = models.IntegerField(_(u"Height"), blank=True, null=True) width = models.IntegerField(_(u"Width"), blank=True, null=True) miniature = models.BooleanField(_(u"Display inside the description?"), default=settings.CHIMERE_MINIATURE_BY_DEFAULT) thumbnailfile = models.ImageField(_(u"Thumbnail"), upload_to='pictures', blank=True, null=True, height_field='thumbnailfile_height', width_field='thumbnailfile_width') thumbnailfile_height = models.IntegerField(_(u"Thumbnail height"), blank=True, null=True) thumbnailfile_width = models.IntegerField(_(u"Thumbnail width"), blank=True, null=True) order = models.IntegerField(_(u"Order"), default=1) marker = models.ForeignKey(Marker, related_name='pictures') def __unicode__(self): return self.name or u"" class Meta: verbose_name = _(u"Picture file") verbose_name_plural = _(u"Picture files") def scale_image(max_x, pair): x, y = pair new_y = (float(max_x) / x) * y return (int(max_x), int(new_y)) IMAGE_EXIF_ORIENTATION_MAP = { 1: 0, 8: 2, 3: 3, 6: 4, } PYEXIV2_OLD_API = not hasattr(pyexiv2, 'ImageMetadata') def picturefile_post_save(sender, **kwargs): if not kwargs['instance']: return picturefile = kwargs['instance'] if kwargs['created']: filename = picturefile.picture.path metadata, orientation = None, None if PYEXIV2_OLD_API: metadata = pyexiv2.Image(filename) metadata.readMetadata() orientation = metadata['Exif.Image.Orientation'] \ if 'Exif.Image.Orientation' in metadata.exifKeys() else None else: metadata = pyexiv2.ImageMetadata(filename) metadata.read() orientation = metadata['Exif.Image.Orientation'].value \ if 'Exif.Image.Orientation' in metadata else None if orientation and orientation in IMAGE_EXIF_ORIENTATION_MAP \ and orientation > 1: metadata['Exif.Image.Orientation'] = 1 if PYEXIV2_OLD_API: metadata.writeMetadata() else: metadata.write() im = Image.open(filename) im = im.transpose(IMAGE_EXIF_ORIENTATION_MAP[orientation]) im.save(filename) if not picturefile.thumbnailfile: file = picturefile.picture # defining the filename and the thumbnail filename filehead, filetail = os.path.split(os.path.abspath(file.path)) basename, format = os.path.splitext(filetail) basename = defaultfilters.slugify(basename) basename = re.sub(r'-','_', basename) miniature = basename + '_thumb.jpg' filename = file.path miniature_filename = os.path.join(filehead, miniature) try: image = Image.open(filename) except: image = None if image: image_x, image_y = image.size if settings.CHIMERE_THUMBS_SCALE_HEIGHT: image_y, image_x = scale_image( settings.CHIMERE_THUMBS_SCALE_HEIGHT, (image_y, image_x)) elif settings.CHIMERE_THUMBS_SCALE_WIDTH: image_x, image_y = scale_image( settings.CHIMERE_THUMBS_SCALE_WIDTH, (image_x, image_y)) image.thumbnail([image_x, image_y], Image.ANTIALIAS) temp_image = open(miniature_filename, 'w') if image.mode != "RGB": image = image.convert('RGB') try: image.save(temp_image, 'JPEG', quality=90, optimize=1) except: image.save(temp_image, 'JPEG', quality=90) short_name = miniature_filename[len(settings.MEDIA_ROOT):] picturefile.thumbnailfile = short_name picturefile.save() if not kwargs['created']: return pfs = PictureFile.objects.filter(marker=picturefile.marker).exclude( pk=picturefile.pk).order_by('order') for idx, pf in enumerate(pfs.all()): pf.order = idx + 1 pf.save() picturefile.order = pfs.count() + 1 picturefile.save() post_save.connect(picturefile_post_save, sender=PictureFile) class RouteFile(models.Model): name = models.CharField(_(u"Name"), max_length=150) raw_file = models.FileField(_(u"Raw file (gpx or kml)"), upload_to='route_files') simplified_file = models.FileField(_(u"Simplified file"), upload_to='route_files', 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") verbose_name_plural = _(u"Route files") 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 file_name = settings.MEDIA_ROOT + self.simplified_file.name tree = etree.parse(file_name) pts = [] for pt in tree.getiterator(): if not pt.tag.endswith('trkpt'): continue 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) has_associated_marker = models.BooleanField(_(u"Has an associated marker"), default=True) objects = models.GeoManager() def __unicode__(self): return self.name class Meta: ordering = ('status', 'name') verbose_name = _(u"Route") def __init__(self, *args, **kwargs): super(Route, self).__init__(*args, **kwargs) self.description = '' try: associated_marker = Marker.objects.get(route=self) self.description = associated_marker.description except: associated_marker = None # add read attributes for properties for pm in self.properties(): attr_name = pm.getAttrName() if not hasattr(self, attr_name): val = '' if associated_marker: property = associated_marker.getProperty(pm) if property: val = property.python_value setattr(self, attr_name, val) if not hasattr(self, attr_name + '_set'): setattr(self, attr_name + '_set', property_setter(self.__class__, pm)) @property def geometry(self): return self.route.wkt @property def geom_attr(self): return 'route' def get_init_multi(self): if not self.associated_marker.count(): return [] multis = [forms.model_to_dict(multi) for multi in self.associated_marker.all()[0].multimedia_files.all()] return multis def get_init_picture(self): if not self.associated_marker.count(): return [] picts = [forms.model_to_dict(pict) for pict in self.associated_marker.all()[0].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) pre_save_route_values = {} def route_pre_save(sender, **kwargs): if not kwargs['instance']: return geometry_pre_save(Route, pre_save_route_values)(sender, **kwargs) pre_save.connect(route_pre_save, sender=Route) def route_post_save(sender, **kwargs): if not kwargs['instance']: return geometry_post_save(pre_save_route_values)(sender, **kwargs) instance = kwargs['instance'] # manage associated marker if instance.has_associated_marker: marker_fields = [f.attname for f in Marker._meta.fields] route_fields = [f.attname for f in Route._meta.fields] marker_dct = dict([(k, getattr(instance, k)) for k in marker_fields if k in route_fields and k not in ('id', 'ref_item_id')]) marker_dct['point'] = "SRID=%d;POINT(%f %f)" % (instance.route.srid, instance.route[0][0], instance.route[0][1]) marker, created = Marker.objects.get_or_create(route=instance, defaults=marker_dct) if not created: marker.status = instance.status marker.point = marker_dct['point'] marker.save() properties = {} for pm in instance.properties(): prop = instance.getProperty(pm) if prop: properties[pm.pk] = prop.python_value # fix mis-initialized markers if created: for cat in instance.categories.all(): marker.categories.add(cat) marker.saveProperties(properties) post_save.connect(route_post_save, sender=Route) def sync_m2m_route(sender, **kwargs): if kwargs['action'] not in ('post_add', 'post_clear', 'post_remove'): return route = kwargs['instance'] marker = route.associated_marker if not marker.count(): return marker = marker.all()[0] marker.categories.clear() if kwargs['action'] == 'post_clear': return for cat in route.categories.all(): marker.categories.add(cat) m2m_changed.connect(sync_m2m_route, sender=Route.categories.through) def getDateCondition(): ''' Return an SQL condition for apparition of dates ''' if not settings.CHIMERE_DAYS_BEFORE_EVENT: return "" now = datetime.datetime.now().strftime('%Y-%m-%d') after = (datetime.datetime.now() + \ datetime.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 AggregatedRoute(models.Model): ''' Database view for aggregated routes ''' route = models.MultiLineStringField() subcategory = models.ForeignKey(SubCategory) status = models.CharField(_(u"Status"), max_length=1, choices=STATUS) class Meta: managed = False db_table = 'chimere_aggregated_routes' def getGeoJSON(self, color="#000"): '''Return a GeoJSON string ''' if '#' not in color: color = '#' + color attributes = {'id':self.id, 'name':json.dumps(u'Aggregated route'), '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 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, area_name=None): """ Get categories for this area """ wheres = [] if area_name: subcategory_pks = [] for cat, subcats in SubCategory.getAvailable(area_name=area_name): for subcat in subcats: subcategory_pks.append(unicode(subcat.pk)) if filter_available: wheres += ['subcat.available = TRUE', 'cat.available = TRUE'] wheres += ['subcat.id in (%s)' % ",".join(subcategory_pks)] where = " where " + " and ".join(wheres) if wheres else "" 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''' sql += where 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''' sql += where # subcats.union(set(SubCategory.objects.raw(sql))) # set union behave strangely. Doing it manualy... for c in set(SubCategory.objects.raw(sql)): subcats.add(c) return subcats class Layer(models.Model): name = models.CharField(_(u"Name"), max_length=150) layer_code = models.TextField(_(u"Layer code"), max_length=300) def __unicode__(self): return self.name class Meta: verbose_name = _("Layer") 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) welcome_message = models.TextField(_(u"Welcome message"), blank=True, null=True) order = models.IntegerField(_(u"Order"), unique=True) 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) default = models.NullBooleanField(_(u"Default area"), help_text=_(u"Only one area is set by default")) layers = SelectMultipleField(Layer, related_name='areas', through='AreaLayers', blank=True) default_subcategories = SelectMultipleField(SubCategory, blank=True, verbose_name=_(u"Sub-categories checked by default")) dynamic_categories = models.NullBooleanField( _(u"Sub-categories dynamicaly displayed"), help_text=_(u"If checked, categories are only displayed in the menu if " u"they are available on the current extent.")) subcategories = SelectMultipleField(SubCategory, related_name='areas', blank=True, db_table='chimere_subcategory_areas', verbose_name=_(u"Restricted to theses sub-categories"), help_text=_(u"If no sub-category is set all sub-categories are " u"available")) external_css = models.URLField(_(u"Link to an external CSS"), blank=True, null=True) restrict_to_extent = models.BooleanField(_(u"Restrict to the area extent"), default=False) 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 getWkt(self): return "SRID=%d;POLYGON((%f %f,%f %f,%f %f,%f %f, %f %f))" % ( settings.CHIMERE_EPSG_DISPLAY_PROJECTION, 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, ) def getIncludeMarker(self): """ Get the sql statement for the test if the point is included in the area """ return Q(point__contained=self.getWkt()) def getIncludeRoute(self): """ Get the sql statement for the test if the route is included in the area """ return Q(route__contained=self.getWkt()) pre_save_area_values = {} def area_pre_save(sender, **kwargs): if not kwargs['instance']: return geometry_pre_save(Area, pre_save_area_values)(sender, **kwargs) pre_save.connect(area_pre_save, sender=Area) def area_post_save(sender, **kwargs): if not kwargs['instance']: return area = kwargs['instance'] if area.default: defaults = Area.objects.filter(default=True).exclude(pk=area.pk) for default in defaults: default.default = False default.save() # manage permissions old_urn, old_name = area.urn, area.name if area.pk in pre_save_area_values: old_urn, old_name = pre_save_area_values[area.pk] perm, old_groups, old_users = None, [], [] if area.urn != old_urn: oldmnemo = 'change_area_' + old_urn old_perm = Permission.objects.filter(codename=oldmnemo) if old_perm.count(): perm = old_perm.all()[0] perm.codename = 'change_area_' + area.urn perm.save() if not area.urn: area.urn = defaultfilters.slugify(area.name) area.save() mnemo = 'change_area_' + area.urn perm = Permission.objects.filter(codename=mnemo) lbl = "Can change " + area.name if not perm.count(): content_type, created = ContentType.objects.get_or_create( app_label="chimere", model="area") perm = Permission(name=lbl, content_type_id=content_type.id, codename=mnemo) perm.save() else: perm = perm.all()[0] if old_name != area.name: perm.name = lbl perm.save() # manage moderation group groupname = area.name + " moderation" if old_name != area.name: old_groupname = old_name + " moderation" old_gp = Group.objects.filter(name=old_groupname) if old_gp.count(): old_gp = old_gp.all()[0] old_gp.name = groupname old_gp.save() group = Group.objects.filter(name=groupname) if not group.count(): group = Group.objects.create(name=groupname) group.permissions.add(perm) for app_label, model in (('chimere', 'marker'), ('chimere', 'route'), ('chimere', 'multimediafile'), ('chimere', 'picturefile'), ('chimere', 'routefile')): ct, created = ContentType.objects.get_or_create(app_label=app_label, model=model) for p in Permission.objects.filter(content_type=ct).all(): group.permissions.add(p) post_save.connect(area_post_save, sender=Area) def get_areas_for_user(user): """ Getting subcats for a specific user """ perms = user.get_all_permissions() areas = set() prefix = 'chimere.change_area_' for perm in perms: if perm.startswith(prefix): try: area = Area.objects.get(urn=perm[len(prefix):]) areas.add(area) except ObjectDoesNotExist: pass return areas def get_users_by_area(area): if not area: return [] perm = 'change_area_'+area.urn return User.objects.filter(Q(groups__permissions__codename=perm)| Q(user_permissions__codename=perm)).all() class AreaLayers(models.Model): area = models.ForeignKey(Area) layer = models.ForeignKey(Layer) order = models.IntegerField(_(u"Order")) default = models.NullBooleanField(_(u"Default layer")) class Meta: ordering = ('order',) verbose_name = _("Layers") verbose_name_plural = _("Layers") 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")) mandatory = models.BooleanField(_(u"Mandatory")) subcategories = SelectMultipleField(SubCategory, related_name='properties', blank=True, verbose_name=_(u"Restricted to theses sub-categories"), help_text=_(u"If no sub-category is set all the property applies to all " u"sub-categories")) TYPE = (('T', _('Text')), ('L', _('Long text')), ('P', _('Password')), ('D', _("Date")), ('C', _("Choices")), ) TYPE_WIDGET = {'T':forms.TextInput, 'L':TextareaWidget, 'P':forms.PasswordInput, 'D':DatePickerWidget, 'C':forms.Select } 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 getAttrName(self): attr_name = defaultfilters.slugify(self.name) attr_name = re.sub(r'-','_', attr_name) return attr_name def getNamedId(self): '''Get the name used as named id (easily sortable) ''' return 'property_%d_%d' % (self.order, self.id) class PropertyModelChoice(models.Model): '''Choices for property model ''' propertymodel = models.ForeignKey(PropertyModel, related_name='choices', verbose_name=_(u"Property model")) value = models.CharField(_(u"Value"), max_length=150) available = models.BooleanField(_(u"Available"), default=True) def __unicode__(self): return unicode(self.value) class Meta: verbose_name = _(u"Model property choice") 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): if self.propertymodel.type == 'C': if not self.value: return '' try: return unicode(PropertyModelChoice.objects.get( pk=self.value).value) except self.DoesNotExist: return "" return unicode(self.value) class Meta: verbose_name = _(u"Property") @property def python_value(self): if self.propertymodel.type == 'D': try: return datetime.date(*[int(val) for val in self.value.split('-')]) except: return "" if self.propertymodel.type == 'C' and self.value: try: return PropertyModelChoice.objects.get(pk=self.value) except self.DoesNotExist: return None else: return self.value