From d3d142174a8ba13a7969112172b2cb1ee4f118c3 Mon Sep 17 00:00:00 2001 From: vincent porte Date: Wed, 18 Sep 2024 15:41:04 +0200 Subject: [PATCH] wip --- Makefile | 10 ++ .../collect_weekly_matomo_forum_stats.sh | 4 +- config/urls.py | 2 + lacommunaute/documentation/abstract_models.py | 38 ++++ lacommunaute/documentation/admin.py | 40 +++++ lacommunaute/documentation/factories.py | 22 +++ lacommunaute/documentation/forms.py | 96 +++++++++++ .../commands/migrate_documentation.py | 163 ++++++++++++++++++ .../documentation/migrations/0001_initial.py | 48 ++++++ .../migrations/0002_alter_category_options.py | 16 ++ .../documentation/migrations/0003_document.py | 81 +++++++++ .../migrations/0004_documentrating.py | 40 +++++ ...lter_documentrating_created_at_and_more.py | 22 +++ lacommunaute/documentation/models.py | 69 ++++++++ .../documentation/tests/tests_models.py | 14 ++ lacommunaute/documentation/urls.py | 17 ++ lacommunaute/documentation/views.py | 60 +++++++ lacommunaute/forum/admin.py | 7 +- lacommunaute/forum/forms.py | 30 +--- lacommunaute/forum/models.py | 15 +- lacommunaute/forum/urls.py | 8 - lacommunaute/forum/views.py | 111 +----------- .../migrations/0009_topic_document.py | 25 +++ lacommunaute/forum_conversation/models.py | 2 + lacommunaute/search/models.py | 4 + .../static/stylesheets/itou_communaute.scss | 6 + lacommunaute/stats/admin.py | 27 ++- .../stats/migrations/0003_documentstat.py | 47 +++++ ...4_documentationstat_delete_documentstat.py | 47 +++++ lacommunaute/stats/models.py | 26 ++- lacommunaute/templates/404.html | 2 +- .../documentation/category_detail.html | 86 +++++++++ .../documentation/category_list.html | 43 +++++ .../documentation/document_detail.html | 48 ++++++ .../documentation/partials/certified.html | 7 + .../partials/content_summary.html | 37 ++++ .../partials/image_and_desc.html | 11 ++ .../documentation/partials/partner.html | 8 + .../partials/title_and_shortdesc.html | 11 ++ .../templates/forum/category_forum_list.html | 67 ------- .../forum/forum_documentation_category.html | 17 -- lacommunaute/templates/pages/home.html | 2 +- .../templates/partials/ask_a_question.html | 2 +- .../templates/partials/breadcrumb.html | 37 ++-- lacommunaute/templates/partials/footer.html | 2 +- lacommunaute/templates/partials/header.html | 2 +- .../partials/social_share_buttons.html | 2 +- scripts/import-latest-db-backup.sh | 10 +- 48 files changed, 1229 insertions(+), 262 deletions(-) create mode 100644 lacommunaute/documentation/abstract_models.py create mode 100644 lacommunaute/documentation/admin.py create mode 100644 lacommunaute/documentation/factories.py create mode 100644 lacommunaute/documentation/forms.py create mode 100644 lacommunaute/documentation/management/commands/migrate_documentation.py create mode 100644 lacommunaute/documentation/migrations/0001_initial.py create mode 100644 lacommunaute/documentation/migrations/0002_alter_category_options.py create mode 100644 lacommunaute/documentation/migrations/0003_document.py create mode 100644 lacommunaute/documentation/migrations/0004_documentrating.py create mode 100644 lacommunaute/documentation/migrations/0005_alter_documentrating_created_at_and_more.py create mode 100644 lacommunaute/documentation/models.py create mode 100644 lacommunaute/documentation/tests/tests_models.py create mode 100644 lacommunaute/documentation/urls.py create mode 100644 lacommunaute/documentation/views.py create mode 100644 lacommunaute/forum_conversation/migrations/0009_topic_document.py create mode 100644 lacommunaute/stats/migrations/0003_documentstat.py create mode 100644 lacommunaute/stats/migrations/0004_documentationstat_delete_documentstat.py create mode 100644 lacommunaute/templates/documentation/category_detail.html create mode 100644 lacommunaute/templates/documentation/category_list.html 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/content_summary.html create mode 100644 lacommunaute/templates/documentation/partials/image_and_desc.html create mode 100644 lacommunaute/templates/documentation/partials/partner.html create mode 100644 lacommunaute/templates/documentation/partials/title_and_shortdesc.html delete mode 100644 lacommunaute/templates/forum/category_forum_list.html delete mode 100644 lacommunaute/templates/forum/forum_documentation_category.html diff --git a/Makefile b/Makefile index 0569c7de2..aeced0c0c 100644 --- a/Makefile +++ b/Makefile @@ -26,3 +26,13 @@ index: INSTANCE_NUMBER=0 \ POSTGRESQL_ADDON_URI=$(POSTGRESQL_ADDON_URI) \ clevercloud/rebuild_index.sh + +# DB + +.PHONY: resetdb +resetdb: + dropdb --if-exists $(POSTGRESQL_ADDON_DB) + createdb $(POSTGRESQL_ADDON_DB) + python manage.py migrate + python manage.py configure_bucket + python manage.py populate diff --git a/clevercloud/collect_weekly_matomo_forum_stats.sh b/clevercloud/collect_weekly_matomo_forum_stats.sh index 494ddf3e4..1a5ff24d0 100755 --- a/clevercloud/collect_weekly_matomo_forum_stats.sh +++ b/clevercloud/collect_weekly_matomo_forum_stats.sh @@ -15,4 +15,6 @@ fi # $APP_HOME is set by default by clever cloud. cd $APP_HOME -python manage.py collect_matomo_forum_stats +# reactivate after documentation migration +# python manage.py collect_matomo_forum_stats +echo "Collecting weekly matomo stats for the forum is disabled for now." diff --git a/config/urls.py b/config/urls.py index 4dddbf67d..fe094e811 100644 --- a/config/urls.py +++ b/config/urls.py @@ -5,6 +5,7 @@ from django.urls import include, path, re_path from machina.core.loading import get_class +from lacommunaute.documentation import urls as documentation_urls from lacommunaute.event import urls as event_urls from lacommunaute.forum import urls as forum_extension_urls from lacommunaute.forum_conversation import urls as forum_conversation_extension_urls @@ -30,6 +31,7 @@ path("inclusion_connect/", include(inclusion_connect_urls)), # www. path("", include(pages_urls)), + path("documentation/", include(documentation_urls)), path("members/", include(forum_member_urls)), path("", include(forum_conversation_extension_urls)), path("", include(forum_extension_urls)), diff --git a/lacommunaute/documentation/abstract_models.py b/lacommunaute/documentation/abstract_models.py new file mode 100644 index 000000000..fb173f5ab --- /dev/null +++ b/lacommunaute/documentation/abstract_models.py @@ -0,0 +1,38 @@ +from django.conf import settings +from django.db import models +from django.utils.encoding import force_str +from django.utils.text import slugify +from django.utils.translation import gettext_lazy as _ +from machina.models.fields import MarkupTextField +from storages.backends.s3boto3 import S3Boto3Storage + +from lacommunaute.utils.validators import validate_image_size + + +class AbstractDatedModel(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True + + +class AbstractPublication(AbstractDatedModel): + name = models.CharField(max_length=100, verbose_name=_("Name")) + slug = models.SlugField(max_length=255, verbose_name=_("Slug"), unique=True) + + description = MarkupTextField(verbose_name=_("Description"), null=True, blank=True) + short_description = models.CharField( + max_length=400, blank=True, null=True, verbose_name="Description courte (SEO)" + ) + image = models.ImageField( + storage=S3Boto3Storage(bucket_name=settings.AWS_STORAGE_BUCKET_NAME, file_overwrite=False), + validators=[validate_image_size], + ) + + class Meta: + abstract = True + + def save(self, *args, **kwargs): + self.slug = slugify(force_str(self.name), allow_unicode=True) + super().save(*args, **kwargs) diff --git a/lacommunaute/documentation/admin.py b/lacommunaute/documentation/admin.py new file mode 100644 index 000000000..72b67f7ae --- /dev/null +++ b/lacommunaute/documentation/admin.py @@ -0,0 +1,40 @@ +from django.contrib import admin + +from lacommunaute.documentation.models import Category, Document, DocumentRating + + +class DocumentInlines(admin.TabularInline): + model = Document + extra = 0 + fields = ("name", "short_description") + readonly_fields = ("name", "short_description") + + def has_delete_permission(self, request, obj=None): + return False + + def has_add_permission(self, request, obj=None): + return False + + +@admin.register(Category) +class CategoryAdmin(admin.ModelAdmin): + list_display = ("name",) + search_fields = ("name",) + fields = ("name", "short_description", "description", "image") + inlines = [DocumentInlines] + + +@admin.register(Document) +class DocumentAdmin(admin.ModelAdmin): + list_display = ("name", "category") + list_filter = ("category",) + search_fields = ("name",) + fields = ("name", "short_description", "description", "image") + + +@admin.register(DocumentRating) +class DocumentRatingAdmin(admin.ModelAdmin): + list_display = ("document", "rating", "created_at") + list_filter = ("document",) + list_display_links = ("rating",) + raw_id_fields = ("document", "user") diff --git a/lacommunaute/documentation/factories.py b/lacommunaute/documentation/factories.py new file mode 100644 index 000000000..ee6e27890 --- /dev/null +++ b/lacommunaute/documentation/factories.py @@ -0,0 +1,22 @@ +import factory +from faker import Faker + +from lacommunaute.documentation.models import Category + + +faker = Faker() + + +class CategoryFactory(factory.django.DjangoModelFactory): + name = factory.Faker("name") + description = factory.Faker("sentence", nb_words=100) + short_description = factory.Faker("sentence", nb_words=10) + image = factory.django.ImageField(filename="banner.jpg") + + class Meta: + model = Category + + class Params: + for_snapshot = factory.Trait( + name="Test Category", description="Test description", short_description="Test description" + ) diff --git a/lacommunaute/documentation/forms.py b/lacommunaute/documentation/forms.py new file mode 100644 index 000000000..a1efeb709 --- /dev/null +++ b/lacommunaute/documentation/forms.py @@ -0,0 +1,96 @@ +import re + +from django import forms +from django.conf import settings +from django.forms import CharField, CheckboxSelectMultiple, ModelMultipleChoiceField +from taggit.models import Tag + +from lacommunaute.documentation.models import Category, Document +from lacommunaute.partner.models import Partner + + +def wrap_iframe_in_div_tag(text): + # iframe tags must be wrapped in a div tag to be displayed correctly + # add div tag if not present + + iframe_regex = r"((
)?(
)?)" + + for match, starts_with, ends_with in re.findall(iframe_regex, text, re.DOTALL): + if not starts_with and not ends_with: + text = text.replace(match, f"
{match}
") + + return text + + +class DocumentationFormMixin: + name = forms.CharField(required=True, label="Titre") + short_description = forms.CharField( + widget=forms.Textarea(attrs={"rows": 3}), + max_length=400, + required=True, + label="Sous-titre (400 caractères pour le SEO)", + ) + description = forms.CharField( + widget=forms.Textarea(attrs={"rows": 20}), required=False, label="Contenu (markdown autorisé)" + ) + image = forms.ImageField( + required=False, + label="Banniere de couverture, format 1200 x 630 pixels recommandé", + widget=forms.FileInput(attrs={"accept": settings.SUPPORTED_IMAGE_FILE_TYPES.keys()}), + ) + + def save(self, commit=True): + instance = super().save(commit=False) + instance.description = wrap_iframe_in_div_tag(self.cleaned_data.get("description")) + + if commit: + instance.save() + return instance + +class CategoryForm(forms.ModelForm, DocumentationFormMixin): + class Meta: + model = Category + fields = ["name", "short_description", "description", "image"] + + +class DocumentForm(forms.ModelForm, DocumentationFormMixin): + 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, + ) + category = forms.ModelChoiceField( + label="Sélectionner une catégorie documentaire", + queryset=Category.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): + instance = super().save(commit=False) + + if commit: + instance.save() + instance.tags.set(self.cleaned_data["tags"]) + ( + instance.tags.add(*[tag.strip() for tag in self.cleaned_data["new_tags"].split(",")]) + if self.cleaned_data.get("new_tags") + else None + ) + return instance + + class Meta: + model = Document + fields = ["name", "short_description", "description", "image", "certified", "partner", "category"] diff --git a/lacommunaute/documentation/management/commands/migrate_documentation.py b/lacommunaute/documentation/management/commands/migrate_documentation.py new file mode 100644 index 000000000..82f40ab40 --- /dev/null +++ b/lacommunaute/documentation/management/commands/migrate_documentation.py @@ -0,0 +1,163 @@ +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/migrations/0001_initial.py b/lacommunaute/documentation/migrations/0001_initial.py new file mode 100644 index 000000000..60e4987ea --- /dev/null +++ b/lacommunaute/documentation/migrations/0001_initial.py @@ -0,0 +1,48 @@ +# Generated by Django 5.0.9 on 2024-09-10 10:16 + +import machina.models.fields +import storages.backends.s3 +from django.db import migrations, models + +import lacommunaute.utils.validators + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Category", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("name", models.CharField(max_length=100, verbose_name="Name")), + ("slug", models.SlugField(max_length=255, unique=True, verbose_name="Slug")), + ( + "description", + machina.models.fields.MarkupTextField( + blank=True, no_rendered_field=True, null=True, verbose_name="Description" + ), + ), + ( + "short_description", + models.CharField(blank=True, max_length=400, null=True, verbose_name="Description courte (SEO)"), + ), + ( + "image", + models.ImageField( + storage=storages.backends.s3.S3Storage(bucket_name="private-bucket", file_overwrite=False), + upload_to="", + validators=[lacommunaute.utils.validators.validate_image_size], + ), + ), + ("_description_rendered", models.TextField(blank=True, editable=False, null=True)), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/lacommunaute/documentation/migrations/0002_alter_category_options.py b/lacommunaute/documentation/migrations/0002_alter_category_options.py new file mode 100644 index 000000000..5e9201073 --- /dev/null +++ b/lacommunaute/documentation/migrations/0002_alter_category_options.py @@ -0,0 +1,16 @@ +# Generated by Django 5.0.9 on 2024-09-10 13:14 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("documentation", "0001_initial"), + ] + + operations = [ + migrations.AlterModelOptions( + name="category", + options={"ordering": ["created_at"], "verbose_name": "Catégorie", "verbose_name_plural": "Catégories"}, + ), + ] diff --git a/lacommunaute/documentation/migrations/0003_document.py b/lacommunaute/documentation/migrations/0003_document.py new file mode 100644 index 000000000..fef88e06f --- /dev/null +++ b/lacommunaute/documentation/migrations/0003_document.py @@ -0,0 +1,81 @@ +# Generated by Django 5.0.9 on 2024-09-11 13:48 + +import django.db.models.deletion +import machina.models.fields +import storages.backends.s3 +import taggit.managers +from django.db import migrations, models + +import lacommunaute.utils.validators + + +class Migration(migrations.Migration): + dependencies = [ + ("documentation", "0002_alter_category_options"), + ("partner", "0002_alter_partner_options"), + ("taggit", "0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx"), + ] + + operations = [ + migrations.CreateModel( + name="Document", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("name", models.CharField(max_length=100, verbose_name="Name")), + ("slug", models.SlugField(max_length=255, unique=True, verbose_name="Slug")), + ( + "description", + machina.models.fields.MarkupTextField( + blank=True, no_rendered_field=True, null=True, verbose_name="Description" + ), + ), + ( + "short_description", + models.CharField(blank=True, max_length=400, null=True, verbose_name="Description courte (SEO)"), + ), + ( + "image", + models.ImageField( + storage=storages.backends.s3.S3Storage(bucket_name="private-bucket", file_overwrite=False), + upload_to="", + validators=[lacommunaute.utils.validators.validate_image_size], + ), + ), + ( + "certified", + models.BooleanField(default=False, verbose_name="Certifié par la communauté de l'inclusion"), + ), + ("_description_rendered", models.TextField(blank=True, editable=False, null=True)), + ( + "category", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="documents", + to="documentation.category", + ), + ), + ( + "partner", + models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="partner.partner" + ), + ), + ( + "tags", + taggit.managers.TaggableManager( + help_text="A comma-separated list of tags.", + through="taggit.TaggedItem", + to="taggit.Tag", + verbose_name="Tags", + ), + ), + ], + options={ + "verbose_name": "Document", + "verbose_name_plural": "Documents", + "ordering": ["-created_at"], + }, + ), + ] diff --git a/lacommunaute/documentation/migrations/0004_documentrating.py b/lacommunaute/documentation/migrations/0004_documentrating.py new file mode 100644 index 000000000..b77418193 --- /dev/null +++ b/lacommunaute/documentation/migrations/0004_documentrating.py @@ -0,0 +1,40 @@ +# Generated by Django 5.0.9 on 2024-09-12 14:14 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("documentation", "0003_document"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="DocumentRating", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("session_id", models.CharField(max_length=40)), + ("rating", models.PositiveSmallIntegerField()), + ( + "document", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="documentation.document"), + ), + ( + "user", + models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), + ), + ], + options={ + "verbose_name": "Notation d'un document", + "verbose_name_plural": "Notations des documents", + "ordering": ("-created_at",), + }, + ), + ] diff --git a/lacommunaute/documentation/migrations/0005_alter_documentrating_created_at_and_more.py b/lacommunaute/documentation/migrations/0005_alter_documentrating_created_at_and_more.py new file mode 100644 index 000000000..6e078fa43 --- /dev/null +++ b/lacommunaute/documentation/migrations/0005_alter_documentrating_created_at_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.9 on 2024-09-12 14:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("documentation", "0004_documentrating"), + ] + + operations = [ + migrations.AlterField( + model_name="documentrating", + name="created_at", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name="documentrating", + name="updated_at", + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/lacommunaute/documentation/models.py b/lacommunaute/documentation/models.py new file mode 100644 index 000000000..e2a7a9a02 --- /dev/null +++ b/lacommunaute/documentation/models.py @@ -0,0 +1,69 @@ +from django.conf import settings +from django.contrib.contenttypes.fields import GenericRelation +from django.db import models +from django.urls import reverse +from taggit.managers import TaggableManager + +from lacommunaute.documentation.abstract_models import AbstractPublication +from lacommunaute.forum_upvote.models import UpVote +from lacommunaute.partner.models import Partner + + +class Category(AbstractPublication): + class Meta: + verbose_name = "Catégorie" + verbose_name_plural = "Catégories" + ordering = ["created_at"] + + def __str__(self): + return f"{self.name}" + + def get_absolute_url(self, with_fqdn=False): + absolute_url = reverse("documentation:category_detail", kwargs={"slug": self.slug, "pk": self.pk}) + if with_fqdn: + return f"{settings.COMMU_PROTOCOL}://{settings.COMMU_FQDN}{absolute_url}" + return absolute_url + + +class Document(AbstractPublication): + category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="documents") + partner = models.ForeignKey(Partner, on_delete=models.CASCADE, null=True, blank=True) + upvotes = GenericRelation(UpVote, related_query_name="document") + certified = models.BooleanField(default=False, verbose_name="Certifié par la communauté de l'inclusion") + tags = TaggableManager() + + class Meta: + verbose_name = "Document" + verbose_name_plural = "Documents" + ordering = ["-created_at"] + + 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): + created_at = models.DateTimeField(null=True, blank=True) + updated_at = models.DateTimeField(null=True, blank=True) + session_id = models.CharField(max_length=40) + document = models.ForeignKey(Document, on_delete=models.CASCADE) + rating = models.PositiveSmallIntegerField() + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True) + + class Meta: + verbose_name = "Notation d'un document" + verbose_name_plural = "Notations des documents" + ordering = ("-created_at",) diff --git a/lacommunaute/documentation/tests/tests_models.py b/lacommunaute/documentation/tests/tests_models.py new file mode 100644 index 000000000..544aa0108 --- /dev/null +++ b/lacommunaute/documentation/tests/tests_models.py @@ -0,0 +1,14 @@ +import pytest # noqa +from django.db.utils import IntegrityError +from lacommunaute.documentation.factories import CategoryFactory + + +class TestCategory: + def test_slug(self, db): + category = CategoryFactory(for_snapshot=True) + assert category.slug == "test-category" + + def test_slug_is_unique(self, db): + CategoryFactory(for_snapshot=True) + with pytest.raises(IntegrityError): + CategoryFactory(for_snapshot=True) diff --git a/lacommunaute/documentation/urls.py b/lacommunaute/documentation/urls.py new file mode 100644 index 000000000..f00233ad1 --- /dev/null +++ b/lacommunaute/documentation/urls.py @@ -0,0 +1,17 @@ +from django.urls import path + +from lacommunaute.documentation.views import CategoryDetailView, CategoryListView, DocumentDetailView + + +app_name = "documentation" + + +urlpatterns = [ + path("", CategoryListView.as_view(), name="category_list"), + path("-/", CategoryDetailView.as_view(), name="category_detail"), + path( + "/-/", + DocumentDetailView.as_view(), + name="document_detail", + ), +] diff --git a/lacommunaute/documentation/views.py b/lacommunaute/documentation/views.py new file mode 100644 index 000000000..1af4db72c --- /dev/null +++ b/lacommunaute/documentation/views.py @@ -0,0 +1,60 @@ + +from django.contrib.auth.mixins import UserPassesTestMixin +from django.contrib.contenttypes.models import ContentType +from django.views.generic import DetailView, ListView, UpdateView +from taggit.models import Tag + +from lacommunaute.documentation.forms import CategoryForm +from lacommunaute.documentation.models import Category, Document + + +class CategoryListView(ListView): + model = Category + template_name = "documentation/category_list.html" + context_object_name = "categories" + paginate_by = 20 * 3 + + +class CategoryDetailView(DetailView): + model = Category + template_name = "documentation/category_detail.html" + context_object_name = "category" + + def get_tags_of_documents(self): + return Tag.objects.filter( + taggit_taggeditem_items__content_type=ContentType.objects.get_for_model(Document), + taggit_taggeditem_items__object_id__in=self.object.documents.all().values_list("id", flat=True), + ).distinct() + + def get_queryset(self): + return super().get_queryset().prefetch_related("documents__tags") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["tags"] = self.get_tags_of_documents() + context["active_tag_slug"] = self.request.GET.get("tag") or None + return context + + +class CategoryUpdateView(UserPassesTestMixin,UpdateView): + model = Category + template_name = "documentation/category_update.html" + form_class = CategoryForm + + def test_func(self): + return self.request.user.is_superuser + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["title"] = f"Mettre à jour la catégorie {self.object.name}" + #context["back_url"] = reverse("forum_extension:forum", kwargs={"pk": self.object.pk, "slug": self.object.slug}) + return context + + +class DocumentDetailView(DetailView): + model = Document + template_name = "documentation/document_detail.html" + context_object_name = "document" + + +# DocumentCreateUpdateView 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 c86796c65..9890af4e8 100644 --- a/lacommunaute/forum/forms.py +++ b/lacommunaute/forum/forms.py @@ -5,6 +5,7 @@ from django.forms import CharField, CheckboxSelectMultiple, ModelMultipleChoiceField from taggit.models import Tag +from lacommunaute.documentation.models import Category from lacommunaute.forum.models import Forum from lacommunaute.partner.models import Partner @@ -38,39 +39,12 @@ 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..b8d74718c 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,11 +59,7 @@ 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): @@ -72,6 +72,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/urls.py b/lacommunaute/forum/urls.py index 77cd7c35c..4b29df8f2 100644 --- a/lacommunaute/forum/urls.py +++ b/lacommunaute/forum/urls.py @@ -2,13 +2,9 @@ from machina.apps.forum.views import IndexView from lacommunaute.forum.views import ( - CategoryForumCreateView, - CategoryForumListView, ForumRatingView, ForumUpdateView, ForumView, - SubCategoryForumCreateView, - SubCategoryForumListView, ) @@ -19,9 +15,5 @@ path("forum/-/", ForumView.as_view(), name="forum"), path("forum/-/update/", ForumUpdateView.as_view(), name="edit_forum"), path("forum/-/rate/", ForumRatingView.as_view(), name="rate"), - path("forum/-/subs/", SubCategoryForumListView.as_view(), name="subcategory_forums"), path("forums/", IndexView.as_view(), name="index"), - path("documentation/", CategoryForumListView.as_view(), name="documentation"), - path("documentation/category/create/", CategoryForumCreateView.as_view(), name="create_category"), - path("documentation/category//create/", SubCategoryForumCreateView.as_view(), name="create_subcategory"), ] diff --git a/lacommunaute/forum/views.py b/lacommunaute/forum/views.py index f16088f96..1f326961a 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,48 +22,15 @@ 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 +64,13 @@ 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 +86,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..e7d52d8e6 --- /dev/null +++ b/lacommunaute/forum_conversation/migrations/0009_topic_document.py @@ -0,0 +1,25 @@ +# Generated by Django 5.0.9 on 2024-09-11 13:48 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("documentation", "0003_document"), + ("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/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..4db9ac6c4 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,23 @@ 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 +61,10 @@ 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_documentstat.py b/lacommunaute/stats/migrations/0003_documentstat.py new file mode 100644 index 000000000..46a756362 --- /dev/null +++ b/lacommunaute/stats/migrations/0003_documentstat.py @@ -0,0 +1,47 @@ +# Generated by Django 5.0.9 on 2024-09-12 14:45 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("documentation", "0005_alter_documentrating_created_at_and_more"), + ("stats", "0002_forumstat"), + ] + + operations = [ + migrations.CreateModel( + name="DocumentStat", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("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é")), + ( + "document", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="documentation.document", + verbose_name="Document", + ), + ), + ], + options={ + "verbose_name": "Stat d'un document", + "verbose_name_plural": "Stats des documents", + "ordering": ["date", "period", "document"], + "unique_together": {("date", "period", "document")}, + }, + ), + ] diff --git a/lacommunaute/stats/migrations/0004_documentationstat_delete_documentstat.py b/lacommunaute/stats/migrations/0004_documentationstat_delete_documentstat.py new file mode 100644 index 000000000..cb7cbf412 --- /dev/null +++ b/lacommunaute/stats/migrations/0004_documentationstat_delete_documentstat.py @@ -0,0 +1,47 @@ +# Generated by Django 5.0.9 on 2024-09-18 12:14 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("contenttypes", "0002_remove_content_type_name"), + ("stats", "0003_documentstat"), + ] + + 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")}, + }, + ), + migrations.DeleteModel( + name="DocumentStat", + ), + ] diff --git a/lacommunaute/stats/models.py b/lacommunaute/stats/models.py index 6d34d474c..c03724e78 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 @@ -37,7 +39,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 +62,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/404.html b/lacommunaute/templates/404.html index b06262438..359bdb9dd 100644 --- a/lacommunaute/templates/404.html +++ b/lacommunaute/templates/404.html @@ -15,7 +15,7 @@

Bienvenue sur le site de la Communauté de l'Inclusion.

Espace d'échanges
  • - Documentation + Documentation
  • Recherche diff --git a/lacommunaute/templates/documentation/category_detail.html b/lacommunaute/templates/documentation/category_detail.html new file mode 100644 index 000000000..8a8164a7d --- /dev/null +++ b/lacommunaute/templates/documentation/category_detail.html @@ -0,0 +1,86 @@ +{% extends "layouts/base.html" %} +{% block title %}{{ category.name }}{{ block.super }}{% endblock %} +{% block meta_description %} + {{ category.short_description }} +{% endblock meta_description %} +{% block breadcrumb %} + {% include "partials/breadcrumb.html" %} +{% endblock %} +{% block content %} + {% load i18n %} + {% include 'documentation/partials/title_and_shortdesc.html' with obj=category user=user only %} + {% if category.description %} +
    +
    +
    {% include 'documentation/partials/image_and_desc.html' with obj=category only %}
    +
    +
    + {% endif %} + {% if tags %} +
    +
    +
    +
    +
    +
    Afficher les fiches contenant l'étiquette
    +
    + {% for tag in tags %} + {% if tag.slug == active_tag_slug %} + + {% else %} + + {% endif %} + {% endfor %} +
    +
    +
    +
    +
    +
    + {% endif %} +
    +
    +
    +
    +
    + {% for obj in category.documents.all %} + {% include 'documentation/partials/content_summary.html' with obj=obj kind='document' only %} + {% endfor %} +
    +
    +
    +
    +
    + {% if user.is_superuser %} +
    + +
    + {% endif %} +{% endblock content %} diff --git a/lacommunaute/templates/documentation/category_list.html b/lacommunaute/templates/documentation/category_list.html new file mode 100644 index 000000000..b3039af64 --- /dev/null +++ b/lacommunaute/templates/documentation/category_list.html @@ -0,0 +1,43 @@ +{% extends "layouts/base.html" %} +{% load i18n %} +{% block title %} + {% trans "Documents" %}{{ block.super }} +{% endblock %} +{% block meta_description %} + Des ressources exclusives pour les professionnels de l'inclusion. Retrouver les ressources nécessaires pour améliorer ses diagnostics socio-professionnels et ses accompagnements des personnes éloignées de l’emploi. +{% endblock meta_description %} +" +{% block content %} +
    +
    +
    +
    +

    {% trans "Documents" %}

    +

    + Retrouver les ressources nécessaires pour améliorer ses diagnostics socio-professionnels et ses accompagnements des personnes éloignées de l’emploi +

    +
    +
    +
    +
    +
    +
    +
    + {% for category in categories %} + {% include 'documentation/partials/content_summary.html' with obj=category kind='category' only %} + {% endfor %} +
    +
    +
    + {% if user.is_superuser %} +
    + +
    + {% endif %} +{% endblock content %} 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 new file mode 100644 index 000000000..2f8037db7 --- /dev/null +++ b/lacommunaute/templates/documentation/partials/content_summary.html @@ -0,0 +1,37 @@ +
    + +
    diff --git a/lacommunaute/templates/documentation/partials/image_and_desc.html b/lacommunaute/templates/documentation/partials/image_and_desc.html new file mode 100644 index 000000000..0ed1b4fad --- /dev/null +++ b/lacommunaute/templates/documentation/partials/image_and_desc.html @@ -0,0 +1,11 @@ +{% load str_filters %} +{% if obj.image %} +
    +
    + {{ obj.name }} +
    +
    +{% endif %} +
    +
    {{ obj.description.rendered|urlizetrunc_target_blank:30|img_fluid }}
    +
    diff --git a/lacommunaute/templates/documentation/partials/partner.html b/lacommunaute/templates/documentation/partials/partner.html new file mode 100644 index 000000000..a40782717 --- /dev/null +++ b/lacommunaute/templates/documentation/partials/partner.html @@ -0,0 +1,8 @@ + +
    + {% if partner.logo %} + {{ partner.name }} + {% endif %} +

    Fiche co-rédigée en partenariat avec {{ partner.name }}

    +
    +
    diff --git a/lacommunaute/templates/documentation/partials/title_and_shortdesc.html b/lacommunaute/templates/documentation/partials/title_and_shortdesc.html new file mode 100644 index 000000000..863f2ec38 --- /dev/null +++ b/lacommunaute/templates/documentation/partials/title_and_shortdesc.html @@ -0,0 +1,11 @@ +
    +
    +
    +
    +

    {{ obj.name }}

    + {% if user.is_superuser %}Mettre à jour{% endif %} + {% if obj.short_description %}

    {{ obj.short_description }}

    {% endif %} +
    +
    +
    +
    diff --git a/lacommunaute/templates/forum/category_forum_list.html b/lacommunaute/templates/forum/category_forum_list.html deleted file mode 100644 index e1949d499..000000000 --- a/lacommunaute/templates/forum/category_forum_list.html +++ /dev/null @@ -1,67 +0,0 @@ -{% extends "layouts/base.html" %} -{% load i18n %} -{% block title %} - {% trans "Documents" %}{{ block.super }} -{% endblock %} -{% block meta_description %} - Des ressources exclusives pour les professionnels de l'inclusion. Retrouver les ressources nécessaires pour améliorer ses diagnostics socio-professionnels et ses accompagnements des personnes éloignées de l’emploi. -{% endblock meta_description %} -" -{% block content %} -
    -
    -
    -
    -

    {% trans "Documents" %}

    -

    - Retrouver les ressources nécessaires pour améliorer ses diagnostics socio-professionnels et ses accompagnements des personnes éloignées de l’emploi -

    -
    -
    -
    -
    -
    -
    -
    - {% for forum in forums %} -
    - -
    - {% endfor %} -
    -
    -
    - {% if user.is_superuser %} -
    - -
    - {% endif %} -{% endblock content %} diff --git a/lacommunaute/templates/forum/forum_documentation_category.html b/lacommunaute/templates/forum/forum_documentation_category.html deleted file mode 100644 index e12bfabe5..000000000 --- a/lacommunaute/templates/forum/forum_documentation_category.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends "forum/forum_detail.html" %} -{% block subforum_list %} - {% include "forum/partials/subcategory_forum_list.html" with forum=forum sub_forums=sub_forums tags_of_descendants=tags_of_descendants active_forum_tag_slug=active_forum_tag_slug only %} -{% endblock subforum_list %} -{% block forum_foot_content %} - {% if user.is_superuser %} -
    - -
    - {% endif %} -{% endblock %} diff --git a/lacommunaute/templates/pages/home.html b/lacommunaute/templates/pages/home.html index d4ad09ad1..784ae8892 100644 --- a/lacommunaute/templates/pages/home.html +++ b/lacommunaute/templates/pages/home.html @@ -10,7 +10,7 @@ {% block body_class %}p-home{{ block.super }}{% endblock %} {% block content %} {% url 'forum_conversation_extension:topics' as publicforum_url %} - {% url 'forum_extension:documentation' as documentation_url %} + {% url 'documentation:category_list' as documentation_url %} {% url 'event:current' as event_url %} {% url 'surveys:dsp_create' as dsp_url %}
    diff --git a/lacommunaute/templates/partials/ask_a_question.html b/lacommunaute/templates/partials/ask_a_question.html index 3516f50cc..9fd7a5e4c 100644 --- a/lacommunaute/templates/partials/ask_a_question.html +++ b/lacommunaute/templates/partials/ask_a_question.html @@ -21,7 +21,7 @@
    ou
    - diff --git a/lacommunaute/templates/partials/footer.html b/lacommunaute/templates/partials/footer.html index 5c39f2564..2f06a8d57 100644 --- a/lacommunaute/templates/partials/footer.html +++ b/lacommunaute/templates/partials/footer.html @@ -14,7 +14,7 @@ {% trans "Discussion area" %}
  • - {% trans "Documents" %} + {% trans "Documents" %}
  • {% trans "Events" %} diff --git a/lacommunaute/templates/partials/header.html b/lacommunaute/templates/partials/header.html index d97774da8..12aee9d07 100644 --- a/lacommunaute/templates/partials/header.html +++ b/lacommunaute/templates/partials/header.html @@ -4,7 +4,7 @@ {% load forum_member_tags %} {% url 'pages:home' as home_url %} {% url 'forum_conversation_extension:topics' as publicforum_url %} -{% url 'forum_extension:documentation' as documentation_url %} +{% url 'documentation:category_list' as documentation_url %} {% url 'members:seekers' as seeker_url %} {% url 'search:index' as search_url %} {% url 'surveys:dsp_create' as dsp_url %} diff --git a/lacommunaute/templates/partials/social_share_buttons.html b/lacommunaute/templates/partials/social_share_buttons.html index 1c6786bc4..cf56f0e5c 100644 --- a/lacommunaute/templates/partials/social_share_buttons.html +++ b/lacommunaute/templates/partials/social_share_buttons.html @@ -1,7 +1,7 @@ {% load i18n %} {% load social_share %}