#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2010-2015 Étienne Loks # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # See the file COPYING for details. """ Models description """ from cStringIO import StringIO import copy import datetime from PIL import Image from importlib import import_module import os import re import tempfile import unicodecsv from django.conf import settings from django.core.cache import cache from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.files import File from django.core.files.uploadedfile import SimpleUploadedFile from django.core.validators import validate_slug from django.core.urlresolvers import reverse, NoReverseMatch from django.db.utils import DatabaseError from django.db.models import Q, Max, Count from django.db.models.base import ModelBase from django.db.models.signals import post_save, pre_delete from django.utils.translation import ugettext_lazy as _, ugettext, \ pgettext_lazy from django.utils.safestring import SafeUnicode, mark_safe from django.template.defaultfilters import slugify from django.contrib.auth.models import User, Group from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes import generic from django.contrib.gis.db import models from simple_history.models import HistoricalRecords as BaseHistoricalRecords from ishtar_common.ooo_replace import ooo_replace from ishtar_common.model_merging import merge_model_objects from ishtar_common.utils import get_cache from ishtar_common.data_importer import Importer, ImportFormater, \ IntegerFormater, FloatFormater, UnicodeFormater, DateFormater, \ TypeFormater, YearFormater, StrToBoolean, FileFormater def post_save_user(sender, **kwargs): user = kwargs['instance'] ishtaruser = None try: q = IshtarUser.objects.filter(username=user.username) if not q.count(): ishtaruser = IshtarUser.create_from_user(user) else: ishtaruser = q.all()[0] ADMINISTRATOR, created = PersonType.objects.get_or_create( txt_idx='administrator') if ishtaruser.is_superuser \ and not ishtaruser.has_right('administrator'): ishtaruser.person.person_types.add(ADMINISTRATOR) except DatabaseError: # manage when db is not synced pass post_save.connect(post_save_user, sender=User) class Imported(models.Model): imports = models.ManyToManyField( 'Import', blank=True, null=True, related_name="imported_%(app_label)s_%(class)s") class Meta: abstract = True class ValueGetter(object): _prefix = "" def get_values(self, prefix=''): if not prefix: prefix = self._prefix values = {} for field_name in self._meta.get_all_field_names(): if not hasattr(self, field_name): continue value = getattr(self, field_name) if hasattr(value, 'get_values'): values.update(value.get_values(prefix + field_name + '_')) else: values[prefix + field_name] = value values['KEYS'] = u'\n'.join(values.keys()) value_list = [] for key in values.keys(): if key in ('KEYS', 'VALUES'): continue value_list.append((key, unicode(values[key]))) values['VALUES'] = u'\n'.join( [u"%s: %s" % (k, v) for k, v in sorted(value_list, key=lambda x:x[0])]) for global_var in GlobalVar.objects.all(): values[global_var.slug] = global_var.value or "" return values @classmethod def get_empty_values(cls, prefix=''): if not prefix: prefix = cls._prefix values = {} for field_name in cls._meta.get_all_field_names(): values[prefix + field_name] = '' return values class HistoricalRecords(BaseHistoricalRecords): def create_historical_record(self, instance, type): try: history_modifier = getattr(instance, 'history_modifier', None) assert history_modifier except (User.DoesNotExist, AssertionError): # on batch removing of users, user could have disapeared return manager = getattr(instance, self.manager_name) attrs = {} for field in instance._meta.fields: attrs[field.attname] = getattr(instance, field.attname) q_history = instance.history\ .filter(history_modifier_id=history_modifier.pk)\ .order_by('-history_date', '-history_id') if not q_history.count(): manager.create(history_type=type, **attrs) return old_instance = q_history.all()[0] # multiple saving by the same user in a very short time are generaly # caused by post_save signals it is not relevant to keep them min_history_date = datetime.datetime.now() \ - datetime.timedelta(seconds=5) q = q_history.filter(history_date__isnull=False, history_date__gt=min_history_date)\ .order_by('-history_date', '-history_id') if q.count(): return # record a new version only if data have been changed for field in instance._meta.fields: if getattr(old_instance, field.attname) != attrs[field.attname]: manager.create(history_type=type, **attrs) return def valid_id(cls): # valid ID validator for models def func(value): try: cls.objects.get(pk=value) except ObjectDoesNotExist: raise ValidationError(_(u"Not a valid item.")) return func def valid_ids(cls): def func(value): if "," in value: value = value.split(",") for v in value: try: cls.objects.get(pk=v) except ObjectDoesNotExist: raise ValidationError( _(u"An item selected is not a valid item.")) return func def is_unique(cls, field): # unique validator for models def func(value): query = {field: value} try: assert cls.objects.filter(**query).count() == 0 except AssertionError: raise ValidationError(_(u"This item already exist.")) return func class OwnPerms: """ Manage special permissions for object's owner """ @classmethod def get_query_owns(cls, user): """ Query object to get own items """ return None # implement for each object def is_own(self, user): """ Check if the current object is owned by the user """ query = self.get_query_owns(user) if not query: return False query = query & Q(pk=self.pk) return self.objects.filter(query).count() @classmethod def has_item_of(cls, user): """ Check if the user own some items """ query = cls.get_query_owns(user) if not query: return False return cls.objects.filter(query).count() @classmethod def get_owns(cls, user): """ Get Own items """ if isinstance(user, User): user = IshtarUser.objects.get(user_ptr=user) if user.is_anonymous(): return cls.objects.filter(pk__isnull=True) query = cls.get_query_owns(user) if not query: return cls.objects.filter(pk__isnull=True) return cls.objects.filter(query).order_by(*cls._meta.ordering) class GeneralType(models.Model): """ Abstract class for "types" """ label = models.CharField(_(u"Label"), max_length=100) txt_idx = models.CharField( _(u"Textual ID"), validators=[validate_slug], max_length=100, unique=True) comment = models.TextField(_(u"Comment"), blank=True, null=True) available = models.BooleanField(_(u"Available"), default=True) HELP_TEXT = u"" class Meta: abstract = True unique_together = (('txt_idx', 'available'),) def __unicode__(self): return self.label @classmethod def create_default_for_test(cls): return [cls.objects.create(label='Test %d' % i) for i in range(5)] @property def short_label(self): return self.label @classmethod def get_help(cls, dct={}, exclude=[]): help_text = cls.HELP_TEXT c_rank = -1 help_items = u"\n" for item in cls.get_types(dct=dct, instances=True, exclude=exclude): if hasattr(item, '__iter__'): # TODO: manage multiple levels continue if not item.comment: continue if c_rank > item.rank: help_items += u"\n" elif c_rank < item.rank: help_items += u"
\n" c_rank = item.rank help_items += u"
%s
%s
" % ( item.label, u"
".join(item.comment.split('\n'))) c_rank += 1 if c_rank: help_items += c_rank * u"
" if help_text or help_items != u'\n': return mark_safe(help_text + help_items) return u"" @classmethod def get_types(cls, dct={}, instances=False, exclude=[], empty_first=True, default=None): base_dct = dct.copy() if hasattr(cls, 'parent'): return cls._get_parent_types( base_dct, instances, exclude=exclude, empty_first=empty_first, default=default) return cls._get_types(base_dct, instances, exclude=exclude, empty_first=empty_first, default=default) @classmethod def _get_types(cls, dct={}, instances=False, exclude=[], empty_first=True, default=None): dct['available'] = True if not instances and empty_first and not default: yield ('', '--') if default: try: default = cls.objects.get(txt_idx=default) yield(default.pk, _(unicode(default))) except cls.DoesNotExist: pass items = cls.objects.filter(**dct) if default: exclude.append(default.txt_idx) if exclude: items = items.exclude(txt_idx__in=exclude) for item in items.order_by(*cls._meta.ordering).all(): if instances: item.rank = 0 yield item else: yield (item.pk, _(unicode(item))) PREFIX = "› " @classmethod def _get_childs(cls, item, dct, prefix=0, instances=False, exclude=[]): prefix += 1 dct['parent'] = item childs = cls.objects.filter(**dct) if exclude: childs = childs.exclude(txt_idx__in=exclude) if hasattr(cls, 'order'): childs = childs.order_by('order') for child in childs.all(): if instances: child.rank = prefix yield child else: yield (child.pk, SafeUnicode(prefix * cls.PREFIX + unicode(_(unicode(child))))) for sub_child in cls._get_childs(child, dct, prefix, instances, exclude=exclude): yield sub_child @classmethod def _get_parent_types(cls, dct={}, instances=False, exclude=[], empty_first=True, default=None): dct['available'] = True if not instances and empty_first: yield ('', '--') dct['parent'] = None items = cls.objects.filter(**dct) if exclude: items = items.exclude(txt_idx__in=exclude) if hasattr(cls, 'order'): items = items.order_by('order') for item in items.all(): if instances: item.rank = 0 yield item else: yield (item.pk, unicode(item)) for child in cls._get_childs(item, dct, instances, exclude=exclude): yield child def save(self, *args, **kwargs): if not self.id and not self.label: self.label = u" ".join(u" ".join(self.txt_idx.split('-')) .split('_')).title() if not self.txt_idx: self.txt_idx = slugify(self.label)[:100] # clean old keys if self.pk: old = self.__class__.objects.get(pk=self.pk) content_type = ContentType.objects.get_for_model(self.__class__) if slugify(self.label) != slugify(old.label): ItemKey.objects.filter( object_id=self.pk, key=slugify(old.label), content_type=content_type).delete() if self.txt_idx != old.txt_idx: ItemKey.objects.filter( object_id=self.pk, key=old.txt_idx, content_type=content_type).delete() obj = super(GeneralType, self).save(*args, **kwargs) self.generate_key(force=True) return obj def add_key(self, key, force=False): content_type = ContentType.objects.get_for_model(self.__class__) if not force and ItemKey.objects.filter( key=key, content_type=content_type).count(): return if force: ItemKey.objects.filter(key=key, content_type=content_type)\ .exclude(object_id=self.pk).delete() ItemKey.objects.get_or_create(object_id=self.pk, key=key, content_type=content_type) def generate_key(self, force=False): for key in (slugify(self.label), self.txt_idx): self.add_key(key) def get_keys(self): keys = [self.txt_idx] content_type = ContentType.objects.get_for_model(self.__class__) for ik in ItemKey.objects.filter(content_type=content_type, object_id=self.pk).all(): keys.append(ik.key) return keys @classmethod def generate_keys(cls): # content_type = ContentType.objects.get_for_model(cls) for item in cls.objects.all(): item.generate_key() class ItemKey(models.Model): key = models.CharField(_(u"Key"), max_length=100) content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() content_object = generic.GenericForeignKey('content_type', 'object_id') importer = models.ForeignKey( 'Import', null=True, blank=True, help_text=_(u"Key specific to an import")) def __unicode__(self): return self.key class ImageModel(models.Model): image = models.ImageField(upload_to="upload/", blank=True, null=True) thumbnail = models.ImageField(upload_to='upload/thumbs/', blank=True, null=True) IMAGE_MAX_SIZE = settings.IMAGE_MAX_SIZE THUMB_MAX_SIZE = settings.THUMB_MAX_SIZE class Meta: abstract = True def has_changed(self, field): if not self.pk: return True manager = getattr(self.__class__, 'objects') old = getattr(manager.get(pk=self.pk), field) return not getattr(self, field) == old def create_thumb(self, image, size): """Returns the image resized to fit inside a box of the given size""" image.thumbnail(size, Image.ANTIALIAS) temp = StringIO() image.save(temp, 'jpeg') temp.seek(0) return SimpleUploadedFile('temp', temp.read()) def save(self, *args, **kwargs): # manage images if self.has_changed('image') and self.image: # convert to jpg filename = os.path.splitext(os.path.split(self.image.name)[-1])[0] old_path = self.image.path filename = "%s.jpg" % filename image = Image.open(self.image.file) # convert to RGB if image.mode not in ('L', 'RGB'): image = image.convert('RGB') # resize if necessary self.image.save(filename, self.create_thumb(image, self.IMAGE_MAX_SIZE), save=False) if old_path != self.image.path: os.remove(old_path) # save the thumbnail self.thumbnail.save( '_%s' % filename, self.create_thumb(image, self.THUMB_MAX_SIZE), save=False) super(ImageModel, self).save(*args, **kwargs) class HistoryError(Exception): def __init__(self, value): self.value = value def __str__(self): return repr(self.value) class BaseHistorizedItem(Imported): history_modifier = models.ForeignKey( User, related_name='+', on_delete=models.SET_NULL, verbose_name=_(u"Last editor"), blank=True, null=True) history_creator = models.ForeignKey( User, related_name='+', on_delete=models.SET_NULL, verbose_name=_(u"Creator"), blank=True, null=True) class Meta: abstract = True def save(self, *args, **kwargs): assert hasattr(self, 'history_modifier') if not self.id: self.history_creator = self.history_modifier super(BaseHistorizedItem, self).save(*args, **kwargs) return True def get_previous(self, step=None, date=None, strict=True): """ Get a "step" previous state of the item """ assert step or date historized = self.history.all() item = None if step: assert len(historized) > step item = historized[step] else: for step, item in enumerate(historized): if item.history_date == date: break # ended with no match if item.history_date != date: return item._step = step if len(historized) != (step + 1): item._previous = historized[step + 1].history_date else: item._previous = None if step > 0: item._next = historized[step - 1].history_date else: item._next = None item.history_date = historized[step].history_date model = self.__class__ for k in model._meta.get_all_field_names(): field = model._meta.get_field_by_name(k)[0] if hasattr(field, 'rel') and field.rel: if not hasattr(item, k + '_id'): setattr(item, k, getattr(self, k)) continue val = getattr(item, k + '_id') if not val: setattr(item, k, None) continue try: val = field.rel.to.objects.get(pk=val) setattr(item, k, val) except ObjectDoesNotExist: if strict: raise HistoryError(u"The class %s has no pk %d" % ( unicode(field.rel.to), val)) setattr(item, k, None) item.pk = self.pk return item @property def last_edition_date(self): try: return self.history.order_by('-history_date').all()[0].history_date except IndexError: return def rollback(self, date): """ Rollback to a previous state """ to_del, new_item = [], None for item in self.history.all(): if item.history_date == date: new_item = item break to_del.append(item) if not new_item: raise HistoryError(u"The date to rollback to doesn't exist.") try: for f in self._meta.fields: k = f.name if k != 'id' and hasattr(self, k): if not hasattr(new_item, k): k = k + "_id" setattr(self, k, getattr(new_item, k)) try: self.history_modifier = User.objects.get( pk=new_item.history_modifier_id) except User.ObjectDoesNotExist: pass self.save() except: raise HistoryError(u"The rollback has failed.") # clean the obsolete history for historized_item in to_del: historized_item.delete() def values(self): values = {} for f in self._meta.fields: k = f.name if k != 'id': values[k] = getattr(self, k) return values def get_show_url(self): try: return reverse('show-' + self.__class__.__name__.lower(), args=[self.pk, '']) except NoReverseMatch: return @property def associated_filename(self): if [True for attr in ('get_town_label', 'get_department', 'reference', 'short_class_name') if not hasattr(self, attr)]: return '' items = [slugify(self.get_department()), slugify(self.get_town_label()).upper(), slugify(self.short_class_name), slugify(self.reference), slugify(self.name or '').replace('-', '_').capitalize()] last_edition_date = self.last_edition_date if last_edition_date: items.append(last_edition_date.strftime('%Y%m%d')) else: items.append('00000000') return u"-".join([unicode(item) for item in items]) class GeneralRelationType(GeneralType): order = models.IntegerField(_(u"Order"), default=1) symmetrical = models.BooleanField(_(u"Symmetrical")) # # an inverse must be set # inverse_relation = models.ForeignKey( # 'RelationType', verbose_name=_(u"Inverse relation"), blank=True, # null=True) class Meta: abstract = True def clean(self): # cannot have symmetrical and an inverse_relation if self.symmetrical and self.inverse_relation: raise ValidationError( _(u"Cannot have symmetrical and an inverse_relation")) def save(self, *args, **kwargs): obj = super(GeneralRelationType, self).save(*args, **kwargs) # after saving check that the inverse_relation of the inverse_relation # point to the saved object if self.inverse_relation \ and (not self.inverse_relation.inverse_relation or self.inverse_relation.inverse_relation != self): self.inverse_relation.inverse_relation = self self.inverse_relation.symmetrical = False self.inverse_relation.save() return obj class GeneralRecordRelations(object): def save(self, *args, **kwargs): super(GeneralRecordRelations, self).save(*args, **kwargs) # after saving create the symetric or the inverse relation sym_rel_type = self.relation_type if not self.relation_type.symmetrical: sym_rel_type = self.relation_type.inverse_relation # no symetric/inverse is defined if not sym_rel_type: return dct = {'right_record': self.left_record, 'left_record': self.right_record, 'relation_type': sym_rel_type} self.__class__.objects.get_or_create(**dct) return self def post_delete_record_relation(sender, instance, **kwargs): # delete symmetrical or inverse relation sym_rel_type = instance.relation_type if not instance.relation_type.symmetrical: sym_rel_type = instance.relation_type.inverse_relation # no symetric/inverse is defined if not sym_rel_type: return dct = {'right_record': instance.left_record, 'left_record': instance.right_record, 'relation_type': sym_rel_type} instance.__class__.objects.filter(**dct).delete() class ShortMenuItem(object): def get_short_menu_class(self): return '' class LightHistorizedItem(BaseHistorizedItem): history_date = models.DateTimeField(default=datetime.datetime.now) class Meta: abstract = True def save(self, *args, **kwargs): super(LightHistorizedItem, self).save(*args, **kwargs) return True class GlobalVar(models.Model): slug = models.SlugField(_(u"Variable name"), unique=True) description = models.TextField(_(u"Description of the variable"), null=True, blank=True) value = models.TextField(_(u"Value"), null=True, blank=True) class Meta: verbose_name = _(u"Global variable") verbose_name_plural = _(u"Global variables") ordering = ['slug'] def __unicode__(self): return unicode(self.slug) @classmethod def get_cache(cls, slug): cache_key, value = get_cache(cls, slug) if value: return value try: obj = cls.objects.get(slug=slug) cache.set(cache_key, obj.value, settings.CACHE_TIMEOUT) return obj.value except cls.DoesNotExist: return None def cached_globalvar_changed(sender, **kwargs): if not kwargs['instance']: return var = kwargs['instance'] cache_key, value = get_cache(GlobalVar, var.slug) cache.set(cache_key, var.value, settings.CACHE_TIMEOUT) post_save.connect(cached_globalvar_changed, sender=GlobalVar) class UserDashboard: def __init__(self): types = IshtarUser.objects.values('person__person_types', 'person__person_types__label') self.types = types.annotate(number=Count('pk'))\ .order_by('person__person_types') class DashboardFormItem(object): @classmethod def get_periods(cls, slice='month', fltr={}, date_source='creation'): date_var = date_source + '_date' q = cls.objects.filter(**{date_var + '__isnull': False}) if fltr: q = q.filter(**fltr) if slice == 'year': return [res[date_var].year for res in list(q.values(date_var) .annotate(Count("id")).order_by())] elif slice == 'month': return [(res[date_var].year, res[date_var].month) for res in list(q.values(date_var) .annotate(Count("id")).order_by())] return [] @classmethod def get_by_year(cls, year, fltr={}, date_source='creation'): date_var = date_source + '_date' q = cls.objects.filter(**{date_var + '__isnull': False}) if fltr: q = q.filter(**fltr) return q.filter(**{date_var + '__year': year}).distinct('pk') @classmethod def get_by_month(cls, year, month, fltr={}, date_source='creation'): date_var = date_source + '_date' q = cls.objects.filter(**{date_var + '__isnull': False}) if fltr: q = q.filter(**fltr) q = q.filter( **{date_var + '__year': year, date_var + '__month': month}) return q.distinct('pk') @classmethod def get_total_number(cls, fltr={}): q = cls.objects if fltr: q = q.filter(**fltr) return q.distinct('pk').count() class Dashboard: def __init__(self, model, slice='year', date_source=None, show_detail=None, fltr={}): # don't provide date_source if it is not relevant self.model = model self.total_number = model.get_total_number(fltr) self.show_detail = show_detail history_model = self.model.history.model # last edited - created self.recents, self.lasts = [], [] for last_lst, modif_type in ((self.lasts, '+'), (self.recents, '~')): last_ids = history_model.objects.values('id')\ .annotate(hd=Max('history_date')) last_ids = last_ids.filter(history_type=modif_type) from archaeological_finds.models import Find if self.model == Find: last_ids = last_ids.filter( downstream_treatment_id__isnull=True) if modif_type == '+': last_ids = last_ids.filter( upstream_treatment_id__isnull=True) last_ids = last_ids.order_by('-hd').distinct().all()[:5] for idx in last_ids: try: obj = self.model.objects.get(pk=idx['id']) except: # deleted object are always referenced in history continue obj.history_date = idx['hd'] last_lst.append(obj) # years base_kwargs = {'fltr': fltr.copy()} if date_source: base_kwargs['date_source'] = date_source periods_kwargs = copy.deepcopy(base_kwargs) periods_kwargs['slice'] = slice self.periods = model.get_periods(**periods_kwargs) self.periods = list(set(self.periods)) self.periods.sort() if not self.total_number or not self.periods: return kwargs_num = copy.deepcopy(base_kwargs) self.serie_labels = [_(u"Total")] # numbers if slice == 'year': self.values = [('year', "", list(reversed(self.periods)))] self.numbers = [model.get_by_year(year, **kwargs_num).count() for year in self.periods] self.values += [('number', _(u"Number"), list(reversed(self.numbers)))] if slice == 'month': periods = list(reversed(self.periods)) self.periods = ["%d-%s-01" % (p[0], ('0' + str(p[1])) if len(str(p[1])) == 1 else p[1]) for p in periods] self.values = [('month', "", self.periods)] if show_detail: for dpt in settings.ISHTAR_DPTS: self.serie_labels.append(unicode(dpt)) idx = 'number_' + unicode(dpt) kwargs_num['fltr']["towns__numero_insee__startswith"] = \ unicode(dpt) numbers = [model.get_by_month(*p.split('-')[:2], **kwargs_num).count() for p in self.periods] self.values += [(idx, dpt, list(numbers))] # put "Total" at the end self.serie_labels.append(self.serie_labels.pop(0)) kwargs_num = base_kwargs.copy() self.numbers = [model.get_by_month(*p.split('-')[:2], **kwargs_num).count() for p in self.periods] self.values += [('number', _(u"Total"), list(self.numbers))] # calculate self.average = self.get_average() self.variance = self.get_variance() self.standard_deviation = self.get_standard_deviation() self.median = self.get_median() self.mode = self.get_mode() # by operation if not hasattr(model, 'get_by_operation'): return operations = model.get_operations() operation_numbers = [model.get_by_operation(op).count() for op in operations] # calculate self.operation_average = self.get_average(operation_numbers) self.operation_variance = self.get_variance(operation_numbers) self.operation_standard_deviation = self.get_standard_deviation( operation_numbers) self.operation_median = self.get_median(operation_numbers) operation_mode_pk = self.get_mode(dict(zip(operations, operation_numbers))) if operation_mode_pk: from archaeological_operations.models import Operation self.operation_mode = unicode(Operation.objects .get(pk=operation_mode_pk)) def get_average(self, vals=[]): if not vals: vals = self.numbers[:] return sum(vals) / len(vals) def get_variance(self, vals=[]): if not vals: vals = self.numbers[:] avrg = self.get_average(vals) return self.get_average([(x - avrg) ** 2 for x in vals]) def get_standard_deviation(self, vals=[]): if not vals: vals = self.numbers[:] return round(self.get_variance(vals) ** 0.5, 3) def get_median(self, vals=[]): if not vals: vals = self.numbers[:] len_vals = len(vals) vals.sort() if (len_vals % 2) == 1: return vals[len_vals / 2] else: return (vals[len_vals / 2 - 1] + vals[len_vals / 2]) / 2.0 def get_mode(self, vals={}): if not vals: vals = dict(zip(self.periods, self.numbers[:])) mx = max(vals.values()) for v in vals: if vals[v] == mx: return v class DocumentTemplate(models.Model): CLASSNAMES = (('archaeological_operations.models.AdministrativeAct', _(u"Administrative Act")),) name = models.CharField(_(u"Name"), max_length=100) template = models.FileField(_(u"Template"), upload_to="upload/templates/") associated_object_name = models.CharField( _(u"Associated object"), max_length=100, choices=CLASSNAMES) available = models.BooleanField(_(u"Available"), default=True) class Meta: verbose_name = _(u"Document template") verbose_name_plural = _(u"Document templates") ordering = ['associated_object_name', 'name'] def __unicode__(self): return self.name @classmethod def get_tuples(cls, dct={}, empty_first=True): dct['available'] = True if empty_first: yield ('', '----------') items = cls.objects.filter(**dct) for item in items.distinct().order_by(*cls._meta.ordering).all(): yield (item.pk, _(unicode(item))) def publish(self, c_object): tempdir = tempfile.mkdtemp("-ishtardocs") output_name = tempdir + os.path.sep + \ slugify(self.name.replace(' ', '_').lower()) + u'-' +\ datetime.date.today().strftime('%Y-%m-%d') +\ u"." + self.template.name.split('.')[-1] values = c_object.get_values() ooo_replace(self.template, output_name, values) return output_name class State(models.Model): label = models.CharField(_(u"Label"), max_length=30) number = models.CharField(_(u"Number"), unique=True, max_length=3) class Meta: verbose_name = _(u"State") ordering = ['number'] def __unicode__(self): return self.label class Department(models.Model): label = models.CharField(_(u"Label"), max_length=30) number = models.CharField(_(u"Number"), unique=True, max_length=3) state = models.ForeignKey('State', verbose_name=_(u"State"), blank=True, null=True) class Meta: verbose_name = _(u"Department") verbose_name_plural = _(u"Departments") ordering = ['number'] def __unicode__(self): return self.label class Address(BaseHistorizedItem): address = models.TextField(_(u"Address"), null=True, blank=True) address_complement = models.TextField(_(u"Address complement"), null=True, blank=True) postal_code = models.CharField(_(u"Postal code"), max_length=10, null=True, blank=True) town = models.CharField(_(u"Town"), max_length=70, null=True, blank=True) country = models.CharField(_(u"Country"), max_length=30, null=True, blank=True) phone = models.CharField(_(u"Phone"), max_length=18, null=True, blank=True) mobile_phone = models.CharField(_(u"Mobile phone"), max_length=18, null=True, blank=True) email = models.EmailField( _(u"Email"), max_length=75, blank=True, null=True) history = HistoricalRecords() class Meta: abstract = True class Merge(models.Model): merge_key = models.CharField(_("Merge key"), max_length=300, blank=True, null=True) merge_candidate = models.ManyToManyField("self", blank=True, null=True) merge_exclusion = models.ManyToManyField("self", blank=True, null=True) # 1 for one word similarity, 2 for two word similarity, etc. MERGE_CLEMENCY = None EMPTY_MERGE_KEY = '--' class Meta: abstract = True def generate_merge_key(self): self.merge_key = slugify(self.name if self.name else '')[:300] if not self.merge_key: self.merge_key = self.EMPTY_MERGE_KEY self.merge_key = self.merge_key[:300] def generate_merge_candidate(self): if not self.merge_key: self.generate_merge_key() self.save() if not self.pk or self.merge_key == self.EMPTY_MERGE_KEY: return q = self.__class__.objects\ .exclude(pk=self.pk)\ .exclude(merge_exclusion=self)\ .exclude(merge_candidate=self) if not self.MERGE_CLEMENCY: q = q.filter(merge_key=self.merge_key) else: subkeys_front = u"-".join( self.merge_key.split('-')[:self.MERGE_CLEMENCY]) subkeys_back = u"-".join( self.merge_key.split('-')[-self.MERGE_CLEMENCY:]) q = q.filter(Q(merge_key__istartswith=subkeys_front) | Q(merge_key__iendswith=subkeys_back)) for item in q.all(): self.merge_candidate.add(item) def save(self, *args, **kwargs): self.generate_merge_key() item = super(Merge, self).save(*args, **kwargs) self.generate_merge_candidate() return item def merge(self, item): merge_model_objects(self, item) self.generate_merge_candidate() class OrganizationType(GeneralType): class Meta: verbose_name = _(u"Organization type") verbose_name_plural = _(u"Organization types") ordering = ('label',) MODELS = [ ('archaeological_operations.models.Operation', _(u"Operation")), ('archaeological_operations.models.Parcel', _(u"Parcels")), ('archaeological_operations.models.OperationSource', _(u"Operation source")), ] IMPORTER_CLASSES = [] if 'archaeological_files' in settings.INSTALLED_APPS: MODELS = [('archaeological_files.models.File', _(u"Archaeological files"))]\ + MODELS IMPORTER_CLASSES = [ 'archaeological_files.data_importer.FileImporterSraPdL'] + \ IMPORTER_CLASSES if 'archaeological_context_records' in settings.INSTALLED_APPS: MODELS = [('archaeological_context_records.models.ContextRecord', _(u"Context records")), ] + MODELS if 'archaeological_finds' in settings.INSTALLED_APPS: MODELS = [('archaeological_finds.models.BaseFind', _(u"Finds")), ] + MODELS def get_model_fields(model): """ Return a dict of fields from model To be replace in Django 1.8 with get_fields, get_field """ fields = {} options = model._meta for field in sorted(options.fields + options.many_to_many): fields[field.name] = field if hasattr(model, 'get_extra_fields'): fields.update(model.get_extra_fields()) return fields def import_class(full_path_classname): mods = full_path_classname.split('.') if len(mods) == 1: mods = ['ishtar_common', 'models', mods[0]] module = import_module('.'.join(mods[:-1])) return getattr(module, mods[-1]) class ImporterType(models.Model): """ Description of a table to be mapped with ishtar database """ name = models.CharField(_(u"Name"), blank=True, null=True, max_length=100) slug = models.SlugField(_(u"Slug"), unique=True, blank=True, null=True, max_length=100) description = models.CharField(_(u"Description"), blank=True, null=True, max_length=500) users = models.ManyToManyField('IshtarUser', verbose_name=_(u"Users"), blank=True, null=True) associated_models = models.CharField(_(u"Associated model"), max_length=200, choices=MODELS) is_template = models.BooleanField(_(u"Is template"), default=False) class Meta: verbose_name = _(u"Importer - Type") verbose_name_plural = _(u"Importer - Types") def __unicode__(self): return self.name def get_importer_class(self): if self.slug and self.slug in IMPORTER_CLASSES: cls = import_class(IMPORTER_CLASSES[self.slug]) return cls OBJECT_CLS = import_class(self.associated_models) DEFAULTS = dict([(default.keys, default.values) for default in self.defaults.all()]) LINE_FORMAT = [] idx = 0 for column in self.columns.order_by('col_number').all(): idx += 1 while column.col_number > idx: LINE_FORMAT.append(None) idx += 1 targets = [] formater_types = [] nb = column.targets.count() if not nb: LINE_FORMAT.append(None) continue force_news = [] for target in column.targets.all(): ft = target.formater_type.get_formater_type(target) if not ft: continue formater_types.append(ft) targets.append(target.target) force_news.append(target.force_new) formater_kwargs = {} if column.regexp_pre_filter: formater_kwargs['regexp'] = re.compile( column.regexp_pre_filter.regexp) formater_kwargs['duplicate_fields'] = [ (field.field_name, field.force_new) for field in column.duplicate_fields.all()] formater_kwargs['required'] = column.required formater_kwargs['force_new'] = force_news formater = ImportFormater(targets, formater_types, **formater_kwargs) LINE_FORMAT.append(formater) args = {'OBJECT_CLS': OBJECT_CLS, 'DESC': self.description, 'DEFAULTS': DEFAULTS, 'LINE_FORMAT': LINE_FORMAT} name = str(''.join( x for x in slugify(self.name).replace('-', ' ').title() if not x.isspace())) newclass = type(name, (Importer,), args) return newclass def get_associated_model(parent_model, keys): model = None OBJECT_CLS = None if isinstance(parent_model, unicode) or \ isinstance(parent_model, str): OBJECT_CLS = import_class(parent_model) else: OBJECT_CLS = parent_model for idx, item in enumerate(keys): if not idx: field = get_model_fields(OBJECT_CLS)[item] if hasattr(field, 'rel') and hasattr(field.rel, 'to'): model = field.rel.to if type(field) == ModelBase: model = field else: return get_associated_model(model, keys[1:]) return model class ImporterDefault(models.Model): """ Targets of default values in an import """ importer_type = models.ForeignKey(ImporterType, related_name='defaults') target = models.CharField(u"Target", max_length=500) class Meta: verbose_name = _(u"Importer - Default") verbose_name_plural = _(u"Importer - Defaults") @property def keys(self): return tuple(self.target.split('__')) @property def associated_model(self): return get_associated_model(self.importer_type.associated_models, self.keys) @property def values(self): values = {} for default_value in self.default_values.all(): values[default_value.target] = default_value.get_value() return values class ImporterDefaultValues(models.Model): """ Default values in an import """ default_target = models.ForeignKey(ImporterDefault, related_name='default_values') target = models.CharField(u"Target", max_length=500) value = models.CharField(u"Value", max_length=500) class Meta: verbose_name = _(u"Importer - Default value") verbose_name_plural = _(u"Importer - Default values") def get_value(self): parent_model = self.default_target.associated_model if not parent_model: return self.value fields = get_model_fields(parent_model) target = self.target.strip() if target not in fields: return field = fields[target] if not hasattr(field, 'rel') or not hasattr(field.rel, 'to'): return model = field.rel.to # if value is an id try: return model.objects.get(pk=int(self.value)) except (ValueError, model.DoesNotExist): pass # try with txt_idx try: return model.objects.get(txt_idx=self.value) except (ValueError, model.DoesNotExist): pass return "" class ImporterColumn(models.Model): """ Import file column description """ importer_type = models.ForeignKey(ImporterType, related_name='columns') col_number = models.IntegerField(_(u"Column number"), default=1) description = models.TextField(_("Description"), blank=True, null=True) regexp_pre_filter = models.ForeignKey("Regexp", blank=True, null=True) required = models.BooleanField(_(u"Required"), default=False) class Meta: verbose_name = _(u"Importer - Column") verbose_name_plural = _(u"Importer - Columns") class ImporterDuplicateField(models.Model): """ Direct copy of result in other fields """ column = models.ForeignKey(ImporterColumn, related_name='duplicate_fields') field_name = models.CharField(_(u"Field name"), blank=True, null=True, max_length=200) force_new = models.BooleanField(_(u"Force creation of new item"), default=False) class Meta: verbose_name = _(u"Importer - Duplicate field") verbose_name_plural = _(u"Importer - Duplicate fields") class Regexp(models.Model): name = models.CharField(_(u"Name"), max_length=100) description = models.CharField(_(u"Description"), blank=True, null=True, max_length=500) regexp = models.CharField(_(u"Regular expression"), max_length=500) class Meta: verbose_name = _(u"Importer - Regular expression") verbose_name_plural = _(u"Importer - Regular expressions") def __unicode__(self): return self.name class ImportTarget(models.Model): """ Ishtar database target for a column """ column = models.ForeignKey(ImporterColumn, related_name='targets') target = models.CharField(u"Target", max_length=500) regexp_filter = models.ForeignKey("Regexp", blank=True, null=True) formater_type = models.ForeignKey("FormaterType") force_new = models.BooleanField(_(u"Force creation of new item"), default=False) class Meta: verbose_name = _(u"Importer - Target") verbose_name_plural = _(u"Importer - Targets") def __unicode__(self): return self.target[:50] @property def associated_model(self): return get_associated_model( self.column.importer_type.associated_models, self.target.split('__')) def get_choices(self): if self.formater_type.formater_type == 'StrToBoolean': return [('', '--' * 8), ('True', _(u"True")), ('False', _(u"False"))] if not self.associated_model or not hasattr(self.associated_model, 'get_types'): return [] return self.associated_model.get_types() class TargetKey(models.Model): """ User's link between import source and ishtar database. Also temporary used for GeneralType to point missing link before adding them in ItemKey table. A targetkey connection can be create to be applied to on particular import (associated_import), one particular user (associated_user) or to all imports (associated_import and associated_user are empty). """ target = models.ForeignKey(ImportTarget, related_name='keys') key = models.TextField(_(u"Key")) value = models.TextField(_(u"Value"), blank=True, null=True) is_set = models.BooleanField(_(u"Is set"), default=False) associated_import = models.ForeignKey('Import', blank=True, null=True) associated_user = models.ForeignKey('IshtarUser', blank=True, null=True) class Meta: unique_together = ('target', 'key', 'associated_user', 'associated_import') verbose_name = _(u"Importer - Target key") verbose_name_plural = _(u"Importer - Targets keys") def __unicode__(self): return u" - ".join([unicode(self.target), self.key[:50]]) def format(self): if not self.is_set: return None if self.target.formater_type.formater_type == 'StrToBoolean': if self.key in ('False', '0'): return False elif self.key: return True return return self.key def save(self, *args, **kwargs): obj = super(TargetKey, self).save(*args, **kwargs) if not self.value: return obj associated_model = self.target.associated_model if associated_model and hasattr(self.target.associated_model, "add_key"): v = None # pk is given try: v = self.target.associated_model.objects.get( pk=unicode(int(self.value))) except (ValueError, self.target.associated_model.DoesNotExist): # try with txt_idx try: v = self.target.associated_model.objects.get( txt_idx=unicode(self.value)) except (self.target.associated_model.DoesNotExist): pass if v: v.add_key(self.key) return obj TARGET_MODELS = [ ('OrganizationType', _(u"Organization type")), ('SourceType', _(u"Source type")), ('AuthorType', _(u"Author type")), ('Format', _(u"Format")), ('archaeological_operations.models.OperationType', _(u"Operation type")), ('archaeological_operations.models.Period', _(u"Period")), ('archaeological_context_records.models.Unit', _(u"Unit")), ('archaeological_finds.models.MaterialType', _(u"Material")), ('archaeological_finds.models.ConservatoryState', _(u"Conservatory state")), ('archaeological_finds.models.PreservationType', _(u"Preservation type")), ('archaeological_finds.models.ObjectType', _(u"Object type")), ('archaeological_context_records.models.IdentificationType', _("Identification type")) ] TARGET_MODELS_KEYS = [tm[0] for tm in TARGET_MODELS] IMPORTER_TYPES = ( ('IntegerFormater', _(u"Integer")), ('FloatFormater', _(u"Float")), ('UnicodeFormater', _(u"String")), ('DateFormater', _(u"Date")), ('TypeFormater', _(u"Type")), ('YearFormater', _(u"Year")), ('StrToBoolean', _(u"String to boolean")), ('FileFormater', pgettext_lazy("filesystem", u"File")), ('UnknowType', _(u"Unknow type")) ) IMPORTER_TYPES_DCT = { 'IntegerFormater': IntegerFormater, 'FloatFormater': FloatFormater, 'UnicodeFormater': UnicodeFormater, 'DateFormater': DateFormater, 'TypeFormater': TypeFormater, 'YearFormater': YearFormater, 'StrToBoolean': StrToBoolean, 'FileFormater': FileFormater, 'UnknowType': None, } DATE_FORMATS = ( ('%Y', _(u"4 digit year. e.g.: \"2015\"")), ('%Y/%m/%d', _(u"4 digit year/month/day. e.g.: \"2015/02/04\"")), ('%d/%m/%Y', _(u"Day/month/4 digit year. e.g.: \"04/02/2015\"")), ) IMPORTER_TYPES_CHOICES = {'TypeFormater': TARGET_MODELS, 'DateFormater': DATE_FORMATS} class FormaterType(models.Model): formater_type = models.CharField(u"Formater type", max_length=20, choices=IMPORTER_TYPES) options = models.CharField(_(u"Options"), max_length=500, blank=True, null=True) many_split = models.CharField(_(u"Split character(s)"), max_length=10, blank=True, null=True) class Meta: verbose_name = _(u"Importer - Formater type") verbose_name_plural = _(u"Importer - Formater types") unique_together = ('formater_type', 'options', 'many_split') ordering = ('formater_type', 'options') def __unicode__(self): return u" - ".join( [unicode(dict(IMPORTER_TYPES)[self.formater_type]) if self.formater_type in IMPORTER_TYPES_DCT else ''] + [getattr(self, k) for k in ('options', 'many_split') if getattr(self, k)]) def get_choices(self): if self.format_type in IMPORTER_TYPES_CHOICES: return IMPORTER_TYPES_CHOICES[self.format_type] def get_formater_type(self, target): if self.formater_type not in IMPORTER_TYPES_DCT.keys(): return kwargs = {'db_target': target} if self.many_split: kwargs['many_split'] = self.many_split if self.formater_type == 'TypeFormater': if self.options not in TARGET_MODELS_KEYS: print('%s not in TARGET_MODELS_KEYS' % self.options) return model = None if self.options in dir(): model = dir()[self.options] else: model = import_class(self.options) return TypeFormater(model, **kwargs) elif self.formater_type == 'UnicodeFormater': try: return UnicodeFormater(int(self.options.strip()), **kwargs) except ValueError: return elif self.formater_type == 'DateFormater': return DateFormater(self.options, **kwargs) elif self.formater_type == 'StrToBoolean': return StrToBoolean(**kwargs) elif self.formater_type == 'UnknowType': return else: return IMPORTER_TYPES_DCT[self.formater_type](**kwargs) IMPORT_STATE = (("C", _(u"Created")), ("AP", _(u"Analyse in progress")), ("A", _(u"Analysed")), ("P", _(u"Import pending")), ("IP", _(u"Import in progress")), ("FE", _(u"Finished with errors")), ("F", _(u"Finished")), ("AC", _(u"Archived")), ) IMPORT_STATE_DCT = dict(IMPORT_STATE) ENCODINGS = [(settings.ENCODING, settings.ENCODING), (settings.ALT_ENCODING, settings.ALT_ENCODING), ('utf-8', 'utf-8')] class Import(models.Model): user = models.ForeignKey('IshtarUser') importer_type = models.ForeignKey(ImporterType) imported_file = models.FileField(_(u"Imported file"), upload_to="upload/imports/") imported_images = models.FileField( _(u"Associated images (zip file)"), upload_to="upload/imports/", blank=True, null=True) encoding = models.CharField(_(u"Encoding"), choices=ENCODINGS, default='utf-8', max_length=15) skip_lines = models.IntegerField(_(u"Skip lines"), default=1) error_file = models.FileField(_(u"Error file"), upload_to="upload/imports/", blank=True, null=True) result_file = models.FileField(_(u"Result file"), upload_to="upload/imports/", blank=True, null=True) state = models.CharField(_(u"State"), max_length=2, choices=IMPORT_STATE, default='C') creation_date = models.DateTimeField(_(u"Creation date"), auto_now_add=True, blank=True, null=True) end_date = models.DateTimeField(_(u"End date"), blank=True, null=True, editable=False) seconds_remaining = models.IntegerField(_(u"Seconds remaining"), blank=True, null=True, editable=False) class Meta: verbose_name = _(u"Import") verbose_name_plural = _(u"Imports") def __unicode__(self): return u"%s - %s" % (unicode(self.importer_type), unicode(self.user)) def need_matching(self): return bool(TargetKey.objects.filter(associated_import=self, is_set=False).count()) def get_actions(self): """ Get available action relevant with the current status """ actions = [] if self.state == 'C': actions.append(('A', _(u"Analyse"))) if self.state == 'A': actions.append(('A', _(u"Re-analyse"))) actions.append(('I', _(u"Launch import"))) if self.state in ('F', 'FE'): actions.append(('A', _(u"Re-analyse"))) actions.append(('I', _(u"Re-import"))) actions.append(('AC', _(u"Archive"))) if self.state == 'AC': actions.append(('A', _(u"Unarchive"))) actions.append(('D', _(u"Delete"))) return actions @property def imported_filename(self): return self.imported_file.name.split(os.sep)[-1] @property def status(self): if self.state not in IMPORT_STATE_DCT: return "" return IMPORT_STATE_DCT[self.state] def get_importer_instance(self): return self.importer_type.get_importer_class()( skip_lines=self.skip_lines, import_instance=self) @property def data_table(self): with open(self.imported_file.path) as csv_file: encodings = [self.encoding] encodings += [coding for coding, c in ENCODINGS] for encoding in encodings: try: return [line for line in unicodecsv.reader(csv_file, encoding=encoding)] except UnicodeDecodeError: if encoding != encodings[-1]: csv_file.seek(0) return [] def initialize(self): self.state = 'AP' self.save() self.get_importer_instance().initialize(self.data_table, output='db') self.state = 'A' self.save() def importation(self): self.state = 'IP' self.save() importer = self.get_importer_instance() importer.importation(self.data_table) # result file filename = slugify(self.importer_type.name) now = datetime.datetime.now().isoformat('-').replace(':', '') result_file = filename + "_result_%s.csv" % now result_file = os.sep.join([self.result_file.storage.location, result_file]) with open(result_file, 'w') as fle: fle.write(importer.get_csv_result().encode('utf-8')) self.result_file = File(open(fle.name)) if importer.errors: self.state = 'FE' error_file = filename + "_errors_%s.csv" % now error_file = os.sep.join([self.error_file.storage.location, error_file]) with open(error_file, 'w') as fle: fle.write(importer.get_csv_errors().encode('utf-8')) self.error_file = File(open(fle.name)) else: self.state = 'F' self.error_file = None self.save() def archive(self): self.state = 'AC' self.save() def get_all_imported(self): imported = [] for related, zorg in \ self._meta.get_all_related_m2m_objects_with_model(): accessor = related.get_accessor_name() imported += [(accessor, obj) for obj in getattr(self, accessor).all()] return imported def pre_delete_import(sender, **kwargs): # deleted imported items when an import is delete instance = kwargs.get('instance') if not instance: return to_delete = [] for accessor, imported in instance.get_all_imported(): to_delete.append(imported) for item in to_delete: item.delete() pre_delete.connect(pre_delete_import, sender=Import) class Organization(Address, Merge, OwnPerms, ValueGetter): TABLE_COLS = ('name', 'organization_type',) name = models.CharField(_(u"Name"), max_length=300) organization_type = models.ForeignKey(OrganizationType, verbose_name=_(u"Type")) history = HistoricalRecords() class Meta: verbose_name = _(u"Organization") verbose_name_plural = _(u"Organizations") permissions = ( ("view_organization", ugettext(u"Can view all Organization")), ("view_own_organization", ugettext(u"Can view own Organization")), ("add_own_organization", ugettext(u"Can add own Organization")), ("change_own_organization", ugettext(u"Can change own Organization")), ("delete_own_organization", ugettext(u"Can delete own Organization")), ) def __unicode__(self): return self.name def generate_merge_key(self): self.merge_key = slugify(self.name if self.name else '') if not self.merge_key: self.merge_key = self.EMPTY_MERGE_KEY if self.town: self.merge_key += "-" + slugify(self.town or '') self.merge_key = self.merge_key[:300] @property def associated_filename(self): values = [unicode(getattr(self, attr)) for attr in ('organization_type', 'name') if getattr(self, attr)] return slugify(u"-".join(values)) class PersonType(GeneralType): # rights = models.ManyToManyField(WizardStep, verbose_name=_(u"Rights")) groups = models.ManyToManyField(Group, verbose_name=_(u"Groups"), blank=True, null=True) class Meta: verbose_name = _(u"Person type") verbose_name_plural = _(u"Person types") ordering = ('label',) class Person(Address, Merge, OwnPerms, ValueGetter): _prefix = 'person_' TYPE = ( ('Mr', _(u'Mr')), ('Ms', _(u'Miss')), ('Mr and Miss', _(u'Mr and Miss')), ('Md', _(u'Mrs')), ('Dr', _(u'Doctor')), ) TABLE_COLS = ('name', 'surname', 'email', 'person_types_list', 'attached_to') title = models.CharField(_(u"Title"), max_length=2, choices=TYPE, blank=True, null=True) surname = models.CharField(_(u"Surname"), max_length=50, blank=True, null=True) name = models.CharField(_(u"Name"), max_length=200, blank=True, null=True) raw_name = models.CharField(_(u"Raw name"), max_length=300, blank=True, null=True) person_types = models.ManyToManyField(PersonType, verbose_name=_(u"Types")) attached_to = models.ForeignKey( 'Organization', related_name='members', on_delete=models.SET_NULL, verbose_name=_(u"Is attached to"), blank=True, null=True) class Meta: verbose_name = _(u"Person") verbose_name_plural = _(u"Persons") permissions = ( ("view_person", ugettext(u"Can view all Person")), ("view_own_person", ugettext(u"Can view own Person")), ("add_own_person", ugettext(u"Can add own Person")), ("change_own_person", ugettext(u"Can change own Person")), ("delete_own_person", ugettext(u"Can delete own Person")), ) def __unicode__(self): values = [unicode(getattr(self, attr)) for attr in ('surname', 'name') if getattr(self, attr)] if not values and self.raw_name: values = [self.raw_name] if self.attached_to: attached_to = unicode(self.attached_to) if values: values.append(u'-') values.append(attached_to) return u" ".join(values) def get_values(self, prefix=''): values = super(Person, self).get_values(prefix=prefix) title = '' TYPES = dict(self.TYPE) if self.title in TYPES: title = dict(self.TYPE)[self.title] values[prefix + 'title'] = title if not self.attached_to: values.update( Person.get_empty_values(prefix=prefix + 'attached_to_')) return values person_types_list_lbl = _(u"Types") @property def person_types_list(self): return u", ".join([unicode(pt) for pt in self.person_types.all()]) def generate_merge_key(self): if self.name and self.name.strip(): self.merge_key = slugify(self.name.strip()) + \ ((u'-' + slugify(self.surname.strip())) if self.surname else u'') elif self.raw_name and self.raw_name.strip(): self.merge_key = slugify(self.raw_name.strip()) elif self.attached_to: self.merge_key = self.attached_to.merge_key else: self.merge_key = self.EMPTY_MERGE_KEY if self.merge_key != self.EMPTY_MERGE_KEY and self.attached_to: self.merge_key += "-" + self.attached_to.merge_key self.merge_key = self.merge_key[:300] def is_natural(self): return not self.attached_to def has_right(self, right_name): if '.' in right_name: right_name = right_name.split('.')[-1] if type(right_name) in (list, tuple): return bool(self.person_types.filter( txt_idx__in=right_name).count()) or \ bool(self.person_types.filter( groups__permissions__codename__in=right_name).count()) # or self.person_types.filter(wizard__url_name__in=right_name).count()) return bool(self.person_types.filter(txt_idx=right_name).count()) or \ bool(self.person_types.filter( groups__permissions__codename=right_name).count()) # or self.person_types.filter(wizard__url_name=right_name).count()) def full_label(self): values = [] if self.title: values = [unicode(_(self.title))] values += [unicode(getattr(self, attr)) for attr in ('surname', 'name') if getattr(self, attr)] if not values and self.raw_name: values = [self.raw_name] if self.attached_to: values.append(u"- " + unicode(self.attached_to)) return u" ".join(values) @property def associated_filename(self): values = [unicode(getattr(self, attr)) for attr in ('surname', 'name', 'attached_to') if getattr(self, attr)] return slugify(u"-".join(values)) def save(self, *args, **kwargs): super(Person, self).save(*args, **kwargs) if hasattr(self, 'responsible_town_planning_service'): for fle in self.responsible_town_planning_service.all(): fle.save() # force update of raw_town_planning_service if hasattr(self, 'general_contractor'): for fle in self.general_contractor.all(): fle.save() # force update of raw_general_contractor class IshtarUser(User): person = models.ForeignKey(Person, verbose_name=_(u"Person"), unique=True, related_name='ishtaruser') class Meta: verbose_name = _(u"Ishtar user") verbose_name_plural = _(u"Ishtar users") @classmethod def create_from_user(cls, user): default = user.username surname = user.first_name or default name = user.last_name or default email = user.email person_type = None if user.is_superuser: ADMINISTRATOR, created = PersonType.objects.get_or_create( txt_idx='administrator') person_type = ADMINISTRATOR else: person_type, created = PersonType.objects.get_or_create( txt_idx='public_access') person = Person.objects.create(title='Mr', surname=surname, name=name, email=email, history_modifier=user) person.person_types.add(person_type) return IshtarUser.objects.create(user_ptr=user, username=default, person=person) def has_right(self, right_name): return self.person.has_right(right_name) def full_label(self): return self.person.full_label() class AuthorType(GeneralType): class Meta: verbose_name = _(u"Author type") verbose_name_plural = _(u"Author types") class Author(models.Model): person = models.ForeignKey(Person, verbose_name=_(u"Person"), related_name='author') author_type = models.ForeignKey(AuthorType, verbose_name=_(u"Author type")) class Meta: verbose_name = _(u"Author") verbose_name_plural = _(u"Authors") def __unicode__(self): return unicode(self.person) + settings.JOINT + \ unicode(self.author_type) def related_sources(self): return list(self.treatmentsource_related.all()) + \ list(self.operationsource_related.all()) + \ list(self.findsource_related.all()) + \ list(self.contextrecordsource_related.all()) class SourceType(GeneralType): class Meta: verbose_name = _(u"Source type") verbose_name_plural = _(u"Source types") class SupportType(GeneralType): class Meta: verbose_name = _(u"Support type") verbose_name_plural = _(u"Support types") class Format(GeneralType): class Meta: verbose_name = _(u"Format") verbose_name_plural = _(u"Formats") class Source(models.Model): title = models.CharField(_(u"Title"), max_length=300) external_id = models.CharField(_(u"External ID"), max_length=12, null=True, blank=True) source_type = models.ForeignKey(SourceType, verbose_name=_(u"Type")) support_type = models.ForeignKey(SupportType, verbose_name=_(u"Support"), blank=True, null=True,) format_type = models.ForeignKey(Format, verbose_name=_(u"Format"), blank=True, null=True,) scale = models.CharField(_(u"Scale"), max_length=30, null=True, blank=True) authors = models.ManyToManyField(Author, verbose_name=_(u"Authors"), related_name="%(class)s_related") associated_url = models.URLField( verify_exists=False, blank=True, null=True, verbose_name=_(u"Numerical ressource (web address)")) receipt_date = models.DateField(blank=True, null=True, verbose_name=_(u"Receipt date")) creation_date = models.DateField(blank=True, null=True, verbose_name=_(u"Creation date")) item_number = models.IntegerField(_(u"Item number"), default=1) reference = models.CharField(_(u"Ref."), max_length=25, null=True, blank=True) internal_reference = models.CharField(_(u"Internal reference"), max_length=25, null=True, blank=True) description = models.TextField(_(u"Description"), blank=True, null=True) comment = models.TextField(_(u"Comment"), blank=True, null=True) additional_information = models.TextField(_(u"Additional information"), blank=True, null=True) TABLE_COLS = ['title', 'source_type', 'authors', ] class Meta: abstract = True def __unicode__(self): return self.title if settings.COUNTRY == 'fr': class Arrondissement(models.Model): name = models.CharField(u"Nom", max_length=30) department = models.ForeignKey(Department, verbose_name=u"Département") def __unicode__(self): return settings.JOINT.join((self.name, unicode(self.department))) class Canton(models.Model): name = models.CharField(u"Nom", max_length=30) arrondissement = models.ForeignKey(Arrondissement, verbose_name=u"Arrondissement") def __unicode__(self): return settings.JOINT.join( (self.name, unicode(self.arrondissement))) class Town(models.Model): name = models.CharField(_(u"Name"), max_length=100) surface = models.IntegerField(_(u"Surface (m²)"), blank=True, null=True) center = models.PointField(_(u"Localisation"), srid=settings.SRID, blank=True, null=True) if settings.COUNTRY == 'fr': numero_insee = models.CharField(u"Numéro INSEE", max_length=6, unique=True) departement = models.ForeignKey( Department, verbose_name=u"Département", null=True, blank=True) canton = models.ForeignKey(Canton, verbose_name=u"Canton", null=True, blank=True) objects = models.GeoManager() class Meta: verbose_name = _(u"Town") verbose_name_plural = _(u"Towns") if settings.COUNTRY == 'fr': ordering = ['numero_insee'] def __unicode__(self): if settings.COUNTRY == "fr": return u"%s (%s)" % (self.name, self.numero_insee[:2]) return self.name