Skip to content

Commit

Permalink
🔥(backend) remove generate_certificate action in django admin
Browse files Browse the repository at this point in the history
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
  • Loading branch information
jonathanreveille committed Feb 25, 2025
1 parent faa3423 commit fdeaf46
Show file tree
Hide file tree
Showing 4 changed files with 8 additions and 162 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
112 changes: 5 additions & 107 deletions src/backend/joanie/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"


Expand Down Expand Up @@ -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",)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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<product_id>.+)/generate-certificates/(?P<course_code>.+)/$",
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
"""
Expand All @@ -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 = []
Expand All @@ -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 = (
'<li style="margin-bottom: 1rem">'
f"<a href='{change_course_url}'>{course.code} | {course.title}</a>"
"</li>"
)

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'<a style="margin-left: 1rem" class="button" href="{generate_certificates_url}">' # pylint: disable=line-too-long
f"{_('Generate certificates')}"
"</a>"
)

raw_html += "</li>"
items.append(raw_html)

return format_html(f"<ul style='margin: 0'>{''.join(items)}</ul>")
Expand All @@ -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 = (
Expand All @@ -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)
Expand Down
7 changes: 0 additions & 7 deletions src/backend/joanie/tests/core/test_admin_course.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
50 changes: 2 additions & 48 deletions src/backend/joanie/tests/core/test_admin_product.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -350,60 +348,16 @@ 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(
links[0].attrib["href"],
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
Expand Down

0 comments on commit fdeaf46

Please sign in to comment.