diff options
author | Étienne Loks <etienne.loks@iggdrasil.net> | 2020-04-08 18:06:26 +0200 |
---|---|---|
committer | Étienne Loks <etienne.loks@iggdrasil.net> | 2021-02-28 12:15:20 +0100 |
commit | 721cf51b30ed646f2073bc56aaf62ff1878aef94 (patch) | |
tree | 47c2df9adb1820c3993f1e50743dafcf86262e15 /archaeological_warehouse | |
parent | 5c0a4d451ec653d76ae26c444da8c000460c9db6 (diff) | |
download | Ishtar-721cf51b30ed646f2073bc56aaf62ff1878aef94.tar.bz2 Ishtar-721cf51b30ed646f2073bc56aaf62ff1878aef94.zip |
New container management - merge action
Diffstat (limited to 'archaeological_warehouse')
-rw-r--r-- | archaeological_warehouse/forms.py | 170 | ||||
-rw-r--r-- | archaeological_warehouse/ishtar_menu.py | 8 | ||||
-rw-r--r-- | archaeological_warehouse/migrations/0107_auto_20200407_1553.py | 35 | ||||
-rw-r--r-- | archaeological_warehouse/models.py | 104 | ||||
-rw-r--r-- | archaeological_warehouse/templates/ishtar/merge_container.html | 23 | ||||
-rw-r--r-- | archaeological_warehouse/tests.py | 61 | ||||
-rw-r--r-- | archaeological_warehouse/urls.py | 9 | ||||
-rw-r--r-- | archaeological_warehouse/views.py | 97 |
8 files changed, 349 insertions, 158 deletions
diff --git a/archaeological_warehouse/forms.py b/archaeological_warehouse/forms.py index a7390a890..83289819e 100644 --- a/archaeological_warehouse/forms.py +++ b/archaeological_warehouse/forms.py @@ -44,12 +44,13 @@ from ishtar_common.forms import name_validator, reverse_lazy, \ get_form_selection, ManageOldType, FinalForm, FormSet, \ CustomForm, FieldType, DocumentItemSelect, FormHeader, TableSelect, \ CustomFormSearch, MultiSearchForm, LockForm -from ishtar_common.forms_common import get_town_field +from ishtar_common.forms_common import get_town_field, MergeForm, ManualMerge,\ + MergeIntoForm from archaeological_finds.forms import FindMultipleFormSelection, \ SelectFindBasketForm -def get_warehouse_field(label=_(u"Warehouse"), required=True): +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) @@ -58,14 +59,14 @@ def get_warehouse_field(label=_(u"Warehouse"), required=True): class SelectedDivisionForm(ManageOldType, forms.Form): - form_label = _(u"Division") + form_label = _("Division") base_model = 'associated_division' associated_models = {'division': models.WarehouseDivision, 'associated_division': models.WarehouseDivisionLink} division = forms.ChoiceField( - label=_(u"Division"), choices=(), + label=_("Division"), choices=(), validators=[valid_id(models.WarehouseDivision)]) - order = forms.IntegerField(label=_(u"Order"), min_value=0, required=False) + order = forms.IntegerField(label=_("Order"), min_value=0, required=False) def __init__(self, *args, **kwargs): super(SelectedDivisionForm, self).__init__(*args, **kwargs) @@ -79,24 +80,24 @@ class DivisionFormSet(FormSet): def clean(self): """Checks that no divisions are duplicated.""" self.check_duplicate(('division',), _("There are identical divisions.")) - self.check_duplicate(('order',), _(u"Order fields must be different."), + self.check_duplicate(('order',), _("Order fields must be different."), check_null=True) SelectedDivisionFormset = formset_factory( SelectedDivisionForm, can_delete=True, formset=DivisionFormSet) -SelectedDivisionFormset.form_label = _(u"Divisions") -SelectedDivisionFormset.form_admin_name = _(u"Warehouse - 020 - Divisions") +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 = _(u"Warehouse - 001 - Search") + form_admin_name = _("Warehouse - 001 - Search") form_slug = "warehouse-001-search" search_vector = forms.CharField( - label=_(u"Full text search"), widget=widgets.SearchWidget( + label=_("Full text search"), widget=widgets.SearchWidget( 'archaeological-warehouse', 'warehouse' )) name = forms.CharField(label=_("Name")) @@ -139,8 +140,8 @@ class WarehouseFormMultiSelection(LockForm, MultiSearchForm): class WarehouseForm(CustomForm, ManageOldType, forms.Form): HEADERS = {} - form_label = _(u"Warehouse") - form_admin_name = _(u"Warehouse - 010 - General") + form_label = _("Warehouse") + form_admin_name = _("Warehouse - 010 - General") form_slug = "warehouse-010-general" extra_form_modals = ["organization", "person"] associated_models = { @@ -151,9 +152,9 @@ class WarehouseForm(CustomForm, ManageOldType, forms.Form): 'spatial_reference_system': SpatialReferenceSystem } - name = forms.CharField(label=_(u"Name"), max_length=200, + name = forms.CharField(label=_("Name"), max_length=200, validators=[name_validator]) - warehouse_type = forms.ChoiceField(label=_(u"Warehouse type"), + warehouse_type = forms.ChoiceField(label=_("Warehouse type"), choices=[]) organization = forms.IntegerField( label=_("Organization"), @@ -173,28 +174,28 @@ class WarehouseForm(CustomForm, ManageOldType, forms.Form): label=_("Create a new organization from this warehouse"), required=False ) - comment = forms.CharField(label=_(u"Comment"), widget=forms.Textarea, + comment = forms.CharField(label=_("Comment"), widget=forms.Textarea, required=False) HEADERS['address'] = FormHeader( - _(u"Address"), collapse=True, + _("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=_(u"Address"), widget=forms.Textarea, + address = forms.CharField(label=_("Address"), widget=forms.Textarea, required=False) - address_complement = forms.CharField(label=_(u"Address complement"), + address_complement = forms.CharField(label=_("Address complement"), widget=forms.Textarea, required=False) - postal_code = forms.CharField(label=_(u"Postal code"), max_length=10, + postal_code = forms.CharField(label=_("Postal code"), max_length=10, required=False) - town = forms.CharField(label=_(u"Town (freeform)"), max_length=150, + town = forms.CharField(label=_("Town (freeform)"), max_length=150, required=False) precise_town = get_town_field(required=False) - country = forms.CharField(label=_(u"Country"), max_length=30, + country = forms.CharField(label=_("Country"), max_length=30, required=False) - phone = forms.CharField(label=_(u"Phone"), max_length=18, required=False) - mobile_phone = forms.CharField(label=_(u"Mobile phone"), max_length=18, + 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) @@ -250,13 +251,13 @@ class WarehouseModifyForm(WarehouseForm): class WarehouseDeletionForm(FinalForm): - confirm_msg = _(u"Would you like to delete this warehouse?") - confirm_end_msg = _(u"Would you like to delete this warehouse?") + 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 = _(u"Container") - form_admin_name = _(u"Container - 010 - General") + form_label = _("Container") + form_admin_name = _("Container - 010 - General") form_slug = "container-010-general" file_upload = True extra_form_modals = ["warehouse", "organization", "person", "container"] @@ -264,10 +265,10 @@ class ContainerForm(CustomForm, ManageOldType, forms.Form): 'location': models.Warehouse, 'parent': models.Container, 'responsible': models.Warehouse} - reference = forms.CharField(label=_(u"Ref."), max_length=200) - old_reference = forms.CharField(label=_(u"Old reference"), required=False, + 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=_(u"Container type"), choices=[]) + container_type = forms.ChoiceField(label=_("Container type"), choices=[]) parent = forms.IntegerField( label=_("Parent container"), widget=widgets.JQueryAutoComplete( @@ -277,18 +278,18 @@ class ContainerForm(CustomForm, ManageOldType, forms.Form): required=False ) responsible = forms.IntegerField( - label=_(u"Responsible warehouse"), + label=_("Responsible warehouse"), widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-warehouse'), associated_model=models.Warehouse, new=True), validators=[valid_id(models.Warehouse)]) location = forms.IntegerField( - label=_(u"Current location (warehouse)"), + label=_("Current location (warehouse)"), widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-warehouse'), associated_model=models.Warehouse, new=True), validators=[valid_id(models.Warehouse)]) - comment = forms.CharField(label=_(u"Comment"), + comment = forms.CharField(label=_("Comment"), widget=forms.Textarea, required=False) TYPES = [ FieldType('container_type', models.ContainerType), @@ -306,12 +307,15 @@ class ContainerForm(CustomForm, ManageOldType, forms.Form): cleaned_data = self.cleaned_data warehouse = cleaned_data.get("location") q = models.Container.objects.filter( - reference=cleaned_data.get("reference"), location__pk=warehouse) + 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(_(u"This reference already exists for " - u"this warehouse.")) + raise forms.ValidationError(_("This reference already exists for " + "this warehouse.")) return cleaned_data def save(self, user): @@ -328,7 +332,7 @@ class ContainerForm(CustomForm, ManageOldType, forms.Form): class ContainerModifyForm(ContainerForm): pk = forms.IntegerField(required=False, widget=forms.HiddenInput) - index = forms.IntegerField(label=_(u"ID"), required=False) + index = forms.IntegerField(label=_("ID"), required=False) def __init__(self, *args, **kwargs): super(ContainerModifyForm, self).__init__(*args, **kwargs) @@ -358,25 +362,25 @@ class ContainerModifyForm(ContainerForm): if 'pk' in cleaned_data and cleaned_data['pk']: q = q.exclude(pk=int(cleaned_data['pk'])) if q.count(): - raise forms.ValidationError(_(u"This ID already exists for " - u"this warehouse.")) + raise forms.ValidationError(_("This ID already exists for " + "this warehouse.")) return cleaned_data class ContainerSelect(DocumentItemSelect): _model = models.Container - form_admin_name = _(u"Container - 001 - Search") + form_admin_name = _("Container - 001 - Search") form_slug = "container-001-search" search_vector = forms.CharField( - label=_(u"Full text search"), widget=widgets.SearchWidget( + 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=_(u"Comment")) + 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")) @@ -388,7 +392,7 @@ class ContainerSelect(DocumentItemSelect): associated_model=ArchaeologicalSite), validators=[valid_id(ArchaeologicalSite)]) archaeological_sites_name = forms.CharField( - label=_(u"Archaeological site name (attached to the operation)") + label=_("Archaeological site name (attached to the operation)") ) archaeological_sites_context_record = forms.IntegerField( label=_("Archaeological site (attached to the context record)"), @@ -397,7 +401,7 @@ class ContainerSelect(DocumentItemSelect): associated_model=ArchaeologicalSite), validators=[valid_id(ArchaeologicalSite)]) archaeological_sites_context_record_name = forms.CharField( - label=_(u"Archaeological site name (attached to the context record)") + label=_("Archaeological site name (attached to the context record)") ) code_patriarche = forms.IntegerField(label=_("Operation - Code PATRIARCHE"), widget=OAWidget) @@ -415,33 +419,33 @@ class ContainerSelect(DocumentItemSelect): validators=[valid_id(ContextRecord)]) find_label = forms.CharField(label=_("Find - Label")) find_denomination = forms.CharField(label=_("Find - Denomination")) - description = forms.CharField(label=_(u"Find - Description")) + description = forms.CharField(label=_("Find - Description")) material_types = forms.IntegerField( - label=_(u"Material type"), + label=_("Material type"), widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-materialtype'), associated_model=MaterialType), ) object_types = forms.IntegerField( - label=_(u"Object type"), + label=_("Object type"), widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-objecttype'), associated_model=ObjectType), ) - integrities = forms.ChoiceField(label=_(u"Integrity / interest"), + integrities = forms.ChoiceField(label=_("Integrity / interest"), choices=[]) - remarkabilities = forms.ChoiceField(label=_(u"Remarkability"), + remarkabilities = forms.ChoiceField(label=_("Remarkability"), choices=[]) - conservatory_state = forms.ChoiceField(label=_(u"Conservatory state"), + conservatory_state = forms.ChoiceField(label=_("Conservatory state"), choices=[]) alterations = forms.ChoiceField( - label=_(u"Alteration"), choices=[]) + label=_("Alteration"), choices=[]) alteration_causes = forms.ChoiceField( - label=_(u"Alteration cause"), choices=[]) + label=_("Alteration cause"), choices=[]) preservation_to_considers = forms.ChoiceField( - choices=[], label=_(u"Preservation type")) + choices=[], label=_("Preservation type")) treatment_emergency = forms.ChoiceField( - choices=[], label=_(u"Treatment emergency") + choices=[], label=_("Treatment emergency") ) TYPES = [ @@ -463,56 +467,82 @@ class ContainerSelect(DocumentItemSelect): ContainerFormSelection = get_form_selection( - 'ContainerFormSelection', _(u"Container search"), 'container', + 'ContainerFormSelection', _("Container search"), 'container', models.Container, ContainerSelect, 'get-container', - _(u"You should select a container."), new=True, - new_message=_(u"Add a new container"), + _("You should select a container."), new=True, + new_message=_("Add a new container"), base_form_select=(LockForm, CustomFormSearch) ) MainContainerFormSelection = get_form_selection( - 'ContainerFormSelection', _(u"Container search"), 'pk', + 'ContainerFormSelection', _("Container search"), 'pk', models.Container, ContainerSelect, 'get-container', - _(u"You should select a container."), gallery=True, map=True, + _("You should select a container."), gallery=True, map=True, base_form_select=CustomFormSearch ) MainContainerFormMultiSelection = get_form_selection( - 'ContainerFormSelection', _(u"Container search"), 'pks', + 'ContainerFormSelection', _("Container search"), 'pks', models.Container, ContainerSelect, 'get-container', - _(u"You should select a container."), gallery=True, map=True, + _("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 = _(u"Packaging") + form_label = _("Packaging") associated_models = {'treatment_type': TreatmentType, 'person': Person, 'location': models.Warehouse, 'basket': FindBasket} person = forms.IntegerField( - label=_(u"Packager"), + label=_("Packager"), widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-person'), associated_model=Person, new=True), validators=[valid_id(Person)]) start_date = forms.DateField( - label=_(u"Date"), required=False, widget=DatePicker, + label=_("Date"), required=False, widget=DatePicker, initial=datetime.date.today ) class FindPackagingFormSelection(FindMultipleFormSelection): - form_label = _(u"Packaged finds") + form_label = _("Packaged finds") class LocalisationForm(CustomForm, forms.Form): - form_admin_name = _(u"Container - 020 - Localisation") + form_admin_name = _("Container - 020 - Localisation") form_slug = "container-020-localisation" - form_label = _(u"Localisation") + form_label = _("Localisation") def __init__(self, *args, **kwargs): self.container, self.warehouse = None, None @@ -524,7 +554,7 @@ class LocalisationForm(CustomForm, forms.Form): if not self.warehouse: return for divlink in self.warehouse.divisions.order_by('order').all(): - initial = u"-" + initial = "-" if self.container: q = models.ContainerLocalisation.objects.filter( division__division=divlink.division, @@ -537,5 +567,5 @@ class LocalisationForm(CustomForm, forms.Form): class ContainerDeletionForm(FinalForm): - confirm_msg = _(u"Would you like to delete this container?") - confirm_end_msg = _(u"Would you like to delete this container?") + confirm_msg = _("Would you like to delete this container?") + confirm_end_msg = _("Would you like to delete this container?") diff --git a/archaeological_warehouse/ishtar_menu.py b/archaeological_warehouse/ishtar_menu.py index b7c820d13..17acae47a 100644 --- a/archaeological_warehouse/ishtar_menu.py +++ b/archaeological_warehouse/ishtar_menu.py @@ -58,6 +58,14 @@ MENU_SECTIONS = [ model=models.Warehouse, access_controls=['change_container', 'change_own_container']), + MenuItem( + 'container-merge', _(u"Automatic merge"), + model=models.Container, + access_controls=['administrator']), + MenuItem( + 'container-manual-merge', _(u"Manual merge"), + model=models.Container, + access_controls=['administrator']), MenuItem('container_deletion', _(u"Deletion"), model=models.Warehouse, access_controls=['change_container', diff --git a/archaeological_warehouse/migrations/0107_auto_20200407_1553.py b/archaeological_warehouse/migrations/0107_auto_20200407_1553.py new file mode 100644 index 000000000..56aeddd47 --- /dev/null +++ b/archaeological_warehouse/migrations/0107_auto_20200407_1553.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.27 on 2020-04-07 15:53 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('archaeological_warehouse', '0106_auto_20200407_1414'), + ] + + operations = [ + migrations.AddField( + model_name='container', + name='archived', + field=models.NullBooleanField(default=False), + ), + migrations.AddField( + model_name='container', + name='merge_candidate', + field=models.ManyToManyField(blank=True, related_name='_container_merge_candidate_+', to='archaeological_warehouse.Container'), + ), + migrations.AddField( + model_name='container', + name='merge_exclusion', + field=models.ManyToManyField(blank=True, related_name='_container_merge_exclusion_+', to='archaeological_warehouse.Container'), + ), + migrations.AddField( + model_name='container', + name='merge_key', + field=models.TextField(blank=True, null=True, verbose_name='Merge key'), + ), + ] diff --git a/archaeological_warehouse/models.py b/archaeological_warehouse/models.py index e62575ccf..53e50976d 100644 --- a/archaeological_warehouse/models.py +++ b/archaeological_warehouse/models.py @@ -35,7 +35,8 @@ from ishtar_common.models import Document, GeneralType, get_external_id, \ LightHistorizedItem, OwnPerms, Address, Person, post_save_cache, \ DashboardFormItem, ShortMenuItem, Organization, OrganizationType, \ document_attached_changed, SearchAltName, DynamicRequest, GeoItem, \ - QRCodeItem, SearchVectorConfig, DocumentItem, QuickAction, MainItem + QRCodeItem, SearchVectorConfig, DocumentItem, QuickAction, MainItem, \ + Merge from ishtar_common.model_merging import merge_model_objects from ishtar_common.utils import cached_label_changed, \ cached_label_and_geo_changed @@ -43,8 +44,8 @@ from ishtar_common.utils import cached_label_changed, \ class WarehouseType(GeneralType): class Meta: - verbose_name = _(u"Warehouse type") - verbose_name_plural = _(u"Warehouse types") + verbose_name = _("Warehouse type") + verbose_name_plural = _("Warehouse types") ordering = ('label',) @@ -97,7 +98,7 @@ class Warehouse(Address, DocumentItem, GeoItem, QRCodeItem, DashboardFormItem, QA_LOCK = QuickAction( url="warehouse-qa-lock", icon_class="fa fa-lock", - text=_(u"Lock/Unlock"), target="many", + text=_("Lock/Unlock"), target="many", rights=['change_warehouse', 'change_own_warehouse'] ) QUICK_ACTIONS = [QA_LOCK] @@ -105,41 +106,41 @@ class Warehouse(Address, DocumentItem, GeoItem, QRCodeItem, DashboardFormItem, objects = UUIDModelManager() uuid = models.UUIDField(default=uuid.uuid4) - name = models.CharField(_(u"Name"), max_length=200) + name = models.CharField(_("Name"), max_length=200) warehouse_type = models.ForeignKey(WarehouseType, - verbose_name=_(u"Warehouse type")) + verbose_name=_("Warehouse type")) person_in_charge = models.ForeignKey( Person, on_delete=models.SET_NULL, related_name='warehouse_in_charge', - verbose_name=_(u"Person in charge"), null=True, blank=True) + verbose_name=_("Person in charge"), null=True, blank=True) organization = models.ForeignKey( Organization, blank=True, null=True, related_name='warehouses', verbose_name=_("Organization"), on_delete=models.SET_NULL) - comment = models.TextField(_(u"Comment"), null=True, blank=True) + comment = models.TextField(_("Comment"), null=True, blank=True) associated_divisions = models.ManyToManyField( 'WarehouseDivision', verbose_name=_("Divisions"), blank=True, through='WarehouseDivisionLink' ) documents = models.ManyToManyField( - Document, related_name='warehouses', verbose_name=_(u"Documents"), + Document, related_name='warehouses', verbose_name=_("Documents"), blank=True) main_image = models.ForeignKey( Document, related_name='main_image_warehouses', on_delete=models.SET_NULL, - verbose_name=_(u"Main image"), blank=True, null=True) - external_id = models.TextField(_(u"External ID"), blank=True, null=True) + verbose_name=_("Main image"), blank=True, null=True) + external_id = models.TextField(_("External ID"), blank=True, null=True) auto_external_id = models.BooleanField( - _(u"External ID is set automatically"), default=False) + _("External ID is set automatically"), default=False) SUB_ADDRESSES = ["organization", "person_in_charge"] class Meta: - verbose_name = _(u"Warehouse") - verbose_name_plural = _(u"Warehouses") + verbose_name = _("Warehouse") + verbose_name_plural = _("Warehouses") permissions = ( - ("view_warehouse", u"Can view all Warehouses"), - ("view_own_warehouse", u"Can view own Warehouse"), - ("add_own_warehouse", u"Can add own Warehouse"), - ("change_own_warehouse", u"Can change own Warehouse"), - ("delete_own_warehouse", u"Can delete own Warehouse"), + ("view_warehouse", "Can view all Warehouses"), + ("view_own_warehouse", "Can view own Warehouse"), + ("add_own_warehouse", "Can add own Warehouse"), + ("change_own_warehouse", "Can change own Warehouse"), + ("delete_own_warehouse", "Can delete own Warehouse"), ) indexes = [ GinIndex(fields=['data']), @@ -152,7 +153,7 @@ class Warehouse(Address, DocumentItem, GeoItem, QRCodeItem, DashboardFormItem, return (self.uuid, ) def _get_base_image_path(self): - return u"{}/{}".format(self.SLUG, self.external_id) + return "{}/{}".format(self.SLUG, self.external_id) def create_attached_organization(self): """ @@ -360,8 +361,8 @@ post_save.connect(cached_label_and_geo_changed, sender=Warehouse) class WarehouseDivision(GeneralType): class Meta: - verbose_name = _(u"Warehouse division type") - verbose_name_plural = _(u"Warehouse division types") + verbose_name = _("Warehouse division type") + verbose_name_plural = _("Warehouse division types") post_save.connect(post_save_cache, sender=WarehouseDivision) @@ -379,16 +380,16 @@ class ContainerType(GeneralType): _("Stationary"), default=False, help_text=_("Container that usually will not be moved. Ex: building, " "room.")) - length = models.IntegerField(_(u"Length (mm)"), blank=True, null=True) - width = models.IntegerField(_(u"Width (mm)"), blank=True, null=True) - height = models.IntegerField(_(u"Height (mm)"), blank=True, null=True) - volume = models.FloatField(_(u"Volume (l)"), blank=True, null=True) - reference = models.CharField(_(u"Ref."), max_length=300, blank=True, + length = models.IntegerField(_("Length (mm)"), blank=True, null=True) + width = models.IntegerField(_("Width (mm)"), blank=True, null=True) + height = models.IntegerField(_("Height (mm)"), blank=True, null=True) + volume = models.FloatField(_("Volume (l)"), blank=True, null=True) + reference = models.CharField(_("Ref."), max_length=300, blank=True, null=True) class Meta: - verbose_name = _(u"Container type") - verbose_name_plural = _(u"Container types") + verbose_name = _("Container type") + verbose_name_plural = _("Container types") ordering = ('label',) @@ -413,7 +414,7 @@ class WarehouseDivisionLink(models.Model): unique_together = ('warehouse', 'division') def __str__(self): - return u"{} - {}".format(self.warehouse, self.division) + return "{} - {}".format(self.warehouse, self.division) def natural_key(self): return self.warehouse.uuid, self.division.txt_idx @@ -458,7 +459,7 @@ class ContainerTree: """ -class Container(DocumentItem, LightHistorizedItem, QRCodeItem, GeoItem, +class Container(DocumentItem, Merge, LightHistorizedItem, QRCodeItem, GeoItem, OwnPerms, MainItem): SLUG = 'container' APP = "archaeological-warehouse" @@ -659,7 +660,7 @@ class Container(DocumentItem, LightHistorizedItem, QRCodeItem, GeoItem, QA_LOCK = QuickAction( url="container-qa-lock", icon_class="fa fa-lock", - text=_(u"Lock/Unlock"), target="many", + text=_("Lock/Unlock"), target="many", rights=['change_container', 'change_own_container'] ) QUICK_ACTIONS = [QA_LOCK] @@ -678,13 +679,13 @@ class Container(DocumentItem, LightHistorizedItem, QRCodeItem, GeoItem, ) container_type = models.ForeignKey(ContainerType, verbose_name=_("Container type")) - reference = models.TextField(_(u"Container ref.")) - comment = models.TextField(_(u"Comment"), null=True, blank=True) - cached_label = models.TextField(_(u"Localisation"), null=True, blank=True, + reference = models.TextField(_("Container ref.")) + comment = models.TextField(_("Comment"), null=True, blank=True) + cached_label = models.TextField(_("Localisation"), null=True, blank=True, db_index=True) - cached_location = models.TextField(_(u"Cached location"), + cached_location = models.TextField(_("Cached location"), null=True, blank=True, db_index=True) - cached_division = models.TextField(_(u"Cached division"), + cached_division = models.TextField(_("Cached division"), null=True, blank=True, db_index=True) parent = models.ForeignKey("Container", verbose_name=_("Parent container"), on_delete=models.SET_NULL, @@ -712,11 +713,11 @@ class Container(DocumentItem, LightHistorizedItem, QRCodeItem, GeoItem, ('location', 'container_type', 'parent', 'reference')] permissions = ( - ("view_container", u"Can view all Containers"), - ("view_own_container", u"Can view own Container"), - ("add_own_container", u"Can add own Container"), - ("change_own_container", u"Can change own Container"), - ("delete_own_container", u"Can delete own Container"), + ("view_container", "Can view all Containers"), + ("view_own_container", "Can view own Container"), + ("add_own_container", "Can add own Container"), + ("change_own_container", "Can change own Container"), + ("delete_own_container", "Can delete own Container"), ) indexes = [ GinIndex(fields=['data']), @@ -726,6 +727,10 @@ class Container(DocumentItem, LightHistorizedItem, QRCodeItem, GeoItem, return self.cached_label or "" @property + def name(self): + return "{} - {}".format(self.container_type.name, self.reference) + + @property def short_label(self): return "{} {}".format(self.container_type.label, self.reference) @@ -767,6 +772,7 @@ class Container(DocumentItem, LightHistorizedItem, QRCodeItem, GeoItem, return self.location._get_base_image_path() + "/" + self.external_id def merge(self, item, keep_old=False): + # TODO: change localisation management locas = [ cl.division.division.txt_idx for cl in ContainerLocalisation.objects.filter(container=self).all() @@ -1050,7 +1056,7 @@ class Container(DocumentItem, LightHistorizedItem, QRCodeItem, GeoItem, if can_edit_find: actions += [ (reverse('container-add-treatment', args=[self.pk]), - _(u"Add treatment"), "fa fa-flask", "", "", False), + _("Add treatment"), "fa fa-flask", "", "", False), ] return actions @@ -1137,21 +1143,21 @@ class ContainerLocalisationManager(models.Manager): class ContainerLocalisation(models.Model): - container = models.ForeignKey(Container, verbose_name=_(u"Container"), + container = models.ForeignKey(Container, verbose_name=_("Container"), related_name='division') division = models.ForeignKey(WarehouseDivisionLink, - verbose_name=_(u"Division")) - reference = models.CharField(_(u"Reference"), max_length=200, default='') + verbose_name=_("Division")) + reference = models.CharField(_("Reference"), max_length=200, default='') objects = ContainerLocalisationManager() class Meta: - verbose_name = _(u"Container localisation") - verbose_name_plural = _(u"Container localisations") + verbose_name = _("Container localisation") + verbose_name_plural = _("Container localisations") unique_together = ('container', 'division') ordering = ('container', 'division__order') def __str__(self): - lbl = u" - ".join((str(self.container), + lbl = " - ".join((str(self.container), str(self.division), self.reference)) return lbl diff --git a/archaeological_warehouse/templates/ishtar/merge_container.html b/archaeological_warehouse/templates/ishtar/merge_container.html new file mode 100644 index 000000000..558828586 --- /dev/null +++ b/archaeological_warehouse/templates/ishtar/merge_container.html @@ -0,0 +1,23 @@ +{% extends "ishtar/merge.html" %} +{% block merge_field_row %} + {% if form.non_field_errors %}<tr><td colspan='4'></td><td colspan='3' class='errorlist'>{% for error in form.non_field_errors %}{{error}} {% endfor%}</tr>{% endif %} + <tr> + <td> + <a href="#" onclick="load_window('{% url 'show-container' form.instance.from_container.pk '' %}', 'container');" class="display_details"><i class="fa fa-info-circle" aria-hidden="true"></i></a> + </td> + <td> + {{form.instance.from_container}} ({{form.instance.from_container.pk}})<br/> + {{form.instance.from_container.address_lbl|linebreaksbr}} + </td> + <td> + <a href="#" onclick="load_window('{% url 'show-container' form.instance.to_container.pk '' %}', 'container');" class="display_details"><i class="fa fa-info-circle" aria-hidden="true"></i></a> + </td> + <td> + {{form.instance.to_container}} ({{form.instance.to_container.pk}})<br/> + {{form.instance.to_container.address_lbl|linebreaksbr}} + </td> + <td class='check'>{{form.b_is_duplicate_a}}</td> + <td class='check'>{{form.a_is_duplicate_b}}</td> + <td class='check'>{{form.not_duplicate}}</td> + </tr> +{% endblock %} diff --git a/archaeological_warehouse/tests.py b/archaeological_warehouse/tests.py index a342dc1f9..37babf1b2 100644 --- a/archaeological_warehouse/tests.py +++ b/archaeological_warehouse/tests.py @@ -538,6 +538,10 @@ class ContainerTest(FindInit, TestCase): label='division2') self.div_link = models.WarehouseDivisionLink.objects.create( warehouse=self.main_warehouse, division=self.division) + self.alt_warehouse = models.Warehouse.objects.create( + name="Alt", + warehouse_type=models.WarehouseType.objects.all()[0] + ) def test_form_creation(self): data = { @@ -718,3 +722,60 @@ class ContainerTest(FindInit, TestCase): container_2.external_id, ct.txt_idx, container_3.reference)) + def test_merge_candidate(self): + ct = models.ContainerType.objects.all()[0] + container_1 = models.Container.objects.create( + reference="Test", responsible=self.main_warehouse, + location=self.main_warehouse, + container_type=ct) + init_mc = container_1.merge_candidate.count() + models.Container.objects.create( + reference="TEST", responsible=self.main_warehouse, + location=self.main_warehouse, + container_type=ct) + self.assertEqual(container_1.merge_candidate.count(), + init_mc + 1) + container_1.archive() + self.assertEqual(container_1.merge_candidate.count(), + init_mc) + + def test_merge_container(self): + ct = models.ContainerType.objects.all()[0] + ct2 = models.ContainerType.objects.all()[1] + self.create_finds() + self.create_finds() + self.create_finds() + find0 = self.finds[0] + find1 = self.finds[1] + find2 = self.finds[2] + + container_1 = models.Container.objects.create( + reference="Test 1", + location=self.main_warehouse, + container_type=ct) + find0.container = container_1 + find0.container_ref = container_1 + find0.save() + find1.container = container_1 + find1.container_ref = container_1 + find1.save() + + container_2 = models.Container.objects.create( + reference="Test 2", + location=self.alt_warehouse, + container_type=ct2) + find2.container = container_2 + find2.container_ref = container_2 + find2.save() + + container_1.merge(container_2) + container_1 = models.Container.objects.get(pk=container_1.pk) + self.assertEqual( + models.Container.objects.filter(pk=container_2.pk).count(), 0) + + # preserve existing fields + self.assertEqual(container_1.reference, 'Test 1') + self.assertEqual(container_1.container_type, ct) + find_lst = [f for f in container_1.finds.all()] + for f in [find0, find1, find2]: + self.assertIn(f, find_lst) diff --git a/archaeological_warehouse/urls.py b/archaeological_warehouse/urls.py index 6519a5db4..5c08af2ad 100644 --- a/archaeological_warehouse/urls.py +++ b/archaeological_warehouse/urls.py @@ -103,4 +103,13 @@ urlpatterns = [ url(r'^container-qa-lock/(?P<pks>[0-9-]+)?/$', views.QAContainerLockView.as_view(), name='container-qa-lock', kwargs={"model": models.Container}), + + url(r'container-merge/(?:(?P<page>\d+)/)?$', views.container_merge, + name='container_merge'), + url(r'container-manual-merge/$', + views.ContainerManualMerge.as_view(), + name='container_manual_merge'), + url(r'container-manual-merge-items/(?P<pks>[0-9_]+?)/$', + views.ContainerManualMergeItems.as_view(), + name='container_manual_merge_items'), ] diff --git a/archaeological_warehouse/views.py b/archaeological_warehouse/views.py index 29f33dc04..cb74b49f9 100644 --- a/archaeological_warehouse/views.py +++ b/archaeological_warehouse/views.py @@ -21,21 +21,19 @@ import json from django.core.urlresolvers import reverse from django.db.models import Q +from django.views.generic.edit import FormView from django.http import HttpResponse, Http404, HttpResponseRedirect from django.shortcuts import redirect from ishtar_common.utils import ugettext_lazy as _ from archaeological_warehouse import models +from archaeological_warehouse import forms -from archaeological_warehouse.forms import WarehouseForm, ContainerForm, \ - ContainerFormSelection, BasePackagingForm, WarehouseFormSelection, \ - WarehouseModifyForm, SelectedDivisionFormset, WarehouseDeletionForm, \ - MainContainerFormSelection, ContainerModifyForm, LocalisationForm, \ - ContainerDeletionForm, ContainerSelect, WarehouseSelect, \ - MainContainerFormMultiSelection, WarehouseFormMultiSelection from ishtar_common.forms import FinalForm -from ishtar_common.views import QABaseLockView, wizard_is_available +from ishtar_common.views import QABaseLockView, wizard_is_available, \ + merge_action, ManualMergeMixin, ManualMergeItemsMixin, IshtarMixin, \ + LoginRequiredMixin from ishtar_common.views_item import get_item, show_item, new_qa_item from archaeological_finds.views import treatment_add @@ -44,18 +42,17 @@ from archaeological_warehouse.wizards import PackagingWizard, WarehouseSearch, \ ContainerSearch, ContainerWizard, ContainerModificationWizard, \ ContainerDeletionWizard -from ishtar_common.utils import put_session_message get_container = get_item(models.Container, 'get_container', 'container', - search_form=ContainerSelect) + search_form=forms.ContainerSelect) show_container = show_item(models.Container, 'container') get_warehouse = get_item(models.Warehouse, 'get_warehouse', 'warehouse', - search_form=WarehouseSelect) + search_form=forms.WarehouseSelect) show_warehouse = show_item(models.Warehouse, 'warehouse') -new_warehouse = new_qa_item(models.Warehouse, WarehouseForm) -new_container = new_qa_item(models.Container, ContainerForm) +new_warehouse = new_qa_item(models.Warehouse, forms.WarehouseForm) +new_container = new_qa_item(models.Container, forms.ContainerForm) def autocomplete_warehouse(request): @@ -115,36 +112,36 @@ def autocomplete_container(request): return HttpResponse(data, content_type='text/plain') warehouse_packaging_wizard = PackagingWizard.as_view([ # AFAC - ('seleccontainer-packaging', ContainerFormSelection), - ('base-packaging', BasePackagingForm), + ('seleccontainer-packaging', forms.ContainerFormSelection), + ('base-packaging', forms.BasePackagingForm), ('final-packaging', FinalForm)], - label=_(u"Packaging"), + label=_("Packaging"), url_name='warehouse_packaging',) warehouse_search_wizard = WarehouseSearch.as_view([ - ('selec-warehouse_search', WarehouseFormSelection)], - label=_(u"Warehouse search"), + ('selec-warehouse_search', forms.WarehouseFormSelection)], + label=_("Warehouse search"), url_name='warehouse_search', ) warehouse_creation_steps = [ - ("warehouse-warehouse_creation", WarehouseForm), - ('divisions-warehouse_creation', SelectedDivisionFormset), + ("warehouse-warehouse_creation", forms.WarehouseForm), + ('divisions-warehouse_creation', forms.SelectedDivisionFormset), ('final-warehouse_creation', FinalForm)] warehouse_creation_wizard = WarehouseWizard.as_view( warehouse_creation_steps, - label=_(u"Warehouse creation"), + label=_("Warehouse creation"), url_name='warehouse_creation', ) warehouse_modification_wizard = WarehouseModificationWizard.as_view([ - ('selec-warehouse_modification', WarehouseFormSelection), - ("warehouse-warehouse_modification", WarehouseModifyForm), - ('divisions-warehouse_modification', SelectedDivisionFormset), + ('selec-warehouse_modification', forms.WarehouseFormSelection), + ("warehouse-warehouse_modification", forms.WarehouseModifyForm), + ('divisions-warehouse_modification', forms.SelectedDivisionFormset), ('final-warehouse_modification', FinalForm)], - label=_(u"Warehouse modification"), + label=_("Warehouse modification"), url_name='warehouse_modification', ) @@ -161,9 +158,9 @@ def warehouse_modify(request, pk): warehouse_deletion_wizard = WarehouseDeletionWizard.as_view([ - ('selec-warehouse_deletion', WarehouseFormMultiSelection), - ('final-warehouse_deletion', WarehouseDeletionForm)], - label=_(u"Warehouse deletion"), + ('selec-warehouse_deletion', forms.WarehouseFormMultiSelection), + ('final-warehouse_deletion', forms.WarehouseDeletionForm)], + label=_("Warehouse deletion"), url_name='warehouse_deletion',) @@ -184,28 +181,28 @@ class QAWarehouseLockView(QABaseLockView): container_search_wizard = ContainerSearch.as_view([ - ('selec-container_search', MainContainerFormSelection)], - label=_(u"Container search"), + ('selec-container_search', forms.MainContainerFormSelection)], + label=_("Container search"), url_name='container_search', ) container_creation_steps = [ - ('container-container_creation', ContainerForm), - ('localisation-container_creation', LocalisationForm), + ('container-container_creation', forms.ContainerForm), + ('localisation-container_creation', forms.LocalisationForm), ('final-container_creation', FinalForm)] container_creation_wizard = ContainerWizard.as_view( container_creation_steps, - label=_(u"Container creation"), + label=_("Container creation"), url_name='container_creation', ) container_modification_wizard = ContainerModificationWizard.as_view([ - ('selec-container_modification', MainContainerFormSelection), - ('container-container_modification', ContainerModifyForm), - ('localisation-container_modification', LocalisationForm), + ('selec-container_modification', forms.MainContainerFormSelection), + ('container-container_modification', forms.ContainerModifyForm), + ('localisation-container_modification', forms.LocalisationForm), ('final-container_modification', FinalForm)], - label=_(u"Container modification"), + label=_("Container modification"), url_name='container_modification', ) @@ -222,9 +219,9 @@ def container_modify(request, pk): container_deletion_wizard = ContainerDeletionWizard.as_view([ - ('selec-container_deletion', MainContainerFormMultiSelection), - ('final-container_deletion', ContainerDeletionForm)], - label=_(u"Container deletion"), + ('selec-container_deletion', forms.MainContainerFormMultiSelection), + ('final-container_deletion', forms.ContainerDeletionForm)], + label=_("Container deletion"), url_name='container_deletion',) @@ -255,6 +252,28 @@ warehouse_packaging_wizard = ItemSourceWizard.as_view([ """ +container_merge = merge_action(models.Container, forms.MergeContainerForm, + 'container', name_key="reference") + + +class ContainerManualMerge(ManualMergeMixin, IshtarMixin, LoginRequiredMixin, + FormView): + form_class = forms.ContainerMergeFormSelection + template_name = 'ishtar/form.html' + page_name = _("Merge containers") + current_url = 'container-manual-merge' + redir_url = 'container_manual_merge_items' + + +class ContainerManualMergeItems(ManualMergeItemsMixin, IshtarMixin, + LoginRequiredMixin, FormView): + form_class = forms.ContainerMergeIntoForm + template_name = 'ishtar/form.html' + page_name = _("Select the main container") + current_url = 'container-manual-merge-items' + item_type = 'container' + + class QAContainerLockView(QABaseLockView): model = models.Container base_url = "container-qa-lock" |