Skip to content

Commit f8f99db

Browse files
committed
Do correct locking with context manager for reward point redemption
1 parent 86f24bf commit f8f99db

File tree

2 files changed

+42
-13
lines changed

2 files changed

+42
-13
lines changed

evap/rewards/forms.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
from contextlib import contextmanager
12
from datetime import date
23

34
from django import forms
45
from django.core.exceptions import ValidationError
56
from django.core.validators import MaxValueValidator, StepValueValidator
7+
from django.db import transaction
68
from django.utils.translation import gettext as _
79

810
from evap.rewards.models import RewardPointRedemption, RewardPointRedemptionEvent
@@ -48,9 +50,40 @@ def clean_event(self):
4850
class BaseRewardPointRedemptionFormSet(forms.BaseFormSet):
4951
def __init__(self, *args, **kwargs):
5052
self.user = kwargs.pop("user")
53+
self.total_points_available = reward_points_of_user(self.user)
5154
super().__init__(*args, **kwargs)
55+
self.locked = False
56+
57+
def get_form_kwargs(self, index):
58+
"""
59+
Return additional keyword arguments for each individual formset form.
60+
61+
index will be None if the form being constructed is a new empty
62+
form.
63+
"""
64+
kwargs = self.form_kwargs.copy()
65+
if not self.initial:
66+
return kwargs
67+
kwargs["initial"] = self.initial[index]
68+
kwargs["initial"]["total_points_available"] = self.total_points_available
69+
return kwargs
70+
71+
@contextmanager
72+
def lock(self):
73+
with transaction.atomic():
74+
# lock these rows to prevent race conditions
75+
list(self.user.reward_point_grantings.select_for_update())
76+
list(self.user.reward_point_redemptions.select_for_update())
77+
78+
self.locked = True
79+
80+
yield
81+
82+
self.locked = False
5283

5384
def clean(self):
85+
assert self.locked
86+
5487
if any(self.errors):
5588
return
5689

@@ -64,9 +97,7 @@ def clean(self):
6497
raise ValidationError(_("You don't have enough reward points."))
6598

6699
def save(self) -> list[RewardPointRedemption]:
67-
# lock these rows to prevent race conditions
68-
list(self.user.reward_point_grantings.select_for_update())
69-
list(self.user.reward_point_redemptions.select_for_update())
100+
assert self.locked
70101

71102
created = []
72103
for form in self.forms:

evap/rewards/views.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from django.contrib import messages
55
from django.contrib.messages.views import SuccessMessageMixin
66
from django.core.exceptions import BadRequest, SuspiciousOperation
7-
from django.db import transaction
87
from django.db.models import Sum
98
from django.http import HttpResponse
109
from django.shortcuts import get_object_or_404, redirect, render
@@ -33,16 +32,15 @@
3332
def index(request):
3433
status = 200
3534

36-
with transaction.atomic():
37-
total_points_available = reward_points_of_user(request.user)
38-
events = RewardPointRedemptionEvent.objects.filter(redeem_end_date__gte=datetime.now().date()).order_by("date")
35+
events = RewardPointRedemptionEvent.objects.filter(redeem_end_date__gte=datetime.now().date()).order_by("date")
3936

40-
# pylint: disable=unexpected-keyword-arg
41-
formset = RewardPointRedemptionFormSet(
42-
request.POST or None,
43-
initial=[{"event": e, "points": 0, "total_points_available": total_points_available} for e in events],
44-
user=request.user,
45-
)
37+
# pylint: disable=unexpected-keyword-arg
38+
formset = RewardPointRedemptionFormSet(
39+
request.POST or None,
40+
initial=[{"event": e, "points": 0} for e in events],
41+
user=request.user,
42+
)
43+
with formset.lock():
4644
if request.method == "POST":
4745
try:
4846
previous_redeemed_points = int(request.POST["previous_redeemed_points"])

0 commit comments

Comments
 (0)