Skip to content

Commit e50708c

Browse files
committed
531: List duplicated words on dashboard
1 parent cf87ce4 commit e50708c

32 files changed

+512
-240
lines changed

lunes_cms/api/utils.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ def find_duplicates_for_word(request, word):
186186
if duplicate.definition
187187
else _("Definition: ") + _("No definition is provided for this word.")
188188
),
189-
"training_sets": _("Training sets: ") + training_sets_description,
189+
"training_sets": _("Training sets") + ": " + training_sets_description,
190190
}
191191

192192
return JsonResponse(result)

lunes_cms/cms/admin.py

+31-9
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,44 @@
11
"""
2-
Register models for Django's CRUD back end and
2+
Register models for Django"s CRUD back end and
33
specify autocomplete_fields, search_fields and nested modules
44
"""
55
from __future__ import absolute_import, unicode_literals
66

77
from django.contrib import admin
88
from django.utils.translation import gettext_lazy as _
99

10-
from .admins import (
11-
DisciplineAdmin,
12-
DocumentAdmin,
13-
FeedbackAdmin,
14-
GroupAPIKeyAdmin,
15-
SponsorAdmin,
16-
TrainingSetAdmin,
17-
)
10+
from .admin.discipline import DisciplineAdmin
11+
from .admin.document import DocumentAdmin
12+
from .admin.document.duplicates import get_duplicate_vocabularies
13+
from .admin.feedback import FeedbackAdmin
14+
from .admin.group_api_key import GroupAPIKeyAdmin
15+
from .admin.sponsor import SponsorAdmin
16+
from .admin.training_set import TrainingSetAdmin
1817
from .models import Discipline, Document, Feedback, GroupAPIKey, Sponsor, TrainingSet
1918

2019

20+
def each_context(self, request):
21+
"""
22+
Return a dictionary of variables to put in the template context for
23+
*every* page in the admin site.
24+
25+
For sites running on a subpath, use the SCRIPT_NAME value if site_url
26+
hasn't been customized.
27+
"""
28+
script_name = request.META["SCRIPT_NAME"]
29+
site_url = script_name if self.site_url == "/" and script_name else self.site_url
30+
return {
31+
"site_title": self.site_title,
32+
"site_header": self.site_header,
33+
"site_url": site_url,
34+
"has_permission": self.has_permission(request),
35+
"available_apps": self.get_app_list(request),
36+
"is_popup": False,
37+
"is_nav_sidebar_enabled": self.enable_nav_sidebar,
38+
"duplicate_vocabularies": get_duplicate_vocabularies(),
39+
}
40+
41+
2142
def get_app_list(self, request):
2243
"""
2344
Function that returns a sorted list of all the installed apps that have been
@@ -54,6 +75,7 @@ def get_app_list(self, request):
5475
return app_list
5576

5677

78+
admin.AdminSite.each_context = each_context
5779
admin.AdminSite.get_app_list = get_app_list
5880
admin.site.register(Discipline, DisciplineAdmin)
5981
admin.site.register(TrainingSet, TrainingSetAdmin)

lunes_cms/cms/admin/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from .discipline_admin import DisciplineAdmin
2+
from .form import DisciplineChoiceField
3+
from .list_filter import DisciplineListFilter
4+
5+
__all__ = [
6+
"DisciplineAdmin",
7+
"DisciplineChoiceField",
8+
"DisciplineListFilter",
9+
]

lunes_cms/cms/admins/discipline_admin.py lunes_cms/cms/admin/discipline/discipline_admin.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
from mptt.admin import DraggableMPTTAdmin
1515
from tablib import Dataset
1616

17-
from ..models import Discipline, Document, Static
18-
from .document_resource import DocumentResource
17+
from ..document import DocumentResource
18+
from ...models import Discipline, Document, Static
1919

2020

2121
class DisciplineAdmin(DraggableMPTTAdmin):
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from django import forms
2+
3+
4+
class DisciplineChoiceField(forms.ModelMultipleChoiceField):
5+
"""
6+
Custom form field in order to include parent nodes in string representation.
7+
Inherits from `forms.ModelMultipleChocieField`.
8+
"""
9+
10+
def label_from_instance(self, obj):
11+
if obj.parent:
12+
ancestors = [
13+
node.title for node in obj.parent.get_ancestors(include_self=True)
14+
]
15+
ancestors.append(obj.title)
16+
return " \u2794 ".join(ancestors)
17+
return obj.title
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from django.contrib import admin
2+
from django.db.models import F
3+
from django.utils.translation import gettext_lazy as _
4+
5+
from ...models import Discipline
6+
7+
8+
class DisciplineListFilter(admin.SimpleListFilter):
9+
"""
10+
Generic Filter for models that have a direct relationship to disciplines.
11+
Inherits from `admin.SimpleListFilter`.
12+
"""
13+
14+
title = _("disciplines")
15+
16+
# Parameter for the filter that will be used in the URL query.
17+
parameter_name = "disciplines"
18+
19+
template = "admin/discipline_filter.html"
20+
21+
def lookups(self, request, model_admin):
22+
"""
23+
Defining look up values that can be seen in the admin
24+
interface. Returns tuples: the first element is a coded
25+
value, whereas the second one is human-readable.
26+
27+
:param request: current user request
28+
:type request: django.http.request
29+
:param model_admin: admin of current model
30+
:type model_admin: ModelAdmin
31+
:return: list of tuples containing id and title of each discipline
32+
:rtype: list
33+
"""
34+
35+
# Verify that only disciplines are displayed that actually can contain training sets
36+
queryset = Discipline.objects.filter(lft=F("rght") - 1)
37+
38+
if "training set" in request.GET:
39+
queryset = queryset.filter(training_sets=request.GET["training set"])
40+
41+
if request.user.is_superuser:
42+
queryset = queryset.filter(creator_is_admin=True)
43+
else:
44+
queryset = queryset.filter(created_by__in=request.user.groups.all())
45+
46+
list_of_disciplines = [
47+
(
48+
str(discipline.id),
49+
f"{discipline.parent} \u2794 {discipline}",
50+
)
51+
for discipline in queryset
52+
]
53+
return sorted(list_of_disciplines, key=lambda tp: tp[1])
54+
55+
def queryset(self, request, queryset):
56+
"""
57+
Returns the filtered queryset based on the value
58+
provided in the query string and retrievable via
59+
`self.value()`.
60+
61+
:param request: current user request
62+
:type request: django.http.request
63+
:param queryset: current queryset
64+
:type queryset: QuerySet
65+
:return: filtered queryset based on the value provided in the query string
66+
:rtype: QuerySet
67+
"""
68+
if self.value():
69+
return queryset.filter(discipline=self.value()).distinct()
70+
return queryset
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from .document_admin import DocumentAdmin
2+
from .document_resource import DocumentResource
3+
from .list_filter import (
4+
ApprovedImageListFilter,
5+
AssignedListFilter,
6+
DocumentDisciplineListFilter,
7+
DocumentTrainingSetListFilter,
8+
)
9+
10+
__all__ = [
11+
"ApprovedImageListFilter",
12+
"AssignedListFilter",
13+
"DocumentAdmin",
14+
"DocumentResource",
15+
"DocumentDisciplineListFilter",
16+
"DocumentTrainingSetListFilter",
17+
]

lunes_cms/cms/admins/alternative_word_admin.py lunes_cms/cms/admin/document/alternative_word_admin.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from django.contrib import admin
22

3-
from ..models import AlternativeWord
3+
from ...models import AlternativeWord
44

55

66
class AlternativeWordAdmin(admin.StackedInline):

lunes_cms/cms/admins/document_admin.py lunes_cms/cms/admin/document/document_admin.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
from django.db.models import Case, Exists, IntegerField, OuterRef, Value, When
55
from django.utils.translation import gettext_lazy as _
66

7-
from ..list_filter import (
7+
from .list_filter import (
88
ApprovedImageListFilter,
9-
AssignedListFilter,
109
DocumentDisciplineListFilter,
1110
DocumentTrainingSetListFilter,
11+
AssignedListFilter,
1212
)
13-
from ..models import DocumentImage, Static
1413
from .alternative_word_admin import AlternativeWordAdmin
1514
from .document_image_admin import DocumentImageAdmin
15+
from ...models import DocumentImage, Static
1616

1717
SUPERUSER_ONLY_LIST_FILTERS = [ApprovedImageListFilter]
1818

lunes_cms/cms/admins/document_image_admin.py lunes_cms/cms/admin/document/document_image_admin.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from django.contrib import admin
22

3-
from ..models import DocumentImage
3+
from ...models import DocumentImage
44

55

66
class DocumentImageAdmin(admin.StackedInline):

lunes_cms/cms/admins/document_resource.py lunes_cms/cms/admin/document/document_resource.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
from import_export import fields, resources
33
from import_export.admin import ExportActionMixin
44

5-
from ..models import Document
6-
from ..models.static import Static
5+
from ...models import Document
6+
from ...models.static import Static
77

88

99
class DocumentResource(resources.ModelResource):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .get_duplicates import get_duplicate_vocabularies
2+
3+
__all__ = [
4+
"get_duplicate_vocabularies",
5+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from collections import defaultdict
2+
from typing import TypedDict, List
3+
4+
from django.db.models import Count
5+
from django.db.models.functions import Lower
6+
7+
from lunes_cms.cms.models import Document
8+
9+
10+
class DuplicateVocabulary(TypedDict):
11+
"""
12+
Dictionary type representing a duplicate vocabulary group
13+
"""
14+
15+
word: str
16+
word_type: str
17+
documents: List[Document]
18+
19+
20+
class DuplicateVocabularyDocument(TypedDict):
21+
"""
22+
Dictionary type representing a document with a duplicate vocabulary group
23+
"""
24+
25+
id: str
26+
word: str
27+
training_sets: List[str]
28+
29+
30+
def map_to_duplicate_vocabulary_documents(documents) -> [DuplicateVocabularyDocument]:
31+
"""
32+
Maps the result of the get duplicate vocabularies query to DuplicateVocabularyDocument
33+
"""
34+
mapped = []
35+
for document in documents:
36+
training_sets = []
37+
for training_set in document.get("_prefetched_objects_cache").get(
38+
"training_sets", None
39+
):
40+
training_sets.append(training_set.title)
41+
mapped.append(
42+
{"id": document.id, "word": document.word, "training_sets": training_sets}
43+
)
44+
return mapped
45+
46+
47+
def get_duplicate_vocabularies() -> List[DuplicateVocabulary]:
48+
"""
49+
Retrieves duplicate vocabularies from the database
50+
"""
51+
duplicate_groups = (
52+
Document.objects.annotate(lower_word=Lower("word"))
53+
.values("lower_word", "word_type")
54+
.annotate(count=Count("id"))
55+
.filter(count__gt=1)
56+
)
57+
duplicate_vocabularies = Document.objects.prefetch_related("training_sets").filter(
58+
id__in=Document.objects.annotate(lower_word=Lower("word"))
59+
.filter(
60+
lower_word__in=[group["lower_word"] for group in duplicate_groups],
61+
word_type__in=[group["word_type"] for group in duplicate_groups],
62+
)
63+
.values_list("id", flat=True)
64+
)
65+
66+
grouped_duplicate_vocabularies = defaultdict(list)
67+
for v in duplicate_vocabularies:
68+
grouped_duplicate_vocabularies[(v.word.lower(), v.word_type)].append(v)
69+
70+
result = []
71+
for _, value in grouped_duplicate_vocabularies.items():
72+
result.append(
73+
{
74+
"word": value[0].word,
75+
"word_type": value[0].word_type,
76+
"documents": map_to_duplicate_vocabulary_documents(value),
77+
}
78+
)
79+
return result

0 commit comments

Comments
 (0)