diff options
Diffstat (limited to 'ishtar/ishtar_base')
24 files changed, 6157 insertions, 0 deletions
diff --git a/ishtar/ishtar_base/__init__.py b/ishtar/ishtar_base/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ishtar/ishtar_base/__init__.py diff --git a/ishtar/ishtar_base/admin.py b/ishtar/ishtar_base/admin.py new file mode 100644 index 000000000..3a93f1852 --- /dev/null +++ b/ishtar/ishtar_base/admin.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2010-2011 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 <http://www.gnu.org/licenses/>. + +# See the file COPYING for details. + +""" +Admin description +""" + +from django import forms +from django.contrib import admin +from django.core.exceptions import ObjectDoesNotExist +from django.utils.translation import ugettext_lazy as _ + +from ishtar import settings +import models + +class HistorizedObjectAdmin(admin.ModelAdmin): + readonly_fields = ('history_modifier',) + def save_model(self, request, obj, form, change): + obj.history_modifier = request.user + obj.save() + +class DepartementAdmin(admin.ModelAdmin): + list_display = ('number', 'label',) + model = models.Departement + +admin.site.register(models.Departement, DepartementAdmin) + +class OrganizationAdmin(HistorizedObjectAdmin): + list_display = ('name', 'organization_type') + list_filter = ("organization_type",) + search_fields = ('name',) + model = models.Organization + +admin.site.register(models.Organization, OrganizationAdmin) + +class PersonAdmin(HistorizedObjectAdmin): + list_display = ('name', 'surname', 'email', 'person_type') + list_filter = ("person_type", "in_charge_storage", "is_author",) + search_fields = ('name', 'surname', 'email',) + model = models.Person + +admin.site.register(models.Person, PersonAdmin) + +class FileAdmin(HistorizedObjectAdmin): + list_display = ['year', 'numeric_reference', 'internal_reference', + 'is_active', 'file_type', 'general_contractor',] + if settings.COUNTRY == 'fr': + list_display += ['saisine_type', 'reference_number'] + list_filter = ("file_type", "year", 'is_active',) + search_fields = ('towns__name',) + model = models.File + +admin.site.register(models.File, FileAdmin) + +class OperationAdmin(HistorizedObjectAdmin): + list_display = ['year', 'operation_code', 'start_date', 'end_date', + 'operation_type'] + list_filter = ("year", "operation_type",) + search_fields = ['towns__name', 'operation_code'] + if settings.COUNTRY == 'fr': + list_display += ['code_patriarche'] + search_fields += ['code_patriarche'] + model = models.Operation + +admin.site.register(models.Operation, OperationAdmin) + +class OperationSourceAdmin(admin.ModelAdmin): + list_display = ('operation', 'title', 'source_type',) + list_filter = ('source_type',) + search_fields = ('title', 'operation__name') + model = models.OperationSource + +admin.site.register(models.OperationSource, OperationSourceAdmin) + +class ParcelAdmin(HistorizedObjectAdmin): + list_display = ('section', 'parcel_number', 'operation', 'associated_file') + search_fields = ('operation__name',) + model = models.Parcel + +admin.site.register(models.Parcel, ParcelAdmin) + +class PeriodAdmin(admin.ModelAdmin): + list_display = ('label', 'start_date', 'end_date', 'parent') + model = models.Period + +admin.site.register(models.Period, PeriodAdmin) + +class DatingAdmin(admin.ModelAdmin): + list_display = ('period', 'start_date', 'end_date', 'dating_type', + 'quality') + list_filter = ("period", 'dating_type', 'quality') + model = models.Dating + +admin.site.register(models.Dating, DatingAdmin) + +class ContextRecordAdmin(HistorizedObjectAdmin): + list_display = ('label', 'length', 'width', + 'thickness', 'depth') + list_filter = ('has_furniture',) + search_fields = ('parcel__operation__name', "datings__period__label") + model = models.ContextRecord + +admin.site.register(models.ContextRecord, ContextRecordAdmin) + +class ContextRecordSourceAdmin(admin.ModelAdmin): + list_display = ('context_record', 'title', 'source_type',) + list_filter = ('source_type',) + search_fields = ('title', ) + model = models.ContextRecordSource + +admin.site.register(models.ContextRecordSource, ContextRecordSourceAdmin) + +class BaseItemAdmin(HistorizedObjectAdmin): + list_display = ('label', 'context_record', 'is_isolated') + search_fields = ('label', 'context_record__parcel__operation__name',) + model = models.BaseItem + +admin.site.register(models.BaseItem, BaseItemAdmin) + +class ItemAdmin(HistorizedObjectAdmin): + list_display = ('label', 'material_type', 'dating', 'volume', 'weight', + 'item_number',) + list_filter = ('material_type',) + search_fields = ('label', "dating__period__label") + model = models.Item + +admin.site.register(models.Item, ItemAdmin) + +class ItemSourceAdmin(admin.ModelAdmin): + list_display = ('item', 'title', 'source_type',) + list_filter = ('source_type',) + search_fields = ('title', ) + model = models.ItemSource + +admin.site.register(models.ItemSource, ItemSourceAdmin) + +class WarehouseAdmin(HistorizedObjectAdmin): + list_display = ('name', 'warehouse_type', 'town') + list_filter = ('warehouse_type',) + search_fields = ('name', 'town') + model = models.Warehouse + +admin.site.register(models.Warehouse, WarehouseAdmin) + +class AdministrativeActAdmin(HistorizedObjectAdmin): + list_display = ('operation', 'act_type', 'signature_date') + list_filter = ('act_type',) + search_fields = ('operation__name',) + model = models.AdministrativeAct + +admin.site.register(models.AdministrativeAct, AdministrativeActAdmin) + +class ContainerTypeAdmin(admin.ModelAdmin): + list_display = ('label', 'reference', 'length', 'width', 'height', + 'volume') + model = models.ContainerType + +admin.site.register(models.ContainerType, ContainerTypeAdmin) + +class ContainerAdmin(admin.ModelAdmin): + list_display = ('reference', 'location', 'container_type',) + list_filter = ("container_type",) + model = models.Container + +admin.site.register(models.Container, ContainerAdmin) + +class TownAdmin(admin.ModelAdmin): + list_display = ['name',] + search_fields = ['name'] + if settings.COUNTRY == 'fr': + list_display += ['numero_insee', 'departement', ] + search_fields += ['numero_insee', 'departement__label', ] + list_filter = ("departement",) + model = models.Town + +admin.site.register(models.Town, TownAdmin) + +class AuthorAdmin(admin.ModelAdmin): + list_display = ['person', 'author_type'] + list_filter = ("author_type",) + model = models.Author + +admin.site.register(models.Author, AuthorAdmin) + +class PropertyAdmin(admin.ModelAdmin): + list_display = ['item', 'person', 'start_date', 'end_date'] + search_fields = ('item__label', 'person__name') + model = models.Property + +admin.site.register(models.Property, PropertyAdmin) + +class TreatmentAdmin(HistorizedObjectAdmin): + list_display = ('location', 'treatment_type', 'container', 'person') + list_filter = ('treatment_type',) + model = models.Treatment + +admin.site.register(models.Treatment, TreatmentAdmin) + +class TreatmentSourceAdmin(admin.ModelAdmin): + list_display = ('treatment', 'title', 'source_type',) + list_filter = ('source_type',) + search_fields = ('title',) + model = models.TreatmentSource + +admin.site.register(models.TreatmentSource, TreatmentSourceAdmin) + +basic_models = [models.PersonType, models.IshtarUser, models.FileType, + models.OperationType, models.DatingType, models.DatingQuality, + models.SourceType, models.MaterialType, models.ParcelOwner, + models.WarehouseType, models.ActType, models.AuthorType, + models.OrganizationType, models.TreatmentType, + models.RemainType, models.PermitType, models.Unit, + models.ActivityType, models.IdentificationType] +if settings.COUNTRY == 'fr': + basic_models += [models.Arrondissement, models.Canton, models.SaisineType] + +for model in basic_models: + admin.site.register(model) diff --git a/ishtar/ishtar_base/backend.py b/ishtar/ishtar_base/backend.py new file mode 100644 index 000000000..f50edd708 --- /dev/null +++ b/ishtar/ishtar_base/backend.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2010-2011 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 <http://www.gnu.org/licenses/>. + +# See the file COPYING for details. + +""" +Permission backend to manage "own" objects +""" + +from django.conf import settings +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist + +import models + +class ObjectOwnPermBackend(object): + supports_object_permissions = True + supports_anonymous_user = True + + def authenticate(self, username, password): + # managed by the default backend + return None + + def has_perm(self, user_obj, perm, model=None, obj=None): + if not user_obj.is_authenticated(): + return False + if not model: + # let it manage by the default backend + return False + try: + ishtar_user = models.IshtarUser.objects.get(user_ptr=user_obj) + except ObjectDoesNotExist: + return False + try: + # only manage "own" permissions + assert perm.split('.')[-1].split('_')[1] == 'own' + except (IndexError, AssertionError): + return False + if ishtar_user.person.person_type \ + == models.PersonType.objects.get(txt_idx="administrator"): + return True + if obj is None: + model_name = perm.split('_')[-1].capitalize() + if not hasattr(models, model_name): + return False + model = getattr(models, model_name) + return user_obj.has_perm(perm) and model.has_item_of(ishtar_user) + return user_obj.has_perm(perm) and obj.is_own(user_obj) diff --git a/ishtar/ishtar_base/context_processors.py b/ishtar/ishtar_base/context_processors.py new file mode 100644 index 000000000..c4eb546f3 --- /dev/null +++ b/ishtar/ishtar_base/context_processors.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2010-2011 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 <http://www.gnu.org/licenses/>. + +# See the file COPYING for details. + +from django.utils.translation import ugettext, ugettext_lazy as _ +from django.contrib.sites.models import Site + +from ishtar import settings +from menus import Menu +import models + +def get_base_context(request): + dct = {'URL_PATH':settings.URL_PATH} + dct["APP_NAME"] = Site.objects.get_current().name + dct["COUNTRY"] = settings.COUNTRY + if 'MENU' not in request.session or \ + request.session['MENU'].user != request.user: + menu = Menu(request.user) + menu.init() + request.session['MENU'] = menu + if 'CURRENT_ACTION' in request.session: + dct['CURRENT_ACTION'] = request.session['CURRENT_ACTION'] + dct['MENU'] = request.session['MENU'] + dct['JQUERY_URL'] = settings.JQUERY_URL + dct['JQUERY_UI_URL'] = settings.JQUERY_UI_URL + dct['current_menu'] = [] + for lbl, model in ((_(u"Archaelogical file"), models.File), + (_(u"Operation"), models.Operation)): + model_name = model.__name__.lower() + current = model_name in request.session and request.session[model_name] + items = [] + for item in model.get_owns(request.user): + items.append((item.pk, unicode(item), unicode(item.pk) == current)) + if items: + dct['current_menu'].append((lbl, model_name, items)) + return dct + diff --git a/ishtar/ishtar_base/fixtures/organization_type-fr.json b/ishtar/ishtar_base/fixtures/organization_type-fr.json new file mode 100644 index 000000000..c25bd8179 --- /dev/null +++ b/ishtar/ishtar_base/fixtures/organization_type-fr.json @@ -0,0 +1,82 @@ +[ + { + "pk": 1, + "model": "furnitures.organizationtype", + "fields": { + "comment": "", + "available": true, + "txt_idx": "sra", + "label": "Service R\u00e9gional d'Arch\u00e9ologie" + } + }, + { + "pk": 2, + "model": "furnitures.organizationtype", + "fields": { + "comment": "D\u00e9cret 2004\r\n\r\n\"Op\u00e9rateurs\" les personnes qui r\u00e9alisent les op\u00e9rations arch\u00e9ologiques.", + "available": true, + "txt_idx": "operator", + "label": "Op\u00e9rateur d'arch\u00e9ologie pr\u00e9ventive" + } + }, + { + "pk": 4, + "model": "furnitures.organizationtype", + "fields": { + "comment": "Laboratoire ayant sous sa responsabilit\u00e9 du mobilier arch\u00e9ologique de mani\u00e8re temporaire. C'est un type de d\u00e9p\u00f4t. C'est un lieu de traitement.", + "available": true, + "txt_idx": "restoration_laboratory", + "label": "Laboratoire de restauration" + } + }, + { + "pk": 5, + "model": "furnitures.organizationtype", + "fields": { + "comment": "Pour des entreprises, collectivit\u00e9s territoriales ou autres organisations", + "available": true, + "txt_idx": "general_contractor", + "label": "Am\u00e9nageur" + } + }, + { + "pk": 6, + "model": "furnitures.organizationtype", + "fields": { + "comment": "Laboratoire de recherche du CNRS. Peut-\u00eatre une UMR et donc int\u00e9gr\u00e9 des chercheurs de l'universit\u00e9.", + "available": true, + "txt_idx": "cnrs_laboratory", + "label": "Laboratoire CNRS" + } + }, + { + "pk": 7, + "model": "furnitures.organizationtype", + "fields": { + "comment": "Cette organisation et ses membres travaillent b\u00e9n\u00e9volement", + "available": true, + "txt_idx": "volunteer", + "label": "B\u00e9n\u00e9vole" + } + }, + { + "pk": 8, + "model": "furnitures.organizationtype", + "fields": { + "comment": "les services qui d\u00e9livrent les autorisations requises pour les diff\u00e9rents projets (DDE, services\r\nurbanisme des collectivit\u00e9s, pr\u00e9fectures, Drire, etc.)", + "available": true, + "txt_idx": "planning_service", + "label": "Service instructeur" + } + }, + { + "pk": 9, + "model": "furnitures.organizationtype", + "fields": { + "comment": "", + "available": true, + "txt_idx": "museum", + "label": "Mus\u00e9e" + } + } +] diff --git a/ishtar/ishtar_base/fixtures/person_type-fr.json b/ishtar/ishtar_base/fixtures/person_type-fr.json new file mode 100644 index 000000000..0613b3129 --- /dev/null +++ b/ishtar/ishtar_base/fixtures/person_type-fr.json @@ -0,0 +1,72 @@ +[ + { + "pk": 1, + "model": "furnitures.persontype", + "fields": { + "comment": "", + "available": true, + "txt_idx": "administrator", + "label": "Administrateur" + } + }, + { + "pk": 2, + "model": "furnitures.persontype", + "fields": { + "comment": "Article 13 D\u00e9cret 2004\r\n\r\nLe pr\u00e9fet de r\u00e9gion \u00e9dicte les prescriptions arch\u00e9ologiques, d\u00e9livre l'autorisation de fouille et d\u00e9signe le responsable scientifique de toute op\u00e9ration d'arch\u00e9ologie pr\u00e9ventive.\r\n\r\nLe responsable scientifique est l'interlocuteur du pr\u00e9fet de r\u00e9gion et le garant de la qualit\u00e9 scientifique de l'op\u00e9ration arch\u00e9ologique. A ce titre, il prend, dans le cadre de la mise en oeuvre du projet d'intervention de l'op\u00e9rateur, les d\u00e9cisions relatives \u00e0 la conduite scientifique de l'op\u00e9ration et \u00e0 l'\u00e9laboration du rapport dont il dirige la r\u00e9daction. Il peut \u00eatre diff\u00e9rent pour la r\u00e9alisation du diagnostic et pour la r\u00e9alisation de la fouille.", + "available": true, + "txt_idx": "head_scientist", + "label": "Responsable scientifique" + } + }, + { + "pk": 3, + "model": "furnitures.persontype", + "fields": { + "comment": "Responsables de dossiers d'arch\u00e9ologie", + "available": true, + "txt_idx": "sra_agent", + "label": "Agent scientifique SRA" + } + }, + { + "pk": 4, + "model": "furnitures.persontype", + "fields": { + "comment": "Acc\u00e8s pour les secr\u00e9taires d'un SRA", + "available": true, + "txt_idx": "secretarial_dept", + "label": "Secr\u00e9tariat SRA" + } + }, + { + "pk": 5, + "model": "furnitures.persontype", + "fields": { + "comment": "Cette personne peut g\u00e9rer du mobilier qu'il n'a pas cr\u00e9\u00e9\r\n\r\n", + "available": true, + "txt_idx": "warehouse_manager", + "label": "Gestionnaire de d\u00e9p\u00f4t" + } + }, + { + "pk": 6, + "model": "furnitures.persontype", + "fields": { + "comment": "Responsable de l'am\u00e9nagement", + "available": true, + "txt_idx": "general_contractor", + "label": "Am\u00e9nageur" + } + }, + { + "pk": 7, + "model": "furnitures.persontype", + "fields": { + "comment": "Un acc\u00e8s limit\u00e9 \u00e0 la base, uniquement en lecture. Apr\u00e8s enregistrement.", + "available": true, + "txt_idx": "public_access", + "label": "Acc\u00e8s publique" + } + } +] diff --git a/ishtar/ishtar_base/fixtures/treatment_type-fr.json b/ishtar/ishtar_base/fixtures/treatment_type-fr.json new file mode 100644 index 000000000..f39f72059 --- /dev/null +++ b/ishtar/ishtar_base/fixtures/treatment_type-fr.json @@ -0,0 +1,123 @@ +[ + { + "pk": 1, + "model": "furnitures.treatmenttype", + "fields": { + "comment": "Le fait de mettre du mobilier dans un contenant. Que cela soit le conditionnement initial ou un re-conditionnement. ", + "available": true, + "txt_idx": "packaging", + "virtual": false, + "label": "Conditionnement" + } + }, + { + "pk": 2, + "model": "furnitures.treatmenttype", + "fields": { + "comment": "Un lot mobilier ou un objet isol\u00e9 subit une radiographie (rayon X) qui produit un ou des films radio.", + "available": true, + "txt_idx": "regular_x_ray", + "virtual": false, + "label": "Radiographie argentique" + } + }, + { + "pk": 3, + "model": "furnitures.treatmenttype", + "fields": { + "comment": "Un lot mobilier ou un objet isol\u00e9 subit une radiographie (rayon X) qui produit un ou des fichiers num\u00e9riques.", + "available": true, + "txt_idx": "digital_x_ray", + "virtual": false, + "label": "Radiographie num\u00e9rique" + } + }, + { + "pk": 4, + "model": "furnitures.treatmenttype", + "fields": { + "comment": "D\u00e9placement de mobilier, entre deux d\u00e9p\u00f4ts : le mobilier ne peut pas \u00eatre stocker ailleurs que dans un lieu consid\u00e9r\u00e9 comme un d\u00e9p\u00f4t.", + "available": true, + "txt_idx": "moving", + "virtual": false, + "label": "D\u00e9placement" + } + }, + { + "pk": 5, + "model": "furnitures.treatmenttype", + "fields": { + "comment": "Regroupement d'un ensemble de mobilier. Exemple : ensemble des outils provenant d'une fouille, Mobilier datant d'un site, tessonier virtuel, etc.", + "available": true, + "txt_idx": "virtual_group", + "virtual": true, + "label": "Groupement virtuel" + } + }, + { + "pk": 7, + "model": "furnitures.treatmenttype", + "fields": { + "comment": "Division d'un lot de mobilier en plusieurs lots", + "available": true, + "txt_idx": "split", + "virtual": false, + "label": "Division" + } + }, + { + "pk": 6, + "model": "furnitures.treatmenttype", + "fields": { + "comment": "R\u00e9union de plusieurs objets ou lots mobiliers en un seul. Ce type de traitement peut impliquer ou non un reconditionnement.\r\n\r\nExemple : Remontage d'une c\u00e9ramique \u00e0 partir de tessons d\u00e9j\u00e0 pr\u00e9sents dans un contenant (pas de reconditionnement), regroupement d'une partie de la faune (os) d'une op\u00e9ration et cr\u00e9ation d'une nouvelle caisse dans ce but (reconditionnement \u00e0 faire)", + "available": true, + "txt_idx": "physical_grouping", + "virtual": false, + "label": "Groupement" + } + }, + { + "pk": 8, + "model": "furnitures.treatmenttype", + "fields": { + "comment": "Prise de vue \u00e0 l'aide d'un appareil photo num\u00e9rique", + "available": true, + "txt_idx": "digital_photography", + "virtual": false, + "label": "Photographie num\u00e9rique" + } + }, + { + "pk": 9, + "model": "furnitures.treatmenttype", + "fields": { + "comment": "Photographie produisant un film (positif ou n\u00e9gatif)", + "available": true, + "txt_idx": "regular_photography", + "virtual": false, + "label": "Photographie argentique" + } + }, + { + "pk": 10, + "model": "furnitures.treatmenttype", + "fields": { + "comment": "Processus qui permet \u00e9liminer le sel qui impr\u00e8gne un objet arch\u00e9ologique ou lot d'objets.", + "available": true, + "txt_idx": "desalinisation", + "virtual": false, + "label": "D\u00e9salinisation" + } + }, + { + "pk": 11, + "model": "furnitures.treatmenttype", + "fields": { + "comment": "R\u00e9duction des oxydes d\u00e9velopp\u00e9s sur/dans un objet arch\u00e9ologique par l'usage de courant \u00e9lectrique.", + "available": true, + "txt_idx": "electrolysis", + "virtual": false, + "label": "Electrolyse" + } + } +] diff --git a/ishtar/ishtar_base/fixtures/user.json b/ishtar/ishtar_base/fixtures/user.json new file mode 100644 index 000000000..c2486d47d --- /dev/null +++ b/ishtar/ishtar_base/fixtures/user.json @@ -0,0 +1,30 @@ +[ + { + "pk": 1, + "model": "auth.user", + "fields": { + "username": "lj.yann", + "first_name": "Yann", + "last_name": "Le Jeune", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "last_login": "2011-04-06 18:19:27", + "groups": [], + "user_permissions": [], + "password": "sha1$6ecea$358d73e2f3dd3d67b64daf247b1b758094c0ed67", + "email": "lj.yann@gmail.com", + "date_joined": "2011-01-04 15:08:05" + } + }, + { + "pk": 1, + "model": "furnitures.ishtaruser", + "fields": { + "user_permissions": [], + "person": 1, + "groups": [] + } + } +] + diff --git a/ishtar/ishtar_base/forms.py b/ishtar/ishtar_base/forms.py new file mode 100644 index 000000000..29d9ab4d6 --- /dev/null +++ b/ishtar/ishtar_base/forms.py @@ -0,0 +1,736 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2010-2011 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 <http://www.gnu.org/licenses/>. + +# See the file COPYING for details. + +""" +Forms definition +""" +import datetime +import re +from itertools import groupby + +from django.core.urlresolvers import reverse +from django.core import validators +from django.core.exceptions import ObjectDoesNotExist +from django.utils import formats +from django.utils.functional import lazy +from django.utils.translation import ugettext_lazy as _ +from django.shortcuts import render_to_response +from django.template import Context, RequestContext, loader +from django.db.models import Max +from django import forms +from django.core.mail import send_mail +from django.forms.formsets import formset_factory, BaseFormSet, \ + DELETION_FIELD_NAME +from django.contrib.auth.models import User +from django.contrib.sites.models import Site + +from formwizard.forms import NamedUrlSessionFormWizard + +import models +import widgets +from ishtar import settings + +reverse_lazy = lazy(reverse, unicode) + +regexp_name = re.compile(r'^[\w\- ]+$', re.UNICODE) +name_validator = validators.RegexValidator(regexp_name, +_(u"Enter a valid name consisting of letters, spaces and hyphens."), 'invalid') + +class FloatField(forms.FloatField): + """ + Allow the use of comma for separating float fields + """ + def clean(self, value): + if value: + value = value.replace(',', '.').replace('%', '') + return super(FloatField, self).clean(value) + +class FinalForm(forms.Form): + final = True + form_label = _(u"Confirm") + +class FormSet(BaseFormSet): + def check_duplicate(self, key_names, error_msg=""): + """Check for duplicate items in the formset""" + if any(self.errors): + return + if not error_msg: + error_msg = _("There are identical items.") + items = [] + for i in range(0, self.total_form_count()): + form = self.forms[i] + if not form.is_valid(): + continue + item = [key_name in form.cleaned_data and form.cleaned_data[key_name] + for key_name in key_names] + if not [v for v in item if v]: + continue + if item in items: + raise forms.ValidationError, error_msg + items.append(item) + + def add_fields(self, form, index): + super(FormSet, self).add_fields(form, index) + form.fields[DELETION_FIELD_NAME].label = '' + form.fields[DELETION_FIELD_NAME].widget = widgets.DeleteWidget() + +class SearchWizard(NamedUrlSessionFormWizard): + model = None + + def get_wizard_name(self): + """ + As the class name can interfere when reused, use the url_name + """ + return self.url_name + + def get_template(self, request, storage): + templates = ['search.html'] + return templates + +class Wizard(NamedUrlSessionFormWizard): + model = None + + def get_wizard_name(self): + """ + As the class name can interfere when reused, use the url_name + """ + return self.url_name + + def get_template(self, request, storage): + templates = ['default_wizard.html'] + current_step = storage.get_current_step() or self.get_first_step( + request, storage) + if current_step == self.get_last_step(request, storage): + templates = ['confirm_wizard.html'] + templates + return templates + + def get_template_context(self, request, storage, form=None): + """ + Add previous and current steps to manage the wizard path + """ + context = super(Wizard, self).get_template_context(request, storage, + form) + step = self.get_first_step(request, storage) + current_step = storage.get_current_step() or self.get_first_step( + request, storage) + context.update({'current_step':self.form_list[current_step]}) + if step == current_step: + return context + previous_steps = [] + while step: + if step == current_step: + break + previous_steps.append(self.form_list[step]) + step = self.get_next_step(request, storage, step) + context.update({'previous_steps':previous_steps}) + # not last step: validation + if step != self.get_last_step(request, storage): + return context + final_form_list = [] + for form_key in self.get_form_list(request, storage).keys(): + form_obj = self.get_form(request, storage, step=form_key, + data=storage.get_step_data(form_key), + files=storage.get_step_files(form_key)) + form_obj.is_valid() + final_form_list.append(form_obj) + last_form = final_form_list[-1] + context.update({'datas':self.get_formated_datas(final_form_list)}) + if hasattr(last_form, 'confirm_msg'): + context.update({'confirm_msg':last_form.confirm_msg}) + if hasattr(last_form, 'confirm_end_msg'): + context.update({'confirm_end_msg':last_form.confirm_end_msg}) + return context + + def get_formated_datas(self, forms): + """ + Get the data to present in the last page + """ + datas = [] + for form in forms: + form_datas = [] + base_form = hasattr(form, 'forms') and form.forms[0] or form + associated_models = hasattr(base_form, 'associated_models') and \ + base_form.associated_models or {} + if not hasattr(form, 'cleaned_data') and hasattr(form, 'forms'): + cleaned_datas = [frm.cleaned_data for frm in form.forms + if frm.is_valid()] + if not cleaned_datas: + continue + elif not hasattr(form, 'cleaned_data'): + continue + else: + cleaned_datas = type(form.cleaned_data) == list and \ + form.cleaned_data \ + or [form.cleaned_data] + for cleaned_data in cleaned_datas: + if not cleaned_data: + continue + if form_datas: + form_datas.append(("", "", "spacer")) + items = hasattr(base_form, 'fields') and \ + base_form.fields.keyOrder or cleaned_data.keys() + for key in items: + lbl = None + if key.startswith('hidden_'): + continue + if hasattr(base_form, 'fields') and key in base_form.fields: + lbl = base_form.fields[key].label + if not lbl: + continue + value = cleaned_data[key] + if not value and value != False: + continue + if type(value) == bool: + if value == True: + value = _("Yes") + elif value == False: + value = _("No") + elif key in associated_models: + item = associated_models[key].objects.get(pk=value) + if hasattr(item, 'short_label'): + value = item.short_label() + else: + value = unicode(item) + form_datas.append((lbl, value, '')) + if form_datas: + datas.append((form.form_label, form_datas)) + return datas + + def get_extra_model(self, dct, request, storage, form_list): + dct['history_modifier'] = request.user + return dct + + def done(self, request, storage, form_list, return_object=False, **kwargs): + """ + Save to the model + """ + dct, m2m, whole_associated_models = {}, [], [] + for form in form_list: + if not form.is_valid(): + return self.render(request, storage, form) + base_form = hasattr(form, 'forms') and form.forms[0] or form + associated_models = hasattr(base_form, 'associated_models') and \ + base_form.associated_models or {} + if hasattr(form, 'forms'): + multi = False + if form.forms: + frm = form.forms[0] + if hasattr(frm, 'base_model') and frm.base_model: + whole_associated_models.append(frm.base_model) + else: + whole_associated_models += associated_models.keys() + fields = frm.fields.copy() + if 'DELETE' in fields: + fields.pop('DELETE') + multi = len(fields) > 1 + if multi: + assert hasattr(frm, 'base_model'), \ + u"Must define a base_model for " + unicode(frm.__class__) + for frm in form.forms: + if not frm.is_valid(): + continue + vals = {} + if "DELETE" in frm.cleaned_data: + if frm.cleaned_data["DELETE"]: + continue + frm.cleaned_data.pop('DELETE') + for key in frm.cleaned_data: + value = frm.cleaned_data[key] + if not value and value != False: + continue + if key in associated_models: + value = associated_models[key].objects.get(pk=value) + if multi: + vals[key] = value + else: + m2m.append((key, value)) + if multi and vals: + m2m.append((frm.base_model, vals)) + elif type(form.cleaned_data) == dict: + for key in form.cleaned_data: + if key.startswith('hidden_'): + continue + value = form.cleaned_data[key] + if key in associated_models: + if value: + value = associated_models[key].objects.get(pk=value) + else: + value = None + dct[key] = value + return self.save_model(dct, m2m, whole_associated_models, request, + storage, form_list, return_object) + + def get_saved_model(self): + """ + Permit a distinguo when saved model is not the base selected model + """ + return self.model + + def get_current_saved_object(self, request, storage): + """ + Permit a distinguo when saved model is not the base selected model + """ + return self.get_current_object(request, storage) + + def save_model(self, dct, m2m, whole_associated_models, request, storage, + form_list, return_object): + dct = self.get_extra_model(dct, request, storage, form_list) + obj = self.get_current_saved_object(request, storage) + + # manage dependant items + other_objs = {} + for k in dct.keys(): + if '__' not in k: + continue + vals = k.split('__') + assert len(vals) == 2, "Only one level of dependant item is managed" + dependant_item, key = vals + if dependant_item not in other_objs: + other_objs[dependant_item] = {} + other_objs[dependant_item][key] = dct.pop(k) + if obj: + for k in dct: + if k.startswith('pk'): + continue + setattr(obj, k, dct[k]) + for dependant_item in other_objs: + c_item = getattr(obj, dependant_item) + # manage ManyToMany if only one associated + if hasattr(c_item, "all"): + c_items = c_item.all() + if len(c_items) != 1: + continue + c_item = c_items[0] + if c_item: + # to check # + for k in other_objs[dependant_item]: + setattr(c_item, k, other_objs[dependant_item][k]) + c_item.save() + else: + m = getattr(self.model, dependant_item) + if hasattr(m, 'related'): + c_item = m.related.model(**other_objs[dependant_item]) + setattr(obj, dependant_item, c_item) + obj.save() + obj.save() + else: + adds = {} + for dependant_item in other_objs: + m = getattr(self.model, dependant_item) + model = m.field.rel.to + c_dct = other_objs[dependant_item].copy() + if hasattr(model, 'history'): + c_dct['history_modifier'] = request.user + c_item = model(**c_dct) + c_item.save() + if hasattr(m, 'through'): + adds[dependant_item] = c_item + elif hasattr(m, 'field'): + dct[dependant_item] = c_item + if 'pk' in dct: + dct.pop('pk') + obj = self.get_saved_model()(**dct) + obj.save() + for k in adds: + getattr(obj, k).add(adds[k]) + # necessary to manage interaction between models like + # material_index management for baseitems + obj.save() + m2m_items = {} + for model in whole_associated_models: + getattr(obj, model+'s').clear() + for key, value in m2m: + if key not in m2m_items: + if type(key) == dict: + vals = [] + for item in getattr(obj, key+'s').all(): + v = {} + for k in value.keys(): + v[k] = getattr(item, k) + vals.append(v) + m2m_items[key] = vals + else: + m2m_items[key] = getattr(obj, key+'s').all() + if value not in m2m_items[key]: + if type(value) == dict: + model = getattr(obj, key+'s').model + if issubclass(model, models.BaseHistorizedItem): + value['history_modifier'] = request.user + value = model.objects.create(**value) + value.save() + getattr(obj, key+'s').add(value) + # necessary to manage interaction between models like + # material_index management for baseitems + obj.save() + res = render_to_response('wizard_done.html', {}, + context_instance=RequestContext(request)) + return return_object and (obj, res) or res + + def get_deleted(self, keys): + """ + Get the deleted and non-deleted items in formsets + """ + not_to_delete, to_delete = set(), set() + for key in keys: + items = key.split('-') + if len(items) < 2 or items[-2] in to_delete: + continue + idx = items[-2] + try: + int(idx) + except: + continue + if items[-1] == u'DELETE': + to_delete.add(idx) + if idx in not_to_delete: + not_to_delete.remove(idx) + elif idx not in not_to_delete: + not_to_delete.add(idx) + return (to_delete, not_to_delete) + + def get_form(self, request, storage, step=None, data=None, files=None): + """ + Manage formset + """ + if data: + data = data.copy() + if not step: + step = self.determine_step(request, storage) + form = self.get_form_list(request, storage)[step] + if hasattr(form, 'management_form'): + # manage deletion + to_delete, not_to_delete = self.get_deleted(data.keys()) + # raz deleted fields + for key in data.keys(): + items = key.split('-') + if len(items) < 2 or items[-2] not in to_delete: + continue + data.pop(key) + if to_delete: + # reorganize + for idx, number in enumerate(sorted(not_to_delete)): + idx = unicode(idx) + if idx == number: + continue + for key in data.keys(): + items = key.split('-') + if len(items) > 2 and number == items[-2]: + items[-2] = unicode(idx) + k = u'-'.join(items) + data[k] = data.pop(key)[0] + # get a form key + base_key = form.form.base_fields.keys()[0] + init = self.get_form_initial(request, storage, step) + total_field = len([key for key in data.keys() + if base_key in key.split('-') + and data[key]]) + if init and not to_delete: + total_field = max((total_field, len(init))) + data[step + u'-INITIAL_FORMS'] = unicode(total_field) + data[step + u'-TOTAL_FORMS'] = unicode(total_field + 1) + data = data or None + form = super(Wizard, self).get_form(request, storage, step, data, files) + return form + + def render_next_step(self, request, storage, form, **kwargs): + """ + Manage the modify or delete button in formset: next_step = current_step + """ + if request.POST.has_key('formset_modify') \ + and request.POST['formset_modify'] \ + or [key for key in request.POST.keys() + if key.endswith('DELETE') and request.POST[key]]: + return self.render(request, storage, form, **kwargs) + return super(Wizard, self).render_next_step(request, storage, form, + **kwargs) + + def process_post_request(self, request, storage, *args, **kwargs): + """ + Convert numerical step number to step name + """ + post_data = request.POST.copy() + if request.POST.has_key('form_prev_step'): + try: + step_number = int(request.POST['form_prev_step']) + post_data['form_prev_step'] = self.get_form_list(request, + storage).keys()[step_number] + except ValueError: + pass + request.POST = post_data + return super(Wizard, self).process_post_request(request, storage, *args, + **kwargs) + @classmethod + def session_has_key(cls, request, storage, form_key, key=None, multi=None): + """ + Check if the session has value of a specific form and (if provided) + of a key + """ + test = storage.prefix in request.session \ + and 'step_data' in request.session[storage.prefix] \ + and form_key in request.session[storage.prefix]['step_data'] + if not key or not test: + return test + key = key.startswith(form_key) and key or \ + not multi and form_key + '-' + key or \ + form_key + '-0-' + key #only check if the first field is available + return key in request.session[storage.prefix]['step_data'][form_key] + + @classmethod + def session_get_value(cls, request, storage, form_key, key, multi=False): + """ + Get the value of a specific form + """ + if not cls.session_has_key(request, storage, form_key, key, multi): + return + if not multi: + key = key.startswith(form_key) and key or form_key + '-' + key + return request.session[storage.prefix]['step_data'][form_key][key] + vals = [] + for k in request.session[storage.prefix]['step_data'][form_key]: + if k.startswith(form_key) and k.endswith(key) and \ + request.session[storage.prefix]['step_data'][form_key][k]: + vals.append(request.session[storage.prefix]['step_data']\ + [form_key][k]) + return vals + + def get_current_object(self, request, storage): + """ + Get the current object for an instancied wizard + """ + current_obj = None + main_form_key = 'selec-' + self.url_name + try: + idx = int(self.session_get_value(request, storage, main_form_key, + 'pk')) + current_obj = self.model.objects.get(pk=idx) + except(TypeError, ValueError, ObjectDoesNotExist): + pass + return current_obj + + def get_form_initial(self, request, storage, step): + current_obj = self.get_current_object(request, storage) + current_step = storage.get_current_step() or self.get_first_step( + request, storage) + if step.startswith('selec-') and step in self.form_list \ + and 'pk' in self.form_list[step].associated_models: + model_name = self.form_list[step].associated_models['pk' + ].__name__.lower() + if step == current_step: + self.reset_wizard(request, storage) + val = model_name in request.session and request.session[model_name] + if val: + return {'pk':val} + elif current_obj: + return self.get_instanced_init(current_obj, request, storage, + step) + current_form = self.form_list[current_step] + if hasattr(current_form, 'currents'): + initial = {} + for key in current_form.currents: + model_name = current_form.currents[key].__name__.lower() + val = model_name in request.session and \ + request.session[model_name] + if val: + initial[key] = val + if initial: + return initial + return super(Wizard, self).get_form_initial(request, storage, step) + + def get_instanced_init(self, obj, request, storage, step): + """ + Get initial data from an init + """ + current_step = storage.get_current_step() or self.get_first_step( + request, storage) + c_form = self.form_list[current_step] + # make the current object the default item for the session + obj_name = obj.__class__.__name__.lower() + # prefer a specialized name if available + prefixes = storage.prefix.split('_') + if len(prefixes) > 1 and prefixes[-2].startswith(obj_name): + obj_name = prefixes[-2] + request.session[obj_name] = unicode(obj.pk) + initial = {} + if request.POST or (step in request.session[storage.prefix] and\ + request.session[storage.prefix]['step_data'][step]): + return {} + if hasattr(c_form, 'base_fields'): + for base_field in c_form.base_fields.keys(): + fields = base_field.split('__') + value = obj + for field in fields: + if not hasattr(value, field) or \ + getattr(value, field) == None: + value = obj + break + value = getattr(value, field) + if value == obj: + continue + if hasattr(value, 'pk'): + value = value.pk + if value in (True, False): + initial[base_field] = value + elif value != None: + initial[base_field] = unicode(value) + elif hasattr(c_form, 'management_form'): + initial = [] + key = current_step.split('-')[0] + if not hasattr(obj, key): + return initial + keys = c_form.form.base_fields.keys() + for child_obj in getattr(obj, key).order_by('pk').all(): + if not keys: + break + vals = {} + if len(keys) == 1: + # only one field: must be the id of the object + vals[keys[0]] = unicode(child_obj.pk) + else: + for field in keys: + if hasattr(child_obj, field): + value = getattr(child_obj, field) + if hasattr(value, 'pk'): + value = value.pk + if value != None: + vals[field] = unicode(value) + if vals: + initial.append(vals) + return initial + +def get_now(): + format = formats.get_format('DATE_INPUT_FORMATS')[0] + value = datetime.datetime.now().strftime(format) + return value + +class DeletionWizard(Wizard): + def get_formated_datas(self, forms): + datas = super(DeletionWizard, self).get_formated_datas(forms) + self.current_obj = None + for form in forms: + if not hasattr(form, "cleaned_data"): + continue + for key in form.cleaned_data: + if key == 'pk': + model = form.associated_models['pk'] + self.current_obj = model.objects.get(pk=form.cleaned_data['pk']) + if not self.current_obj: + return datas + res = {} + for field in self.model._meta.fields + self.model._meta.many_to_many: + if field.name not in self.fields: + continue + value = getattr(self.current_obj, field.name) + if not value: + continue + if hasattr(value, 'all'): + value = ", ".join([unicode(item) for item in value.all()]) + if not value: + continue + else: + value = unicode(value) + res[field.name] = (field.verbose_name, value, '') + if not datas and self.fields: + datas = [['', []]] + for field in self.fields: + if field in res: + datas[0][1].append(res[field]) + return datas + + def done(self, request, storage, form_list, **kwargs): + obj = self.get_current_object(request, storage) + obj.delete() + return render_to_response('wizard_delete_done.html', {}, + context_instance=RequestContext(request)) + +class ClosingWizard(Wizard): + # "close" an item + # to be define in the overloaded class + model = None + fields = [] + + def get_formated_datas(self, forms): + datas = super(ClosingWizard, self).get_formated_datas(forms) + self.current_obj = None + for form in forms: + if not hasattr(form, "cleaned_data"): + continue + for key in form.cleaned_data: + if key == 'pk': + model = form.associated_models['pk'] + self.current_obj = model.objects.get( + pk=form.cleaned_data['pk']) + if not self.current_obj: + return datas + res = {} + for field in self.model._meta.fields + self.model._meta.many_to_many: + if field.name not in self.fields: + continue + value = getattr(self.current_obj, field.name) + if not value: + continue + if hasattr(value, 'all'): + value = ", ".join([unicode(item) for item in value.all()]) + if not value: + continue + else: + value = unicode(value) + res[field.name] = (field.verbose_name, value, '') + if not datas and self.fields: + datas = [['', []]] + for field in self.fields: + if field in res: + datas[0][1].append(res[field]) + return datas + + def done(self, request, storage, form_list, **kwargs): + obj = self.get_current_object(request, storage) + for form in form_list: + if form.is_valid(): + if 'end_date' in form.cleaned_data and hasattr(obj, 'end_date'): + obj.end_date = form.cleaned_data['end_date'] + obj.save() + return render_to_response('wizard_closing_done.html', {}, + context_instance=RequestContext(request)) + +def get_form_selection(class_name, label, key, model, base_form, get_url, + not_selected_error=_(u"You should select an item.")): + """ + Generate a class selection form + class_name -- name of the class + label -- label of the form + key -- model, + base_form -- base form to select + get_url -- url to get the item + not_selected_error -- message displayed when no item is selected + """ + attrs = {'_main_key':key, + '_not_selected_error':not_selected_error, + 'form_label':label, + 'associated_models':{key:model}, + 'currents':{key:model},} + attrs[key] = forms.IntegerField(label="", required=False, + widget=widgets.JQueryJqGrid(reverse_lazy(get_url), + base_form(), model), validators=[models.valid_id(model)]) + def clean(self): + cleaned_data = self.cleaned_data + if self._main_key not in cleaned_data \ + or not cleaned_data[self._main_key]: + raise forms.ValidationError(self._not_selected_error) + return cleaned_data + return type(class_name, (forms.Form,), attrs) diff --git a/ishtar/ishtar_base/forms_common.py b/ishtar/ishtar_base/forms_common.py new file mode 100644 index 000000000..aff263a3b --- /dev/null +++ b/ishtar/ishtar_base/forms_common.py @@ -0,0 +1,467 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2010-2011 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 <http://www.gnu.org/licenses/>. + +# See the file COPYING for details. + +""" +Administrative forms definitions: manage accounts and persons +""" +import datetime + +from django import forms +from django.template import Context, RequestContext, loader +from django.shortcuts import render_to_response +from django.core import validators +from django.core.mail import send_mail +from django.core.exceptions import ObjectDoesNotExist +from django.utils.safestring import mark_safe +from django.forms.formsets import formset_factory, DELETION_FIELD_NAME +from django.utils.translation import ugettext_lazy as _ +from django.contrib.auth.models import User +from django.contrib.sites.models import Site + +from ishtar import settings + +import models +import widgets +from forms import Wizard, FinalForm, FormSet, reverse_lazy, name_validator + +def get_town_field(required=True): + help_text = _(u"<p>Type name, department code and/or postal code of the " + u"town you would like to select. The search is insensitive to case.</p>\n" + u"<p>Only the first twenty results are displayed but specifying the " + u"department code is generally sufficient to get the appropriate result.</p>" + u"\n<p class='example'>For instance type \"saint denis 93\" for getting " + u"the french town Saint-Denis in the Seine-Saint-Denis department.</p>") + # !FIXME hard_link, reverse_lazy doen't seem to work with formsets + return forms.IntegerField( + widget=widgets.JQueryAutoComplete("/" + settings.URL_PATH + \ + 'autocomplete-town', associated_model=models.Town), + validators=[models.valid_id(models.Town)], label=_(u"Town"), + help_text=mark_safe(help_text), required=required) + +class WarehouseForm(forms.Form): + name = forms.CharField(label=_(u"Name"), max_length=40, + validators=[name_validator]) + warehouse_type = forms.ChoiceField(label=_(u"Warehouse type"), + choices=[]) + person_in_charge = forms.IntegerField(label=_(u"Person in charge"), + widget=widgets.JQueryAutoComplete( + reverse_lazy('autocomplete-person'), associated_model=models.Person), + validators=[models.valid_id(models.Person)], + required=False) + comment = forms.CharField(label=_(u"Comment"), widget=forms.Textarea, + required=False) + address = forms.CharField(label=_(u"Address"), widget=forms.Textarea, + required=False) + address_complement = forms.CharField(label=_(u"Address complement"), + widget=forms.Textarea, required=False) + postal_code = forms.CharField(label=_(u"Postal code"), max_length=10, + required=False) + town = forms.CharField(label=_(u"Town"), max_length=30, required=False) + country = forms.CharField(label=_(u"Country"), max_length=30, + required=False) + phone = forms.CharField(label=_(u"Phone"), max_length=18, required=False) + mobile_phone = forms.CharField(label=_(u"Town"), max_length=18, + required=False) + + def __init__(self, *args, **kwargs): + super(WarehouseForm, self).__init__(*args, **kwargs) + self.fields['warehouse_type'].choices = \ + models.WarehouseType.get_types() + self.fields['warehouse_type'].help_text = \ + models.WarehouseType.get_help() + + def save(self, user): + dct = self.cleaned_data + dct['history_modifier'] = user + dct['warehouse_type'] = models.WarehouseType.objects.get( + pk=dct['warehouse_type']) + if 'person_in_charge' in dct and dct['person_in_charge']: + dct['person_in_charge'] = models.Person.objects.get( + pk=dct['person_in_charge']) + new_item = models.Warehouse(**dct) + new_item.save() + return new_item + +class OrganizationForm(forms.Form): + name = forms.CharField(label=_(u"Name"), max_length=40, + validators=[name_validator]) + organization_type = forms.ChoiceField(label=_(u"Organization type"), + choices=[]) + address = forms.CharField(label=_(u"Address"), widget=forms.Textarea, + required=False) + address_complement = forms.CharField(label=_(u"Address complement"), + widget=forms.Textarea, required=False) + postal_code = forms.CharField(label=_(u"Postal code"), max_length=10, + required=False) + town = forms.CharField(label=_(u"Town"), max_length=30, required=False) + country = forms.CharField(label=_(u"Country"), max_length=30, + required=False) + phone = forms.CharField(label=_(u"Phone"), max_length=18, required=False) + mobile_phone = forms.CharField(label=_(u"Town"), max_length=18, + required=False) + + def __init__(self, *args, **kwargs): + super(OrganizationForm, self).__init__(*args, **kwargs) + self.fields['organization_type'].choices = \ + models.OrganizationType.get_types() + self.fields['organization_type'].help_text = \ + models.OrganizationType.get_help() + + def save(self, user): + dct = self.cleaned_data + dct['history_modifier'] = user + dct['organization_type'] = models.OrganizationType.objects.get( + pk=dct['organization_type']) + new_item = models.Organization(**dct) + new_item.save() + return new_item + +class PersonWizard(Wizard): + model = models.Person + +class PersonFormSelection(forms.Form): + form_label = _(u"Person search") + associated_models = {'pk':models.Person} + currents = {'pk':models.Person} + pk = forms.IntegerField(label=_("Person"), + widget=widgets.JQueryAutoComplete(reverse_lazy('autocomplete-person'), + associated_model=models.Person), + validators=[models.valid_id(models.Person)]) + +class PersonForm(forms.Form): + form_label = _("Identity") + associated_models = {'attached_to':models.Organization, + 'person_type':models.PersonType} + title = forms.ChoiceField(label=_("Title"), choices=models.Person.TYPE) + surname = forms.CharField(label=_(u"Surname"), max_length=20, + validators=[name_validator]) + name = forms.CharField(label=_(u"Name"), max_length=30, + validators=[name_validator]) + email = forms.CharField(label=_(u"Email"), max_length=40, required=False, + validators=[validators.validate_email]) + person_type = forms.ChoiceField(label=_("Person type"), + choices=[]) + attached_to = forms.IntegerField(label=_("Current organization"), + widget=widgets.JQueryAutoComplete(reverse_lazy('autocomplete-organization'), + associated_model=models.Organization, new=True), + validators=[models.valid_id(models.Organization)], required=False) + is_author = forms.BooleanField(label=_(u"Is an author?"), + required=False) + in_charge_storage = forms.BooleanField(required=False, + label=_(u"In charge of a storage?")) + + def __init__(self, *args, **kwargs): + super(PersonForm, self).__init__(*args, **kwargs) + self.fields['person_type'].choices = models.PersonType.get_types() + self.fields['person_type'].help_text = models.PersonType.get_help() + + def save(self, user): + dct = self.cleaned_data + dct['history_modifier'] = user + dct['person_type'] = models.PersonType.objects.get( + pk=dct['person_type']) + if 'attached_to' in dct and dct['attached_to']: + dct['attached_to'] = models.Organization.objects.get( + pk=dct['attached_to']) + new_item = models.Person(**dct) + new_item.save() + return new_item + +person_creation_wizard = PersonWizard([ + ('identity-person_creation', PersonForm), + ('final-person_creation', FinalForm)], + url_name='person_creation',) + + +person_modification_wizard = PersonWizard([ + ('selec-person_modification', PersonFormSelection), + ('identity-person_modification', PersonForm), + ('final-person_modification', FinalForm)], + url_name='person_modification',) + +class AccountWizard(Wizard): + model = models.Person + def get_formated_datas(self, forms): + datas = super(AccountWizard, self).get_formated_datas(forms) + for form in forms: + if not hasattr(form, "cleaned_data"): + continue + for key in form.cleaned_data: + if key == 'hidden_password' and form.cleaned_data[key]: + datas[-1][1].append((_("New password"), "*"*8)) + return datas + + def done(self, request, storage, form_list, **kwargs): + ''' + Save the account + ''' + dct = {} + for form in form_list: + if not form.is_valid(): + return self.render(request, storage, form) + associated_models = hasattr(form, 'associated_models') and \ + form.associated_models or {} + if type(form.cleaned_data) == dict: + for key in form.cleaned_data: + if key == 'pk': + continue + value = form.cleaned_data[key] + if key in associated_models and value: + value = associated_models[key].objects.get(pk=value) + dct[key] = value + person = self.get_current_object(request, storage) + if not person: + return self.render(request, storage, form) + for key in dct.keys(): + if key.startswith('hidden_password'): + dct['password'] = dct.pop(key) + try: + account = models.IshtarUser.objects.get(person=person) + account.username = dct['username'] + account.email = dct['email'] + except ObjectDoesNotExist: + now = datetime.datetime.now() + account = models.IshtarUser(person=person, username=dct['username'], + email=dct['email'], first_name=person.surname, + last_name=person.name, is_staff=False, is_active=True, + is_superuser=False, last_login=now, date_joined=now) + if dct['password']: + account.set_password(dct['password']) + account.save() + + if 'send_password' in dct and dct['send_password'] and \ + settings.ADMINS: + site = Site.objects.get_current() + + app_name = site and ("Ishtar - " + site.name) \ + or "Ishtar" + context = Context({'login':dct['username'], + 'password':dct['password'], + 'app_name':app_name, + 'site': site and site.domain or "" + }) + t = loader.get_template('account_activation_email.txt') + msg = t.render(context) + subject = _(u"[%(app_name)s] Account creation/modification") % { + "app_name":app_name} + send_mail(subject, msg, settings.ADMINS[0][1], + [dct['email']], fail_silently=True) + res = render_to_response('wizard_done.html', {}, + context_instance=RequestContext(request)) + return res + + def get_form(self, request, storage, step=None, data=None, files=None): + """ + Display the "Send email" field if necessary + """ + form = super(AccountWizard, self).get_form(request, storage, step, data, + files) + if not hasattr(form, 'is_hidden'): + return form + if self.session_get_value(request, storage, + 'account-account_management', 'hidden_password'): + form.is_hidden = False + return form + + +class AccountForm(forms.Form): + form_label = _("Account") + associated_models = {'pk':models.Person} + currents = {'pk':models.Person} + pk = forms.IntegerField(label=u"", widget=forms.HiddenInput, required=False) + username = forms.CharField(label=_(u"Account"), max_length=30) + email = forms.CharField(label=_(u"Email"), max_length=75, + validators=[validators.validate_email]) + hidden_password = forms.CharField(label=_(u"New password"), max_length=128, + widget=forms.PasswordInput, required=False, + validators=[validators.MinLengthValidator(4)]) + hidden_password_confirm = forms.CharField( + label=_(u"New password (confirmation)"), max_length=128, + widget=forms.PasswordInput, required=False) + + def __init__(self, *args, **kwargs): + if 'initial' in kwargs and 'pk' in kwargs['initial']: + try: + person = models.Person.objects.get(pk=kwargs['initial']['pk']) + account = models.IshtarUser.objects.get(person=person) + kwargs['initial'].update({'username':account.username, + 'email':account.email}) + except ObjectDoesNotExist: + pass + return super(AccountForm, self).__init__(*args, **kwargs) + + def clean(self): + cleaned_data = self.cleaned_data + password = cleaned_data.get("hidden_password") + if password and password != cleaned_data.get("hidden_password_confirm"): + raise forms.ValidationError(_(u"Your password and confirmation " + u"password do not match.")) + if not cleaned_data.get("pk"): + models.is_unique(User, 'username')(cleaned_data.get("username")) + if not password: + raise forms.ValidationError(_(u"You must provide a correct \ +password.")) + return cleaned_data + +class FinalAccountForm(forms.Form): + final = True + form_label = _("Confirm") + send_password = forms.BooleanField(label=_(u"Send the new password by " + u"email?"), required=False) + + def __init__(self, *args, **kwargs): + self.is_hidden = True + return super(FinalAccountForm, self).__init__(*args, **kwargs) + +account_management_wizard = AccountWizard([ + ('selec-account_management', PersonFormSelection), + ('account-account_management', AccountForm), + ('final-account_management', FinalAccountForm)], + url_name='account_management',) + +class TownForm(forms.Form): + form_label = _("Towns") + associated_models = {'town':models.Town} + town = get_town_field(required=False) + +class TownFormSet(FormSet): + def clean(self): + """Checks that no towns are duplicated.""" + return self.check_duplicate(('town',), + _("There are identical towns.")) + +TownFormSet = formset_factory(TownForm, can_delete=True, formset=TownFormSet) +TownFormSet.form_label = _("Towns") + +class ParcelForm(forms.Form): + form_label = _("Parcels") + base_model = 'parcel' + associated_models = {'parcel':models.Parcel, 'town':models.Town} + town = forms.ChoiceField(label=_("Town"), choices=(), required=False, + validators=[models.valid_id(models.Town)]) + section = forms.CharField(label=_(u"Section"), required=False, + validators=[validators.MaxLengthValidator(4)]) + parcel_number = forms.CharField(label=_(u"Parcel number"), required=False, + validators=[validators.MaxLengthValidator(6)]) + year = forms.IntegerField(label=_("Year"), required=False, + initial=lambda:datetime.datetime.now().year, + validators=[validators.MinValueValidator(1900), + validators.MaxValueValidator(2100)]) + def __init__(self, *args, **kwargs): + towns = None + if 'data' in kwargs and 'TOWNS' in kwargs['data']: + towns = kwargs['data']['TOWNS'] + # clean data if not "real" data + prefix_value = kwargs['prefix'] + '-town' + if not [k for k in kwargs['data'].keys() + if k.startswith(prefix_value) and kwargs['data'][k]]: + kwargs['data'] = None + if 'files' in kwargs: + kwargs.pop('files') + super(ParcelForm, self).__init__(*args, **kwargs) + if towns: + self.fields['town'].choices = [('', '--')] + towns + + def clean(self): + """Check required fields""" + if any(self.errors): + return + if not self.cleaned_data or DELETION_FIELD_NAME in self.cleaned_data \ + and self.cleaned_data[DELETION_FIELD_NAME]: + return + for key in ('town', 'parcel_number', 'year'): + if not key in self.cleaned_data or not self.cleaned_data[key]: + raise forms.ValidationError(_(u"All fields are required")) + return self.cleaned_data + + +class ParcelFormSet(FormSet): + def clean(self): + """Checks that no parcels are duplicated.""" + return self.check_duplicate(('town', 'parcel_number', 'year'), + _(u"There are identical parcels.")) + +ParcelFormSet = formset_factory(ParcelForm, can_delete=True, + formset=ParcelFormSet) +ParcelFormSet.form_label = _(u"Parcels") + +# sources management + +class SourceWizard(Wizard): + model = None + def get_extra_model(self, dct, request, storage, form_list): + dct = super(SourceWizard, self).get_extra_model(dct, request, storage, + form_list) + if 'history_modifier' in dct: + dct.pop('history_modifier') + return dct + +class SourceForm(forms.Form): + form_label = _(u"Documentation informations") + associated_models = {'source_type':models.SourceType} + title = forms.CharField(label=_(u"Title"), + validators=[validators.MaxLengthValidator(200)]) + source_type = forms.ChoiceField(label=_(u"Source type"), choices=[]) + + def __init__(self, *args, **kwargs): + super(SourceForm, self).__init__(*args, **kwargs) + self.fields['source_type'].choices = models.SourceType.get_types() + +class AuthorForm(forms.Form): + form_label = _(u"Author") + associated_models = {'person':models.Person, + 'author_type':models.AuthorType} + person = forms.IntegerField( + widget=widgets.JQueryAutoComplete("/" + settings.URL_PATH + \ + 'autocomplete-person', associated_model=models.Person, new=True), + validators=[models.valid_id(models.Person)], label=_(u"Person")) + author_type = forms.ChoiceField(label=_(u"Author type"), choices=[]) + + def __init__(self, *args, **kwargs): + super(AuthorForm, self).__init__(*args, **kwargs) + self.fields['author_type'].choices = models.AuthorType.get_types() + + def save(self, user): + dct = self.cleaned_data + dct['author_type'] = models.AuthorType.objects.get( + pk=dct['author_type']) + dct['person'] = models.Person.objects.get(pk=dct['person']) + new_item = models.Author(**dct) + new_item.save() + return new_item + + +class AuthorFormSelection(forms.Form): + form_label = _(u"Author selection") + associated_models = {'author':models.Author} + author = forms.IntegerField( + widget=widgets.JQueryAutoComplete("/" + settings.URL_PATH + \ + 'autocomplete-author', associated_model=models.Author, new=True), + validators=[models.valid_id(models.Author)], label=_(u"Author")) + +class AuthorFormSet(FormSet): + def clean(self): + """Checks that no author are duplicated.""" + return self.check_duplicate(('author',), + _("There are identical authors.")) + +AuthorFormset = formset_factory(AuthorFormSelection, can_delete=True, + formset=AuthorFormSet) +AuthorFormset.form_label = _("Authors") + diff --git a/ishtar/ishtar_base/forms_context_records.py b/ishtar/ishtar_base/forms_context_records.py new file mode 100644 index 000000000..a46d67995 --- /dev/null +++ b/ishtar/ishtar_base/forms_context_records.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2010-2011 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 <http://www.gnu.org/licenses/>. + +# See the file COPYING for details. + +""" +Context records forms definitions +""" +import datetime +from itertools import groupby + +from django import forms +from django.core import validators +from django.core.exceptions import ObjectDoesNotExist +from django.db.models import Max +from django.utils.translation import ugettext_lazy as _ + +from ishtar import settings + +import models +import widgets +from forms import Wizard, FinalForm, FormSet, SearchWizard, DeletionWizard, \ + formset_factory, get_now, reverse_lazy +from forms_common import get_town_field +from forms_operations import OperationFormSelection + +class RecordWizard(Wizard): + model = models.ContextRecord + edit = False + + def get_current_operation(self, request, storage): + step = storage.get_current_step() + if not step: + return + if step.endswith('_creation'): # an operation has been selected + main_form_key = 'selec-' + self.url_name + try: + idx = int(self.session_get_value(request, storage, + main_form_key, 'pk')) + current_ope = models.Operation.objects.get(pk=idx) + return current_ope + except(TypeError, ValueError, ObjectDoesNotExist): + pass + current_cr = self.get_current_object(request, storage) + if current_cr: + return current_cr.parcel.operation + + def get_template_context(self, request, storage, form=None): + """ + Get the operation "reminder" on top of wizard forms + """ + context = super(RecordWizard, self).get_template_context(request, + storage, form) + operation = self.get_current_operation(request, storage) + if not operation: + return context + items = [] + if hasattr(operation, 'code_patriarche') and operation.code_patriarche: + items.append(unicode(operation.code_patriarche)) + items.append("-".join((unicode(operation.year), + unicode(operation.operation_code)))) + context['reminder'] = _("Current operation: ") + " - ".join(items) + return context + + def get_form(self, request, storage, step=None, data=None, files=None): + """ + Get associated operation + """ + if data: + data = data.copy() + else: + data = {} + if not step: + step = self.determine_step(request, storage) + form = self.get_form_list(request, storage)[step] + + general_form_key = 'general-' + self.url_name + if step.startswith('general-'): + if step.endswith('_creation'): # an operation has been selected + main_form_key = 'selec-' + self.url_name + try: + idx = int(self.session_get_value(request, storage, + main_form_key, 'pk')) + current_obj = models.Operation.objects.get(pk=idx) + data['operation'] = current_obj + except(TypeError, ValueError, ObjectDoesNotExist): + pass + else: + current_object = self.get_current_object(request, storage) + data['context_record'] = current_object + form = super(RecordWizard, self).get_form(request, storage, step, data, + files) + return form + +class RecordModifWizard(RecordWizard): + model = models.ContextRecord + +class RecordSelect(forms.Form): + parcel__town = get_town_field() + operation__year = forms.IntegerField(label=_("Year")) + datings__period = forms.ChoiceField(label=_("Period"), + choices=models.Period.get_types()) + unit = forms.ChoiceField(label=_("Unit type"), + choices=models.Unit.get_types()) + +class RecordFormSelection(forms.Form): + form_label = _("Context record search") + associated_models = {'pk':models.ContextRecord} + currents = {'pk':models.ContextRecord} + pk = forms.IntegerField(label="", required=False, + widget=widgets.JQueryJqGrid(reverse_lazy('get-contextrecord'), + RecordSelect(), models.ContextRecord), + validators=[models.valid_id(models.ContextRecord)]) + + def clean(self): + cleaned_data = self.cleaned_data + if 'pk' not in cleaned_data or not cleaned_data['pk']: + raise forms.ValidationError(_(u"You should select a context " + u"record.")) + return cleaned_data + + +class RecordFormGeneral(forms.Form): + form_label = _("General") + associated_models = {'parcel':models.Parcel, 'unit':models.Unit} + pk = forms.IntegerField(required=False, widget=forms.HiddenInput) + operation_id = forms.IntegerField(widget=forms.HiddenInput) + parcel = forms.ChoiceField(label=_("Parcel"), choices=[]) + label = forms.CharField(label=_(u"ID"), + validators=[validators.MaxLengthValidator(200)]) + description = forms.CharField(label=_(u"Description"), + widget=forms.Textarea, required=False) + length = forms.IntegerField(label=_(u"Length (cm)"), required=False) + width = forms.IntegerField(label=_(u"Width (cm)"), required=False) + thickness = forms.IntegerField(label=_(u"Thickness (cm)"), required=False) + depth = forms.IntegerField(label=_(u"Depth (cm)"), required=False) + unit = forms.ChoiceField(label=_("Unit"), required=False, + choices=models.Unit.get_types()) + location = forms.CharField(label=_(u"Location"), widget=forms.Textarea, + required=False, validators=[validators.MaxLengthValidator(200)]) + + def __init__(self, *args, **kwargs): + operation = None + if 'data' in kwargs and kwargs['data'] and \ + ('operation' in kwargs['data'] or 'context_record' in kwargs['data']): + if 'operation' in kwargs['data']: + operation = kwargs['data']['operation'] + if 'context_record' in kwargs['data'] and \ + kwargs['data']['context_record']: + operation = kwargs['data']['context_record'].parcel.operation + # clean data if not "real" data + prefix_value = kwargs['prefix'] + if not [k for k in kwargs['data'].keys() + if k.startswith(kwargs['prefix']) and kwargs['data'][k]]: + kwargs['data'] = None + if 'files' in kwargs: + kwargs.pop('files') + super(RecordFormGeneral, self).__init__(*args, **kwargs) + self.fields['parcel'].choices = [('', '--')] + if operation: + self.fields['operation_id'].initial = operation.pk + parcels = operation.parcels.all() + sort = lambda x: (x.town.name, x.section) + parcels = sorted(parcels, key=sort) + for key, gparcels in groupby(parcels, sort): + self.fields['parcel'].choices.append( + (" - ".join(key), [(parcel.pk, parcel.short_label()) for parcel in gparcels]) + ) + + def clean(self): + # manage unique context record ID + cleaned_data = self.cleaned_data + operation_id = cleaned_data.get("operation_id") + label = cleaned_data.get("label") + cr = models.ContextRecord.objects.filter(label=label, + parcel__operation__pk=operation_id) + if 'pk' in cleaned_data and cleaned_data['pk']: + cr = cr.exclude(pk=cleaned_data['pk']) + if cr.count(): + raise forms.ValidationError(_(u"This ID already exist for " + u"this operation.")) + return cleaned_data + +class DatingForm(forms.Form): + form_label = _("Dating") + base_model = 'dating' + associated_models = {'dating_type':models.DatingType, + 'quality':models.DatingQuality, + 'period':models.Period} + period = forms.ChoiceField(label=_("Period"), + choices=models.Period.get_types()) + start_date = forms.IntegerField(label=_(u"Start date"), required=False) + end_date = forms.IntegerField(label=_(u"End date"), required=False) + quality = forms.ChoiceField(label=_("Quality"), required=False, + choices=models.DatingQuality.get_types()) + dating_type = forms.ChoiceField(label=_("Dating type"), required=False, + choices=[]) + + def __init__(self, *args, **kwargs): + super(DatingForm, self).__init__(*args, **kwargs) + self.fields['dating_type'].choices = models.DatingType.get_types() + self.fields['dating_type'].help_text = models.DatingType.get_help() + + +DatingFormSet = formset_factory(DatingForm, can_delete=True, + formset=FormSet) +DatingFormSet.form_label = _("Dating") + +class RecordFormInterpretation(forms.Form): + form_label = _("Interpretation") + associated_models = {'activity':models.ActivityType, + 'identification':models.IdentificationType,} + has_furniture = forms.NullBooleanField(label=_(u"Has furniture?"), + required=False) + filling = forms.CharField(label=_(u"Filling"), + widget=forms.Textarea, required=False) + interpretation = forms.CharField(label=_(u"Interpretation"), + widget=forms.Textarea, required=False) + activity = forms.ChoiceField(label=_(u"Activity"), required=False, + choices=[]) + identification = forms.ChoiceField(label=_("Identification"), + required=False, choices=[]) + taq = forms.IntegerField(label=_(u"TAQ"), required=False) + taq_estimated = forms.IntegerField(label=_(u"Estimated TAQ"), + required=False) + tpq = forms.IntegerField(label=_(u"TPQ"), required=False) + tpq_estimated = forms.IntegerField(label=_(u"Estimated TPQ"), + required=False) + + def __init__(self, *args, **kwargs): + super(RecordFormInterpretation, self).__init__(*args, **kwargs) + self.fields['activity'].choices = models.ActivityType.get_types() + self.fields['activity'].help_text = models.ActivityType.get_help() + self.fields['identification'].choices = \ + models.IdentificationType.get_types() + self.fields['identification'].help_text = \ + models.IdentificationType.get_help() + +record_search_wizard = SearchWizard([ + ('general-record_search', RecordFormSelection)], + url_name='record_search',) + +record_creation_wizard = RecordWizard([ + ('selec-record_creation', OperationFormSelection), + ('general-record_creation', RecordFormGeneral), + ('datings-record_creation', DatingFormSet), + ('interpretation-record_creation', RecordFormInterpretation), + ('final-record_creation', FinalForm)], + url_name='record_creation',) + +record_modification_wizard = RecordModifWizard([ + ('selec-record_modification', RecordFormSelection), + ('general-record_modification', RecordFormGeneral), + ('datings-record_modification', DatingFormSet), + ('interpretation-record_modification', RecordFormInterpretation), + ('final-record_modification', FinalForm)], + url_name='record_modification',) + +class RecordDeletionWizard(DeletionWizard): + model = models.ContextRecord + fields = ['label', 'parcel', 'description', 'length', 'width', 'thickness', + 'depth', 'location', 'datings', 'units', 'has_furniture', + 'filling', 'interpretation', 'taq', 'taq_estimated', 'tpq', + 'tpq_estimated'] + +class RecordDeletionForm(FinalForm): + confirm_msg = " " + confirm_end_msg = _(u"Would you like to delete this context record?") + +record_deletion_wizard = RecordDeletionWizard([ + ('selec-record_deletion', RecordFormSelection), + ('final-record_deletion', RecordDeletionForm)], + url_name='record_deletion',) + + diff --git a/ishtar/ishtar_base/forms_files.py b/ishtar/ishtar_base/forms_files.py new file mode 100644 index 000000000..9b45f0afc --- /dev/null +++ b/ishtar/ishtar_base/forms_files.py @@ -0,0 +1,375 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2010-2011 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 <http://www.gnu.org/licenses/>. + +# See the file COPYING for details. + +""" +Files forms definitions +""" +import datetime + +from django import forms +from django.shortcuts import render_to_response +from django.template import RequestContext +from django.core import validators +from django.core.exceptions import ObjectDoesNotExist +from django.db.models import Max +from django.utils.translation import ugettext_lazy as _ + +from ishtar import settings + +import models +import widgets +from forms import Wizard, FinalForm, FormSet, ClosingWizard, SearchWizard, \ + formset_factory, get_now, reverse_lazy +from forms_common import TownFormSet, ParcelFormSet, get_town_field +from forms_operations import OperationAdministrativeActWizard, \ +AdministrativeActOpeForm, AdministrativeActOpeFormSelection, \ +AdministrativeActDeletionWizard, FinalAdministrativeActDeleteForm, is_preventive + +class FileWizard(Wizard): + model = models.File + object_parcel_type = 'associated_file' + + def get_form(self, request, storage, step=None, data=None, files=None): + """ + Manage towns + """ + if data: + data = data.copy() + else: + data = {} + # manage the dynamic choice of towns + if not step: + step = self.determine_step(request, storage) + form = self.get_form_list(request, storage)[step] + town_form_key = 'towns-' + self.url_name + if step.startswith('parcels-') and hasattr(form, 'management_form') \ + and self.session_has_key(request, storage, town_form_key): + towns = [] + qdict = request.session[storage.prefix]['step_data'][town_form_key] + for k in qdict.keys(): + if k.endswith("town") and qdict[k]: + try: + town = models.Town.objects.get(pk=int(qdict[k])) + towns.append((town.pk, unicode(town))) + except (ObjectDoesNotExist, ValueError): + pass + data['TOWNS'] = sorted(towns, key=lambda x:x[1]) + form = super(FileWizard, self).get_form(request, storage, step, data, + files) + return form + + def get_extra_model(self, dct, request, storage, form_list): + dct = super(FileWizard, self).get_extra_model(dct, request, storage, + form_list) + current_ref = models.File.objects.filter(year=dct['year'] + ).aggregate(Max('numeric_reference'))["numeric_reference__max"] + dct['numeric_reference'] = current_ref and current_ref + 1 or 1 + return dct + + def done(self, request, storage, form_list, **kwargs): + ''' + Save parcels + ''' + r = super(FileWizard, self).done(request, storage, form_list, + return_object=True, **kwargs) + if type(r) not in (list, tuple) or len(r) != 2: + return r + obj, res = r + obj.parcels.clear() + for form in form_list: + if not hasattr(form, 'prefix') \ + or not form.prefix.startswith('parcels-') \ + or not hasattr(form, 'forms'): + continue + for frm in form.forms: + if not frm.is_valid(): + continue + dct = frm.cleaned_data.copy() + if 'parcel' in dct: + try: + parcel = models.Parcel.objects.get(pk=dct['parcel']) + setattr(parcel, self.object_parcel_type, obj) + parcel.save() + except (ValueError, ObjectDoesNotExist): + continue + continue + try: + dct['town'] = models.Town.objects.get(pk=int(dct['town'])) + except (ValueError, ObjectDoesNotExist): + continue + dct['associated_file'], dct['operation'] = None, None + dct[self.object_parcel_type] = obj + if 'DELETE' in dct: + dct.pop('DELETE') + parcel = models.Parcel.objects.filter(**dct).count() + if not parcel: + dct['history_modifier'] = request.user + parcel = models.Parcel(**dct) + parcel.save() + return res + +class FileSelect(forms.Form): + towns = get_town_field() + file_type = forms.ChoiceField(label=_("File type"), + choices=models.FileType.get_types()) + saisine_type = forms.ChoiceField(label=_("Saisine type"), choices=[]) + year = forms.IntegerField(label=_("Year")) + + def __init__(self, *args, **kwargs): + super(FileSelect, self).__init__(*args, **kwargs) + self.fields['saisine_type'].choices = models.SaisineType.get_types() + self.fields['saisine_type'].help_text = models.SaisineType.get_help() + +class FileFormSelection(forms.Form): + form_label = _("Archaeological file search") + associated_models = {'pk':models.File} + currents = {'pk':models.File} + pk = forms.IntegerField(label="", required=False, + widget=widgets.JQueryJqGrid(reverse_lazy('get-file'), + FileSelect(), models.File), + validators=[models.valid_id(models.File)]) + + def clean(self): + cleaned_data = self.cleaned_data + if 'pk' not in cleaned_data or not cleaned_data['pk']: + raise forms.ValidationError(_(u"You should select a file.")) + return cleaned_data + +class FileFormGeneral(forms.Form): + form_label = _("General") + associated_models = {'in_charge':models.Person, + 'related_file':models.File, + 'file_type':models.FileType} + in_charge = forms.IntegerField(label=_("Person in charge"), + widget=widgets.JQueryAutoComplete(reverse_lazy('autocomplete-person', + args=[models.PersonType.objects.get(txt_idx='sra_agent').pk]), + associated_model=models.Person, new=True), + validators=[models.valid_id(models.Person)]) + year = forms.IntegerField(label=_("Year"), + initial=lambda:datetime.datetime.now().year, + validators=[validators.MinValueValidator(1900), + validators.MaxValueValidator(2100)]) + numeric_reference = forms.IntegerField(label=_("Numeric reference"), + widget=forms.HiddenInput, required=False) + internal_reference = forms.CharField(label=_(u"Internal reference"), +max_length=60, validators=[models.is_unique(models.File, 'internal_reference')]) + creation_date = forms.DateField(label=_(u"Creation date"), + initial=get_now, widget=widgets.JQueryDate) + file_type = forms.ChoiceField(label=_("File type"), + choices=models.FileType.get_types()) + related_file = forms.IntegerField(label=_("Related file"), required=False, + widget=widgets.JQueryAutoComplete(reverse_lazy('autocomplete-file'), + associated_model=models.File), + validators=[models.valid_id(models.File)]) + comment = forms.CharField(label=_(u"Comment"), widget=forms.Textarea, + required=False) + +class FileFormGeneralRO(FileFormGeneral): + year = forms.IntegerField(label=_("Year"), + widget=forms.TextInput(attrs={'readonly':True})) + numeric_reference = forms.IntegerField(label=_("Numeric reference"), + widget=forms.TextInput(attrs={'readonly':True})) + internal_reference = forms.CharField(label=_(u"Internal reference"), + widget=forms.TextInput(attrs={'readonly':True},)) + +class FileFormAddress(forms.Form): + form_label = _("Address") + associated_models = {'town':models.Town} + total_surface = forms.IntegerField(required=False, + widget=widgets.AreaWidget, + label=_(u"Total surface (m²)"), + validators=[validators.MinValueValidator(0), + validators.MaxValueValidator(999999999)]) + address = forms.CharField(label=_(u"Main address"), widget=forms.Textarea) + address_complement = forms.CharField(label=_(u"Main address - complement"), + required=False) + postal_code = forms.CharField(label=_(u"Main address - postal code"), + max_length=10) + +class FileFormPreventive(forms.Form): + form_label = _("Preventive informations") + associated_models = {'general_contractor':models.Person, + 'saisine_type':models.SaisineType, + 'permit_type':models.PermitType, + 'town_planning_service':models.Organization} + general_contractor = forms.IntegerField(label=_(u"General contractor"), + widget=widgets.JQueryAutoComplete( + reverse_lazy('autocomplete-person', + args=[models.PersonType.objects.get(txt_idx='general_contractor').pk]), + associated_model=models.Person, new=True), + validators=[models.valid_id(models.Person)]) + town_planning_service = forms.IntegerField(required=False, + label=_(u"Town planning service"), + widget=widgets.JQueryAutoComplete( + reverse_lazy('autocomplete-organization', + args=[models.OrganizationType.objects.get(txt_idx='planning_service').pk]), + associated_model=models.Organization, new=True), + validators=[models.valid_id(models.Organization)]) + permit_type = forms.ChoiceField(label=_("Permit type"), required=False, + choices=models.PermitType.get_types()) + permit_reference = forms.CharField(label=_(u"Permit reference"), + required=False, validators=[validators.MaxLengthValidator(60)]) + total_developed_surface = forms.IntegerField(widget=widgets.AreaWidget, + label=_(u"Total developed surface (m²)"), + required=False, validators=[validators.MinValueValidator(0), + validators.MaxValueValidator(999999999)]) + if settings.COUNTRY == 'fr': + saisine_type = forms.ChoiceField(label=_(u"Saisine type"), + choices=[]) + reception_date = forms.DateField(label=_(u"Reception date"), + initial=get_now, widget=widgets.JQueryDate) + def __init__(self, *args, **kwargs): + super(FileFormPreventive, self).__init__(*args, **kwargs) + self.fields['saisine_type'].choices = models.SaisineType.get_types() + self.fields['saisine_type'].help_text = models.SaisineType.get_help() + +file_search_wizard = SearchWizard([('general-file_search', FileFormSelection)], + url_name='file_search',) + +file_creation_wizard = FileWizard([ + ('general-file_creation', FileFormGeneral), + ('address-file_creation', FileFormAddress), + ('towns-file_creation', TownFormSet), + ('parcels-file_creation', ParcelFormSet), + ('preventive-file_creation', FileFormPreventive), + ('final-file_creation', FinalForm)], + condition_list={ +'preventive-file_creation':is_preventive('general-file_creation', + models.FileType, type_key='file_type') + }, + url_name='file_creation',) + +file_modification_wizard = FileWizard([ + ('selec-file_modification', FileFormSelection), + ('general-file_modification', FileFormGeneralRO), + ('adress-file_modification', FileFormAddress), + ('towns-file_modification', TownFormSet), + ('parcels-file_modification', ParcelFormSet), + ('preventive-file_modification', FileFormPreventive), + ('final-file_modification', FinalForm)], + condition_list={ +'preventive-file_modification':is_preventive('general-file_modification', + models.FileType, type_key='file_type') + }, + url_name='file_modification',) + +class FileClosingWizard(ClosingWizard): + model = models.File + fields = ['year', 'numeric_reference', 'internal_reference', + 'file_type', 'in_charge', 'general_contractor', 'creation_date', + 'reception_date', 'total_surface', 'total_developed_surface', + 'address', 'address_complement', 'postal_code', 'comment'] + if settings.COUNTRY == 'fr': + fields += ['saisine_type', 'reference_number'] + fields += ['towns'] + +class FileDeletionWizard(FileClosingWizard): + def get_formated_datas(self, forms): + datas = super(FileDeletionWizard, self).get_formated_datas(forms) + datas.append((_("Associated operations"), [])) + for operation in models.Operation.objects.filter( + associated_file=self.current_obj).all(): + if operation.end_date: + datas[-1][1].append(('', unicode(operation))) + return datas + + def done(self, request, storage, form_list, **kwargs): + obj = self.get_current_object(request, storage) + for operation in models.Operation.objects.filter( + associated_file=obj).all(): + operation.delete() + obj.delete() + return render_to_response('wizard_done.html', {}, + context_instance=RequestContext(request)) + + +class FinalFileDeleteForm(FinalForm): + confirm_msg = " " + confirm_end_msg = _(u"Would you like to delete this archaelogical file ?") + +file_deletion_wizard = FileDeletionWizard([ + ('selec-file_deletion', FileFormSelection), + ('final-file_deletion', FinalFileDeleteForm)], + url_name='file_deletion',) +""" +file_closing_wizard = FileWizard([ + ('selec-file_closing', FileFormSelection), + ('date-operation_closing', OperationDateFormSelection), + ('final-operation_closing', FinalOperationClosingForm)], + url_name='operation_closing',) +""" + +class FileAdministrativeActWizard(OperationAdministrativeActWizard): + model = models.File + +class FileEditAdministrativeActWizard(FileAdministrativeActWizard): + model = models.AdministrativeAct + edit = True + def get_associated_item(self, request, storage, dct): + return self.get_current_object(request, storage).associated_file + +class AdministrativeActFileSelect(forms.Form): + associated_file__towns = get_town_field() + act_type = forms.ChoiceField(label=_("Act type"), choices=[]) + + def __init__(self, *args, **kwargs): + super(AdministrativeActFileSelect, self).__init__(*args, **kwargs) + self.fields['act_type'].choices = models.ActType.get_types( + dct={'intented_to':'F'}) + self.fields['act_type'].help_text = models.ActType.get_help( + dct={'intented_to':'F'}) + +class AdministrativeActFileFormSelection(AdministrativeActOpeFormSelection): + pk = forms.IntegerField(label="", required=False, + widget=widgets.JQueryJqGrid(reverse_lazy('get-administrativeactfile'), + AdministrativeActFileSelect(), models.AdministrativeAct, + table_cols='TABLE_COLS_FILE'), + validators=[models.valid_id(models.AdministrativeAct)]) + +class AdministrativeActFileForm(AdministrativeActOpeForm): + act_type = forms.ChoiceField(label=_(u"Act type"), choices=[]) + + def __init__(self, *args, **kwargs): + super(AdministrativeActFileForm, self).__init__(*args, **kwargs) + self.fields['act_type'].choices = models.ActType.get_types( + dct={'intented_to':'F'}) + self.fields['act_type'].help_text = models.ActType.get_help( + dct={'intented_to':'F'}) + +file_administrativeactfile_wizard = FileAdministrativeActWizard([ + ('selec-file_administrativeactfile', FileFormSelection), + ('administrativeact-file_administrativeactfile', AdministrativeActFileForm), + ('final-file_administrativeactfile', FinalForm)], + url_name='file_administrativeactfile',) + +file_administrativeactfile_modification_wizard = FileEditAdministrativeActWizard([ + ('selec-file_administrativeactfile_modification', + AdministrativeActFileFormSelection), + ('administrativeact-file_administrativeactfile_modification', + AdministrativeActFileForm), + ('final-file_administrativeactfile_modification', FinalForm)], + url_name='file_administrativeactfile_modification',) + +file_administrativeactfile_deletion_wizard = AdministrativeActDeletionWizard([ + ('selec-file_administrativeactfile_deletion', + AdministrativeActFileFormSelection), + ('final-file_administrativeactfile_deletion', + FinalAdministrativeActDeleteForm)], + url_name='file_administrativeactfile_deletion',) + diff --git a/ishtar/ishtar_base/forms_items.py b/ishtar/ishtar_base/forms_items.py new file mode 100644 index 000000000..b6da0f889 --- /dev/null +++ b/ishtar/ishtar_base/forms_items.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2010-2011 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 <http://www.gnu.org/licenses/>. + +# See the file COPYING for details. + +""" +Items forms definitions +""" +import datetime + +from django import forms +from django.core import validators +from django.core.exceptions import ObjectDoesNotExist +from django.utils.safestring import mark_safe +from django.db.models import Max +from django.utils.translation import ugettext_lazy as _ + +from ishtar import settings + +import models +import widgets +from forms import Wizard, FinalForm, FormSet, SearchWizard, FloatField,\ + formset_factory, get_now, reverse_lazy +from forms_common import get_town_field +from forms_context_records import RecordFormSelection + +class ItemWizard(Wizard): + model = models.Item + + def get_current_contextrecord(self, request, storage): + step = storage.get_current_step() + if not step: + return + if step.endswith('_creation'): # a context record has been selected + main_form_key = 'selecrecord-' + self.url_name + try: + idx = int(self.session_get_value(request, storage, + main_form_key, 'pk')) + current_cr = models.ContextRecord.objects.get(pk=idx) + return current_cr + except(TypeError, ValueError, ObjectDoesNotExist): + pass + current_item = self.get_current_object(request, storage) + if current_item: + base_items = current_item.base_items.all() + if base_items: + return base_items[0].context_record + + def get_template_context(self, request, storage, form=None): + """ + Get the operation and context record "reminder" on top of wizard forms + """ + context = super(ItemWizard, self).get_template_context(request, + storage, form) + current_cr = self.get_current_contextrecord(request, storage) + if not current_cr: + return context + operation = current_cr.operation + items = [] + if hasattr(operation, 'code_patriarche') and operation.code_patriarche: + items.append(unicode(operation.code_patriarche)) + items.append("-".join((unicode(operation.year), + unicode(operation.operation_code)))) + reminder = unicode(_("Current operation: ")) + u" - ".join(items) + reminder += u"<br/>" + unicode(_("Current context record: "))\ + + unicode(current_cr.label) + context['reminder'] = mark_safe(reminder) + return context + + def get_extra_model(self, dct, request, storage, form_list): + dct = super(ItemWizard, self).get_extra_model(dct, request, storage, + form_list) + dct['order'] = 1 + if 'pk' in dct and type(dct['pk']) == models.ContextRecord: + dct['base_items__context_record'] = dct.pop('pk') + return dct + +class ItemForm(forms.Form): + form_label = _("Item") + base_model = 'base_items' + associated_models = {'material_type':models.MaterialType,} + label = forms.CharField(label=_(u"ID"), + validators=[validators.MaxLengthValidator(60)]) + description = forms.CharField(label=_("Description"), + widget=forms.Textarea) + base_items__is_isolated = forms.NullBooleanField(label=_(u"Is isolated?"), + required=False) + material_type = forms.ChoiceField(label=_("Material type"), + choices=models.MaterialType.get_types()) + volume = FloatField(label=_(u"Volume (l)"), required=False) + weight = FloatField(label=_(u"Weight (g)"), required=False) + item_number = forms.IntegerField(label=_(u"Item number"), required=False) + +class DateForm(forms.Form): + form_label = _("Dating") + base_model = 'dating' + associated_models = {'dating__dating_type':models.DatingType, + 'dating__quality':models.DatingQuality, + 'dating__period':models.Period} + dating__period = forms.ChoiceField(label=_("Period"), + choices=models.Period.get_types()) + dating__start_date = forms.IntegerField(label=_(u"Start date"), + required=False) + dating__end_date = forms.IntegerField(label=_(u"End date"), required=False) + dating__quality = forms.ChoiceField(label=_("Quality"), required=False, + choices=models.DatingQuality.get_types()) + dating__dating_type = forms.ChoiceField(label=_("Dating type"), + required=False, choices=[]) + + def __init__(self, *args, **kwargs): + super(DateForm, self).__init__(*args, **kwargs) + self.fields['dating__dating_type'].choices = models.DatingType.get_types() + self.fields['dating__dating_type'].help_text = models.DatingType.get_help() + +item_creation_wizard = ItemWizard([ + ('selecrecord-item_creation', RecordFormSelection), + ('item-item_creation', ItemForm), + ('dating-item_creation', DateForm), + ('final-item_creation', FinalForm)], + url_name='item_creation',) + +class ItemSelect(forms.Form): + base_items__context_record__parcel__town = get_town_field() + base_items__context_record__operation__year = forms.IntegerField( + label=_(u"Year")) + base_items__context_record__operation__code_patriarche = \ + forms.IntegerField(label=_(u"Code PATRIARCHE")) + dating__period = forms.ChoiceField( + label=_(u"Period"), choices=models.Period.get_types()) + # TODO search by warehouse + material_type = forms.ChoiceField( + label=_("Material type"), choices=models.MaterialType.get_types()) + base_items__item__description = forms.CharField(label=_(u"Description")) + base_items__is_isolated = forms.NullBooleanField(label=_(u"Is isolated?")) + +class ItemFormSelection(forms.Form): + form_label = _("Item search") + associated_models = {'pk':models.Item} + currents = {'pk':models.Item} + pk = forms.IntegerField(label="", required=False, + widget=widgets.JQueryJqGrid(reverse_lazy('get-item'), + ItemSelect(), models.Item), + validators=[models.valid_id(models.Item)]) + +item_search_wizard = SearchWizard([ + ('general-item_search', ItemFormSelection)], + url_name='item_search',) + +item_modification_wizard = ItemWizard([ + ('selec-item_modification', ItemFormSelection), + ('item-item_modification', ItemForm), + ('dating-item_modification', DateForm), + ('final-item_modification', FinalForm)], + url_name='item_modification',) + +class TreatmentWizard(Wizard): + model = models.Treatment + +class BaseTreatmentForm(forms.Form): + form_label = _(u"Base treatment") + associated_models = {'treatment_type':models.TreatmentType, + 'person':models.Person, + 'location':models.Warehouse} + treatment_type = forms.ChoiceField(label=_(u"Treatment type"), + choices=models.TreatmentType.get_types()) + description = forms.CharField(label=_(u"Description"), + widget=forms.Textarea, required=False) + person = forms.IntegerField(label=_(u"Person"), + widget=widgets.JQueryAutoComplete( + reverse_lazy('autocomplete-person'), associated_model=models.Person), + validators=[models.valid_id(models.Person)]) + location = forms.IntegerField(label=_(u"Location"), + widget=widgets.JQueryAutoComplete( + reverse_lazy('autocomplete-warehouse'), associated_model=models.Warehouse, + new=True), + validators=[models.valid_id(models.Warehouse)]) + start_date = forms.DateField(label=_(u"Start date"), required=False, + widget=widgets.JQueryDate) + end_date = forms.DateField(label=_(u"End date"), required=False, + widget=widgets.JQueryDate) + +class ItemMultipleFormSelection(forms.Form): + form_label = _(u"Upstream items") + associated_models = {'items':models.Item} + items = forms.IntegerField(label="", required=False, + widget=widgets.JQueryJqGrid(reverse_lazy('get-item'), + ItemSelect(), models.Item, multiple=True), + validators=[models.valid_id(models.Item)]) + +class ContainerForm(forms.Form): + form_label = _(u"Container") + associated_models = {'container_type':models.ContainerType,} + reference = forms.CharField(label=_(u"Reference")) + container_type = forms.ChoiceField(label=_(u"Container type"), + choices=models.ContainerType.get_types()) + comment = forms.CharField(label=_(u"Comment"), + widget=forms.Textarea, required=False) + +def check_treatment(form_name, type_key, type_list=[], not_type_list=[]): + type_list = [models.TreatmentType.objects.get(txt_idx=tpe).pk + for tpe in type_list] + not_type_list = [models.TreatmentType.objects.get(txt_idx=tpe).pk + for tpe in not_type_list] + def func(self, request, storage): + if storage.prefix not in request.session or \ + 'step_data' not in request.session[storage.prefix] or \ + form_name not in request.session[storage.prefix]['step_data'] or\ + form_name + '-' + type_key not in \ + request.session[storage.prefix]['step_data'][form_name]: + return False + try: + type = int(request.session[storage.prefix]['step_data']\ + [form_name][form_name+'-'+type_key]) + return (not type_list or type in type_list) \ + and type not in not_type_list + except ValueError: + return False + return func + +class ResultItemForm(forms.Form): + form_label = _("Resulting item") + associated_models = {'material_type':models.MaterialType} + label = forms.CharField(label=_(u"ID"), + validators=[validators.MaxLengthValidator(60)]) + description = forms.CharField(label=_("Precise description"), + widget=forms.Textarea) + material_type = forms.ChoiceField(label=_("Material type"), + choices=models.MaterialType.get_types()) + volume = forms.IntegerField(label=_(u"Volume (l)")) + weight = forms.IntegerField(label=_(u"Weight (g)")) + item_number = forms.IntegerField(label=_(u"Item number")) + +ResultItemFormSet = formset_factory(ResultItemForm, can_delete=True, + formset=FormSet) +ResultItemFormSet.form_label = _(u"Resulting items") + +class UpstreamItemFormSelection(ItemFormSelection): + form_label = _(u"Upstream item") + +treatment_creation_wizard = TreatmentWizard([ + ('basetreatment-treatment_creation', BaseTreatmentForm), + ('selecitem-treatment_creation', UpstreamItemFormSelection), + ('multiselecitems-treatment_creation', ItemMultipleFormSelection), + ('container-treatment_creation', ContainerForm), + ('resultitem-treatment_creation', ResultItemForm), + ('resultitems-treatment_creation', ResultItemFormSet), + ('final-treatment_creation', FinalForm)], + condition_list={ +'selecitem-treatment_creation': + check_treatment('basetreatment-treatment_creation', 'treatment_type', + not_type_list=['physical_grouping']), +'multiselecitems-treatment_creation': + check_treatment('basetreatment-treatment_creation', 'treatment_type', + ['physical_grouping']), +'resultitems-treatment_creation': + check_treatment('basetreatment-treatment_creation', 'treatment_type', + ['split']), +'resultitem-treatment_creation': + check_treatment('basetreatment-treatment_creation', 'treatment_type', + not_type_list=['split']), +'container-treatment_creation': + check_treatment('basetreatment-treatment_creation', 'treatment_type', + ['packaging']), + }, + url_name='treatment_creation',) + diff --git a/ishtar/ishtar_base/forms_main.py b/ishtar/ishtar_base/forms_main.py new file mode 100644 index 000000000..723b80d83 --- /dev/null +++ b/ishtar/ishtar_base/forms_main.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2011 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 <http://www.gnu.org/licenses/>. + +# See the file COPYING for details. + +from forms_common import * +from forms_files import * +from forms_operations import * +from forms_context_records import * +from forms_items import * +from forms import * + diff --git a/ishtar/ishtar_base/forms_operations.py b/ishtar/ishtar_base/forms_operations.py new file mode 100644 index 000000000..ffa310263 --- /dev/null +++ b/ishtar/ishtar_base/forms_operations.py @@ -0,0 +1,640 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2010-2011 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 <http://www.gnu.org/licenses/>. + +# See the file COPYING for details. + +""" +Operations forms definitions +""" +import datetime + +from django import forms +from django.shortcuts import render_to_response +from django.template import RequestContext +from django.core import validators +from django.core.exceptions import ObjectDoesNotExist +from django.db.models import Max +from django.utils.translation import ugettext_lazy as _ + +from ishtar import settings + +import models +import widgets +from forms import Wizard, FinalForm, FormSet, SearchWizard, ClosingWizard, \ + DeletionWizard, formset_factory, get_now, reverse_lazy, get_form_selection +from forms_common import TownForm, TownFormSet, ParcelFormSet, ParcelForm,\ + AuthorFormset, SourceForm, SourceWizard, get_town_field + +def is_preventive(form_name, model, type_key='operation_type', key=''): + def func(self, request, storage): + if storage.prefix not in request.session or \ + 'step_data' not in request.session[storage.prefix] or \ + form_name not in request.session[storage.prefix]['step_data'] or\ + form_name + '-' + type_key not in \ + request.session[storage.prefix]['step_data'][form_name]: + return False + try: + typ = int(request.session[storage.prefix]['step_data']\ + [form_name][form_name+'-'+type_key]) + return model.is_preventive(typ, key) + except ValueError: + return False + return func + +class OperationWizard(Wizard): + model = models.Operation + object_parcel_type = 'operation' + + def get_template(self, request, storage): + templates = super(OperationWizard, self).get_template(request, storage) + current_step = storage.get_current_step() or self.get_first_step( + request, storage) + if current_step.startswith('towns-'): + templates = ['towns_wizard.html'] + templates + return templates + + def get_extra_context(self, request, storage): + """ + Return extra context for templates + """ + context = super(OperationWizard, self).get_extra_context(request, + storage) + step = self.determine_step(request, storage) + if not step.startswith('towns-'): + return context + context['TOWNS'] = self.get_towns(request, storage) + return context + + def get_towns(self, request, storage): + """ + Obtention des villes disponibles + """ + general_form_key = 'general-' + self.url_name + towns = [] + file_id = self.session_get_value(request, storage, general_form_key, + "associated_file") + if file_id: + try: + for town in models.File.objects.get(pk=int(file_id) + ).towns.all(): + towns.append((town.pk, unicode(town))) + except (ValueError, ObjectDoesNotExist): + pass + return sorted(towns, key=lambda x:x[1]) + else: + return -1 + + def get_form(self, request, storage, step=None, data=None, files=None): + """ + Manage specifics fields + """ + if data: + data = data.copy() + else: + data = {} + if not step: + step = self.determine_step(request, storage) + form = self.get_form_list(request, storage)[step] + general_form_key = 'general-' + self.url_name + # put hidden year field for refs + if data and step.startswith('refs-') \ + and self.session_has_key(request, storage, general_form_key): + prefix = 'refs-' + self.url_name + year = int(request.session[storage.prefix]['step_data']\ + [general_form_key][general_form_key+"-year"]) + data[prefix+'-hidden_year'] = year + # manage the dynamic choice of towns + if step.startswith('towns') and hasattr(form, 'management_form'): + data['TOWNS'] = self.get_towns(request, storage) + elif step.startswith('parcels') and hasattr(form, 'management_form'): + file_id = self.session_get_value(request, storage, general_form_key, + "associated_file") + if file_id: + parcels = [] + try: + for parcel in models.File.objects.get(pk=int(file_id) + ).parcels.all(): + parcels.append((parcel.pk, parcel.short_label())) + except (ValueError, ObjectDoesNotExist): + pass + data['PARCELS'] = sorted(parcels, key=lambda x:x[1]) + else: + town_form_key = step.startswith('parcelsgeneral') \ + and 'townsgeneral-' or 'towns-' + town_form_key += self.url_name + town_ids = self.session_get_value(request, storage, + town_form_key, 'town', multi=True) or [] + towns = [] + for town_id in town_ids: + try: + town = models.Town.objects.get(pk=int(town_id)) + towns.append((town.pk, unicode(town))) + except (ValueError, ObjectDoesNotExist): + pass + data['TOWNS'] = sorted(towns, key=lambda x:x[1]) + data = data or None + form = super(OperationWizard, self).get_form(request, storage, step, + data, files) + return form + + def get_form_initial(self, request, storage, step): + initial = super(OperationWizard, self).get_form_initial(request, + storage, step) + # put hidden year and default operation_code field for refs + general_form_key = 'general-' + self.url_name + if step.startswith('refs-') \ + and self.session_has_key(request, storage, general_form_key)\ + and 'operation_code' not in initial: + year = int(request.session[storage.prefix]['step_data']\ + [general_form_key][general_form_key+"-year"]) + initial['hidden_year'] = year + max_val = models.Operation.objects.filter(year=year).aggregate( + Max('operation_code'))["operation_code__max"] + initial['operation_code'] = max_val and (max_val + 1) or 1 + return initial + + def get_formated_datas(self, forms): + """ + Show a specific warning if no archaelogical file is provided + """ + datas = super(OperationWizard, self).get_formated_datas(forms) + # simple selection town is used if no Archaelogical File is provided + has_af = [form for form in forms + if isinstance(form, SelectedTownFormSet)] + if not has_af: + datas = [[_(u"Warning: No Archaelogical File is provided. " + u"If you have forget it return to the first step."), []]]\ + + datas + return datas + +class OperationSelect(forms.Form): + towns = get_town_field() + operation_type = forms.ChoiceField(label=_("Operation type"), + choices=[]) + remains = forms.ChoiceField(label=_("Remains"), + choices=models.RemainType.get_types()) + year = forms.IntegerField(label=_("Year")) + + def __init__(self, *args, **kwargs): + super(OperationSelect, self).__init__(*args, **kwargs) + self.fields['operation_type'].choices = models.OperationType.get_types() + self.fields['operation_type'].help_text = models.OperationType.get_help() + +class OperationFormSelection(forms.Form): + form_label = _(u"Operation search") + associated_models = {'pk':models.Operation} + currents = {'pk':models.Operation} + pk = forms.IntegerField(label="", required=False, + widget=widgets.JQueryJqGrid(reverse_lazy('get-operation'), + OperationSelect(), models.Operation), + validators=[models.valid_id(models.Operation)]) + + def clean(self): + cleaned_data = self.cleaned_data + if 'pk' not in cleaned_data or not cleaned_data['pk']: + raise forms.ValidationError(_(u"You should select an operation.")) + return cleaned_data + +class OperationFormGeneral(forms.Form): + form_label = _("General") + associated_models = {'in_charge':models.Person, + 'associated_file':models.File, + 'operation_type':models.OperationType} + currents = {'associated_file':models.File} + in_charge = forms.IntegerField(label=_("Person in charge of the operation"), + widget=widgets.JQueryAutoComplete(reverse_lazy('autocomplete-person', + args=["_".join( + [unicode(models.PersonType.objects.get(txt_idx='head_scientist').pk), + unicode(models.PersonType.objects.get(txt_idx='sra_agent').pk)])]), + associated_model=models.Person, new=True), + validators=[models.valid_id(models.Person)], required=False) + associated_file = forms.IntegerField(label=_(u"Archaelogical file"), + widget=widgets.JQueryAutoComplete(reverse_lazy('autocomplete-file'), + associated_model=models.File), + validators=[models.valid_id(models.File)], required=False) + operation_type = forms.ChoiceField(label=_(u"Operation type"), + choices=[]) + start_date = forms.DateField(label=_(u"Start date"), required=False, + widget=widgets.JQueryDate) + surface = forms.IntegerField(required=False, widget=widgets.AreaWidget, + label=_(u"Total surface (m²)"), + validators=[validators.MinValueValidator(0), + validators.MaxValueValidator(999999999)]) + year = forms.IntegerField(label=_(u"Year"), + initial=lambda:datetime.datetime.now().year, + validators=[validators.MinValueValidator(1900), + validators.MaxValueValidator(2100)]) + comment = forms.CharField(label=_(u"Comment"), widget=forms.Textarea, + required=False) + + def __init__(self, *args, **kwargs): + super(OperationFormGeneral, self).__init__(*args, **kwargs) + self.fields['operation_type'].choices = models.OperationType.get_types() + self.fields['operation_type'].help_text = models.OperationType.get_help() + +class OperationFormReference(forms.Form): + form_label = _("References") + associated_models = {'in_charge':models.Person, + 'associated_file':models.File, + 'operation_type':models.OperationType} + currents = {'associated_file':models.File} + pk = forms.IntegerField(required=False, widget=forms.HiddenInput) + hidden_year = forms.IntegerField(widget=forms.HiddenInput) + operation_code = forms.IntegerField(label=_(u"Operation code")) + if settings.COUNTRY == 'fr': + code_patriarche = forms.IntegerField(label=u"Code PATRIARCHE", + required=False) + code_dracar = forms.CharField(label=u"Code DRACAR", required=False, + validators=[validators.MaxLengthValidator(10)]) + + def clean(self): + # manage unique operation ID + cleaned_data = self.cleaned_data + year = cleaned_data.get("hidden_year") + operation_code = cleaned_data.get("operation_code") + ops = models.Operation.objects.filter(year=year, + operation_code=operation_code) + if 'pk' in cleaned_data and cleaned_data['pk']: + ops = ops.exclude(pk=cleaned_data['pk']) + if ops.count(): + max_val = models.Operation.objects.filter(year=year).aggregate( + Max('operation_code'))["operation_code__max"] + raise forms.ValidationError(_(u"Operation code already exist for " +"year: %(year)d - use a value bigger than %(last_val)d") % {'year':year, + 'last_val':max_val}) + return cleaned_data + +class OperationFormPreventive(forms.Form): + form_label = _("Preventive informations - excavation") + cost = forms.IntegerField(label=_(u"Cost (€)"), required=False) + if settings.COUNTRY == 'fr': + fnap_financing = forms.FloatField(required=False, + label=u"Pourcentage de financement FNAP", + validators=[validators.MinValueValidator(0), + validators.MaxValueValidator(100)]) + +class OperationFormPreventiveDiag(forms.Form): + form_label = _("Preventive informations - diagnostic") + if settings.COUNTRY == 'fr': + zoning_prescription = forms.NullBooleanField(required=False, + label=_(u"Prescription on zoning")) + large_area_prescription = forms.NullBooleanField(required=False, + label=_(u"Prescription on large area")) + geoarchaeological_context_prescription = forms.NullBooleanField( + required=False, label=_(u"Prescription on geoarchaeological context")) + +class SelectedTownForm(forms.Form): + form_label = _("Towns") + associated_models = {'town':models.Town} + town = forms.ChoiceField(label=_("Town"), choices=(), + validators=[models.valid_id(models.Town)]) + def __init__(self, *args, **kwargs): + towns = None + if 'data' in kwargs and 'TOWNS' in kwargs['data']: + towns = kwargs['data']['TOWNS'] + # clean data if not "real" data + prefix_value = kwargs['prefix'] + '-town' + if not [k for k in kwargs['data'].keys() + if k.startswith(prefix_value) and kwargs['data'][k]]: + kwargs['data'] = None + if 'files' in kwargs: + kwargs.pop('files') + super(SelectedTownForm, self).__init__(*args, **kwargs) + if towns and towns != -1: + self.fields['town'].choices = [('', '--')] + towns + +SelectedTownFormSet = formset_factory(SelectedTownForm, can_delete=True, + formset=TownFormSet) +SelectedTownFormSet.form_label = _("Towns") + +SelectedTownGeneralFormSet = formset_factory(TownForm, can_delete=True, + formset=TownFormSet) +SelectedTownGeneralFormSet.form_label = _("Towns") + +class SelectedParcelForm(forms.Form): + form_label = _("Parcels") + associated_models = {'parcel':models.Parcel} + parcel = forms.ChoiceField(label=_("Parcel"), choices=(), + validators=[models.valid_id(models.Parcel)]) + def __init__(self, *args, **kwargs): + parcels = None + if 'data' in kwargs and 'PARCELS' in kwargs['data']: + parcels = kwargs['data']['PARCELS'] + # clean data if not "real" data + prefix_value = kwargs['prefix'] + '-parcel' + if not [k for k in kwargs['data'].keys() + if k.startswith(prefix_value) and kwargs['data'][k]]: + kwargs['data'] = None + if 'files' in kwargs: + kwargs.pop('files') + super(SelectedParcelForm, self).__init__(*args, **kwargs) + if parcels: + self.fields['parcel'].choices = [('', '--')] + parcels + +SelectedParcelFormSet = formset_factory(SelectedParcelForm, can_delete=True, + formset=ParcelFormSet) +SelectedParcelFormSet.form_label = _("Parcels") + +SelectedParcelGeneralFormSet = formset_factory(ParcelForm, can_delete=True, + formset=ParcelFormSet) +SelectedParcelGeneralFormSet.form_label = _("Parcels") + +class RemainForm(forms.Form): + form_label = _("Remain types") + associated_models = {'remain':models.RemainType} + remain = forms.ChoiceField(label=_("Remain type"), required=False, + choices=models.RemainType.get_types()) + +class RemainFormSet(FormSet): + def clean(self): + """Checks that no remain types are duplicated.""" + return self.check_duplicate(['remain_type'], + _(u"There are identical remain types")) + +RemainFormset = formset_factory(RemainForm, can_delete=True, + formset=RemainFormSet) +RemainFormset.form_label = _("Remain types") + +class PeriodForm(forms.Form): + form_label = _("Periods") + associated_models = {'period':models.Period} + period = forms.ChoiceField(label=_("Period"), required=False, + choices=models.Period.get_types()) + +class PeriodFormSet(FormSet): + def clean(self): + """Checks that no period are duplicated.""" + return self.check_duplicate(['period'], + _(u"There are identical periods")) + +PeriodFormset = formset_factory(PeriodForm, can_delete=True, + formset=PeriodFormSet) +PeriodFormset.form_label = _("Periods") + +operation_search_wizard = SearchWizard([ + ('general-operation_search', OperationFormSelection)], + url_name='operation_search',) + +def has_associated_file(form_name, file_key='associated_file', negate=False): + def func(self, request, storage): + if storage.prefix not in request.session or \ + 'step_data' not in request.session[storage.prefix] or \ + form_name not in request.session[storage.prefix]['step_data'] or\ + form_name + '-' + file_key not in \ + request.session[storage.prefix]['step_data'][form_name]: + return negate + try: + file_id = int(request.session[storage.prefix]['step_data']\ + [form_name][form_name+'-'+file_key]) + return not negate + except ValueError: + return negate + return func + +operation_creation_wizard = OperationWizard([ + ('general-operation_creation', OperationFormGeneral), + ('refs-operation_creation', OperationFormReference), + ('preventive-operation_creation', OperationFormPreventive), + ('preventivediag-operation_creation', OperationFormPreventiveDiag), + ('townsgeneral-operation_creation', SelectedTownGeneralFormSet), + ('towns-operation_creation', SelectedTownFormSet), + ('parcelsgeneral-operation_creation', SelectedParcelGeneralFormSet), + ('parcels-operation_creation', SelectedParcelFormSet), + ('remains-operation_creation', RemainFormset), + ('periods-operation_creation', PeriodFormset), + ('final-operation_creation', FinalForm)], + condition_list={ +'preventive-operation_creation':is_preventive('general-operation_creation', + models.OperationType, 'operation_type', 'prev_excavation'), +'preventivediag-operation_creation':is_preventive('general-operation_creation', + models.OperationType, 'operation_type', 'arch_diagnostic'), +'townsgeneral-operation_creation':has_associated_file( + 'general-operation_creation', negate=True), +'towns-operation_creation':has_associated_file('general-operation_creation'), +'parcelsgeneral-operation_creation':has_associated_file( + 'general-operation_creation', negate=True), +'parcels-operation_creation':has_associated_file('general-operation_creation'), + }, + url_name='operation_creation',) + +operation_modification_wizard = OperationWizard([ + ('selec-operation_modification', OperationFormSelection), + ('general-operation_modification', OperationFormGeneral), + ('refs-operation_modification', OperationFormReference), + ('preventive-operation_modification', OperationFormPreventive), + ('preventivediag-operation_modification', OperationFormPreventiveDiag), + ('towns-operation_modification', SelectedTownFormSet), + ('townsgeneral-operation_modification', SelectedTownGeneralFormSet), + ('parcels-operation_modification', SelectedParcelFormSet), + ('parcelsgeneral-operation_modification', SelectedParcelGeneralFormSet), + ('remains-operation_modification', RemainFormset), + ('periods-operation_modification', PeriodFormset), + ('final-operation_modification', FinalForm)], + condition_list={ +'preventive-operation_modification':is_preventive( + 'general-operation_modification', models.OperationType, + 'operation_type', 'prev_excavation'), +'preventivediag-operation_modification':is_preventive( + 'general-operation_modification', models.OperationType, + 'operation_type', 'arch_diagnostic'), +'townsgeneral-operation_modification':has_associated_file( + 'general-operation_modification', negate=True), +'towns-operation_modification':has_associated_file( + 'general-operation_modification'), +'parcelsgeneral-operation_modification':has_associated_file( + 'general-operation_modification', negate=True), +'parcels-operation_modification':has_associated_file( + 'general-operation_modification'), + }, + url_name='operation_modification',) + +class OperationDateFormSelection(forms.Form): + form_label = _("Closing date") + end_date = forms.DateField(label=_(u"Closing date"), + widget=widgets.JQueryDate) + +class OperationClosingWizard(ClosingWizard): + model = models.Operation + fields = ['year', 'operation_code', 'operation_type', 'associated_file', + 'in_charge', 'start_date', 'end_date', 'comment', 'towns', 'remains'] + +class FinalOperationClosingForm(FinalForm): + confirm_msg = " " + confirm_end_msg = _(u"Would you like to close this operation?") + +operation_closing_wizard = OperationClosingWizard([ + ('selec-operation_closing', OperationFormSelection), + ('date-operation_closing', OperationDateFormSelection), + ('final-operation_closing', FinalOperationClosingForm)], + url_name='operation_closing',) + +class OperationDeletionWizard(DeletionWizard): + model = models.Operation + +class OperationDeletionForm(FinalForm): + confirm_msg = " " + confirm_end_msg = _(u"Would you like to delete this operation?") + +operation_deletion_wizard = OperationDeletionWizard([ + ('selec-operation_deletion', OperationFormSelection), + ('final-operation_deletion', OperationDeletionForm)], + url_name='operation_deletion',) + +class OperationSourceWizard(SourceWizard): + model = models.OperationSource + +OperationSourceFormSelection = get_form_selection( + 'OperationSourceFormSelection', _(u"Operation search"), 'operation', + models.Operation, OperationSelect, 'get-operation', + _(u"You should select an operation.")) + +operation_source_add_wizard = OperationSourceWizard([ + ('selec-operation_source_add', OperationSourceFormSelection), + ('source-operation_source_add', SourceForm), + ('authors-operation_source_add', AuthorFormset), + ('final-operation_source_add', FinalForm)], + url_name='operation_source_add',) + +class OperationAdministrativeActWizard(OperationWizard): + edit = False + + def get_extra_model(self, dct, request, storage, form_list): + dct['history_modifier'] = request.user + return dct + + def get_associated_item(self, request, storage, dct): + return self.get_current_object(request, storage) + + def save_model(self, dct, m2m, whole_associated_models, request, storage, + form_list, return_object): + associated_item = self.get_associated_item(request, storage, dct) + if not associated_item: + return self.render(request, storage, form_list[-1]) + if isinstance(associated_item, models.File): + dct['associated_file'] = associated_item + elif isinstance(associated_item, models.Operation): + dct['operation'] = associated_item + dct['history_modifier'] = request.user + if 'pk' in dct: + dct.pop('pk') + if self.edit: + admact = self.get_current_object(request, storage) + for k in dct: + if hasattr(admact, k): + setattr(admact, k, dct[k]) + else: + admact = models.AdministrativeAct(**dct) + admact.save() + res = render_to_response('wizard_done.html', {}, + context_instance=RequestContext(request)) + return res + +class OperationEditAdministrativeActWizard(OperationAdministrativeActWizard): + model = models.AdministrativeAct + edit = True + def get_associated_item(self, request, storage, dct): + return self.get_current_object(request, storage).operation + +class AdministrativeActOpeSelect(forms.Form): + operation__towns = get_town_field() + act_type = forms.ChoiceField(label=_("Act type"), choices=[]) + + def __init__(self, *args, **kwargs): + super(AdministrativeActOpeSelect, self).__init__(*args, **kwargs) + self.fields['act_type'].choices = models.ActType.get_types( + dct={'intented_to':'O'}) + self.fields['act_type'].help_text = models.ActType.get_help( + dct={'intented_to':'O'}) + +class AdministrativeActOpeFormSelection(forms.Form): + form_label = _("Administrative act search") + associated_models = {'pk':models.AdministrativeAct} + currents = {'pk':models.AdministrativeAct} + pk = forms.IntegerField(label="", required=False, + widget=widgets.JQueryJqGrid(reverse_lazy('get-administrativeactop'), + AdministrativeActOpeSelect(), models.AdministrativeAct, + table_cols='TABLE_COLS_OPE'), + validators=[models.valid_id(models.AdministrativeAct)]) + + def clean(self): + cleaned_data = self.cleaned_data + if 'pk' not in cleaned_data or not cleaned_data['pk']: + raise forms.ValidationError(_(u"You should select an administrative" + " act.")) + return cleaned_data + +class AdministrativeActOpeForm(forms.Form): + form_label = _("General") + associated_models = {'act_type':models.ActType, + 'signatory':models.Person} + act_type = forms.ChoiceField(label=_("Act type"), choices=[]) + signatory = forms.IntegerField(label=_("Signatory"), + widget=widgets.JQueryAutoComplete(reverse_lazy('autocomplete-person'), + associated_model=models.Person, new=True), + validators=[models.valid_id(models.Person)]) + act_object = forms.CharField(label=_(u"Object"), max_length=200, + widget=forms.Textarea) + signature_date = forms.DateField(label=_(u"Signature date"), + widget=widgets.JQueryDate) + if settings.COUNTRY == 'fr': + ref_sra = forms.CharField(label=u"Référence SRA", max_length=15) + + def __init__(self, *args, **kwargs): + super(AdministrativeActOpeForm, self).__init__(*args, **kwargs) + self.fields['act_type'].choices = models.ActType.get_types( + dct={'intented_to':'O'}) + self.fields['act_type'].help_text = models.ActType.get_help( + dct={'intented_to':'O'}) + +class AdministrativeActDeletionWizard(ClosingWizard): + model = models.AdministrativeAct + fields = ['act_type', 'in_charge', 'operator', 'scientific', 'signatory', + 'operation', 'associated_file', 'signature_date', 'act_object',] + if settings.COUNTRY == 'fr': + fields += ['ref_sra'] + + def done(self, request, storage, form_list, **kwargs): + obj = self.get_current_object(request, storage) + obj.delete() + return render_to_response('wizard_done.html', {}, + context_instance=RequestContext(request)) + +class FinalAdministrativeActDeleteForm(FinalForm): + confirm_msg = " " + confirm_end_msg = _(u"Would you like to delete this administrative act?") + +operation_administrativeactop_wizard = OperationAdministrativeActWizard([ + ('selec-operation_administrativeactop', OperationFormSelection), + ('administrativeact-operation_administrativeactop', AdministrativeActOpeForm), + ('final-operation_administrativeactop', FinalForm)], + url_name='operation_administrativeactop',) + +operation_administrativeactop_modification_wizard = \ + OperationEditAdministrativeActWizard([ + ('selec-operation_administrativeactop_modification', + AdministrativeActOpeFormSelection), + ('administrativeact-operation_administrativeactop_modification', + AdministrativeActOpeForm), + ('final-operation_administrativeactop_modification', FinalForm)], + url_name='operation_administrativeactop_modification',) + +operation_administrativeactop_deletion_wizard = AdministrativeActDeletionWizard([ + ('selec-operation_administrativeactop_deletion', + AdministrativeActOpeFormSelection), + ('final-operation_administrativeactop_deletion', + FinalAdministrativeActDeleteForm)], + url_name='operation_administrativeactop_deletion',) diff --git a/ishtar/ishtar_base/menus.py b/ishtar/ishtar_base/menus.py new file mode 100644 index 000000000..3f8a7dd59 --- /dev/null +++ b/ishtar/ishtar_base/menus.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2010-2011 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 <http://www.gnu.org/licenses/>. + +# See the file COPYING for details. + +""" +Menus +""" + +from django.utils.translation import ugettext_lazy as _ + +import models + +class SectionItem: + def __init__(self, idx, label, childs=[]): + self.idx = idx + self.label = label + self.childs = childs + self.available = False + self.items = {} + + def can_be_available(self, user): + for child in self.childs: + if child.can_be_available(user): + return True + return False + + def is_available(self, user, obj=None): + for child in self.childs: + if child.is_available(user, obj): + return True + return False + + def set_items(self, user, items): + if user: + self.available = self.can_be_available(user) + for child in self.childs: + child.set_items(user, items) + items[child.idx] = child + +class MenuItem: + def __init__(self, idx, label, model=None, access_controls=[]): + self.idx = idx + self.label = label + self.model = model + self.access_controls = access_controls + self.available = False + + def can_be_available(self, user): + if not self.access_controls: + return True + for access_control in self.access_controls: + if user.has_perm('furnitures.' + access_control, self.model): + return True + return False + + def is_available(self, user, obj=None): + if not self.access_controls: + return True + for access_control in self.access_controls: + if user.has_perm('furnitures.' + access_control, self.model, obj): + return True + return False + + def set_items(self, user, items): + if user: + self.available = self.can_be_available(user) + +class Menu: + def __init__(self, user): + self.user = user + self.initialized = False + self.childs = [ + SectionItem('administration', _(u"Administration"), + childs=[SectionItem('person', _(u"Person"), + childs=[ + MenuItem('person_creation', _(u"Creation"), + model=models.Person, + access_controls=['add_person', 'add_own_person']), + MenuItem('person_modification', _(u"Modification"), + model=models.Person, + access_controls=['change_person', 'change_own_person']), + ]), + MenuItem('account_management', _(u"Account management"), + model=models.IshtarUser, + access_controls=['add_ishtaruser',]), + ]), + SectionItem('file_management', _(u"Archaelogical file"), + childs=[ + MenuItem('file_search', _(u"Search"), + model=models.File, + access_controls=['view_file', 'view_own_file']), + MenuItem('file_creation', _(u"Creation"), + model=models.File, + access_controls=['add_file', 'add_own_file']), + MenuItem('file_modification', _(u"Modification"), + model=models.File, + access_controls=['change_file', 'change_own_file']), + MenuItem('file_deletion', _(u"Deletion"), + model=models.File, + access_controls=['delete_file', 'delete_own_file']), + SectionItem('admin_act_files', _(u"Administrative act"), + childs=[ + MenuItem('file_administrativeactfile', + _(u"Add"), + model=models.Operation, + access_controls=['change_file', 'change_own_file']), + MenuItem('file_administrativeactfile_modification', + _(u"Modification"), + model=models.AdministrativeAct, + access_controls=['change_file', 'change_own_file']), + MenuItem('file_administrativeactfile_deletion', + _(u"Deletion"), + model=models.AdministrativeAct, + access_controls=['delete_file', 'delete_own_file']), + ],), + ]), + SectionItem('operation_management', _(u"Operation"), + childs=[ + MenuItem('operation_search', _(u"Search"), + model=models.Operation, + access_controls=['view_operation', + 'view_own_operation']), + MenuItem('operation_creation', _(u"Creation"), + model=models.Operation, + access_controls=['add_operation', + 'add_own_operation']), + MenuItem('operation_modification', _(u"Modification"), + model=models.Operation, + access_controls=['change_operation', + 'change_own_operation']), + MenuItem('operation_closing', _(u"Closing"), + model=models.Operation, + access_controls=['change_operation', + 'change_own_operation']), + MenuItem('operation_deletion', _(u"Deletion"), + model=models.Operation, + access_controls=['change_operation', + 'change_own_operation']), + SectionItem('admin_act_operations', + _(u"Administrative act"), + childs=[ + MenuItem('operation_administrativeactop', + _(u"Add"), + model=models.Operation, + access_controls=['change_operation', + 'change_own_operation']), + MenuItem('operation_administrativeactop_modification', + _(u"Modification"), + model=models.AdministrativeAct, + access_controls=['change_operation', + 'change_own_operation']), + MenuItem('operation_administrativeactop_deletion', + _(u"Deletion"), + model=models.AdministrativeAct, + access_controls=['operation_deletion', + 'delete_own_operation']), + ],), + ]), + SectionItem('record_management', _(u"Context record"), + childs=[ + MenuItem('record_search', _(u"Search"), + model=models.ContextRecord, + access_controls=['view_contextrecord', + 'view_own_contextrecord']), + MenuItem('record_creation', _(u"Creation"), + model=models.ContextRecord, + access_controls=['add_contextrecord', + 'add_own_contextrecord']), + MenuItem('record_modification', _(u"Modification"), + model=models.ContextRecord, + access_controls=['change_contextrecord', + 'change_own_contextrecord']), + MenuItem('record_deletion', _(u"Deletion"), + model=models.ContextRecord, + access_controls=['delete_contextrecord', + 'delete_own_contextrecord']), + ]), + SectionItem('item_management', _(u"Item"), + childs=[ + MenuItem('item_search', _(u"Search"), + model=models.Item, + access_controls=['view_item', + 'view_own_item']), + MenuItem('item_creation', _(u"Creation"), + model=models.Item, + access_controls=['add_item', + 'add_own_item']), + MenuItem('item_modification', _(u"Modification"), + model=models.Item, + access_controls=['change_item', + 'change_own_item']), + MenuItem('treatment_creation', _(u"Add a treatment"), + model=models.Treatment, + access_controls=['add_treatment', + 'add_own_treatment']), + ]), + SectionItem('source_management', _(u"Documentation"), + childs=[SectionItem('admin_add_sources', _(u"Add"), + childs=[ + MenuItem('operation_source_add', _(u"Related to an operation"), + model=models.OperationSource, + access_controls=['change_operation', + 'change_own_operation']), + ]), + ]), + SectionItem('warehouse', _(u"Warehouse"), + childs=[ + MenuItem('warehouse_inventory', _(u"Inventory"), + model=models.Warehouse, + access_controls=['change_warehouse',]), + MenuItem('warehouse_recording', _(u"Recording"), + model=models.Treatment, + access_controls=['add_treatment', 'add_own_treatment']), + MenuItem('warehouse_packaging', _(u"Packaging"), + model=models.Treatment, + access_controls=['add_treatment', 'add_own_treatment']), + MenuItem('warehouse_lend', _(u"Lending"), + model=models.Treatment, + access_controls=['add_treatment', 'add_own_treatment']), + ]), + SectionItem('dashboard', _(u"Dashboard"), + childs=[ + MenuItem('dashboard_file', _(u"Files"), + model=models.File, + access_controls=['change_file',]), + MenuItem('dashboard_operation', _(u"Operations"), + model=models.Operation, + access_controls=['change_operation',]), + MenuItem('dashboard_treatment', _(u"Treatments"), + model=models.Treatment, + access_controls=['change_treatment',]), + MenuItem('dashboard_warehouse', _(u"Warehouses"), + model=models.Warehouse, + access_controls=['change_warehouse',]), + ]), + ] + self.items = {} + + def init(self): + if self.initialized: + return + self.items = {} + for main_menu in self.childs: + main_menu.set_items(self.user, self.items) + self.initialized = True + +menu = Menu(None) +menu.init() diff --git a/ishtar/ishtar_base/models.py b/ishtar/ishtar_base/models.py new file mode 100644 index 000000000..7de7b239b --- /dev/null +++ b/ishtar/ishtar_base/models.py @@ -0,0 +1,1204 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2010-2011 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 <http://www.gnu.org/licenses/>. + +# See the file COPYING for details. + +""" +Models description +""" +import datetime + +from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.core.validators import validate_slug +from django.utils.translation import ugettext_lazy as _, ugettext +from django.db.utils import DatabaseError +from django.utils.safestring import SafeUnicode, mark_safe +from django.db.models import Q, Max +from django.db.models.signals import m2m_changed + +from django.contrib.auth.models import User +from django.contrib.gis.db import models +from django.contrib import admin + +from simple_history.models import HistoricalRecords as BaseHistoricalRecords + +from ishtar import settings + +JOINT = u" - " + +# HistoricalRecords enhancement: don't save identical versions +class HistoricalRecords(BaseHistoricalRecords): + def create_historical_record(self, instance, type): + manager = getattr(instance, self.manager_name) + attrs = {} + for field in instance._meta.fields: + attrs[field.attname] = getattr(instance, field.attname) + history = instance.history.all() + if not history: + manager.create(history_type=type, **attrs) + return + old_instance = history[0] + for field in instance._meta.fields: + if getattr(old_instance, field.attname) != attrs[field.attname]: + manager.create(history_type=type, **attrs) + return + +# valid ID validator for models +def valid_id(cls): + def func(value): + try: + cls.objects.get(pk=value) + except ObjectDoesNotExist: + raise ValidationError(_(u"Not a valid item.")) + return func + +# unique validator for models +def is_unique(cls, field): + 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 cls.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 [] + query = cls.get_query_owns(user) + if not query: + return [] + return cls.objects.filter(query).order_by(*cls._meta.ordering).all() + +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=30, unique=True) + comment = models.TextField(_(u"Comment"), blank=True, null=True) + available = models.BooleanField(_(u"Available")) + HELP_TEXT = u"" + + class Meta: + abstract = True + + def __unicode__(self): + return self.label + + @classmethod + def get_help(cls, dct={}): + help_text = cls.HELP_TEXT + c_rank = -1 + help_items = u"\n" + for item in cls.get_types(dct=dct, instances=True): + if not item.comment: + continue + if c_rank > item.rank: + help_items += u"</dl>\n" + elif c_rank < item.rank: + help_items += u"<dl>\n" + c_rank = item.rank + help_items += u"<dt>%s</dt><dd>%s</dd>" % (item.label, + u"<br/>".join(item.comment.split('\n'))) + c_rank += 1 + if c_rank: + help_items += c_rank*u"</dl>" + return mark_safe(help_text + help_items) + + @classmethod + def get_types(cls, dct={}, instances=False): + base_dct = dct.copy() + if hasattr(cls, 'parent'): + return cls._get_parent_types(base_dct, instances) + return cls._get_types(base_dct, instances) + + @classmethod + def _get_types(cls, dct={}, instances=False): + dct['available'] = True + if not instances: + yield ('', '--') + for item in cls.objects.filter(**dct).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): + prefix += 1 + dct['parent'] = item + childs = cls.objects.filter(**dct) + 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): + yield sub_child + + @classmethod + def _get_parent_types(cls, dct={}, instances=False): + dct['available'] = True + if not instances: + yield ('', '--') + dct['parent'] = None + items = cls.objects.filter(**dct) + 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): + yield child + +class HistoryError(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class BaseHistorizedItem(models.Model): + history_modifier = models.ForeignKey(User, related_name='+', + verbose_name=_(u"Last modifier")) + class Meta: + abstract = True + + def save(self, *args, **kwargs): + assert hasattr(self, 'history_modifier') == True + 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 + + def rollback(self, date): + """ + Rollback to a previous state + """ + to_del, new_item = [], None + for item in self.history.all(): + to_del.append(item) + if item.history_date == date: + new_item = item + break + 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)) + 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 + +class LightHistorizedItem(BaseHistorizedItem): + history_date = models.DateTimeField(default=datetime.datetime.now) + class Meta: + abstract = True + +class Departement(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"Departement") + verbose_name_plural = _(u"Departements") + ordering = ['number'] + + def __unicode__(self): + return unicode(self.number) + JOINT + 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=30, 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) + history = HistoricalRecords() + + class Meta: + abstract = True + +class OrganizationType(GeneralType): + class Meta: + verbose_name = _(u"Organization type") + verbose_name_plural = _(u"Organization types") + +class Organization(Address, OwnPerms): + name = models.CharField(_(u"Name"), max_length=100) + organization_type = models.ForeignKey(OrganizationType, + verbose_name=_(u"Type")) + history = HistoricalRecords() + class Meta: + verbose_name = _(u"Organization") + verbose_name_plural = _(u"Organizations") + permissions = ( + ("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 + +class PersonType(GeneralType): + class Meta: + verbose_name = _(u"Person type") + verbose_name_plural = _(u"Person types") + +class Person(Address, OwnPerms) : + TYPE = (('Mr', _(u'Mr')), + ('Ms', _(u'Miss')), + ('Md', _(u'Mrs')), + ('Dr', _(u'Doctor')), + ) + title = models.CharField(_(u"Title"), max_length=2, choices=TYPE) + surname = models.CharField(_(u"Surname"), max_length=20) + name = models.CharField(_(u"Name"), max_length=30) + email = models.CharField(_(u"Email"), max_length=40, blank=True, null=True) + person_type = models.ForeignKey(PersonType, verbose_name=_(u"Type")) + attached_to = models.ForeignKey('Organization', + verbose_name=_(u"Is attached to"), blank=True, null=True) + is_author = models.NullBooleanField(_(u"Is an author?"), blank=True, + null=True) + in_charge_storage = models.NullBooleanField(_(u"In charge of a storage?"), + blank=True, null=True) + + class Meta: + verbose_name = _(u"Person") + verbose_name_plural = _(u"Persons") + permissions = ( + ("view_person", ugettext(u"Can view 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): + lbl = u"%s %s" % (self.name, self.surname) + lbl += JOINT + if self.attached_to: + lbl += unicode(self.attached_to) + elif self.email: + lbl += self.email + return lbl + + def full_label(self): + return u" ".join([unicode(getattr(self, attr)) + for attr in ('title', 'surname', 'name', 'attached_to') + if getattr(self, attr)]) + +class IshtarUser(User): + person = models.ForeignKey(Person, verbose_name=_(u"Person"), unique=True) + + class Meta: + verbose_name = _(u"Ishtar user") + verbose_name_plural = _(u"Ishtar users") + +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")) + 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) + JOINT + unicode(self.author_type) + +class SourceType(GeneralType): + class Meta: + verbose_name = _(u"Source type") + verbose_name_plural = _(u"Source types") + +class Source(models.Model): + title = models.CharField(_(u"Title"), max_length=200) + source_type = models.ForeignKey(SourceType, verbose_name=_(u"Type")) + authors = models.ManyToManyField(Author, verbose_name=_(u"Authors")) + + class Meta: + abstract = True + + def __unicode__(self): + return self.title + +class FileType(GeneralType): + class Meta: + verbose_name = _(u"Archaeological file type") + verbose_name_plural = _(u"Archaeological file types") + + @classmethod + def is_preventive(cls, file_type_id, key=''): + key = key or 'preventive' + try: + preventive = FileType.objects.get(txt_idx=key).pk + return file_type_id == preventive + except ObjectDoesNotExist: + return False + +class PermitType(GeneralType): + class Meta: + verbose_name = _(u"Permit type") + verbose_name_plural = _(u"Permit types") + +if settings.COUNTRY == 'fr': + class SaisineType(GeneralType): + delay = models.IntegerField(_(u"Delay (in days)")) + class Meta: + verbose_name = u"Type Saisine" + verbose_name_plural = u"Types Saisine" + +class File(BaseHistorizedItem, OwnPerms): + TABLE_COLS = ['numeric_reference', 'year', 'internal_reference', + 'file_type', 'saisine_type', 'towns', ] + year = models.IntegerField(_(u"Year"), + default=lambda:datetime.datetime.now().year) + numeric_reference = models.IntegerField(_(u"Numeric reference")) + internal_reference = models.CharField(_(u"Internal reference"), + max_length=60, unique=True) + file_type = models.ForeignKey(FileType, verbose_name=_(u"File type")) + in_charge = models.ForeignKey(Person, related_name='+', + verbose_name=_(u"Person in charge")) + general_contractor = models.ForeignKey(Person, related_name='+', + verbose_name=_(u"General contractor"), blank=True, null=True) + town_planning_service = models.ForeignKey(Organization, related_name='+', + verbose_name=_(u"Town planning service"), blank=True, null=True) + permit_type = models.ForeignKey(PermitType, verbose_name=_(u"Permit type"), + blank=True, null=True) + permit_reference = models.CharField(_(u"Permit reference"), + max_length=60, blank=True, null=True) + is_active = models.BooleanField(_(u"Is active?"), default=True) + towns = models.ManyToManyField("Town", verbose_name=_(u"Towns")) + creation_date = models.DateField(_(u"Creation date"), + default=datetime.date.today) + reception_date = models.DateField(_(u'Reception date'), blank=True, + null=True) + related_file = models.ForeignKey("File", verbose_name=_(u"Related file"), + blank=True, null=True) + if settings.COUNTRY == 'fr': + saisine_type = models.ForeignKey(SaisineType, blank=True, null=True, + verbose_name= u"Type de saisine") + reference_number = models.IntegerField(_(u"Reference number"), + blank=True, null=True) + total_surface = models.IntegerField(_(u"Total surface (m²)"), + blank=True, null=True) + total_developed_surface = models.IntegerField( + _(u"Total developed surface (m²)"), blank=True, null=True) + address = models.TextField(_(u"Main address"), null=True, blank=True) + address_complement = models.TextField(_(u"Main address - complement"), + null=True, blank=True) + postal_code = models.CharField(_(u"Main address - postal code"), + max_length=10, null=True, blank=True) + comment = models.TextField(_(u"Comment")) + history = HistoricalRecords() + + class Meta: + verbose_name = _(u"Archaeological file") + verbose_name_plural = _(u"Archaeological files") + permissions = ( + ("view_own_file", ugettext(u"Can view own Archaelogical file")), + ("add_own_file", ugettext(u"Can add own Archaelogical file")), + ("change_own_file", ugettext(u"Can change own Archaelogical file")), + ("delete_own_file", ugettext(u"Can delete own Archaelogical file")), + ) + ordering = ['-year', '-numeric_reference'] + + def __unicode__(self): + items = [unicode(_('Intercommunal'))] + if self.towns.count() == 1: + items[0] = unicode(self.towns.all()[0]) + items.append("-".join((unicode(self.year), + unicode(self.numeric_reference)))) + items += [unicode(getattr(self, k))[:36] + for k in ['internal_reference',] if getattr(self, k)] + return JOINT.join(items) + + @classmethod + def get_query_owns(cls, user): + return Q(history_modifier=user) & Q(is_active=True) + + def closing(self): + if self.is_active: + return + for item in self.history.all(): + if item.is_active(): + break + closing_item = item + return {'date':item.history_date, 'user':item.history_modifier} + + def total_surface_ha(self): + if self.total_surface: + return self.total_surface/10000.0 + + def total_developed_surface_ha(self): + if self.total_developed_surface: + return self.total_developed_surface/10000.0 + + def operation_acts(self): + acts = [] + for ope in self.operations.all(): + for act in ope.administrative_act.all(): + acts.append(act) + return acts + + def is_preventive(self): + return FileType.is_preventive(self.file_type.pk) + +class OperationType(GeneralType): + class Meta: + verbose_name = _(u"Operation type") + verbose_name_plural = _(u"Operation types") + + @classmethod + def is_preventive(cls, ope_type_id, key=''): + key = key or 'prev_excavation' + try: + preventive = OperationType.objects.get(txt_idx=key).pk + return ope_type_id == preventive + except ObjectDoesNotExist: + return False + +class RemainType(GeneralType): + class Meta: + verbose_name = _(u"Remain type") + verbose_name_plural = _(u"Remain types") + +class Operation(BaseHistorizedItem, OwnPerms): + TABLE_COLS = ['operation_code', 'year', 'operation_type', + 'remains', 'towns', 'associated_file', 'start_date'] + start_date = models.DateField(_(u"Start date"), null=True, blank=True) + end_date = models.DateField(_(u"Closing date"), null=True, blank=True) + in_charge = models.ForeignKey('Person', related_name='+', null=True, + blank=True, verbose_name=_(u"In charge")) + year = models.IntegerField(_(u"Year")) + operation_code = models.IntegerField(_(u"Operation code")) + associated_file = models.ForeignKey(File, related_name='operations', + verbose_name=_(u"File"), blank=True, null=True) + operation_type = models.ForeignKey(OperationType, related_name='+', + verbose_name=_(u"Operation type")) + surface = models.IntegerField(_(u"Surface (m²)"), blank=True, null=True) + remains = models.ManyToManyField("RemainType", verbose_name=_(u'Remains')) + towns = models.ManyToManyField("Town", verbose_name=_(u"Towns")) + cost = models.IntegerField(_(u"Cost (€)"), blank=True, null=True) + periods = models.ManyToManyField('Period', verbose_name=_(u"Periods")) + if settings.COUNTRY == 'fr': + code_patriarche = models.IntegerField(u"Code PATRIARCHE", null=True, + blank=True) + code_dracar = models.CharField(u"Code DRACAR", max_length=10, null=True, + blank=True) + fnap_financing = models.FloatField(u"Financement FNAP", + blank=True, null=True) + TABLE_COLS += ["code_patriarche"] + zoning_prescription = models.NullBooleanField( + _(u"Prescription on zoning"), blank=True, null=True) + large_area_prescription = models.NullBooleanField( + _(u"Prescription on large area"), blank=True, null=True) + geoarchaeological_context_prescription = models.NullBooleanField( + _(u"Prescription on geoarchaeological context"), blank=True, null=True) + comment = models.TextField(_(u"Comment"), null=True, blank=True) + history = HistoricalRecords() + + class Meta: + verbose_name = _(u"Operation") + verbose_name_plural = _(u"Operations") + permissions = ( + ("view_own_operation", ugettext(u"Can view own Operation")), + ("add_own_operation", ugettext(u"Can add own Operation")), + ("change_own_operation", ugettext(u"Can change own Operation")), + ("delete_own_operation", ugettext(u"Can delete own Operation")), + ) + + def __unicode__(self): + items = [unicode(_('Intercommunal'))] + if self.towns.count() == 1: + items[0] = unicode(self.towns.all()[0]) + items.append("-".join((unicode(self.year), + unicode(self.operation_code)))) + return JOINT.join(items) + + def is_own(self, person): + return False + + @classmethod + def get_query_owns(cls, user): + return Q(in_charge=user.person)|Q(history_modifier=user)\ + & Q(end_date__isnull=True) + + def is_active(self): + return not bool(self.end_date) + + def closing(self): + if self.is_active(): + return + for item in self.history.all(): + if not item.end_date: + break + return {'date':item.history_date, + 'user':IshtarUser.objects.get(pk=item.history_modifier_id)} + +class OperationSource(Source): + class Meta: + verbose_name = _(u"Operation documentation") + verbose_name_plural = _(u"Operation documentations") + operation = models.ForeignKey(Operation, verbose_name=_(u"Operation"), + related_name="source") + +class Parcel(LightHistorizedItem): + associated_file = models.ForeignKey(File, related_name='parcels', + blank=True, null=True, verbose_name=_(u"File")) + operation = models.ForeignKey(Operation, related_name='parcels', blank=True, + null=True, verbose_name=_(u"Operation")) + year = models.IntegerField(_(u"Year"), + default=lambda:datetime.datetime.now().year) + town = models.ForeignKey("Town", related_name='parcels', + verbose_name=_(u"Town")) + section = models.CharField(_(u"Section"), max_length=4) + parcel_number = models.CharField(_(u"Parcel number"), max_length=6) + + class Meta: + verbose_name = _(u"Parcel") + verbose_name_plural = _(u"Parcels") + + def short_label(self): + return JOINT.join([unicode(item) for item in [self.section, + self.parcel_number] if item]) + + def __unicode__(self): + return self.short_label() + + def long_label(self): + return JOINT.join([unicode(item) for item in \ + [self.associated_file, self.operation, self.section, self.parcel_number] + if item]) + +class Period(GeneralType) : + order = models.IntegerField(_(u"Order")) + start_date = models.IntegerField(_(u"Start date")) + end_date = models.IntegerField(_(u"End date")) + parent = models.ForeignKey("Period", verbose_name=_(u"Parent period"), + blank=True, null=True) + + class Meta: + verbose_name = _(u"Type Period") + verbose_name_plural = _(u"Types Period") + + def __unicode__(self): + return self.label + +class DatingType(GeneralType): + class Meta: + verbose_name = _(u"Dating type") + verbose_name_plural = _(u"Dating types") + +class DatingQuality(GeneralType): + class Meta: + verbose_name = _(u"Dating quality") + verbose_name_plural = _(u"Dating qualities") + +class Dating(models.Model): + period = models.ForeignKey(Period, verbose_name=_(u"Period")) + start_date = models.IntegerField(_(u"Start date"), blank=True, null=True) + end_date = models.IntegerField(_(u"End date"), blank=True, null=True) + dating_type = models.ForeignKey(DatingType, verbose_name=_(u"Dating type"), + blank=True, null=True) + quality = models.ForeignKey(DatingQuality, verbose_name=_(u"Quality"), + blank=True, null=True) + + class Meta: + verbose_name = _(u"Dating") + verbose_name_plural = _(u"Datings") + + def __unicode__(self): + start_date = self.start_date and unicode(self.start_date) or u"" + end_date = self.end_date and unicode(self.end_date) or u"" + if not start_date and not end_date: + return unicode(self.period) + return u"%s (%s-%s)" % (self.period, start_date, end_date) + +class Unit(GeneralType): + order = models.IntegerField(_(u"Order")) + parent = models.ForeignKey("Unit", verbose_name=_(u"Parent unit"), + blank=True, null=True) + + class Meta: + verbose_name = _(u"Type Unit") + verbose_name_plural = _(u"Types Unit") + + def __unicode__(self): + return self.label + +class ActivityType(GeneralType): + order = models.IntegerField(_(u"Order")) + + class Meta: + verbose_name = _(u"Type Activity") + verbose_name_plural = _(u"Types Activity") + + def __unicode__(self): + return self.label + +class IdentificationType(GeneralType): + order = models.IntegerField(_(u"Order")) + class Meta: + verbose_name = _(u"Type Identification") + verbose_name_plural = _(u"Types Identification") + + def __unicode__(self): + return self.label + +class ContextRecord(BaseHistorizedItem, OwnPerms): + TABLE_COLS = ['parcel.town', 'parcel.operation.year', + 'parcel.operation.operation_code', + 'label', 'unit'] + if settings.COUNTRY == 'fr': + TABLE_COLS.insert(1, 'parcel.operation.code_patriarche') + parcel = models.ForeignKey(Parcel, verbose_name=_(u"Parcel"), + related_name='context_record') + operation = models.ForeignKey(Operation, verbose_name=_(u"Operation"), + related_name='context_record') + label = models.CharField(_(u"ID"), max_length=200) + description = models.TextField(_("Description"), blank=True, null=True) + length = models.IntegerField(_(u"Length (cm)"), blank=True, null=True) + width = models.IntegerField(_(u"Width (cm)"), blank=True, null=True) + thickness = models.IntegerField(_(u"Thickness (cm)"), blank=True, null=True) + depth = models.IntegerField(_(u"Depth (cm)"), blank=True, null=True) + location = models.CharField(_(u"Location"), blank=True, null=True, + max_length=200, + help_text=_(u"A short description of the location of the context record")) + datings = models.ManyToManyField(Dating) + unit = models.ForeignKey(Unit, verbose_name=_(u"Unit"), related_name='+', + blank=True, null=True) + has_furniture = models.NullBooleanField(u"Has furniture?", blank=True, + null=True) + filling = models.TextField(_(u"Filling"), blank=True, null=True) + interpretation = models.TextField(_(u"Interpretation"), blank=True, + null=True) + taq = models.IntegerField(_(u"TAQ"), blank=True, null=True, + help_text=_("\"Terminus Ante Quem\" the context record can't have been " + "created after this date")) + taq_estimated = models.IntegerField(_(u"Estimated TAQ"), blank=True, + null=True, help_text=_("Estimation of a \"Terminus Ante Quem\"")) + tpq = models.IntegerField(_(u"TPQ"), blank=True, null=True, + help_text=_("\"Terminus Post Quem\" the context record can't have been " + " created before this date")) + tpq_estimated = models.IntegerField(_(u"Estimated TPQ"), blank=True, + null=True, help_text=_("Estimation of a \"Terminus Post Quem\"")) + identification = models.ForeignKey(IdentificationType, blank=True, + null=True, verbose_name=_(u"Identification"),) + activity = models.ForeignKey(ActivityType,blank=True, null=True, + verbose_name=_(u"Activity"),) + history = HistoricalRecords() + + class Meta: + verbose_name = _(u"Context Record") + verbose_name_plural = _(u"Context Record") + permissions = ( + ("view_own_contextrecord", ugettext(u"Can view own Context Record")), + ("add_own_contextrecord", ugettext(u"Can add own Context Record")), + ("change_own_contextrecord", ugettext(u"Can change own Context Record")), + ("delete_own_contextrecord", ugettext(u"Can delete own Context Record")), + ) + + def __unicode__(self): + return JOINT.join((unicode(self.parcel), self.label)) + + def full_label(self): + if not self.parcel.operation: + return unicode(self) + return self._real_label() or self._temp_label() + + def _real_label(self): + if not self.parcel.operation.code_patriarche: + return + return JOINT.join((self.parcel.operation.code_patriarche, + self.label)) + + def _temp_label(self): + if self.parcel.operation.code_patriarche: + return + return JOINT.join([unicode(lbl) for lbl in [self.parcel.operation.year, + self.parcel.operation.operation_code, + self.label] if lbl]) +class ContextRecordSource(Source): + class Meta: + verbose_name = _(u"Context record documentation") + verbose_name_plural = _(u"Context record documentations") + context_record = models.ForeignKey(ContextRecord, + verbose_name=_(u"Context record"), related_name="source") + +class MaterialType(GeneralType): + recommendation = models.TextField(_(u"Recommendation")) + parent = models.ForeignKey("MaterialType", blank=True, null=True, + verbose_name=_(u"Parent material")) + + class Meta: + verbose_name = _(u"Material type") + verbose_name_plural = _(u"Material types") + +class BaseItem(BaseHistorizedItem, OwnPerms): + label = models.CharField(_(u"ID"), max_length=60) + description = models.TextField(_(u"Description")) + context_record = models.ForeignKey(ContextRecord, + related_name='base_items', verbose_name=_(u"Context Record")) + is_isolated = models.NullBooleanField(_(u"Is isolated?"), blank=True, + null=True) + index = models.IntegerField(u"Index", default=0) + material_index = models.IntegerField(u"Material index", default=0) + history = HistoricalRecords() + + class Meta: + verbose_name = _(u"Base item") + verbose_name_plural = _(u"Base items") + permissions = ( + ("view_own_baseitem", ugettext(u"Can view own Base item")), + ("add_own_baseitem", ugettext(u"Can add own Base item")), + ("change_own_baseitem", ugettext(u"Can change own Base item")), + ("delete_own_baseitem", ugettext(u"Can delete own Base item")), + ) + + def __unicode__(self): + return self.label + + def get_last_item(self): + #TODO: manage virtuals - property(last_item) ? + items = self.item.filter().order_by("-order").all() + return items and items[0] + + def full_label(self): + return self._real_label() or self._temp_label() + + def material_type_label(self): + item = self.get_last_item() + lbl = item and (unicode(item.material_type) + unicode(_(":"))) or '' + if self.context_record.parcel.operation.code_patriarche: + return lbl + JOINT.join([unicode(it) for it in ( + self.context_record.parcel.operation.code_patriarche, + self.context_record.label, + self.material_index, + self.label)]) + return lbl + JOINT.join([unicode(it) for it in ( + self.context_record.parcel.year, + self.index, + self.context_record.label, + self.material_index, + self.label)]) + + + def _real_label(self): + if not self.context_record.parcel.operation.code_patriarche: + return + return JOINT.join([unicode(it) for it in ( + self.context_record.parcel.operation.code_patriarche, + self.context_record.label, + self.label)]) + + def _temp_label(self): + if self.context_record.parcel.operation.code_patriarche: + return + return JOINT.join([unicode(it) for it in ( + self.context_record.parcel.year, + self.index, + self.context_record.label, + self.label)]) +class Item(BaseHistorizedItem, OwnPerms): + TABLE_COLS = ['base_items.context_record.parcel.town', + 'base_items.context_record.parcel.operation.year', + 'base_items.context_record.parcel.operation.operation_code', + 'label', 'material_type', 'dating.period', + 'base_items.is_isolated'] + if settings.COUNTRY == 'fr': + TABLE_COLS.insert(1, + 'base_items.context_record.parcel.operation.code_patriarche') + base_items = models.ManyToManyField(BaseItem, verbose_name=_(u"Base item"), + related_name='item') + order = models.IntegerField(_(u"Order")) + label = models.CharField(_(u"ID"), max_length=60) + description = models.TextField(_(u"Description"), blank=True, null=True) + material_type = models.ForeignKey(MaterialType, + verbose_name = _(u"Material type")) + volume = models.FloatField(_(u"Volume (l)"), blank=True, null=True) + weight = models.FloatField(_(u"Weight (g)"), blank=True, null=True) + item_number = models.IntegerField(_("Item number"), blank=True, null=True) + upstream_treatment = models.ForeignKey("Treatment", blank=True, null=True, + related_name='downstream_treatment', verbose_name=_("Upstream treatment")) + downstream_treatment = models.ForeignKey("Treatment", blank=True, null=True, + related_name='upstream_treatment', verbose_name=_("Downstream treatment")) + dating = models.ForeignKey(Dating, verbose_name=_(u"Dating")) + history = HistoricalRecords() + + class Meta: + verbose_name = _(u"Item") + verbose_name_plural = _(u"Items") + permissions = ( + ("view_own_item", ugettext(u"Can view own Item")), + ("add_own_item", ugettext(u"Can add own Item")), + ("change_own_item", ugettext(u"Can change own Item")), + ("delete_own_item", ugettext(u"Can delete own Item")), + ) + + def __unicode__(self): + return self.label + + def save(self, *args, **kwargs): + if not self.pk: + super(Item, self).save(*args, **kwargs) + for base_item in self.base_items.all(): + if not base_item.index: + idx = BaseItem.objects.filter(context_record=\ + base_item.context_record).aggregate(Max('index')) + base_item.index = idx and idx['index__max'] + 1 or 1 + if not base_item.material_index: + idx = BaseItem.objects.filter(context_record=\ + base_item.context_record, + item__material_type=self.material_type).aggregate( + Max('material_index')) + base_item.material_index = idx and \ + idx['material_index__max'] + 1 or 1 + base_item.save() + super(Item, self).save(*args, **kwargs) + +class ItemSource(Source): + class Meta: + verbose_name = _(u"Item documentation") + verbose_name_plural = _(u"Item documentations") + item = models.ForeignKey(Item, verbose_name=_(u"Item"), + related_name="source") + +class ParcelOwner(LightHistorizedItem): + owner = models.ForeignKey(Person, verbose_name=_(u"Owner")) + parcel = models.ForeignKey(Parcel, verbose_name=_(u"Parcel")) + start_date = models.DateField(_(u"Start date")) + end_date = models.DateField(_(u"End date")) + + class Meta: + verbose_name = _(u"Parcel owner") + verbose_name_plural = _(u"Parcel owners") + + def __unicode__(self): + return self.owner + JOINT + self.parcel + +class WarehouseType(GeneralType): + class Meta: + verbose_name = _(u"Warehouse type") + verbose_name_plural = _(u"Warehouse types") + +class Warehouse(Address, OwnPerms): + name = models.CharField(_(u"Name"), max_length=40) + warehouse_type = models.ForeignKey(WarehouseType, + verbose_name=_(u"Warehouse type")) + person_in_charge = models.ForeignKey(Person, + verbose_name=_(u"Person in charge"), null=True, blank=True) + comment = models.TextField(_(u"Comment"), null=True, blank=True) + + class Meta: + verbose_name = _(u"Warehouse") + verbose_name_plural = _(u"Warehouses") + permissions = ( + ("view_own_warehouse", ugettext(u"Can view own Warehouse")), + ("add_own_warehouse", ugettext(u"Can add own Warehouse")), + ("change_own_warehouse", ugettext(u"Can change own Warehouse")), + ("delete_own_warehouse", ugettext(u"Can delete own Warehouse")), + ) + + def __unicode__(self): + return u"%s (%s)" % (self.name, unicode(self.warehouse_type)) + +class ActType(GeneralType): + TYPE = (('F', _(u'Archaelogical file')), + ('O', _(u'Operation')), + ) + intented_to = models.CharField(_(u"Intended to"), max_length=1, + choices=TYPE) + class Meta: + verbose_name = _(u"Act type") + verbose_name_plural = _(u"Act types") + +class AdministrativeAct(BaseHistorizedItem, OwnPerms): + TABLE_COLS = ['act_type', 'associated_file', 'operation', + 'associated_file.towns', 'operation.towns'] + TABLE_COLS_FILE = ['act_type', 'associated_file', 'associated_file.towns',] + TABLE_COLS_OPE = ['act_type', 'operation', 'operation.towns'] + act_type = models.ForeignKey(ActType, verbose_name=_(u"Act type")) + in_charge = models.ForeignKey(Person, blank=True, null=True, + related_name='+', verbose_name=_(u"Person in charge of the operation")) + operator = models.ForeignKey(Organization, blank=True, null=True, + verbose_name=_(u"Archaeological preventive operator")) + scientific = models.ForeignKey(Person, blank=True, null=True, +related_name='+', verbose_name=_(u"Person in charge of the scientific part")) + signatory = models.ForeignKey(Person, blank=True, null=True, + related_name='+', verbose_name=_(u"Signatory")) + operation = models.ForeignKey(Operation, blank=True, null=True, + related_name='administrative_act', verbose_name=_(u"Operation")) + associated_file = models.ForeignKey(File, blank=True, null=True, + related_name='administrative_act', verbose_name=_(u"Archaelogical file")) + signature_date = models.DateField(_(u"Signature date")) + act_object = models.CharField(_(u"Object"), max_length=200) + if settings.COUNTRY == 'fr': + ref_sra = models.CharField(u"Référence SRA", max_length=15) + history = HistoricalRecords() + + class Meta: + verbose_name = _(u"Administrative act") + verbose_name_plural = _(u"Administrative acts") + permissions = ( +("view_own_administrativeact", ugettext(u"Can view own Administrative act")), +("add_own_administrativeact", ugettext(u"Can add own Administrative act")), +("change_own_administrativeact", ugettext(u"Can change own Administrative act")), +("delete_own_administrativeact", ugettext(u"Can delete own Administrative act")), + ) + + def __unicode__(self): + return JOINT.join([unicode(item) + for item in [self.operation, self.associated_file, self.act_object] + if item]) + +class ContainerType(GeneralType): + length = models.IntegerField(_(u"Length (mm)")) + width = models.IntegerField(_(u"Width (mm)")) + height = models.IntegerField(_(u"Height (mm)")) + volume = models.IntegerField(_(u"Volume (l)")) + reference = models.CharField(_(u"Reference"), max_length=30) + + class Meta: + verbose_name = _(u"Container type") + verbose_name_plural = _(u"Container types") + +class Container(LightHistorizedItem): + location = models.ForeignKey(Warehouse, verbose_name=_(u"Location")) + container_type = models.ForeignKey(ContainerType, + verbose_name=_("Container type")) + reference = models.CharField(_(u"Reference"), max_length=40) + comment = models.TextField(_(u"Comment")) + + class Meta: + verbose_name = _(u"Container") + verbose_name_plural = _(u"Containers") + +if settings.COUNTRY == 'fr': + class Arrondissement(models.Model): + name = models.CharField(u"Nom", max_length=30) + department = models.ForeignKey(Departement, verbose_name=u"Département") + + def __unicode__(self): + return 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 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(Departement, 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) + return self.name + +class TreatmentType(GeneralType): + virtual = models.BooleanField(_(u"Virtual")) + class Meta: + verbose_name = _(u"Treatment type") + verbose_name_plural = _(u"Treatment types") + +class Treatment(BaseHistorizedItem, OwnPerms): + container = models.ForeignKey(Container, verbose_name=_(u"Container"), + blank=True, null=True) + description = models.TextField(_(u"Description"), blank=True, null=True) + treatment_type = models.ForeignKey(TreatmentType, + verbose_name=_(u"Treatment type")) + location = models.ForeignKey(Warehouse, verbose_name=_(u"Location"), + blank=True, null=True) + person = models.ForeignKey(Person, verbose_name=_(u"Person"), + blank=True, null=True) + start_date = models.DateField(_(u"Start date"), blank=True, null=True) + end_date = models.DateField(_(u"End date"), blank=True, null=True) + history = HistoricalRecords() + + class Meta: + verbose_name = _(u"Treatment") + verbose_name_plural = _(u"Treatments") + permissions = ( + ("view_own_treatment", ugettext(u"Can view own Treatment")), + ("add_own_treatment", ugettext(u"Can add own Treatment")), + ("change_own_treatment", ugettext(u"Can change own Treatment")), + ("delete_own_treatment", ugettext(u"Can delete own Treatment")), + ) + +class TreatmentSource(Source): + class Meta: + verbose_name = _(u"Treatment documentation") + verbose_name_plural = _(u"Treament documentations") + treatment = models.ForeignKey(Treatment, verbose_name=_(u"Treatment"), + related_name="source") + +class Property(LightHistorizedItem): + item = models.ForeignKey(Item, verbose_name=_(u"Item")) + administrative_act = models.ForeignKey(AdministrativeAct, + verbose_name=_(u"Administrative act")) + person = models.ForeignKey(Person, verbose_name=_(u"Person")) + start_date = models.DateField(_(u"Start date")) + end_date = models.DateField(_(u"End date")) + + class Meta: + verbose_name = _(u"Property") + verbose_name_plural = _(u"Properties") + + def __unicode__(self): + return self.person + JOINT + self.item + diff --git a/ishtar/ishtar_base/templatetags/__init__.py b/ishtar/ishtar_base/templatetags/__init__.py new file mode 100644 index 000000000..792d60054 --- /dev/null +++ b/ishtar/ishtar_base/templatetags/__init__.py @@ -0,0 +1 @@ +# diff --git a/ishtar/ishtar_base/templatetags/range.py b/ishtar/ishtar_base/templatetags/range.py new file mode 100644 index 000000000..3b3a9097b --- /dev/null +++ b/ishtar/ishtar_base/templatetags/range.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from django.template import Library + +register = Library() + +@register.filter +def get_range(value): + return [val+1 for val in xrange(value)] diff --git a/ishtar/ishtar_base/templatetags/table_form.py b/ishtar/ishtar_base/templatetags/table_form.py new file mode 100644 index 000000000..7adb54d65 --- /dev/null +++ b/ishtar/ishtar_base/templatetags/table_form.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from django.template import Library + +register = Library() + +@register.inclusion_tag('form_snippet.html') +def table_form(form): + return {'form': form} diff --git a/ishtar/ishtar_base/tests.py b/ishtar/ishtar_base/tests.py new file mode 100644 index 000000000..5a433b381 --- /dev/null +++ b/ishtar/ishtar_base/tests.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2010-2011 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 <http://www.gnu.org/licenses/>. + +# See the file COPYING for details. + +""" +Unit tests +""" +import json + +from django.test import TestCase + +import models + +class FileTest(TestCase): + fixtures = ['user.json', 'person_type-fr.json', 'organization_type-fr.json', + 'treatment_type-fr.json'] + model = models.File + + def setUp(self): + self.extra_models, self.model_list = {}, [] + user = models.IshtarUser.objects.get(pk=1) + person_type = models.PersonType(label=u'Test person type', + txt_idx='test_person', available=True) + person_type.save() + self.extra_models['person_type'] = person_type + self.model_list.append(person_type) + + person = models.Person(title='Mr', surname='Surname', name='Name', + person_type=person_type, history_modifier=user) + person.save() + self.extra_models['person'] = person + self.model_list.append(person) + + file_type = models.FileType(label=u'Test file type', + txt_idx='test_file', available=True) + file_type.save() + self.extra_models['file_type'] = file_type + self.model_list.append(file_type) + + dct = {'year':2010, 'numeric_reference':1000, 'file_type':file_type, + 'internal_reference':u'UNIT_testÉ ?', 'in_charge':person, + 'history_modifier':user, 'total_surface':10000} + self.item = self.model(**dct) + self.item.save() + + def tearDown(self): + self.item.delete() + for item in reversed(self.model_list): + item.delete() + + def testAddAndGetHistorized(self): + """ + Test correct new version and correct access to history + """ + nb_hist = self.item.history.count() + self.assertTrue(self.item.history.count() >= 1) + base_label = self.item.internal_reference + self.item.internal_reference = u"Unité_Test" + self.item.save() + self.failUnlessEqual(self.item.history.count(), nb_hist+1) + self.failUnlessEqual(self.item.history.all()[1].internal_reference, + base_label) + + def testIntelligentHistorisation(self): + """ + Test that to identical version are not recorded twice in the history + """ + nb_hist = self.item.history.count() + self.item.internal_reference = u"Unité_Test" + self.item.save() + self.failUnlessEqual(self.item.history.count(), nb_hist+1) + nb_hist = self.item.history.count() + self.item.save() + self.failUnlessEqual(self.item.history.count(), nb_hist) + + def testRollbackFile(self): + nb_hist = self.item.history.count() + initial_values = self.item.values() + backup_date = self.item.history.all()[0].history_date + self.item.internal_reference = u"Unité_Test" + self.item.save() + self.item.rollback(backup_date) + self.failUnlessEqual(self.item.history.count(), nb_hist) + new_values = self.item.values() + for k in initial_values.keys(): + self.assertTrue(k in new_values) + self.assertEqual(new_values[k], initial_values[k]) + + def testRESTGetFile(self): + response = self.client.post('/get-file/', + {'numeric_reference':self.item.numeric_reference}) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content) + self.assertTrue('records' in data) + self.assertTrue(data['records'] == 1) + + def testRESTGetOldFile(self): + initial_ref = self.item.internal_reference + new_ref = u"Unité_Test_old_file" + new_ref = initial_ref != new_ref and new_ref or new_ref + u"extra" + self.item.internal_reference = new_ref + self.item.save() + response = self.client.post('/get-file/', + {'numeric_reference':self.item.numeric_reference, 'old':1}) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content) + self.assertTrue('records' in data) + self.assertTrue(data['records'] == 1) + self.assertEqual(data['rows'][0]['internal_reference'], initial_ref) + diff --git a/ishtar/ishtar_base/urls.py b/ishtar/ishtar_base/urls.py new file mode 100644 index 000000000..cea34508f --- /dev/null +++ b/ishtar/ishtar_base/urls.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2010-2011 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 <http://www.gnu.org/licenses/>. + +# See the file COPYING for details. + +from django.conf.urls.defaults import * + +from ishtar.urls import BASE_URL +from menus import menu +import forms_main as ishtar_forms + +urlpatterns, actions = [], [] + +urlpatterns = patterns('', + url(BASE_URL + r'person_creation/(?P<step>.+)$', + ishtar_forms.person_creation_wizard, name='person_creation'), + url(BASE_URL + r'person_modification/(?P<step>.+)$', + ishtar_forms.person_modification_wizard, name='person_modification'), + url(BASE_URL + r'account_management/(?P<step>.+)$', + ishtar_forms.account_management_wizard, name='account_management'), + url(BASE_URL + r'file_search/(?P<step>.+)$', + ishtar_forms.file_search_wizard, name='file_search'), + url(BASE_URL + r'file_creation/(?P<step>.+)$', + ishtar_forms.file_creation_wizard, name='file_creation'), + url(BASE_URL + r'file_modification/(?P<step>.+)$', + ishtar_forms.file_modification_wizard, name='file_modification'), + url(BASE_URL + r'file_deletion/(?P<step>.+)$', + ishtar_forms.file_deletion_wizard, name='file_deletion'), + url(BASE_URL + r'file_administrativeactfile/(?P<step>.+)$', +ishtar_forms.file_administrativeactfile_wizard, name='file_administrativeactfile'), + url(BASE_URL + r'file_administrativeactfile_modification/(?P<step>.+)$', + ishtar_forms.file_administrativeactfile_modification_wizard, + name='file_administrativeactfile_modification'), + url(BASE_URL + r'file_administrativeactfile_deletion/(?P<step>.+)$', + ishtar_forms.file_administrativeactfile_deletion_wizard, + name='file_administrativeactfile_deletion'), + url(BASE_URL + r'operation_search/(?P<step>.+)$', + ishtar_forms.operation_search_wizard, name='operation_search'), + url(BASE_URL + r'operation_creation/(?P<step>.+)$', + ishtar_forms.operation_creation_wizard, name='operation_creation'), + url(BASE_URL + r'operation_modification/(?P<step>.+)$', + ishtar_forms.operation_modification_wizard, name='operation_modification'), + url(BASE_URL + r'operation_closing/(?P<step>.+)$', + ishtar_forms.operation_closing_wizard, name='operation_closing'), + url(BASE_URL + r'operation_deletion/(?P<step>.+)$', + ishtar_forms.operation_deletion_wizard, name='operation_deletion'), + url(BASE_URL + r'operation_administrativeactop/(?P<step>.+)$', + ishtar_forms.operation_administrativeactop_wizard, + name='operation_administrativeactop'), + url(BASE_URL + r'operation_administrativeactop_modification/(?P<step>.+)$', + ishtar_forms.operation_administrativeactop_modification_wizard, + name='operation_administrativeactop_modification'), + url(BASE_URL + r'operation_administrativeactop_deletion/(?P<step>.+)$', + ishtar_forms.operation_administrativeactop_deletion_wizard, + name='operation_administrativeactop_deletion'), + url(BASE_URL + r'operation_source_add/(?P<step>.+)$', + ishtar_forms.operation_source_add_wizard, name='operation_source_add'), + url(BASE_URL + r'record_search/(?P<step>.+)$', + ishtar_forms.record_search_wizard, name='record_search'), + url(BASE_URL + r'record_creation/(?P<step>.+)$', + ishtar_forms.record_creation_wizard, name='record_creation'), + url(BASE_URL + r'record_modification/(?P<step>.+)$', + ishtar_forms.record_modification_wizard, name='record_modification'), + url(BASE_URL + r'record_deletion/(?P<step>.+)$', + ishtar_forms.record_deletion_wizard, name='record_deletion'), + url(BASE_URL + r'item_search/(?P<step>.+)$', + ishtar_forms.item_search_wizard, name='item_search'), + url(BASE_URL + r'item_creation/(?P<step>.+)$', + ishtar_forms.item_creation_wizard, name='item_creation'), + url(BASE_URL + r'item_modification/(?P<step>.+)$', + ishtar_forms.item_modification_wizard, name='item_modification'), + url(BASE_URL + r'treatment_creation/(?P<step>.+)$', + ishtar_forms.treatment_creation_wizard, name='treatment_creation'), + ) +for section in menu.childs: + for menu_item in section.childs: + if hasattr(menu_item, 'childs'): + for menu_subitem in menu_item.childs: + actions.append(menu_subitem.idx) + else: + actions.append(menu_item.idx) +actions = r"|".join(actions) + +urlpatterns += patterns('ishtar.ishtar_base.views', + url(BASE_URL + r'(?P<action_slug>' + actions + r')/$', 'action', + name='action'), + url(BASE_URL + r'autocomplete-person/([0-9_]+)?$', 'autocomplete_person', + name='autocomplete-person'), + url(BASE_URL + r'autocomplete-town/$', 'autocomplete_town', + name='autocomplete-town'), + url(BASE_URL + r'autocomplete-author/$', 'autocomplete_author', + name='autocomplete-author'), + url(BASE_URL + r'autocomplete-organization/([0-9_]+)?$', + 'autocomplete_organization', + name='autocomplete-organization'), + url(BASE_URL + r'autocomplete-file/$', 'autocomplete_file', + name='autocomplete-file'), + url(BASE_URL + r'autocomplete-warehouse/$', 'autocomplete_warehouse', + name='autocomplete-warehouse'), + url(BASE_URL + r'get-file/(?P<type>.+)?$', 'get_file', + name='get-file'), + url(BASE_URL + r'show-file/(?P<pk>.+)?/(?P<type>.+)?$', 'show_file', + name='show-file'), + url(BASE_URL + r'show-historized-file/(?P<pk>.+)?/(?P<date>.+)?$', + 'show_file', name='show-historized-file'), + url(BASE_URL + r'revert-file/(?P<pk>.+)/(?P<date>.+)$', + 'revert_file', name='revert-file'), + url(BASE_URL + r'autocomplete-operation/$', 'autocomplete_operation', + name='autocomplete-operation'), + url(BASE_URL + r'get-operation/(?P<type>.+)?$', 'get_operation', + name='get-operation'), + url(BASE_URL + r'revert-operation/(?P<pk>.+)/(?P<date>.+)$', + 'revert_operation', name='revert-operation'), + url(BASE_URL + r'show-operation/(?P<pk>.+)?/(?P<type>.+)?$', + 'show_operation', + name='show-operation'), + url(BASE_URL + r'show-contextrecord/(?P<pk>.+)?/(?P<type>.+)?$', + 'show_contextrecord', + name='show-contextrecord'), + url(BASE_URL + r'update-current-item/$', 'update_current_item', + name='update-current-item'), + url(BASE_URL + r'get-administrativeactfile/(?P<type>.+)?$', + 'get_administrativeactfile', name='get-administrativeactfile'), + url(BASE_URL + r'get-administrativeactop/(?P<type>.+)?$', + 'get_administrativeactop', name='get-administrativeactop'), + url(BASE_URL + r'get-contextrecord/(?P<type>.+)?$', 'get_contextrecord', + name='get-contextrecord'), + url(BASE_URL + r'get-item/(?P<type>.+)?$', 'get_archaeologicalitem', + name='get-item'), + url(BASE_URL + r'new-warehouse/(?P<parent_name>.+)?/$', + 'new_warehouse', name='new-warehouse'), + url(BASE_URL + r'new-person/(?P<parent_name>.+)?/$', + 'new_person', name='new-person'), + url(BASE_URL + r'new-author/(?P<parent_name>.+)?/$', + 'new_author', name='new-author'), + url(BASE_URL + r'new-organization/(?P<parent_name>.+)?/$', + 'new_organization', name='new-organization'), +) diff --git a/ishtar/ishtar_base/views.py b/ishtar/ishtar_base/views.py new file mode 100644 index 000000000..9e998c2dc --- /dev/null +++ b/ishtar/ishtar_base/views.py @@ -0,0 +1,581 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2010-2011 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 <http://www.gnu.org/licenses/>. + +# See the file COPYING for details. + +""" +Furnitures views +""" + +import tidy +import re +import csv +import json +import datetime +import optparse +import cStringIO as StringIO +from tempfile import NamedTemporaryFile +import ho.pisa as pisa + +from django.http import HttpResponse, Http404 +from django.template import RequestContext, loader +from django.template.defaultfilters import slugify +from django.shortcuts import render_to_response, redirect +from django.utils.translation import ugettext, ugettext_lazy as _ +from django.core.exceptions import ObjectDoesNotExist +from django.core.urlresolvers import reverse, NoReverseMatch +from django.db.models import Q +from django.core import serializers + +from ishtar import settings +if settings.XHTML2ODT_PATH: + import sys + sys.path.append(settings.XHTML2ODT_PATH) + from xhtml2odt import xhtml2odt + +from menus import menu +import forms_main as ishtar_forms +import models + +CSV_OPTIONS = {'delimiter':';', 'quotechar':'"', 'quoting':csv.QUOTE_ALL} +ENCODING = settings.ENCODING or 'utf-8' + +def index(request): + """ + Main page + """ + dct = {} + return render_to_response('index.html', dct, + context_instance=RequestContext(request)) + +def update_current_item(request): + if not request.is_ajax() and not request.method == 'POST': + raise Http404 + if 'value' in request.POST and 'item' in request.POST: + request.session[request.POST['item']] = request.POST['value'] + return HttpResponse('ok') + +def check_permission(request, action_slug, obj_id=None): + if action_slug not in menu.items: + #! TODO + return True + if obj_id: + return menu.items[action_slug].is_available(request.user, obj_id) + return menu.items[action_slug].can_be_available(request.user) + +def autocomplete_person(request, person_type=None): + if not request.user.has_perm('ishtar_base.view_person', models.Person) and \ + not request.user.has_perm('ishtar_base.view_own_person', models.Person) : + return HttpResponse(mimetype='text/plain') + if not request.GET.get('term'): + return HttpResponse(mimetype='text/plain') + q = request.GET.get('term') + limit = request.GET.get('limit', 20) + try: + limit = int(limit) + except ValueError: + return HttpResponseBadRequest() + query = Q() + for q in q.split(' '): + query = query & (Q(name__icontains=q) | Q(surname__icontains=q) | \ + Q(email__icontains=q)) + if person_type: + try: + typs = [int(tp) for tp in person_type.split('_') if tp] + typ = models.PersonType.objects.filter(pk__in=typs).all() + query = query & Q(person_type__in=typ) + except (ValueError, ObjectDoesNotExist): + pass + limit = 20 + persons = models.Person.objects.filter(query)[:limit] + data = json.dumps([{'id':person.pk, 'value':unicode(person)} + for person in persons if person]) + return HttpResponse(data, mimetype='text/plain') + +def autocomplete_town(request): + if not request.GET.get('term'): + return HttpResponse(mimetype='text/plain') + q = request.GET.get('term') + query = Q() + for q in q.split(' '): + extra = Q(name__icontains=q) + if settings.COUNTRY == 'fr': + extra = (extra | Q(numero_insee__istartswith=q) | \ + Q(departement__label__istartswith=q)) + query = query & extra + limit = 20 + towns = models.Town.objects.filter(query)[:limit] + data = json.dumps([{'id':town.pk, 'value':unicode(town)} + for town in towns]) + return HttpResponse(data, mimetype='text/plain') + +def autocomplete_file(request): + if not request.user.has_perm('ishtar_base.view_file', models.File) and \ + not request.user.has_perm('ishtar_base.view_own_file', models.File) : + return HttpResponse(mimetype='text/plain') + if not request.GET.get('term'): + return HttpResponse(mimetype='text/plain') + q = request.GET.get('term') + query = Q() + for q in q.split(' '): + extra = Q(internal_reference__icontains=q) | \ + Q(towns__name__icontains=q) + try: + value = int(q) + extra = extra | Q(year=q) | Q(numeric_reference=q) + except ValueError: + pass + query = query & extra + limit = 20 + files = models.File.objects.filter(query)[:limit] + data = json.dumps([{'id':file.pk, 'value':unicode(file)} + for file in files]) + return HttpResponse(data, mimetype='text/plain') + +from types import NoneType + +def format_val(val): + if type(val) == NoneType: + return u"" + if type(val) == bool: + if val: + return unicode(_(u"True")) + else: + return unicode(_(u"False")) + return unicode(val) + +HIERARCHIC_LEVELS = 5 +HIERARCHIC_FIELDS = ['period', 'unit', 'material'] +def get_item(model, func_name, default_name, extra_request_keys=[], + bool_fields=[]): + """ + Generic treatment of tables + """ + def func(request, data_type='json', **dct): + if 'type' in dct: + data_type = dct.pop('type') + if not data_type: + data_type = 'json' + fields = [model._meta.get_field_by_name(k)[0] + for k in model._meta.get_all_field_names()] + request_keys = dict([(field.name, + field.name + (hasattr(field, 'rel') and field.rel and '__pk' or '')) + for field in fields]) + request_keys.update(extra_request_keys) + request_items = request.method == 'POST' and request.POST or request.GET + dct = {} + try: + old = 'old' in request_items and int(request_items['old']) + except ValueError: + return HttpResponse(None, mimetype='text/plain') + for k in request_keys: + q = request_items.get(k) + if not q: + continue + dct[request_keys[k]] = q + if not dct and 'submited' not in request_items: + if default_name in request.session and \ + request.session[default_name]: + dct = {"pk":request.session[default_name]} + if (not dct or data_type == 'csv') and func_name in request.session: + dct = request.session[func_name] + else: + request.session[func_name] = dct + for k in bool_fields: + if k in dct: + if dct[k] == u"1": + dct.pop(k) + else: + dct[k] = dct[k] == u"2" and True or False + # manage hierarchic conditions + or_reqs = [] + for k in HIERARCHIC_FIELDS: + for req in dct.copy(): + if req.endswith(k + '__pk'): + val = dct.pop(req) + reqs = Q(**{req:val}) + req = req[:-2] + '__' + for idx in xrange(HIERARCHIC_LEVELS): + req = req[:-2] + 'parent__pk' + q = Q(**{req:val}) + reqs = reqs | q + or_reqs.append(reqs) + query = Q(**dct) + for or_req in or_reqs: + query = query & or_req + items = model.objects.filter(query) + q = request_items.get('sidx') + # manage tables + if q and q in request_keys: + k = request_keys[q] + if k.endswith("__pk"): + k = k[:-len("__pk")] + "__label" + q = request_items.get('sord') + sign = q and q == u'desc' and "-" or '' + items = items.order_by(sign + k) + datas = [] + if old: + items = [item.get_previous(old) for item in items] + for item in items: + data = [item.pk] + for k in model.TABLE_COLS: + vals = [item] + for ky in k.split('.'): + new_vals = [] + for val in vals: + if hasattr(val, 'all'): # manage related objects + val = list(val.all()) + for v in val: + new_vals.append(getattr(v, ky)) + elif val: + new_vals.append(getattr(val, ky)) + vals = new_vals + if vals and hasattr(vals[0], 'all'): # manage last related objects + new_vals = [] + for val in vals: + new_vals += list(val.all()) + vals = new_vals + data.append(", ".join([format_val(v) for v in vals]) or u"") + datas.append(data) + link_template = "<a href='#' onclick='load_window(\"%%s\")'>%s</a>" % \ + (unicode(_("Details"))) + if data_type == "json": + rows = [] + for data in datas: + try: + lnk = link_template % reverse('show-'+default_name, + args=[data[0], '']) + except NoReverseMatch: + lnk = '' + res = {'id':data[0], 'link':lnk} + for idx, value in enumerate(data[1:]): + if value: + res[model.TABLE_COLS[idx].split('.')[-1]] = value + rows.append(res) + data = json.dumps({ + "records":len(items), + "rows":rows + }) + return HttpResponse(data, mimetype='text/plain') + elif data_type == "csv": + response = HttpResponse(mimetype='text/csv') + n = datetime.datetime.now() + filename = u'%s_%s.csv' % (default_name, + n.strftime('%Y%m%d-%H%M%S')) + response['Content-Disposition'] = 'attachment; filename=%s'%filename + writer = csv.writer(response, **CSV_OPTIONS) + col_names = [] + for field_name in model.TABLE_COLS: + try: + field = model._meta.get_field(field_name) + except: + col_names.append(u"".encode(ENCODING)) + continue + col_names.append(unicode(field.verbose_name).encode(ENCODING)) + writer.writerow(col_names) + for data in datas: + writer.writerow([val.encode(ENCODING) for val in data[1:]]) + return response + return HttpResponse(None, mimetype='text/plain') + + return func + +def show_item(model, name): + def func(request, pk, **dct): + try: + item = model.objects.get(pk=pk) + except ObjectDoesNotExist: + return HttpResponse(None) + doc_type = 'type' in dct and dct.pop('type') + date = 'date' in dct and dct.pop('date') + dct['window_id'] = "%s-%d-%s" % (name, item.pk, + datetime.datetime.now().strftime('%M%s')) + if date: + try: + date = datetime.datetime.strptime(date, '%Y-%m-%dT%H:%M:%S.%f') + item = item.get_previous(date=date) + assert item != None + except (ValueError, AssertionError): + return HttpResponse(None, mimetype='text/plain') + dct['previous'] = item._previous + dct['next'] = item._next + else: + historized = item.history.all() + if historized: + item.history_date = historized[0].history_date + if len(historized) > 1: + dct['previous'] = historized[1].history_date + dct['item'], dct['item_name'] = item, name + context_instance = RequestContext(request) + context_instance.update(dct) + n = datetime.datetime.now() + filename = u'%s_%s_%s' % (name, slugify(unicode(item)), + n.strftime('%Y%m%d-%H%M%S')) + if doc_type == "odt" and settings.XHTML2ODT_PATH and \ + settings.ODT_TEMPLATE: + tpl = loader.get_template('sheet_%s.html' % name) + content = tpl.render(context_instance) + try: + tidy_options = dict(output_xhtml=1, add_xml_decl=1, indent=1, + tidy_mark=0, output_encoding='utf8', doctype='auto', + wrap=0, char_encoding='utf8') + html = str(tidy.parseString(content.encode('utf-8'), + **tidy_options)) + html = html.replace(" ", " ") + html = re.sub('<pre([^>]*)>\n', '<pre\\1>', html) + + odt = NamedTemporaryFile() + options = optparse.Values() + options.with_network = True + for k, v in (('input', ''), + ('output', odt.name), + ('template', settings.ODT_TEMPLATE), + ('with_network', True), + ('top_header_level', 1), + ('img_width', '8cm'), + ('img_height', '6cm'), + ('verbose', False), + ('replace_keyword', 'ODT-INSERT'), + ('cut_start', 'ODT-CUT-START'), + ('htmlid', None), + ('url', "#")): + setattr(options, k, v) + odtfile = xhtml2odt.ODTFile(options) + odtfile.open() + odtfile.import_xhtml(html) + odtfile = odtfile.save() + except xhtml2odt.ODTExportError, ex: + return HttpResponse(content, content_type="application/xhtml") + response = HttpResponse( + mimetype='application/vnd.oasis.opendocument.text') + response['Content-Disposition'] = 'attachment; filename=%s.odt' % \ + filename + response.write(odtfile) + return response + elif doc_type == 'pdf': + tpl = loader.get_template('sheet_%s_pdf.html' % name) + content = tpl.render(context_instance) + result = StringIO.StringIO() + html = content.encode('utf-8') + html = html.replace("<table", "<pdf:nextpage/><table repeat='1'") + pdf = pisa.pisaDocument(StringIO.StringIO(html), result) + response = HttpResponse(result.getvalue(), + mimetype='application/pdf') + response['Content-Disposition'] = 'attachment; filename=%s.pdf' % \ + filename + if not pdf.err: + return response + return HttpResponse(content, content_type="application/xhtml") + else: + tpl = loader.get_template('sheet_%s_window.html' % name) + content = tpl.render(context_instance) + return HttpResponse(content, content_type="application/xhtml") + return func + +def revert_item(model): + def func(request, pk, date, **dct): + try: + item = model.objects.get(pk=pk) + date = datetime.datetime.strptime(date, '%Y-%m-%dT%H:%M:%S.%f') + item.rollback(date) + except (ObjectDoesNotExist, ValueError, HistoryError): + return HttpResponse(None, mimetype='text/plain') + return HttpResponse("True", mimetype='text/plain') + return func + + +get_file = get_item(models.File, 'get_file', 'file') +show_file = show_item(models.File, 'file') +revert_file = revert_item(models.File) + +def autocomplete_operation(request, non_closed=True): + if not request.user.has_perm('ishtar_base.view_operation', models.Operation)\ + and not request.user.has_perm('ishtar_base.view_own_operation', + models.Operation): + return HttpResponse(mimetype='text/plain') + if not request.GET.get('term'): + return HttpResponse(mimetype='text/plain') + q = request.GET.get('term') + query = Q() + for q in q.split(' '): + extra = Q(towns__name__icontains=q) + try: + value = int(q) + extra = extra | Q(year=q) | Q(operation_code=q) + except ValueError: + pass + query = query & extra + if non_closed: + query = query & Q(end_date__isnull=True) + limit = 15 + operations = models.Operation.objects.filter(query)[:limit] + data = json.dumps([{'id':operation.pk, 'value':unicode(operation)} + for operation in operations]) + return HttpResponse(data, mimetype='text/plain') + +get_operation = get_item(models.Operation, 'get_operation', 'operation') +show_operation = show_item(models.Operation, 'operation') +revert_operation = revert_item(models.Operation) + +get_administrativeactfile = get_item(models.AdministrativeAct, + 'get_administrativeactfile', 'administrativeactfile', + extra_request_keys={'associated_file__towns':'associated_file__towns__pk', + 'operation__towns':'operation__towns__pk', + 'act_type__intented_to':'act_type__intented_to'}) +get_administrativeactop = get_item(models.AdministrativeAct, + 'get_administrativeactop', 'administrativeactop', + extra_request_keys={'associated_file__towns':'associated_file__towns__pk', + 'operation__towns':'operation__towns__pk', + 'act_type__intented_to':'act_type__intented_to'}) + +def autocomplete_organization(request, orga_type=None): + if not request.user.has_perm('ishtar_base.view_organization', + models.Organization) and \ + not request.user.has_perm('ishtar_base.view_own_organization', + models.Organization): + return HttpResponse(mimetype='text/plain') + if not request.GET.get('term'): + return HttpResponse(mimetype='text/plain') + q = request.GET.get('term') + query = Q() + for q in q.split(' '): + extra = Q(name__icontains=q) + query = query & extra + if orga_type: + try: + typs = [int(tp) for tp in orga_type.split('_') if tp] + typ = models.OrganizationType.objects.filter(pk__in=typs).all() + query = query & Q(organization_type__in=typ) + except (ValueError, ObjectDoesNotExist): + pass + limit = 15 + organizations = models.Organization.objects.filter(query)[:limit] + data = json.dumps([{'id':org.pk, 'value':unicode(org)} + for org in organizations]) + return HttpResponse(data, mimetype='text/plain') + +show_contextrecord = show_item(models.ContextRecord, 'contextrecord') +get_contextrecord = get_item(models.ContextRecord, + 'get_contextrecord', 'contextrecord', + extra_request_keys={'parcel__town':'parcel__town__pk', + 'operation__year':'operation__year__contains', + 'datings__period':'datings__period__pk'},) +get_archaeologicalitem = get_item(models.Item, + 'get_archaeologicalitem', 'item', + bool_fields = ['base_items__is_isolated'], + extra_request_keys={ +'base_items__context_record__parcel__town': + 'base_items__context_record__parcel__town', +'base_items__context_record__operation__year': + 'base_items__context_record__operation__year__contains', +'base_items__context_record__operation__code_patriarche': + 'base_items__context_record__operation__code_patriarche', +'dating__period':'dating__period__pk', +'base_items__item__description':'base_items__item__description__icontains', +'base_items__is_isolated':'base_items__is_isolated'}) + +def autocomplete_warehouse(request): + if not request.user.has_perm('ishtar_base.view_warehouse', models.Warehouse)\ + and not request.user.has_perm('ishtar_base.view_own_warehouse', + models.Warehouse) : + return HttpResponse(mimetype='text/plain') + if not request.GET.get('term'): + return HttpResponse(mimetype='text/plain') + q = request.GET.get('term') + query = Q() + for q in q.split(' '): + extra = Q(name__icontains=q) | \ + Q(warehouse_type__label__icontains=q) + query = query & extra + limit = 15 + warehouses = models.Warehouse.objects.filter(query)[:limit] + data = json.dumps([{'id':warehouse.pk, 'value':unicode(warehouse)} + for warehouse in warehouses]) + return HttpResponse(data, mimetype='text/plain') + +def autocomplete_author(request): + if not request.user.has_perm('ishtar_base.view_author', models.Author)\ + and not request.user.has_perm('ishtar_base.view_own_author', + models.Warehouse) : + return HttpResponse(mimetype='text/plain') + if not request.GET.get('term'): + return HttpResponse(mimetype='text/plain') + q = request.GET.get('term') + query = Q() + for q in q.split(' '): + extra = Q(person__name__icontains=q) | \ + Q(person__surname__icontains=q) | \ + Q(person__email__icontains=q) | \ + Q(author_type__label__icontains=q) + query = query & extra + limit = 15 + authors = models.Author.objects.filter(query)[:limit] + data = json.dumps([{'id':author.pk, 'value':unicode(author)} + for author in authors]) + return HttpResponse(data, mimetype='text/plain') + +def new_item(model): + def func(request, parent_name): + model_name = model._meta.object_name + if not check_permission(request, 'add_'+model_name.lower()): + not_permitted_msg = ugettext(u"Operation not permitted.") + return HttpResponse(not_permitted_msg) + frm = getattr(ishtar_forms, model_name + 'Form') + dct = {'title':unicode(_(u'New %s' % model_name.lower()))} + if request.method == 'POST': + dct['form'] = frm(request.POST) + if dct['form'].is_valid(): + new_item = dct['form'].save(request.user) + dct['new_item_label'] = unicode(new_item) + dct['new_item_pk'] = new_item.pk + dct['parent_name'] = parent_name + dct['parent_pk'] = parent_name + if '_select_' in dct['parent_pk']: + parents = dct['parent_pk'].split('_') + dct['parent_pk'] = "_".join([parents[0]] + parents[2:]) + return render_to_response('window.html', dct, + context_instance=RequestContext(request)) + else: + dct['form'] = frm() + return render_to_response('window.html', dct, + context_instance=RequestContext(request)) + return func + +new_warehouse = new_item(models.Warehouse) +new_person = new_item(models.Person) +new_organization = new_item(models.Organization) +new_author = new_item(models.Author) + +def action(request, action_slug, obj_id=None, *args, **kwargs): + """ + Action management + """ + if not check_permission(request, action_slug, obj_id): + not_permitted_msg = ugettext(u"Operation not permitted.") + return HttpResponse(not_permitted_msg) + request.session['CURRENT_ACTION'] = action_slug + associated_wizard = action_slug + '_wizard' + dct = {} + globals_dct = globals() + if action_slug in globals_dct: + return globals_dct[action_slug](request, dct, obj_id, *args, **kwargs) + elif hasattr(ishtar_forms, action_slug + "_wizard"): + return getattr(ishtar_forms, action_slug+"_wizard")(request, *args, + **kwargs) + return render_to_response('index.html', dct, + context_instance=RequestContext(request)) + diff --git a/ishtar/ishtar_base/widgets.py b/ishtar/ishtar_base/widgets.py new file mode 100644 index 000000000..e058f7074 --- /dev/null +++ b/ishtar/ishtar_base/widgets.py @@ -0,0 +1,343 @@ +#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright (C) 2010-2011 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet>
+# Copyright (C) 2007 skam <massimo dot scamarcia at gmail.com>
+# (http://djangosnippets.org/snippets/233/)
+
+# 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 <http://www.gnu.org/licenses/>.
+
+# See the file COPYING for details.
+from django import forms
+from django.template import Context, loader
+from django.utils.safestring import mark_safe
+from django.forms.widgets import flatatt
+from django.utils.encoding import smart_unicode
+from django.utils.html import escape
+from django.utils.simplejson import JSONEncoder
+from django.core.urlresolvers import resolve, reverse
+from django.utils.translation import ugettext_lazy as _
+
+from ishtar import settings
+import models
+
+class DeleteWidget(forms.CheckboxInput):
+ def render(self, name, value, attrs=None):
+ final_attrs = flatatt(self.build_attrs(attrs, name=name,
+ value='1'))
+ output = ['<tr class="delete"><td colspan="2">']
+ output.append(u"<button%s>%s</button>" % (final_attrs, _("Delete")))
+ output.append('</td></tr>')
+ return mark_safe('\n'.join(output))
+
+class SquareMeterWidget(forms.TextInput):
+ def render(self, name, value, attrs=None):
+ if not value:
+ value = u""
+ final_attrs = flatatt(self.build_attrs(attrs, name=name, value=value))
+ output = u'<input class="area_widget" type="text"%s> %s '\
+ u'(<span id="ha_%s">0</span> ha)' % (final_attrs,
+ settings.SURFACE_UNIT_LABEL, attrs['id'])
+ output += """
+<script type="text/javascript"><!--//
+ function evaluate_%(safe_id)s(){
+ value = parseFloat($("#%(id)s").val());
+ if(!isNaN(value)){
+ value = value/10000;
+ } else {
+ value = 0;
+ }
+ $("#ha_%(id)s").html(value);
+ }
+ $("#%(id)s").keyup(evaluate_%(safe_id)s);
+ $(document).ready(evaluate_%(safe_id)s());
+//--></script>
+""" % {"id":attrs['id'], "safe_id":attrs['id'].replace('-', '_')}
+ return mark_safe(output)
+
+AreaWidget = forms.TextInput
+if settings.SURFACE_UNIT == 'square-metre':
+ global AreaWidget
+ AreaWidget = SquareMeterWidget
+
+class JQueryDate(forms.TextInput):
+ def render(self, name, value=None, attrs=None):
+ rendered = super(JQueryDate, self).render(name, value, attrs)
+ rendered += """
+<script type="text/javascript"><!--//
+ $(function() {
+ $("#id_%(name)s").datepicker($.datepicker.regional["%(country)s"]);
+ var val = $("#id_%(name)s").val();
+ if(val){
+ var dtp = $.datepicker.parseDate('yy-mm-dd', val);
+ val = $.datepicker.formatDate(
+ $.datepicker.regional["%(country)s"]['dateFormat'],
+ dtp);
+ $("#id_%(name)s").val(val);
+ }
+ });
+//--></script>
+""" % {"name":name, "country":settings.COUNTRY}
+ return rendered
+
+class JQueryDate(forms.TextInput):
+ def render(self, name, value=None, attrs=None):
+ rendered = super(JQueryDate, self).render(name, value, attrs)
+ # use window.onload to be sure that datepicker don't interfere
+ # with autocomplete fields
+ rendered += """
+<script type="text/javascript"><!--//
+ $(window).load(function() {
+ $("#id_%(name)s").datepicker($.datepicker.regional["%(country)s"]);
+ var val = $("#id_%(name)s").val();
+ if(val){
+ var dt = $.datepicker.parseDate('yy-mm-dd', val);
+ val = $.datepicker.formatDate(
+ $.datepicker.regional["%(country)s"]['dateFormat'],
+ dt);
+ $("#id_%(name)s").val(val);
+ }
+ });
+//--></script>
+""" % {"name":name, "country":settings.COUNTRY}
+ return rendered
+
+class JQueryAutoComplete(forms.TextInput):
+ def __init__(self, source, associated_model=None, options={}, attrs={},
+ new=False):
+ """
+ Source can be a list containing the autocomplete values or a
+ string containing the url used for the request.
+ """
+ self.options = None
+ self.attrs = {}
+ self.source = source
+ self.associated_model = associated_model
+ if len(options) > 0:
+ self.options = JSONEncoder().encode(options)
+ self.attrs.update(attrs)
+ self.new = new
+
+ def render_js(self, field_id):
+ if isinstance(self.source, list):
+ source = JSONEncoder().encode(self.source)
+ elif isinstance(self.source, str) or isinstance(self.source, unicode):
+ source = "'%s'" % escape(self.source)
+ else:
+ try:
+ source = "'" + unicode(self.source) + "'"
+ except:
+ raise ValueError('source type is not valid')
+ options = 'source : ' + source
+ options += ''', select: function( event, ui ) {
+ if(ui.item){
+ $('#id_%s').val(ui.item.id);
+ } else {
+ $('#id_%s').val(null);
+ }
+ }, minLength: 2
+ ''' % (field_id, field_id)
+ if self.options:
+ options += ',%s' % self.options
+
+ js = u'$(\'#id_select_%s\').autocomplete({%s});\n' % (field_id, options)
+ js += u'''$(\'#id_select_%s\').live('click', function(){
+ $('#id_%s').val(null);
+ $('#id_select_%s').val(null);
+});''' % (field_id, field_id, field_id)
+ return js
+
+ def render(self, name, value=None, attrs=None):
+ attrs_hidden = self.build_attrs(attrs, name=name)
+ attrs_select = self.build_attrs(attrs)
+
+ if value:
+ val = escape(smart_unicode(value))
+ attrs_hidden['value'] = val
+ attrs_select['value'] = val
+ if self.associated_model:
+ try:
+ attrs_select['value'] = unicode(self.associated_model.\
+objects.get(pk=value))
+ except:
+ attrs_select['value'] = ""
+ if not self.attrs.has_key('id'):
+ attrs_hidden['id'] = 'id_%s' % name
+ attrs_select['id'] = 'id_select_%s' % name
+ if 'class' not in attrs_select:
+ attrs_select['class'] = 'autocomplete'
+ new = ''
+ if self.new:
+ model_name = self.associated_model._meta.object_name.lower()
+ url_new = reverse('new-' + model_name, args=[attrs_select['id']])
+ new = u' <a href="#" class="add-button" '\
+ u'onclick="open_window(\'%s\');">+</a>' % url_new
+ html = u'''<input%(attrs_select)s/>%(new)s\
+<input type="hidden"%(attrs_hidden)s/>\
+ <script type="text/javascript"><!--//
+ $(function() {%(js)s});//--></script>
+ ''' % {
+ 'attrs_select' : flatatt(attrs_select),
+ 'attrs_hidden' : flatatt(attrs_hidden),
+ 'js' : self.render_js(name),
+ 'new':new
+ }
+ return html
+
+class JQueryJqGrid(forms.RadioSelect):
+ COL_TPL = "{name:'%(idx)s', index:'%(idx)s', sortable:true}"
+ class Media:
+ js = ['%s/js/i18n/grid.locale-%s.js' % (settings.MEDIA_URL,
+ settings.COUNTRY),
+ '%s/js/jquery.jqGrid.min.js' % settings.MEDIA_URL,
+ ]
+ css = {'all':['%s/media/ui.jqgrid.css' % settings.MEDIA_URL,
+ ]}
+
+ def __init__(self, source, form, associated_model, attrs={},
+ table_cols='TABLE_COLS', multiple=False):
+ self.source = source
+ self.form = form
+ self.attrs = attrs
+ self.associated_model = associated_model
+ self.table_cols = table_cols
+ self.multiple = multiple
+
+ def render(self, name, value=None, attrs=None):
+ t = loader.get_template('form_snippet.html')
+ rendered = t.render(Context({'form':self.form}))
+ rendered += """
+</table>
+<button id='search_%s' class='submit'>%s</button>
+<h4>%s</h4>
+""" % (name, unicode(_("Search")), unicode(_("Search and select an item")))
+ extra_cols = []
+ col_names, col_idx = [], []
+ for k in self.form.fields:
+ field = self.form.fields[k]
+ col_idx.append(u'"%s"' % k)
+ for field_name in getattr(self.associated_model, self.table_cols):
+ field = self.associated_model
+ keys = field_name.split('.')
+ try:
+ for key in keys:
+ if hasattr(field, 'rel'):
+ field = field.rel.to
+ field = field._meta.get_field(key)
+ except:
+ continue
+ col_names.append(u'"%s"' % field.verbose_name)
+ extra_cols.append(self.COL_TPL % {'idx':field.name})
+ col_names = col_names and ",\n".join(col_names) or ""
+ col_idx = col_idx and ",\n".join(col_idx) or ""
+ extra_cols = extra_cols and ",\n".join(extra_cols) or ""
+ rendered += """<table id="grid_%s" class='jqgrid'></table>
+<div id="pager_%s"></div>
+""" % (name, name)
+ encoding = settings.ENCODING or 'utf-8'
+ rendered += """
+<div id="foot_%s" class="gridfooter"><a href="%scsv" target="_blank">%s (%s)</a></div>
+""" % (name, unicode(self.source), unicode(_("Export as CSV")), encoding)
+ if self.multiple:
+ rendered += '''
+<input type="button" id="add_button_%s" value="%s"/>
+<ul id='selectmulti_%s' class='selectmulti'>
+</ul>
+''' % (name, unicode(_("Add")), name)
+ rendered += '<input type="hidden" id="hidden_%s" name="%s"/>' % (name,
+ name)
+ dct = {'name':name, 'col_names':col_names, 'extra_cols':extra_cols,
+ 'source':unicode(self.source), 'col_idx':col_idx,
+ 'no_result':unicode(_("No results")), 'loading':unicode(_("Loading...")),
+ 'remove':unicode(_("Remove")), 'sname':name.replace('-', '')}
+
+ rendered += """
+<script type="text/javascript">
+var query_vars = new Array(%(col_idx)s);
+jQuery(document).ready(function(){
+ jQuery("#search_%(name)s").click(function (){
+ var data = "";
+ for (idx in query_vars)
+ {
+ var key = query_vars[idx];
+ var val = jQuery("#id_"+key).val();
+ if (val){
+ if (data) data += "&";
+ data += key + "=" + val;
+ }
+ }
+ var mygrid = jQuery("#grid_%(name)s");
+ var url = "%(source)s?submited=1&" + data;
+ mygrid.setGridParam({url:url});
+ mygrid.trigger("reloadGrid");
+ return false;
+ });
+
+ jQuery("#grid_%(name)s").jqGrid({
+ url:'%(source)s',
+ datatype: "json",
+ mtype: 'GET',
+ colNames:['id', '', %(col_names)s],
+ colModel:[
+ {name:'id', index:'id', hidden:true},
+ {name:'link', index:'link', width:80},
+ %(extra_cols)s
+ ],
+ sortname: 'value',
+ viewrecords: true,
+ sortorder: "asc",
+ emptyrecords: "%(no_result)s",
+ loadtext: "%(loading)s",
+ pager: '#pager_%(name)s',
+ pgbuttons: false,
+ pginput: false,
+ width:740,
+ jsonReader : {repeatitems: false}
+ });
+""" % dct
+ if self.multiple:
+ rendered += """
+ var selItems_%(sname)s = new Array();
+ jQuery("#add_button_%(name)s").click(function (){
+ var mygrid = jQuery("#grid_%(name)s");
+ var idx = mygrid.getGridParam('selrow');
+ var label = mygrid.getCell(idx, 2);
+ for (id in selItems_%(sname)s){
+ if(selItems_%(sname)s[id] == idx){
+ return false;
+ }
+ }
+ selItems_%(sname)s.push(idx);
+ jQuery("#selectmulti_%(name)s").append(
+ "<li id='selected_%(name)s_"+idx+"'>"+label+" <a href='#' class='remove' onclick=\\"multiRemoveItem('selItems_%(sname)s', '%(name)s', "+ idx +");return false;\\">%(remove)s</a></li>");
+ return true;
+ });
+ jQuery("#submit_form").click(function (){
+ jQuery("#hidden_%(name)s").val(selItems_%(sname)s);
+ return true;
+ });
+""" % dct
+ else:
+ rendered += """
+ jQuery("#submit_form").click(function (){
+ var mygrid = jQuery("#grid_%(name)s");
+ jQuery("#hidden_%(name)s").val(mygrid.getGridParam('selrow'));
+ return true;
+ });
+""" % dct
+ rendered += """
+});
+</script>
+"""
+ return mark_safe(rendered)
+
|
