diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a40fa8..32e8358 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/accounts/migrations/0013_user_district_changed_at.py b/accounts/migrations/0013_user_district_changed_at.py new file mode 100644 index 0000000..370dbfe --- /dev/null +++ b/accounts/migrations/0013_user_district_changed_at.py @@ -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), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index fe9ffc3..4a573f0 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -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) diff --git a/mobile_api/forms/user_forms.py b/mobile_api/forms/user_forms.py index 805d2ce..28a64f3 100644 --- a/mobile_api/forms/user_forms.py +++ b/mobile_api/forms/user_forms.py @@ -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 diff --git a/mobile_api/views/user.py b/mobile_api/views/user.py index 064e77e..129cd24 100644 --- a/mobile_api/views/user.py +++ b/mobile_api/views/user.py @@ -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,