From fdeaf46ab246b3caa51b7daa0ad4f51c3e75f838 Mon Sep 17 00:00:00 2001 From: Jonathan Reveille Date: Tue, 25 Feb 2025 11:59:41 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A5(backend)=20remove=20`generate=5Fce?= =?UTF-8?q?rtificate`=20action=20in=20django=20admin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since we have fixed the generation of certificates into the BO for orders with product type certificate, we don't want our admin users to use the django admin anymore for this task. We decided to remove the action from the admin courses, products and orders views. It is now impossible to generate certificate through the django admin views. Fix #1061 --- CHANGELOG.md | 1 + src/backend/joanie/core/admin.py | 112 +----------------- .../joanie/tests/core/test_admin_course.py | 7 -- .../joanie/tests/core/test_admin_product.py | 50 +------- 4 files changed, 8 insertions(+), 162 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7719a9ba9..8ed794b7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to ### Changed +- Remove `generate_certificates` action in django admin views - Remove `owner` and `is_main` in CreditCard model permanently - Remove `is_active` on order group client serializer diff --git a/src/backend/joanie/core/admin.py b/src/backend/joanie/core/admin.py index b965bfb25..33985a99d 100644 --- a/src/backend/joanie/core/admin.py +++ b/src/backend/joanie/core/admin.py @@ -6,21 +6,18 @@ from django.contrib.admin.options import csrf_protect_m from django.contrib.auth import admin as auth_admin from django.contrib.sites.models import Site -from django.http import HttpResponseRedirect -from django.urls import re_path, reverse +from django.urls import reverse from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from django.utils.translation import ngettext_lazy from admin_auto_filters.filters import AutocompleteFilter from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin -from django_object_actions import DjangoObjectActions, takes_instance_or_queryset +from django_object_actions import DjangoObjectActions from parler.admin import TranslatableAdmin, TranslatableStackedInline -from joanie.core import forms, helpers, models -from joanie.core.enums import PRODUCT_TYPE_CERTIFICATE_ALLOWED +from joanie.core import forms, models -ACTION_NAME_GENERATE_CERTIFICATES = "generate_certificates" ACTION_NAME_CANCEL = "cancel" @@ -275,9 +272,7 @@ class CourseCourseRunsInline(admin.TabularInline): class CourseAdmin(DjangoObjectActions, TranslatableAdmin): """Admin class for the Course model""" - actions = (ACTION_NAME_GENERATE_CERTIFICATES,) autocomplete_fields = ["organizations"] - change_actions = (ACTION_NAME_GENERATE_CERTIFICATES,) change_form_template = "joanie/admin/translatable_change_form_with_actions.html" list_display = ("code", "title", "state") readonly_fields = ("course_runs",) @@ -305,18 +300,6 @@ class CourseAdmin(DjangoObjectActions, TranslatableAdmin): ) search_fields = ["code", "translations__title"] - @takes_instance_or_queryset - def generate_certificates(self, request, queryset): # pylint: disable no-self-use - """ - Custom action to generate certificates for a collection of courses - passed as a queryset - """ - certificate_generated_count = helpers.generate_certificates_for_orders( - models.Order.objects.filter(course__in=queryset) - ) - - summarize_certification_to_user(request, certificate_generated_count) - @admin.register(models.CourseRun) class CourseRunAdmin(TranslatableAdmin): @@ -479,67 +462,8 @@ class ProductAdmin( "id", "related_courses", ) - actions = (ACTION_NAME_GENERATE_CERTIFICATES,) - change_actions = (ACTION_NAME_GENERATE_CERTIFICATES,) search_fields = ["translations__title"] - def get_change_actions(self, request, object_id, form_url): - """ - Remove the generate_certificates action from list of actions - if the product instance is not certifying - """ - actions = super().get_change_actions(request, object_id, form_url) - actions = list(actions) - - if not self.model.objects.filter( - pk=object_id, type__in=PRODUCT_TYPE_CERTIFICATE_ALLOWED - ).exists(): - actions.remove(ACTION_NAME_GENERATE_CERTIFICATES) - - return actions - - def get_urls(self): - """ - Add url to trigger certificate generation for a course - product couple. - """ - url_patterns = super().get_urls() - - return [ - re_path( - r"^(?P.+)/generate-certificates/(?P.+)/$", - self.admin_site.admin_view(self.generate_certificates_for_course), - name=ACTION_NAME_GENERATE_CERTIFICATES, - ) - ] + url_patterns - - @takes_instance_or_queryset - def generate_certificates(self, request, queryset): # pylint: disable=no-self-use - """ - Custom action to generate certificates for a collection of products - passed as a queryset - """ - certificate_generated_count = helpers.generate_certificates_for_orders( - models.Order.objects.filter(product__in=queryset) - ) - - summarize_certification_to_user(request, certificate_generated_count) - - def generate_certificates_for_course(self, request, product_id, course_code): # pylint: disable=no-self-use - """ - A custom action to generate certificates for a course - product couple. - """ - certificate_generated_count = helpers.generate_certificates_for_orders( - models.Order.objects.filter( - product__id=product_id, course__code=course_code - ) - ) - - summarize_certification_to_user(request, certificate_generated_count) - - return HttpResponseRedirect( - reverse("admin:core_product_change", args=(product_id,)) - ) - @admin.display(description="Related courses") def related_courses(self, obj): # pylint: disable=no-self-use """ @@ -553,7 +477,6 @@ def get_related_courses_as_html(obj): # pylint: disable=no-self-use Get the html representation of the product's related courses """ related_courses = obj.courses.all() - is_certifying = obj.type in PRODUCT_TYPE_CERTIFICATE_ALLOWED if related_courses: items = [] @@ -562,26 +485,11 @@ def get_related_courses_as_html(obj): # pylint: disable=no-self-use "admin:core_course_change", args=(course.id,), ) - raw_html = ( '
  • ' f"{course.code} | {course.title}" + "
  • " ) - - if is_certifying: - # Add a button to generate certificate - generate_certificates_url = reverse( - f"admin:{ACTION_NAME_GENERATE_CERTIFICATES}", - kwargs={"product_id": obj.id, "course_code": course.code}, - ) - - raw_html += ( - f'' # pylint: disable=line-too-long - f"{_('Generate certificates')}" - "" - ) - - raw_html += "" items.append(raw_html) return format_html(f"
      {''.join(items)}
    ") @@ -601,9 +509,8 @@ class DiscountAdmin(admin.ModelAdmin): class OrderAdmin(DjangoObjectActions, admin.ModelAdmin): """Admin class for the Order model""" - actions = (ACTION_NAME_CANCEL, ACTION_NAME_GENERATE_CERTIFICATES) + actions = (ACTION_NAME_CANCEL,) autocomplete_fields = ["course", "enrollment", "organization", "owner", "product"] - change_actions = (ACTION_NAME_GENERATE_CERTIFICATES,) list_display = ("id", "created_on", "organization", "owner", "product", "state") list_filter = [OwnerFilter, OrganizationFilter, ProductFilter, "state"] readonly_fields = ( @@ -621,15 +528,6 @@ def cancel(self, request, queryset): # pylint: disable=no-self-use for order in queryset: order.flow.cancel() - @takes_instance_or_queryset - def generate_certificates(self, request, queryset): # pylint: disable=no-self-use - """ - Custom action to launch generate_certificates management commands - over the order selected - """ - certificate_generated_count = helpers.generate_certificates_for_orders(queryset) - summarize_certification_to_user(request, certificate_generated_count) - def invoice(self, obj): # pylint: disable=no-self-use """Retrieve the root invoice related to the order.""" invoice = obj.invoices.get(parent__isnull=True) diff --git a/src/backend/joanie/tests/core/test_admin_course.py b/src/backend/joanie/tests/core/test_admin_course.py index aac6d36e5..6968d53ab 100644 --- a/src/backend/joanie/tests/core/test_admin_course.py +++ b/src/backend/joanie/tests/core/test_admin_course.py @@ -45,10 +45,3 @@ def test_admin_course_use_translatable_change_form_with_actions_template(self): # Django parler tabs should be displayed parler_tabs = html.cssselect(".parler-language-tabs span") self.assertEqual(len(parler_tabs), len(settings.LANGUAGES)) - - # Django object actions should be displayed - object_actions = html.cssselect(".objectaction-item") - self.assertEqual(len(object_actions), 1) - self.assertEqual( - object_actions[0].attrib["data-tool-name"], "generate_certificates" - ) diff --git a/src/backend/joanie/tests/core/test_admin_product.py b/src/backend/joanie/tests/core/test_admin_product.py index cf4d41866..b6768c59b 100644 --- a/src/backend/joanie/tests/core/test_admin_product.py +++ b/src/backend/joanie/tests/core/test_admin_product.py @@ -5,10 +5,8 @@ import random import uuid from http import HTTPStatus -from unittest import mock from django.conf import settings -from django.contrib.messages import get_messages from django.urls import reverse import lxml.html @@ -350,9 +348,9 @@ def test_admin_product_should_allow_to_generate_certificate_for_related_course( # - The related course should be displayed related_course = related_courses_field.cssselect("li") self.assertEqual(len(related_course), 1) - # - And it should contain two links + # - And it should contain one link links = related_course[0].cssselect("a") - self.assertEqual(len(links), 2) + self.assertEqual(len(links), 1) # - 1st a link to go to the related course change view self.assertEqual(links[0].text_content(), f"{course.code} | {course.title}") self.assertEqual( @@ -360,50 +358,6 @@ def test_admin_product_should_allow_to_generate_certificate_for_related_course( reverse("admin:core_course_change", args=(course.pk,)), ) - # - 2nd a link to generate certificate for the course - product couple - self.assertEqual(links[1].text_content(), "Generate certificates") - self.assertEqual( - links[1].attrib["href"], - reverse( - "admin:generate_certificates", - kwargs={"product_id": product.id, "course_code": course.code}, - ), - ) - - @mock.patch("joanie.core.helpers.generate_certificates_for_orders", return_value=0) - def test_admin_product_generate_certificate_for_course( - self, mock_generate_certificates - ): - """ - Product Admin should contain an endpoint which triggers the - `create_certificates` management command with product and course as options. - """ - user = factories.UserFactory(is_staff=True, is_superuser=True) - self.client.login(username=user.username, password="password") - - course = factories.CourseFactory() - product = factories.ProductFactory(courses=[course]) - - response = self.client.get( - reverse( - "admin:generate_certificates", - kwargs={"course_code": course.code, "product_id": product.id}, - ), - ) - - # - Generate certificates command should have been called - mock_generate_certificates.assert_called_once() - - # Check the presence of a confirmation message - messages = list(get_messages(response.wsgi_request)) - self.assertEqual(len(messages), 1) - self.assertEqual(str(messages[0]), "No certificates have been generated.") - - # - User should be redirected to the product change view - self.assertRedirects( - response, reverse("admin:core_product_change", args=(product.id,)) - ) - def test_admin_product_use_translatable_change_form_with_actions_template(self): """ The product admin change view should use a custom change form template