Skip to content

Commit 489d893

Browse files
authored
Merge pull request #243 from edubadges/feature/improve-performance-of-direct-award-audit-trail-endpoint
Feature/improve performance of direct award audit trail endpoint
2 parents 14ce28a + e724192 commit 489d893

File tree

6 files changed

+136
-59
lines changed

6 files changed

+136
-59
lines changed

apps/directaward/api.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from drf_spectacular.utils import extend_schema, inline_serializer, OpenApiExample, OpenApiResponse, OpenApiParameter
55
from rest_framework import serializers
66
from rest_framework import status
7+
from rest_framework.generics import ListAPIView
78
from rest_framework.response import Response
89
from rest_framework.views import APIView
910

@@ -680,8 +681,9 @@ def put(self, request, **kwargs):
680681
)
681682

682683

683-
class DirectAwardAuditTrailView(APIView):
684+
class DirectAwardAuditTrailListView(ListAPIView):
684685
permission_classes = (IsSuperUser,)
686+
serializer_class = DirectAwardAuditTrailSerializer
685687

686688
@extend_schema(
687689
description='Get all direct award audit trail entries (superuser only)',
@@ -708,7 +710,18 @@ class DirectAwardAuditTrailView(APIView):
708710
403: permission_denied_response,
709711
},
710712
)
711-
def get(self, request, *args, **kwargs):
712-
audit_trails = DirectAwardAuditTrail.objects.filter(action='CREATE').order_by('-action_datetime')
713-
serializer = DirectAwardAuditTrailSerializer(audit_trails, many=True)
714-
return Response(serializer.data, status=status.HTTP_200_OK)
713+
714+
def get_queryset(self):
715+
return (
716+
DirectAwardAuditTrail.objects
717+
.filter(
718+
action='CREATE',
719+
direct_award__isnull=False,
720+
)
721+
.select_related(
722+
'direct_award',
723+
'badgeclass',
724+
'badgeclass__institution',
725+
)
726+
.order_by('-action_datetime')
727+
)

apps/directaward/api_urls.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
DirectAwardRevoke,
88
DirectAwardDelete,
99
DirectAwardBundleView,
10-
DirectAwardAuditTrailView,
10+
DirectAwardAuditTrailListView,
1111
)
1212

1313
urlpatterns = [
@@ -16,5 +16,5 @@
1616
path('accept/<str:entity_id>', DirectAwardAccept.as_view(), name='direct_award_accept'),
1717
path('revoke-direct-awards', DirectAwardRevoke.as_view(), name='direct_award_revoke'),
1818
path('delete-direct-awards', DirectAwardDelete.as_view(), name='direct_award_delete'),
19-
path('audittrail', DirectAwardAuditTrailView.as_view(), name='direct_award_audittrail'),
19+
path('audittrail', DirectAwardAuditTrailListView.as_view(), name='direct_award_audittrail'),
2020
]
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Generated by Django 4.2.27 on 2026-01-19 11:57
2+
3+
from django.db import migrations, models
4+
5+
6+
def forwards_populate_audit_trail_fks(apps, schema_editor):
7+
DirectAwardAuditTrail = apps.get_model('directaward', 'DirectAwardAuditTrail')
8+
DirectAward = apps.get_model('directaward', 'DirectAward')
9+
BadgeClass = apps.get_model('issuer', 'BadgeClass')
10+
11+
for audit in DirectAwardAuditTrail.objects.all().iterator():
12+
if audit.direct_award_entity_id and not audit.direct_award:
13+
audit.direct_award = (
14+
DirectAward.objects
15+
.filter(entity_id=audit.direct_award_entity_id)
16+
.first()
17+
)
18+
19+
if audit.badgeclass_entity_id and not audit.badgeclass:
20+
audit.badgeclass = (
21+
BadgeClass.objects
22+
.filter(entity_id=audit.badgeclass_entity_id)
23+
.first()
24+
)
25+
26+
audit.save(update_fields=['direct_award', 'badgeclass'])
27+
28+
29+
def backwards_restore_entity_ids(apps, schema_editor):
30+
DirectAwardAuditTrail = apps.get_model('directaward', 'DirectAwardAuditTrail')
31+
32+
for audit in DirectAwardAuditTrail.objects.all().iterator():
33+
if audit.direct_award and not audit.direct_award_entity_id:
34+
audit.direct_award_entity_id = audit.direct_award.entity_id
35+
36+
if audit.badgeclass and not audit.badgeclass_entity_id:
37+
audit.badgeclass_entity_id = audit.badgeclass.entity_id
38+
39+
audit.save(update_fields=[
40+
'direct_award_entity_id',
41+
'badgeclass_entity_id',
42+
])
43+
44+
class Migration(migrations.Migration):
45+
46+
dependencies = [
47+
('directaward', '0023_directawardbundle_direct_award_removed_count'),
48+
('issuer',
49+
'0117_rename_badgeinstance_recipient_identifier_badgeclass_revoked_issuer_badg_recipie_6a2cd8_idx_and_more'),
50+
]
51+
52+
operations = [
53+
migrations.RenameField(
54+
model_name='directawardaudittrail',
55+
old_name='badgeclass_id',
56+
new_name='badgeclass_entity_id',
57+
),
58+
migrations.RenameField(
59+
model_name='directawardaudittrail',
60+
old_name='direct_award_id',
61+
new_name='direct_award_entity_id',
62+
),
63+
migrations.AddField(
64+
model_name='directawardaudittrail',
65+
name='badgeclass',
66+
field=models.ForeignKey(blank=True, null=True, on_delete=models.deletion.SET_NULL, to='issuer.badgeclass'),
67+
),
68+
migrations.AddField(
69+
model_name='directawardaudittrail',
70+
name='direct_award',
71+
field=models.ForeignKey(blank=True, null=True, on_delete=models.deletion.SET_NULL, to='directaward.directaward'),
72+
),
73+
migrations.RunPython(
74+
forwards_populate_audit_trail_fks,
75+
backwards_restore_entity_ids,
76+
),
77+
migrations.RemoveField(
78+
model_name='directawardaudittrail',
79+
name='badgeclass_entity_id',
80+
),
81+
migrations.RemoveField(
82+
model_name='directawardaudittrail',
83+
name='direct_award_entity_id',
84+
),
85+
]

apps/directaward/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,5 +263,5 @@ class DirectAwardAuditTrail(models.Model):
263263
user_agent_info = models.CharField(max_length=255, blank=True)
264264
action = models.CharField(max_length=40)
265265
change_summary = models.CharField(max_length=199, blank=True)
266-
direct_award_id = models.CharField(max_length=255, blank=True)
267-
badgeclass_id = models.CharField(max_length=255, blank=True)
266+
direct_award = models.ForeignKey('directaward.DirectAward', on_delete=models.SET_NULL, null=True, blank=True)
267+
badgeclass = models.ForeignKey('issuer.BadgeClass', on_delete=models.SET_NULL, null=True, blank=True)

apps/directaward/serializer.py

Lines changed: 16 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,22 @@ def to_representation(self, instance):
168168

169169

170170
class DirectAwardAuditTrailSerializer(serializers.ModelSerializer):
171-
badgeclass_name = serializers.SerializerMethodField()
172-
institution_name = serializers.SerializerMethodField()
173-
recipient_email = serializers.SerializerMethodField()
174-
recipient_eppn = serializers.SerializerMethodField()
171+
badgeclass_name = serializers.CharField(
172+
source='badgeclass.name',
173+
read_only=True,
174+
)
175+
institution_name = serializers.CharField(
176+
source='badgeclass.institution.name',
177+
read_only=True,
178+
)
179+
recipient_email = serializers.EmailField(
180+
source='direct_award.recipient_email',
181+
read_only=True
182+
)
183+
recipient_eppn = serializers.CharField(
184+
source='direct_award.eppn',
185+
read_only=True
186+
)
175187

176188
class Meta:
177189
model = DirectAwardAuditTrail
@@ -183,46 +195,3 @@ class Meta:
183195
'recipient_email',
184196
'recipient_eppn',
185197
]
186-
187-
def get_badgeclass_name(self, obj):
188-
"""Get the badge class name from the badgeclass_id"""
189-
if obj.badgeclass_id:
190-
try:
191-
badgeclass = BadgeClass.objects.get(id=obj.badgeclass_id)
192-
return badgeclass.name
193-
except BadgeClass.DoesNotExist:
194-
return None
195-
return None
196-
197-
def get_institution_name(self, obj):
198-
"""Get the institution name from the badgeclass"""
199-
if obj.badgeclass_id:
200-
try:
201-
badgeclass = BadgeClass.objects.get(id=obj.badgeclass_id)
202-
institution = badgeclass.institution
203-
return institution.name if institution else None
204-
except BadgeClass.DoesNotExist:
205-
return None
206-
return None
207-
208-
def get_recipient_email(self, obj):
209-
"""Get the recipient email from the direct award"""
210-
if obj.badgeclass_id:
211-
try:
212-
directaward = DirectAward.objects.get(entity_id=obj.direct_award_id)
213-
recipient_email = directaward.recipient_email
214-
return recipient_email if directaward else None
215-
except DirectAward.DoesNotExist:
216-
return None
217-
return None
218-
219-
def get_recipient_eppn(self, obj):
220-
"""Get the recipient eppn from the direct award"""
221-
if obj.badgeclass_id:
222-
try:
223-
directaward = DirectAward.objects.get(entity_id=obj.direct_award_id)
224-
eppn = directaward.eppn
225-
return eppn if eppn else ''
226-
except DirectAward.DoesNotExist:
227-
return None
228-
return None

apps/directaward/signals.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from django.dispatch import receiver
55

66
from .models import DirectAwardAuditTrail
7+
from directaward.models import DirectAward
8+
from issuer.models import BadgeClass
79

810
# Signals doc: https://docs.djangoproject.com/en/4.2/topics/signals/
911
audit_trail_signal = django.dispatch.Signal() # creates a custom signal and specifies the args required.
@@ -25,17 +27,25 @@ def get_client_ip(request):
2527
def direct_award_audit_trail(sender, user, request, direct_award_id, badgeclass_id, method, summary, **kwargs):
2628
try:
2729
user_agent_info = (request.headers.get('user-agent', '<unknown>')[:255],)
30+
31+
direct_award = None
32+
badgeclass = None
33+
if direct_award_id:
34+
direct_award = DirectAward.objects.filter(entity_id=direct_award_id).first()
35+
if badgeclass_id:
36+
badgeclass = BadgeClass.objects.filter(entity_id=badgeclass_id).first()
37+
2838
audit_trail = DirectAwardAuditTrail.objects.create(
2939
user=user,
3040
user_agent_info=user_agent_info,
3141
login_IP=get_client_ip(request),
3242
action=method,
3343
change_summary=summary,
34-
direct_award_id=direct_award_id,
35-
badgeclass_id=badgeclass_id,
44+
direct_award=direct_award,
45+
badgeclass=badgeclass,
3646
)
3747
logger.info(
38-
f'direct_award_audit_trail created {audit_trail.id} for user {audit_trail.user} and directaward {audit_trail.direct_award_id}'
48+
f'direct_award_audit_trail created {audit_trail.id} for user {audit_trail.user} and directaward {direct_award_id}'
3949
)
4050
except Exception as e:
4151
logger.error('direct_award_audit_trail request: %s, error: %s' % (request, e))

0 commit comments

Comments
 (0)