feat: Phase 1+2 - JWT auth, dashboard metrics API, DB indexes
Phase 1 - JWT Auth Foundation: - Replace token auth with djangorestframework-simplejwt - POST /api/v1/admin/auth/login/ - returns access + refresh JWT - POST /api/v1/auth/refresh/ - JWT refresh - GET /api/v1/auth/me/ - current admin profile - GET /api/v1/health/ - DB health check - Add ledger app to INSTALLED_APPS Phase 2 - Dashboard Metrics API: - GET /api/v1/dashboard/metrics/ - revenue, partners, events, tickets - GET /api/v1/dashboard/revenue/ - 7-day revenue vs payouts chart data - GET /api/v1/dashboard/activity/ - last 10 platform events feed - GET /api/v1/dashboard/actions/ - KYC queue, flagged events, pending payouts DB Indexes (dashboard query optimisation): - RazorpayTransaction: status, captured_at - Partner: status, kyc_compliance_status - Event: event_status, start_date, created_date - Booking: created_date - PaymentTransaction: payment_type, payment_transaction_status, payment_transaction_date Infra: - Add Dockerfile for eventify-backend container - Add simplejwt to requirements.txt - All 4 dashboard views use IsAuthenticated permission class
This commit is contained in:
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y gcc libpq-dev && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY requirements-docker.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements-docker.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN python manage.py collectstatic --noinput || true
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "--timeout", "120", "eventify.wsgi:application"]
|
||||||
@@ -7,4 +7,9 @@ urlpatterns = [
|
|||||||
path('auth/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
path('auth/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||||
path('auth/me/', views.MeView.as_view(), name='auth_me'),
|
path('auth/me/', views.MeView.as_view(), name='auth_me'),
|
||||||
path('health/', views.HealthView.as_view(), name='health'),
|
path('health/', views.HealthView.as_view(), name='health'),
|
||||||
|
# Phase 2: Dashboard endpoints
|
||||||
|
path('dashboard/metrics/', views.DashboardMetricsView.as_view(), name='dashboard-metrics'),
|
||||||
|
path('dashboard/revenue/', views.DashboardRevenueView.as_view(), name='dashboard-revenue'),
|
||||||
|
path('dashboard/activity/', views.DashboardActivityView.as_view(), name='dashboard-activity'),
|
||||||
|
path('dashboard/actions/', views.DashboardActionsView.as_view(), name='dashboard-actions'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -50,3 +50,239 @@ class HealthView(APIView):
|
|||||||
except Exception:
|
except Exception:
|
||||||
db_status = 'error'
|
db_status = 'error'
|
||||||
return Response({'status': 'ok', 'db': db_status})
|
return Response({'status': 'ok', 'db': db_status})
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase 2: Dashboard Views
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class DashboardMetricsView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
from ledger.models import RazorpayTransaction
|
||||||
|
from partner.models import Partner
|
||||||
|
from events.models import Event
|
||||||
|
from bookings.models import Ticket, Booking
|
||||||
|
from django.db.models import Sum
|
||||||
|
from django.utils import timezone
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
today = timezone.now().date()
|
||||||
|
|
||||||
|
# --- Revenue ---
|
||||||
|
total_paise = (
|
||||||
|
RazorpayTransaction.objects
|
||||||
|
.filter(status='captured')
|
||||||
|
.aggregate(total=Sum('amount'))['total'] or 0
|
||||||
|
)
|
||||||
|
total_revenue = total_paise / 100
|
||||||
|
|
||||||
|
# This-month / last-month revenue for growth
|
||||||
|
first_of_this_month = today.replace(day=1)
|
||||||
|
first_of_last_month = (first_of_this_month - datetime.timedelta(days=1)).replace(day=1)
|
||||||
|
|
||||||
|
this_month_paise = (
|
||||||
|
RazorpayTransaction.objects
|
||||||
|
.filter(status='captured', captured_at__date__gte=first_of_this_month)
|
||||||
|
.aggregate(total=Sum('amount'))['total'] or 0
|
||||||
|
)
|
||||||
|
last_month_paise = (
|
||||||
|
RazorpayTransaction.objects
|
||||||
|
.filter(
|
||||||
|
status='captured',
|
||||||
|
captured_at__date__gte=first_of_last_month,
|
||||||
|
captured_at__date__lt=first_of_this_month,
|
||||||
|
)
|
||||||
|
.aggregate(total=Sum('amount'))['total'] or 0
|
||||||
|
)
|
||||||
|
this_month_rev = this_month_paise / 100
|
||||||
|
last_month_rev = last_month_paise / 100
|
||||||
|
revenue_growth = (
|
||||||
|
round((this_month_rev - last_month_rev) / last_month_rev * 100, 2)
|
||||||
|
if last_month_rev else 0
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Partners ---
|
||||||
|
active_partners = Partner.objects.filter(status='active').count()
|
||||||
|
pending_partners = Partner.objects.filter(kyc_compliance_status='pending').count()
|
||||||
|
|
||||||
|
# --- Events ---
|
||||||
|
live_events = Event.objects.filter(event_status='live').count()
|
||||||
|
events_today = Event.objects.filter(start_date=today).count()
|
||||||
|
|
||||||
|
# --- Tickets ---
|
||||||
|
ticket_sales = Ticket.objects.count()
|
||||||
|
|
||||||
|
# This-week / last-week ticket growth
|
||||||
|
week_start = today - datetime.timedelta(days=today.weekday()) # Monday
|
||||||
|
last_week_start = week_start - datetime.timedelta(days=7)
|
||||||
|
|
||||||
|
this_week_tickets = Ticket.objects.filter(
|
||||||
|
booking__created_date__gte=week_start
|
||||||
|
).count()
|
||||||
|
last_week_tickets = Ticket.objects.filter(
|
||||||
|
booking__created_date__gte=last_week_start,
|
||||||
|
booking__created_date__lt=week_start,
|
||||||
|
).count()
|
||||||
|
ticket_growth = (
|
||||||
|
round((this_week_tickets - last_week_tickets) / last_week_tickets * 100, 2)
|
||||||
|
if last_week_tickets else 0
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'totalRevenue': total_revenue,
|
||||||
|
'revenueGrowth': revenue_growth,
|
||||||
|
'activePartners': active_partners,
|
||||||
|
'pendingPartners': pending_partners,
|
||||||
|
'liveEvents': live_events,
|
||||||
|
'eventsToday': events_today,
|
||||||
|
'ticketSales': ticket_sales,
|
||||||
|
'ticketGrowth': ticket_growth,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardRevenueView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
from ledger.models import RazorpayTransaction
|
||||||
|
from banking_operations.models import PaymentTransaction
|
||||||
|
from django.db.models import Sum
|
||||||
|
from django.utils import timezone
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
today = timezone.now().date()
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for i in range(6, -1, -1):
|
||||||
|
day = today - datetime.timedelta(days=i)
|
||||||
|
|
||||||
|
rev_paise = (
|
||||||
|
RazorpayTransaction.objects
|
||||||
|
.filter(status='captured', captured_at__date=day)
|
||||||
|
.aggregate(total=Sum('amount'))['total'] or 0
|
||||||
|
)
|
||||||
|
revenue = rev_paise / 100
|
||||||
|
|
||||||
|
payouts = (
|
||||||
|
PaymentTransaction.objects
|
||||||
|
.filter(
|
||||||
|
payment_type='debit',
|
||||||
|
payment_transaction_status='completed',
|
||||||
|
payment_transaction_date=day,
|
||||||
|
)
|
||||||
|
.aggregate(total=Sum('payment_transaction_amount'))['total'] or 0
|
||||||
|
)
|
||||||
|
|
||||||
|
result.append({
|
||||||
|
'day': day.strftime('%a'),
|
||||||
|
'revenue': float(revenue),
|
||||||
|
'payouts': float(payouts),
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response(result)
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardActivityView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
from partner.models import Partner
|
||||||
|
from events.models import Event
|
||||||
|
from bookings.models import Booking
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
items = []
|
||||||
|
|
||||||
|
# --- Partners (last 5 by id desc; no date field — timestamp=None, filtered out later) ---
|
||||||
|
for p in Partner.objects.order_by('-id')[:5]:
|
||||||
|
items.append({
|
||||||
|
'id': f'partner-{p.id}',
|
||||||
|
'type': 'partner',
|
||||||
|
'title': f'{p.name} registered',
|
||||||
|
'description': f'New partner — {getattr(p, "partner_type", "individual")}',
|
||||||
|
'timestamp': None,
|
||||||
|
'status': p.kyc_compliance_status,
|
||||||
|
})
|
||||||
|
|
||||||
|
# --- Events (last 5 by created_date desc) ---
|
||||||
|
for e in Event.objects.order_by('-created_date')[:5]:
|
||||||
|
display_name = e.title if getattr(e, 'title', None) else getattr(e, 'name', '')
|
||||||
|
ts = e.created_date
|
||||||
|
items.append({
|
||||||
|
'id': f'event-{e.id}',
|
||||||
|
'type': 'event',
|
||||||
|
'title': f'{display_name} created',
|
||||||
|
'description': f'Event status: {e.event_status}',
|
||||||
|
'timestamp': ts.isoformat() if ts else None,
|
||||||
|
'status': e.event_status,
|
||||||
|
})
|
||||||
|
|
||||||
|
# --- Bookings (last 5 by id desc) ---
|
||||||
|
for b in Booking.objects.order_by('-id')[:5]:
|
||||||
|
ts = b.created_date
|
||||||
|
items.append({
|
||||||
|
'id': f'booking-{b.booking_id}',
|
||||||
|
'type': 'booking',
|
||||||
|
'title': f'Booking {b.booking_id} placed',
|
||||||
|
'description': f'{b.quantity} ticket(s) at ₹{b.price}',
|
||||||
|
'timestamp': ts.isoformat() if ts else None,
|
||||||
|
'status': 'confirmed',
|
||||||
|
})
|
||||||
|
|
||||||
|
# Filter out items with no timestamp, sort desc, return top 10
|
||||||
|
dated = [item for item in items if item['timestamp'] is not None]
|
||||||
|
dated.sort(key=lambda x: x['timestamp'], reverse=True)
|
||||||
|
|
||||||
|
return Response(dated[:10])
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardActionsView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
from partner.models import Partner
|
||||||
|
from events.models import Event
|
||||||
|
from banking_operations.models import PaymentTransaction
|
||||||
|
from django.db.models import Sum
|
||||||
|
|
||||||
|
kyc_count = Partner.objects.filter(kyc_compliance_status='pending').count()
|
||||||
|
flagged_count = Event.objects.filter(event_status='flagged').count()
|
||||||
|
pending_payouts = float(
|
||||||
|
PaymentTransaction.objects
|
||||||
|
.filter(payment_type='debit', payment_transaction_status='pending')
|
||||||
|
.aggregate(total=Sum('payment_transaction_amount'))['total'] or 0
|
||||||
|
)
|
||||||
|
|
||||||
|
actions = [
|
||||||
|
{
|
||||||
|
'id': 'kyc',
|
||||||
|
'type': 'kyc',
|
||||||
|
'count': kyc_count,
|
||||||
|
'title': 'Partner Approval Queue',
|
||||||
|
'description': f'{kyc_count} partners awaiting KYC review',
|
||||||
|
'href': '/partners?filter=pending',
|
||||||
|
'priority': 'high',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'flagged',
|
||||||
|
'type': 'flagged',
|
||||||
|
'count': flagged_count,
|
||||||
|
'title': 'Flagged Events',
|
||||||
|
'description': f'{flagged_count} events reported for review',
|
||||||
|
'href': '/events?filter=flagged',
|
||||||
|
'priority': 'high',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'payout',
|
||||||
|
'type': 'payout',
|
||||||
|
'count': pending_payouts,
|
||||||
|
'title': 'Pending Payouts',
|
||||||
|
'description': f'₹{pending_payouts:,.0f} ready for release',
|
||||||
|
'href': '/financials?tab=payouts',
|
||||||
|
'priority': 'medium',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return Response(actions)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class PaymentGatewayCredentials(models.Model):
|
|||||||
|
|
||||||
class PaymentTransaction(models.Model):
|
class PaymentTransaction(models.Model):
|
||||||
payment_transaction_id = models.CharField(max_length=250)
|
payment_transaction_id = models.CharField(max_length=250)
|
||||||
payment_type = models.CharField(max_length=250, choices=[
|
payment_type = models.CharField(max_length=250, db_index=True, choices=[
|
||||||
('credit', 'Credit'),
|
('credit', 'Credit'),
|
||||||
('debit', 'Debit'),
|
('debit', 'Debit'),
|
||||||
('transfer', 'Transfer'),
|
('transfer', 'Transfer'),
|
||||||
@@ -58,14 +58,14 @@ class PaymentTransaction(models.Model):
|
|||||||
payment_gateway = models.ForeignKey(PaymentGateway, on_delete=models.CASCADE)
|
payment_gateway = models.ForeignKey(PaymentGateway, on_delete=models.CASCADE)
|
||||||
payment_transaction_amount = models.DecimalField(max_digits=10, decimal_places=2)
|
payment_transaction_amount = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
payment_transaction_currency = models.CharField(max_length=10)
|
payment_transaction_currency = models.CharField(max_length=10)
|
||||||
payment_transaction_status = models.CharField(max_length=250, choices=[
|
payment_transaction_status = models.CharField(max_length=250, db_index=True, choices=[
|
||||||
('pending', 'Pending'),
|
('pending', 'Pending'),
|
||||||
('completed', 'Completed'),
|
('completed', 'Completed'),
|
||||||
('failed', 'Failed'),
|
('failed', 'Failed'),
|
||||||
('refunded', 'Refunded'),
|
('refunded', 'Refunded'),
|
||||||
('cancelled', 'Cancelled'),
|
('cancelled', 'Cancelled'),
|
||||||
])
|
])
|
||||||
payment_transaction_date = models.DateField(auto_now_add=True)
|
payment_transaction_date = models.DateField(auto_now_add=True, db_index=True)
|
||||||
payment_transaction_time = models.TimeField(auto_now_add=True)
|
payment_transaction_time = models.TimeField(auto_now_add=True)
|
||||||
payment_transaction_notes = models.TextField(blank=True, null=True)
|
payment_transaction_notes = models.TextField(blank=True, null=True)
|
||||||
payment_transaction_raw_data = models.JSONField(blank=True, null=True)
|
payment_transaction_raw_data = models.JSONField(blank=True, null=True)
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class Booking(models.Model):
|
|||||||
ticket_type = models.ForeignKey(TicketType, on_delete=models.CASCADE)
|
ticket_type = models.ForeignKey(TicketType, on_delete=models.CASCADE)
|
||||||
quantity = models.IntegerField()
|
quantity = models.IntegerField()
|
||||||
price = models.DecimalField(max_digits=10, decimal_places=2)
|
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
created_date = models.DateField(auto_now_add=True)
|
created_date = models.DateField(auto_now_add=True, db_index=True)
|
||||||
updated_date = models.DateField(auto_now=True)
|
updated_date = models.DateField(auto_now=True)
|
||||||
|
|
||||||
transaction_id = models.CharField(max_length=250, blank=True, null=True)
|
transaction_id = models.CharField(max_length=250, blank=True, null=True)
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ INSTALLED_APPS = [
|
|||||||
'rest_framework.authtoken',
|
'rest_framework.authtoken',
|
||||||
'rest_framework_simplejwt',
|
'rest_framework_simplejwt',
|
||||||
'admin_api',
|
'admin_api',
|
||||||
'django_summernote'
|
'django_summernote',
|
||||||
|
'ledger',
|
||||||
]
|
]
|
||||||
|
|
||||||
INSTALLED_APPS += [
|
INSTALLED_APPS += [
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ from partner.models import Partner
|
|||||||
|
|
||||||
|
|
||||||
class Event(models.Model):
|
class Event(models.Model):
|
||||||
created_date = models.DateField(auto_now_add=True)
|
created_date = models.DateField(auto_now_add=True, db_index=True)
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
description = models.TextField()
|
description = models.TextField()
|
||||||
start_date = models.DateField(blank=True, null=True)
|
start_date = models.DateField(blank=True, null=True, db_index=True)
|
||||||
end_date = models.DateField(blank=True, null=True)
|
end_date = models.DateField(blank=True, null=True)
|
||||||
start_time = models.TimeField(blank=True, null=True)
|
start_time = models.TimeField(blank=True, null=True)
|
||||||
end_time = models.TimeField(blank=True, null=True)
|
end_time = models.TimeField(blank=True, null=True)
|
||||||
@@ -42,7 +42,7 @@ class Event(models.Model):
|
|||||||
('published', 'Published'),
|
('published', 'Published'),
|
||||||
('live', 'Live'),
|
('live', 'Live'),
|
||||||
('flagged', 'Flagged'),
|
('flagged', 'Flagged'),
|
||||||
], default='pending')
|
], default='pending', db_index=True)
|
||||||
cancelled_reason = models.TextField(default='NA')
|
cancelled_reason = models.TextField(default='NA')
|
||||||
|
|
||||||
title = models.CharField(max_length=250, blank=True)
|
title = models.CharField(max_length=250, blank=True)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from django.views import generic
|
from django.views import generic
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
from django.db.models import Q
|
||||||
from .models import Event
|
from .models import Event
|
||||||
from .models import EventImages
|
from .models import EventImages
|
||||||
from .forms import EventForm
|
from .forms import EventForm
|
||||||
@@ -18,6 +19,17 @@ class EventListView(LoginRequiredMixin, generic.ListView):
|
|||||||
template_name = 'events/event_list.html'
|
template_name = 'events/event_list.html'
|
||||||
paginate_by = 10
|
paginate_by = 10
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = super().get_queryset()
|
||||||
|
query = self.request.GET.get('q')
|
||||||
|
if query:
|
||||||
|
queryset = queryset.filter(
|
||||||
|
Q(name__icontains=query) |
|
||||||
|
Q(district__icontains=query) |
|
||||||
|
Q(state__icontains=query)
|
||||||
|
)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class EventCreateView(LoginRequiredMixin, generic.CreateView):
|
class EventCreateView(LoginRequiredMixin, generic.CreateView):
|
||||||
model = Event
|
model = Event
|
||||||
@@ -91,5 +103,3 @@ def delete_event_image(request, pk, img_id):
|
|||||||
image.delete()
|
image.delete()
|
||||||
messages.success(request, "Image deleted!")
|
messages.success(request, "Image deleted!")
|
||||||
return redirect("events:event_images", pk=pk)
|
return redirect("events:event_images", pk=pk)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class RazorpayTransaction(models.Model):
|
|||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
help_text="created/authorized/captured/failed/refunded",
|
help_text="created/authorized/captured/failed/refunded",
|
||||||
|
db_index=True,
|
||||||
)
|
)
|
||||||
method = models.CharField(
|
method = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
@@ -59,7 +60,7 @@ class RazorpayTransaction(models.Model):
|
|||||||
# Timestamps
|
# Timestamps
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
captured_at = models.DateTimeField(blank=True, null=True)
|
captured_at = models.DateTimeField(blank=True, null=True, db_index=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.razorpay_payment_id or self.razorpay_order_id} - {self.status}"
|
return f"{self.razorpay_payment_id or self.razorpay_order_id} - {self.status}"
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class Partner(models.Model):
|
|||||||
primary_contact_person_email = models.EmailField()
|
primary_contact_person_email = models.EmailField()
|
||||||
primary_contact_person_phone = models.CharField(max_length=15)
|
primary_contact_person_phone = models.CharField(max_length=15)
|
||||||
|
|
||||||
status = models.CharField(max_length=250, choices=STATUS_CHOICES, default='active')
|
status = models.CharField(max_length=250, choices=STATUS_CHOICES, default='active', db_index=True)
|
||||||
|
|
||||||
address = models.TextField(blank=True, null=True)
|
address = models.TextField(blank=True, null=True)
|
||||||
city = models.CharField(max_length=250, blank=True, null=True)
|
city = models.CharField(max_length=250, blank=True, null=True)
|
||||||
@@ -58,7 +58,7 @@ class Partner(models.Model):
|
|||||||
longitude = models.DecimalField(max_digits=9, decimal_places=6, blank=True, null=True)
|
longitude = models.DecimalField(max_digits=9, decimal_places=6, blank=True, null=True)
|
||||||
|
|
||||||
is_kyc_compliant = models.BooleanField(default=False)
|
is_kyc_compliant = models.BooleanField(default=False)
|
||||||
kyc_compliance_status = models.CharField(max_length=250, choices=KYC_COMPLIANCE_STATUS_CHOICES, default='pending')
|
kyc_compliance_status = models.CharField(max_length=250, choices=KYC_COMPLIANCE_STATUS_CHOICES, default='pending', db_index=True)
|
||||||
kyc_compliance_reason = models.TextField(blank=True, null=True)
|
kyc_compliance_reason = models.TextField(blank=True, null=True)
|
||||||
kyc_compliance_document_type = models.CharField(max_length=250, choices=KYC_DOCUMENT_TYPE_CHOICES, blank=True, null=True)
|
kyc_compliance_document_type = models.CharField(max_length=250, choices=KYC_DOCUMENT_TYPE_CHOICES, blank=True, null=True)
|
||||||
kyc_compliance_document_other_type = models.CharField(max_length=250, blank=True, null=True)
|
kyc_compliance_document_other_type = models.CharField(max_length=250, blank=True, null=True)
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
Django>=4.2
|
Django>=4.2
|
||||||
Pillow
|
Pillow
|
||||||
|
django-summernote
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset='utf-8'>
|
<meta charset='utf-8'>
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1'>
|
<meta name='viewport' content='width=device-width, initial-scale=1'>
|
||||||
<title>Eventify</title>
|
<title>Eventify</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<!-- jQuery required for Summernote -->
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="{% url 'accounts:dashboard' %}">Eventify</a>
|
<a class="navbar-brand" href="{% url 'accounts:dashboard' %}">Eventify</a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navmenu">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navmenu">
|
||||||
@@ -54,22 +58,24 @@
|
|||||||
{{ user.email }}
|
{{ user.email }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a></li>
|
</a></li>
|
||||||
<li class="nav-item"><a class="nav-link text-danger" href="{% url 'accounts:logout' %}">Logout</a></li>
|
<li class="nav-item"><a class="nav-link text-danger" href="{% url 'accounts:logout' %}">Logout</a>
|
||||||
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="nav-item"><a class="nav-link" href="{% url 'accounts:login' %}">Login</a></li>
|
<li class="nav-item"><a class="nav-link" href="{% url 'accounts:login' %}">Login</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
|
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
<h3>{% if object %}Edit{% else %}Add{% endif %} Event</h3>
|
<h3>{% if object %}Edit{% else %}Add{% endif %} Event</h3>
|
||||||
|
|
||||||
<form method="post" novalidate>
|
<form method="post" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{{ form.media }}
|
||||||
|
|
||||||
{% for field in form %}
|
{% for field in form %}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -19,10 +20,10 @@
|
|||||||
<button class="btn btn-primary">Save</button>
|
<button class="btn btn-primary">Save</button>
|
||||||
<a class="btn btn-secondary" href="{% url 'events:event_list' %}">Cancel</a>
|
<a class="btn btn-secondary" href="{% url 'events:event_list' %}">Cancel</a>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const allYearEventCheckbox = document.getElementById('id_all_year_event');
|
const allYearEventCheckbox = document.getElementById('id_all_year_event');
|
||||||
const startDateField = document.getElementById('id_start_date');
|
const startDateField = document.getElementById('id_start_date');
|
||||||
const endDateField = document.getElementById('id_end_date');
|
const endDateField = document.getElementById('id_end_date');
|
||||||
@@ -45,5 +46,5 @@
|
|||||||
allYearEventCheckbox.addEventListener('change', toggleDateTimeFields);
|
allYearEventCheckbox.addEventListener('change', toggleDateTimeFields);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,9 +1,20 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex justify-content-between mb-3">
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
<h3>Events</h3>
|
<h3>Events</h3>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<form method="get" action="." class="d-flex">
|
||||||
|
<input class="form-control me-2" type="search" name="q" placeholder="Search events..." aria-label="Search" value="{{ request.GET.q }}">
|
||||||
|
<button class="btn btn-outline-success" type="submit">Search</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 text-end">
|
||||||
<a class="btn btn-success" href="{% url 'events:event_add' %}">Add Event</a>
|
<a class="btn btn-success" href="{% url 'events:event_add' %}">Add Event</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -44,4 +55,35 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
{% if is_paginated %}
|
||||||
|
<nav aria-label="Page navigation">
|
||||||
|
<ul class="pagination">
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page=1{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}">« First</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}">Previous</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<span class="page-link">
|
||||||
|
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}">Next</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}">Last »</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user