SciPost Code Repository

Skip to content
Snippets Groups Projects
Commit 7d8ba261 authored by George Katsikas's avatar George Katsikas :goat:
Browse files

feat(common): :sparkles: add non duplicate declaration to object merger

fixes #304
parent 4c0458b5
No related branches found
No related tags found
No related merge requests found
# Generated by Django 5.0.12 on 2025-03-04 16:49
from django.db import migrations, models
from django.db.models import F
def enforce_object_id_order(apps, schema_editor):
Nonduplicate = apps.get_model("common", "Nonduplicate")
Nonduplicate.objects.filter(object_a_id__gt=F("object_b_id")).update(
object_a_id=F("object_b_id"), object_b_id=F("object_a_id")
)
class Migration(migrations.Migration):
dependencies = [
("common", "0002_migrate_nonduplicate_profiles"),
("contenttypes", "0002_remove_content_type_name"),
(
"scipost",
"0041_alter_remark_contributor_alter_remark_recommendation_and_more",
),
]
operations = [
migrations.AlterUniqueTogether(
name="nonduplicate",
unique_together=set(),
),
migrations.AddConstraint(
model_name="nonduplicate",
constraint=models.UniqueConstraint(
fields=("content_type", "object_a_id", "object_b_id"),
name="unique_non_duplicate",
violation_error_message="This non-duplicate declaration already exists",
),
),
migrations.RunPython(
enforce_object_id_order, reverse_code=migrations.RunPython.noop
),
migrations.AddConstraint(
model_name="nonduplicate",
constraint=models.CheckConstraint(
check=models.Q(("object_a_id__lt", models.F("object_b_id"))),
name="object_a_lt_object_b",
violation_error_message="To avoid duplicate declarations, object_a_id must be less than object_b_id",
),
),
]
......@@ -8,7 +8,6 @@ from scipost.models import Contributor
class NonDuplicate(models.Model):
contributor = models.ForeignKey[Contributor](
"scipost.Contributor",
on_delete=models.CASCADE,
......@@ -29,7 +28,18 @@ class NonDuplicate(models.Model):
created = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ("content_type", "object_a_id", "object_b_id")
constraints = [
models.UniqueConstraint(
fields=["content_type", "object_a_id", "object_b_id"],
name="unique_non_duplicate",
violation_error_message="This non-duplicate declaration already exists",
),
models.CheckConstraint(
check=models.Q(object_a_id__lt=models.F("object_b_id")),
name="object_a_lt_object_b",
violation_error_message="To avoid duplicate declarations, object_a_id must be less than object_b_id",
),
]
verbose_name = "Non-duplicate"
verbose_name_plural = "Non-duplicates"
......
......@@ -18,19 +18,42 @@
<div class="col fs-4">
<a href="{{ object_a.get_absolute_url }}">{{ object_a }}</a>
</div>
<div class="col-auto d-flex flex-column justify-content-center">
<a class="btn btn-sm btn-primary"
href="{% url "common:object_merger_merge" content_type.id object_a.id object_b.id %}">
{% include "bi/arrow-right.html" %}
</a>
<a class="btn btn-sm btn-secondary"
href="{% url "common:object_merger_merge" content_type.id object_b.id object_a.id %}">
{% include "bi/arrow-left.html" %}
</a>
</div>
{% if not non_duplicate_declaration %}
<div class="col-auto d-flex flex-column justify-content-center">
<div class="d-flex flex-row">
<a class="btn btn-sm btn-primary flex-fill"
href="{% url "common:object_merger_merge" content_type.id object_a.id object_b.id %}">
{% include "bi/arrow-right.html" %}
</a>
<a class="btn btn-sm btn-secondary flex-fill"
href="{% url "common:object_merger_merge" content_type.id object_b.id object_a.id %}">
{% include "bi/arrow-left.html" %}
</a>
</div>
<button class="btn btn-sm btn-warning"
hx-swap="afterend"
hx-get="{% url "common:object_merger_mark_non_duplicate" content_type.id object_b.id object_a.id %}"
hx-prompt="What is the reason/source for marking these as definitively not duplicates?">
Non Duplicate
</button>
</div>
{% endif %}
<div class="col fs-4">
<a href="{{ object_b.get_absolute_url }}">{{ object_b }}</a>
</div>
{% if non_duplicate_declaration %}
<div class="bg-warning bg-opacity-25 p-2 mt-2">
<div class="d-flex justify-content-between">
<span class="fw-bold">Non Duplicate Declaration by {{ non_duplicate_declaration.contributor.profile.full_name }}</span>
<span>{{ non_duplicate_declaration.created|date:"Y-m-d" }}</span>
</div>
<p class="mb-0">{{ non_duplicate_declaration.description }}</p>
</div>
{% endif %}
</div>
{% for group_name, group in field_groups.items %}
......@@ -67,20 +90,24 @@
{% if not forloop.last %}
<div class="col-12 col-md-auto">
<hr class="d-md-none">
<hr class="d-md-none" />
</div>
{% endif %}
{% endfor %}
</div>
</details>
{% endfor %}
{% else %}
{% for field_name, objects_field_value in group.items %}
<div class="row">
<h3 class="fs-5 bg-light">{{ field_name|title }}</h3>
{% for object_field_value in objects_field_value %}
<div class="col-12 col-md">
......@@ -91,10 +118,13 @@
{% endif %}
</div>
{% endfor %}
{% endfor %}
</div>
{% endfor %}
{% endif %}
{% endfor %}
......
......@@ -43,6 +43,11 @@ urlpatterns = [
merger_views.HXMergeView.as_view(),
name="object_merger_merge",
),
path(
"mark_non_duplicate",
merger_views.HXNonDuplicateCreateView.as_view(),
name="object_merger_mark_non_duplicate",
),
]
),
),
......
......@@ -10,6 +10,9 @@ from django.db.models import Model, QuerySet, Field, ForeignObjectRel
from django.http import Http404, HttpRequest, HttpResponse
from django.views.generic import TemplateView
from common.models import NonDuplicate
from scipost.permissions import HTMXPermissionsDenied, HTMXResponse
FieldValue = Model | list[Model] | None
FieldOrRel = Field[Any, Any] | ForeignObjectRel
T = TypeVar("T")
......@@ -69,6 +72,28 @@ class CompareView(PermissionRequiredMixin, TemplateView):
return object_a, object_b
def get_non_duplicate_declaration(self) -> NonDuplicate | None:
"""
Retrieve all NonDuplicate declarations for the two objects.
"""
if not (content_type_id := self.kwargs.get(self.content_type_kwarg)):
raise BadRequest("No content type ID provided.")
if not (object_a_id := self.kwargs.get(self.object_a_kwarg)):
raise BadRequest("No object A ID provided.")
if not (object_b_id := self.kwargs.get(self.object_b_kwarg)):
raise BadRequest("No object B ID provided.")
# Ensure object_a_id < object_b_id
if object_a_id > object_b_id:
object_a_id, object_b_id = object_b_id, object_a_id
return NonDuplicate.objects.filter(
content_type_id=content_type_id,
object_a_id=object_a_id,
object_b_id=object_b_id,
).first()
def get_permission_required(self) -> list[str]:
perms = list(super().get_permission_required())
......@@ -139,7 +164,6 @@ class CompareView(PermissionRequiredMixin, TemplateView):
@staticmethod
def group_fields(field_data: dict[FieldOrRel, T]) -> dict[str, dict[FieldOrRel, T]]:
groups: dict[str, dict[FieldOrRel, T]] = {
"fields": {},
"related_objects_one": {},
......@@ -170,12 +194,14 @@ class HXCompareView(CompareView):
content_type = self.get_content_type()
object_a, object_b = self.get_objects()
non_duplicate_declaration = self.get_non_duplicate_declaration()
context |= {
"content_type": content_type,
"objects": [object_a, object_b],
"object_a": object_a,
"object_b": object_b,
"non_duplicate_declaration": non_duplicate_declaration,
}
if model := content_type.model_class():
......@@ -249,12 +275,14 @@ class HXMergeView(CompareView):
content_type = self.get_content_type()
object_from, object_to = self.get_objects()
non_duplicate_declaration = self.get_non_duplicate_declaration()
context |= {
"content_type": content_type,
"objects": [object_from, object_to],
"object_from": object_from,
"object_to": object_to,
"non_duplicate_declaration": non_duplicate_declaration,
}
if model := content_type.model_class():
......@@ -277,3 +305,38 @@ class HXMergeView(CompareView):
return context
class HXNonDuplicateCreateView(CompareView):
"""
View for creating a NonDuplicate object.
"""
permission_required = "scipost.can_mark_non_duplicates"
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
if request.headers.get("HX-Request") != "true":
raise BadRequest("This view is only accessible via HTMX.")
if not request.user.has_perm("scipost.can_mark_non_duplicates"):
return HTMXPermissionsDenied(
"You do not have permission to mark non-duplicates."
)
content_type = self.get_content_type()
object_a, object_b = self.get_objects()
# Ensure object_a_id < object_b_id
if object_a.id > object_b.id:
object_a, object_b = object_b, object_a
NonDuplicate.objects.create(
object_a=object_a,
object_b=object_b,
contributor=request.user.contributor,
description=request.headers.get("HX-Prompt"),
)
return HTMXResponse(
"Objects marked as non-duplicates.",
tag="success",
)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment