The updates for the customer dashboard cum accounts
This commit is contained in:
62
accounts/customer_forms.py
Normal file
62
accounts/customer_forms.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
|
||||
class RegisterForm(UserCreationForm):
|
||||
full_name = forms.CharField(max_length=150, required=False, label="Full name")
|
||||
email = forms.EmailField(required=True, label="Email")
|
||||
|
||||
class Meta:
|
||||
model = get_user_model()
|
||||
fields = ("username", "full_name", "email", "password1", "password2")
|
||||
|
||||
def clean_email(self):
|
||||
email = self.cleaned_data.get("email")
|
||||
user = get_user_model()
|
||||
if user.objects.filter(email__iexact=email).exists():
|
||||
raise forms.ValidationError("A user with that email already exists.")
|
||||
return email
|
||||
|
||||
def save(self, commit=True):
|
||||
user = super().save(commit=False)
|
||||
# Save full name to first_name/last_name if possible
|
||||
full_name = self.cleaned_data.get("full_name", "").strip()
|
||||
if full_name:
|
||||
parts = full_name.split(None, 1)
|
||||
user.first_name = parts[0]
|
||||
if len(parts) > 1:
|
||||
user.last_name = parts[1]
|
||||
user.email = self.cleaned_data["email"]
|
||||
user.is_active = True # create inactive until email verified
|
||||
if commit:
|
||||
user.save()
|
||||
return user
|
||||
|
||||
|
||||
class CustomerLoginForm(AuthenticationForm):
|
||||
"""
|
||||
Wrapper around Django's AuthenticationForm to customize widgets.
|
||||
"""
|
||||
username = forms.CharField(
|
||||
label="Email or Username",
|
||||
widget=forms.TextInput(attrs={
|
||||
"autofocus": True,
|
||||
"placeholder": "Email or username",
|
||||
"class": "input",
|
||||
"autocomplete": "username",
|
||||
})
|
||||
)
|
||||
|
||||
password = forms.CharField(
|
||||
label="Password",
|
||||
strip=False,
|
||||
widget=forms.PasswordInput(attrs={
|
||||
"placeholder": "Enter your password",
|
||||
"class": "input",
|
||||
"autocomplete": "current-password",
|
||||
})
|
||||
)
|
||||
130
accounts/customer_views.py
Normal file
130
accounts/customer_views.py
Normal file
@@ -0,0 +1,130 @@
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import login as auth_login
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
from django.shortcuts import render, redirect
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
|
||||
from django.utils.encoding import force_bytes, force_str
|
||||
from django.views import View
|
||||
from django.views.generic import FormView
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
|
||||
from django.contrib.auth import authenticate, login
|
||||
from django.shortcuts import render
|
||||
|
||||
from .customer_forms import RegisterForm, CustomerLoginForm
|
||||
|
||||
from django.contrib.auth import logout
|
||||
from django.shortcuts import redirect
|
||||
from django.contrib import messages
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
from master_data.models import EventType
|
||||
from events.models import Event
|
||||
|
||||
from django.db.models import Prefetch
|
||||
from events.models import EventImages
|
||||
|
||||
from django.forms.models import model_to_dict
|
||||
# from utils.date_convertor import convert_date_to_dd_mm_yyyy
|
||||
|
||||
def send_verification_email(request, user):
|
||||
"""
|
||||
Renders and sends verification email with activation link.
|
||||
Requires EMAIL_BACKEND configured.
|
||||
"""
|
||||
current_site = get_current_site(request)
|
||||
subject = "Verify your email for {}".format(current_site.name)
|
||||
token = default_token_generator.make_token(user)
|
||||
uid = urlsafe_base64_encode(force_bytes(user.pk))
|
||||
activate_path = reverse_lazy('accounts:login', kwargs={'uidb64': uid, 'token': token})
|
||||
activate_url = request.build_absolute_uri(activate_path)
|
||||
message = render_to_string('auth/email_verification_email.txt', {
|
||||
'user': user,
|
||||
'activate_url': activate_url,
|
||||
'domain': current_site.domain,
|
||||
})
|
||||
user.email_user(subject, message)
|
||||
|
||||
|
||||
class RegisterView(FormView):
|
||||
template_name = "customer/customer_registration.html"
|
||||
form_class = RegisterForm
|
||||
success_url = reverse_lazy('login')
|
||||
|
||||
def form_valid(self, form):
|
||||
user = form.save()
|
||||
# send_verification_email(self.request, user)
|
||||
messages.success(self.request, "Account created. Kindly login to continue.")
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class EmailVerificationSentView(View):
|
||||
template_name = "auth/email_verification.html"
|
||||
|
||||
def get(self, request):
|
||||
return render(request, self.template_name)
|
||||
|
||||
|
||||
class ActivateAccountView(View):
|
||||
"""
|
||||
Activation link view: sets user.is_active=True if token valid.
|
||||
"""
|
||||
def get(self, request, uidb64, token):
|
||||
try:
|
||||
uid = force_str(urlsafe_base64_decode(uidb64))
|
||||
user = User.objects.get(pk=uid)
|
||||
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
|
||||
user = None
|
||||
|
||||
if user is not None and default_token_generator.check_token(user, token):
|
||||
user.is_active = True
|
||||
user.save()
|
||||
messages.success(request, "Email verified — you can now sign in.")
|
||||
return redirect('login')
|
||||
else:
|
||||
return render(request, 'auth/activation_invalid.html')
|
||||
|
||||
|
||||
def login_view(request):
|
||||
if request.method == "POST":
|
||||
form = CustomerLoginForm(request, data=request.POST)
|
||||
if form.is_valid():
|
||||
user = form.get_user()
|
||||
login(request, user)
|
||||
return redirect('customer_dashboard')
|
||||
else:
|
||||
form = CustomerLoginForm(request)
|
||||
return render(request, 'customer/customer_login.html', {'form': form})
|
||||
|
||||
@login_required(login_url="login")
|
||||
def customer_dashboard(request):
|
||||
event_types = EventType.objects.all()
|
||||
|
||||
events = Event.objects.all()
|
||||
|
||||
events_dict = [model_to_dict(obj) for obj in events]
|
||||
|
||||
for event in events_dict:
|
||||
event['event_image'] = EventImages.objects.get(event=event['id'], is_primary=True).event_image.url
|
||||
# event['start_date'] = convert_date_to_dd_mm_yyyy(event['start_date'])
|
||||
|
||||
print('*' * 10)
|
||||
print(events_dict)
|
||||
print('*' * 10)
|
||||
|
||||
context = {
|
||||
'event_types': event_types,
|
||||
'events': events_dict,
|
||||
}
|
||||
return render(request, "customer/customer_dashboard.html", context)
|
||||
|
||||
def logout_view(request):
|
||||
logout(request)
|
||||
messages.success(request, "You have been logged out successfully.")
|
||||
return redirect("login")
|
||||
@@ -10,6 +10,11 @@ User = get_user_model()
|
||||
|
||||
|
||||
class UserForm(forms.ModelForm):
|
||||
full_name = forms.CharField(
|
||||
max_length=150,
|
||||
required=True,
|
||||
label="Full Name"
|
||||
)
|
||||
password = forms.CharField(
|
||||
widget=forms.PasswordInput,
|
||||
label="Password"
|
||||
@@ -39,7 +44,7 @@ class UserForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["username", "email", "phone_number", "role", "password", "confirm_password"]
|
||||
fields = ["username","full_name", "email", "phone_number", "role", "password", "confirm_password"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Generated by Django 5.0 on 2025-12-08 04:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0002_alter_user_managers_alter_user_role'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='country',
|
||||
field=models.CharField(blank=True, max_length=100, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='district',
|
||||
field=models.CharField(blank=True, max_length=100, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='latitude',
|
||||
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='longitude',
|
||||
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='pincode',
|
||||
field=models.CharField(blank=True, max_length=10, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='place',
|
||||
field=models.CharField(blank=True, max_length=200, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='state',
|
||||
field=models.CharField(blank=True, max_length=100, null=True),
|
||||
),
|
||||
]
|
||||
18
accounts/migrations/0004_alter_user_role.py
Normal file
18
accounts/migrations/0004_alter_user_role.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0 on 2025-12-08 20:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0003_user_country_user_district_user_latitude_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='role',
|
||||
field=models.CharField(choices=[('Admin', 'admin'), ('Manager', 'manager'), ('Staff', 'staff')], default='Staff', max_length=20),
|
||||
),
|
||||
]
|
||||
18
accounts/migrations/0005_alter_user_role.py
Normal file
18
accounts/migrations/0005_alter_user_role.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0 on 2025-12-08 21:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0004_alter_user_role'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='role',
|
||||
field=models.CharField(choices=[('admin', 'Admin'), ('manager', 'Manager'), ('staff', 'Staff')], default='Staff', max_length=20),
|
||||
),
|
||||
]
|
||||
@@ -4,9 +4,9 @@ from django.db import models
|
||||
from accounts.manager import UserManager
|
||||
|
||||
ROLE_CHOICES = (
|
||||
('Admin', 'Admin'),
|
||||
('Manager', 'Manager'),
|
||||
('Staff', 'Staff'),
|
||||
('admin', 'Admin'),
|
||||
('manager', 'Manager'),
|
||||
('staff', 'Staff'),
|
||||
)
|
||||
|
||||
|
||||
@@ -18,6 +18,17 @@ class User(AbstractUser):
|
||||
is_customer = models.BooleanField(default=False)
|
||||
is_user = models.BooleanField(default=False)
|
||||
|
||||
# Location fields
|
||||
pincode = models.CharField(max_length=10, blank=True, null=True)
|
||||
district = models.CharField(max_length=100, blank=True, null=True)
|
||||
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)
|
||||
|
||||
# Location fields
|
||||
latitude = 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)
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -4,6 +4,9 @@ from . import views
|
||||
app_name = 'accounts'
|
||||
|
||||
urlpatterns = [
|
||||
path('login/', views.login_view, name='login'),
|
||||
path('logout/', views.logout_view, name='logout'),
|
||||
path('dashboard/', views.dashboard, name='dashboard'),
|
||||
path('users/', views.UserListView.as_view(), name='user_list'),
|
||||
path('users/add/', views.UserCreateView.as_view(), name='user_add'),
|
||||
path('users/<int:pk>/edit/', views.UserUpdateView.as_view(), name='user_edit'),
|
||||
|
||||
@@ -53,7 +53,7 @@ class UserDeleteView(LoginRequiredMixin, generic.DeleteView):
|
||||
|
||||
def login_view(request):
|
||||
if request.user.is_authenticated:
|
||||
return redirect("dashboard") # Redirect authenticated user
|
||||
return redirect("accounts:dashboard") # Redirect authenticated user
|
||||
|
||||
form = LoginForm(request, data=request.POST or None)
|
||||
|
||||
@@ -61,7 +61,10 @@ def login_view(request):
|
||||
if form.is_valid():
|
||||
user = form.get_user()
|
||||
login(request, user)
|
||||
return redirect("dashboard")
|
||||
if user.role == 'admin' or user.role == 'manager' or user.role == 'staff':
|
||||
return redirect("accounts:dashboard")
|
||||
else:
|
||||
messages.error(request, "You are not authorized to access this page.")
|
||||
else:
|
||||
messages.error(request, "Invalid username or password")
|
||||
|
||||
@@ -70,4 +73,5 @@ def login_view(request):
|
||||
|
||||
def logout_view(request):
|
||||
logout(request)
|
||||
return redirect("login")
|
||||
messages.success(request, "You have been logged out successfully.")
|
||||
return redirect("accounts:login")
|
||||
0
bookings/__init__.py
Normal file
0
bookings/__init__.py
Normal file
3
bookings/admin.py
Normal file
3
bookings/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
bookings/apps.py
Normal file
6
bookings/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BookingsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'bookings'
|
||||
0
bookings/migrations/__init__.py
Normal file
0
bookings/migrations/__init__.py
Normal file
3
bookings/models.py
Normal file
3
bookings/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
3
bookings/tests.py
Normal file
3
bookings/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
bookings/views.py
Normal file
3
bookings/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -104,3 +104,6 @@ AUTH_USER_MODEL = 'accounts.User'
|
||||
LOGIN_URL = 'login'
|
||||
LOGIN_REDIRECT_URL = 'dashboard'
|
||||
LOGOUT_REDIRECT_URL = 'login'
|
||||
|
||||
# EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
# DEFAULT_FROM_EMAIL = 'no-reply@example.com'
|
||||
@@ -2,18 +2,17 @@ from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.contrib.auth import views as auth_views
|
||||
from accounts.views import dashboard
|
||||
from accounts.views import login_view
|
||||
from accounts.views import logout_view
|
||||
from accounts.customer_views import RegisterView
|
||||
from accounts.customer_views import login_view, logout_view, customer_dashboard
|
||||
from django.conf.urls.static import static
|
||||
from django.conf import settings
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
# path('', auth_views.LoginView.as_view(template_name='accounts/login.html'), name='login'),
|
||||
# path('logout/', auth_views.LogoutView.as_view(), name='logout'),
|
||||
path("", login_view, name="login"),
|
||||
path("logout/", logout_view, name="logout"),
|
||||
path('dashboard/', dashboard, name='dashboard'),
|
||||
path("register/", RegisterView.as_view(), name="register"),
|
||||
path('dashboard/', customer_dashboard, name='customer_dashboard'),
|
||||
|
||||
path('master-data/', include('master_data.urls')),
|
||||
path('events/', include('events.urls')),
|
||||
|
||||
@@ -10,6 +10,10 @@ from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
|
||||
from django.http import JsonResponse
|
||||
from .models import Event
|
||||
|
||||
|
||||
class EventListView(LoginRequiredMixin, generic.ListView):
|
||||
model = Event
|
||||
context_object_name = 'events'
|
||||
@@ -89,3 +93,5 @@ def delete_event_image(request, pk, img_id):
|
||||
image.delete()
|
||||
messages.success(request, "Image deleted!")
|
||||
return redirect("events:event_images", pk=pk)
|
||||
|
||||
|
||||
|
||||
18
master_data/migrations/0002_eventtype_event_type_icon.py
Normal file
18
master_data/migrations/0002_eventtype_event_type_icon.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0 on 2025-12-08 16:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('master_data', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='eventtype',
|
||||
name='event_type_icon',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='event_type_icons/'),
|
||||
),
|
||||
]
|
||||
@@ -2,6 +2,7 @@ from django.db import models
|
||||
|
||||
class EventType(models.Model):
|
||||
event_type = models.CharField(max_length=50, null=False, blank=False)
|
||||
event_type_icon = models.ImageField(upload_to='event_type_icons/', null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.event_type
|
||||
|
||||
@@ -17,4 +17,5 @@ urlpatterns += [
|
||||
path('events/pincode-events/', EventListAPI.as_view()),
|
||||
path('events/event-details/', EventDetailAPI.as_view()),
|
||||
path('events/event-images/', EventImagesListAPI.as_view()),
|
||||
path('events/events-by-category/<int:slug>/', api_events_by_category, name='api_events_by_category'),
|
||||
]
|
||||
|
||||
@@ -10,6 +10,8 @@ from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
import json
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class EventTypeListAPIView(APIView):
|
||||
@@ -214,3 +216,16 @@ class EventImagesListAPI(APIView):
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": str(e)},
|
||||
)
|
||||
|
||||
|
||||
@login_required(login_url="login")
|
||||
def api_events_by_category(request, slug):
|
||||
events = Event.objects.filter(event_type=slug)
|
||||
|
||||
events_dict = [model_to_dict(obj) for obj in events]
|
||||
|
||||
for event in events_dict:
|
||||
event['event_image'] = EventImages.objects.get(event=event['id'], is_primary=True).event_image.url
|
||||
# event['start_date'] = convert_date_to_dd_mm_yyyy(event['start_date'])
|
||||
|
||||
return JsonResponse({"events": events_dict})
|
||||
BIN
templates/.DS_Store
vendored
Normal file
BIN
templates/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -9,7 +9,7 @@
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{% url '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">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
<!-- Accessible by Admin, Manager, Staff -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'dashboard' %}">Dashboard</a>
|
||||
<a class="nav-link" href="{% url 'accounts:dashboard' %}">Dashboard</a>
|
||||
</li>
|
||||
|
||||
{% if user.role == "admin" or user.role == "manager" %}
|
||||
@@ -46,9 +46,9 @@
|
||||
<ul class="navbar-nav">
|
||||
{% if user.is_authenticated %}
|
||||
<li class="nav-item"><a class="nav-link" href="#">{{ user.username }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link text-danger" href="{% url 'logout' %}">Logout</a></li>
|
||||
<li class="nav-item"><a class="nav-link text-danger" href="{% url 'accounts:logout' %}">Logout</a></li>
|
||||
{% else %}
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'login' %}">Login</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'accounts:login' %}">Login</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
BIN
templates/customer/.DS_Store
vendored
Normal file
BIN
templates/customer/.DS_Store
vendored
Normal file
Binary file not shown.
86
templates/customer/base_auth.html
Normal file
86
templates/customer/base_auth.html
Normal file
@@ -0,0 +1,86 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<title>{% block title %}Auth{% endblock %}</title>
|
||||
<style>
|
||||
/* GLOBAL */
|
||||
:root{
|
||||
--blue1:#1e3cfa;
|
||||
--blue2:#3c63ff;
|
||||
--muted:#f4f6ff;
|
||||
--card-bg:#fff;
|
||||
--accent: #3c63ff;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;font-family:Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;background:var(--muted);color:#111}
|
||||
.auth-wrapper{display:flex;min-height:100vh}
|
||||
.auth-left{
|
||||
width:40%;
|
||||
min-width:320px;
|
||||
background:linear-gradient(180deg,var(--blue1),var(--blue2));
|
||||
color:#fff;padding:48px;display:flex;flex-direction:column;justify-content:center;gap:10px;
|
||||
}
|
||||
.brand{font-weight:700;font-size:28px}
|
||||
.auth-left h1{font-size:36px;margin:0}
|
||||
.auth-left p{opacity:.92;margin:0;font-size:16px}
|
||||
|
||||
.auth-right{flex:1;display:flex;align-items:center;justify-content:center;padding:30px}
|
||||
.auth-card{width:100%;max-width:480px;background:var(--card-bg);border-radius:16px;padding:36px;box-shadow:0 10px 30px rgba(16,24,40,0.08)}
|
||||
|
||||
h2{margin:0 0 6px 0;font-size:24px}
|
||||
.subtitle{color:#666;font-size:14px;margin-bottom:18px}
|
||||
|
||||
.input-group{margin-bottom:14px}
|
||||
.input-group label{display:block;font-size:13px;margin-bottom:8px;color:#333}
|
||||
.input-group input{width:100%;padding:12px;border-radius:10px;border:1.5px solid #e6eefc;background:#f7f9ff;outline:none;transition:.15s}
|
||||
.input-group input:focus{border-color:var(--accent);background:#fff;box-shadow:0 0 0 4px rgba(60,99,255,0.06)}
|
||||
|
||||
.btn-primary{display:inline-block;width:100%;padding:12px;border-radius:10px;border:none;background:var(--accent);color:#fff;font-weight:600;cursor:pointer}
|
||||
.btn-primary:hover{filter:brightness(.95)}
|
||||
|
||||
.form-links, .redirect{text-align:right;margin-top:10px;font-size:13px}
|
||||
a.link{color:var(--accent);text-decoration:none}
|
||||
.redirect{text-align:center;font-size:14px;margin-top:18px}
|
||||
|
||||
/* messages */
|
||||
.messages{margin-bottom:12px}
|
||||
.message{padding:10px;border-radius:8px;margin-bottom:8px;font-size:14px}
|
||||
.message.info{background:#eef5ff;color:#024}
|
||||
.message.success{background:#ecfdf5;color:#064}
|
||||
.message.warning{background:#fff7e6;color:#7a4b00}
|
||||
.errorlist{color:#b00020;margin:6px 0 0 0;font-size:13px}
|
||||
|
||||
/* responsive */
|
||||
@media (max-width:900px){
|
||||
.auth-wrapper{flex-direction:column}
|
||||
.auth-left{width:100%;min-height:180px;padding:28px;text-align:center}
|
||||
.auth-right{padding:20px}
|
||||
.auth-card{border-radius:14px;padding:28px}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-wrapper">
|
||||
<div class="auth-left">
|
||||
<div class="brand">Eventify</div>
|
||||
<p>{% block left_subtext %}Your events at your fingertips.{% endblock %}</p>
|
||||
</div>
|
||||
|
||||
<div class="auth-right">
|
||||
<div class="auth-card">
|
||||
{% if messages %}
|
||||
<div class="messages">
|
||||
{% for m in messages %}
|
||||
<div class="message {{ m.tags }}">{{ m }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
205
templates/customer/base_dashboard.html
Normal file
205
templates/customer/base_dashboard.html
Normal file
@@ -0,0 +1,205 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Dashboard{% endblock %}</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Inter", sans-serif;
|
||||
background: #f5f7ff;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* SIDEBAR */
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
background: linear-gradient(180deg, #1e3cfa, #1436c7);
|
||||
color: white;
|
||||
padding: 30px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 12px 15px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 8px;
|
||||
font-size: 15px;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.nav-item:hover,
|
||||
.nav-item.active {
|
||||
background: white;
|
||||
color: #1436c7;
|
||||
}
|
||||
|
||||
.bottom-nav {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
/* MAIN CONTENT AREA */
|
||||
.content {
|
||||
flex: 1;
|
||||
background: #f5f7ff;
|
||||
padding: 0;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
/* TOPBAR */
|
||||
.topbar {
|
||||
background: white;
|
||||
padding: 20px 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #e6e9f5;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
width: 350px;
|
||||
padding: 12px 14px;
|
||||
border: 1.5px solid #dfe3f8;
|
||||
border-radius: 12px;
|
||||
outline: none;
|
||||
background: #f8faff;
|
||||
}
|
||||
|
||||
.profile-section {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 50%;
|
||||
background: #e6e6e6;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.profile-avatar i {
|
||||
font-size: 20px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* DROPDOWN MENU */
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 55px;
|
||||
right: 0;
|
||||
background: white;
|
||||
width: 160px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 12px;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
padding: 8px 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 12px 15px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background: #f1f4ff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="layout">
|
||||
|
||||
<!-- SIDEBAR -->
|
||||
<div class="sidebar">
|
||||
<div class="logo">EVENTIFY</div>
|
||||
|
||||
<div class="nav">
|
||||
<div class="nav-item active">🏠 Home</div>
|
||||
<div class="nav-item">📅 Calendar</div>
|
||||
<div class="nav-item">👤 Profile</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-nav">
|
||||
<div class="nav-item">❓ Help</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MAIN CONTENT -->
|
||||
<div class="content">
|
||||
<div class="topbar">
|
||||
<div class="search-box">
|
||||
<input type="text" placeholder="Search">
|
||||
</div>
|
||||
|
||||
<div class="profile-section" id="profileDropdownBtn">
|
||||
<div class="profile-avatar">
|
||||
<i class="fa fa-user"></i>
|
||||
</div>
|
||||
|
||||
<!-- DROPDOWN -->
|
||||
<div class="dropdown-menu" id="dropdownMenu">
|
||||
<div class="dropdown-item" onclick="window.location.href='{% url 'logout' %}'">Logout</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const dropdownBtn = document.getElementById("profileDropdownBtn");
|
||||
const dropdownMenu = document.getElementById("dropdownMenu");
|
||||
|
||||
dropdownBtn.addEventListener("click", () => {
|
||||
dropdownMenu.style.display =
|
||||
dropdownMenu.style.display === "flex" ? "none" : "flex";
|
||||
});
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!dropdownBtn.contains(e.target)) {
|
||||
dropdownMenu.style.display = "none";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
17
templates/customer/cusotmer_email_verification.html
Normal file
17
templates/customer/cusotmer_email_verification.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends "base_auth.html" %}
|
||||
{% block title %}Verify Email{% endblock %}
|
||||
{% block left_heading %}Verify your email{% endblock %}
|
||||
{% block left_subtext %}A verification link has been sent to your email{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Verify Your Email</h2>
|
||||
<p class="subtitle">Please check your inbox and click the verification link</p>
|
||||
|
||||
<div style="font-size:14px;color:#444;margin-top:12px;">
|
||||
If you don't receive the email within a few minutes, check your spam folder.
|
||||
</div>
|
||||
|
||||
<div class="redirect">
|
||||
<a class="link" href="{% url 'myapp:login' %}">Back to Login</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
203
templates/customer/customer_dashboard.html
Normal file
203
templates/customer/customer_dashboard.html
Normal file
@@ -0,0 +1,203 @@
|
||||
{% extends "customer/base_dashboard.html" %}
|
||||
{% block title %}Dashboard{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<style>
|
||||
.welcome-box {
|
||||
background: #3c63ff;
|
||||
color: white;
|
||||
border-radius: 18px;
|
||||
padding: 25px;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.welcome-text h2 {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.event-cards-wrapper {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.event-card {
|
||||
background: white;
|
||||
padding: 10px 15px;
|
||||
border-radius: 14px;
|
||||
width: 280px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.event-img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: gray;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.categories {
|
||||
margin: 30px 0 10px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cat {
|
||||
background: #f1f3ff;
|
||||
padding: 8px 15px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.cat.active {
|
||||
background: #3c63ff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.event-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 22px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
padding: 12px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.card img {
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
height: 170px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
margin: 8px 0 5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-details {
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.cat-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dashboard-container {
|
||||
padding: 30px 40px;
|
||||
max-width: 1200px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="dashboard-container">
|
||||
<!-- WELCOME BANNER -->
|
||||
<div class="welcome-box">
|
||||
<div class="welcome-text">
|
||||
<p>Welcome Back,</p>
|
||||
<h2>{{ request.user.first_name|default:"Jane Doe" }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Events Around You</h3>
|
||||
|
||||
<!-- CATEGORY LIST -->
|
||||
<div class="categories">
|
||||
{% for category in event_types %}
|
||||
<div class="cat" data-slug="{{ category.id }}">
|
||||
{% if category.event_type_icon %}
|
||||
<img src="{{ category.event_type_icon.url }}" alt="{{ category.event_type }} icon" class="cat-icon">
|
||||
{% endif %}
|
||||
{{ category.event_type }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- EVENT GRID -->
|
||||
<div id="event-grid" class="event-grid">
|
||||
{% for event in events %}
|
||||
<div class="card">
|
||||
|
||||
{% if event.event_image %}
|
||||
<img src="{{ event.event_image }}" alt="">
|
||||
{% else %}
|
||||
<img src="/static/default.jpg" alt="">
|
||||
{% endif %}
|
||||
|
||||
<div class="card-title">{{ event.title }}</div>
|
||||
<div class="card-details">
|
||||
📅 {{ mydate|date:"d-m-Y" }}<br>
|
||||
📍 {{ event.place }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
const categories = document.querySelectorAll(".cat");
|
||||
const eventGrid = document.getElementById("event-grid");
|
||||
|
||||
categories.forEach(cat => {
|
||||
cat.addEventListener("click", function () {
|
||||
|
||||
categories.forEach(c => c.classList.remove("active"));
|
||||
this.classList.add("active");
|
||||
|
||||
const slug = this.getAttribute("data-slug");
|
||||
const url = `/api/events/events-by-category/${slug}/`;
|
||||
|
||||
eventGrid.innerHTML = "<p>Loading...</p>";
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
eventGrid.innerHTML = "";
|
||||
|
||||
if (data.events.length === 0) {
|
||||
eventGrid.innerHTML = "<p>No events found for this category.</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
data.events.forEach(event => {
|
||||
const card = `
|
||||
<div class="card">
|
||||
<img src="${event.event_image}" alt="">
|
||||
<div class="card-title">${event.title}</div>
|
||||
<div class="card-details">
|
||||
📅 ${event.start_date}<br>
|
||||
📍 ${event.place}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
eventGrid.innerHTML += card;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
35
templates/customer/customer_login.html
Normal file
35
templates/customer/customer_login.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{% extends "customer/base_auth.html" %}
|
||||
{% block title %}Login{% endblock %}
|
||||
{% block left_heading %}Welcome Back{% endblock %}
|
||||
{% block left_subtext %}Login to access your events dashboard{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Sign In</h2>
|
||||
<p class="subtitle">Enter your credentials to continue</p>
|
||||
|
||||
<form method="post" action="{% url 'login' %}">
|
||||
{% csrf_token %}
|
||||
<div class="input-group">
|
||||
<label for="id_username">Email or Username</label>
|
||||
<input id="id_username" name="username" type="text" required>
|
||||
{% if form.username.errors %}
|
||||
<div class="errorlist">{{ form.username.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="id_password">Password</label>
|
||||
<input id="id_password" name="password" type="password" required>
|
||||
{% if form.password.errors %}
|
||||
<div class="errorlist">{{ form.password.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
<button class="btn-primary" type="submit">Login</button>
|
||||
</form>
|
||||
|
||||
<div class="redirect">
|
||||
Don't have an account? <a class="link" href="{% url 'register' %}">Sign Up</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
30
templates/customer/customer_password_confimation.html
Normal file
30
templates/customer/customer_password_confimation.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{% extends "base_auth.html" %}
|
||||
{% block title %}Choose New Password{% endblock %}
|
||||
{% block left_heading %}Choose new password{% endblock %}
|
||||
{% block left_subtext %}Create a new secure password for your account{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Reset Password</h2>
|
||||
<p class="subtitle">Enter your new password</p>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.non_field_errors }}
|
||||
<div class="input-group">
|
||||
{{ form.new_password1.label_tag }}
|
||||
{{ form.new_password1 }}
|
||||
{% if form.new_password1.errors %}<div class="errorlist">{{ form.new_password1.errors }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="input-group">
|
||||
{{ form.new_password2.label_tag }}
|
||||
{{ form.new_password2 }}
|
||||
{% if form.new_password2.errors %}<div class="errorlist">{{ form.new_password2.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<button class="btn-primary" type="submit">Reset Password</button>
|
||||
</form>
|
||||
|
||||
<div class="redirect">
|
||||
Back to <a class="link" href="{% url 'login' %}">Login</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
23
templates/customer/customer_password_reset.html
Normal file
23
templates/customer/customer_password_reset.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% extends "auth/base_auth.html" %}
|
||||
{% block title %}Forgot Password{% endblock %}
|
||||
{% block left_heading %}Reset Password{% endblock %}
|
||||
{% block left_subtext %}Enter your email to receive a reset link{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Forgot Password</h2>
|
||||
<p class="subtitle">Enter your email to receive a reset link</p>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="input-group">
|
||||
<label for="id_email">Email</label>
|
||||
<input id="id_email" name="email" type="email" required>
|
||||
</div>
|
||||
|
||||
<button class="btn-primary" type="submit">Send Reset Link</button>
|
||||
</form>
|
||||
|
||||
<div class="redirect">
|
||||
Remember your password? <a class="link" href="{% url 'login' %}">Login</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
49
templates/customer/customer_registration.html
Normal file
49
templates/customer/customer_registration.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{% extends "customer/base_auth.html" %}
|
||||
{% block title %}Register{% endblock %}
|
||||
{% block left_heading %}Create Account{% endblock %}
|
||||
{% block left_subtext %}Join and explore exciting events{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Create Account</h2>
|
||||
<p class="subtitle">Join and explore exciting events</p>
|
||||
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
{{ form.non_field_errors }}
|
||||
<div class="input-group">
|
||||
{{ form.full_name.label_tag }}
|
||||
{{ form.full_name }}
|
||||
{% if form.full_name.errors %}<div class="errorlist">{{ form.full_name.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
{{ form.email.label_tag }}
|
||||
{{ form.email }}
|
||||
{% if form.email.errors %}<div class="errorlist">{{ form.email.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
{{ form.username.label_tag }}
|
||||
{{ form.username }}
|
||||
{% if form.username.errors %}<div class="errorlist">{{ form.username.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
{{ form.password1.label_tag }}
|
||||
{{ form.password1 }}
|
||||
{% if form.password1.errors %}<div class="errorlist">{{ form.password1.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
{{ form.password2.label_tag }}
|
||||
{{ form.password2 }}
|
||||
{% if form.password2.errors %}<div class="errorlist">{{ form.password2.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<button class="btn-primary" type="submit">Create Account</button>
|
||||
</form>
|
||||
|
||||
<div class="redirect">
|
||||
Already have an account? <a class="link" href="{% url 'login' %}">Login</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
7
templates/customer/email_password_reset.txt
Normal file
7
templates/customer/email_password_reset.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
You're receiving this email because a password reset was requested for your account.
|
||||
|
||||
Please go to the following page and choose a new password:
|
||||
|
||||
{{ protocol }}://{{ domain }}{% url 'myapp:password_reset_confirm' uidb64=uid token=token %}
|
||||
|
||||
If you didn't request a password reset, please ignore this message.
|
||||
10
templates/customer/email_verification_body.txt
Normal file
10
templates/customer/email_verification_body.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
Hi {{ user.get_full_name|default: user.username }},
|
||||
|
||||
Thanks for signing up. Please verify your email address by clicking the link below:
|
||||
|
||||
{{ activate_url }}
|
||||
|
||||
If you did not sign up, ignore this email.
|
||||
|
||||
Thanks,
|
||||
{{ domain }}
|
||||
16
utils/date_convertor.py
Normal file
16
utils/date_convertor.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from datetime import datetime
|
||||
|
||||
def convert_date_to_dd_mm_yyyy(date_str):
|
||||
if isinstance(date_str, datetime.date):
|
||||
return date_str.strftime("%d-%m-%Y")
|
||||
|
||||
# If the input is a string → parse it as YYYY-MM-DD
|
||||
if isinstance(date_str, str):
|
||||
try:
|
||||
dt = datetime.strptime(date_str, "%Y-%m-%d")
|
||||
return dt.strftime("%d-%m-%Y")
|
||||
except ValueError:
|
||||
raise ValueError("String date must be in 'YYYY-MM-DD' format")
|
||||
|
||||
# Unsupported type
|
||||
raise TypeError("Input must be a string or datetime.date object")
|
||||
Reference in New Issue
Block a user