From abd6cdc60ccbb21f1d9dce146ca22cb9679a27e8 Mon Sep 17 00:00:00 2001 From: Olivier Bado-Faustin <12731381+Badatos@users.noreply.github.com> Date: Tue, 3 Dec 2024 16:31:20 +0100 Subject: [PATCH] Display a tag cloud with tags assigned to videos + Store tag list in CACHE + add a reindex_videos script to recreate ES video index --- pod/main/configuration.json | 36 ++++++++----- pod/main/static/css/pod.css | 29 ++++++++++ pod/main/templates/aside.html | 15 +++--- pod/urls.py | 4 +- pod/video/context_processors.py | 8 +++ .../management/commands/cache_video_data.py | 4 +- .../management/commands/reindex_videos.py | 54 +++++++++++++++++++ pod/video/templates/videos/filter_aside.html | 38 +++++++++---- pod/video/templatetags/video_tags.py | 19 +++---- pod/video/utils.py | 9 ++++ pod/video/views.py | 5 -- 11 files changed, 174 insertions(+), 47 deletions(-) create mode 100644 pod/video/management/commands/reindex_videos.py diff --git a/pod/main/configuration.json b/pod/main/configuration.json index 20b03f0fd5..dc8540008d 100644 --- a/pod/main/configuration.json +++ b/pod/main/configuration.json @@ -5541,7 +5541,7 @@ }, "settings": { "CAS": { - "default_value": "1.5.2", + "default_value": "1.5.3", "description": { "en": "", "fr": [ @@ -5553,7 +5553,7 @@ "pod_version_init": "3.1" }, "ModelTranslation": { - "default_value": "0.18.7", + "default_value": "0.19.11", "description": { "en": "", "fr": [ @@ -5566,7 +5566,7 @@ "pod_version_init": "3.1" }, "captcha": { - "default_value": "0.5.17", + "default_value": "0.6.0", "description": { "en": "", "fr": [ @@ -5619,7 +5619,7 @@ "pod_version_init": "3.1" }, "honeypot": { - "default_value": "1.0.3", + "default_value": "1.2.1", "description": { "en": "", "fr": [ @@ -5632,7 +5632,7 @@ "pod_version_init": "3.1" }, "mozilla_django_oidc": { - "default_value": "3.0.0", + "default_value": "4.0.1", "description": { "en": "", "fr": [ @@ -5644,7 +5644,7 @@ "pod_version_init": "3.1" }, "pwa": { - "default_value": "1.1.0", + "default_value": "2.0.1", "description": { "en": "", "fr": [ @@ -5676,11 +5676,11 @@ "pod_version_init": "3.4" }, "rest_framework": { - "default_value": "3.14.0", + "default_value": "3.15.2", "description": { "en": "", "fr": [ - "version 3.14.0 : mise en place de l’API rest pour l’application", + "mise en place de l’API rest pour l’application", "[django-rest-framework.org](https://www.django-rest-framework.org/)" ] }, @@ -5700,7 +5700,7 @@ "pod_version_init": "3.1" }, "sorl.thumbnail": { - "default_value": "12.9.0", + "default_value": "12.11.0", "description": { "en": "", "fr": [ @@ -5720,13 +5720,25 @@ "[django-tagging.readthedocs.io](https://django-tagging.readthedocs.io/en/develop/#settings)" ] }, - "pod_version_end": "", + "pod_version_end": "4.0.0", "pod_version_init": "3.1" + }, + "tagulous": { + "default_value": "2.1.0", + "description": { + "en": "", + "fr": [ + "Gestion des mots-clés associés à un objet Django", + "[django-tagulous.readthedocs.io](https://django-tagulous.readthedocs.io)" + ] + }, + "pod_version_end": "", + "pod_version_init": "4.0.0" } }, "title": { - "en": "", - "fr": "Information générale" + "en": "General information", + "fr": "Informations générales" } } } diff --git a/pod/main/static/css/pod.css b/pod/main/static/css/pod.css index c125a28371..374d99ba1b 100755 --- a/pod/main/static/css/pod.css +++ b/pod/main/static/css/pod.css @@ -65,6 +65,11 @@ --pod-alert: #fc8670; --pod-alert-dark: #b11030; + /* Tag cloud colors */ + --pod-tag-color1: 16, 131, 22; + --pod-tag-color2: 51, 51, 170; + --pod-tag-color3: 197, 53, 143; + /**** font family ****/ /* For better accessibility, avoid fonts where [1, i, L] or [O, 0] are the same. */ @@ -162,6 +167,30 @@ tr, margin-right: 0.5em; } +/** TAGs Cloud */ +.tag-cloud{ + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-evenly; +} + +.tag-cloud>a { + line-height: 1.5; +} + +.tag-cloud>*:nth-child(2n+1) { --bs-link-color-rgb: var(--pod-tag-color1); } +.tag-cloud>*:nth-child(3n+1) { --bs-link-color-rgb: var(--pod-tag-color2); } +.tag-cloud>*:nth-child(4n+1) { --bs-link-color-rgb: var(--pod-tag-color3); } + +.tag-1{font-size: 1em;} +.tag-2{font-size: 1.1em;} +.tag-3{font-size: 1.2em;} +.tag-4{font-size: 1.3em;} +.tag-5{font-size: 1.4em;} +.tag-6{font-size: 1.5em;} + + .pod-card--video a:not(.btn) { color: inherit; } diff --git a/pod/main/templates/aside.html b/pod/main/templates/aside.html index b455ec2364..0831ef99ad 100644 --- a/pod/main/templates/aside.html +++ b/pod/main/templates/aside.html @@ -1,5 +1,4 @@ -{% load i18n %} -{% load video_tags %} +{% load i18n video_tags %} {% spaceless %} {% if HIDE_SHARE == False %} @@ -24,7 +23,7 @@

@@ -45,7 +44,7 @@

@@ -63,14 +62,14 @@

{% endif %} {% if HIDE_TAGS == False %} - {% if tag_cloud|length > 0 %} + {% if TAGS|length > 0 %}

 {% trans 'Tags' %}

-

- {% for tag in tag_cloud %} - +

+ {% for tag in TAGS %} + {{ tag.name }} {% endfor %} diff --git a/pod/urls.py b/pod/urls.py index 3de81b5bdc..75abf501bb 100644 --- a/pod/urls.py +++ b/pod/urls.py @@ -45,8 +45,8 @@ urlpatterns = [ path("select2/", include("django_select2.urls")), - re_path("robots.txt", robots_txt), - re_path("info_pod.json", info_pod), + path("robots.txt", robots_txt), + path("info_pod.json", info_pod), re_path(r"^admin/", admin.site.urls), # Translation path("i18n/", include("django.conf.urls.i18n")), diff --git a/pod/video/context_processors.py b/pod/video/context_processors.py index 86b045162c..30bfd69854 100644 --- a/pod/video/context_processors.py +++ b/pod/video/context_processors.py @@ -6,6 +6,8 @@ from pod.video.models import Discipline from pod.video.models import Video +from pod.video.utils import get_tag_cloud + from django.db.models import Count, Sum from django.db.models import Q from django.db.models import Exists @@ -97,6 +99,11 @@ def context_video_data(request): ) cache.set("TYPES", types, timeout=CACHE_VIDEO_DEFAULT_TIMEOUT) + tags = cache.get("TAGS") + if tags is None: + tags = get_tag_cloud() + cache.set("TAGS", tags, timeout=CACHE_VIDEO_DEFAULT_TIMEOUT) + disciplines = cache.get("DISCIPLINES") if disciplines is None: disciplines = ( @@ -132,4 +139,5 @@ def context_video_data(request): "VIDEOS_COUNT": VIDEOS_COUNT, "VIDEOS_DURATION": VIDEOS_DURATION, "CHANNELS_PER_BATCH": CHANNELS_PER_BATCH, + "TAGS": tags } diff --git a/pod/video/management/commands/cache_video_data.py b/pod/video/management/commands/cache_video_data.py index 22c5517200..760d4f0633 100644 --- a/pod/video/management/commands/cache_video_data.py +++ b/pod/video/management/commands/cache_video_data.py @@ -15,9 +15,9 @@ class Command(BaseCommand): + "types, discipline, video count and videos duration" ) - def handle(self, *args, **options): + def handle(self, *args, **options) -> None: """Store video data in cache.""" - cache.delete_many(["DISCIPLINES", "VIDEOS_COUNT", "VIDEOS_DURATION", "TYPES"]) + cache.delete_many(["DISCIPLINES", "VIDEOS_COUNT", "VIDEOS_DURATION", "TYPES", "TAGS"]) video_data = context_video_data(request=None) msg = "Successfully store video data in cache" for data in video_data: diff --git a/pod/video/management/commands/reindex_videos.py b/pod/video/management/commands/reindex_videos.py new file mode 100644 index 0000000000..c09665fdc6 --- /dev/null +++ b/pod/video/management/commands/reindex_videos.py @@ -0,0 +1,54 @@ +"""Script reindexing all videos (useful in case of loss of the ElasticSearch database)""" + +from django.core.management.base import BaseCommand +from pod.video.models import Video +from pod.video_search.models import index_video + + +def reindex_all_videos(dry_run: bool) -> int: + """Reindex all videos.""" + print("\nReindexing all videos...") + videos = Video.objects.all() + nb_videos = 0 + for vid in videos: + print(".", end="") + if not dry_run: + index_video(vid) + nb_videos += 1 + print("") + return nb_videos + + +class Command(BaseCommand): + """Reindex all videos.""" + + help = "Reindex all videos (useful in case of loss of the ElasticSearch database)" + + def add_arguments(self, parser) -> None: + """Allow arguments to be used with the command.""" + parser.add_argument( + "--dry", + help="Simulate what would be reindexed.", + action="store_true", + default=False, + ) + + def handle(self, *args, **options) -> None: + """Handle the clean_video_files command call.""" + if options["dry"]: + print("Simulation mode ('dry'). Nothing will be deleted.") + self.nb_reindexed = reindex_all_videos(options["dry"]) + + self.print_resume(options["dry"]) + + def print_resume(self, dry_run: bool) -> None: + """Print summary of reindexed objects.""" + + if dry_run: + print( + "[DRY RUN] %i video(s) would have been reindexed." + % (self.nb_reindexed) + ) + else: + print("%i video(s) reindexed." % self.nb_reindexed) + print("Have a nice day ;)") diff --git a/pod/video/templates/videos/filter_aside.html b/pod/video/templates/videos/filter_aside.html index a29306a2e1..2da17edd67 100644 --- a/pod/video/templates/videos/filter_aside.html +++ b/pod/video/templates/videos/filter_aside.html @@ -1,6 +1,4 @@ -{% load i18n %} -{% load video_tags %} -{% load thumbnail %} +{% load i18n video_tags thumbnail %} {% spaceless %}

@@ -92,12 +90,34 @@

{% endif %} {% if HIDE_TAGS == False %} -
-  {% trans 'Tags' %} -
- TODO: DISPLAY TAG CLOUD HERE. -
-
+ {% if TAGS|length > 0 %} +
+ +  {% trans 'Tags' %} + +
+
+ {% for tag in TAGS %} +
+ + +
+ {% endfor %} +
+ {% if TAGS|length > 5 %} + + + + {% endif %} +
+
+ {% endif %} {% endif %} {% if HIDE_CURSUS == False %}
diff --git a/pod/video/templatetags/video_tags.py b/pod/video/templatetags/video_tags.py index b8f55c7080..e93b724cd7 100644 --- a/pod/video/templatetags/video_tags.py +++ b/pod/video/templatetags/video_tags.py @@ -153,8 +153,11 @@ def get_video_infos(video): """ +No more used functions. +To Delete in 4.0.1 + class getTagsForModelNode(TagsForModelNode): - def __init__(self, model, context_var, counts): + def __init__(self, model, context_var, counts) -> None: super(getTagsForModelNode, self).__init__(model, context_var, counts) def render(self, context): @@ -219,11 +222,10 @@ def do_tags_for_model(parser, token): return getTagsForModelNode(bits[1], bits[3], counts=False) else: return getTagsForModelNode(bits[1], bits[3], counts=True) -""" -# def do_tag_cloud_for_model(parser, token): -""" +def do_tag_cloud_for_model(parser, token) -> None: + ### Retrieve a list of `Tag` objects with tag cloud attributes set. Retriev tags for a given model, @@ -260,8 +262,7 @@ def do_tags_for_model(parser, token): {% tag_cloud_for_model products.Widget as widget_tags with steps=9 min_count=3 distribution=log %} -""" -""" + ### bits = token.contents.split() len_bits = len(bits) if len_bits != 4 and len_bits not in range(6, 9): @@ -341,7 +342,7 @@ def update_kwargs_from_bits(kwargs, name, value, bits): } ) return kwargs -""" -# register.tag("tags_for_model", do_tags_for_model) -# register.tag("tag_cloud_for_model", do_tag_cloud_for_model) +register.tag("tags_for_model", do_tags_for_model) +register.tag("tag_cloud_for_model", do_tag_cloud_for_model) +""" diff --git a/pod/video/utils.py b/pod/video/utils.py index 770a69b768..f21450671d 100644 --- a/pod/video/utils.py +++ b/pod/video/utils.py @@ -200,6 +200,15 @@ def get_videos( return JsonResponse(response, safe=False) +def get_tag_cloud() -> list: + """Get only tags with weight between TAGULOUS_WEIGHT_MIN and TAGULOUS_WEIGHT_MAX.""" + # Convert tag cloud to list of dict, so it can be stored in CACHE + tags = [] + for tag in Video.tags.tag_model.objects.weight(): + tags.append({"name": tag.name, "weight": tag.weight, "slug": tag.slug}) + return tags + + def sort_videos_list(videos_list: list, sort_field: str, sort_direction: str = ""): """Return videos list sorted by sort_field. diff --git a/pod/video/views.py b/pod/video/views.py index 76bfd05dd6..a07385dffa 100644 --- a/pod/video/views.py +++ b/pod/video/views.py @@ -1108,11 +1108,6 @@ def video(request, slug, slug_c=None, slug_t=None, slug_private=None): return render_video(request, id, slug_c, slug_t, slug_private, template_video, params) -def tag_cloud(request): - """Get only tags with weight between TAGULOUS_WEIGHT_MIN and TAGULOUS_WEIGHT_MAX.""" - return Video.tags.tag_model.objects.weight() - - def toggle_render_video_user_can_see_video( show_page, is_password_protected, request, slug_private, video ) -> bool: