diff options
author | Étienne Loks <etienne.loks@iggdrasil.net> | 2022-05-22 20:31:19 +0200 |
---|---|---|
committer | Étienne Loks <etienne.loks@iggdrasil.net> | 2022-12-12 12:21:00 +0100 |
commit | 514be394a72d3a08a1a7cb0d588ba4c3b51cb395 (patch) | |
tree | b7bece5905a4a433cbde9132bec669669f1c8ea0 | |
parent | 5a16bff4556fc70b8feb5e39bad362c7f0a23d41 (diff) | |
download | Ishtar-514be394a72d3a08a1a7cb0d588ba4c3b51cb395.tar.bz2 Ishtar-514be394a72d3a08a1a7cb0d588ba4c3b51cb395.zip |
Geodata - geo forms: new forms - many adaptations
-rw-r--r-- | UPGRADE_V4.md | 14 | ||||
-rw-r--r-- | archaeological_finds/urls.py | 7 | ||||
-rw-r--r-- | archaeological_finds/views.py | 1 | ||||
-rw-r--r-- | ishtar_common/forms_common.py | 204 | ||||
-rw-r--r-- | ishtar_common/models.py | 5 | ||||
-rw-r--r-- | ishtar_common/models_common.py | 12 | ||||
-rw-r--r-- | ishtar_common/models_imports.py | 11 | ||||
-rw-r--r-- | ishtar_common/templates/blocks/bs_form_snippet.html | 2 | ||||
-rw-r--r-- | ishtar_common/templates/ishtar/blocks/sheet_geographic.html | 2 | ||||
-rw-r--r-- | ishtar_common/templates/ishtar/forms/base_related_items.html (renamed from ishtar_common/templates/ishtar/forms/document.html) | 0 | ||||
-rw-r--r-- | ishtar_common/urls.py | 14 | ||||
-rw-r--r-- | ishtar_common/utils.py | 8 | ||||
-rw-r--r-- | ishtar_common/views.py | 99 | ||||
-rw-r--r-- | ishtar_common/widgets.py | 11 |
14 files changed, 376 insertions, 14 deletions
diff --git a/UPGRADE_V4.md b/UPGRADE_V4.md new file mode 100644 index 000000000..33a2448db --- /dev/null +++ b/UPGRADE_V4.md @@ -0,0 +1,14 @@ +# backup database and media +sudo vim /etc/apt/sources.list.d/iggdrasil.list +# buster -> bullseye +sudo apt update +sudo apt upgrade +screen # data migration is long... +# for each instance in /etc/ishtar/instances +cd /srv/ishtar/{instance_name} +# update fixtures +./manage.py loaddata /usr/share/python3-django-ishtar/fixtures/initial_data-auth-fr.json +# migrate to new geo management +./manage.py migrate_to_geo_v4 +# edit profile type permissions for geovectordata +-> http(s)://{my-ihstar}/admin/ishtar_common/profiletypesummary/
\ No newline at end of file diff --git a/archaeological_finds/urls.py b/archaeological_finds/urls.py index ae12c2cbd..4bb771359 100644 --- a/archaeological_finds/urls.py +++ b/archaeological_finds/urls.py @@ -631,6 +631,13 @@ urlpatterns = [ "api/get/find/<int:pk>/", views_api.GetFindAPI.as_view(), name="api-get-find" ), + url( + r"autocomplete-basefind/$", + check_rights(["view_basefind", "view_own_basefind"])( + views.autocomplete_basefind + ), + name="autocomplete-basefind", + ), ] urlpatterns += get_urls_for_model(models.Find, views, own=True, autocomplete=True) diff --git a/archaeological_finds/views.py b/archaeological_finds/views.py index 4def2e14b..d643c81b7 100644 --- a/archaeological_finds/views.py +++ b/archaeological_finds/views.py @@ -115,6 +115,7 @@ get_find_inside_container = get_item( ) autocomplete_find = get_autocomplete_item(model=models.Find) +autocomplete_basefind = get_autocomplete_item(model=models.BaseFind) show_treatment = show_item(models.Treatment, "treatment") revert_treatment = revert_item(models.Treatment) diff --git a/ishtar_common/forms_common.py b/ishtar_common/forms_common.py index 6e9e11006..695638120 100644 --- a/ishtar_common/forms_common.py +++ b/ishtar_common/forms_common.py @@ -30,6 +30,7 @@ from urllib.parse import urlparse, quote import zipfile from django import forms +from django.contrib.gis import forms as gis_forms from django.conf import settings from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType @@ -71,7 +72,8 @@ from .forms import ( LockForm, ) from ishtar_common.data_importer import ImporterError -from ishtar_common.utils import is_downloadable, clean_session_cache, max_size_help +from ishtar_common.utils import is_downloadable, clean_session_cache, max_size_help, \ + reverse_coordinates from archaeological_operations.models import Operation, OperationType from archaeological_context_records.models import ContextRecord @@ -1972,7 +1974,7 @@ class DocumentForm(forms.ModelForm, CustomForm, ManageOldType): if cleaned_data.get(rel, None): return cleaned_data raise forms.ValidationError( - _("A document has to be attached at least " "to one item") + _("A document has to be attached at least to one item") ) def clean_publisher(self): @@ -2562,3 +2564,201 @@ class QRSearchForm(forms.Form): os.makedirs(dest_dir) shutil.move(filename, dest_dir) return os.path.join(settings.MEDIA_URL, "tmp", base_filename) + + +class GISForm(forms.ModelForm, CustomForm, ManageOldType): + form_label = _("Geo item") + form_admin_name = _("Geo item - General") + form_slug = "geoitem-general" + + extra_form_modals = [] + associated_models = { + "data_type": models.GeoDataType, + "origin": models.GeoOriginType, + "provider": models.GeoProviderType, + } + + pk = forms.IntegerField(label="", required=False, widget=forms.HiddenInput) + name = forms.CharField( + label=_("Name"), + required=False, + validators=[validators.MaxLengthValidator(500)], + ) + import_key = forms.CharField( + label=_("Import key"), + required=False, + disabled=True, + help_text=_("An update via import corresponding to the source element and " + "this key will overwrite the data."), + ) + source_content_type_id = forms.IntegerField( + label="", required=True, widget=forms.HiddenInput, disabled=True) + source_id = forms.IntegerField( + label="", required=True, widget=forms.HiddenInput, disabled=True) + data_type = widgets.ModelChoiceField( + model=models.GeoDataType, label=_("Data type"), choices=[], required=False + ) + origin = widgets.ModelChoiceField( + model=models.GeoOriginType, label=_("Origin"), choices=[], required=False + ) + provider = widgets.ModelChoiceField( + model=models.GeoProviderType, label=_("Provider"), choices=[], required=False + ) + comment = forms.CharField(label=_("Comment"), widget=forms.Textarea, required=False) + + TYPES = [ + FieldType("origin", models.GeoOriginType), + FieldType("data_type", models.GeoDataType), + FieldType("provider", models.GeoProviderType), + ] + + class Meta: + model = models.GeoVectorData + exclude = ["need_update", "imports", "cached_x", "cached_y", "cached_z", + "point_3d"] + + HEADERS = { + "related_items_ishtar_common_town": FormHeader( + _("Related items"), collapse=True), + "name": FormHeader(_("Meta-data")), + "geo_field": FormHeader(_("Geography")), + } + OPTIONS_PERMISSIONS = [ + # field name, permission, options + ("tags", ("ishtar_common.add_documenttag",), {"new": True}), + ] + + GEO_FIELDS = ( + ("point_2d",), + ("multi_points",), + ("multi_line",), + ("multi_polygon",), + ("x", "z") + ) + + def __init__(self, *args, **kwargs): + main_items_fields = {} + if "main_items_fields" in kwargs: + main_items_fields = kwargs.pop("main_items_fields") + self.user = None + if kwargs.get("user", None): + self.user = kwargs.pop("user") + instance = kwargs.get("instance", False) + self.is_instancied = bool(instance) + super(GISForm, self).__init__(*args, **kwargs) + if not self.fields["import_key"].initial: + self.fields.pop("import_key") + self.source_content_type = kwargs.pop("source_content_type", None) + self.source_id = kwargs.pop("source_id", None) + if not self.source_content_type: + self.fields.pop("source_content_type_id") + self.fields.pop("source_id") + else: + self.fields["source_content_type_id"].initial = self.source_content_type + self.fields["source_id"].initial = self.source_id + self.geo_keys = [] + if instance: + for keys in self.GEO_FIELDS: + if any(getattr(instance, key) for key in keys): + if keys[0] != "x": + map_srid = getattr(instance, keys[0]).srid or 4326 + widget = gis_forms.OSMWidget + if map_srid == 4326: + widget = widgets.ReversedOSMWidget + self.fields[keys[0]].widget = widget( + attrs={"map_srid": map_srid}) + self.fields.pop("spatial_reference_system") + self.geo_keys = keys[:] + else: + self.geo_keys = [ + "x", "estimated_error_x", + "y", "estimated_error_y", + "z", "estimated_error_z", + "spatial_reference_system", + ] + for geo_fields in self.GEO_FIELDS: + if geo_fields != keys: + for geo_field in geo_fields: + self.fields.pop(geo_field) + if geo_field == "x": + self.fields.pop("estimated_error_x") + self.fields.pop("y") + self.fields.pop("estimated_error_y") + if geo_field == "z": + self.fields.pop("estimated_error_z") + break + if not self.geo_keys: + # TODO.... + pass + + fields = OrderedDict() + for related_key in models.GeoVectorData.RELATED_MODELS: + model = models.GeoVectorData._meta.get_field(related_key).related_model + fields[related_key] = widgets.Select2MultipleField( + model=model, + remote=True, + label=model._meta.verbose_name_plural, + required=False, + style="width: 100%", + ) + if related_key in main_items_fields: + for field_key, label in main_items_fields[related_key]: + disabled = False + if kwargs.get("initial", None) and kwargs["initial"].get( + field_key, False + ): + disabled = True + fields[field_key] = forms.BooleanField( + label=label, required=False, disabled=disabled + ) + for k in self.geo_keys: + fields[k] = self.fields[k] + for k in self.fields: + if k not in self.geo_keys: + fields[k] = self.fields[k] + self.fields = fields + + def get_headers(self): + headers = self.HEADERS.copy() + if self.geo_keys: + headers[self.geo_keys[0]] = headers.pop("geo_field") + return headers + + def clean(self): + cleaned_data = self.cleaned_data + if "x" not in self.geo_keys: + # reverse... + geo_value = cleaned_data[self.geo_keys[0]] + if geo_value: + if not isinstance(geo_value, str): + geo_value = geo_value.ewkt + if geo_value.startswith("SRID=4326;"): + cleaned_data[self.geo_keys[0]] = reverse_coordinates(geo_value) + for rel in models.GeoVectorData.RELATED_MODELS: + if cleaned_data.get(rel, None): + return cleaned_data + raise forms.ValidationError( + _("A geo item has to be attached at least to one item") + ) + + def save(self, commit=True): + item = super().save(commit=commit) + for related_key in models.GeoVectorData.RELATED_MODELS: + related = getattr(item, related_key) + initial = dict([(rel.pk, rel) for rel in related.all()]) + new = [int(pk) for pk in sorted(self.cleaned_data.get(related_key, []))] + for pk, value in initial.items(): + if pk in new: + continue + related.remove(value) + for new_pk in new: + related_item = related.model.objects.get(pk=new_pk) + if new_pk not in initial.keys(): + related.add(related_item) + item = models.GeoVectorData.objects.get(pk=item.pk) + if self.user: + item.history_creator = self.user + item.history_modifier = self.user + item.skip_history_when_saving = True + item.save() # resave to regen the attached items + return item diff --git a/ishtar_common/models.py b/ishtar_common/models.py index 2bd983906..870cce90b 100644 --- a/ishtar_common/models.py +++ b/ishtar_common/models.py @@ -145,6 +145,8 @@ from ishtar_common.models_common import ( DynamicRequest, GeoItem, GeoDataType, + GeoOriginType, + GeoProviderType, GeoVectorData, CompleteIdentifierItem, SearchVectorConfig, @@ -204,6 +206,9 @@ __all__ = [ "State", "CompleteIdentifierItem", "GeoVectorData", + "GeoDataType", + "GeoOriginType", + "GeoProviderType", ] logger = logging.getLogger(__name__) diff --git a/ishtar_common/models_common.py b/ishtar_common/models_common.py index 35dd4aef1..0c2d67140 100644 --- a/ishtar_common/models_common.py +++ b/ishtar_common/models_common.py @@ -2100,6 +2100,15 @@ GEOMETRY_TYPE_LBL = { class GeoVectorData(Imported, OwnPerms): SLUG = "geovectordata" + RELATED_MODELS = [ + "related_items_ishtar_common_town", + "related_items_archaeological_operations_operation", + "related_items_archaeological_operations_archaeologicalsite", + "related_items_archaeological_context_records_contextrecord", + "related_items_archaeological_finds_basefind", + "related_items_archaeological_warehouse_warehouse", + "related_items_archaeological_warehouse_container", + ] name = models.TextField(_("Name"), default="-") source_content_type = models.ForeignKey( @@ -2115,6 +2124,7 @@ class GeoVectorData(Imported, OwnPerms): null=True, on_delete=models.PROTECT, verbose_name=_("Origin"), + help_text=_("For instance: topographical surveys, georeferencing, ..."), ) data_type = models.ForeignKey( GeoDataType, @@ -2122,6 +2132,7 @@ class GeoVectorData(Imported, OwnPerms): null=True, on_delete=models.PROTECT, verbose_name=_("Data type"), + help_text=_("For instance: outline, z-sup, ..."), ) provider = models.ForeignKey( GeoProviderType, @@ -2129,6 +2140,7 @@ class GeoVectorData(Imported, OwnPerms): null=True, on_delete=models.PROTECT, verbose_name=_("Provider"), + help_text=_("Data provider"), ) comment = models.TextField(_("Comment"), default="", blank=True) x = models.FloatField(_("X"), blank=True, null=True, help_text=_("User input")) diff --git a/ishtar_common/models_imports.py b/ishtar_common/models_imports.py index 6f6d7109e..1fb5d4fcc 100644 --- a/ishtar_common/models_imports.py +++ b/ishtar_common/models_imports.py @@ -64,6 +64,7 @@ from ishtar_common.utils import ( num2col, max_size_help, import_class, + reverse_coordinates, ) from ishtar_common.data_importer import ( Importer, @@ -1086,14 +1087,6 @@ def delayed_check(import_pk): imp.check_modified() -RE_NUMBER = r"[+-]?\d+(?:\.\d*)?" -RE_COORDS = r"(" + RE_NUMBER + r") (" + RE_NUMBER + r")" - - -def _reverse_coordinates(wkt): - return re.sub(RE_COORDS, r"\2 \1", wkt) - - def convert_geom(feature, srid): geo_type = feature["type"] if geo_type in ("LineString", "Polygon"): @@ -1110,7 +1103,7 @@ def convert_geom(feature, srid): srs = profile.srs.srid if srs != srid: # Coordinates are reversed - should be fixed on Django 3.2 - feature = _reverse_coordinates( + feature = reverse_coordinates( GEOSGeometry(feature).transform(srs, clone=True).ewkt) return feature diff --git a/ishtar_common/templates/blocks/bs_form_snippet.html b/ishtar_common/templates/blocks/bs_form_snippet.html index 3d84ce3dc..769234949 100644 --- a/ishtar_common/templates/blocks/bs_form_snippet.html +++ b/ishtar_common/templates/blocks/bs_form_snippet.html @@ -72,7 +72,7 @@ </div> <div class="modal-body body-scroll search-fields"> {% endif %} -{% if field.name in form.HEADERS %} +{% if field.name in form.get_headers %} {% if forloop.counter0 %} </div>{% endif %} diff --git a/ishtar_common/templates/ishtar/blocks/sheet_geographic.html b/ishtar_common/templates/ishtar/blocks/sheet_geographic.html index 69cd67fcc..e1a8a1200 100644 --- a/ishtar_common/templates/ishtar/blocks/sheet_geographic.html +++ b/ishtar_common/templates/ishtar/blocks/sheet_geographic.html @@ -14,7 +14,7 @@ {% for geo in geo_item.geodata.all %} <tr> {% if permission_change_geo %} - <td><a href="#">{% if geo|can_edit_item:request %}<i class="fa fa-pencil"></i></a>{% else %}–{% endif %}</td> + <td><a href="{% url 'edit-geo' geo.pk %}">{% if geo|can_edit_item:request %}<i class="fa fa-pencil"></i></a>{% else %}–{% endif %}</td> {% endif %} <td>{% if geo.id == geo_item.main_geodata_id %}<i class="fa fa-check-circle text-success" aria-hidden="true"></i>{% else %}–{% endif %}</td> <td>{% if geo.data_type %}{{ geo.data_type }}{% else %}-{% endif %}</td> diff --git a/ishtar_common/templates/ishtar/forms/document.html b/ishtar_common/templates/ishtar/forms/base_related_items.html index fe3df8c74..fe3df8c74 100644 --- a/ishtar_common/templates/ishtar/forms/document.html +++ b/ishtar_common/templates/ishtar/forms/base_related_items.html diff --git a/ishtar_common/urls.py b/ishtar_common/urls.py index ffa8daebf..60a2d8767 100644 --- a/ishtar_common/urls.py +++ b/ishtar_common/urls.py @@ -575,6 +575,20 @@ urlpatterns += [ name="new-documenttag", ), url( + r"geo/create/(?P<app_source>[-\w]+)/(?P<model_source>[-\w]+)/(?P<source_pk>\d+)/$", + check_rights(["add_geovectordata", "add_own_geovectordata"])( + views.GeoCreateView.as_view() + ), + name="create-geo", + ), + url( + r"geo/edit/(?P<pk>\d+)/$", + check_rights(["change_geovectordata", "change_own_geovectordata"])( + views.GeoEditView.as_view() + ), + name="edit-geo", + ), + url( r"^qa-not-available(?:/(?P<context>[0-9a-z-]+))?/$", views.QANotAvailable.as_view(), name="qa-not-available", diff --git a/ishtar_common/utils.py b/ishtar_common/utils.py index 3202afede..2a41ab0aa 100644 --- a/ishtar_common/utils.py +++ b/ishtar_common/utils.py @@ -2355,3 +2355,11 @@ def get_eta(current, total, base_time, current_time): if eta < 1: return "-" return f"{int(eta // 3600):02d}:{int(eta % 3600 // 60):02d}:{int(eta % 60):02d}" + + +RE_NUMBER = r"[+-]?\d+(?:\.\d*)?" +RE_COORDS = r"(" + RE_NUMBER + r") (" + RE_NUMBER + r")" + + +def reverse_coordinates(wkt): + return re.sub(RE_COORDS, r"\2 \1", wkt) diff --git a/ishtar_common/views.py b/ishtar_common/views.py index b6686e0bd..361c32022 100644 --- a/ishtar_common/views.py +++ b/ishtar_common/views.py @@ -2131,7 +2131,7 @@ document_search_wizard = wizards.DocumentSearch.as_view( class DocumentFormMixin(IshtarMixin, LoginRequiredMixin): form_class = forms.DocumentForm - template_name = "ishtar/forms/document.html" + template_name = "ishtar/forms/base_related_items.html" model = models.Document def get_context_data(self, **kwargs): @@ -2711,3 +2711,100 @@ class DisplayItemView(IshtarMixin, TemplateView): else: data["show_url"] = "/show-{}/{}/".format(item_type, pk) return data + + +class GeoFormMixin(IshtarMixin, LoginRequiredMixin): + form_class = forms.GISForm + template_name = "ishtar/forms/base_related_items.html" + model = models.GeoVectorData + + def _get_source(self, request): + self.success_url = request.GET.get("source_url") + + def get(self, request, *args, **kwargs): + self._get_source(request) + return super().get(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + self._get_source(request) + return super().post(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + data = super(GeoFormMixin, self).get_context_data(**kwargs) + data["extra_form_modals"] = self.form_class.extra_form_modals + return data + + def get_success_url(self): + if not self.success_url: + return reverse("edit-geo", kwargs={"pk": self.object.pk}) + return f"{self.success_url}?open_item={self.object.pk}" + + +class GeoEditView(GeoFormMixin, UpdateView): + page_name = _("Geo item modification") + + def get_form_kwargs(self): + kwargs = super(GeoEditView, self).get_form_kwargs() + try: + geo = models.GeoVectorData.objects.get(pk=self.kwargs.get("pk")) + assert check_permission(self.request, "geo/edit", geo.pk) + except (AssertionError, models.GeoVectorData.DoesNotExist): + raise Http404() + initial = {} + + for k in ( + list(self.form_class.base_fields.keys()) + + models.GeoVectorData.RELATED_MODELS + ): + value = getattr(geo, k) + if hasattr(value, "all"): + value = ",".join([str(v.pk) for v in value.all()]) + if hasattr(value, "pk"): + value = value.pk + initial[k] = value + + kwargs["initial"] = initial + kwargs["user"] = self.request.user + self.geo = geo + return kwargs + + def get_context_data(self, **kwargs): + kwargs = super(GeoEditView, self).get_context_data(**kwargs) + return kwargs + + +class GeoCreateView(GeoFormMixin, UpdateView): + page_name = _("Geo item creation") + + def get_form_kwargs(self): + if not hasattr(self.request.user, "ishtaruser"): + raise Http404() + ishtaruser = self.request.user.ishtaruser + kwargs = super(GeoCreateView, self).get_form_kwargs() + try: + content_type = ContentType.objects.get( + app_label=self.kwargs.get("app_source"), + model=self.kwargs.get("model_source") + ) + except ContentType.DoesNotExist: + raise Http404() + model = content_type.model_class() + try: + obj = model.objects.get(pk=self.kwargs.get("source_pk")) + except model.DoesNotExist: + raise Http404() + if not ishtaruser.has_perm("add_geovectordata"): # -> add_own_geovectordata + # check permission to view attached item + if not getattr(model, "SLUG", None): + raise Http404() + if not ishtaruser.has_right(f"view_{model.SLUG}") \ + or not ishtaruser.has_right(f"view_own_{model.SLUG}") \ + or not obj.is_own(ishtaruser): + # check permission to view own attached item + raise Http404() + kwargs["user"] = self.request.user + return kwargs + + def get_context_data(self, **kwargs): + kwargs = super(GeoCreateView, self).get_context_data(**kwargs) + return kwargs diff --git a/ishtar_common/widgets.py b/ishtar_common/widgets.py index 05605a258..2b7204dc3 100644 --- a/ishtar_common/widgets.py +++ b/ishtar_common/widgets.py @@ -23,6 +23,7 @@ import logging from django import forms from django.conf import settings +from django.contrib.gis import forms as gis_forms from django.core.exceptions import ValidationError from django.core.files import File from django.db.models import fields @@ -43,6 +44,7 @@ from json import JSONEncoder from django.utils.translation import ugettext_lazy as _ from ishtar_common import models +from ishtar_common.utils import reverse_coordinates logger = logging.getLogger(__name__) @@ -1334,3 +1336,12 @@ class DataTable(Select2Media, forms.RadioSelect): class RangeInput(NumberInput): input_type = "range" + + +class ReversedOSMWidget(gis_forms.OSMWidget): + def get_context(self, name, value, attrs): + if value: + if not isinstance(value, str): # should be geo + value = reverse_coordinates(value.ewkt) + context = super().get_context(name, value, attrs) + return context |