Compare commits
13 Commits
4a9f754fda
...
sprint/7-c
| Author | SHA1 | Date | |
|---|---|---|---|
| b6c2b93fd0 | |||
| fee67385d5 | |||
| f587c4dd24 | |||
| 4669907a02 | |||
| 611d653938 | |||
| 16c21c17d2 | |||
| 761b702e57 | |||
| 46b391bd51 | |||
| d9a2af7168 | |||
| f75d4f2915 | |||
| 05de552820 | |||
| f85188ca6b | |||
| 64ff08b2b2 |
20
CHANGELOG.md
20
CHANGELOG.md
@@ -5,6 +5,26 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), version
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [1.14.2] — 2026-04-22
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Admin "Login as Partner" impersonation now completes into `/dashboard`** instead of bouncing back to `/login?error=ImpersonationFailed`. Two linked issues:
|
||||||
|
- **`ALLOWED_HOSTS`** (`eventify/settings.py`) — partner portal's server-side `authorize()` (Next.js) calls `${BACKEND_API_URL}/api/v1/auth/me/` with `BACKEND_API_URL=http://eventify-backend:8000`, so the HTTP `Host` header was `eventify-backend` — not in the Django allowlist. `SecurityMiddleware` rejected with HTTP 400 DisallowedHost, `authorize()` returned null, `signIn()` failed, and the `/impersonate` page redirected to the login error. Added `partner.eventifyplus.com`, `eventify-backend`, and `eventify-django` to `ALLOWED_HOSTS`. Same Host issue was silently breaking regular partner password login too — fixed as a side effect.
|
||||||
|
- **`UserSerializer` missing `partner` field** (`admin_api/serializers.py`) — `MeView` returned `/api/v1/auth/me/` payload with no `partner` key, so the partner portal's `auth.ts` set `partnerId: ""` on the NextAuth session. Downstream dashboard queries that filter by `partnerId` would then return empty/403. Added `partner = PrimaryKeyRelatedField(read_only=True)` to the serializer's `fields` list. Payload now includes `"partner": <id>`.
|
||||||
|
- Deploy: `docker cp` both files into **both** `eventify-backend` and `eventify-django` containers + `kill -HUP 1` on each (per shared admin_api rule).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.14.1] — 2026-04-22
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **`_serialize_review()` now returns `profile_photo`** (`mobile_api/views/reviews.py`) — `/api/reviews/list` payload was missing the reviewer's photo URL, so the consumer app had no choice but to render DiceBear placeholders for every reviewer regardless of whether they had uploaded a real profile picture
|
||||||
|
- Resolves `r.reviewer.profile_picture.url` when the field is non-empty and the file name is not `default.png` (the model's placeholder default); returns empty string otherwise so the frontend can fall back cleanly to DiceBear
|
||||||
|
- Mirrors the existing pattern in `mobile_api/views/user.py` (`LoginView`, `StatusView`, `UpdateProfileView`) — same defensive try/except around FK dereference
|
||||||
|
- Pure serializer change — no migration, no URL change, no permission change; `gunicorn kill -HUP 1` picks it up
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.14.0] — 2026-04-21
|
## [1.14.0] — 2026-04-21
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ User = get_user_model()
|
|||||||
class UserSerializer(serializers.ModelSerializer):
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
name = serializers.SerializerMethodField()
|
name = serializers.SerializerMethodField()
|
||||||
role = serializers.SerializerMethodField()
|
role = serializers.SerializerMethodField()
|
||||||
|
partner = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = ['id', 'email', 'username', 'name', 'role']
|
fields = ['id', 'email', 'username', 'name', 'role', 'partner']
|
||||||
def get_name(self, obj):
|
def get_name(self, obj):
|
||||||
return f"{obj.first_name} {obj.last_name}".strip() or obj.username
|
return f"{obj.first_name} {obj.last_name}".strip() or obj.username
|
||||||
def get_role(self, obj):
|
def get_role(self, obj):
|
||||||
|
|||||||
@@ -18,8 +18,30 @@ urlpatterns = [
|
|||||||
path('partners/<int:pk>/', views.PartnerDetailView.as_view(), name='partner-detail'),
|
path('partners/<int:pk>/', views.PartnerDetailView.as_view(), name='partner-detail'),
|
||||||
path('partners/<int:pk>/status/', views.PartnerStatusView.as_view(), name='partner-status'),
|
path('partners/<int:pk>/status/', views.PartnerStatusView.as_view(), name='partner-status'),
|
||||||
path('partners/<int:pk>/kyc/review/', views.PartnerKYCReviewView.as_view(), name='partner-kyc-review'),
|
path('partners/<int:pk>/kyc/review/', views.PartnerKYCReviewView.as_view(), name='partner-kyc-review'),
|
||||||
|
path('partners/<int:pk>/impersonate/', views.PartnerImpersonateView.as_view(), name='partner-impersonate'),
|
||||||
path('partners/onboard/', views.PartnerOnboardView.as_view(), name='partner-onboard'),
|
path('partners/onboard/', views.PartnerOnboardView.as_view(), name='partner-onboard'),
|
||||||
path('partners/<int:partner_id>/staff/', views.PartnerStaffCreateView.as_view(), name='partner-staff-create'),
|
path('partners/<int:partner_id>/staff/', views.PartnerStaffCreateView.as_view(), name='partner-staff-create'),
|
||||||
|
# Partner-Me: partner portal self-service (Sprint 1)
|
||||||
|
path('partners/me/profile/', views.PartnerMeProfileView.as_view(), name='partner-me-profile'),
|
||||||
|
path('partners/me/notifications/', views.PartnerMeNotificationsView.as_view(), name='partner-me-notifications'),
|
||||||
|
path('partners/me/payout/', views.PartnerMePayoutView.as_view(), name='partner-me-payout'),
|
||||||
|
path('partners/me/change-password/', views.PartnerMeChangePasswordView.as_view(), name='partner-me-change-password'),
|
||||||
|
# Partner-Me: events (Sprint 2)
|
||||||
|
path('partners/me/events/', views.PartnerMeEventsView.as_view(), name='partner-me-events'),
|
||||||
|
path('partners/me/events/<int:pk>/', views.PartnerMeEventDetailView.as_view(), name='partner-me-event-detail'),
|
||||||
|
path('partners/me/events/<int:pk>/duplicate/', views.PartnerMeEventDuplicateView.as_view(), name='partner-me-event-duplicate'),
|
||||||
|
# Partner-Me: ticket tiers (Sprint 3)
|
||||||
|
path('partners/me/events/<int:event_pk>/tiers/', views.PartnerMeEventTiersView.as_view(), name='partner-me-event-tiers'),
|
||||||
|
path('partners/me/events/<int:event_pk>/tiers/<int:tier_pk>/', views.PartnerMeEventTierDetailView.as_view(), name='partner-me-event-tier-detail'),
|
||||||
|
# Partner-Me: bookings (Sprint 4)
|
||||||
|
path('partners/me/bookings/', views.PartnerBookingListView.as_view(), name='partner-me-bookings'),
|
||||||
|
# Partner-Me: customers (Sprint 5)
|
||||||
|
path('partners/me/customers/', views.PartnerCustomerListView.as_view(), name='partner-me-customers'),
|
||||||
|
# Partner-Me: staff CRUD (Sprint 6)
|
||||||
|
path('partners/me/staff/', views.PartnerMeStaffListView.as_view(), name='partner-me-staff-list'),
|
||||||
|
path('partners/me/staff/<int:pk>/', views.PartnerMeStaffDetailView.as_view(), name='partner-me-staff-detail'),
|
||||||
|
# Partner-Me: check-in (Sprint 7)
|
||||||
|
path('partners/me/check-in/', views.PartnerMeCheckInView.as_view(), name='partner-me-check-in'),
|
||||||
path('users/metrics/', views.UserMetricsView.as_view(), name='user-metrics'),
|
path('users/metrics/', views.UserMetricsView.as_view(), name='user-metrics'),
|
||||||
path('users/', views.UserListView.as_view(), name='user-list'),
|
path('users/', views.UserListView.as_view(), name='user-list'),
|
||||||
path('users/<int:pk>/', views.UserDetailView.as_view(), name='user-detail'),
|
path('users/<int:pk>/', views.UserDetailView.as_view(), name='user-detail'),
|
||||||
|
|||||||
1006
admin_api/views.py
1006
admin_api/views.py
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,9 @@ ALLOWED_HOSTS = [
|
|||||||
'backend.eventifyplus.com',
|
'backend.eventifyplus.com',
|
||||||
'admin.eventifyplus.com',
|
'admin.eventifyplus.com',
|
||||||
'app.eventifyplus.com',
|
'app.eventifyplus.com',
|
||||||
|
'partner.eventifyplus.com',
|
||||||
|
'eventify-backend',
|
||||||
|
'eventify-django',
|
||||||
'localhost',
|
'localhost',
|
||||||
'127.0.0.1',
|
'127.0.0.1',
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -29,11 +29,20 @@ def _serialize_review(r, user_interactions=None):
|
|||||||
uname = r.reviewer.username
|
uname = r.reviewer.username
|
||||||
except Exception:
|
except Exception:
|
||||||
uname = ''
|
uname = ''
|
||||||
|
try:
|
||||||
|
pic = r.reviewer.profile_picture
|
||||||
|
if pic and pic.name and 'default.png' not in pic.name:
|
||||||
|
profile_photo = pic.url
|
||||||
|
else:
|
||||||
|
profile_photo = ''
|
||||||
|
except Exception:
|
||||||
|
profile_photo = ''
|
||||||
return {
|
return {
|
||||||
'id': r.id,
|
'id': r.id,
|
||||||
'event_id': r.event_id,
|
'event_id': r.event_id,
|
||||||
'username': uname,
|
'username': uname,
|
||||||
'display_name': display,
|
'display_name': display,
|
||||||
|
'profile_photo': profile_photo,
|
||||||
'rating': r.rating,
|
'rating': r.rating,
|
||||||
'comment': r.review_text,
|
'comment': r.review_text,
|
||||||
'status': _STATUS_TO_JSON.get(r.status, r.status),
|
'status': _STATUS_TO_JSON.get(r.status, r.status),
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('partner', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# Profile extras
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='partner',
|
||||||
|
name='bio',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
|
||||||
|
# Payout settings
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='partner',
|
||||||
|
name='payout_account_holder_name',
|
||||||
|
field=models.CharField(blank=True, max_length=250, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='partner',
|
||||||
|
name='payout_account_number',
|
||||||
|
field=models.CharField(blank=True, max_length=50, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='partner',
|
||||||
|
name='payout_ifsc_code',
|
||||||
|
field=models.CharField(blank=True, max_length=20, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='partner',
|
||||||
|
name='payout_bank_name',
|
||||||
|
field=models.CharField(blank=True, max_length=250, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='partner',
|
||||||
|
name='payout_schedule',
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[('weekly', 'Weekly'), ('biweekly', 'Bi-weekly'), ('monthly', 'Monthly')],
|
||||||
|
default='monthly',
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
# Notification preferences
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='partner',
|
||||||
|
name='notif_new_booking',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='partner',
|
||||||
|
name='notif_event_status',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='partner',
|
||||||
|
name='notif_payout_update',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='partner',
|
||||||
|
name='notif_weekly_report',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -65,5 +65,28 @@ class Partner(models.Model):
|
|||||||
kyc_compliance_document_file = models.FileField(upload_to='kyc_documents/', blank=True, null=True)
|
kyc_compliance_document_file = models.FileField(upload_to='kyc_documents/', blank=True, null=True)
|
||||||
kyc_compliance_document_number = models.CharField(max_length=250, blank=True, null=True)
|
kyc_compliance_document_number = models.CharField(max_length=250, blank=True, null=True)
|
||||||
|
|
||||||
|
# Profile extras
|
||||||
|
bio = models.TextField(blank=True, null=True)
|
||||||
|
|
||||||
|
# Payout settings
|
||||||
|
PAYOUT_SCHEDULE_CHOICES = (
|
||||||
|
('weekly', 'Weekly'),
|
||||||
|
('biweekly', 'Bi-weekly'),
|
||||||
|
('monthly', 'Monthly'),
|
||||||
|
)
|
||||||
|
payout_account_holder_name = models.CharField(max_length=250, blank=True, null=True)
|
||||||
|
payout_account_number = models.CharField(max_length=50, blank=True, null=True)
|
||||||
|
payout_ifsc_code = models.CharField(max_length=20, blank=True, null=True)
|
||||||
|
payout_bank_name = models.CharField(max_length=250, blank=True, null=True)
|
||||||
|
payout_schedule = models.CharField(
|
||||||
|
max_length=20, choices=PAYOUT_SCHEDULE_CHOICES, default='monthly'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Notification preferences
|
||||||
|
notif_new_booking = models.BooleanField(default=True)
|
||||||
|
notif_event_status = models.BooleanField(default=True)
|
||||||
|
notif_payout_update = models.BooleanField(default=True)
|
||||||
|
notif_weekly_report = models.BooleanField(default=False)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|||||||
Reference in New Issue
Block a user