diff options
23 files changed, 715 insertions, 60 deletions
diff --git a/archaeological_finds/fixtures/initial_data-fr.json b/archaeological_finds/fixtures/initial_data-fr.json index b969d15ca..09017e750 100644 --- a/archaeological_finds/fixtures/initial_data-fr.json +++ b/archaeological_finds/fixtures/initial_data-fr.json @@ -7089,6 +7089,7 @@ "label": "Exposition", "txt_idx": "exhibition", "comment": "", + "treatment_file_type": ["loan-for-exhibition"], "available": true } } diff --git a/archaeological_finds/forms_treatments.py b/archaeological_finds/forms_treatments.py index 88b26905d..c624ebfc6 100644 --- a/archaeological_finds/forms_treatments.py +++ b/archaeological_finds/forms_treatments.py @@ -25,6 +25,7 @@ from bootstrap_datepicker.widgets import DateField from django import forms from django.core import validators from ishtar_common.utils import ugettext_lazy as _ +from ishtar_common.forms import FormHeader from archaeological_finds import models from archaeological_operations.forms import AdministrativeActForm, \ @@ -705,16 +706,6 @@ class TreatmentFileFormSelectionMultiple(MultiSearchForm): validators=[valid_ids(models.TreatmentFile)]) -class ExhibitionFormSelection(TreatmentFileFormSelection): - pk = forms.CharField( - label="", required=False, - widget=widgets.DataTable( - reverse_lazy('get-exhibition'), - TreatmentFileSelect, models.TreatmentFile, - ), - validators=[valid_ids(models.TreatmentFile)]) - - class TreatmentFileForm(CustomForm, ManageOldType): form_label = _("Treatment request") base_models = ['treatment_type_type'] @@ -903,3 +894,224 @@ class AdministrativeActTreatmentFileModifForm( AdministrativeActModifForm, AdministrativeActTreatmentFileForm): pk = forms.IntegerField(required=False, widget=forms.HiddenInput) index = forms.IntegerField(label=_("Index"), required=False) + + +# Exhibitions + + +class ExhibitionSelect(DocumentItemSelect): + _model = models.Exhibition + form_admin_name = _("Exhibition - 001 - Search") + form_slug = "exhibition-001-search" + + search_vector = forms.CharField( + label=_("Full text search"), widget=widgets.SearchWidget( + 'archaeological-finds', 'Exhibition' + )) + name = forms.CharField(label=_("Name")) + exhibition_type = forms.ChoiceField(label=_("Type"), choices=[]) + year = forms.IntegerField(label=_("Year")) + reference = forms.CharField(label=_("Reference")) + in_charge = forms.IntegerField( + label=_("In charge"), + widget=widgets.JQueryAutoComplete( + reverse_lazy('autocomplete-person'), + associated_model=Person), + validators=[valid_id(Person)]) + TYPES = [ + FieldType("exhibition_type", models.ExhibitionType), + ] + + +class ExhibitionFormSelection(CustomForm, forms.Form): + SEARCH_AND_SELECT = True + form_label = _("Exhibition search") + associated_models = {'pk': models.Exhibition} + currents = {'pk': models.Exhibition} + pk = forms.CharField( + label="", required=False, + widget=widgets.DataTable( + reverse_lazy('get-exhibition'), + ExhibitionSelect, models.Exhibition, + ), + validators=[valid_ids(models.Exhibition)]) + + +class ExhibitionForm(forms.ModelForm, CustomForm, ManageOldType): + form_label = _("Exhibition") + form_admin_name = _("Exhibition - 020 - Main form") + form_slug = "exhibition-20-general" + extra_form_modals = ["person"] + + pk = forms.IntegerField(label="", required=False, widget=forms.HiddenInput) + name = forms.CharField(label=_("Name"), max_length=500) + exhibition_type = forms.ChoiceField(label=_("Type"), choices=[]) + year = forms.IntegerField(label=_("Year"), + initial=lambda: datetime.datetime.now().year, + validators=[validators.MinValueValidator(1000), + validators.MaxValueValidator(2100)]) + reference = forms.CharField( + label=_("Reference"), max_length=500, required=False) + in_charge = forms.IntegerField( + label=_("Responsible"), + widget=widgets.JQueryAutoComplete( + reverse_lazy('autocomplete-person'), associated_model=Person, + new=True), + validators=[valid_id(Person)], required=False) + associated_basket_id = forms.IntegerField( + label=_("Associated basket"), + widget=widgets.JQueryAutoComplete( + reverse_lazy('autocomplete-findbasket'), + associated_model=models.FindBasket), required=False) + comment = forms.CharField(label=_("Comment"), + widget=forms.Textarea, required=False) + + class Meta: + model = models.Exhibition + fields = [ + "pk", + "name", + "exhibition_type", + "year", + "reference", + "in_charge", + "comment", + "associated_basket_id", + ] + + HEADERS = { + "name": FormHeader(_("General")), + } + TYPES = [ + FieldType("exhibition_type", models.ExhibitionType, empty_first=False), + ] + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop("user") + super().__init__(*args, **kwargs) + type_field = self.fields["exhibition_type"] + if len(type_field.choices) == 1: + type_field.initial = type_field.choices[0][0] + type_field.widget.attrs["readonly"] = True + + def clean_in_charge(self): + return self._clean_model_field("in_charge", Person) + + def clean_exhibition_type(self): + return self._clean_model_field("exhibition_type", models.ExhibitionType) + + def save(self, *args, **kwargs): + obj = super().save(*args, **kwargs) + obj = models.Exhibition.objects.get(pk=obj.pk) + if self.user and not obj.history_creator: + obj.history_creator = self.user + obj.history_modifier = self.user + obj.skip_history_when_saving = True + obj.save() + return obj + + +class QANewExhibitionLoanForm(IshtarForm): + extra_form_modals = [ + "applicant_organisation", "exhibition_location", + "insurance_provider", "in_charge" + ] + qa_exhibition_start = DateField(label=_("Exhibition start date")) + qa_exhibition_end = DateField(label=_("Exhibition end date")) + qa_applicant_organisation = forms.IntegerField( + label=_("Beneficiary of the loan"), + widget=widgets.JQueryAutoComplete( + reverse_lazy('autocomplete-organization'), + associated_model=Organization, # new=True + ), + validators=[valid_id(Organization)]) + qa_in_charge = forms.IntegerField( + label=_("Scientific manager of the exhibition"), + widget=widgets.JQueryAutoComplete( + reverse_lazy('autocomplete-person'), + associated_model=Person, # new=True + ), + validators=[valid_id(Person)]) + qa_exhibition_location = forms.IntegerField( + label=_("Exhibition location"), + widget=widgets.JQueryAutoComplete( + reverse_lazy('autocomplete-warehouse'), + associated_model=Warehouse, # new=True + ), + validators=[valid_id(Warehouse)], + help_text=_("The exhibition location must have an organization attached.") + ) + qa_insurance_provider = forms.IntegerField( + label=_("Insurance provider"), + widget=widgets.JQueryAutoComplete( + reverse_lazy('autocomplete-organization'), + associated_model=Organization, # new=True + ), + validators=[valid_id(Organization)], required=False) + + def __init__(self, *args, **kwargs): + self.user = None + if 'user' in kwargs: + self.user = kwargs.pop('user') + if hasattr(self.user, 'ishtaruser'): + self.user = self.user.ishtaruser + self.exhibition = kwargs.pop('items')[0] + super().__init__(*args, **kwargs) + + def clean(self): + data = self.cleaned_data + if not self.exhibition.associated_basket_id: + raise forms.ValidationError( + _("Cannot create loan when no basket is associated to this" + " exhibition.")) + return data + + def save(self): + basket = self.exhibition.associated_basket + if not basket: + return + values = { + "year": self.cleaned_data["qa_exhibition_start"].year, + "type": self.exhibition.exhibition_type.treatment_file_type, + "exhibition_name": self.exhibition.name, + "exhibition_start_date": self.cleaned_data["qa_exhibition_start"], + "exhibition_end_date": self.cleaned_data["qa_exhibition_end"], + "history_creator": self.user.user_ptr, + "history_modifier": self.user.user_ptr, + } + try: + exhibition_location = Warehouse.objects.get( + pk=self.cleaned_data["qa_exhibition_location"] + ) + except Warehouse.DoesNotExist: + return + loan_name = f"{self.exhibition.name} | {exhibition_location}" + values["name"] = loan_name + values["exhibition_location"] = exhibition_location + new_basket = basket.duplicate() + basket_label = f"{_('Exhibition')} | {loan_name}" + new_basket.label = basket_label + new_basket.save() + values["associated_basket_id"] = new_basket.id + try: + values["in_charge"] = Person.objects.get( + pk=self.cleaned_data["qa_in_charge"] + ) + except Person.DoesNotExist: + return + if self.cleaned_data.get("qa_insurance_provider", None): + try: + values["insurance_provider"] = Organization.objects.get( + pk=self.cleaned_data["qa_insurance_provider"] + ) + except Organization.DoesNotExist: + return + try: + values["applicant_organisation"] = Organization.objects.get( + pk=self.cleaned_data["qa_applicant_organisation"] + ) + except Organization.DoesNotExist: + return + obj = models.TreatmentFile.objects.create(**values) + self.exhibition.treatment_files.add(obj) + return obj diff --git a/archaeological_finds/migrations/0133_exhibition.py b/archaeological_finds/migrations/0133_exhibition.py index a4a29168e..9ef1aa7e8 100644 --- a/archaeological_finds/migrations/0133_exhibition.py +++ b/archaeological_finds/migrations/0133_exhibition.py @@ -17,9 +17,18 @@ import simple_history.models def create_default(apps, __): + TreatmentFileType = apps.get_model( + "archaeological_finds", "TreatmentFileType") + loan, __ = TreatmentFileType.objects.get_or_create( + txt_idx="loan-for-exhibition", + defaults={"label": "Exposition"} + ) + loan.is_exhibition = True + loan.save() ExhibitionType = apps.get_model("archaeological_finds", "ExhibitionType") ExhibitionType.objects.get_or_create( - txt_idx="exhibition", defaults={"label": "Exposition"} + txt_idx="exhibition", + defaults={"label": "Exposition", "treatment_file_type_id": loan.id} ) @@ -40,6 +49,7 @@ class Migration(migrations.Migration): ('txt_idx', models.TextField(help_text='The slug is the standardized version of the name. It contains only lowercase letters, numbers and hyphens. Each slug must be unique.', unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z'), "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.", 'invalid')], verbose_name='Textual ID')), ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), ('available', models.BooleanField(default=True, verbose_name='Available')), + ('treatment_file_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='archaeological_finds.TreatmentFileType', verbose_name='Treatment request type')), ], options={ 'verbose_name': 'Exhibition type', @@ -128,4 +138,24 @@ class Migration(migrations.Migration): unique_together={('year', 'name')}, ), migrations.RunPython(create_default), + migrations.AddField( + model_name='historicaltreatmentfile', + name='exhibition_location', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='archaeological_warehouse.Warehouse', verbose_name='Exhibition location'), + ), + migrations.AddField( + model_name='treatmentfile', + name='exhibition_location', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='archaeological_warehouse.Warehouse', verbose_name='Exhibition location'), + ), + migrations.AddField( + model_name='historicaltreatmentfile', + name='insurance_provider', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='ishtar_common.Organization', verbose_name='Insurance provider'), + ), + migrations.AddField( + model_name='treatmentfile', + name='insurance_provider', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='insurance_provider_of', to='ishtar_common.Organization', verbose_name='Insurance provider'), + ), ] diff --git a/archaeological_finds/models_treatments.py b/archaeological_finds/models_treatments.py index 933df0fa8..60a4440fd 100644 --- a/archaeological_finds/models_treatments.py +++ b/archaeological_finds/models_treatments.py @@ -1304,6 +1304,21 @@ class TreatmentFile( exhibition_end_date = models.DateField( _("Exhibition end date"), blank=True, null=True ) + exhibition_location = models.ForeignKey( + Warehouse, + verbose_name=_("Exhibition location"), + blank=True, + null=True, + on_delete=models.SET_NULL, + ) + insurance_provider = models.ForeignKey( + Organization, + related_name="insurance_provider_of", + verbose_name=_("Insurance provider"), + on_delete=models.SET_NULL, + blank=True, + null=True, + ) comment = models.TextField(_("Comment"), blank=True, default="") documents = models.ManyToManyField( @@ -1457,6 +1472,12 @@ post_save.connect(cached_label_changed, sender=TreatmentFile) class ExhibitionType(GeneralType): + treatment_file_type = models.ForeignKey( + TreatmentFileType, + verbose_name=_("Treatment request type"), + on_delete=models.PROTECT, + ) + class Meta: verbose_name = _("Exhibition type") verbose_name_plural = _("Exhibition types") @@ -1474,11 +1495,11 @@ class Exhibition( AssociatedFindBasket, ): SLUG = "exhibition" - APP = "archaeological_finds" + APP = "archaeological-finds" MODEL = SLUG SHOW_URL = "show-exhibition" - DELETE_URL = "delete-exhibition" - TABLE_COLS = ["year", "reference", "name"] + # DELETE_URL = "delete-exhibition" + TABLE_COLS = ["year", "name", "reference"] BASE_SEARCH_VECTORS = [ SearchVectorConfig("exhibition_type__label"), SearchVectorConfig("reference"), @@ -1552,3 +1573,24 @@ class Exhibition( ] ADMIN_SECTION = _("Treatments") + def get_extra_actions(self, request): + """ + For sheet template: + """ + actions = super().get_extra_actions(request) + can_add_tf = self.can_do( + request, "archaeological_finds.add_treatmentfile" + ) + if can_add_tf: + + actions += [ + ( + reverse("exhibition-qa-add-loan", args=[self.pk]), + _("Add exhibition loan"), + "fa fa-plus", + _("exhibition loan"), + "", + True, + ), + ] + return actions diff --git a/archaeological_finds/templates/ishtar/sheet_exhibition.html b/archaeological_finds/templates/ishtar/sheet_exhibition.html new file mode 100644 index 000000000..cd37bac4b --- /dev/null +++ b/archaeological_finds/templates/ishtar/sheet_exhibition.html @@ -0,0 +1,133 @@ +{% extends "ishtar/sheet.html" %} +{% load i18n l10n ishtar_helpers window_field from_dict link_to_window window_tables window_ope_tables window_header humanize %} + +{% block head_title %}<strong><i class="fa fa-users" aria-hidden="true"></i> {% trans "Exhibition" %}</strong> - {{ item.name|default:"" }} [{{item.year|unlocalize}}]{% endblock %} + +{% block toolbar %} +{% window_nav item window_id 'show-exhibition' 'exhibition-modify' 'exhibition-show-historized' 'exhibition-revert' previous next 1 %} +{% endblock %} + +{% block content %} +{% with perm_documents=permission_view_own_document|or_:permission_view_document %} +{% with perm_change_basket=permission_view_own_find|or_:permission_view_find %} +{% with has_documents=item|safe_or:"documents.count|documents_list" %} +{% with display_documents=perm_documents|and_:has_documents %} + +{% if output != "ODT" and output != "PDF"%} +<ul class="nav nav-tabs" id="{{window_id}}-tabs" role="tablist"> + <li class="nav-item"> + <a class="nav-link active" id="{{window_id}}-exhibition-tab" + data-toggle="tab" href="#{{window_id}}-exhibition" role="tab" + aria-controls="{{window_id}}-exhibition" aria-selected="true"> + {% trans "Exhibition" %} + </a> + </li> + {% for loan in item.treatment_files.all %} + <li class="nav-item"> + <a class="nav-link" id="{{window_id}}-loan{{forloop.counter}}-tab" + data-toggle="tab" href="#{{window_id}}-loan{{forloop.counter}}" role="tab" + aria-controls="{{window_id}}-loan{{forloop.counter}}" aria-selected="true"> + {{loan.name}} + </a> + </li> + {% endfor %} +</ul> +{% endif %} + +<div class="tab-content" id="{{window_id}}-tab-content"> + <div class="tab-pane fade show active" id="{{window_id}}-exhibition" + role="tabpanel" aria-labelledby="{{window_id}}-exhibition-tab"> + + {% with has_image=item.images.count %} + {% if has_image %} + <div class="clearfix"> + <div class="card float-left col-12 col-md-6 col-lg-4"> + {% include "ishtar/blocks/window_image.html" %} + </div> + {% endif %} + <div class="row"> + {% field_flex _("Name") item.name %} + {% field_flex _("Type") item.exhibition_type %} + {% field_flex _("Year") item.year %} + {% field_flex_detail _("Responsible") item.in_charge %} + {% field_flex_full "Comment" item.comment "<pre>" "</pre>" %} + {% include "ishtar/blocks/sheet_json.html" %} + </div> + {% if has_image %} + </div> + {% endif %} + {% endwith %} + + {% if item.associated_basket %} + <h3>{% trans "Associated basket" %}</h3> + <div class="row"> + {% field_flex_detail _("Associated basket") item.associated_basket %} + + {% if perm_change_basket %} + <p class="col-12 col-md-6 col-lg-3 flex-wrap"> + <a class="wait-button btn btn-success" href="/find_basket_modification_add/{{item.associated_basket.pk}}/?back_url={% url 'display-exhibition' item.pk %}" title="{% trans 'Manage basket' %}"> + <i class="fa fa-shopping-basket"></i> {% trans "manage items of this basket" %} + </a> + </p> + {% endif %} + + </div> + {% dynamic_table_document finds 'finds' 'basket_id' item.associated_basket.pk 'TABLE_COLS' output %} + {% endif %} + + + {% if display_documents %} + {% trans "Associated documents" as associated_docs %} + {% dynamic_table_document associated_docs 'documents' 'treatment_files' item.pk '' output %} + {% endif %} + + {% if item.administrative_act.count %} + {% trans "Administrative acts" as admact_lbl %} + {% table_administrativact admact_lbl item.administrative_act.all %} + {% endif %} + + {% if not is_external %} + {% if item.history_creator or item.last_edition_date or item.created %} + <h3>{% trans "Sheet"%}</h3> + <div class="row"> + {% include "ishtar/blocks/sheet_creation_section.html" %} + </div> + {% endif %} + {% endif %} + </div> + {% for loan in item.treatment_files.all %} + <div class="tab-pane fade" id="{{window_id}}-loan{{forloop.counter}}" + role="tabpanel" aria-labelledby="{{window_id}}-loan{{forloop.counter}}-tab"> + <div class="row"> + {% with loan.exhibition_start_date|date:"SHORT_DATE_FORMAT" as exhibition_start_date %} + <dl class="col-12 col-md-6 col-lg-3 flex-wrap"> + <dt>{% trans "Dates" %}</dt> + <dd>{{loan.exhibition_start_date|date:"SHORT_DATE_FORMAT"}} / {{loan.exhibition_end_date|date:"SHORT_DATE_FORMAT"}}</dd> + </dl> + {% endwith %} + {% field_flex_detail _("Beneficiary of the loan") loan.applicant_organisation %} + {% field_flex_detail _("Scientific manager of the exhibition") loan.in_charge %} + {% field_flex_detail _("Exhibition location") loan.exhibition_location %} + {% field_flex_detail _("Insurance provider") loan.insurance_provider %} + </div> + <h3>{% trans "Associated basket" %}</h3> + <div class="row"> + {% field_flex_detail _("Associated basket") loan.associated_basket %} + + {% if perm_change_basket %} + <p class="col-12 col-md-6 col-lg-3 flex-wrap"> + <a class="wait-button btn btn-success" href="/find_basket_modification_add/{{loan.associated_basket.pk}}/?back_url={% url 'display-exhibition' loan.pk %}" title="{% trans 'Manage basket' %}"> + <i class="fa fa-shopping-basket"></i> {% trans "manage items of this basket" %} + </a> + </p> + {% endif %} + + </div> + {% dynamic_table_document finds 'finds' 'basket_id' loan.associated_basket.pk 'TABLE_COLS' output %} + + </div> + {% endfor %} +</div> + +{% endwith %}{% endwith %}{% endwith %}{% endwith %} +{% endblock %} diff --git a/archaeological_finds/templates/ishtar/sheet_exhibition_pdf.html b/archaeological_finds/templates/ishtar/sheet_exhibition_pdf.html new file mode 100644 index 000000000..6540e1d58 --- /dev/null +++ b/archaeological_finds/templates/ishtar/sheet_exhibition_pdf.html @@ -0,0 +1,14 @@ +{% extends "ishtar/sheet_exhibition.html" %} +{% block header %} +{% endblock %} +{% block main_head %} +{{ block.super }} +<div id="pdfheader"> + Ishtar – {{APP_NAME}} – {{item}} +</div> +{% endblock %} +{%block head_sheet%}{%endblock%} +{%block main_foot%} +</body> +</html> +{%endblock%} diff --git a/archaeological_finds/templates/ishtar/sheet_exhibition_window.html b/archaeological_finds/templates/ishtar/sheet_exhibition_window.html new file mode 100644 index 000000000..e6a8dcc5b --- /dev/null +++ b/archaeological_finds/templates/ishtar/sheet_exhibition_window.html @@ -0,0 +1,3 @@ +{% extends "ishtar/sheet_exhibition.html" %} +{% block main_head %}{%endblock%} +{% block main_foot %}{%endblock%} diff --git a/archaeological_finds/templates/ishtar/wizard/exhibition.html b/archaeological_finds/templates/ishtar/wizard/exhibition.html new file mode 100644 index 000000000..e60126632 --- /dev/null +++ b/archaeological_finds/templates/ishtar/wizard/exhibition.html @@ -0,0 +1,13 @@ +{% extends "ishtar/wizard/search.html" %} +{% load i18n %} +{% block wizard_top_button %} +{% if permission_add_exhibition %} +<div class="row mb-3"> + <div class="col"> + <a href="{% url "exhibition-create" %}" class="btn btn-success"> + <i class="fa fa-plus" aria-hidden="true"></i> {% trans "new exhibition" %} + </a> + </div> +</div> +{% endif %} +{% endblock %} diff --git a/archaeological_finds/templates/ishtar/wizard/wizard_find_creation.html b/archaeological_finds/templates/ishtar/wizard/wizard_find_creation.html new file mode 100644 index 000000000..65806e303 --- /dev/null +++ b/archaeological_finds/templates/ishtar/wizard/wizard_find_creation.html @@ -0,0 +1,13 @@ +{% extends "ishtar/wizard/default_wizard.html" %} +{% load i18n l10n %} +{% block wizard_top_button %} +{% if no_context_cr %} +<div class="row mb-3"> + <div class="col"> + <a href="/find_create/{{no_context_cr|unlocalize}}/" class="btn btn-success"> + <i class="fa fa-plus" aria-hidden="true"></i> {% trans "find without context" %} + </a> + </div> +</div> +{% endif %} +{% endblock %} diff --git a/archaeological_finds/urls.py b/archaeological_finds/urls.py index 467df9770..6e6f6ff0a 100644 --- a/archaeological_finds/urls.py +++ b/archaeological_finds/urls.py @@ -18,16 +18,19 @@ # See the file COPYING for details. from django.conf.urls import url -from django.urls import path +from django.urls import path, register_converter from ishtar_common.utils import check_permissions, get_urls_for_model +from ishtar_common import urls_converters from ishtar_common.views import QALinkView from archaeological_finds import views from archaeological_finds import views_api from archaeological_operations.views import administrativeactfile_document from archaeological_finds import models +register_converter(urls_converters.DateTimeConverter, "datetime") + # be careful: each check_permissions must be relevant with ishtar_menu # forms @@ -455,6 +458,36 @@ urlpatterns = [ name="exhibition-create", ), path( + "exhibition/qa/add-loan/<int:pks>/", + check_permissions( + ["archaeological_finds.add_treatmentfile"] + )(views.QAExhibitionLoanFormView.as_view()), + name="exhibition-qa-add-loan", + ), + path( + "exhibition/modify/<int:pk>/", + check_permissions( + ["archaeological_finds.change_exhibition", + "archaeological_finds.change_own_exhibition"] + )(views.ExhibitionEditView.as_view()), + name="exhibition-modify", + ), + path( + "exhibition/show/<int:pk>/", + views.show_exhibition, + name="exhibition-show-historized", + ), + path( + "exhibition/show/<int:pk>/<datetime:date>/", + views.show_exhibition, + name="exhibition-show-historized", + ), + path( + "exhibition/revert/<int:pk>/<datetime:date>/", + views.revert_exhibition, + name="exhibition-revert" + ), + path( "exhibition/<step>/", check_permissions( ["archaeological_finds.view_exhibition", @@ -462,6 +495,12 @@ urlpatterns = [ )(views.exhibition_wizard), name="exhibition-search", ), + path("show-exhibition/<int:pk>/<slug:type>", + views.show_exhibition, name="show-exhibition"), + path("show-exhibition/<int:pk>/", + views.show_exhibition, name="show-exhibition"), + path("display-exhibition/<int:pk>/", + views.display_exhibition, name="display-exhibition"), url( r"^treatmentfle_search/(?P<step>.+)?$", check_permissions( diff --git a/archaeological_finds/views.py b/archaeological_finds/views.py index 777b064c5..52085f2ef 100644 --- a/archaeological_finds/views.py +++ b/archaeological_finds/views.py @@ -27,11 +27,11 @@ from django.http import HttpResponseRedirect, HttpResponse, Http404 from django.shortcuts import redirect from django.urls import reverse -from ishtar_common.utils import ugettext_lazy as _ +from ishtar_common.utils import ugettext_lazy as _, BSMessage from django.views.generic import TemplateView -from django.views.generic.edit import CreateView, FormView +from django.views.generic.edit import CreateView, FormView, UpdateView -from ishtar_common.models import IshtarUser, get_current_profile +from ishtar_common.models import get_current_profile, IshtarUser, QuickAction from archaeological_operations.models import AdministrativeAct, Operation from archaeological_context_records.models import ContextRecord from archaeological_finds import models @@ -142,12 +142,21 @@ get_treatmentfile = get_item( search_form=forms.TreatmentFileSelect, ) +show_exhibition = show_item(models.Exhibition, "exhibition") +revert_exhibition = revert_item(models.Exhibition) get_exhibition = get_item( - models.TreatmentFile, - "get_treatmentfile", - "treatmentfile", - search_form=forms.TreatmentFileSelect, - base_request={"type__is_exhibition": True}, + models.Exhibition, "get_exhibition", "exhibition", + search_form=forms_treatments.ExhibitionSelect +) +display_exhibition = display_item(models.Exhibition) + +autocomplete_exhibition = get_autocomplete_item(model=models.Exhibition) + +get_exhibition = get_item( + models.Exhibition, + "get_exhibition", + "exhibition", + search_form=forms_treatments.ExhibitionSelect, ) get_administrativeacttreatmentfile = get_item( @@ -553,6 +562,7 @@ class SelectItemsInBasket(OwnBasket, IshtarMixin, LoginRequiredMixin, TemplateVi context["form"] = forms.MultipleFindFormSelectionWarehouseModule() else: context["form"] = forms.MultipleFindFormSelection() + context["back_url"] = self.request.GET.get("back_url", None) context["add_url"] = reverse("add_iteminbasket") context["list_url"] = reverse( "list_iteminbasket", kwargs={"pk": self.basket.pk} @@ -630,12 +640,40 @@ get_downstreamtreatment = get_item( ) -exhibition_wizard = wizards.TreatmentFileSearch.as_view( +exhibition_wizard = wizards.ExhibitionSearch.as_view( [("search", forms_treatments.ExhibitionFormSelection)], - label=_("Exhibition: search"), + label=_("Exhibition"), url_name="exhibition-search", ) + +class ExhibitionFormMixin(IshtarMixin, LoginRequiredMixin): + form_class = forms_treatments.ExhibitionForm + template_name = "ishtar/forms/base_form.html" + model = models.Exhibition + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["user"] = self.request.user + return kwargs + + def get_success_url(self): + return f"{reverse('exhibition-search')}?open_item={self.object.pk}" + + +class ExhibitionCreateView(ExhibitionFormMixin, CreateView): + page_name = _("Exhibition creation") + + def get_context_data(self, **kwargs): + data = super().get_context_data(**kwargs) + # data["extra_form_modals"] = self.form_class.extra_form_modals + return data + + +class ExhibitionEditView(ExhibitionFormMixin, UpdateView): + page_name = _("Exhibition modification") + + treatment_wizard_steps = [ ("selecfind-treatment_creation", forms.UpstreamFindFormSelection), ("file-treatment_creation", forms.TreatmentFormFileChoice), @@ -1368,3 +1406,41 @@ def get_geo_items(request, current_right=None): geo = item.get_geo_items() return HttpResponse(json.dumps(geo).encode("utf-8")) + +class QAExhibitionLoanFormView(QAItemForm): + model = models.Exhibition + form_class = forms_treatments.QANewExhibitionLoanForm + page_name = _("Add loan for exhibition") + modal_size = "large" + base_url = "exhibition-qa-add-loan" + icon = "fa fa-plus" + action_name = _("Create") + + def get_quick_action(self): + return QuickAction( + url=self.base_url, + icon_class=self.icon, + text=self.page_name, + rights=[ + "archaeological_finds.add_treatmentfile", + ], + ) + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["user"] = self.request.user + return kwargs + + def get_context_data(self, **kwargs): + data = super().get_context_data(**kwargs) + if not self.items[0].associated_basket_id: + data["messages"] = [ + BSMessage( + _("No basket associted to the exhibition."), + "danger", "fa fa-exclamation-triangle") + ] + return data + + def form_valid(self, form): + form.save() + return HttpResponseRedirect(reverse("success")) diff --git a/archaeological_finds/wizards.py b/archaeological_finds/wizards.py index c43f65356..dd2e65ff8 100644 --- a/archaeological_finds/wizards.py +++ b/archaeological_finds/wizards.py @@ -43,6 +43,9 @@ class FindWizard(Wizard): model = models.Find wizard_done_window = reverse_lazy("show-find") redirect_url = "find_modification" + wizard_templates = { + "selecrecord-find_creation": "ishtar/wizard/wizard_find_creation.html" + } def get_current_contextrecord(self): step = self.steps.current @@ -626,3 +629,13 @@ class FindBasketDeletionWizard(DeletionWizard): model = models.FindBasket redirect_url = "find_basket_deletion" wizard_confirm = "ishtar/wizard/wizard_findbasket_deletion.html" + + +class ExhibitionSearch(SearchWizard): + model = models.Exhibition + template_name = "ishtar/wizard/exhibition.html" + + def get_context_data(self, form, **kwargs): + data = super().get_context_data(form, **kwargs) + data["permission_add_exhibition"] = self.request.user.has_perm("ishtar_common.add_exhibition") + return data diff --git a/ishtar_common/forms.py b/ishtar_common/forms.py index ca9f37623..6531ad6bb 100644 --- a/ishtar_common/forms.py +++ b/ishtar_common/forms.py @@ -627,15 +627,28 @@ class FormSetWithDeleteSwitches(FormSet): delete_widget = widgets.DeleteSwitchWidget -class FieldType(object): - def __init__(self, key, model, is_multiple=False, extra_args=None): +class FieldType: + """ + Define field choices, help for SELECT field from a model. + :key: fields key for the form + :model: associated model + :is_multiple: True if multi select + :extra_args: extra args for 'get_types' call + :empty_first: first entry is empty. True by default. Always False when multiple + """ + + def __init__(self, key, model, is_multiple=False, extra_args=None, + empty_first=True): self.key = key self.model = model self.is_multiple = is_multiple self.extra_args = extra_args + if self.is_multiple: + empty_first = False + self.empty_first = empty_first def get_choices(self, initial=None): - args = {"empty_first": not self.is_multiple, "initial": initial} + args = {"empty_first": self.empty_first, "initial": initial} if self.extra_args: args.update(self.extra_args) return self.model.get_types(**args) @@ -779,6 +792,18 @@ class IshtarForm(BSForm, forms.Form): self.fields[field.key].choices = field.get_choices() self.fields[field.key].help_text = field.get_help() + def _clean_model_field(self, key, model): + """ + Clean autocomplete field returning integer associated to a model. + """ + value = self.cleaned_data.get(key, None) + if not value: + return + try: + return model.objects.get(pk=int(value)) + except (model.DoesNotExist, ValueError): + return + def get_headers(self): if self._headers: return self._headers diff --git a/ishtar_common/templates/ishtar/forms/qa_form.html b/ishtar_common/templates/ishtar/forms/qa_form.html index c843dbd2d..178910215 100644 --- a/ishtar_common/templates/ishtar/forms/qa_form.html +++ b/ishtar_common/templates/ishtar/forms/qa_form.html @@ -2,6 +2,15 @@ {% load i18n inline_formset table_form %} {% block main_form %} + {% for message in messages %} + <div class="alert alert-{{message.type}} alert-dismissible fade show" role="alert"> + {% if message.icon %}<i class="{{message.icon}}" aria-hidden="true"></i> {% endif %} + {{message.message}} + <button type="button" class="close" data-dismiss="alert" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + {% endfor %} {% for error in form.non_field_errors %} <p>{{ error }}</p> {% endfor %} diff --git a/ishtar_common/templates/ishtar/manage_basket.html b/ishtar_common/templates/ishtar/manage_basket.html index 5f3bf2c8f..61def50f6 100644 --- a/ishtar_common/templates/ishtar/manage_basket.html +++ b/ishtar_common/templates/ishtar/manage_basket.html @@ -69,9 +69,9 @@ jQuery(document).ready(function(){ <div id='validation-bar' class="row text-center"> <div class="col-sm"> <a class="btn btn-success" - href="{% url 'display-findbasket' basket.id %}" + href="{% if back_url %}{{back_url}}{% else %}{% url 'display-findbasket' basket.id %}{% endif %}" id="validate-button" - class='button'>{% trans "Close" %}</a> + class='button'>{% trans "Back" %}</a> </div> </div> {% include 'ishtar/blocks/footer.html' %} diff --git a/ishtar_common/templates/ishtar/wizard/default_wizard.html b/ishtar_common/templates/ishtar/wizard/default_wizard.html index 0d3863af9..b9364d012 100644 --- a/ishtar_common/templates/ishtar/wizard/default_wizard.html +++ b/ishtar_common/templates/ishtar/wizard/default_wizard.html @@ -6,16 +6,8 @@ {% block content %} {% block wizard_head %} <h3>{{wizard_label}}</h3> -{% if no_context_cr %} -<div class="row mb-3"> - <div class="col"> - <a href="/find_create/{{no_context_cr|unlocalize}}/" class="btn btn-success"> - <i class="fa fa-plus" aria-hidden="true"></i> {% trans "find without context" %} - </a> - </div> -</div> -{% endif %} - +{% block wizard_top_button %} +{% endblock %} {% include "ishtar/blocks/wizard_breadcrumb.html" %} {% endblock %} diff --git a/ishtar_common/templates/ishtar/wizard/search.html b/ishtar_common/templates/ishtar/wizard/search.html index 9047c47fc..19a6540dd 100644 --- a/ishtar_common/templates/ishtar/wizard/search.html +++ b/ishtar_common/templates/ishtar/wizard/search.html @@ -5,6 +5,8 @@ {% endblock %} {% block content %} <h3>{{wizard_label}}</h3> +{% block wizard_top_button %} +{% endblock %} {% if default_search_vector or open_url %} <script type="text/javascript">{% localize off %} {% if default_search_vector %} diff --git a/ishtar_common/urls_converters.py b/ishtar_common/urls_converters.py index 9395af648..b5185fd1f 100644 --- a/ishtar_common/urls_converters.py +++ b/ishtar_common/urls_converters.py @@ -17,6 +17,8 @@ # See the file COPYING for details. +from datetime import datetime + class UnderscoreSlug: regex = '[_0-9a-z]+' @@ -27,3 +29,15 @@ class UnderscoreSlug: def to_url(self, value): return str(value) + +class DateTimeConverter: + regex = r"\d{4}-\d{1,2}-\d{1,2}T\d{1,2}\:\d{1,2}\:\d{1,2}\.\d{1,6}" + date_format = '%Y-%m-%dT%H:%M:%S.%f' + + def to_python(self, value): + return datetime.strptime(value, self.date_format) + + def to_url(self, value): + if isinstance(value, datetime): + return value.strftime(self.date_format) + return value diff --git a/ishtar_common/utils.py b/ishtar_common/utils.py index 152b78c9c..eb51b6ef4 100644 --- a/ishtar_common/utils.py +++ b/ishtar_common/utils.py @@ -1386,6 +1386,13 @@ def get_random_item_image_link(request): return _get_image_link(q.all()[image_nb]) +class BSMessage: + def __init__(self, message, message_type="info", icon=None): + self.message = message + self.type = message_type + self.icon = icon + + def convert_coordinates_to_point(x, y, z=None, srid=4326): if z: geom = GEOSGeometry("POINT({} {} {})".format(x, y, z), srid=srid) diff --git a/ishtar_common/views.py b/ishtar_common/views.py index 21b563bfc..6411441f1 100644 --- a/ishtar_common/views.py +++ b/ishtar_common/views.py @@ -3128,6 +3128,7 @@ class QAItemForm(IshtarMixin, LoginRequiredMixin, FormView): success_url = "/success/" modal_size = None # large, small or None (medium) icon = "fa fa-pencil" + action_name = None def get_quick_action(self): # if not listed in QUICK_ACTIONS overload this method @@ -3139,7 +3140,11 @@ class QAItemForm(IshtarMixin, LoginRequiredMixin, FormView): self.model = kwargs["model"] else: raise NotImplementedError("No attribute model defined.") - pks = [int(pk) for pk in kwargs.get("pks").split("-")] + pks = kwargs.get("pks") + if isinstance(pks, int): + pks = [pks] + else: + pks = [int(pk) for pk in kwargs.get("pks").split("-")] self.items = list(self.model.objects.filter(pk__in=pks)) if not self.items: raise Http404() @@ -3182,6 +3187,8 @@ class QAItemForm(IshtarMixin, LoginRequiredMixin, FormView): data["items"] = self.items data["modal_size"] = self.modal_size data["page_name"] = self.get_page_name() + if self.action_name: + data["action_name"] = self.action_name return data diff --git a/ishtar_common/views_item.py b/ishtar_common/views_item.py index 8d41d1cc6..345bd0025 100644 --- a/ishtar_common/views_item.py +++ b/ishtar_common/views_item.py @@ -55,6 +55,7 @@ from weasyprint.fonts import FontConfiguration from bootstrap_datepicker.widgets import DateField +from ishtar_common.urls_converters import DateTimeConverter from ishtar_common.utils import ( API_MAIN_MODELS, check_model_access_control, @@ -368,9 +369,9 @@ def show_source_item(request, source_id, model, name, base_dct, extra_dct): permissions = ["permission_view_document"] for p in permissions: dct[p] = True - dct["permission_change_own_document"] = False - dct["permission_change_document"] = False - + for perm in ["document", "findbasket"]: + dct[f"permission_change_own_{perm}"] = False + dct[f"permission_change_{perm}"] = False tpl = loader.get_template(f"ishtar/sheet_{name}_window.html") content = tpl.render(dct, request) return HttpResponse(content, content_type="application/xhtml") @@ -477,8 +478,9 @@ def show_item(model, name, extra_dct=None, model_for_perms=None, callback=None): for perm in Permission.objects.filter( codename__startswith='view_').values_list("codename", flat=True).all(): dct["permission_" + perm] = False - dct["permission_change_own_document"] = False - dct["permission_change_document"] = False + for perm in ["document", "findbasket"]: + dct[f"permission_change_own_{perm}"] = False + dct[f"permission_change_{perm}"] = False if hasattr(request.user, "ishtaruser") and request.user.ishtaruser: cache_key = "{}-{}-{}".format( settings.PROJECT_SLUG, @@ -503,9 +505,12 @@ def show_item(model, name, extra_dct=None, model_for_perms=None, callback=None): dct["get_import_updated"] = item.get_imports_updated(request.user, limit=5) if hasattr(item, "history") and request.user.is_superuser: + if date: try: - date = datetime.datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%f") + if not isinstance(date, datetime.datetime): + date = datetime.datetime.strptime( + date, DateTimeConverter.date_format) dct["IS_HISTORY"] = True if item.get_last_history_date() != date: item = item.get_previous(date=date) @@ -630,7 +635,8 @@ def revert_item(model): def func(request, pk, date, **dct): try: item = model.objects.get(pk=pk) - date = datetime.datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%f") + if not isinstance(date, datetime.datetime): + date = datetime.datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%f") item.rollback(date) except (ObjectDoesNotExist, ValueError, HistoryError): return HttpResponse(None, content_type="text/plain") @@ -2897,6 +2903,19 @@ def get_item( if not no_link: try: curl = reverse("show-" + default_name, args=[data[0], ""]) + except NoReverseMatch: + try: + curl = reverse("show-" + default_name, args=[data[0]]) + except NoReverseMatch: + logger.warning( + '**WARN "show-' + + default_name + + '" args (' + + str(data[0]) + + ") url not available" + ) + curl, lnk = "", "" + if curl: if not curl.endswith("/"): curl += "/" lnk_template = link_template @@ -2908,15 +2927,6 @@ def get_item( lnk = lnk.replace("<lock>", lock) else: lnk = lnk.replace("<lock>", "") - except NoReverseMatch: - logger.warning( - '**WARN "show-' - + default_name - + '" args (' - + str(data[0]) - + ") url not available" - ) - lnk = "" res["link"] = lnk for idx, value in enumerate(data[1:]): if not value or idx >= len(table_cols): diff --git a/ishtar_common/widgets.py b/ishtar_common/widgets.py index e131521e0..b9166f361 100644 --- a/ishtar_common/widgets.py +++ b/ishtar_common/widgets.py @@ -621,7 +621,7 @@ class SearchWidget(forms.TextInput): self.app_name = app_name self.model = model if not pin_model: - pin_model = self.model + pin_model = self.model.lower() self.pin_model = pin_model def get_context(self, name, value, attrs): diff --git a/ishtar_common/wizards.py b/ishtar_common/wizards.py index 740a1c18d..0d23bdcb4 100644 --- a/ishtar_common/wizards.py +++ b/ishtar_common/wizards.py @@ -1678,6 +1678,7 @@ class SearchWizard(IshtarWizard): label = "" modification = None # True when the wizard modify an item storage_name = "formtools.wizard.storage.session.SessionStorage" + template_name = "ishtar/wizard/search.html" def get_wizard_name(self): """ @@ -1692,8 +1693,7 @@ class SearchWizard(IshtarWizard): ) def get_template_names(self): - templates = ["ishtar/wizard/search.html"] - return templates + return [self.template_name] def get_label(self): return self.label |