feat(accounts): home district with 6-month cooldown
- accounts/models.py: add district_changed_at DateTimeField + VALID_DISTRICTS constant (14 Kerala districts) - migration 0013_user_district_changed_at: nullable DateTimeField, no backfill - WebRegisterForm: accept optional district during signup, stamp district_changed_at - UpdateProfileView: enforce 183-day cooldown with human-readable error - LoginView/WebRegisterView/StatusView: include district_changed_at in responses Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
13
CHANGELOG.md
13
CHANGELOG.md
@@ -5,6 +5,19 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), version
|
||||
|
||||
---
|
||||
|
||||
## [1.7.0] — 2026-04-04
|
||||
|
||||
### Added
|
||||
- **Home District with 6-month cooldown**
|
||||
- `district_changed_at` DateTimeField on User model (migration `0013_user_district_changed_at`) — nullable, no backfill; NULL means "eligible to change immediately"
|
||||
- `VALID_DISTRICTS` constant (14 Kerala districts) in `accounts/models.py` for server-side validation
|
||||
- `WebRegisterForm` now accepts optional `district` field; stamps `district_changed_at` on valid selection during signup
|
||||
- `UpdateProfileView` enforces 183-day (~6 months) cooldown — rejects district changes within the window with a human-readable "Next change: {date}" error
|
||||
- `district_changed_at` included in all relevant API responses: `LoginView`, `WebRegisterView`, `StatusView`, `UpdateProfileView`
|
||||
- `StatusView` now also returns `district` field (was previously missing)
|
||||
|
||||
---
|
||||
|
||||
## [1.6.2] — 2026-04-03
|
||||
|
||||
### Security
|
||||
|
||||
16
accounts/migrations/0013_user_district_changed_at.py
Normal file
16
accounts/migrations/0013_user_district_changed_at.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0012_user_eventify_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='district_changed_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -24,6 +24,12 @@ ROLE_CHOICES = (
|
||||
('partner_customer', 'Partner Customer'),
|
||||
)
|
||||
|
||||
VALID_DISTRICTS = [
|
||||
"Thiruvananthapuram", "Kollam", "Pathanamthitta", "Alappuzha", "Kottayam",
|
||||
"Idukki", "Ernakulam", "Thrissur", "Palakkad", "Malappuram",
|
||||
"Kozhikode", "Wayanad", "Kannur", "Kasaragod",
|
||||
]
|
||||
|
||||
class User(AbstractUser):
|
||||
eventify_id = models.CharField(
|
||||
max_length=12,
|
||||
@@ -49,6 +55,7 @@ class User(AbstractUser):
|
||||
state = models.CharField(max_length=100, blank=True, null=True)
|
||||
country = models.CharField(max_length=100, blank=True, null=True)
|
||||
place = models.CharField(max_length=200, blank=True, null=True)
|
||||
district_changed_at = models.DateTimeField(blank=True, null=True)
|
||||
|
||||
# Location fields
|
||||
latitude = models.DecimalField(max_digits=9, decimal_places=6, blank=True, null=True)
|
||||
|
||||
@@ -45,7 +45,7 @@ class WebRegisterForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['first_name', 'last_name', 'email', 'phone_number', 'password', 'confirm_password']
|
||||
fields = ['first_name', 'last_name', 'email', 'phone_number', 'password', 'confirm_password', 'district']
|
||||
|
||||
def clean_email(self):
|
||||
email = self.cleaned_data.get('email')
|
||||
@@ -76,6 +76,12 @@ class WebRegisterForm(forms.ModelForm):
|
||||
# Mark as a customer / end-user
|
||||
user.is_customer = True
|
||||
user.role = 'customer'
|
||||
from django.utils import timezone
|
||||
from accounts.models import VALID_DISTRICTS
|
||||
if user.district and user.district in VALID_DISTRICTS:
|
||||
user.district_changed_at = timezone.now()
|
||||
elif user.district:
|
||||
user.district = None # reject invalid district silently
|
||||
if commit:
|
||||
user.save()
|
||||
return user
|
||||
|
||||
@@ -58,6 +58,11 @@ class WebRegisterView(View):
|
||||
'username': user.username,
|
||||
'email': user.email,
|
||||
'phone_number': user.phone_number,
|
||||
'district': user.district or '',
|
||||
'district_changed_at': user.district_changed_at.isoformat() if user.district_changed_at else None,
|
||||
'first_name': user.first_name,
|
||||
'last_name': user.last_name,
|
||||
'eventify_id': user.eventify_id or '',
|
||||
}
|
||||
return JsonResponse(response, status=201)
|
||||
log("warning", "Web registration failed", request=request, logger_data=dict(errors=form.errors))
|
||||
@@ -93,6 +98,7 @@ class LoginView(View):
|
||||
'role': user.role,
|
||||
'pincode': user.pincode,
|
||||
'district': user.district,
|
||||
'district_changed_at': user.district_changed_at.isoformat() if user.district_changed_at else None,
|
||||
'state': user.state,
|
||||
'country': user.country,
|
||||
'place': user.place,
|
||||
@@ -124,6 +130,8 @@ class StatusView(View):
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"eventify_id": user.eventify_id or '',
|
||||
"district": user.district or '',
|
||||
"district_changed_at": user.district_changed_at.isoformat() if user.district_changed_at else None,
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
@@ -245,15 +253,33 @@ class UpdateProfileView(View):
|
||||
user.pincode = None
|
||||
updated_fields.append('pincode')
|
||||
|
||||
# Update district
|
||||
# Update district (with 6-month cooldown)
|
||||
if 'district' in json_data:
|
||||
district = json_data.get('district', '').strip()
|
||||
if district:
|
||||
user.district = district
|
||||
updated_fields.append('district')
|
||||
elif district == '':
|
||||
user.district = None
|
||||
updated_fields.append('district')
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
from accounts.models import VALID_DISTRICTS
|
||||
|
||||
COOLDOWN = timedelta(days=183) # ~6 months
|
||||
new_district = json_data.get('district', '').strip()
|
||||
|
||||
if new_district and new_district not in VALID_DISTRICTS:
|
||||
errors['district'] = 'Invalid district.'
|
||||
elif new_district and new_district != (user.district or ''):
|
||||
if user.district_changed_at and timezone.now() < user.district_changed_at + COOLDOWN:
|
||||
next_date = (user.district_changed_at + COOLDOWN).strftime('%d %b %Y')
|
||||
errors['district'] = f'District can only be changed once every 6 months. Next change: {next_date}.'
|
||||
else:
|
||||
user.district = new_district
|
||||
user.district_changed_at = timezone.now()
|
||||
updated_fields.append('district')
|
||||
elif new_district == '' and user.district:
|
||||
if user.district_changed_at and timezone.now() < user.district_changed_at + COOLDOWN:
|
||||
next_date = (user.district_changed_at + COOLDOWN).strftime('%d %b %Y')
|
||||
errors['district'] = f'District can only be changed once every 6 months. Next change: {next_date}.'
|
||||
else:
|
||||
user.district = None
|
||||
user.district_changed_at = timezone.now()
|
||||
updated_fields.append('district')
|
||||
|
||||
# Update state
|
||||
if 'state' in json_data:
|
||||
@@ -318,6 +344,7 @@ class UpdateProfileView(View):
|
||||
'phone_number': user.phone_number,
|
||||
'pincode': user.pincode,
|
||||
'district': user.district,
|
||||
'district_changed_at': user.district_changed_at.isoformat() if user.district_changed_at else None,
|
||||
'state': user.state,
|
||||
'country': user.country,
|
||||
'place': user.place,
|
||||
|
||||
Reference in New Issue
Block a user