diff options
author | Étienne Loks <etienne.loks@iggdrasil.net> | 2023-04-18 17:21:38 +0200 |
---|---|---|
committer | Étienne Loks <etienne.loks@iggdrasil.net> | 2023-04-18 17:21:38 +0200 |
commit | a589b3ef96c9adf4e408713201ffe7d269e4f78f (patch) | |
tree | 5ad635c7fba5f5d1d1a40a5c5066321c870edfac | |
parent | 2b9862d29073e31cc89e807fd355a691b0d932dd (diff) | |
download | Ishtar-a589b3ef96c9adf4e408713201ffe7d269e4f78f.tar.bz2 Ishtar-a589b3ef96c9adf4e408713201ffe7d269e4f78f.zip |
Document -> Town/Area: models, admin, forms
-rw-r--r-- | ishtar_common/admin.py | 17 | ||||
-rw-r--r-- | ishtar_common/forms_common.py | 3 | ||||
-rw-r--r-- | ishtar_common/migrations/0228_auto_20230418_1622.py | 53 | ||||
-rw-r--r-- | ishtar_common/models.py | 47 | ||||
-rw-r--r-- | ishtar_common/models_common.py | 359 | ||||
-rw-r--r-- | ishtar_common/tests.py | 32 | ||||
-rw-r--r-- | ishtar_common/urls.py | 1 | ||||
-rw-r--r-- | ishtar_common/views.py | 17 |
8 files changed, 351 insertions, 178 deletions
diff --git a/ishtar_common/admin.py b/ishtar_common/admin.py index c824b36f5..e4c4e90ed 100644 --- a/ishtar_common/admin.py +++ b/ishtar_common/admin.py @@ -671,7 +671,8 @@ class PersonAdmin(HistorizedObjectAdmin): "merge_exclusion", "merge_candidate", ) - autocomplete_fields = ["attached_to"] + autocomplete_fields = ["attached_to", "lock_user", "precise_town"] + readonly_fields = HistorizedObjectAdmin.readonly_fields + ["cached_label"] model = models.Person inlines = [ProfileInline] @@ -1151,7 +1152,7 @@ class TownAdmin(ImportGEOJSONActionAdmin, ImportActionAdmin): search_fields += ["numero_insee"] list_filter = ("areas",) form = AdminTownForm - autocomplete_fields = ["children", "main_geodata", "geodata"] + autocomplete_fields = ["children", "main_geodata", "geodata", "documents", "main_image"] inlines = [TownParentInline] actions = [ export_as_csv_action(exclude=["center", "limit"]), @@ -1429,6 +1430,16 @@ class DocumentTag(MergeActionAdmin, GeneralTypeAdmin): class DocumentAdmin(admin.ModelAdmin): model = models.Document search_fields = ("title", "reference", "internal_reference") + autocomplete_fields = ("lock_user", "source", "authors") + readonly_fields = [ + "history_creator", + "history_modifier", + "search_vector", + "history_m2m", + "imports", + "cached_label", + "cache_related_label" + ] admin_site.register(models.Document, DocumentAdmin) @@ -1439,7 +1450,7 @@ class AreaAdmin(CreateDepartmentActionAdmin): search_fields = ("label", "reference") list_filter = ("parent",) model = models.Area - autocomplete_fields = ["towns", "parent"] + autocomplete_fields = ["towns", "parent", "documents", "main_image"] admin_site.register(models.Area, AreaAdmin) diff --git a/ishtar_common/forms_common.py b/ishtar_common/forms_common.py index f031b280f..76e5bbf1e 100644 --- a/ishtar_common/forms_common.py +++ b/ishtar_common/forms_common.py @@ -2210,6 +2210,8 @@ class DocumentSelect(HistorySelect): ), validators=[models.valid_id(Container)], ) + town = get_town_field() + area = widgets.Select2SimpleField(label=_("Area")) receipt_date__before = forms.DateField( label=_("Receipt date before"), widget=DatePicker ) @@ -2237,6 +2239,7 @@ class DocumentSelect(HistorySelect): FieldType("language", models.Language), FieldType("licenses", models.LicenseType), FieldType("operations__operation_type", models.OperationType), + FieldType("area", models.Area), ] PROFILE_FILTER = { diff --git a/ishtar_common/migrations/0228_auto_20230418_1622.py b/ishtar_common/migrations/0228_auto_20230418_1622.py new file mode 100644 index 000000000..65154f308 --- /dev/null +++ b/ishtar_common/migrations/0228_auto_20230418_1622.py @@ -0,0 +1,53 @@ +# Generated by Django 2.2.24 on 2023-04-18 16:22 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('ishtar_common', '0227_auto_20230406_1834'), + ] + + operations = [ + migrations.AlterModelOptions( + name='area', + options={'ordering': ('label',), 'verbose_name': 'Area', 'verbose_name_plural': 'Areas'}, + ), + migrations.AddField( + model_name='area', + name='documents', + field=models.ManyToManyField(blank=True, related_name='areas', to='ishtar_common.Document', verbose_name='Documents'), + ), + migrations.AddField( + model_name='area', + name='main_image', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='main_image_areas', to='ishtar_common.Document', verbose_name='Main image'), + ), + migrations.AddField( + model_name='town', + name='documents', + field=models.ManyToManyField(blank=True, related_name='towns', to='ishtar_common.Document', verbose_name='Documents'), + ), + migrations.AddField( + model_name='town', + name='main_image', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='main_image_towns', to='ishtar_common.Document', verbose_name='Main image'), + ), + migrations.AlterField( + model_name='document', + name='authors', + field=models.ManyToManyField(blank=True, related_name='documents', to='ishtar_common.Author', verbose_name='Authors'), + ), + migrations.AlterField( + model_name='document', + name='container_ref_id', + field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Container reference ID'), + ), + migrations.AlterField( + model_name='historicaldocument', + name='container_ref_id', + field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Container reference ID'), + ), + ] diff --git a/ishtar_common/models.py b/ishtar_common/models.py index ba317998f..dd968f891 100644 --- a/ishtar_common/models.py +++ b/ishtar_common/models.py @@ -2341,7 +2341,8 @@ class DocumentTemplate(models.Model): return output_name -class Area(HierarchicalType): +class Area(HierarchicalType, DocumentItem): + SLUG = "area" towns = models.ManyToManyField( Town, verbose_name=_("Towns"), blank=True, related_name="areas" ) @@ -2355,10 +2356,21 @@ class Area(HierarchicalType): related_name="children", on_delete=models.SET_NULL, ) + documents = models.ManyToManyField( + "Document", related_name="areas", verbose_name=_("Documents"), blank=True + ) + main_image = models.ForeignKey( + "Document", + related_name="main_image_areas", + on_delete=models.SET_NULL, + verbose_name=_("Main image"), + blank=True, + null=True, + ) class Meta: - verbose_name = _("Town - Area") - verbose_name_plural = _("Town - Areas") + verbose_name = _("Area") + verbose_name_plural = _("Areas") ordering = ("label",) ADMIN_SECTION = _("Geography") @@ -2435,6 +2447,12 @@ class Area(HierarchicalType): label.append(self.parent.full_label) return " / ".join(label) + def _get_base_image_path(self): + return self.SLUG + + +#m2m_changed.connect(document_attached_changed, sender=Area.documents.through) + GENDER = ( ("M", _("Male")), @@ -3844,6 +3862,8 @@ class Document( "containers", "files", "administrativeacts", + "towns", + "areas", ] # same fields but in order for forms RELATED_MODELS_ALT = [ @@ -3857,6 +3877,8 @@ class Document( "containers", "treatments", "treatment_files", + "towns", + "areas", ] SLUG = "document" LINK_SPLIT = "<||>" @@ -3874,6 +3896,10 @@ class Document( "history_creator_id", "containers", "sites", + "towns", + "areas", + "main_image_towns", + "main_image_areas", "main_image_warehouses", "main_image_operations", "main_image_treatments", @@ -3915,6 +3941,8 @@ class Document( SearchVectorConfig("warehouses__name"), SearchVectorConfig("containers__cached_label"), SearchVectorConfig("files__cached_label"), + SearchVectorConfig("towns__name"), + SearchVectorConfig("areas__label"), ] PARENT_SEARCH_VECTORS = [ "authors", @@ -4082,6 +4110,14 @@ class Document( pgettext_lazy("key for text search", "warehouse"), "warehouses__name__iexact", ), + "town": SearchAltName( + pgettext_lazy("key for text search", "town"), + "towns__name__iexact", + ), + "area": SearchAltName( + pgettext_lazy("key for text search", "area"), + "areas__label__iexact", + ), "image__isnull": SearchAltName( pgettext_lazy("key for text search", "has-image"), "image__isnull" ), @@ -4275,7 +4311,8 @@ class Document( ) scale = models.CharField(_("Scale"), max_length=30, null=True, blank=True) authors = models.ManyToManyField( - Author, verbose_name=_("Authors"), related_name="documents" + Author, verbose_name=_("Authors"), related_name="documents", + blank=True ) authors_raw = models.CharField( verbose_name=_("Authors (raw)"), blank=True, null=True, max_length=250 @@ -4302,7 +4339,7 @@ class Document( ) # container = models.ForeignKey("archaeological_warehouse.Container") container_ref_id = models.PositiveIntegerField( - verbose_name=_("Container ID"), blank=True, null=True + verbose_name=_("Container reference ID"), blank=True, null=True ) # container_ref = models.ForeignKey("archaeological_warehouse.Container") comment = models.TextField(_("Comment"), blank=True, default="") diff --git a/ishtar_common/models_common.py b/ishtar_common/models_common.py index 1e6da2b7d..8942d57c2 100644 --- a/ishtar_common/models_common.py +++ b/ishtar_common/models_common.py @@ -1978,6 +1978,175 @@ class OwnPerms(object): return q +class DocumentItem: + ALT_NAMES = { + "documents__image__isnull": SearchAltName( + pgettext_lazy("key for text search", "has-image"), + "documents__image__isnull", + ), + "documents__associated_url__isnull": SearchAltName( + pgettext_lazy("key for text search", "has-url"), + "documents__associated_url__isnull", + ), + "documents__associated_file__isnull": SearchAltName( + pgettext_lazy("key for text search", "has-attached-file"), + "documents__associated_file__isnull", + ), + } + + def documents_list(self) -> list: + Document = apps.get_model("ishtar_common", "Document") + return self.get_associated_main_item_list("documents", Document) + + def public_representation(self): + images = [] + if getattr(self, "main_image", None): + images.append(self.main_image.public_representation()) + images += [ + image.public_representation() + for image in self.images_without_main_image.all() + ] + return {"images": images} + + @property + def images(self): + if not hasattr(self, "documents"): + Document = apps.get_model("ishtar_common", "Document") + return Document.objects.none() + return ( + self.documents.filter(image__isnull=False).exclude(image="").order_by("pk") + ) + + @property + def images_number(self): + return self.images.count() + + @property + def images_without_main_image(self): + if not hasattr(self, "main_image") or not hasattr(self, "documents"): + return self.images + if not self.main_image: + return ( + self.documents.filter(image__isnull=False) + .exclude(image="") + .order_by("pk") + ) + return ( + self.documents.filter(image__isnull=False) + .exclude(image="") + .exclude(pk=self.main_image.pk) + .order_by("pk") + ) + + @property + def pdf_attached(self): + for document in self.documents.filter( + Q(associated_file__isnull=False) | Q(source__associated_file__isnull=False) + ).all(): + return document.pdf_attached + + def get_extra_actions(self, request): + """ + For sheet template: return "Add document / image" action + """ + # url, base_text, icon, extra_text, extra css class, is a quick action + try: + actions = super(DocumentItem, self).get_extra_actions(request) + except AttributeError: + actions = [] + + if not hasattr(self, "SLUG"): + return actions + + can_add_doc = self.can_do(request, "add_document") + if can_add_doc and ( + not hasattr(self, "is_locked") or not self.is_locked(request.user) + ): + actions += [ + ( + reverse("create-document") + "?{}={}".format(self.SLUG, self.pk), + _("Add document/image"), + "fa fa-plus", + _("doc./image"), + "", + False, + ) + ] + return actions + + +def clean_duplicate_association(document, related_item, action): + profile = get_current_profile() + if not profile.clean_redundant_document_association or action != "post_add": + return + class_name = related_item.__class__.__name__ + if class_name not in ("Find", "ContextRecord", "Operation"): + return + if class_name == "Find": + for cr in document.context_records.filter( + base_finds__find__pk=related_item.pk + ).all(): + document.context_records.remove(cr) + for ope in document.operations.filter( + context_record__base_finds__find__pk=related_item.pk + ).all(): + document.operations.remove(ope) + return + if class_name == "ContextRecord": + for ope in document.operations.filter(context_record__pk=related_item.pk).all(): + document.operations.remove(ope) + if document.finds.filter(base_finds__context_record=related_item.pk).count(): + document.context_records.remove(related_item) + return + if class_name == "Operation": + if document.context_records.filter(operation=related_item.pk).count(): + document.operations.remove(related_item) + return + if document.finds.filter( + base_finds__context_record__operation=related_item.pk + ).count(): + document.operations.remove(related_item) + return + + +def document_attached_changed(sender, **kwargs): + # associate a default main image + instance = kwargs.get("instance", None) + model = kwargs.get("model", None) + pk_set = kwargs.get("pk_set", None) + if not instance or not model: + return + + if hasattr(instance, "documents"): + items = [instance] + else: + if not pk_set: + return + try: + items = [model.objects.get(pk=pk) for pk in pk_set] + except model.DoesNotExist: + return + + for item in items: + clean_duplicate_association(instance, item, kwargs.get("action", None)) + for doc in item.documents.all(): + doc.regenerate_all_ids() + q = item.documents.filter(image__isnull=False).exclude(image="") + if item.main_image: + if q.filter(pk=item.main_image.pk).count(): + return + # the association has disappear not the main image anymore + item.main_image = None + item.skip_history_when_saving = True + item.save() + if not q.count(): + return + # by default get the lowest pk + item.main_image = q.order_by("pk").all()[0] + item.skip_history_when_saving = True + item.save() + + class NumberManager(models.Manager): def get_by_natural_key(self, number): return self.get(number=number) @@ -2927,7 +3096,8 @@ class TownManager(models.Manager): return self.get(numero_insee=numero_insee, year=year) -class Town(GeographicItem, Imported, models.Model): +class Town(GeographicItem, Imported, DocumentItem, models.Model): + SLUG = "town" name = models.CharField(_("Name"), max_length=100) surface = models.IntegerField(_("Surface (m2)"), blank=True, null=True) center = models.PointField( @@ -2956,6 +3126,17 @@ class Town(GeographicItem, Imported, models.Model): cached_label = models.CharField( _("Cached name"), max_length=500, null=True, blank=True, db_index=True ) + documents = models.ManyToManyField( + "Document", related_name="towns", verbose_name=_("Documents"), blank=True + ) + main_image = models.ForeignKey( + "Document", + related_name="main_image_towns", + on_delete=models.SET_NULL, + verbose_name=_("Main image"), + blank=True, + null=True, + ) objects = TownManager() class Meta: @@ -3085,6 +3266,12 @@ class Town(GeographicItem, Imported, models.Model): if self.numero_insee != old_num: return True + def _get_base_image_path(self): + if self.numero_insee and len(self.numero_insee) == 5: + prefix = self.numero_insee[:2] + return f"{self.SLUG}/{prefix}" + return self.SLUG + def _generate_cached_label(self): cached_label = self.name if settings.COUNTRY == "fr" and self.numero_insee: @@ -3112,6 +3299,7 @@ def post_save_town(sender, **kwargs): post_save.connect(post_save_town, sender=Town) m2m_changed.connect(geodata_attached_changed, sender=Town.geodata.through) +m2m_changed.connect(document_attached_changed, sender=Town.documents.through) def town_child_changed(sender, **kwargs): @@ -3563,175 +3751,6 @@ class DashboardFormItem: return q.order_by("pk").distinct("pk").count() -class DocumentItem: - ALT_NAMES = { - "documents__image__isnull": SearchAltName( - pgettext_lazy("key for text search", "has-image"), - "documents__image__isnull", - ), - "documents__associated_url__isnull": SearchAltName( - pgettext_lazy("key for text search", "has-url"), - "documents__associated_url__isnull", - ), - "documents__associated_file__isnull": SearchAltName( - pgettext_lazy("key for text search", "has-attached-file"), - "documents__associated_file__isnull", - ), - } - - def documents_list(self) -> list: - Document = apps.get_model("ishtar_common", "Document") - return self.get_associated_main_item_list("documents", Document) - - def public_representation(self): - images = [] - if getattr(self, "main_image", None): - images.append(self.main_image.public_representation()) - images += [ - image.public_representation() - for image in self.images_without_main_image.all() - ] - return {"images": images} - - @property - def images(self): - if not hasattr(self, "documents"): - Document = apps.get_model("ishtar_common", "Document") - return Document.objects.none() - return ( - self.documents.filter(image__isnull=False).exclude(image="").order_by("pk") - ) - - @property - def images_number(self): - return self.images.count() - - @property - def images_without_main_image(self): - if not hasattr(self, "main_image") or not hasattr(self, "documents"): - return self.images - if not self.main_image: - return ( - self.documents.filter(image__isnull=False) - .exclude(image="") - .order_by("pk") - ) - return ( - self.documents.filter(image__isnull=False) - .exclude(image="") - .exclude(pk=self.main_image.pk) - .order_by("pk") - ) - - @property - def pdf_attached(self): - for document in self.documents.filter( - Q(associated_file__isnull=False) | Q(source__associated_file__isnull=False) - ).all(): - return document.pdf_attached - - def get_extra_actions(self, request): - """ - For sheet template: return "Add document / image" action - """ - # url, base_text, icon, extra_text, extra css class, is a quick action - try: - actions = super(DocumentItem, self).get_extra_actions(request) - except AttributeError: - actions = [] - - if not hasattr(self, "SLUG"): - return actions - - can_add_doc = self.can_do(request, "add_document") - if can_add_doc and ( - not hasattr(self, "is_locked") or not self.is_locked(request.user) - ): - actions += [ - ( - reverse("create-document") + "?{}={}".format(self.SLUG, self.pk), - _("Add document/image"), - "fa fa-plus", - _("doc./image"), - "", - False, - ) - ] - return actions - - -def clean_duplicate_association(document, related_item, action): - profile = get_current_profile() - if not profile.clean_redundant_document_association or action != "post_add": - return - class_name = related_item.__class__.__name__ - if class_name not in ("Find", "ContextRecord", "Operation"): - return - if class_name == "Find": - for cr in document.context_records.filter( - base_finds__find__pk=related_item.pk - ).all(): - document.context_records.remove(cr) - for ope in document.operations.filter( - context_record__base_finds__find__pk=related_item.pk - ).all(): - document.operations.remove(ope) - return - if class_name == "ContextRecord": - for ope in document.operations.filter(context_record__pk=related_item.pk).all(): - document.operations.remove(ope) - if document.finds.filter(base_finds__context_record=related_item.pk).count(): - document.context_records.remove(related_item) - return - if class_name == "Operation": - if document.context_records.filter(operation=related_item.pk).count(): - document.operations.remove(related_item) - return - if document.finds.filter( - base_finds__context_record__operation=related_item.pk - ).count(): - document.operations.remove(related_item) - return - - -def document_attached_changed(sender, **kwargs): - # associate a default main image - instance = kwargs.get("instance", None) - model = kwargs.get("model", None) - pk_set = kwargs.get("pk_set", None) - if not instance or not model: - return - - if hasattr(instance, "documents"): - items = [instance] - else: - if not pk_set: - return - try: - items = [model.objects.get(pk=pk) for pk in pk_set] - except model.DoesNotExist: - return - - for item in items: - clean_duplicate_association(instance, item, kwargs.get("action", None)) - for doc in item.documents.all(): - doc.regenerate_all_ids() - q = item.documents.filter(image__isnull=False).exclude(image="") - if item.main_image: - if q.filter(pk=item.main_image.pk).count(): - return - # the association has disappear not the main image anymore - item.main_image = None - item.skip_history_when_saving = True - item.save() - if not q.count(): - return - # by default get the lowest pk - item.main_image = q.order_by("pk").all()[0] - item.skip_history_when_saving = True - item.save() - - class QuickAction: """ Quick action available from tables diff --git a/ishtar_common/tests.py b/ishtar_common/tests.py index 7328a5f95..4e450c3f8 100644 --- a/ishtar_common/tests.py +++ b/ishtar_common/tests.py @@ -3821,9 +3821,41 @@ class DocumentTest(TestCase): self.find2.base_finds.add(bf2) self.st1 = models.SourceType.objects.create(label="Report", code="REP") self.st2 = models.SourceType.objects.create(label="Illustration", code="ILL") + self.town = models.Town.objects.create(name="Daisy town", numero_insee="59134") self.username, self.password, self.user = create_superuser() + def test_create_form(self): + nb_doc = models.Document.objects.count() + c = Client() + url = reverse("create-document") + response = c.get(url) + self.assertEqual(response.status_code, 302) + + c.login(username=self.username, password=self.password) + response = c.get(url) + self.assertEqual(response.status_code, 200) + + posted = { + "authors": [], + "title": "A document", + "operations": [str(self.ope1.pk)], + "towns": [str(self.town.pk)], + } + response = c.post(url, posted) + new_child_document = self.ope1.documents.order_by("-pk").all()[0] + self.assertEqual(nb_doc + 1, models.Document.objects.count()) + self.assertRedirects( + response, + "/document/edit/?open_item={}".format( + new_child_document.pk + ), + ) + self.assertIn( + self.town.pk, + list(new_child_document.towns.values_list("pk", flat=True).all()) + ) + def test_custom_index(self): profile, created = models.IshtarSiteProfile.objects.get_or_create( slug="default", active=True diff --git a/ishtar_common/urls.py b/ishtar_common/urls.py index ad7e22019..0fe9c1716 100644 --- a/ishtar_common/urls.py +++ b/ishtar_common/urls.py @@ -339,6 +339,7 @@ urlpatterns += [ views.new_person_noorga, name="new-person-noorga", ), + url(r"autocomplete-area/$", views.autocomplete_area, name="autocomplete-area"), url(r"autocomplete-user/$", views.autocomplete_user, name="autocomplete-user"), url( r"autocomplete-ishtaruser/$", diff --git a/ishtar_common/views.py b/ishtar_common/views.py index ba205abf3..b469df12d 100644 --- a/ishtar_common/views.py +++ b/ishtar_common/views.py @@ -823,6 +823,23 @@ def autocomplete_person( return HttpResponse(data, content_type="text/plain") +def autocomplete_area(request): + if not request.GET.get("term"): + return HttpResponse("[]", content_type="text/plain") + q = request.GET.get("term") + q = unicodedata.normalize("NFKD", q).encode("ascii", "ignore").decode() + query = Q() + for q in q.split(" "): + extra = Q(label__icontains=q) + query = query & extra + limit = 20 + areas = models.Area.objects.filter(query).distinct()[:limit] + data = json.dumps( + [{"id": area.pk, "value": str(area)} for area in areas] + ) + return HttpResponse(data, content_type="text/plain") + + def autocomplete_department(request): if not request.GET.get("term"): return HttpResponse("[]", content_type="text/plain") |