#!/usr/bin/env python3 # -*- 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 doesn'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" 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 associated_models = { "container_type": models.ContainerType, "location": models.Warehouse, "responsibility": 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, ) responsibility = forms.IntegerField( label=_("Responsibility"), widget=widgets.JQueryAutoComplete( reverse_lazy("autocomplete-warehouse"), associated_model=models.Warehouse, new=True, ), validators=[valid_id(models.Warehouse)], help_text=_( "Automatically attached to the current warehouse if not " "filled." ), required=False, ) weight = forms.FloatField( label=_("Measured weight (g)"), widget=widgets.GramKilogramWidget, 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"), ) pk = None if "pk" in cleaned_data and cleaned_data["pk"]: pk = int(cleaned_data["pk"]) q = q.exclude(pk=pk) if q.count(): raise forms.ValidationError( _("This reference already exists for " "this warehouse.") ) if ( pk and cleaned_data.get("parent", None) and pk == int(cleaned_data.get("parent")) ): raise forms.ValidationError( _("A container cannot be a parent of " "himself.") ) 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"]) if dct.get("parent", None): dct["parent"] = models.Container.objects.get(pk=dct["parent"]) if dct.get("responsibility", None): dct["responsibility"] = models.Warehouse.objects.get( pk=dct["responsibility"] ) 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() container_type = cleaned_data.get("container_type", None) try: container_type = models.ContainerType.objects.get(pk=container_type) except models.ContainerType.DoesNotExist: return cleaned_data if container_type.stationary: # no index return cleaned_data 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")) responsibility_name = get_warehouse_field(label=_("Warehouse (responsibility)")) 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")) 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, ) 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", "qaresponsibility"] associated_models = { "qaparent": models.Container, "qacontainer_type": models.ContainerType, "qalocation": models.Warehouse, "qaresponsibility": models.Warehouse, } MULTI = True REPLACE_FIELDS = ["qaparent", "qacontainer_type", "qalocation", "qaresponsibility"] 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, ) qaresponsibility = forms.IntegerField( label=_("Responsibility"), 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 = {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_qaresponsibility(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: if ( new_values.get("parent_id", None) and int(new_values["parent_id"]) == item.pk ): raise forms.ValidationError( _("A container cannot be a parent of himself.") ) 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()