Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
Release Notes
=============

Version 1.149.0
---------------

- Define typed serializers for course outline response (#3525)
- feat: enhance certificate admin (#3502)
- Add pytest.mark.django_db to image test (#3523)
- Fixes 500 errors when viewing an Organization in the Django Admin (#3519)

Version 1.147.6 (Released April 27, 2026)
---------------

Expand Down
38 changes: 37 additions & 1 deletion b2b/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,47 @@ class UserOrganizationAdminInline(admin.TabularInline):
model = UserOrganization
extra = 0
verbose_name = "Organization Admin"
list_display = [
"user_email",
"keep_until_seen",
"is_manager",
]
readonly_fields = [
"user_email",
"keep_until_seen",
"is_manager",
]

def get_queryset(self, request):
"""Filter the queryset to just users with Manager access."""

return super().get_queryset(request).filter(is_manager=True)
return (
super()
.get_queryset(request)
.prefetch_related("user")
.filter(is_manager=True)
)

def has_add_permission(self, request, obj): # noqa: ARG002
"""Determine if the user can add new ones from here (no, they cannot)"""

return False

def has_change_permission(self, request, obj=None): # noqa: ARG002
"""Determine if the user can add new ones from here (no, they cannot)"""

return False

def has_delete_permission(self, request, obj=None): # noqa: ARG002
"""Determine if the user can add new ones from here (no, they cannot)"""

return False

@admin.display(description="User Email")
def user_email(self, obj):
"""Return the user's email address."""

return obj.user.email


class ReadOnlyModelAdmin(admin.ModelAdmin):
Expand Down
1 change: 1 addition & 0 deletions cms/api_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def test_ensure_home_page_and_site():
assert home_page_qset.count() == 1


@pytest.mark.django_db
def test_get_wagtail_img_src(settings):
"""get_wagtail_img_src should return the correct image URL"""
settings.MEDIA_URL = "/mediatest/"
Expand Down
46 changes: 43 additions & 3 deletions courses/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,25 @@ def get_order_state(self, obj):
return obj.order.state


class HasCertificateRevisionFilter(admin.SimpleListFilter):
title = "Has Certificate Revision"
parameter_name = "has_certificate_revision"

def lookups(self, request, model_admin): # noqa: ARG002
return (
("yes", "Yes"),
("no", "No"),
)

def queryset(self, request, queryset): # noqa: ARG002
value = self.value()
if value == "yes":
return queryset.filter(certificate_page_revision__isnull=False)
if value == "no":
return queryset.filter(certificate_page_revision__isnull=True)
return queryset


@admin.register(CourseRunCertificate)
class CourseRunCertificateAdmin(TimestampedModelAdmin):
"""Admin for CourseRunCertificate"""
Expand All @@ -610,14 +629,16 @@ class CourseRunCertificateAdmin(TimestampedModelAdmin):
"course_run",
"get_certificate_page_title",
"get_revoked_state",
"get_has_certificate_revision",
]
search_fields = [
"uuid",
"course_run__courseware_id",
"course_run__title",
"user__username",
"user__email",
]
list_filter = ["is_revoked", "course_run__course"]
list_filter = ["is_revoked", HasCertificateRevisionFilter, "course_run__course"]
raw_id_fields = ("user", "course_run", "verifiable_credential")
autocomplete_fields = ("certificate_page_revision",)

Expand Down Expand Up @@ -648,6 +669,14 @@ def get_revoked_state(self, obj):
"""Return the revoked state"""
return obj.is_revoked is not True

@admin.display(
description="Has Certificate Revision",
boolean=True,
)
def get_has_certificate_revision(self, obj):
"""Return whether a certificate page revision is associated"""
return obj.certificate_page_revision is not None

def get_queryset(self, request): # noqa: ARG002
return self.model.all_objects.get_queryset().select_related(
"user", "course_run"
Expand All @@ -665,15 +694,18 @@ class ProgramCertificateAdmin(TimestampedModelAdmin):
"user",
"program",
"get_revoked_state",
"get_has_certificate_revision",
]
search_fields = [
"program__readable_id",
"program__title",
"user__username",
"user__email",
"uuid",
]
list_filter = ["program__title", "is_revoked"]
raw_id_fields = ("user", "verifiable_credential")
list_filter = ["program__title", HasCertificateRevisionFilter, "is_revoked"]
raw_id_fields = ("user", "verifiable_credential", "program")
autocomplete_fields = ("certificate_page_revision",)

@admin.display(
description="Active",
Expand All @@ -683,6 +715,14 @@ def get_revoked_state(self, obj):
"""Return the revoked state"""
return obj.is_revoked is not True

@admin.display(
description="Has Certificate Revision",
boolean=True,
)
def get_has_certificate_revision(self, obj):
"""Return whether a certificate page revision is associated"""
return obj.certificate_page_revision is not None

def get_queryset(self, request): # noqa: ARG002
return self.model.all_objects.get_queryset().select_related("user", "program")

Expand Down
31 changes: 31 additions & 0 deletions courses/serializers/v3/courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,34 @@ class Meta(BaseCourseRunEnrollmentSerializer.Meta):
"b2b_contract_id",
"certificate",
]


@extend_schema_serializer(component_name="CourseOutlineModuleCounts")
class CourseOutlineModuleCountsSerializer(serializers.Serializer):
"""Activity counts within a course outline module."""

videos = serializers.IntegerField()
readings = serializers.IntegerField()
problems = serializers.IntegerField()
assignments = serializers.IntegerField()
app_items = serializers.IntegerField()


@extend_schema_serializer(component_name="CourseOutlineModule")
class CourseOutlineModuleSerializer(serializers.Serializer):
"""A single module within a course outline."""

id = serializers.CharField()
title = serializers.CharField()
effort_time = serializers.IntegerField()
effort_activities = serializers.IntegerField()
counts = CourseOutlineModuleCountsSerializer()


@extend_schema_serializer(component_name="CourseOutlineResponse")
class CourseOutlineResponseSerializer(serializers.Serializer):
"""Course outline data fetched from Open edX."""

course_id = serializers.CharField()
generated_at = serializers.DateTimeField()
modules = CourseOutlineModuleSerializer(many=True)
14 changes: 5 additions & 9 deletions courses/views/v3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@
Program,
ProgramEnrollment,
)
from courses.serializers.v3.courses import CourseRunEnrollmentSerializer
from courses.serializers.v3.courses import (
CourseOutlineResponseSerializer,
CourseRunEnrollmentSerializer,
)
from courses.serializers.v3.programs import (
ProgramEnrollmentCreateSerializer,
ProgramEnrollmentSerializer,
Expand Down Expand Up @@ -274,14 +277,7 @@ def destroy(self, request, *args, **kwargs): # noqa: ARG002
)
],
responses={
200: inline_serializer(
name="CourseOutlineResponse",
fields={
"course_id": serializers.CharField(),
"generated_at": serializers.CharField(),
"modules": serializers.ListField(child=serializers.DictField()),
},
),
200: CourseOutlineResponseSerializer,
400: inline_serializer(
name="CourseOutlineBadRequestResponse",
fields={"detail": serializers.CharField()},
Expand Down
2 changes: 1 addition & 1 deletion main/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from main.sentry import init_sentry
from openapi.settings_spectacular import open_spectacular_settings

VERSION = "1.147.6"
VERSION = "1.149.0"

log = logging.getLogger()

Expand Down
45 changes: 43 additions & 2 deletions openapi/specs/v0.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4091,18 +4091,59 @@ components:
type: string
required:
- detail
CourseOutlineModule:
type: object
description: A single module within a course outline.
properties:
id:
type: string
title:
type: string
effort_time:
type: integer
effort_activities:
type: integer
counts:
$ref: '#/components/schemas/CourseOutlineModuleCounts'
required:
- counts
- effort_activities
- effort_time
- id
- title
CourseOutlineModuleCounts:
type: object
description: Activity counts within a course outline module.
properties:
videos:
type: integer
readings:
type: integer
problems:
type: integer
assignments:
type: integer
app_items:
type: integer
required:
- app_items
- assignments
- problems
- readings
- videos
CourseOutlineResponse:
type: object
description: Course outline data fetched from Open edX.
properties:
course_id:
type: string
generated_at:
type: string
format: date-time
modules:
type: array
items:
type: object
additionalProperties: {}
$ref: '#/components/schemas/CourseOutlineModule'
required:
- course_id
- generated_at
Expand Down
45 changes: 43 additions & 2 deletions openapi/specs/v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4091,18 +4091,59 @@ components:
type: string
required:
- detail
CourseOutlineModule:
type: object
description: A single module within a course outline.
properties:
id:
type: string
title:
type: string
effort_time:
type: integer
effort_activities:
type: integer
counts:
$ref: '#/components/schemas/CourseOutlineModuleCounts'
required:
- counts
- effort_activities
- effort_time
- id
- title
CourseOutlineModuleCounts:
type: object
description: Activity counts within a course outline module.
properties:
videos:
type: integer
readings:
type: integer
problems:
type: integer
assignments:
type: integer
app_items:
type: integer
required:
- app_items
- assignments
- problems
- readings
- videos
CourseOutlineResponse:
type: object
description: Course outline data fetched from Open edX.
properties:
course_id:
type: string
generated_at:
type: string
format: date-time
modules:
type: array
items:
type: object
additionalProperties: {}
$ref: '#/components/schemas/CourseOutlineModule'
required:
- course_id
- generated_at
Expand Down
Loading
Loading