#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (C) 2010-2016 É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. import json import itertools from unidecode import unidecode from django.core.urlresolvers import reverse from django.db.models import Q from django.views.generic import RedirectView 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 ishtar_common.forms import FinalForm from ishtar_common.views import ( QABaseLockView, wizard_is_available, merge_action, ManualMergeMixin, ManualMergeItemsMixin, IshtarMixin, LoginRequiredMixin, QAItemEditForm, ) from ishtar_common.views_item import get_item, show_item, new_qa_item, revert_item from archaeological_finds.views import treatment_add from archaeological_warehouse.wizards import ( PackagingWizard, WarehouseSearch, WarehouseWizard, WarehouseModificationWizard, WarehouseDeletionWizard, ContainerSearch, ContainerWizard, ContainerModificationWizard, ContainerDeletionWizard, ) get_container = get_item( models.Container, "get_container", "container", search_form=forms.ContainerSelect ) get_divisions_container = get_item( models.Container, "get_container", "container", search_form=forms.ContainerSelect, base_request={"container_type__stationary": "True"}, ) get_non_divisions_container = get_item( models.Container, "get_container", "container", search_form=forms.ContainerSelect, base_request={"container_type__stationary": "False"}, ) show_container = show_item(models.Container, "container") get_warehouse = get_item( models.Warehouse, "get_warehouse", "warehouse", search_form=forms.WarehouseSelect ) show_warehouse = show_item(models.Warehouse, "warehouse") revert_warehouse = revert_item(models.Warehouse) new_warehouse = new_qa_item( models.Warehouse, forms.WarehouseForm, page_name=_("New warehouse") ) new_container = new_qa_item( models.Container, forms.ContainerForm, page_name=_("New container") ) def autocomplete_warehouse(request): if not request.user.has_perm( "ishtar_common.view_warehouse", models.Warehouse ) and not request.user.has_perm( "ishtar_common.view_own_warehouse", models.Warehouse ): return HttpResponse(content_type="text/plain") if not request.GET.get("term"): return HttpResponse(content_type="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": str(warehouse)} for warehouse in warehouses] ) return HttpResponse(data, content_type="text/plain") def autocomplete_container(request, warehouse_id=None): if not request.user.has_perm( "ishtar_common.view_warehouse", models.Warehouse ) and not request.user.has_perm( "ishtar_common.view_own_warehouse", models.Warehouse ): return HttpResponse(content_type="text/plain") if not request.GET.get("term"): return HttpResponse(content_type="text/plain") term = request.GET.get("term").strip() limit = 15 base_query = Q() if warehouse_id: base_query = Q(location_id=warehouse_id) try: w = models.Warehouse.objects.get(id=warehouse_id) except models.Warehouse.DoesNotExist: return HttpResponse(content_type="text/plain") # remove warehouse name if warehouse already set lower_term = unidecode(term).lower() warehouse_name = unidecode(w.name).lower() if warehouse_name in lower_term: idx = lower_term.index(warehouse_name) term = (term[:idx] + term[idx + len(warehouse_name) :]).strip() query = base_query # exact index try: query = query & Q(index=int(term)) containers = list( models.Container.objects.filter(query).values("id", "cached_label")[:limit] ) except ValueError: containers = [] # exact ref query = query & Q(reference__unaccent__iexact=term) containers += list( models.Container.objects.filter(query).values("id", "cached_label")[:limit] ) limit = 15 - len(containers) splitted = [s.lower() for s in term.split(" ") if s and s != "|"] unaccent_splitted = [ unidecode(s).lower() for s in term.split(" ") if s and s != "|" ] if limit > 0 and len(splitted) > 1: type_positions = [] # container_type ID, pos inf, pos sup container_types = [ (c[0], unidecode(c[1]).lower()) for c in models.ContainerType.objects.values_list("id", "label") ] for container_type_id, value in container_types: value = unidecode(value).lower() if value in unidecode(term).lower(): # container_type is in search q values = [unidecode(v.lower()) for v in value.split(" ") if v] # verify that all term match in splitted try: index = unaccent_splitted.index(values[0]) except ValueError: index = None index_is_ok = False while not index_is_ok and index is not None: for idx, v in enumerate(values): try: assert unaccent_splitted[index + idx] == v except (ValueError, AssertionError): break index_is_ok = True if not index_is_ok: try: index = unaccent_splitted.index(values[0], index + 1) except ValueError: index = None if index_is_ok: type_positions.append( (container_type_id, index, index + len(values)) ) query = base_query if not warehouse_id and type_positions and type_positions[0][1] > 0: for idx in range(type_positions[0][1]): query &= Q(location__name__icontains=splitted[idx]) # group by container type, ref tuple # can be in any order if type_positions: for positions in itertools.permutations(type_positions): groups = [] for idx, (container_type_id, pos_inf, pos_sup) in enumerate(positions): next_positions = [ p[1] for p in positions if p[0] != container_type_id and p[1] > pos_sup ] if not next_positions: value = " ".join(splitted[pos_sup:]) else: value = " ".join(splitted[pos_sup : min(next_positions)]) if value: groups.append((container_type_id, value)) query = base_query for idx, g in enumerate(groups): base_key = "parent__" * idx key1 = base_key + "container_type_id" key2 = base_key + "reference__unaccent__iexact" query &= Q(**{key1: g[0], key2: g[1]}) ids = {c["id"] for c in containers} containers += list( models.Container.objects.filter(query) .exclude(pk__in=ids) .values("id", "cached_label")[:limit] ) if len(splitted) > 1 and len(containers) < 15: # group to do a "type" "reference" search for idx in range(1, len(splitted)): group_1 = splitted[:idx] group_2 = splitted[idx:] extra = Q( container_type__label__unaccent__iexact=" ".join(group_1), reference__unaccent__iexact=" ".join(group_2), ) query = base_query & extra ids = {c["id"] for c in containers} containers += list( models.Container.objects.filter(query) .exclude(pk__in=ids) .values("id", "cached_label")[:limit] ) if len(containers) >= 15: break if len(containers) < 15: query = base_query for q in splitted: extra = Q(reference__unaccent__iexact=q) query = query & extra ids = {c["id"] for c in containers} containers += list( models.Container.objects.filter(query) .exclude(pk__in=ids) .values("id", "cached_label")[:limit] ) limit = 15 - len(containers) if limit > 0: query = base_query for q in splitted: extra = ( Q(container_type__label__unaccent__icontains=q) | Q(container_type__reference__unaccent__icontains=q) | Q(reference__unaccent__icontains=q) | Q(cached_label__unaccent__icontains=q) ) if not warehouse_id: extra |= Q(location__name__unaccent=q) | Q( location__town__unaccent=q ) query = query & extra ids = {c["id"] for c in containers} containers += list( models.Container.objects.filter(query) .exclude(pk__in=ids) .values("id", "cached_label") .order_by("-index", "id")[:limit] ) # order by set to display static container first (no index) and # first created in db data = json.dumps( [ {"id": container["id"], "value": container["cached_label"]} for container in containers ] ) return HttpResponse(data, content_type="text/plain") warehouse_packaging_wizard = PackagingWizard.as_view( [ # AFAC ("seleccontainer-packaging", forms.ContainerFormSelection), ("base-packaging", forms.BasePackagingForm), ("final-packaging", FinalForm), ], label=_("Packaging"), url_name="warehouse_packaging", ) warehouse_search_wizard = WarehouseSearch.as_view( [("selec-warehouse_search", forms.WarehouseFormSelection)], label=_("Warehouse search"), url_name="warehouse_search", ) warehouse_creation_steps = [ ("warehouse-warehouse_creation", forms.WarehouseForm), ("divisions-warehouse_creation", forms.SelectedDivisionFormset), ("final-warehouse_creation", FinalForm), ] warehouse_creation_wizard = WarehouseWizard.as_view( warehouse_creation_steps, label=_("Warehouse creation"), url_name="warehouse_creation", ) warehouse_modification_wizard = WarehouseModificationWizard.as_view( [ ("selec-warehouse_modification", forms.WarehouseFormSelection), ("warehouse-warehouse_modification", forms.WarehouseModifyForm), ("divisions-warehouse_modification", forms.SelectedDivisionFormset), ("final-warehouse_modification", FinalForm), ], label=_("Warehouse modification"), url_name="warehouse_modification", ) def warehouse_modify(request, pk): if not wizard_is_available( warehouse_modification_wizard, request, models.Warehouse, pk ): return HttpResponseRedirect("/") wizard_url = "warehouse_modification" WarehouseModificationWizard.session_set_value( request, "selec-" + wizard_url, "pk", pk, reset=True ) return redirect(reverse(wizard_url, kwargs={"step": "warehouse-" + wizard_url})) warehouse_deletion_wizard = WarehouseDeletionWizard.as_view( [ ("selec-warehouse_deletion", forms.WarehouseFormMultiSelection), ("final-warehouse_deletion", forms.WarehouseDeletionForm), ], label=_("Warehouse deletion"), url_name="warehouse_deletion", ) def warehouse_delete(request, pk): if not wizard_is_available( warehouse_deletion_wizard, request, models.Warehouse, pk ): return HttpResponseRedirect("/") wizard_url = "warehouse_deletion" WarehouseDeletionWizard.session_set_value( request, "selec-" + wizard_url, "pks", pk, reset=True ) return redirect(reverse(wizard_url, kwargs={"step": "final-" + wizard_url})) class QAWarehouseLockView(QABaseLockView): model = models.Warehouse base_url = "warehouse-qa-lock" container_search_wizard = ContainerSearch.as_view( [("selec-container_search", forms.MainContainerFormSelection)], label=_("Container search"), url_name="container_search", ) container_creation_steps = [ ("container-container_creation", forms.ContainerForm), ("final-container_creation", FinalForm), ] container_creation_wizard = ContainerWizard.as_view( container_creation_steps, label=_("Container creation"), url_name="container_creation", ) container_modification_wizard = ContainerModificationWizard.as_view( [ ("selec-container_modification", forms.MainContainerFormSelection), ("container-container_modification", forms.ContainerModifyForm), ("final-container_modification", FinalForm), ], label=_("Container modification"), url_name="container_modification", ) def container_modify(request, pk): if not wizard_is_available( container_modification_wizard, request, models.Container, pk ): return HttpResponseRedirect("/") wizard_url = "container_modification" ContainerModificationWizard.session_set_value( request, "selec-" + wizard_url, "pk", pk, reset=True ) return redirect(reverse(wizard_url, kwargs={"step": "container-" + wizard_url})) container_deletion_wizard = ContainerDeletionWizard.as_view( [ ("selec-container_deletion", forms.MainContainerFormMultiSelection), ("final-container_deletion", forms.ContainerDeletionForm), ], label=_("Container deletion"), url_name="container_deletion", ) def container_delete(request, pk): if not wizard_is_available( container_deletion_wizard, request, models.Container, pk ): return HttpResponseRedirect("/") wizard_url = "container_deletion" ContainerDeletionWizard.session_set_value( request, "selec-" + wizard_url, "pks", pk, reset=True ) return redirect(reverse(wizard_url, kwargs={"step": "final-" + wizard_url})) def container_treatment_add(request, pk, current_right=None): try: container = models.Container.objects.get(pk=pk) except models.Container.DoesNotExist: raise Http404() return treatment_add(request, ",".join(str(f.pk) for f in container.finds.all())) """ warehouse_packaging_wizard = ItemSourceWizard.as_view([ ('selec-warehouse_packaging', ItemsSelection), ('final-warehouse_packaging', FinalForm)], url_name='warehouse_packaging',) """ 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" def reset_wizards(request): for wizard_class, url_name in ( (PackagingWizard, "warehouse_packaging"), (WarehouseWizard, "warehouse_creation"), (WarehouseModificationWizard, "warehouse_modification"), (WarehouseDeletionWizard, "warehouse_deletion"), (ContainerWizard, "container_creation"), (ContainerModificationWizard, "container_modification"), (ContainerDeletionWizard, "container_deletion"), ): wizard_class.session_reset(request, url_name) class QAContainerForm(QAItemEditForm): model = models.Container form_class = forms.QAContainerFormMulti def get_form_kwargs(self): kwargs = super(QAContainerForm, self).get_form_kwargs() # item list is necessary to verify uniqueness rules kwargs["items"] = self.items return kwargs class GenerateStats(IshtarMixin, LoginRequiredMixin, RedirectView): model = None def get_redirect_url(self, *args, **kwargs): return reverse("display-item", args=[self.model.SLUG, self.item.pk]) + "#stats" def get(self, request, *args, **kwargs): self.item = self.model.objects.get(pk=kwargs["pk"]) self.item._get_or_set_stats( "_number_of_finds_by_place", update=True, expected_type=list ) self.item._get_or_set_stats( "_number_of_containers_by_place", update=True, expected_type=list ) return super(GenerateStats, self).get(request, *args, **kwargs) class GenerateStatsContainer(GenerateStats): model = models.Container class GenerateStatsWarehouse(GenerateStats): model = models.Warehouse