#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2010-2017 Étienne Loks # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # See the file COPYING for details. from collections import OrderedDict import datetime from django import forms from django.db.models import Max from django.conf import settings from django.forms.formsets import formset_factory from ishtar_common.utils import ugettext_lazy as _ from ishtar_common.models import Person, valid_id, Town, \ SpatialReferenceSystem, Organization, valid_ids, person_type_pks_lazy from archaeological_operations.models import ArchaeologicalSite from archaeological_context_records.models import ContextRecord from archaeological_finds.models import TreatmentType, FindBasket, \ MaterialType, ObjectType, IntegrityType, RemarkabilityType, \ ConservatoryState, AlterationType, AlterationCauseType, \ TreatmentEmergencyType from . import models from ishtar_common import widgets from archaeological_operations.widgets import OAWidget from bootstrap_datepicker.widgets import DatePicker from ishtar_common.forms import name_validator, reverse_lazy, \ get_form_selection, ManageOldType, FinalForm, FormSet, \ CustomForm, FieldType, DocumentItemSelect, FormHeader, TableSelect, \ CustomFormSearch, MultiSearchForm, LockForm, QAForm from ishtar_common.forms_common import get_town_field, MergeForm, ManualMerge,\ MergeIntoForm from archaeological_finds.forms import FindMultipleFormSelection, \ SelectFindBasketForm def get_warehouse_field(label=_("Warehouse"), required=True): # !FIXME hard_link, reverse_lazy doen't seem to work with formsets url = "/" + settings.URL_PATH + 'autocomplete-warehouse' widget = widgets.JQueryAutoComplete(url, associated_model=models.Warehouse) return forms.IntegerField(widget=widget, label=label, required=required, validators=[valid_id(models.Warehouse)]) class SelectedDivisionForm(ManageOldType, forms.Form): form_label = _("Default divisions") base_model = 'associated_division' associated_models = {'container_type': models.ContainerType, 'associated_division': models.WarehouseDivisionLink} container_type = forms.ChoiceField( label=_("Division type"), choices=(), validators=[valid_id(models.ContainerType)]) order = forms.IntegerField(label=_("Order"), min_value=0, required=False) def __init__(self, *args, **kwargs): super(SelectedDivisionForm, self).__init__(*args, **kwargs) self.fields['container_type'].choices = \ models.ContainerType.get_types( dct={"stationary": True}, initial=self.init_data.get('container_type')) class DivisionFormSet(FormSet): def clean(self): """Checks that no divisions are duplicated.""" self.check_duplicate(('container_type',), _("There are identical divisions.")) self.check_duplicate(('order',), _("Order fields must be different."), check_null=True) SelectedDivisionFormset = formset_factory( SelectedDivisionForm, can_delete=True, formset=DivisionFormSet) SelectedDivisionFormset.form_label = _("Divisions") SelectedDivisionFormset.form_admin_name = _("Warehouse - 020 - Divisions") SelectedDivisionFormset.form_slug = "warehouse-020-divisions" class WarehouseSelect(CustomForm, TableSelect): _model = models.Warehouse form_admin_name = _("Warehouse - 001 - Search") form_slug = "warehouse-001-search" search_vector = forms.CharField( label=_("Full text search"), widget=widgets.SearchWidget( 'archaeological-warehouse', 'warehouse' )) name = forms.CharField(label=_("Name")) warehouse_type = forms.ChoiceField(label=_("Warehouse type"), choices=[]) town = get_town_field(label=_("Town")) def __init__(self, *args, **kwargs): super(WarehouseSelect, self).__init__(*args, **kwargs) self.fields['warehouse_type'].choices = \ models.WarehouseType.get_types() self.fields['warehouse_type'].help_text = \ models.WarehouseType.get_help() class WarehouseFormSelection(LockForm, CustomFormSearch): SEARCH_AND_SELECT = True form_label = _("Warehouse search") associated_models = {'pk': models.Warehouse} currents = {'pk': models.Warehouse} pk = forms.IntegerField( label="", required=False, widget=widgets.DataTable( reverse_lazy('get-warehouse'), WarehouseSelect, models.Warehouse, gallery=True, map=True), validators=[valid_id(models.Warehouse)]) class WarehouseFormMultiSelection(LockForm, MultiSearchForm): form_label = _("Warehouse search") associated_models = {'pks': models.Warehouse} pk = forms.CharField( label="", required=False, widget=widgets.DataTable( reverse_lazy('get-warehouse'), WarehouseSelect, models.Warehouse, gallery=True, map=True, multiple_select=True ), validators=[valid_ids(models.Warehouse)]) class WarehouseForm(CustomForm, ManageOldType, forms.Form): HEADERS = {} form_label = _("Warehouse") form_admin_name = _("Warehouse - 010 - General") form_slug = "warehouse-010-general" extra_form_modals = ["organization", "person"] associated_models = { 'warehouse_type': models.WarehouseType, 'person_in_charge': Person, 'organization': Organization, 'precise_town': Town, 'spatial_reference_system': SpatialReferenceSystem } name = forms.CharField(label=_("Name"), max_length=200, validators=[name_validator]) warehouse_type = forms.ChoiceField(label=_("Warehouse type"), choices=[]) organization = forms.IntegerField( label=_("Organization"), widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-organization'), associated_model=Organization, new=True), validators=[valid_id(Organization)], required=False) person_in_charge = forms.IntegerField( label=_("Person in charge"), widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-person'), associated_model=Person, new=True), validators=[valid_id(Person)], required=False) create_organization = forms.BooleanField( label=_("Create a new organization from this warehouse"), required=False ) comment = forms.CharField(label=_("Comment"), widget=forms.Textarea, required=False) HEADERS['address'] = FormHeader( _("Address"), collapse=True, help_message=_( "Only fill the following fields if no organization is provided or " "if the address of the warehouse is different from the one of the " "organization. If a new organization is created from this " "warehouse, the following fields are used for the organization.")) address = forms.CharField(label=_("Address"), widget=forms.Textarea, required=False) address_complement = forms.CharField(label=_("Address complement"), widget=forms.Textarea, required=False) postal_code = forms.CharField(label=_("Postal code"), max_length=10, required=False) town = forms.CharField(label=_("Town (freeform)"), max_length=150, required=False) precise_town = get_town_field(required=False) country = forms.CharField(label=_("Country"), max_length=30, required=False) phone = forms.CharField(label=_("Phone"), max_length=18, required=False) mobile_phone = forms.CharField(label=_("Mobile phone"), max_length=18, required=False) HEADERS['x'] = FormHeader(_("Coordinates")) x = forms.FloatField(label=_("X"), required=False) y = forms.FloatField(label=_("Y"), required=False) spatial_reference_system = forms.ChoiceField( label=_("Spatial Reference System"), required=False, choices=[]) TYPES = [ FieldType('warehouse_type', models.WarehouseType), FieldType('spatial_reference_system', SpatialReferenceSystem) ] def __init__(self, *args, **kwargs): if 'limits' in kwargs: kwargs.pop('limits') super(WarehouseForm, self).__init__(*args, **kwargs) def clean(self): if self.cleaned_data.get("organization", None) and \ self.cleaned_data.get("create_organization", None): raise forms.ValidationError( _("A new organization is not created if an organization is " "selected.")) return self.cleaned_data 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'] = Person.objects.get( pk=dct['person_in_charge']) if 'organization' in dct and dct['organization']: dct['organization'] = Organization.objects.get( pk=dct['organization']) if not dct.get("spatial_reference_system", None): dct.pop("spatial_reference_system") create_orga = dct.pop("create_organization") new_item = models.Warehouse(**dct) new_item.save() if not create_orga: return new_item new_item.create_attached_organization() return new_item class WarehouseModifyForm(WarehouseForm): def __init__(self, *args, **kwargs): super(WarehouseModifyForm, self).__init__(*args, **kwargs) self.fields.pop("create_organization") class WarehouseDeletionForm(FinalForm): confirm_msg = _("Would you like to delete this warehouse?") confirm_end_msg = _("Would you like to delete this warehouse?") class ContainerForm(CustomForm, ManageOldType, forms.Form): form_label = _("Container") form_admin_name = _("Container - 010 - General") form_slug = "container-010-general" file_upload = True extra_form_modals = ["warehouse", "organization", "person", "container"] associated_models = {'container_type': models.ContainerType, 'location': models.Warehouse, 'parent': models.Container} reference = forms.CharField(label=_("Ref."), max_length=200) old_reference = forms.CharField(label=_("Old reference"), required=False, max_length=200) container_type = forms.ChoiceField(label=_("Container type"), choices=[]) location = forms.IntegerField( label=_("Current location (warehouse)"), widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-warehouse'), associated_model=models.Warehouse, new=True), validators=[valid_id(models.Warehouse)]) parent = forms.IntegerField( label=_("Parent container"), widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-container'), associated_model=models.Container, dynamic_limit=['location']), validators=[valid_id(models.Container)], required=False ) comment = forms.CharField(label=_("Comment"), widget=forms.Textarea, required=False) TYPES = [ FieldType('container_type', models.ContainerType), ] class Media: js = ('forms/container.js',) def __init__(self, *args, **kwargs): if 'limits' in kwargs: kwargs.pop('limits') super(ContainerForm, self).__init__(*args, **kwargs) def clean_parent(self): if not self.cleaned_data.get("parent", None): return warehouse_id = self.cleaned_data.get("location") q = models.Container.objects.filter( pk=self.cleaned_data["parent"], location_id=warehouse_id) if not q.count(): raise forms.ValidationError( _("The parent container is not attached to the same " "warehouse.")) return self.cleaned_data["parent"] def clean(self): cleaned_data = self.cleaned_data warehouse = cleaned_data.get("location") q = models.Container.objects.filter( reference=cleaned_data.get("reference"), location__pk=warehouse, container_type_id=cleaned_data.get("container_type"), parent_id=cleaned_data.get("parent") ) if 'pk' in cleaned_data and cleaned_data['pk']: q = q.exclude(pk=int(cleaned_data['pk'])) if q.count(): raise forms.ValidationError(_("This reference already exists for " "this warehouse.")) return cleaned_data def save(self, user): dct = self.cleaned_data dct['history_modifier'] = user dct['container_type'] = models.ContainerType.objects.get( pk=dct['container_type']) dct['location'] = models.Warehouse.objects.get(pk=dct['location']) new_item = models.Container(**dct) new_item.save() return new_item class ContainerModifyForm(ContainerForm): pk = forms.IntegerField(required=False, widget=forms.HiddenInput) index = forms.IntegerField(label=_("ID"), required=False) def __init__(self, *args, **kwargs): super(ContainerModifyForm, self).__init__(*args, **kwargs) fields = OrderedDict() idx = self.fields.pop('index') reordered = False for key, value in self.fields.items(): fields[key] = value if key == 'container_type': fields['index'] = idx reordered = True if not reordered: fields['index'] = idx self.fields = fields def clean(self): # manage unique ID cleaned_data = super(ContainerModifyForm, self).clean() index = cleaned_data.get("index", None) warehouse = cleaned_data.get("location") if not index: q = models.Container.objects.filter(location__pk=warehouse) if not q.count(): cleaned_data["index"] = 1 else: cleaned_data["index"] = int(q.aggregate( Max("index"))["index__max"]) + 1 else: q = models.Container.objects.filter( index=index, location__pk=warehouse) if 'pk' in cleaned_data and cleaned_data['pk']: q = q.exclude(pk=int(cleaned_data['pk'])) if q.count(): raise forms.ValidationError(_("This ID already exists for " "this warehouse.")) return cleaned_data class ContainerSelect(DocumentItemSelect): _model = models.Container form_admin_name = _("Container - 001 - Search") form_slug = "container-001-search" search_vector = forms.CharField( label=_("Full text search"), widget=widgets.SearchWidget( 'archaeological-warehouse', 'container' )) location_name = get_warehouse_field(label=_("Warehouse")) container_type = forms.ChoiceField(label=_("Container type"), choices=[]) reference = forms.CharField(label=_("Ref.")) old_reference = forms.CharField(label=_("Old reference")) comment = forms.CharField(label=_("Comment")) contain_containers = forms.NullBooleanField(label=_("Contain containers")) empty = forms.NullBooleanField(label=_("Currently empty")) is_stationary = forms.NullBooleanField(label=_("Is stationary")) archaeological_sites = forms.IntegerField( label=_("Archaeological site (attached to the operation)"), widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-archaeologicalsite'), associated_model=ArchaeologicalSite), validators=[valid_id(ArchaeologicalSite)]) archaeological_sites_name = forms.CharField( label=_("Archaeological site name (attached to the operation)") ) archaeological_sites_context_record = forms.IntegerField( label=_("Archaeological site (attached to the context record)"), widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-archaeologicalsite'), associated_model=ArchaeologicalSite), validators=[valid_id(ArchaeologicalSite)]) archaeological_sites_context_record_name = forms.CharField( label=_("Archaeological site name (attached to the context record)") ) code_patriarche = forms.IntegerField(label=_("Operation - Code PATRIARCHE"), widget=OAWidget) operation_town = get_town_field(label=_("Operation - town")) operation_scientist = forms.IntegerField( widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-person-permissive'), associated_model=Person), label=_("Operation - Scientist")) context_record = forms.IntegerField( label=_("Context record"), widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-contextrecord'), associated_model=ContextRecord), validators=[valid_id(ContextRecord)]) find_label = forms.CharField(label=_("Find - Label")) find_denomination = forms.CharField(label=_("Find - Denomination")) description = forms.CharField(label=_("Find - Description")) material_types = forms.IntegerField( label=_("Material type"), widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-materialtype'), associated_model=MaterialType), ) object_types = forms.IntegerField( label=_("Object type"), widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-objecttype'), associated_model=ObjectType), ) integrities = forms.ChoiceField(label=_("Integrity / interest"), choices=[]) remarkabilities = forms.ChoiceField(label=_("Remarkability"), choices=[]) conservatory_state = forms.ChoiceField(label=_("Conservatory state"), choices=[]) alterations = forms.ChoiceField( label=_("Alteration"), choices=[]) alteration_causes = forms.ChoiceField( label=_("Alteration cause"), choices=[]) preservation_to_considers = forms.ChoiceField( choices=[], label=_("Preservation type")) treatment_emergency = forms.ChoiceField( choices=[], label=_("Treatment emergency") ) TYPES = [ FieldType('integrities', IntegrityType), FieldType('remarkabilities', RemarkabilityType), FieldType('conservatory_state', ConservatoryState), FieldType('alterations', AlterationType), FieldType('alteration_causes', AlterationCauseType), FieldType('preservation_to_considers', TreatmentType), FieldType('treatment_emergency', TreatmentEmergencyType), FieldType('container_type', models.ContainerType) ] SITE_KEYS = { "archaeological_sites": "attached-to-operation", "archaeological_sites_name": "name-attached-to-operation", "archaeological_sites_context_record": "attached-to-cr", "archaeological_sites_context_record_name": "name-attached-to-cr", } ContainerFormSelection = get_form_selection( 'ContainerFormSelection', _("Container search"), 'container', models.Container, ContainerSelect, 'get-container', _("You should select a container."), new=True, new_message=_("Add a new container"), base_form_select=(LockForm, CustomFormSearch) ) MainContainerFormSelection = get_form_selection( 'ContainerFormSelection', _("Container search"), 'pk', models.Container, ContainerSelect, 'get-container', _("You should select a container."), gallery=True, map=True, base_form_select=CustomFormSearch ) MainContainerFormMultiSelection = get_form_selection( 'ContainerFormSelection', _("Container search"), 'pks', models.Container, ContainerSelect, 'get-container', _("You should select a container."), gallery=True, map=True, alt_pk_field="pk", multi=True, base_form_select=(LockForm, MultiSearchForm) ) class MergeContainerForm(MergeForm): class Meta: model = models.Container fields = [] FROM_KEY = 'from_container' TO_KEY = 'to_container' class ContainerMergeFormSelection(ManualMerge, forms.Form): SEARCH_AND_SELECT = True form_label = _("Container to merge") associated_models = {'to_merge': models.Container} currents = {'to_merge': models.Container} to_merge = forms.CharField( label="", required=False, widget=widgets.DataTable( reverse_lazy('get-container'), ContainerSelect, models.Container, multiple_select=True,),) class ContainerMergeIntoForm(MergeIntoForm): associated_model = models.Container class BasePackagingForm(SelectFindBasketForm): form_label = _("Packaging") associated_models = {'treatment_type': TreatmentType, 'person': Person, 'location': models.Warehouse, 'basket': FindBasket} person = forms.IntegerField( label=_("Packager"), widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-person'), associated_model=Person, new=True), validators=[valid_id(Person)]) start_date = forms.DateField( label=_("Date"), required=False, widget=DatePicker, initial=datetime.date.today ) class FindPackagingFormSelection(FindMultipleFormSelection): form_label = _("Packaged finds") class ContainerDeletionForm(FinalForm): confirm_msg = _("Would you like to delete this container?") confirm_end_msg = _("Would you like to delete this container?") class QAContainerFormMulti(QAForm): PREFIX = "qa" form_admin_name = _("Container - Quick action - Modify") form_slug = "container-quickaction-modify" base_models = ['qaparent', 'qacontainer_type', 'qalocation'] associated_models = { 'qaparent': models.Container, 'qacontainer_type': models.ContainerType, 'qalocation': models.Warehouse, } MULTI = True REPLACE_FIELDS = [ "qaparent", "qacontainer_type", "qalocation" ] HEADERS = { "qalocation": FormHeader(_("Warehouse")), } SINGLE_FIELDS = [] qacontainer_type = forms.ChoiceField(label=_("Container type"), required=False, choices=[]) qalocation = forms.IntegerField( label=_("Location"), widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-warehouse'), associated_model=models.Warehouse), validators=[valid_id(models.Warehouse)], required=False) qaparent = forms.IntegerField( label=_("Parent"), widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-container'), dynamic_limit=['qalocation'], associated_model=models.Container), validators=[valid_id(models.Container)], required=False) TYPES = [ FieldType('qacontainer_type', models.ContainerType), ] def __init__(self, *args, **kwargs): self.items = kwargs["items"] super(QAContainerFormMulti, self).__init__(*args, **kwargs) locations = set([item.location_id for item in self.items]) if len(locations) == 1 and "qalocation" in self.fields: self.fields["qalocation"].initial = locations.pop() def _get_qalocation(self, value): try: return models.Warehouse.objects.get(pk=value).name except models.Warehouse.DoesNotExist: return "" def _get_qaparent(self, value): try: return models.Container.objects.get(pk=value).cached_label except models.Container.DoesNotExist: return "" def clean(self): new_values = {} if self.cleaned_data.get("qacontainer_type", None): new_values["container_type_id"] = self.cleaned_data[ "qacontainer_type"] if self.cleaned_data.get("qalocation", None): new_values["location_id"] = self.cleaned_data[ "qalocation"] if self.cleaned_data.get("qaparent", None): new_values["parent_id"] = self.cleaned_data[ "qaparent"] new_tuples = [] for item in self.items: vals = { "container_type_id": item.container_type_id, "location_id": item.location_id, "parent_id": item.parent_id, "reference": item.reference.strip() } vals.update(new_values) c_tuple = (vals["location_id"], vals["container_type_id"], vals["parent_id"], vals["reference"]) q = models.Container.objects.filter(**vals).exclude(id=item.id) if c_tuple in new_tuples or q.count(): parent = models.Container.objects.get(pk=vals["parent_id"]) raise forms.ValidationError( str( _("Cannot do this changes because it would generate " "many containers with location: {}, container type: " "{}, parent: {} {} and reference: {}. " "Merge these containers first?")).format( models.Warehouse.objects.get(pk=vals["location_id"]), models.ContainerType.objects.get( pk=vals["container_type_id"]), parent.container_type, parent.reference, vals["reference"] ) ) return self.cleaned_data def save(self, items, user): super(QAContainerFormMulti, self).save(items, user) if self.cleaned_data.get("qaparent", None): return for item in items: item = models.Container.objects.get(pk=item.pk) # remove parent if do not share the same location if item.parent and item.parent.location != item.location: item.parent = None item.save()