summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--archaeological_finds/fixtures/initial_data-fr.json1
-rw-r--r--archaeological_finds/forms_treatments.py232
-rw-r--r--archaeological_finds/migrations/0133_exhibition.py32
-rw-r--r--archaeological_finds/models_treatments.py48
-rw-r--r--archaeological_finds/templates/ishtar/sheet_exhibition.html133
-rw-r--r--archaeological_finds/templates/ishtar/sheet_exhibition_pdf.html14
-rw-r--r--archaeological_finds/templates/ishtar/sheet_exhibition_window.html3
-rw-r--r--archaeological_finds/templates/ishtar/wizard/exhibition.html13
-rw-r--r--archaeological_finds/templates/ishtar/wizard/wizard_find_creation.html13
-rw-r--r--archaeological_finds/urls.py41
-rw-r--r--archaeological_finds/views.py96
-rw-r--r--archaeological_finds/wizards.py13
-rw-r--r--ishtar_common/forms.py31
-rw-r--r--ishtar_common/templates/ishtar/forms/qa_form.html9
-rw-r--r--ishtar_common/templates/ishtar/manage_basket.html4
-rw-r--r--ishtar_common/templates/ishtar/wizard/default_wizard.html12
-rw-r--r--ishtar_common/templates/ishtar/wizard/search.html2
-rw-r--r--ishtar_common/urls_converters.py14
-rw-r--r--ishtar_common/utils.py7
-rw-r--r--ishtar_common/views.py9
-rw-r--r--ishtar_common/views_item.py42
-rw-r--r--ishtar_common/widgets.py2
-rw-r--r--ishtar_common/wizards.py4
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>&nbsp; {% 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>&nbsp; {% 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>&nbsp; {% 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 &ndash; {{APP_NAME}} &ndash; {{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>&nbsp; {% 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>&nbsp;&nbsp;{% 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