base setup

This commit is contained in:
2026-01-07 12:09:20 +05:30
commit 0c275efea1
278 changed files with 11228 additions and 0 deletions

0
B42/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

16
B42/asgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
ASGI config for B42 project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'B42.settings')
application = get_asgi_application()

141
B42/settings.py Normal file
View File

@@ -0,0 +1,141 @@
"""
Django settings for B42 project.
Generated by 'django-admin startproject' using Django 4.2.11.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-+#=200%4@^lboy^ovldlwc=mret9fu$d4zf-2abm3qn#g5n@rp'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'at_django_boilerplate.accounts',
'at_django_boilerplate.core',
'at_django_boilerplate.backend_admin',
'at_django_boilerplate.blogs',
'at_django_boilerplate.communications',
'at_django_boilerplate.user_activity',
'at_django_boilerplate.notification',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'B42.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'B42.wsgi.application'
AUTHENTICATION_BACKENDS = [
'at_django_boilerplate.accounts.auth_backends.CustomAuthBackend',
]
AUTH_USER_MODEL = 'accounts.CustomUser'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
# Static files configuration (add these)
import os
STATIC_URL = '/static/'
STATICFILES_DIRS = [
BASE_DIR / "static",
]
STATIC_ROOT = BASE_DIR / "staticfiles" # for collectstatic in production
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

33
B42/urls.py Normal file
View File

@@ -0,0 +1,33 @@
"""
URL configuration for B42 project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path("accounts/", include("at_django_boilerplate.accounts.urls")),
path("auth/", include("at_django_boilerplate.accounts.urls")),
path("",include("at_django_boilerplate.backend_admin.urls")),
path("",include("at_django_boilerplate.core.urls")),
path("communications/", include("at_django_boilerplate.communications.urls")),
]
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

16
B42/wsgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
WSGI config for B42 project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'B42.settings')
application = get_wsgi_application()

View File

View File

View File

@@ -0,0 +1,29 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import CustomUser
class CustomUserAdmin(UserAdmin):
model = CustomUser
list_display = ('email', 'name', 'is_staff', 'is_active',)
list_filter = ('is_staff', 'is_active',)
fieldsets = (
(None, {'fields': ('email', 'password')}),
('Personal info', {'fields': ('first_name', 'last_name', 'dob', 'contact_number', 'profile_photo')}),
('Permissions', {'fields': ('is_staff', 'is_active', 'is_superuser', 'groups', 'user_permissions')}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('email', 'first_name', 'last_name', 'password1', 'password2', 'is_staff', 'is_active')}
),
)
search_fields = ('email',)
ordering = ('email',)
def save_model(self, request, obj, form, change):
if not change: # If the user is being created
obj.set_password(form.cleaned_data['password1'])
obj.save() # Call save with custom_save=True
else:
obj.save()
admin.site.register(CustomUser, CustomUserAdmin)

View File

View File

@@ -0,0 +1,273 @@
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.authtoken.models import Token
from django.contrib.auth import authenticate
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.authentication import TokenAuthentication
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.contrib.auth.tokens import default_token_generator
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.authtoken.models import Token
from django.contrib.auth import authenticate
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.authentication import TokenAuthentication
from django.views.decorators.csrf import csrf_exempt
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
from django.contrib.auth import password_validation
from django.core.exceptions import ValidationError
from at_django_boilerplate.accounts.models import CustomUser
import logging
logger = logging.getLogger(__name__)
class LoginApiView(APIView):
permission_classes = [AllowAny] # This makes the endpoint open to all users
def post(self, request, *args, **kwargs):
# Get the username and password from the request data
username = request.data.get('username')
password = request.data.get('password')
if not username or not password:
return Response({"message": "Both username and password are required."}, status=status.HTTP_400_BAD_REQUEST)
# Authenticate the user
user = authenticate(username=username, password=password)
if user is not None:
# User is authenticated, generate token
token, created = Token.objects.get_or_create(user=user)
# You can retrieve the user's role or permission-based data here
if user.is_superuser:
role = "admin"
elif user.get_manager():
role = "manager"
elif user.get_technician():
role = "technician"
else:
role = "customer"
return Response({
"token": token.key,
"role": role,
"userId": user.id,
"username": f"{user.first_name} {user.last_name}",
"profile_photo": user.profile_photo.url if user.profile_photo.url else None,
"message": "Login successful"
}, status=status.HTTP_200_OK)
# Invalid credentials
return Response({"message": "Invalid username or password."}, status=status.HTTP_401_UNAUTHORIZED)
class ValidatePasswordAPIView(APIView):
permission_classes = [IsAuthenticated]
authentication_classes = [TokenAuthentication]
def post(self, request):
password = request.data.get('password')
if not password:
return Response({"success": False, "message": "Password is required."}, status=400)
user = request.user
if user.check_password(password):
return Response({"success": True, "message": "Password is valid."}, status=200)
return Response({"success": False, "message": "Password is incorrect."}, status=400)
class ResetPasswordAPIView(APIView):
def post(self, request):
# Get the email from the request data
email = request.data.get('email')
# Ensure the email is provided
if not email:
return Response({"success": False, "message": "Email address is required."}, status=400)
try:
# Check if the user exists in the database
user = CustomUser.objects.get(email=email)
except CustomUser.DoesNotExist:
return Response({"success": False, "message": "User with this email does not exist."}, status=404)
# Send the password reset email
if self.send_reset_email(request, user, email):
return Response({"success": True, "message": "Please check your email to reset your password."}, status=200)
else:
return Response({"success": False, "message": "Failed to send email. Please try again later."}, status=500)
def send_reset_email(self, request, user, email):
# Generate a password reset token
token = default_token_generator.make_token(user)
# Convert user.pk (integer) to string and then encode it to base64
uid = urlsafe_base64_encode(str(user.pk).encode('utf-8')) # Encode user ID to base64 string
# Prepare the context for the email template
context = {
'user': user,
'reset_link': f"http://localhost:3000/reset-password/{uid}/{token}/" # Update the URL if needed (to your front-end)
}
# Render the HTML email template with the context
html_message = render_to_string('email_template.html', context)
# Subject of the email
subject = "Password Reset Request"
# Strip HTML tags for the plain-text version of the email
message = strip_tags(html_message)
# From email and recipient list
from_email = settings.DEFAULT_FROM_EMAIL
recipient_list = [email]
try:
# Send the email
send_mail(subject, message, from_email, recipient_list, html_message=html_message)
return True
except Exception as e:
print(f"Error sending email: {str(e)}")
return False
class ResetPasswordConfirmAPIView(APIView):
def get(self, request, uidb64, token):
try:
# Decode the user ID from base64
uid = urlsafe_base64_decode(uidb64).decode('utf-8')
user = CustomUser.objects.get(pk=uid)
# Check if the token is valid
if default_token_generator.check_token(user, token):
return Response(
{"success": True, "message": "Token is valid."},
status=status.HTTP_200_OK
)
else:
return Response(
{"success": False, "message": "Invalid or expired token."},
status=status.HTTP_400_BAD_REQUEST
)
except (TypeError, ValueError, OverflowError, CustomUser.DoesNotExist):
return Response(
{"success": False, "message": "Invalid or expired token."},
status=status.HTTP_400_BAD_REQUEST
)
def post(self, request, uidb64, token):
password = request.data.get('password')
if not password:
return Response(
{"success": False, "message": "Password is required."},
status=status.HTTP_400_BAD_REQUEST
)
try:
# Decode the user ID from base64
uid = urlsafe_base64_decode(uidb64).decode('utf-8')
user = CustomUser.objects.get(pk=uid)
except (TypeError, ValueError, OverflowError, CustomUser.DoesNotExist):
return Response(
{"success": False, "message": "Invalid user ID."},
status=status.HTTP_400_BAD_REQUEST
)
try:
password_validation.validate_password(password=password)
except ValidationError as e:
return Response(
{"success": False, "message": str(e)},
status=status.HTTP_400_BAD_REQUEST
)
# Validate the token
if default_token_generator.check_token(user, token):
# Set and save the new password
user.set_password(password)
user.save()
return Response(
{"success": True, "message": "Password has been reset successfully."},
status=status.HTTP_200_OK
)
else:
return Response(
{"success": False, "message": "Invalid or expired token."},
status=status.HTTP_400_BAD_REQUEST
)
class UserProfileView(APIView):
permission_classes = [IsAuthenticated]
authentication_classes = [TokenAuthentication]
def get(self, request):
user = request.user
if user.is_superuser:
print('Is Superuser')
users = CustomUser.objects.all()
data = [
{
'id': u.id,
'email': u.email,
'first_name': u.first_name,
'last_name': u.last_name,
}
for u in users
]
elif user.is_manager: # Manager user only sees their own utility's data
utility = user.get_utility() # Get Utility
users = utility.accounts_in_utility.all()
data = [{
'id': user.id,
'email': user.email,
'first_name': user.first_name,
'last_name': user.last_name,
'utility': str(utility)
}
for u in users
]
elif user.is_technician:
utility = user.get_utility() # Get Utility
users = utility.accounts_in_utility.all()
data = [{
'id': u.id,
'email': u.email,
'first_name': u.first_name,
'last_name': u.last_name,
'utility': str(utility)
}
for u in users if not u.is_manager and not u.is_superuser
]
elif user.is_customer:
utility = user.get_utility() # Get Utility
data = [{
'id': user.id,
'email': user.email,
'first_name': user.first_name,
'last_name': user.last_name,
'utility': str(utility)
}]
else:
data = None
return Response(data, status=status.HTTP_200_OK)

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'at_django_boilerplate.accounts'

View File

@@ -0,0 +1,51 @@
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from at_django_boilerplate.utils.hash_utils import hexdigest
import logging
logger = logging.getLogger(__name__)
class CustomAuthBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
print('Username:',username)
if username is None:
username = kwargs.get('email') # fallback if email is passed explicitly
print('Username:',username)
if not username or not password:
return None
username = username.lower()
email_hash = hexdigest(username)
user_found=False
UserModel = get_user_model()
try:
user = UserModel.objects.get_by_email(email=username)
if user:
user_found=True
except UserModel.DoesNotExist:
logger.info(f'User with email {username} not found.')
try:
if not user_found:
user = UserModel.objects.get_by_contact_number(contact_number=username)
if user:
user_found=True
except UserModel.DoesNotExist:
logger.info(f'User with contact_number {username} not found.')
return None
if user_found:
if user.check_password(password) and self.user_can_authenticate(user):
return user
logger.info(f'Authentication failed for user with email hash {email_hash}.')
return None
def user_can_authenticate(self, user):
return user.is_active

View File

@@ -0,0 +1,165 @@
from django import forms
from django.contrib.auth.forms import PasswordResetForm
from django.utils.http import urlsafe_base64_encode
from django.utils.encoding import force_bytes
from django.contrib.auth.tokens import default_token_generator
from django.contrib.auth.password_validation import (validate_password,
get_password_validators,
MinimumLengthValidator,
UserAttributeSimilarityValidator,
CommonPasswordValidator,
NumericPasswordValidator,)
from django.conf import settings
from .models import CustomUser
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from at_django_boilerplate.utils.hash_utils import hexdigest
import logging
logger = logging.getLogger(__name__)
class UserSignUpForm(forms.ModelForm):
password = forms.CharField(widget=forms.PasswordInput)
confirm_password = forms.CharField(widget=forms.PasswordInput)
terms_accepted = forms.BooleanField(required=True, label='I accept the terms and conditions')
class Meta:
model = CustomUser
fields = ['first_name', 'last_name', 'email','contact_number','dob','password','confirm_password','terms_accepted']
widgets = {
'dob': forms.DateInput(attrs={'type': 'date'}),
'first_name': forms.TextInput(attrs={'type': 'text'}),
'last_name': forms.TextInput(attrs={'type': 'text'}),
'email': forms.EmailInput(attrs={'type': 'email'}),
'contact_number': forms.TextInput(attrs={'type': 'text'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['password'].help_text = self.get_password_requirements()
def get_password_requirements(self):
password_validators = get_password_validators(settings.AUTH_PASSWORD_VALIDATORS)
requirements = []
for validator in password_validators:
if isinstance(validator, MinimumLengthValidator):
requirements.append(f"*Password must be at least {validator.min_length} characters long.<br>")
elif isinstance(validator, UserAttributeSimilarityValidator):
requirements.append("*Password cannot be too similar to your other personal information.<br>")
elif isinstance(validator, CommonPasswordValidator):
requirements.append("*Password cannot be a commonly used password.<br>")
elif isinstance(validator, NumericPasswordValidator):
requirements.append("*Password cannot be entirely numeric.<br>")
return " ".join(requirements)
def clean(self):
cleaned_data = super().clean()
password = cleaned_data.get("password")
confirm_password = cleaned_data.get("confirm_password")
if password and confirm_password:
try:
validate_password(password, self.instance)
except forms.ValidationError as error:
self.add_error('password', error)
return cleaned_data
if password != confirm_password:
self.add_error('confirm_password', "Passwords do not match")
return cleaned_data
class SigninForm(forms.Form):
username = forms.CharField(max_length=65)
password = forms.CharField(max_length=65, widget=forms.PasswordInput)
class CustomPasswordResetForm(PasswordResetForm):
def get_users(self, email):
"""Given an email, return matching user(s) who should receive a reset."""
email_hash = hexdigest(email)
active_custom_users = CustomUser.objects.filter(email_hash=email_hash)
active_users = [user for user in active_custom_users ]
valid_users = [u for u in active_users if u.has_usable_password()]
return valid_users
def save(self, domain_override=None,
subject_template_name='reset/password_reset_subject.txt',
email_template_name='reset/password_reset_email.html',
use_https=False, token_generator=default_token_generator,
from_email=None, request=None, html_email_template_name=None,
extra_email_context=None):
"""
Generates a one-use only link for resetting password and sends to the user.
"""
email = self.cleaned_data["email"]
for user in self.get_users(email):
context = {
'email': email,
'domain': domain_override or request.get_host(),
'request':request,
'site_name': 'we-kwick',
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
'user': user,
'token': token_generator.make_token(user),
'protocol': 'https' if use_https else 'http',
**(extra_email_context or {}),
}
from_email = settings.EMAIL_HOST_USER
# print('From:',from_email)
# print('Email Template:',email_template_name)
# print('HTML Email Template:',html_email_template_name)
self.send_mail(
subject_template_name, email_template_name, context, from_email,
email, html_email_template_name=html_email_template_name,
)
class ProfileUpdateForm(forms.ModelForm):
class Meta:
model = CustomUser
fields = [ 'contact_number']
widgets = {
'dob': forms.DateInput(attrs={'type': 'date'}),
}
class UpdateProfileForm(forms.ModelForm):
class Meta:
model = CustomUser
fields = [ 'contact_number']
widgets = {
'dob': forms.DateInput(attrs={'type': 'date'}),
}
def validate_image(file):
valid_mime_types = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml']
valid_file_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.svg','.JPG']
file_mime_type = file.content_type
if file_mime_type not in valid_mime_types:
raise ValidationError(_('Unsupported file type. Please upload a JPEG, PNG, GIF, or SVG image.'))
extension = file.name.split('.')[-1].lower()
if not any(file.name.endswith(ext) for ext in valid_file_extensions):
raise ValidationError(_('Unsupported file extension.'))
class ProfilePictureUpdateForm(forms.ModelForm):
profile_photo = forms.ImageField(
widget=forms.ClearableFileInput(attrs={'accept': 'image/jpeg,image/png,image/gif,image/svg+xml'}),
validators=[validate_image]
)
class Meta:
model = CustomUser
fields = ['profile_photo']

View File

@@ -0,0 +1,19 @@
from django.core.management.base import BaseCommand
from at_django_boilerplate.accounts.models import CustomUser
from django.db.utils import IntegrityError
class Command(BaseCommand):
help = 'Add predefined tags to the database'
def handle(self, *args, **kwargs):
tmp = CustomUser.objects.create(email='admin@rys.com',first_name='Admin',last_name='User')
tmp.save()
tmp.set_password('1234')
tmp.is_superuser = True
tmp.is_active = True
tmp.is_staff = True
tmp.save()
self.stdout.write(self.style.SUCCESS("✅ Seeded Accounts & Businesses"))

View File

@@ -0,0 +1,50 @@
# Generated by Django 5.2.6 on 2025-12-29 05:20
import at_django_boilerplate.accounts.models
import at_django_boilerplate.utils.custom_fields
import django.utils.timezone
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='CustomUser',
fields=[
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('first_name', at_django_boilerplate.utils.custom_fields.EncryptedTextField()),
('last_name', at_django_boilerplate.utils.custom_fields.EncryptedTextField()),
('email', at_django_boilerplate.utils.custom_fields.EncryptedSearchableTextField()),
('dob', models.DateField(blank=True, null=True, verbose_name='Date Of Birth')),
('contact_number', at_django_boilerplate.utils.custom_fields.EncryptedSearchableTextField(blank=True, null=True)),
('profile_photo', models.ImageField(blank=True, default='assets/default_profile.png', null=True, upload_to=at_django_boilerplate.accounts.models.user_directory_path)),
('is_staff', models.BooleanField(default=False)),
('is_active', models.BooleanField(default=True)),
('date_joined', models.DateTimeField(default=django.utils.timezone.now)),
('terms_accepted', models.BooleanField(default=False, verbose_name='Terms & Conditions')),
('terms_accepted_time', models.DateTimeField(default=django.utils.timezone.now)),
('source', models.CharField(default='manual', max_length=10)),
('last_active', models.DateTimeField(blank=True, null=True)),
('is_technician', models.BooleanField(default=False)),
('is_manager', models.BooleanField(default=False)),
('email_hash', models.CharField(blank=True, editable=False, max_length=64, null=True)),
('contact_number_hash', models.CharField(blank=True, editable=False, max_length=64, null=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'abstract': False,
},
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.2.6 on 2025-12-30 09:23
import at_django_boilerplate.utils.custom_fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='customuser',
name='email',
field=at_django_boilerplate.utils.custom_fields.EncryptedSearchableTextField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,178 @@
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from django.urls import reverse
import uuid
from at_django_boilerplate.utils.encryption_utils import EncryptionUtils
# ✅ imports fixed according to your project structure
from at_django_boilerplate.utils.hash_utils import hexdigest
from at_django_boilerplate.utils.custom_fields import (
EncryptedTextField,
EncryptedSearchableTextField,
)
from at_django_boilerplate.utils.mixins import UUIDMixin
def user_directory_path(instance, filename):
user_id = str(instance.id).zfill(10)
return f'uploads/users/{user_id}/{filename}'
class CustomUserManager(BaseUserManager):
def create_user(self, email=None, contact_number=None, password=None, **extra_fields):
"""
Create and save a User with the given email and/or contact_number and password.
At least one of email or contact_number must be provided.
"""
if not email and not contact_number:
raise ValueError("Either email or contact number must be provided.")
if email:
email = self.normalize_email(email)
user = self.model(email=email, contact_number=contact_number, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email=None, contact_number=None, password=None, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
if extra_fields.get('is_staff') is not True:
raise ValueError('Superuser must have is_staff=True.')
if extra_fields.get('is_superuser') is not True:
raise ValueError('Superuser must have is_superuser=True.')
# Superuser usually needs at least one identifier
if not email and not contact_number:
raise ValueError('Superuser must have email or contact number.')
return self.create_user(email, contact_number, password, **extra_fields)
# Keep your existing search helpers unchanged
def get_by_email(self, email):
try:
return self.get(email_hash=hexdigest(email))
except CustomUser.DoesNotExist:
return None
def filter_by_email(self, email):
return self.filter(email_hash=hexdigest(email))
def get_by_contact_number(self, contact_number):
try:
return self.get(contact_number_hash=hexdigest(contact_number))
except CustomUser.DoesNotExist:
return None
def filter_by_contact_number(self, contact_number):
return self.filter(contact_number_hash=hexdigest(contact_number))
class CustomUser(AbstractBaseUser, PermissionsMixin,UUIDMixin):
first_name = EncryptedTextField()
last_name = EncryptedTextField()
email = EncryptedSearchableTextField(hash_field='email_hash',null=True, # ← Must be True
blank=True)
dob = models.DateField(_('Date Of Birth'), blank=True, null=True)
contact_number = EncryptedSearchableTextField( hash_field='contact_number_hash',null=True, blank=True)
profile_photo = models.ImageField(
upload_to=user_directory_path,
default='assets/default_profile.png',
null=True,
blank=True
)
is_staff = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
date_joined = models.DateTimeField(default=timezone.now)
terms_accepted = models.BooleanField("Terms & Conditions", default=False)
terms_accepted_time = models.DateTimeField(default=timezone.now)
source = models.CharField(max_length=10, default='manual')
last_active = models.DateTimeField(null=True,blank=True)
is_technician = models.BooleanField(default=False)
is_manager = models.BooleanField(default=False)
objects = CustomUserManager()
USERNAME_FIELD = 'id'
REQUIRED_FIELDS = ['first_name', 'last_name']
def __str__(self):
return self.get_decrypted_name()
@property
def name(self):
return self.get_decrypted_name()
@property
def is_admin(self):
return self.is_superuser
def get_absolute_url(self):
return reverse("user_profile")
def get_decrypted_first_name(self):
return self.first_name or ''
def get_decrypted_last_name(self):
return self.last_name or ''
def get_decrypted_name(self):
return f"{self.get_decrypted_first_name()} {self.get_decrypted_last_name()}".strip()
def get_decrypted_email(self):
return self.email or ''
def get_decrypted_contact_number(self):
return self.contact_number or ''
def set_contact_number(self, value):
self.contact_number = value
self.save()
def set_first_name(self, value):
self.first_name = value
self.save()
def set_last_name(self, value):
self.last_name = value
self.save()
def set_name(self, value):
names = value.strip().split(' ', 1)
self.set_first_name(names[0])
self.set_last_name(names[1] if len(names) > 1 else '')
def has_contact_number(self):
return bool(self.contact_number)
def is_customer(self):
try:
return self.connection_account.all().count() >=1
except Exception as e:
print(e)
return False
def get_connection(self):
return self.connection_account.all().first()
def get_technician(self):
return self.is_technician
def get_manager(self):
return self.is_manager
# def ge

View File

@@ -0,0 +1,75 @@
{% extends "public_base.html" %}
{% block content %}
<div class="form-content my-3 p-3">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-5">
<div class="card shadow-lg border-0 rounded-lg mt-0 mb-3">
<div class="card-header justify-content-center">
<h3 class="font-weight-light my-4 text-center">Change Your Password</h3>
</div>
{% if form.errors %}
<div class="alert alert-danger alert-dismissible" role="alert">
<div id="form_errors">
{% for key, value in form.errors.items %}
<strong>{{ value }}</strong>
{% endfor %}
</div>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{% endif %}
<div class="card-body">
<form method="POST">
{% csrf_token %}
<div class="form-row">
<div class="col-md-10 offset-md-1">
<div class="form-group">
<label class="small mb-1" for="id_old_password">Old Password</label>
<input type="password" name="old_password" autocomplete="new-password"
class="form-control" required id="id_old_password"
placeholder="Enter Old Password"/>
<span>
</span>
</div>
</div>
</div>
<div class="form-row">
<div class="col-md-10 offset-md-1">
<div class="form-group">
<label class="small mb-1" for="id_new_password1">New Password</label>
<input type="password" name="new_password1" autocomplete="new-password"
class="form-control" required id="id_new_password1"
placeholder="Enter New Password"/>
<span>
</span>
</div>
</div>
</div>
<div class="form-row">
<div class="col-md-10 offset-md-1">
<div class="form-group">
<label class="small mb-1" for="id_new_password2">New Password Confirmation</label>
<input type="password" name="new_password2" autocomplete="new-password"
required id="id_new_password2" class="form-control"
placeholder="Confirm New Password"/>
</div>
</div>
</div>
<div class="form-row">
<div class="col-md-10 offset-md-1">
<div class="form-group mt-0 mb-1">
<button type="submit" class="col-md-12 btn btn-dark" id="reset">Update Password</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}

View File

@@ -0,0 +1,596 @@
{% extends 'public_base.html' %}
{% block title %}
Login
{% endblock %}
{% block content %}
{% load static %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
:root {
--primary: #6366f1;
--accent: #4f46e5;
--gradient-start: #6366f1;
--gradient-end: #8b5cf6;
--bg: #ffffff;
--muted: #6b7280;
--danger: #ef4444;
--success: #10b981;
--radius: 16px;
--shadow: 0 20px 40px rgba(99, 102, 241, 0.15);
--shadow-hover: 0 25px 50px rgba(99, 102, 241, 0.25);
}
body {
font-family: 'Inter', sans-serif;
margin: 0;
padding: 0;
background: linear-gradient(135deg, #f0f4ff 0%, #fdf2ff 50%, #f0fdf4 100%);
min-height: 100vh;
}
.login-page {
display: flex;
min-height: 100vh;
align-items: center;
justify-content: center;
padding: 2rem;
position: relative;
overflow: hidden;
}
.login-page::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(99, 102, 241, 0.05) 0%, transparent 70%);
animation: float 20s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translate(0, 0) rotate(0deg); }
25% { transform: translate(10px, 10px) rotate(1deg); }
50% { transform: translate(-5px, 15px) rotate(-1deg); }
75% { transform: translate(15px, -5px) rotate(1deg); }
}
.login-container {
display: flex;
max-width: 1200px;
width: 100%;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.2);
position: relative;
z-index: 1;
}
.login-left {
flex: 1;
padding: 4rem 3rem;
background: white;
position: relative;
}
.login-left::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 4px;
height: 100%;
background: linear-gradient(to bottom, var(--gradient-start), var(--gradient-end));
}
.form-container {
max-width: 400px;
margin: 0 auto;
}
.logo-section {
text-align: center;
margin-bottom: 2.5rem;
}
.logo {
width: 60px;
height: 60px;
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem;
box-shadow: 0 10px 25px rgba(99, 102, 241, 0.3);
}
.logo i {
font-size: 1.8rem;
color: white;
}
.form-container h3 {
margin-bottom: 0.5rem;
font-size: 2rem;
font-weight: 800;
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-align: center;
}
.form-container p.subtext {
margin-bottom: 2.5rem;
font-size: 1rem;
color: var(--muted);
text-align: center;
line-height: 1.6;
}
.form-group {
margin-bottom: 1.5rem;
position: relative;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.9rem;
font-weight: 600;
color: #374151;
}
.input-group {
position: relative;
}
.form-control {
width: 100%;
padding: 1rem 1rem 1rem 3rem;
border: 2px solid #e5e7eb;
border-radius: 12px;
font-size: 1rem;
transition: all 0.3s ease;
background: #fafafa;
}
.form-control:focus {
border-color: var(--accent);
background: white;
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1);
outline: none;
transform: translateY(-2px);
}
.input-icon {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: var(--muted);
transition: all 0.3s ease;
}
.form-control:focus + .input-icon {
color: var(--accent);
}
.form-options {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.remember-me {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
color: var(--muted);
}
.forgot-password {
color: var(--accent);
text-decoration: none;
font-size: 0.9rem;
font-weight: 500;
}
.forgot-password:hover {
color: var(--gradient-end);
text-decoration: underline;
}
.btn-submit {
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
color: white;
border: none;
padding: 1rem 2rem;
width: 100%;
border-radius: 12px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 10px 25px rgba(99, 102, 241, 0.3);
position: relative;
overflow: hidden;
}
.btn-submit:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-hover);
}
/* OTP Button */
.btn-otp {
background: linear-gradient(135deg, #10b981, #059669);
margin-top: 1rem;
}
.btn-otp:hover {
background: linear-gradient(135deg, #059669, #047857);
transform: translateY(-3px);
box-shadow: var(--shadow-hover);
}
.signup-link {
text-align: center;
font-size: 0.9rem;
color: var(--muted);
margin-top: 1.5rem;
}
.signup-link a {
color: var(--accent);
font-weight: 600;
}
.login-right {
flex: 1;
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
padding: 4rem 3rem;
color: white;
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
overflow: hidden;
}
.login-right::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse"><path d="M 10 0 L 0 0 0 10" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"/></pattern></defs><rect width="100" height="100" fill="url(%23grid)"/></svg>');
}
.features-title {
font-size: 2rem;
font-weight: 700;
margin-bottom: 2rem;
}
.feature-item {
display: flex;
align-items: flex-start;
margin-bottom: 1.5rem;
}
.feature-icon {
width: 24px;
height: 24px;
background: rgba(255,255,255,0.2);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 1rem;
flex-shrink: 0;
}
.alert {
padding: 1rem 1.5rem;
margin-bottom: 1.5rem;
border-radius: 12px;
font-size: 0.9rem;
border-left: 4px solid;
}
.alert-success { background: rgba(16,185,129,0.1); border-color: var(--success); color: #065f46; }
.alert-danger { background: rgba(239,68,68,0.1); border-color: var(--danger); color: #991b1b; }
/* Modal Styles (No Bootstrap) */
.otp-modal {
display: none;
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.otp-modal.active {
display: flex;
}
.otp-modal-content {
background: white;
border-radius: 16px;
box-shadow: var(--shadow);
width: 90%;
max-width: 480px;
overflow: hidden;
}
.otp-modal-header {
padding: 1.5rem 2rem;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.otp-modal-body {
padding: 2rem;
}
.otp-input-group {
display: flex;
gap: 12px;
justify-content: center;
margin: 1.5rem 0;
}
.otp-input {
width: 56px;
height: 64px;
text-align: center;
font-size: 1.8rem;
font-weight: bold;
border: 2px solid #e5e7eb;
border-radius: 12px;
}
.otp-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 4px rgba(99,102,241,0.1);
outline: none;
}
@media (max-width: 768px) {
.login-container { flex-direction: column; max-width: 500px; }
.login-left::before { display: none; }
.login-right { display: none; }
.login-left { padding: 3rem 2rem; }
}
</style>
<div class="login-page">
<div class="login-container">
<div class="login-left">
<div class="form-container">
<div class="logo-section">
<div class="logo">
<i class="fas fa-shield-alt"></i>
</div>
<h3>Welcome Back</h3>
<p class="subtext">Sign in to your account to continue</p>
</div>
{% if messages %}
{% for message in messages %}
<div class="alert {% if 'success' in message.tags %}alert-success{% else %}alert-danger{% endif %}">
{{ message }}
</div>
{% endfor %}
{% endif %}
<form method="POST" action="{% url 'login' %}">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.GET.next }}">
<div class="form-group">
<label class="form-label" for="username">Username</label>
<div class="input-group">
<input id="username" name="username" class="form-control" placeholder="Enter your username" required autofocus type="text">
<div class="input-icon"><i class="fas fa-user"></i></div>
</div>
</div>
<div class="form-group">
<label class="form-label" for="password">Password</label>
<div class="input-group">
<input type="password" id="password" name="password" class="form-control" placeholder="Enter your password" required>
<div class="input-icon"><i class="fas fa-lock"></i></div>
</div>
</div>
<div class="form-options">
<label class="remember-me">
<input type="checkbox" name="remember"> Remember me
</label>
<a href="{% url 'password_reset' %}" class="forgot-password">Forgot password?</a>
</div>
<button type="submit" class="btn-submit">
<i class="fas fa-sign-in-alt"></i> Sign In
</button>
<button type="button" class="btn-submit btn-otp" id="openOtpModal">
<i class="fas fa-mobile-alt"></i> Login with OTP
</button>
<div class="signup-link">
Don't have an account? <a href="{% url 'signup' %}">Sign up here</a>
</div>
</form>
</div>
</div>
<div class="login-right">
<h2 class="features-title">Why Choose Us?</h2>
<ul class="feature-list">
<li class="feature-item">
<div class="feature-icon"><i class="fas fa-rocket"></i></div>
<div class="feature-text"><strong>Lightning Fast</strong><br>Quick and seamless login experience</div>
</li>
<li class="feature-item">
<div class="feature-icon"><i class="fas fa-shield-alt"></i></div>
<div class="feature-text"><strong>Bank-Level Security</strong><br>Your data is protected with enterprise-grade security</div>
</li>
<li class="feature-item">
<div class="feature-icon"><i class="fas fa-sync-alt"></i></div>
<div class="feature-text"><strong>Sync Across Devices</strong><br>Access your account from anywhere, anytime</div>
</li>
<li class="feature-item">
<div class="feature-icon"><i class="fas fa-headset"></i></div>
<div class="feature-text"><strong>24/7 Support</strong><br>Our team is always here to help you</div>
</li>
<li class="feature-item">
<div class="feature-icon"><i class="fas fa-chart-line"></i></div>
<div class="feature-text"><strong>Advanced Analytics</strong><br>Get insights into your business performance</div>
</li>
</ul>
</div>
</div>
</div>
<!-- Custom OTP Modal (No Bootstrap) -->
<div class="otp-modal" id="otpModal">
<div class="otp-modal-content">
<div class="otp-modal-header">
<h5 class="fw-bold">Login with OTP</h5>
<button type="button" id="closeOtpModal" style="background:none;border:none;font-size:1.5rem;cursor:pointer;">&times;</button>
</div>
<div class="otp-modal-body">
<div id="step1">
<p class="text-center text-muted mb-4">Enter your registered email or mobile number</p>
<input type="text" id="otpIdentifier" class="form-control" placeholder="Email or Phone (+91...)" required autofocus>
<button id="sendOtpBtn" class="btn-submit w-100 mt-4">
<i class="fas fa-paper-plane"></i> Send OTP
</button>
</div>
<div id="step2" style="display:none;">
<p class="text-center text-muted mb-4">Enter the 6-digit OTP sent to<br><strong id="sentTo"></strong></p>
<div class="otp-input-group">
<input type="text" class="otp-input" maxlength="1" inputmode="numeric">
<input type="text" class="otp-input" maxlength="1" inputmode="numeric">
<input type="text" class="otp-input" maxlength="1" inputmode="numeric">
<input type="text" class="otp-input" maxlength="1" inputmode="numeric">
<input type="text" class="otp-input" maxlength="1" inputmode="numeric">
<input type="text" class="otp-input" maxlength="1" inputmode="numeric">
</div>
<button id="verifyOtpBtn" class="btn-submit w-100">
<i class="fas fa-check-circle"></i> Verify & Login
</button>
<p class="text-center mt-3">
<a href="#" id="resendOtp" class="text-muted small">Resend OTP</a>
</p>
</div>
</div>
</div>
</div>
<script>
// Modal Controls
const modal = document.getElementById('otpModal');
const openBtn = document.getElementById('openOtpModal');
const closeBtn = document.getElementById('closeOtpModal');
const step1 = document.getElementById('step1');
const step2 = document.getElementById('step2');
const identifierInput = document.getElementById('otpIdentifier');
const sentTo = document.getElementById('sentTo');
const otpInputs = document.querySelectorAll('.otp-input');
openBtn.onclick = () => modal.classList.add('active');
closeBtn.onclick = () => modal.classList.remove('active');
modal.onclick = (e) => { if (e.target === modal) modal.classList.remove('active'); };
// Auto-focus OTP inputs
otpInputs.forEach((input, i) => {
input.addEventListener('input', () => {
if (input.value && i < 5) otpInputs[i + 1].focus();
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' && !input.value && i > 0) otpInputs[i - 1].focus();
});
});
// Send OTP
document.getElementById('sendOtpBtn').onclick = async () => {
const identifier = identifierInput.value.trim();
if (!identifier) return alert('Please enter email or phone');
const btn = document.getElementById('sendOtpBtn');
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending...';
btn.disabled = true;
const res = await fetch("{% url 'request_otp' %}", {
method: 'POST',
headers: { 'X-CSRFToken': '{{ csrf_token }}' },
body: new URLSearchParams({ 'identifier': identifier })
});
if (res.ok) {
step1.style.display = 'none';
step2.style.display = 'block';
sentTo.textContent = identifier;
otpInputs[0].focus();
} else {
alert('Failed to send OTP');
}
btn.innerHTML = '<i class="fas fa-paper-plane"></i> Send OTP';
btn.disabled = false;
};
// Verify OTP
document.getElementById('verifyOtpBtn').onclick = async () => {
const otp = Array.from(otpInputs).map(i => i.value).join('');
if (otp.length !== 6) return alert('Enter full 6-digit OTP');
const btn = document.getElementById('verifyOtpBtn');
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Verifying...';
btn.disabled = true;
const res = await fetch("{% url 'verify_otp' %}", {
method: 'POST',
headers: { 'X-CSRFToken': '{{ csrf_token }}' },
body: new URLSearchParams({ 'otp': otp })
});
if (res.ok) {
window.location.href = "{% url 'home' %}";
} else {
alert('Invalid or expired OTP');
}
btn.innerHTML = '<i class="fas fa-check-circle"></i> Verify & Login';
btn.disabled = false;
};
// Resend
document.getElementById('resendOtp').onclick = (e) => {
e.preventDefault();
step2.style.display = 'none';
step1.style.display = 'block';
identifierInput.value = sentTo.textContent;
identifierInput.focus();
};
// Loading spinner on normal login
document.querySelector('form').addEventListener('submit', function() {
this.querySelector('.btn-submit').innerHTML = '<i class="fas fa-spinner fa-spin"></i> Signing In...';
});
</script>
{% endblock %}

View File

@@ -0,0 +1,31 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Your OTP for {{ purpose|capfirst }}</title>
<style>
body { font-family: Arial, sans-serif; background-color: #f9f9f9; color: #333; }
.container { max-width: 600px; margin: auto; padding: 30px; background-color: #fff; border-radius: 8px; }
.otp-box { font-size: 24px; font-weight: bold; color: #2c3e50; background: #ecf0f1; padding: 15px; text-align: center; border-radius: 6px; margin: 20px 0; }
.footer { font-size: 12px; color: #999; margin-top: 30px; }
</style>
</head>
<body>
<div class="container">
<h2>Hello,</h2>
<p>You requested an OTP for <strong>{{ purpose|capfirst }}</strong>.</p>
<p>Your One-Time Password (OTP) is:</p>
<div class="otp-box">{{ otp }}</div>
<p>This OTP is valid for <strong>{{ validity_minutes }} minutes</strong>.</p>
<p><strong>⚠️ Do not share this OTP with anyone.</strong></p>
<p>If you did not request this OTP, please ignore this email or <a href="mailto:support@example.com">contact support</a> immediately.</p>
<div class="footer">
This is an automated message. Please do not reply.
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,66 @@
{% extends 'base.html' %}
{% block title %}User Profile{% endblock %}
{% load crispy_forms_tags %}
{% load static %}
{% block content %}
<!-- Profile Picture Update Modal -->
<div class="fixed inset-0 z-50 hidden overflow-y-auto bg-black bg-opacity-60" id="profilePicModal">
<div class="flex items-center justify-center min-h-screen px-4">
<div class="bg-white rounded-xl shadow-2xl w-full max-w-md">
<div class="flex justify-between items-center p-6 border-b border-gray-200">
<h3 class="text-xl font-semibold text-[#1E40AF]">Update Profile Picture</h3>
<button onclick="document.getElementById('profilePicModal').classList.add('hidden')" class="text-gray-500 hover:text-[#1E40AF] text-2xl transition-colors">&times;</button>
</div>
<div class="p-6">
<form method="POST" action="{% url 'update_profile_picture' %}" enctype="multipart/form-data">
{% csrf_token %}
{{ update_profile_photo_form|crispy }}
<button type="submit" class="mt-4 w-full bg-[#1E40AF] hover:bg-[#1E3A8A] text-white font-semibold py-2.5 px-4 rounded-lg transition-colors duration-200">Save Changes</button>
</form>
</div>
</div>
</div>
</div>
<!-- Profile Content -->
<div class="max-w-6xl mx-auto px-4 py-12">
<div class="bg-white rounded-xl shadow-xl overflow-hidden">
<div class="flex flex-col md:flex-row">
<!-- Left Sidebar -->
<div class="bg-gray-50 md:w-1/3 flex flex-col items-center justify-center p-8 text-center">
<img src="{{ user.profile_photo.url }}" alt="Profile Image" class="w-36 h-36 rounded-full object-cover mb-6 shadow-lg border-4 border-white">
<h2 class="text-2xl font-bold text-[#1E40AF]">{{ user }}</h2>
<a href="{% url 'update_profile' user.id %}" class="mt-6 w-full bg-[#1E40AF] text-white py-2.5 rounded-lg hover:bg-[#1E3A8A] font-semibold transition-colors duration-200">Update Profile</a>
<button onclick="document.getElementById('profilePicModal').classList.remove('hidden')" class="mt-3 w-full bg-[#1E40AF] text-white py-2.5 rounded-lg hover:bg-[#1E3A8A] font-semibold transition-colors duration-200">Update Profile Picture</button>
</div>
<!-- Right Content -->
<div class="md:w-2/3 p-8">
<h3 class="text-xl font-semibold text-[#1E40AF] mb-4 border-b border-gray-200 pb-3">Account Information</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div>
<p class="text-sm font-semibold text-gray-500">Email</p>
<p class="text-[#1E3A8A]">{{ user.email }}</p>
</div>
<div>
<p class="text-sm font-semibold text-gray-500">Phone</p>
<p class="text-[#1E3A8A]">{{ user.get_decrypted_contact_number }}</p>
</div>
</div>
<h3 class="text-xl font-semibold text-[#1E40AF] mb-4 border-b border-gray-200 pb-3">Social Media</h3>
<div class="flex space-x-6 mt-4">
<a href="#" class="text-[#1E40AF] hover:text-[#1E3A8A] text-2xl transition-colors"><i class="mdi mdi-facebook"></i></a>
<a href="#" class="text-[#1E40AF] hover:text-[#1E3A8A] text-2xl transition-colors"><i class="mdi mdi-twitter"></i></a>
<a href="#" class="text-[#1E40AF] hover:text-[#1E3A8A] text-2xl transition-colors"><i class="mdi mdi-instagram"></i></a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,21 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block title %}
Update Profile
{% endblock %}
{% block content %}
<div class="container mx-auto max-w-lg p-6 bg-white rounded-lg shadow-lg mt-10">
<h2 class="text-3xl font-bold text-center text-gray-800 mb-6">Update Profile</h2>
<form method="POST" class="space-y-6">
{% csrf_token %}
{{ form|crispy }}
<button type="submit" class="w-full bg-gradient-to-r from-indigo-500 to-purple-500 hover:from-indigo-600 hover:to-purple-600 text-white font-bold py-2 px-4 rounded-lg shadow-lg transition duration-300">
Save changes
</button>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{% extends 'base.html' %}
{% block title %}
Update Profile Picture
{% endblock %}
{% block content %}
<div class="container">
<h2>Update Profile Picture</h2>
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">Save changes</button>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<title>Create User</title>
</head>
<body>
<h2>Create User</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Create</button>
</form>
<nav>
<ul>
<li><a href="{% url 'index' %}">Index</a></li>
<li><a href="{% url 'home' %}">Home</a></li>
<li><a href="{% url 'user_list' %}">User List</a></li>
</ul>
</nav>
</body>
</html>

View File

@@ -0,0 +1,316 @@
{% extends 'public_base.html' %}
{% load static %}
{% load custom_tags %}
{% block title %}Sign Up | Register your startup{% endblock %}
{% block content %}
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-indigo-50 to-purple-50 py-8 px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-6xl">
<div class="bg-white shadow-xl rounded-2xl overflow-hidden border border-gray-100">
<div class="flex flex-col lg:flex-row">
<!-- Left Side - Image Card (Hidden on mobile) -->
<div class="hidden lg:block lg:w-2/5 bg-gradient-to-br from-indigo-600 to-purple-700 p-8 lg:p-12">
<div class="flex flex-col justify-center items-center text-center text-white h-full">
<div class="max-w-sm">
<img src="{% static 'img/images or rys/Startup India Registration/hero1.png' %}" alt="Startup Registration" class="w-full h-48 lg:h-56 mb-6 lg:mb-8 rounded-lg">
<h2 class="text-2xl font-bold mb-4">Start Your Entrepreneurial Journey</h2>
<div class="space-y-4 text-left">
<div class="flex items-start">
<i class="fas fa-check-circle text-green-300 mr-3 mt-1 flex-shrink-0"></i>
<span class="text-sm">Quick and hassle-free registration process</span>
</div>
<div class="flex items-start">
<i class="fas fa-check-circle text-green-300 mr-3 mt-1 flex-shrink-0"></i>
<span class="text-sm">Expert guidance for startup compliance</span>
</div>
<div class="flex items-start">
<i class="fas fa-check-circle text-green-300 mr-3 mt-1 flex-shrink-0"></i>
<span class="text-sm">Secure and confidential data handling</span>
</div>
<div class="flex items-start">
<i class="fas fa-check-circle text-green-300 mr-3 mt-1 flex-shrink-0"></i>
<span class="text-sm">Dedicated support for your business growth</span>
</div>
</div>
</div>
</div>
</div>
<!-- Right Side - Form Card -->
<div class="w-full lg:w-3/5 py-8 lg:py-10 px-6 lg:px-8">
<!-- Mobile Header with Logo -->
<div class="lg:hidden mb-6 text-center">
<div class="flex justify-center mb-4">
<div class="w-16 h-16 bg-indigo-600 rounded-full flex items-center justify-center">
<i class="fas fa-rocket text-white text-2xl"></i>
</div>
</div>
<h2 class="text-2xl font-bold text-gray-900 mb-2">Create Your Account</h2>
<p class="text-sm text-gray-600">Join thousands of successful startups</p>
</div>
<!-- Desktop Header -->
<div class="hidden lg:block text-center mb-8">
<h2 class="text-3xl font-bold text-gray-900 mb-2">Create Your Account</h2>
<p class="text-gray-600">Start your entrepreneurial journey with us today</p>
</div>
<!-- Error Display -->
{% if form.errors %}
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6 animate-pulse">
<div class="flex items-center">
<i class="fa-solid fa-exclamation-circle mr-2"></i>
<strong class="font-medium">Please correct the following errors:</strong>
</div>
<ul class="list-disc list-inside mt-2 ml-2 text-sm">
{% for field, errors in form.errors.items %}
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
</div>
{% endif %}
<!-- Email Validation Status -->
<div id="email-status" class="flex items-center space-x-2 text-sm mb-4 hidden">
<span id="email-icon" class="text-lg"></span>
<span id="email-text"></span>
</div>
<form method="post" action="{% url 'signup' %}" class="space-y-6" novalidate>
{% csrf_token %}
<!-- Personal Information Section -->
<div class="space-y-5">
<div class="flex items-center mb-2">
<div class="w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center mr-3">
<i class="fas fa-user text-indigo-600 text-sm"></i>
</div>
<h3 class="text-lg font-semibold text-gray-900">Personal Information</h3>
</div>
<!-- Name Fields -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- First Name -->
<div>
<label for="{{ form.first_name.id_for_label }}" class="block text-sm font-medium text-gray-700 mb-2">
First Name <span class="text-red-500">*</span>
</label>
<div class="relative">
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400">
<i class="fa-solid fa-user"></i>
</span>
{{ form.first_name|add_class:"appearance-none block w-full pl-10 px-3 py-3 border border-gray-300 rounded-lg shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition duration-200" }}
</div>
</div>
<!-- Last Name -->
<div>
<label for="{{ form.last_name.id_for_label }}" class="block text-sm font-medium text-gray-700 mb-2">
Last Name <span class="text-red-500">*</span>
</label>
<div class="relative">
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400">
<i class="fa-solid fa-user"></i>
</span>
{{ form.last_name|add_class:"appearance-none block w-full pl-10 px-3 py-3 border border-gray-300 rounded-lg shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition duration-200" }}
</div>
</div>
</div>
<!-- Contact & DOB Fields -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Contact Number -->
<div>
<label for="{{ form.contact_number.id_for_label }}" class="block text-sm font-medium text-gray-700 mb-2">
Contact Number
</label>
<div class="relative">
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400">
<i class="fa-solid fa-phone"></i>
</span>
{{ form.contact_number|add_class:"appearance-none block w-full pl-10 px-3 py-3 border border-gray-300 rounded-lg shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition duration-200" }}
</div>
</div>
<!-- Email Field -->
<div>
<label for="{{ form.email.id_for_label }}" class="block text-sm font-medium text-gray-700 mb-2">
Email Address <span class="text-red-500">*</span>
</label>
<div class="relative">
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400">
<i class="fa-solid fa-envelope"></i>
</span>
{{ form.email|add_class:"appearance-none block w-full pl-10 px-3 py-3 border border-gray-300 rounded-lg shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition duration-200" }}
</div>
</div>
</div>
</div>
<!-- Password Section -->
<div class="space-y-5">
<div class="flex items-center mb-2">
<div class="w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center mr-3">
<i class="fas fa-lock text-indigo-600 text-sm"></i>
</div>
<h3 class="text-lg font-semibold text-gray-900">Security</h3>
</div>
<!-- Password Field -->
<div>
<label for="{{ form.password.id_for_label }}" class="block text-sm font-medium text-gray-700 mb-2">
Password <span class="text-red-500">*</span>
</label>
<div class="relative">
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400">
<i class="fa-solid fa-lock"></i>
</span>
{{ form.password|add_class:"appearance-none block w-full pl-10 px-3 py-3 border border-gray-300 rounded-lg shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition duration-200" }}
</div>
<!-- Compact Password Requirements -->
<div class="mt-2">
<button type="button" id="password-toggle" class="flex items-center text-xs text-indigo-600 hover:text-indigo-500 transition duration-200">
<i class="fas fa-info-circle mr-1"></i>
View password requirements
</button>
<div id="password-requirements" class="hidden mt-2 p-3 bg-blue-50 border border-blue-200 rounded-lg">
<p class="text-xs font-medium text-blue-700 mb-2">Password Requirements:</p>
<ul class="text-xs text-blue-600 space-y-1">
<li class="flex items-start">
<span class="text-blue-500 mr-1"></span>
<span>Cannot be too similar to your personal information</span>
</li>
<li class="flex items-start">
<span class="text-blue-500 mr-1"></span>
<span>Must be at least 8 characters long</span>
</li>
<li class="flex items-start">
<span class="text-blue-500 mr-1"></span>
<span>Cannot be a commonly used password</span>
</li>
<li class="flex items-start">
<span class="text-blue-500 mr-1"></span>
<span>Cannot be entirely numeric</span>
</li>
</ul>
</div>
</div>
</div>
<!-- Confirm Password -->
<div>
<label for="{{ form.confirm_password.id_for_label }}" class="block text-sm font-medium text-gray-700 mb-2">
Confirm Password <span class="text-red-500">*</span>
</label>
<div class="relative">
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400">
<i class="fa-solid fa-lock"></i>
</span>
{{ form.confirm_password|add_class:"appearance-none block w-full pl-10 px-3 py-3 border border-gray-300 rounded-lg shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition duration-200" }}
</div>
</div>
</div>
<!-- Terms and Conditions -->
<div class="bg-indigo-50 p-4 rounded-lg border border-indigo-100">
<div class="flex items-start">
{{ form.terms_accepted|add_class:"h-5 w-5 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded mt-0.5" }}
<label for="{{ form.terms_accepted.id_for_label }}" class="ml-3 block text-sm text-gray-700">
<span class="font-medium text-gray-900">I accept the terms and conditions</span> <span class="text-red-500">*</span>
<p class="text-xs text-gray-500 mt-1">By creating an account, you agree to our Terms of Service and Privacy Policy.</p>
</label>
</div>
{% if form.terms_accepted.errors %}
<div class="text-red-600 text-sm mt-2 ml-8">
{{ form.terms_accepted.errors }}
</div>
{% endif %}
</div>
<!-- Submit Button -->
<div>
<button type="submit"
class="w-full flex justify-center py-3 px-4 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 transition duration-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 shadow-lg transform hover:scale-[1.02] active:scale-[0.98]">
<i class="fa-solid fa-rocket mr-2"></i>
Launch Your Startup Journey
</button>
</div>
<!-- Login Redirect -->
<div class="text-center pt-4">
<p class="text-sm text-gray-600">
Already have an account?
<a href="{% url 'login' %}" class="font-medium text-indigo-600 hover:text-indigo-500 transition duration-200 ml-1">
Sign in here
</a>
</p>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Email AJAX Validation -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(document).ready(function () {
// Email validation
$('#id_email').on('blur', function () {
const username = $(this).val();
if (username) {
$.ajax({
url: '{% url "check_username" %}',
data: { 'username': username },
dataType: 'json',
success: function (data) {
$('#email-status').removeClass('hidden');
if (data.is_taken) {
$('#email-icon').html('<i class="fa-solid fa-times-circle"></i>').removeClass('text-green-600').addClass('text-red-600');
$('#email-text').text('This email is already registered.').removeClass('text-green-600').addClass('text-red-600');
$('#id_email').addClass('border-red-500 focus:border-red-500 focus:ring-red-500');
} else {
$('#email-icon').html('<i class="fa-solid fa-check-circle"></i>').removeClass('text-red-600').addClass('text-green-600');
$('#email-text').text('Email is available.').removeClass('text-red-600').addClass('text-green-600');
$('#id_email').removeClass('border-red-500 focus:border-red-500 focus:ring-red-500');
}
},
error: function() {
$('#email-status').addClass('hidden');
}
});
} else {
$('#email-status').addClass('hidden');
}
});
// Password requirements toggle
$('#password-toggle').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
$('#password-requirements').toggleClass('hidden');
if ($('#password-requirements').hasClass('hidden')) {
$(this).html('<i class="fas fa-info-circle mr-1"></i>View password requirements');
} else {
$(this).html('<i class="fas fa-times mr-1"></i>Hide password requirements');
}
});
// Hide password requirements when clicking outside
$(document).on('click', function(e) {
if (!$(e.target).closest('#password-toggle, #password-requirements').length) {
$('#password-requirements').addClass('hidden');
$('#password-toggle').html('<i class="fas fa-info-circle mr-1"></i>View password requirements');
}
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends 'public_base.html' %}
{% block title %}
Verify
{% endblock %}
{% block content %}
<h1>You need to verify your email</h1>
<form method="post">
{% csrf_token %}
<input class="btn btn-primary max-btn" type="submit" value="Verify">
</form>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{% extends 'public_base.html' %}
{% block title %}
Verify
{% endblock %}
{% block content %}
<div class="alert alert-success">
You have successfully verified your e-mail
</div>
{% endblock %}

View File

@@ -0,0 +1,18 @@
{% extends 'public_base.html' %}
{% block title %}
Verify
{% endblock %}
{% block content %}
<div class="container">
{% if messages %}
{% for message in messages %}
<div class="alert">
{{ message }}
</div>
{% endfor %}
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,12 @@
{% extends 'public_base.html' %}
{% block title %}
Verify
{% endblock %}
{% block content %}
<h5>An email has been sent with instructions to verify your email</h5>
<h5>If you have not received the email. Please check the spam folder</h5>
{% endblock %}

View File

@@ -0,0 +1,20 @@
<div style="background-color: #c2c2c2; padding: 15px;">
<h2 style="margin: 0; padding: 10px; border-top-left-radius: 10px; border-top-right-radius: 10px; background-color: #fdc038; color: #ffffff;">Verify Email</h2>
<div style="margin: 0; padding: 15px; background-color: #ffffff;">
<p>Hi {{ user.name }},</p>
<p>You created an account on we-kwick, you need to verify your email. Please click on the button below to verify your email.</p>
<a href="{{ request.scheme }}://{{ domain }}{% url 'verify-email-confirm' uidb64=uid token=token %}"
style="border: 0; color: #ffffff; background-color: #fdc038; padding: 15px; font-weight: bold; text-decoration: none; border-radius: 5px; display: inline-block; margin-top: 20px;">
Verify Email
</a>
<p style="margin-top: 40px;">Or you can copy the link below to your browser</p>
<p>{{ request.scheme }}://{{ domain }}{% url 'verify-email-confirm' uidb64=uid token=token %}</p>
<p>The We-Kwick Team</p>
</div>
<div style="text-align: center; margin-top: 20px 0;">
<p>© {% now 'Y' %} <a href="">Blog</a></p>
<p>Follow us on <a href="">Twitter</a></p>
</div>
</div>

View File

@@ -0,0 +1,49 @@
{% extends 'public_base.html' %}
{% load static %}
{% block title %}Verify OTP{% endblock %}
{% block content %}
<div class="flex items-center justify-center min-h-screen bg-gray-100 px-4">
<div class="w-full max-w-md bg-white rounded-xl shadow-md p-6">
<h4 class="text-2xl font-semibold text-center text-gray-800 mb-6">Verify OTP</h4>
{% if messages %}
{% for message in messages %}
<div class="mb-4 px-4 py-3 rounded text-sm
{% if message.tags == 'error' %}
bg-red-100 text-red-700
{% elif message.tags == 'success' %}
bg-green-100 text-green-700
{% else %}
bg-blue-100 text-blue-700
{% endif %}
">
{{ message }}
</div>
{% endfor %}
{% endif %}
<form method="post" novalidate>
{% csrf_token %}
<div class="mb-4">
<label for="otpInput" class="block text-sm font-medium text-gray-700 mb-1">Enter OTP sent to your email</label>
<input type="text" name="otp" id="otpInput" maxlength="6" pattern="\d{6}"
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter 6-digit OTP" required>
</div>
<div class="mb-4">
<button type="submit"
class="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md text-lg font-medium transition">
<i class="fas fa-check-circle mr-2"></i>Verify OTP
</button>
</div>
</form>
<div class="text-center text-sm text-gray-600">
Didnt receive the OTP?
<a href="{% url 'request_otp' %}" class="text-blue-600 hover:underline">Resend</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,62 @@
{% extends "public_base.html" %}
{% block content %}
<div class="flex items-center justify-center min-h-screen bg-gray-100 px-4">
<div class="w-full max-w-md">
<div class="bg-white shadow-xl rounded-2xl overflow-hidden">
<!-- Header -->
<div class="px-6 py-4 border-b border-gray-200 text-center">
<h3 class="text-xl font-semibold text-gray-800">Forgot Password?</h3>
</div>
<!-- Error Messages -->
{% if form.errors %}
<div class="m-4 p-3 rounded-md bg-red-50 border border-red-200 text-red-700">
<ul class="list-disc list-inside text-sm">
{% for key, value in form.errors.items %}
<li>{{ value }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<!-- Body -->
<div class="px-6 py-4">
<form method="POST" class="space-y-4">
{% csrf_token %}
<!-- Email Field -->
<div>
<label for="id_email" class="block text-sm font-medium text-gray-700">Email</label>
<input type="email"
name="email"
id="id_email"
class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
placeholder="Enter email"
autocomplete="email"
maxlength="254"
required>
</div>
<!-- Submit Button -->
<div>
<button type="submit"
class="w-full py-2 px-4 bg-gray-900 text-white font-medium rounded-lg shadow hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900">
Submit
</button>
</div>
</form>
</div>
<!-- Footer -->
<div class="px-6 py-4 border-t border-gray-200 text-center">
<a href="{% url 'login' %}" class="text-sm text-indigo-600 hover:text-indigo-800">
Back To Login
</a>
</div>
</div>
</div>
</div>
{% endblock content %}

View File

@@ -0,0 +1,9 @@
<!-- templates/registration/password_reset_complete.html -->
{% extends 'base.html' %}
{% block title %}Password reset complete{% endblock %}
{% block content %}
<h1>Password reset complete</h1>
<p>Your new password has been set. You can log in now on the <a href="{% url 'login' %}">log in page</a>.</p>
{% endblock %}

View File

@@ -0,0 +1,42 @@
{% extends "base.html" %}
{% block title %}Password Reset{% endblock %}
{% block content %}
{% load crispy_forms_tags %}
<style>
.centered-form {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 1.0); /* Adjust the opacity as needed */
padding: 20px;
border-radius: 10px;
}
</style>
{% if validlink %}
<div class="container-fluid h-100">
<div class="row h-100">
<div class="col-sm-8">
<div class="centered-form">
<h3>Set a New Password</h3>
<form method="POST">
{% csrf_token %}
{{form|crispy}}
<button type="submit" class="btn btn-primary">Change My Password</button>
</form>
</div>
</div>
</div>
</div>
{% else %}
<p>The password reset link was invalid, possibly because it has already been used. Please request a new password reset.</p>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,26 @@
<!-- templates/registration/password_reset_done.html -->
{% extends "base.html" %}
{% block title %}Email Sent{% endblock %}
{% block content %}
<div>
{% if messages %}
{% for message in messages %}
<div class="alert mt-2
{% if 'success' in message.tags %} alert-success
{% elif 'warning' in message.tags %} alert-warning
{% elif 'info' in message.tags %} alert-info
{% else %} alert-danger
{% endif %}">
<span class="text-danger fst-italic">{{ message }}</span>
</div>
{% endfor %}
{% endif %}
</div>
<h1>Check your inbox.</h1>
<p>We've emailed you instructions for setting your password. You should receive the email shortly!</p>
{% endblock %}

View File

@@ -0,0 +1,13 @@
{% autoescape off %}
To initiate the password reset process for your we-kwick account,
click the link below:
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
If clicking the link above doesn't work, please copy and paste the URL below in a new browser
window instead.
Sincerely,
we-kwick Team
{% endautoescape %}

View File

@@ -0,0 +1,24 @@
<div style="background-color: #c2c2c2; padding: 15px;">
<h2 style="margin: 0; padding: 10px; border-top-left-radius: 10px; border-top-right-radius: 10px; background-color: #fdc038; color: #ffffff;">
Password Reset
</h2>
<div style="margin: 0; padding: 15px; background-color: #ffffff;">
<p>Hi {{ user.name }},</p>
<p>
To initiate the password reset process for your we-kwick account,
click the link below:
<a href="{{ request.scheme }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}"
style="border: 0; color: #ffffff; background-color: #fdc038; padding: 15px; font-weight: bold; text-decoration: none; border-radius: 5px; display: inline-block; margin-top: 20px;">
Reset Password
</a>
<p style="margin-top: 40px;">Or you can copy the link below to your browser</p>
<p>{{ request.scheme }}://{{ domain }}{% url 'verify-email-confirm' uidb64=uid token=token %}</p>
<p>The We-Kwick Team</p>
</div>
<div style="text-align: center; margin-top: 20px 0;">
<p>© {% now 'Y' %} <a href="">Blog</a></p>
<p>Follow us on <a href="">Twitter</a></p>
</div>
</div>

View File

@@ -0,0 +1 @@
We-Kwick Password Reset

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<title>User List</title>
</head>
<body>
<h2>User List</h2>
<ul>
{% for user in users %}
<li>
Name: {{ user.name }}, Birthday: {{ user.birthday }},
Address: {{ user.address }}, Email: {{ user.email_address }},
Contact: {{ user.contact_number }}
</li>
{% endfor %}
</ul>
<nav>
<ul>
<li><a href="{% url 'index' %}">Index</a></li>
<li><a href="{% url 'home' %}">Home</a></li>
<li><a href="{% url 'signup' %}">Create user</a></li>
</ul>
</nav>
</body>
</html>

View File

@@ -0,0 +1,7 @@
from django import template
register = template.Library()
@register.filter(name='add_class')
def add_class(field, css):
return field.as_widget(attrs={"class": css})

View File

@@ -0,0 +1,8 @@
from django import template
from django.forms.widgets import Textarea
register = template.Library()
@register.filter
def is_textarea(field):
return isinstance(field.field.widget, Textarea)

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,10 @@
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from six import text_type
class TokenGenerator(PasswordResetTokenGenerator):
def _make_hash_value(self, user, timestamp):
return (
text_type(user.pk) + text_type(timestamp) +
text_type(getattr(user, 'is_active', True))
)
account_activation_token = TokenGenerator()

View File

@@ -0,0 +1,57 @@
# urls.py
from django.urls import path
from django.contrib.auth.decorators import login_required
from django.contrib.auth import views as auth_views
from . import views
from at_django_boilerplate.accounts.api import v1 as api_v1
urlpatterns = [
path('signup/', views.SignupView.as_view(), name='signup'),
path('ajax/check_username/', views.CheckUsernameAvailability.as_view(), name='check_username'),
path('login/', views.LoginPageView.as_view(), name='login'),
path('login/', views.LoginPageView.as_view(), name='signin'),
path('login', views.LoginPageView.as_view(), name='login'),
path('login', views.LoginPageView.as_view(), name='signin'),
path('profile/', login_required(views.ProfileView.as_view()), name='user_profile'),
path('update_profile/<uuid:pk>', login_required(views.UpdateProfileView.as_view()), name='update_profile'),
path('update_profile_picture/', login_required(views.update_profile_picture), name='update_profile_picture'),
path('password-reset/', views.ResetPasswordView.as_view(), name='password_reset'),
path('password-reset/applied', views.ResetPasswordDoneView.as_view(), name='password_reset_done'),
path('password-reset-confirm/<uidb64>/<token>/',
auth_views.PasswordResetConfirmView.as_view(template_name='reset/password_reset_confirm.html'),
name='password_reset_confirm'),
path('password-reset-complete/',
auth_views.PasswordResetCompleteView.as_view(template_name='reset/password_reset_complete.html'),
name='password_reset_complete'),
path('password_change/',views.ChangePasswordView.as_view()),
path('verify-email/', views.verify_email, name='verify-email'),
path('verify-email/done/', views.verify_email_done, name='verify-email-done'),
path('verify-email-confirm/<uidb64>/<token>/', views.verify_email_confirm, name='verify-email-confirm'),
path('verify-email/complete/', views.verify_email_complete, name='verify-email-complete'),
path('logout/', views.LogoutView.as_view(), name='logout'),
# Otp via signin
path('request-otp/', views.request_otp_view, name='request_otp'),
path('verify-otp/', views.verify_otp_view, name='verify_otp'),
]
api_v1=[
path('api/login/', api_v1.LoginApiView.as_view(), name='login-api'),
path('validate-password/', api_v1.ValidatePasswordAPIView.as_view(), name='validate-password-api'),
path('api/reset-password/', api_v1.ResetPasswordAPIView.as_view(), name='reset-password-api'),
path('reset-password/<uidb64>/<token>/', api_v1.ResetPasswordConfirmAPIView.as_view(), name='reset-password-confirm'),
path('api/profile/', api_v1.UserProfileView.as_view(), name='profile-api'),
]
urlpatterns +=api_v1

View File

@@ -0,0 +1,189 @@
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from django.template.loader import render_to_string
from .tokens import account_activation_token
from django.core.mail import EmailMessage
from django.shortcuts import redirect
from django.core.mail import EmailMultiAlternatives
from django.conf import settings
from django.contrib.auth import login
from .models import CustomUser
from at_django_boilerplate.utils.hash_utils import hexdigest
from at_django_boilerplate.utils.encryption_utils import EncryptionUtils
def send_activation_email(request, user, to_email):
try:
domain = request.get_host()
message = render_to_string('registration/verify_email_message.html', {
'request': request,
'user': user,
'domain': domain,
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
'token': account_activation_token.make_token(user),
})
subject = 'Welcome to we-kwick. Verify your Email'
from_email = settings.EMAIL_HOST_USER
_to_email = [to_email]
text_content = ''
html_content = message
email = EmailMultiAlternatives(subject, text_content, from_email, _to_email)
email.attach_alternative(html_content, "text/html")
email.send()
print(f'Sent Joining Email to {to_email} ')
return True
except Exception as e:
print('Failed to Send Email')
return False
def redirect_to_next_or_home(request):
try:
if 'next' in request.POST:
_next = request.POST.get('next')
if _next is not None:
if _next == '' or _next == '/':
return redirect('home')
else:
return redirect(_next)
else:
return redirect('home')
else:
return redirect('home')
except Exception as e:
print('Exception Post:', e)
return redirect('home')
def save_user(request,user_form):
email = user_form.cleaned_data['email'].lower()
contact_number = user_form.cleaned_data['contact_number']
email_hash = hexdigest(email)
existing_email = CustomUser.objects.filter(email_hash=email_hash)
existing_email_exists = existing_email.exists()
existing_number_exists = False
if all([contact_number != '',contact_number != None]):
contact_number_hash = hexdigest(contact_number)
existing_number = CustomUser.objects.filter(contact_number_hash=contact_number_hash)
existing_number_exists = existing_number.exists()
if existing_email_exists:
user_form.add_error('email', 'Email is already in use.')
elif existing_number_exists:
user_form.add_error('contact_number', 'Contact number is already in use.')
else:
try:
user = user_form.save(commit=False)
user.set_password(user.password)
user.save(custom_save=True)
login(request, user)
user.is_active = False
user.save()
send_activation_email(request=request,user=user,to_email=user.get_decrypted_email())
return True,user
except Exception as e:
return False,user_form
return False,user_form
# Otp
import random
import re
from django.core.mail import send_mail
from django.core.cache import cache
from django.conf import settings
PHONE_REGEX = re.compile(r'^\+?\d{10,15}$')
def generate_otp(length=6):
return ''.join([str(random.randint(0, 9)) for _ in range(length)])
# def send_otp(identifier, purpose='login'):
# """
# Sends an OTP via email or SMS based on the identifier.
# Returns a tuple: (success: bool, method: 'email'|'sms')
# """
# otp = generate_otp()
# cache_key = f'otp_{purpose}_{identifier}'
# if PHONE_REGEX.match(identifier):
# method = 'sms'
# cache.set(cache_key, otp, timeout=300)
# # Replace with your SMS API logic (e.g., Twilio)
# print(f"📲 SMS OTP sent to {identifier}: {otp}")
# else:
# method = 'email'
# cache.set(cache_key, otp, timeout=300)
# try:
# send_mail(
# subject=f"Your {purpose.capitalize()} OTP",
# message=f"Your {purpose} OTP is: {otp}\n\nThis OTP is valid for 5 minutes.",
# from_email=settings.EMAIL_HOST_USER,
# recipient_list=[identifier],
# fail_silently=False,
# )
# except Exception as e:
# print(f"❌ Failed to send OTP email to {identifier}: {e}")
# return False, 'email'
# return True, method
from django.template.loader import render_to_string
from django.core.mail import EmailMessage
def send_otp(identifier, purpose='login'):
"""
Sends an OTP via email or SMS based on the identifier.
Returns a tuple: (success: bool, method: 'email'|'sms')
"""
otp = generate_otp()
cache_key = f'otp_{purpose}_{identifier}'
if PHONE_REGEX.match(identifier):
method = 'sms'
cache.set(cache_key, otp, timeout=300)
# Replace with your SMS API logic
print(f"📲 SMS OTP sent to {identifier}: {otp}")
else:
method = 'email'
cache.set(cache_key, otp, timeout=300)
try:
email_subject = f"Your {purpose.capitalize()} OTP"
email_body = render_to_string('otp/otp_email.html', {
'otp': otp,
'purpose': purpose,
'identifier': identifier,
'validity_minutes': 5,
})
email = EmailMessage(
subject=email_subject,
body=email_body,
from_email=settings.EMAIL_HOST_USER,
to=[identifier],
)
email.content_subtype = 'html' # Important for HTML emails
email.send(fail_silently=False)
except Exception as e:
print(f"❌ Failed to send OTP email to {identifier}: {e}")
return False, 'email'
return True, method
def verify_otp(identifier, user_input_otp, purpose='login'):
"""
Verifies the OTP entered by the user against the cache.
"""
cache_key = f'otp_{purpose}_{identifier}'
cached_otp = cache.get(cache_key)
if cached_otp and cached_otp == user_input_otp:
cache.delete(cache_key)
return True
return False

View File

@@ -0,0 +1,415 @@
from django.urls import reverse_lazy
from django.views import View
from django.views.generic import FormView
from django.views.generic.edit import UpdateView
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
from django.contrib.auth import login,authenticate,logout
from django.contrib import messages
from django.shortcuts import redirect, render
from django.db import IntegrityError
import hashlib
from django.contrib.auth.tokens import default_token_generator
from django.contrib.auth.decorators import login_required
from .forms import (UserSignUpForm,
SigninForm,
CustomPasswordResetForm,
ProfilePictureUpdateForm,
UpdateProfileForm)
from django.contrib.auth.views import (PasswordResetView,
PasswordChangeView)
from django.http import JsonResponse
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth.mixins import UserPassesTestMixin
from .models import CustomUser
from django.utils.encoding import force_str
from django.utils.http import urlsafe_base64_decode
from .tokens import account_activation_token
from django.contrib import messages
from django.contrib.auth import authenticate, login
from django.utils.http import url_has_allowed_host_and_scheme
from .utils import (send_activation_email,
redirect_to_next_or_home,
)
from at_django_boilerplate.utils.encryption_utils import EncryptionUtils
from django.shortcuts import render, redirect
from .forms import UserSignUpForm
import logging
logger = logging.getLogger(__name__)
class CheckUsernameAvailability(View):
def get(self, request):
username = request.GET.get('username', None)
is_taken = CustomUser.objects.filter_by_email(username).exists()
data = {
'is_taken': is_taken
}
return JsonResponse(data)
class SignupView(FormView):
template_name = 'registration/signup.html'
form_class = UserSignUpForm
success_url = reverse_lazy('home')
def post(self, request, *args, **kwargs):
next_url = request.GET.get('next')
user_form = self.form_class(request.POST)
if user_form.is_valid():
email = user_form.cleaned_data['email'].lower()
contact_number = user_form.cleaned_data.get('contact_number')
email_exists = CustomUser.objects.filter_by_email(email).exists()
number_exists = False
if contact_number:
number_exists = CustomUser.objects.filter_by_contact_number(contact_number).exists()
if email_exists:
user_form.add_error('email', 'Email is already in use.')
elif number_exists:
user_form.add_error('contact_number', 'Contact number is already in use.')
else:
try:
user = user_form.save(commit=False)
user.set_password(user.password)
user.save()
login(request, user)
user.is_active = True
user.save()
send_activation_email(
request=request,
user=user,
to_email=user.get_decrypted_email()
)
# return redirect(next_url or 'verify-email-done')
return redirect('dashboard')
except IntegrityError:
# Rollback if needed
user.delete()
return self.render_to_response(self.get_context_data(form=user_form))
def verify_email(request):
return render(request, 'registration/verify_email.html')
def verify_email_done(request):
return render(request, 'registration/verify_email_done.html')
def verify_email_confirm(request, uidb64, token):
try:
uid = force_str(urlsafe_base64_decode(uidb64))
user = CustomUser.objects.get(pk=uid)
except(TypeError, ValueError, OverflowError, CustomUser.DoesNotExist):
user = None
if user is not None and account_activation_token.check_token(user, token):
user.is_active = True
user.save()
messages.success(request, 'Your email has been verified.')
return redirect('verify-email-complete')
else:
messages.warning(request, 'The link is invalid.')
return render(request, 'registration/verify_email_confirm.html')
def verify_email_complete(request):
return render(request, 'registration/verify_email_complete.html')
class LoginPageView(View):
template_name = 'login.html'
form_class = SigninForm
def get(self, request):
if request.user.is_authenticated:
next_url = request.GET.get('next')
if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts={request.get_host()}):
return redirect(next_url)
return redirect('home')
form = self.form_class()
return render(request, self.template_name, {'form': form, 'message': ''})
def post(self, request):
print('Login')
form = self.form_class(request.POST)
message = 'Login failed!'
if form.is_valid():
print('Form Valid')
username = form.cleaned_data['username'].lower()
password = form.cleaned_data['password']
user = authenticate(request, username=username, password=password)
# user.backend = 'accounts.auth_backends.CustomAuthBackend' # <--- key line added
print(f"Authenticated User: {user}")
if user:
if user.is_active:
login(request, user)
return redirect_to_next_or_home(request)
else:
messages.error(request, 'Please verify your account before logging in.')
else:
messages.error(request, 'Invalid username or password.')
else:
messages.error(request, 'Invalid input.')
return render(request, self.template_name, {
'form': form,
'message': message,
})
class LogoutView(View):
def get(self, request):
logout(request)
return redirect('home')
class ResetPasswordView(SuccessMessageMixin, PasswordResetView):
template_name = 'reset/password_reset.html'
email_template_name = 'reset/password_reset_email.html'
html_email_template_name = 'reset/password_reset_email_html.html'
subject_template_name = 'reset/password_reset_subject.txt'
success_message = "We've emailed you instructions for setting your password, " \
"if an account exists with the email you entered. You should receive them shortly." \
" If you don't receive an email, " \
"please make sure you've entered the address you registered with, and check your spam folder."
success_url = reverse_lazy('password_reset_done')
token_generator = default_token_generator
form_class = CustomPasswordResetForm
class ResetPasswordDoneView(View):
template_name = 'reset/password_reset_done.html'
success_message = "We've emailed you instructions for setting your password, " \
"if an account exists with the email you entered. You should receive them shortly." \
" If you don't receive an email, " \
"please make sure you've entered the address you registered with, and check your spam folder."
def get(self, request):
return render(request=request,template_name=self.template_name)
class ChangePasswordView(SuccessMessageMixin, PasswordChangeView):
template_name = 'change_password.html'
success_message = "Successfully Changed Your Password"
success_url = reverse_lazy('home')
class CustomUserListView(ListView):
model = CustomUser
template_name = 'user_list.html'
users = 'CustomUsers'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
users = CustomUser.objects.all()
context['users'] = [{
'username': custom_user.username,
'name': custom_user.get_decrypted_name(),
'email': custom_user.get_decrypted_email(),
'birthday': custom_user.birthday,
'address': custom_user.get_decrypted_address(),
'contact_number': custom_user.get_decrypted_contact_number()
} for custom_user in users]
return context
class ProfileView(DetailView):
model = CustomUser
template_name = 'profile/profile.html'
context_object_name = 'custom_user'
def get_object(self):
return self.request.user
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Initialize the form with the current user's data
form = ProfilePictureUpdateForm(instance=self.get_object())
context['update_profile_photo_form'] = form
return context
class UpdateProfileView(UserPassesTestMixin, UpdateView):
model = CustomUser
form_class = UpdateProfileForm
template_name = 'profile/update_profile.html'
def test_func(self):
obj = self.get_object()
requested_by_user = self.request.user
return (obj == requested_by_user)
def get(self, request, *args, **kwargs):
self.object = self.get_object()
# Decrypt the data before passing it to the form
encryption_tool = EncryptionUtils()
contact_number = None
if self.object.contact_number:
contact_number = encryption_tool.decrypt(self.object.contact_number)
initial_data = {
'contact_number': contact_number
}
form = self.form_class(instance=self.object, initial=initial_data)
return self.render_to_response(self.get_context_data(form=form))
def post(self, request, *args, **kwargs):
self.object = self.get_object()
form = self.form_class(request.POST, instance=self.object)
if form.is_valid():
obj = form.save(commit=False)
encryption_tool = EncryptionUtils()
# address = form.cleaned_data['address']
# obj.address = encryption_tool.encrypt(address)
contact_number = form.cleaned_data['contact_number']
if all([contact_number!=None,contact_number!='']):
obj.contact_number = encryption_tool.encrypt(contact_number)
obj.save()
return self.form_valid(form)
else:
# Decrypt the data before re-rendering the form with errors
encryption_tool = EncryptionUtils()
contact_number = None
if self.object.contact_number:
contact_number = encryption_tool.decrypt(self.object.contact_number)
initial_data = {
'contact_number': contact_number
}
form = self.form_class(instance=self.object, initial=initial_data)
return self.form_invalid(form)
@login_required
def update_profile_picture(request):
if request.user.pk !=None:
user_profile = request.user
if request.method == 'POST':
form = ProfilePictureUpdateForm(request.POST, request.FILES, instance=user_profile)
if form.is_valid():
form.save()
messages.success(request, 'Your profile picture has been updated successfully!')
return redirect('user_profile')
else:
form = ProfilePictureUpdateForm(instance=user_profile)
return render(request, 'profile/update_profile_picture.html', {'form': form})
from django.core.validators import EmailValidator, ValidationError
from django.core.cache import cache
from .utils import send_otp, verify_otp
from .models import CustomUser
import re
PHONE_REGEX = re.compile(r'^\+?\d{10,15}$')
def is_email(identifier):
validator = EmailValidator()
try:
validator(identifier)
return True
except ValidationError:
return False
def request_otp_view(request):
if request.method == 'POST':
identifier = request.POST.get('identifier', '').strip().lower()
if not identifier:
messages.error(request, "Please enter your email or mobile number.")
return redirect('login') # Assuming OTP modal is on the login page
print(f"Identifier: {identifier}")
user = None
if is_email(identifier):
print(f"Email: {identifier}")
user = CustomUser.objects.filter_by_email(email=identifier).first()
else:
if not PHONE_REGEX.match(identifier):
messages.error(request, "Invalid mobile number format.")
return redirect('login')
print(f"Mobile hash: {identifier}")
user = CustomUser.objects.filter_by_contact_number(contact_number=identifier).first()
print(f"User: {user}")
if not user:
messages.error(request, "No user found with this email or mobile number.")
return redirect('login')
# Rate limiting
cache_key = f"otp_rate_limit_{identifier}"
if cache.get(cache_key):
messages.error(request, "Please wait before requesting another OTP.")
return redirect('login')
# Send OTP
success, _ = send_otp(identifier, purpose='login')
if success:
cache.set(cache_key, True, timeout=60) # 60 seconds
request.session['otp_identifier'] = identifier
messages.success(request, "OTP sent successfully.")
return redirect('verify_otp')
else:
messages.error(request, "Failed to send OTP. Please try again.")
return redirect('login')
return redirect('login')
def verify_otp_view(request):
identifier = request.session.get('otp_identifier')
if not identifier:
messages.error(request, "Session expired. Please request a new OTP.")
return redirect('login')
if request.method == 'POST':
user_otp = request.POST.get('otp', '').strip()
if not user_otp:
messages.error(request, "Please enter the OTP.")
return render(request, 'registration/verify_otp.html')
if verify_otp(identifier, user_otp):
# Match user
if is_email(identifier):
print(f"Email: {identifier}")
user = CustomUser.objects.get_by_email(email=identifier)
else:
user = CustomUser.objects.get_by_contact_number(contact_number=identifier)
# user.backend = 'django.contrib.auth.backends.ModelBackend' # <--- key line added
login(request, user)
messages.success(request, "Logged in successfully.")
request.session.pop('otp_identifier', None)
return redirect('home')
else:
messages.error(request, "Invalid or expired OTP.")
return render(request, 'registration/verify_otp.html')

View File

@@ -0,0 +1,5 @@
from django.contrib import admin
from .models import SEOConfiguration
# Register your models here.
admin.site.register(SEOConfiguration)

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class BackendAdminConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'at_django_boilerplate.backend_admin'

View File

@@ -0,0 +1,38 @@
from django import forms
from .models import SEOConfiguration
class SEOConfigurationForm(forms.ModelForm):
class Meta:
model = SEOConfiguration
fields = "__all__"
# widgets = {
# "home_page_meta_title": forms.Text(attrs={"class": "form-control", "placeholder": "Home Page Meta Title"}),
# "home_page_meta_description": forms.Textarea(attrs={"class": "form-control", "rows": 3, "placeholder": "Home Page Meta Description"}),
# "about_meta_title": forms.TextInput(attrs={"class": "form-control", "placeholder": "About Page Meta Title"}),
# "about_meta_description": forms.Textarea(attrs={"class": "form-control", "rows": 3, "placeholder": "About Page Meta Description"}),
# "tools_meta_title": forms.TextInput(attrs={"class": "form-control", "placeholder": "Tools Page Meta Title"}),
# "tools_meta_description": forms.Textarea(attrs={"class": "form-control", "rows": 3, "placeholder": "Tools Page Meta Description"}),
# "contact_meta_title": forms.TextInput(attrs={"class": "form-control", "placeholder": "Contact Page Meta Title"}),
# "contact_meta_description": forms.Textarea(attrs={"class": "form-control", "rows": 3, "placeholder": "Contact Page Meta Description"}),
# "career_meta_title": forms.TextInput(attrs={"class": "form-control", "placeholder": "Career Page Meta Title"}),
# "career_meta_description": forms.Textarea(attrs={"class": "form-control", "rows": 3, "placeholder": "Career Page Meta Description"}),
# "blog_meta_title": forms.TextInput(attrs={"class": "form-control", "placeholder": "Blog Page Meta Title"}),
# "blog_meta_description": forms.Textarea(attrs={"class": "form-control", "rows": 3, "placeholder": "Blog Page Meta Description"}),
# "term_and_con_meta_title": forms.TextInput(attrs={"class": "form-control", "placeholder": "Terms & Conditions Meta Title"}),
# "term_and_con_meta_description": forms.Textarea(attrs={"class": "form-control", "rows": 3, "placeholder": "Terms & Conditions Meta Description"}),
# "term_and_con_canonical_url": forms.URLInput(attrs={"class": "form-control", "placeholder": "Canonical URL"}),
# "privacy_pol_meta_title": forms.TextInput(attrs={"class": "form-control", "placeholder": "Privacy Policy Meta Title"}),
# "privacy_pol_meta_description": forms.Textarea(attrs={"class": "form-control", "rows": 3, "placeholder": "Privacy Policy Meta Description"}),
# "privacy_pol_canonical_url": forms.URLInput(attrs={"class": "form-control", "placeholder": "Canonical URL"}),
# }

View File

@@ -0,0 +1,38 @@
# Generated by Django 5.2.6 on 2025-12-29 05:20
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='SEOConfiguration',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('home_page_meta_title', models.CharField(help_text='SEO title for the page (max 60 characters).', max_length=100)),
('home_page_meta_description', models.CharField(help_text='Meta description for search engines (max 160 characters).', max_length=300)),
('about_meta_title', models.CharField(help_text='SEO title for the page (max 60 characters).', max_length=100)),
('about_meta_description', models.CharField(help_text='Meta description for search engines (max 160 characters).', max_length=300)),
('tools_meta_title', models.CharField(help_text='SEO title for the page (max 60 characters).', max_length=100)),
('tools_meta_description', models.CharField(help_text='Meta description for search engines (max 160 characters).', max_length=300)),
('contact_meta_title', models.CharField(help_text='SEO title for the page (max 60 characters).', max_length=100)),
('contact_meta_description', models.CharField(help_text='Meta description for search engines (max 160 characters).', max_length=300)),
('career_meta_title', models.CharField(help_text='SEO title for the page (max 60 characters).', max_length=100)),
('career_meta_description', models.CharField(help_text='Meta description for search engines (max 160 characters).', max_length=300)),
('blog_meta_title', models.CharField(help_text='SEO title for the page (max 60 characters).', max_length=100)),
('blog_meta_description', models.CharField(help_text='Meta description for search engines (max 160 characters).', max_length=300)),
('term_and_con_meta_title', models.CharField(help_text='SEO title for the page (max 60 characters).', max_length=100)),
('term_and_con_meta_description', models.CharField(help_text='Meta description for search engines (max 160 characters).', max_length=300)),
('term_and_con_canonical_url', models.URLField(blank=True, help_text='Canonical URL to avoid duplicate content.', null=True)),
('privacy_pol_meta_title', models.CharField(help_text='SEO title for the page (max 60 characters).', max_length=100)),
('privacy_pol_meta_description', models.CharField(help_text='Meta description for search engines (max 160 characters).', max_length=300)),
('privacy_pol_canonical_url', models.URLField(blank=True, help_text='Canonical URL to avoid duplicate content.', null=True)),
],
),
]

View File

@@ -0,0 +1,30 @@
from django.db import models
class SEOConfiguration(models.Model):
home_page_meta_title = models.CharField(max_length=100, help_text="SEO title for the page (max 60 characters).")
home_page_meta_description = models.CharField(max_length=300, help_text="Meta description for search engines (max 160 characters).")
about_meta_title = models.CharField(max_length=100, help_text="SEO title for the page (max 60 characters).")
about_meta_description = models.CharField(max_length=300, help_text="Meta description for search engines (max 160 characters).")
tools_meta_title = models.CharField(max_length=100, help_text="SEO title for the page (max 60 characters).")
tools_meta_description = models.CharField(max_length=300, help_text="Meta description for search engines (max 160 characters).")
contact_meta_title = models.CharField(max_length=100, help_text="SEO title for the page (max 60 characters).")
contact_meta_description = models.CharField(max_length=300, help_text="Meta description for search engines (max 160 characters).")
career_meta_title = models.CharField(max_length=100, help_text="SEO title for the page (max 60 characters).")
career_meta_description = models.CharField(max_length=300, help_text="Meta description for search engines (max 160 characters)." )
blog_meta_title = models.CharField(max_length=100, help_text="SEO title for the page (max 60 characters).")
blog_meta_description = models.CharField(max_length=300, help_text="Meta description for search engines (max 160 characters).")
term_and_con_meta_title = models.CharField(max_length=100, help_text="SEO title for the page (max 60 characters).")
term_and_con_meta_description = models.CharField(max_length=300, help_text="Meta description for search engines (max 160 characters).")
term_and_con_canonical_url = models.URLField(blank=True, null=True, help_text="Canonical URL to avoid duplicate content.")
privacy_pol_meta_title = models.CharField(max_length=100, help_text="SEO title for the page (max 60 characters).")
privacy_pol_meta_description = models.CharField(max_length=300, help_text="Meta description for search engines (max 160 characters).")
privacy_pol_canonical_url = models.URLField(blank=True, null=True, help_text="Canonical URL to avoid duplicate content.")

View File

@@ -0,0 +1,25 @@
{% extends 'base.html' %}
{% block content %}
{% load crispy_forms_tags %}
<h2 class="text-2xl font-bold mb-6 text-gray-800">Admin Dashboard</h2>
<div class="flex gap-5">
<a href="{% url 'contact_list' %}"
class="px-6 py-3 border-2 border-blue-600 rounded-xl font-semibold text-blue-600 hover:text-blue-800 transition-colors">
View Contacts
</a>
<a href="{% url 'subscriber_list' %}"
class="px-6 py-3 border-2 border-green-600 rounded-xl font-semibold text-green-600 hover:text-green-800 transition-colors">
View Subscribers
</a>
<a href="{% url 'user_activity' %}"
class="px-6 py-3 border-2 border-green-600 rounded-xl font-semibold text-green-600 hover:text-green-800 transition-colors">
View Activiy
</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,188 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Appointment Leads - Admin{% endblock %}
{% block content %}
<div class="container mx-auto mt-10 px-4 max-w-7xl">
<div class="bg-white shadow-lg rounded-2xl overflow-hidden">
<!-- Header -->
<div class="px-8 py-6 border-b border-gray-200 bg-gradient-to-r from-indigo-50 to-purple-50">
<h2 class="text-3xl font-bold text-gray-800">Appointment Requests</h2>
<p class="text-gray-600 mt-2">Manage appointments: update status, reschedule, and track progress</p>
</div>
{% if appointments %}
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<!-- Table Head -->
<thead class="bg-gray-100">
<tr>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">#</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ticket ID</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Contact</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Expert</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Scheduled For</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{% for apt in appointments %}
<tr class="hover:bg-gray-50 transition">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">{{ forloop.counter }}</td>
<td class="px-6 py-4 whitespace-nowrap font-medium text-indigo-600">
APP-{{ apt.pk|stringformat:"06d" }}
</td>
<td class="px-6 py-4 whitespace-nowrap font-medium text-gray-900">
{{ apt.full_name }}
</td>
<td class="px-6 py-4 text-sm text-gray-600">
{{ apt.email }}<br>
<small class="text-gray-500">{{ apt.phone }}</small>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
{{ apt.get_meeting_with_display }}
</td>
<td class="px-6 py-4 text-sm">
{{ apt.appointment_datetime|date:"d M Y" }} at {{ apt.appointment_datetime|time:"h:i A" }}
</td>
<!-- Status Badge -->
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-3 py-1 inline-flex text-xs font-semibold rounded-full
{% if apt.status == 'pending' %}bg-yellow-100 text-yellow-800
{% elif apt.status == 'contacted' %}bg-blue-100 text-blue-800
{% elif apt.status == 'in_process' %}bg-purple-100 text-purple-800
{% elif apt.status == 'rescheduled' %}bg-indigo-100 text-indigo-800
{% elif apt.status == 'completed' %}bg-green-100 text-green-800
{% elif apt.status == 'cancelled' %}bg-red-100 text-red-800
{% else %}bg-gray-100 text-gray-800{% endif %}">
{{ apt.get_status_display }}
</span>
</td>
<!-- Actions -->
<td class="px-6 py-4 text-sm">
<button onclick="openModal('modal-{{ apt.id }}')"
class="bg-gradient-to-r from-indigo-600 to-purple-600 text-white px-4 py-2 rounded-lg text-xs font-medium hover:shadow-lg transition">
Manage
</button>
<!-- MODAL -->
<div id="modal-{{ apt.id }}" class="fixed inset-0 bg-black bg-opacity-60 hidden flex items-center justify-center z-50">
<div class="bg-white w-full max-w-lg rounded-2xl shadow-2xl p-8 relative animate-fadeIn">
<button onclick="closeModal('modal-{{ apt.id }}')"
class="absolute top-4 right-6 text-gray-400 hover:text-gray-600 text-3xl">&times;</button>
<h3 class="text-2xl font-bold mb-6 text-gray-800">
Manage Appointment {{ apt.full_name }}
</h3>
<p class="text-sm text-gray-500 mb-6">Ticket: APP-{{ apt.pk|stringformat:"06d" }}</p>
<!-- Update Status -->
<form method="post" class="mb-8">
{% csrf_token %}
<input type="hidden" name="appointment_id" value="{{ apt.id }}">
<input type="hidden" name="action" value="update_status">
<label class="block text-sm font-semibold text-gray-700 mb-2">Update Status</label>
<select name="status" class="w-full border border-gray-300 rounded-lg px-4 py-3 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
{% for value, label in status_choices %}
<option value="{{ value }}" {% if apt.status == value %}selected{% endif %}>
{{ label }}
</option>
{% endfor %}
</select>
<button type="submit" class="mt-4 w-full bg-indigo-600 text-white py-3 rounded-lg font-medium hover:bg-indigo-700 transition">
Update Status
</button>
</form>
<!-- Reschedule -->
<form method="post">
{% csrf_token %}
<input type="hidden" name="appointment_id" value="{{ apt.id }}">
<input type="hidden" name="action" value="reschedule">
<label class="block text-sm font-semibold text-gray-700 mb-2">Reschedule Appointment</label>
<div class="grid grid-cols-2 gap-4 mb-4">
<input type="date" name="new_date"
value="{{ apt.appointment_datetime|date:'Y-m-d' }}"
class="border border-gray-300 rounded-lg px-4 py-3" required>
<input type="time" name="new_time"
value="{{ apt.appointment_datetime|time:'H:i' }}"
class="border border-gray-300 rounded-lg px-4 py-3" required>
</div>
<button type="submit" class="w-full bg-green-600 text-white py-3 rounded-lg font-medium hover:bg-green-700 transition">
Reschedule Appointment
</button>
</form>
<!-- Notes (Read-only) -->
{% if apt.notes %}
<div class="mt-8 p-4 bg-gray-50 rounded-lg">
<p class="text-sm font-medium text-gray-700">User Notes:</p>
<p class="text-sm text-gray-600 mt-1">{{ apt.notes }}</p>
</div>
{% endif %}
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="p-16 text-center">
<p class="text-xl text-gray-500">No appointment requests yet.</p>
</div>
{% endif %}
</div>
</div>
<!-- Modal Scripts -->
<script>
function openModal(id) {
document.getElementById(id).classList.remove('hidden');
document.getElementById(id).classList.add('flex');
}
function closeModal(id) {
document.getElementById(id).classList.add('hidden');
document.getElementById(id).classList.remove('flex');
}
// Close modal when clicking outside
window.addEventListener('click', function(e) {
document.querySelectorAll('[id^="modal-"]').forEach(modal => {
if (e.target === modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
});
});
</script>
<style>
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fadeIn { animation: fadeIn 0.3s ease-out; }
</style>
{% endblock %}

View File

@@ -0,0 +1,239 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<!-- Back -->
<div class="mb-6">
<a href="{% url 'admin_dashboard' %}"
class="inline-block px-4 py-2 border-2 border-gray-800 rounded-xl font-bold text-gray-800 hover:text-gray-900 hover:border-gray-900">
&larr; Admin Dashboard
</a>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-8">
<h1 class="text-3xl font-bold mb-6 text-gray-800 text-center">All Contacts</h1>
<!-- ================= FILTER ================= -->
<!-- ================= FILTER ================= -->
<form method="get" class="mb-6 flex flex-col sm:flex-row gap-3">
<!-- Search -->
<input
type="text"
name="search"
value="{{ request.GET.search }}"
placeholder="Search by name, email, or message..."
class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring"
>
<!-- Junk Filter -->
<select
name="status"
class="px-4 py-2 border rounded-lg focus:outline-none focus:ring">
<option value="">All Messages</option>
<option value="inbox"
{% if request.GET.status == "inbox" %}selected{% endif %}>
Inbox
</option>
<option value="junk"
{% if request.GET.status == "junk" %}selected{% endif %}>
Junk
</option>
</select>
<button class="px-6 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700">
Apply
</button>
</form>
<!-- ================= TABLE ================= -->
<div class="overflow-x-auto shadow-lg rounded-lg">
<table class="min-w-[700px] w-full table-auto bg-white divide-y divide-gray-200">
<thead class="bg-indigo-600">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-white uppercase">Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-white uppercase">Email</th>
<th class="px-6 py-3 text-left text-xs font-medium text-white uppercase">Message</th>
<th class="px-6 py-3 text-left text-xs font-medium text-white uppercase">Date</th>
<th class="px-6 py-3 text-left text-xs font-medium text-white uppercase">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{% for c in contacts %}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 font-medium">
{{ c.name }}
</td>
<td class="px-6 py-4 text-gray-600">
{{ c.email }}
</td>
<td class="px-6 py-4 max-w-xs truncate text-gray-700">
{{ c.message }}
</td>
<td class="px-6 py-4 text-gray-500 whitespace-nowrap">
{{ c.created_at|date:"M d, Y H:i" }}
</td>
<!-- ================= ACTIONS ================= -->
<td class="px-6 py-4 flex gap-3">
<!-- View -->
<button
onclick="openModal(
'{{ c.name }}',
'{{ c.email }}',
'{{ c.message|escapejs }}',
'{{ c.created_at|date:"M d, Y H:i" }}'
)"
class="text-indigo-600 hover:text-indigo-800"
title="View">
<i class="fas fa-eye"></i>
</button>
<!-- Junk -->
{% if not c.is_junk %}
<form method="post"
action="{% url 'set_message_to_junk' c.id %}"
onsubmit="return confirmJunk('{{ c.email }}', '{{ c.phone|default:'' }}')">
{% csrf_token %}
<button
type="submit"
class="text-red-600 hover:text-red-800"
title="Mark as junk">
<i class="fas fa-trash"></i>
</button>
</form>
{% else %}
<span class="text-xs text-gray-400">Junk</span>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center py-10 text-gray-400">
No contacts found.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- ================= PAGINATION ================= -->
{% if is_paginated %}
<div class="mt-6 flex justify-center items-center gap-2">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}&search={{ request.GET.search }}&status={{ request.GET.status }}"
class="px-3 py-1 border rounded hover:bg-gray-100">
Prev
</a>
{% endif %}
<span class="px-3 py-1 bg-indigo-600 text-white rounded">
{{ page_obj.number }} / {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}&search={{ request.GET.search }}&status={{ request.GET.status }}"
class="px-3 py-1 border rounded hover:bg-gray-100">
Next
</a>
{% endif %}
</div>
{% endif %}
</div>
<!-- ================= MODAL ================= -->
<div id="contactModal"
class="fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center z-50">
<div class="bg-white rounded-xl shadow-2xl max-w-lg w-full p-6 relative">
<button onclick="closeModal()"
class="absolute top-3 right-3 text-gray-400 hover:text-gray-700 text-2xl">
&times;
</button>
<h2 class="text-2xl font-bold mb-4 text-gray-800">Contact Details</h2>
<div class="space-y-4">
<p><strong>Name:</strong> <span id="modalName"></span></p>
<p><strong>Email:</strong> <span id="modalEmail"></span></p>
<p><strong>Date:</strong> <span id="modalDate"></span></p>
<div>
<p class="font-semibold mb-1">Message</p>
<div id="modalMessage"
class="bg-gray-100 p-3 rounded-lg text-sm whitespace-pre-wrap">
</div>
</div>
</div>
<div class="mt-6 text-right">
<button onclick="closeModal()"
class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700">
Close
</button>
</div>
</div>
</div>
<!-- ================= JS ================= -->
<script>
function confirmJunk(email, phone) {
let message = "Set ALL messages";
if (email) {
message += " from email: " + email;
}
if (phone) {
message += email ? " and phone: " + phone : " from phone: " + phone;
}
message += " to junk?\n\nThis will also add the user to the spam list.";
return confirm(message);
}
</script>
<script>
function openModal(name, email, message, date) {
document.getElementById("modalName").innerText = name;
document.getElementById("modalEmail").innerText = email;
document.getElementById("modalMessage").innerText = message;
document.getElementById("modalDate").innerText = date;
document.getElementById("contactModal").classList.remove("hidden");
}
function closeModal() {
document.getElementById("contactModal").classList.add("hidden");
}
</script>
{% endblock %}

View File

@@ -0,0 +1,22 @@
{% extends 'base.html' %}
{% block content %}
{% load crispy_forms_tags %}
{% load static %}
<h2 class="mb-4">SEO Configuration</h2>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
{% endif %}
<form method="post" class="card p-4 shadow-sm">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">Save</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,60 @@
{% extends 'base.html' %}
{% block content %}
{% load crispy_forms_tags %}
<div class="mb-6">
<a href="{% url 'admin_dashboard' %}"
class="inline-block px-4 py-2 border-2 border-gray-800 rounded-xl font-bold text-gray-800 hover:text-gray-900 hover:border-gray-900 transition-colors">
&larr; Admin Dashboard
</a>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-8">
<h2 class="text-3xl font-bold mb-6 text-gray-800 text-center">Subscriber List</h2>
<div class="overflow-x-auto shadow-lg rounded-lg">
<table class="min-w-full bg-white divide-y divide-gray-200">
<thead class="bg-indigo-600">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-white uppercase tracking-wider">ID</th>
<th class="px-6 py-3 text-left text-xs font-medium text-white uppercase tracking-wider">Email</th>
<th class="px-6 py-3 text-left text-xs font-medium text-white uppercase tracking-wider">Subscribed At</th>
<th class="px-6 py-3 text-left text-xs font-medium text-white uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-white uppercase tracking-wider">Action</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{% for subscriber in subscribers %}
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 whitespace-nowrap text-gray-800 font-medium">{{ subscriber.id }}</td>
<td class="px-6 py-4 whitespace-nowrap text-gray-700">{{ subscriber.email }}</td>
<td class="px-6 py-4 whitespace-nowrap text-gray-600">{{ subscriber.subscribed_at|date:"M d, Y H:i" }}</td>
<td class="px-6 py-4 whitespace-nowrap">
{% if subscriber.is_active %}
<span class="text-green-600 font-semibold">✅ Active</span>
{% else %}
<span class="text-red-600 font-semibold">❌ Inactive</span>
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<a href="{% url 'toggle_subscriber' subscriber.id %}"
class="text-indigo-600 hover:text-indigo-800 font-medium">
{% if subscriber.is_active %}Deactivate{% else %}Activate{% endif %}
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center py-8 text-gray-400">No subscribers yet.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,21 @@
# urls.py
from django.urls import path
# from .views import (AdminBusinessListView,
# admin_add_business_ajax)
from . import views
urlpatterns = [
# path('admin/businesses/', AdminBusinessListView.as_view(), name='admin-business-list'),
# path('admin/business/add/', admin_add_business_ajax, name='admin_add_business_ajax'),
path("admin_view", views.admin_dashboard, name="admin_dashboard"), # default dashboard
path('admin_view/contact_list/', views.ContactListView.as_view(), name='contact_list'),
path("admin_view/contacts/<uuid:pk>/junk/", views.set_message_to_junk, name="set_message_to_junk"),
path('admin_view/subscriber_list/', views.subscriber_list, name='subscriber_list'),
path('admin_view/subscriber_toggle/<uuid:pk>/', views.toggle_subscriber, name='toggle_subscriber'),
path('appointments/', views.appointment_list, name='appointment_list'),
path("seo/config/", views.SEOConfigView.as_view(), name="seo_config"),
]

Some files were not shown because too many files have changed in this diff Show More