From 9b851a3e7bc77a0d632df2805943a7ae98e702f7 Mon Sep 17 00:00:00 2001 From: vincent porte Date: Thu, 19 Sep 2024 16:53:38 +0200 Subject: [PATCH] wip --- .../commands/migrate_documentation.py | 167 ++++++++++++++++++ lacommunaute/documentation/models.py | 13 ++ .../documentation/tests/tests_models.py | 4 + lacommunaute/documentation/urls.py | 7 +- lacommunaute/documentation/views.py | 6 + lacommunaute/forum/admin.py | 7 +- lacommunaute/forum/forms.py | 38 +--- lacommunaute/forum/models.py | 15 +- lacommunaute/forum/views.py | 115 +----------- .../migrations/0009_topic_document.py | 26 +++ lacommunaute/forum_conversation/models.py | 2 + lacommunaute/pages/sitemaps.py | 3 + ...y_and_document_in_materialized_view.py.wip | 0 lacommunaute/search/models.py | 4 + .../static/stylesheets/itou_communaute.scss | 6 + lacommunaute/stats/admin.py | 24 ++- .../migrations/0003_documentationstat.py | 44 +++++ lacommunaute/stats/models.py | 25 +++ .../documentation/document_detail.html | 48 +++++ .../documentation/partials/certified.html | 7 + .../partials/content_summary.html | 1 + .../documentation/partials/form_document.html | 26 +++ .../documentation/partials/partner.html | 8 + .../templates/forum/category_forum_list.html | 67 ------- .../forum/forum_documentation_category.html | 17 -- .../templates/partials/breadcrumb.html | 37 ++-- .../partials/social_share_buttons.html | 2 +- 27 files changed, 467 insertions(+), 252 deletions(-) create mode 100644 lacommunaute/documentation/management/commands/migrate_documentation.py create mode 100644 lacommunaute/forum_conversation/migrations/0009_topic_document.py create mode 100644 lacommunaute/search/0003_add_category_and_document_in_materialized_view.py.wip create mode 100644 lacommunaute/stats/migrations/0003_documentationstat.py create mode 100644 lacommunaute/templates/documentation/document_detail.html create mode 100644 lacommunaute/templates/documentation/partials/certified.html create mode 100644 lacommunaute/templates/documentation/partials/form_document.html create mode 100644 lacommunaute/templates/documentation/partials/partner.html delete mode 100644 lacommunaute/templates/forum/category_forum_list.html delete mode 100644 lacommunaute/templates/forum/forum_documentation_category.html diff --git a/lacommunaute/documentation/management/commands/migrate_documentation.py b/lacommunaute/documentation/management/commands/migrate_documentation.py new file mode 100644 index 000000000..c99bef709 --- /dev/null +++ b/lacommunaute/documentation/management/commands/migrate_documentation.py @@ -0,0 +1,167 @@ +import sys + +from django.contrib.contenttypes.models import ContentType +from django.contrib.redirects.models import Redirect +from django.core.management.base import BaseCommand +from taggit.models import TaggedItem + +from lacommunaute.documentation.models import Category, Document, DocumentRating +from lacommunaute.forum.models import Forum, ForumRating +from lacommunaute.forum_conversation.models import Topic +from lacommunaute.forum_upvote.models import UpVote +from lacommunaute.stats.models import DocumentationStat, ForumStat + + +def create_categories_from_catforums(): + transpo_dict = {} + redirections = [] + + for forum in Forum.objects.filter(type=1, level=0): + category = Category.objects.create( + name=forum.name, + short_description=forum.short_description, + description=forum.description, + image=forum.image, + ) + redirections.append( + Redirect(site_id=1, old_path=forum.get_absolute_url(), new_path=category.get_absolute_url()) + ) + print(f"{category} created") + transpo_dict[forum] = category + + Redirect.objects.bulk_create(redirections) + + return transpo_dict + + +def create_document_from_forums(category_transpo_dict): + forum_content_type = ContentType.objects.get_for_model(Forum) + document_content_type = ContentType.objects.get_for_model(Document) + transpo_dict = {} + redirections = [] + + for forum in Forum.objects.filter(parent__type=1): + document = Document.objects.create( + name=forum.name, + short_description=forum.short_description, + description=forum.description, + image=forum.image, + category=category_transpo_dict[forum.parent], + partner=forum.partner, + certified=forum.certified, + ) + UpVote.objects.filter(content_type=forum_content_type, object_id=forum.id).update( + content_type=document_content_type, object_id=document.id + ) + TaggedItem.objects.filter(content_type=forum_content_type, object_id=forum.id).update( + content_type=document_content_type, object_id=document.id + ) + redirections.append( + Redirect(site_id=1, old_path=forum.get_absolute_url(), new_path=document.get_absolute_url()) + ) + transpo_dict[forum] = document + + Redirect.objects.bulk_create(redirections) + + return transpo_dict + + +def migrate_ratings(document_transpo_dict): + document_ratings = [ + DocumentRating( + document=document_transpo_dict[rating.forum], + session_id=rating.session_id, + rating=rating.rating, + user=rating.user, + created_at=rating.created, + updated_at=rating.updated, + ) + for rating in ForumRating.objects.all() + ] + DocumentRating.objects.bulk_create(document_ratings) + ForumRating.objects.all().delete() + + +def migrate_topics(document_transpo_dict): + main_forum = Forum.objects.get_main_forum() + + for forum, document in document_transpo_dict.items(): + topics = Topic.objects.filter(forum=forum) + sys.stdout.write(f"*** {len(topics)} topics to migrate from {forum} ({forum.id}) to {main_forum}\n") + + for topic in topics: + topic.document = document + topic.forum = main_forum + topic.save() + forum.save() + + +def migrate_stats(category_transpo_dict, document_transpo_dict): + category_content_type = ContentType.objects.get_for_model(Category) + document_content_type = ContentType.objects.get_for_model(Document) + documentation_stats = [] + + for forum, category in category_transpo_dict.items(): + forum_stats = ForumStat.objects.filter(forum=forum) + documentation_stats += [ + DocumentationStat( + content_type=category_content_type, + object_id=category.id, + date=stat.date, + period=stat.period, + visits=stat.visits, + entry_visits=stat.entry_visits, + time_spent=stat.time_spent, + ) + for stat in forum_stats + ] + + for forum, document in document_transpo_dict.items(): + forum_stats = ForumStat.objects.filter(forum=forum) + documentation_stats += [ + DocumentationStat( + content_type=document_content_type, + object_id=document.id, + date=stat.date, + period=stat.period, + visits=stat.visits, + entry_visits=stat.entry_visits, + time_spent=stat.time_spent, + ) + for stat in forum_stats + ] + + DocumentationStat.objects.bulk_create(documentation_stats) + + +def del_forums(category_transpo_dict, document_transpo_dict): + forums_to_delete = list(category_transpo_dict.keys()) + list(document_transpo_dict.keys()) + return Forum.objects.filter(pk__in=[forum.pk for forum in forums_to_delete]).delete() + + +class Command(BaseCommand): + help = "migration des forums de fiches pratiques vers la documentation" + + def handle(self, *args, **options): + sys.stdout.write("let's go!\n") + + category_transpo_dict = create_categories_from_catforums() + sys.stdout.write("Categories created\n") + + document_transpo_dict = create_document_from_forums(category_transpo_dict) + sys.stdout.write("Documents created\n") + + migrate_ratings(document_transpo_dict) + sys.stdout.write("Ratings migrated\n") + + migrate_topics(document_transpo_dict) + sys.stdout.write("Topics migrated\n") + + migrate_stats(category_transpo_dict, document_transpo_dict) + sys.stdout.write("Stats migrated\n") + + deleted_forums = del_forums(category_transpo_dict, document_transpo_dict) + sys.stdout.write(f"{deleted_forums} forums deleted\n") + + sys.stdout.write("that's all folks!") + sys.stdout.flush() diff --git a/lacommunaute/documentation/models.py b/lacommunaute/documentation/models.py index d8c56981d..ce3804d34 100644 --- a/lacommunaute/documentation/models.py +++ b/lacommunaute/documentation/models.py @@ -43,6 +43,19 @@ class Meta: def __str__(self): return f"{self.name}" + def get_absolute_url(self, with_fqdn=False): + absolute_url = reverse( + "documentation:document_detail", + kwargs={ + "category_pk": self.category.pk, + "slug": self.slug, + "pk": self.pk, + }, + ) + if with_fqdn: + return f"{settings.COMMU_PROTOCOL}://{settings.COMMU_FQDN}{absolute_url}" + return absolute_url + # use AbstractDatedModel after ForumRanting migration class DocumentRating(models.Model): diff --git a/lacommunaute/documentation/tests/tests_models.py b/lacommunaute/documentation/tests/tests_models.py index 0d912435a..6cb9156aa 100644 --- a/lacommunaute/documentation/tests/tests_models.py +++ b/lacommunaute/documentation/tests/tests_models.py @@ -31,3 +31,7 @@ def test_slug_is_unique(self, db): DocumentFactory(for_snapshot=True) with pytest.raises(IntegrityError): DocumentFactory(for_snapshot=True) + + def test_get_absolute_url(self, db): + document = DocumentFactory() + assert document.get_absolute_url() == f"/documentation/{document.category.pk}/{document.slug}-{document.pk}/" diff --git a/lacommunaute/documentation/urls.py b/lacommunaute/documentation/urls.py index cabe96293..ca7bc4adb 100644 --- a/lacommunaute/documentation/urls.py +++ b/lacommunaute/documentation/urls.py @@ -5,7 +5,7 @@ CategoryDetailView, CategoryListView, CategoryUpdateView, - CategoryCreateView, + DocumentDetailView, ) @@ -17,4 +17,9 @@ path("-/", CategoryDetailView.as_view(), name="category_detail"), path("create/", CategoryCreateView.as_view(), name="category_create"), path("-/update/", CategoryUpdateView.as_view(), name="category_update"), + path( + "/-/", + DocumentDetailView.as_view(), + name="document_detail", + ), ] diff --git a/lacommunaute/documentation/views.py b/lacommunaute/documentation/views.py index e30664cd5..1f909a0d2 100644 --- a/lacommunaute/documentation/views.py +++ b/lacommunaute/documentation/views.py @@ -65,3 +65,9 @@ def get_context_data(self, **kwargs): context["title"] = f"Mettre à jour la catégorie {self.object.name}" context["back_url"] = self.object.get_absolute_url() return context + + +class DocumentDetailView(DetailView): + model = Document + template_name = "documentation/document_detail.html" + context_object_name = "document" diff --git a/lacommunaute/forum/admin.py b/lacommunaute/forum/admin.py index 21b624c63..69a018271 100644 --- a/lacommunaute/forum/admin.py +++ b/lacommunaute/forum/admin.py @@ -6,7 +6,12 @@ class ForumAdmin(BaseForumAdmin): fieldsets = BaseForumAdmin.fieldsets - fieldsets[0][1]["fields"] += ("short_description", "certified", "tags", "partner") + fieldsets[0][1]["fields"] += ( + "short_description", + "certified", + "tags", + "partner", + ) @admin.register(ForumRating) diff --git a/lacommunaute/forum/forms.py b/lacommunaute/forum/forms.py index 7dedba42a..b9197544a 100644 --- a/lacommunaute/forum/forms.py +++ b/lacommunaute/forum/forms.py @@ -1,10 +1,7 @@ from django import forms from django.conf import settings -from django.forms import CharField, CheckboxSelectMultiple, ModelMultipleChoiceField -from taggit.models import Tag from lacommunaute.forum.models import Forum -from lacommunaute.partner.models import Partner from lacommunaute.utils.iframe import wrap_iframe_in_div_tag @@ -24,39 +21,16 @@ class ForumForm(forms.ModelForm): label="Banniere de couverture, format 1200 x 630 pixels recommandé", widget=forms.FileInput(attrs={"accept": settings.SUPPORTED_IMAGE_FILE_TYPES.keys()}), ) - certified = forms.BooleanField(required=False, label="Certifiée par la communauté de l'inclusion") - partner = forms.ModelChoiceField( - label="Sélectionner un partenaire", - queryset=Partner.objects.all(), - required=False, - ) - tags = ModelMultipleChoiceField( - label="Sélectionner un ou plusieurs tags", - queryset=Tag.objects.all(), - widget=CheckboxSelectMultiple, - required=False, - ) - new_tags = CharField(required=False, label="Ajouter un tag ou plusieurs tags (séparés par des virgules)") - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if self.instance.pk: - self.fields["tags"].initial = self.instance.tags.all() def save(self, commit=True): forum = super().save(commit=False) forum.description = wrap_iframe_in_div_tag(self.cleaned_data.get("description")) - if commit: - forum.save() - forum.tags.set(self.cleaned_data["tags"]) - ( - forum.tags.add(*[tag.strip() for tag in self.cleaned_data["new_tags"].split(",")]) - if self.cleaned_data.get("new_tags") - else None - ) - return forum - class Meta: model = Forum - fields = ["name", "short_description", "description", "image", "certified", "partner"] + fields = [ + "name", + "short_description", + "description", + "image", + ] diff --git a/lacommunaute/forum/models.py b/lacommunaute/forum/models.py index 0f73b9baa..73eeaf025 100644 --- a/lacommunaute/forum/models.py +++ b/lacommunaute/forum/models.py @@ -20,18 +20,22 @@ def get_main_forum(self): class Forum(AbstractForum): + # to be removed after documentation refactor short_description = models.CharField( max_length=400, blank=True, null=True, verbose_name="Description courte (SEO)" ) + # to be removed after documentation refactor image = models.ImageField( storage=S3Boto3Storage(bucket_name=settings.AWS_STORAGE_BUCKET_NAME, file_overwrite=False), validators=[validate_image_size], ) + # to be removed after documentation refactor certified = models.BooleanField(default=False, verbose_name="Certifié par la communauté de l'inclusion") - + # to be removed after documentation refactor upvotes = GenericRelation(UpVote, related_query_name="forum") - + # to be removed after documentation refactor tags = TaggableManager() + # to be removed after documentation refactor partner = models.ForeignKey(Partner, on_delete=models.CASCADE, null=True, blank=True) objects = ForumQuerySet().as_manager() @@ -55,12 +59,6 @@ def count_unanswered_topics(self): def upvotes_count(self): return self.upvotes.count() - @cached_property - def is_in_documentation_area(self): - return (self.type == Forum.FORUM_CAT and self.get_level() == 0) or ( - self.get_level() > 0 and self.get_ancestors().first().type == Forum.FORUM_CAT - ) - @cached_property def is_toplevel_discussion_area(self): return self == Forum.objects.get_main_forum() @@ -72,6 +70,7 @@ def get_average_rating(self): return ForumRating.objects.filter(forum=self).aggregate(models.Avg("rating"))["rating__avg"] +# to be removed after documentation refactor class ForumRating(DatedModel): session_id = models.CharField(max_length=40) forum = models.ForeignKey(Forum, on_delete=models.CASCADE) diff --git a/lacommunaute/forum/views.py b/lacommunaute/forum/views.py index f16088f96..37c76b872 100644 --- a/lacommunaute/forum/views.py +++ b/lacommunaute/forum/views.py @@ -3,21 +3,18 @@ from django.conf import settings from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.contenttypes.models import ContentType -from django.db.models.query import QuerySet from django.shortcuts import get_object_or_404, render -from django.urls import reverse, reverse_lazy +from django.urls import reverse from django.views import View -from django.views.generic import CreateView, ListView, UpdateView +from django.views.generic import UpdateView from machina.apps.forum.views import ForumView as BaseForumView from machina.core.loading import get_class -from taggit.models import Tag from lacommunaute.forum.forms import ForumForm from lacommunaute.forum.models import Forum, ForumRating from lacommunaute.forum_conversation.forms import PostForm from lacommunaute.forum_conversation.view_mixins import FilteredTopicsListViewMixin from lacommunaute.forum_upvote.models import UpVote -from lacommunaute.utils.perms import add_public_perms_on_forum, forum_visibility_content_tree_from_forums logger = logging.getLogger(__name__) @@ -25,49 +22,14 @@ PermissionRequiredMixin = get_class("forum_permission.viewmixins", "PermissionRequiredMixin") -class SubCategoryForumListMixin: - def get_descendants(self): - qs = self.get_forum().get_descendants() - - forum_tag = self.request.GET.get("forum_tag") or None - if forum_tag: - qs = qs.filter(tags__slug=forum_tag) - - return qs.prefetch_related("tags") - - def get_tags_of_descendants(self): - return Tag.objects.filter( - taggit_taggeditem_items__content_type=ContentType.objects.get_for_model(Forum), - taggit_taggeditem_items__object_id__in=self.get_forum().get_descendants().values_list("id", flat=True), - ).distinct() - - def forum_tag_context(self): - return { - # TODO : remove permission management, though all forums are public in our case - "sub_forums": forum_visibility_content_tree_from_forums(self.request, self.get_descendants()), - "tags_of_descendants": self.get_tags_of_descendants(), - "active_forum_tag_slug": self.request.GET.get("forum_tag") or None, - } - - -class ForumView(BaseForumView, FilteredTopicsListViewMixin, SubCategoryForumListMixin): +class ForumView(BaseForumView, FilteredTopicsListViewMixin): paginate_by = settings.FORUM_TOPICS_NUMBER_PER_PAGE def get_template_names(self): if self.request.META.get("HTTP_HX_REQUEST"): return ["forum_conversation/topic_list.html"] - if self.will_render_documentation_variant(): - return ["forum/forum_documentation.html"] - if self.will_render_documentation_category_variant(): - return ["forum/forum_documentation_category.html"] return ["forum/forum_detail.html"] - def will_render_documentation_variant(self): - return self.get_forum().parent and self.forum.is_in_documentation_area - - def will_render_documentation_category_variant(self): - return self.get_forum().is_in_documentation_area and self.forum.level == 0 - def get_queryset(self): return self.filter_queryset(self.get_forum().topics.optimized_for_topics_list(self.request.user.id)) @@ -100,25 +62,11 @@ def get_context_data(self, **kwargs): ) context = context | self.get_topic_filter_context() - if self.will_render_documentation_category_variant(): - context = context | self.forum_tag_context() - - if self.will_render_documentation_variant(): - context["sibling_forums"] = forum.get_siblings(include_self=True) - if forum.image: context["og_image"] = forum.image return context -class SubCategoryForumListView(BaseForumView, SubCategoryForumListMixin): - template_name = "forum/partials/subcategory_forum_list.html" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) | self.forum_tag_context() - return context - - class ForumUpdateView(UserPassesTestMixin, UpdateView): template_name = "forum/forum_create_or_update.html" form_class = ForumForm @@ -134,63 +82,6 @@ def get_context_data(self, **kwargs): return context -class CategoryForumListView(ListView): - template_name = "forum/category_forum_list.html" - context_object_name = "forums" - - def get_queryset(self) -> QuerySet[Forum]: - return Forum.objects.filter(type=Forum.FORUM_CAT, level=0) - - -class BaseCategoryForumCreateView(UserPassesTestMixin, CreateView): - template_name = "forum/forum_create_or_update.html" - form_class = ForumForm - - def test_func(self): - return self.request.user.is_superuser - - def form_valid(self, form): - response = super().form_valid(form) - add_public_perms_on_forum(form.instance) - return response - - -class CategoryForumCreateView(BaseCategoryForumCreateView): - success_url = reverse_lazy("forum_extension:documentation") - - def form_valid(self, form): - form.instance.parent = None - form.instance.type = Forum.FORUM_CAT - return super().form_valid(form) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["title"] = "Créer une nouvelle catégorie documentaire" - context["back_url"] = reverse("forum_extension:documentation") - return context - - -class SubCategoryForumCreateView(BaseCategoryForumCreateView): - def get_success_url(self): - return reverse("forum_extension:forum", kwargs={"pk": self.object.pk, "slug": self.object.slug}) - - def get_parent_forum(self): - return Forum.objects.get(pk=self.kwargs["pk"]) - - def form_valid(self, form): - form.instance.type = Forum.FORUM_POST - form.instance.parent = self.get_parent_forum() - return super().form_valid(form) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["title"] = f"Créer une fiche pratique dans la catégorie {self.get_parent_forum().name}" - context["back_url"] = reverse( - "forum_extension:forum", kwargs={"pk": self.get_parent_forum().pk, "slug": self.get_parent_forum().slug} - ) - return context - - class ForumRatingView(View): def post(self, request, *args, **kwargs): forum_rating = ForumRating.objects.create( diff --git a/lacommunaute/forum_conversation/migrations/0009_topic_document.py b/lacommunaute/forum_conversation/migrations/0009_topic_document.py new file mode 100644 index 000000000..57258ab4e --- /dev/null +++ b/lacommunaute/forum_conversation/migrations/0009_topic_document.py @@ -0,0 +1,26 @@ +# Generated by Django 5.0.9 on 2024-09-19 08:37 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("documentation", "0001_initial"), + ("forum_conversation", "0008_remove_topic_likers"), + ] + + operations = [ + migrations.AddField( + model_name="topic", + name="document", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="topics", + to="documentation.document", + ), + ), + ] diff --git a/lacommunaute/forum_conversation/models.py b/lacommunaute/forum_conversation/models.py index c0325e804..0d92706dc 100644 --- a/lacommunaute/forum_conversation/models.py +++ b/lacommunaute/forum_conversation/models.py @@ -9,6 +9,7 @@ from machina.models.abstract_models import DatedModel from taggit.managers import TaggableManager +from lacommunaute.documentation.models import Document from lacommunaute.forum_conversation.signals import post_create from lacommunaute.forum_member.shortcuts import get_forum_member_display_name from lacommunaute.forum_upvote.models import UpVote @@ -52,6 +53,7 @@ def optimized_for_topics_list(self, user_id): class Topic(AbstractTopic): tags = TaggableManager() + document = models.ForeignKey(Document, on_delete=models.SET_NULL, null=True, blank=True, related_name="topics") def get_absolute_url(self, with_fqdn=False): absolute_url = reverse( diff --git a/lacommunaute/pages/sitemaps.py b/lacommunaute/pages/sitemaps.py index bf76bb54a..450edf512 100644 --- a/lacommunaute/pages/sitemaps.py +++ b/lacommunaute/pages/sitemaps.py @@ -8,6 +8,9 @@ from lacommunaute.partner.models import Partner +# ajouter CategorySitemap et DocumentSitemap ici + + class PagesSitemap(Sitemap): def items(self): return FlatPage.objects.filter(registration_required=False).order_by("title") diff --git a/lacommunaute/search/0003_add_category_and_document_in_materialized_view.py.wip b/lacommunaute/search/0003_add_category_and_document_in_materialized_view.py.wip new file mode 100644 index 000000000..e69de29bb diff --git a/lacommunaute/search/models.py b/lacommunaute/search/models.py index a4c4840e4..3f0795d42 100644 --- a/lacommunaute/search/models.py +++ b/lacommunaute/search/models.py @@ -41,3 +41,7 @@ class CommonIndex(models.Model): class Meta: managed = False + + +# ajouter Document dans la materilized view +# ajouter Categorie dans la materilized view diff --git a/lacommunaute/static/stylesheets/itou_communaute.scss b/lacommunaute/static/stylesheets/itou_communaute.scss index 63dc19715..914e3dfbf 100644 --- a/lacommunaute/static/stylesheets/itou_communaute.scss +++ b/lacommunaute/static/stylesheets/itou_communaute.scss @@ -220,3 +220,9 @@ span.highlighted { .s-home-title-01::after { left: 52%; } + +.vertical-line { + border-left: 1px solid #a1a1a1; + padding-left: 10px; + height: 100%; +} diff --git a/lacommunaute/stats/admin.py b/lacommunaute/stats/admin.py index c802d823a..425b95c9e 100644 --- a/lacommunaute/stats/admin.py +++ b/lacommunaute/stats/admin.py @@ -1,8 +1,9 @@ from dateutil.relativedelta import relativedelta from django.contrib import admin +from django.contrib.contenttypes.models import ContentType from lacommunaute.forum.models import Forum -from lacommunaute.stats.models import ForumStat, Stat +from lacommunaute.stats.models import DocumentationStat, ForumStat, Stat class ForumWithStatsFilter(admin.SimpleListFilter): @@ -19,6 +20,21 @@ def queryset(self, request, queryset): return queryset +class DocumentationContentTypeFilter(admin.SimpleListFilter): + title = "Documentation type" + parameter_name = "content_type" + + def lookups(self, request, model_admin): + content_types = ContentType.objects.filter(model__in=["category", "document"], app_label="documentation") + + return [(ct.id, ct.name) for ct in content_types] + + def queryset(self, request, queryset): + if self.value(): + return queryset.filter(content_type_id=self.value()) + return queryset + + class BaseStatAdmin(admin.ModelAdmin): list_display = ("explicit_period",) list_filter = ("date", "period") @@ -43,3 +59,9 @@ class ForumStatAdmin(BaseStatAdmin): list_display = BaseStatAdmin.list_display + ("forum", "visits", "entry_visits", "time_spent") list_filter = BaseStatAdmin.list_filter + (ForumWithStatsFilter,) raw_id_fields = ("forum",) + + +@admin.register(DocumentationStat) +class DocumentionStatAdmin(BaseStatAdmin): + list_display = BaseStatAdmin.list_display + ("content_type", "object_id", "visits", "entry_visits", "time_spent") + list_filter = BaseStatAdmin.list_filter + (DocumentationContentTypeFilter,) diff --git a/lacommunaute/stats/migrations/0003_documentationstat.py b/lacommunaute/stats/migrations/0003_documentationstat.py new file mode 100644 index 000000000..dc8e433bc --- /dev/null +++ b/lacommunaute/stats/migrations/0003_documentationstat.py @@ -0,0 +1,44 @@ +# Generated by Django 5.0.9 on 2024-09-19 08:37 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("contenttypes", "0002_remove_content_type_name"), + ("stats", "0002_forumstat"), + ] + + operations = [ + migrations.CreateModel( + name="DocumentationStat", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("object_id", models.PositiveBigIntegerField()), + ("date", models.DateField(verbose_name="Date")), + ( + "period", + models.CharField( + choices=[("month", "Month"), ("week", "Week"), ("day", "Day")], + max_length=10, + verbose_name="Période", + ), + ), + ("visits", models.IntegerField(default=0, verbose_name="Visites")), + ("entry_visits", models.IntegerField(default=0, verbose_name="Visites entrantes")), + ("time_spent", models.IntegerField(default=0, verbose_name="Temps passé")), + ( + "content_type", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="contenttypes.contenttype"), + ), + ], + options={ + "verbose_name": "Stat de la documentation", + "verbose_name_plural": "Stats de la documentation", + "ordering": ["date", "period", "content_type", "object_id"], + "unique_together": {("date", "period", "content_type", "object_id")}, + }, + ), + ] diff --git a/lacommunaute/stats/models.py b/lacommunaute/stats/models.py index 6d34d474c..07ab00be0 100644 --- a/lacommunaute/stats/models.py +++ b/lacommunaute/stats/models.py @@ -1,3 +1,5 @@ +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType from django.db import models from lacommunaute.forum.models import Forum @@ -38,6 +40,7 @@ def __str__(self): objects = StatQuerySet().as_manager() +# to be removed after documentation refactor class ForumStat(models.Model): """ Represents a statistical data point, relative to a forum, for a given date and period. @@ -60,3 +63,25 @@ class Meta: def __str__(self): return f"{self.date} - {self.period} - {self.forum}" + + +class DocumentationStat(models.Model): + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveBigIntegerField() + content_object = GenericForeignKey("content_type", "object_id") + date = models.DateField(verbose_name="Date") + period = models.CharField(max_length=10, verbose_name="Période", choices=Period.choices) + visits = models.IntegerField(verbose_name="Visites", default=0) + entry_visits = models.IntegerField(verbose_name="Visites entrantes", default=0) + time_spent = models.IntegerField(verbose_name="Temps passé", default=0) + + objects = models.Manager() + + class Meta: + verbose_name = "Stat de la documentation" + verbose_name_plural = "Stats de la documentation" + ordering = ["date", "period", "content_type", "object_id"] + unique_together = ("date", "period", "content_type", "object_id") + + def __str__(self): + return f"{self.date} - {self.period} " diff --git a/lacommunaute/templates/documentation/document_detail.html b/lacommunaute/templates/documentation/document_detail.html new file mode 100644 index 000000000..16ec10260 --- /dev/null +++ b/lacommunaute/templates/documentation/document_detail.html @@ -0,0 +1,48 @@ +{% extends "layouts/base.html" %} +{% block title %}{{ document.name }}{{ block.super }}{% endblock %} +{% block meta_description %} + {{ document.short_description }} +{% endblock meta_description %} +{% block breadcrumb %} + {% include "partials/breadcrumb.html" with document=document only %} +{% endblock %} +{% block content %} + {% load i18n %} + {% include 'documentation/partials/title_and_shortdesc.html' with obj=document user=user only %} + {% if document.description %} +
+
+
+
+
+
{% include 'documentation/partials/certified.html' with obj=document only %}
+
{% include "partials/upvotes.html" with obj=document %}
+ {% include 'documentation/partials/image_and_desc.html' with obj=document only %} + {% if document.partner %} +
+ {% include "documentation/partials/partner.html" with partner=document.partner only %} +
+ {% endif %} + {% comment %}{% include "forum/partials/rating.html" with forum=obj rating_area_id="1" %}{% endcomment %} +
+
+
+ Les autres fiches du thème {{ document.category.name }} + +
+
+
+
+ {% endif %} +{% endblock content %} diff --git a/lacommunaute/templates/documentation/partials/certified.html b/lacommunaute/templates/documentation/partials/certified.html new file mode 100644 index 000000000..87d52279d --- /dev/null +++ b/lacommunaute/templates/documentation/partials/certified.html @@ -0,0 +1,7 @@ +{% if obj.certified %} + + + Certifiée par la communauté de l'inclusion + +{% endif %} +Mis à jour le {{ obj.updated_at|date:"d/m/Y" }} diff --git a/lacommunaute/templates/documentation/partials/content_summary.html b/lacommunaute/templates/documentation/partials/content_summary.html index ced787574..9496f49fe 100644 --- a/lacommunaute/templates/documentation/partials/content_summary.html +++ b/lacommunaute/templates/documentation/partials/content_summary.html @@ -15,6 +15,7 @@