commit 0c275efea140460b12e136d276637fc765f77fd8 Author: anurag.r Date: Wed Jan 7 12:09:20 2026 +0530 base setup diff --git a/B42/__init__.py b/B42/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/B42/__pycache__/__init__.cpython-312.pyc b/B42/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..d392f0a Binary files /dev/null and b/B42/__pycache__/__init__.cpython-312.pyc differ diff --git a/B42/__pycache__/settings.cpython-312.pyc b/B42/__pycache__/settings.cpython-312.pyc new file mode 100644 index 0000000..b601266 Binary files /dev/null and b/B42/__pycache__/settings.cpython-312.pyc differ diff --git a/B42/__pycache__/urls.cpython-312.pyc b/B42/__pycache__/urls.cpython-312.pyc new file mode 100644 index 0000000..d23b6f9 Binary files /dev/null and b/B42/__pycache__/urls.cpython-312.pyc differ diff --git a/B42/__pycache__/wsgi.cpython-312.pyc b/B42/__pycache__/wsgi.cpython-312.pyc new file mode 100644 index 0000000..3cb724c Binary files /dev/null and b/B42/__pycache__/wsgi.cpython-312.pyc differ diff --git a/B42/asgi.py b/B42/asgi.py new file mode 100644 index 0000000..a78da75 --- /dev/null +++ b/B42/asgi.py @@ -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() diff --git a/B42/settings.py b/B42/settings.py new file mode 100644 index 0000000..a0fdb96 --- /dev/null +++ b/B42/settings.py @@ -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' diff --git a/B42/urls.py b/B42/urls.py new file mode 100644 index 0000000..91320dd --- /dev/null +++ b/B42/urls.py @@ -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) \ No newline at end of file diff --git a/B42/wsgi.py b/B42/wsgi.py new file mode 100644 index 0000000..85aea1e --- /dev/null +++ b/B42/wsgi.py @@ -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() diff --git a/at_django_boilerplate/__init__.py b/at_django_boilerplate/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/at_django_boilerplate/__pycache__/__init__.cpython-312.pyc b/at_django_boilerplate/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..a252557 Binary files /dev/null and b/at_django_boilerplate/__pycache__/__init__.cpython-312.pyc differ diff --git a/at_django_boilerplate/accounts/__init__.py b/at_django_boilerplate/accounts/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/at_django_boilerplate/accounts/__pycache__/__init__.cpython-312.pyc b/at_django_boilerplate/accounts/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..19aae82 Binary files /dev/null and b/at_django_boilerplate/accounts/__pycache__/__init__.cpython-312.pyc differ diff --git a/at_django_boilerplate/accounts/__pycache__/admin.cpython-312.pyc b/at_django_boilerplate/accounts/__pycache__/admin.cpython-312.pyc new file mode 100644 index 0000000..5de9811 Binary files /dev/null and b/at_django_boilerplate/accounts/__pycache__/admin.cpython-312.pyc differ diff --git a/at_django_boilerplate/accounts/__pycache__/apps.cpython-312.pyc b/at_django_boilerplate/accounts/__pycache__/apps.cpython-312.pyc new file mode 100644 index 0000000..e23f154 Binary files /dev/null and b/at_django_boilerplate/accounts/__pycache__/apps.cpython-312.pyc differ diff --git a/at_django_boilerplate/accounts/__pycache__/auth_backends.cpython-312.pyc b/at_django_boilerplate/accounts/__pycache__/auth_backends.cpython-312.pyc new file mode 100644 index 0000000..ebdf21d Binary files /dev/null and b/at_django_boilerplate/accounts/__pycache__/auth_backends.cpython-312.pyc differ diff --git a/at_django_boilerplate/accounts/__pycache__/forms.cpython-312.pyc b/at_django_boilerplate/accounts/__pycache__/forms.cpython-312.pyc new file mode 100644 index 0000000..abb0d6a Binary files /dev/null and b/at_django_boilerplate/accounts/__pycache__/forms.cpython-312.pyc differ diff --git a/at_django_boilerplate/accounts/__pycache__/models.cpython-312.pyc b/at_django_boilerplate/accounts/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000..fa54976 Binary files /dev/null and b/at_django_boilerplate/accounts/__pycache__/models.cpython-312.pyc differ diff --git a/at_django_boilerplate/accounts/__pycache__/tokens.cpython-312.pyc b/at_django_boilerplate/accounts/__pycache__/tokens.cpython-312.pyc new file mode 100644 index 0000000..6a60051 Binary files /dev/null and b/at_django_boilerplate/accounts/__pycache__/tokens.cpython-312.pyc differ diff --git a/at_django_boilerplate/accounts/__pycache__/urls.cpython-312.pyc b/at_django_boilerplate/accounts/__pycache__/urls.cpython-312.pyc new file mode 100644 index 0000000..d25d064 Binary files /dev/null and b/at_django_boilerplate/accounts/__pycache__/urls.cpython-312.pyc differ diff --git a/at_django_boilerplate/accounts/__pycache__/utils.cpython-312.pyc b/at_django_boilerplate/accounts/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000..9ca7b08 Binary files /dev/null and b/at_django_boilerplate/accounts/__pycache__/utils.cpython-312.pyc differ diff --git a/at_django_boilerplate/accounts/__pycache__/views.cpython-312.pyc b/at_django_boilerplate/accounts/__pycache__/views.cpython-312.pyc new file mode 100644 index 0000000..07843c1 Binary files /dev/null and b/at_django_boilerplate/accounts/__pycache__/views.cpython-312.pyc differ diff --git a/at_django_boilerplate/accounts/admin.py b/at_django_boilerplate/accounts/admin.py new file mode 100755 index 0000000..2f585dc --- /dev/null +++ b/at_django_boilerplate/accounts/admin.py @@ -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) diff --git a/at_django_boilerplate/accounts/api/__init__.py b/at_django_boilerplate/accounts/api/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/at_django_boilerplate/accounts/api/__pycache__/__init__.cpython-312.pyc b/at_django_boilerplate/accounts/api/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..b3ad9fa Binary files /dev/null and b/at_django_boilerplate/accounts/api/__pycache__/__init__.cpython-312.pyc differ diff --git a/at_django_boilerplate/accounts/api/__pycache__/v1.cpython-312.pyc b/at_django_boilerplate/accounts/api/__pycache__/v1.cpython-312.pyc new file mode 100644 index 0000000..615d7e2 Binary files /dev/null and b/at_django_boilerplate/accounts/api/__pycache__/v1.cpython-312.pyc differ diff --git a/at_django_boilerplate/accounts/api/v1.py b/at_django_boilerplate/accounts/api/v1.py new file mode 100755 index 0000000..22fc58d --- /dev/null +++ b/at_django_boilerplate/accounts/api/v1.py @@ -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) diff --git a/at_django_boilerplate/accounts/apps.py b/at_django_boilerplate/accounts/apps.py new file mode 100755 index 0000000..f7c12be --- /dev/null +++ b/at_django_boilerplate/accounts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'at_django_boilerplate.accounts' diff --git a/at_django_boilerplate/accounts/auth_backends.py b/at_django_boilerplate/accounts/auth_backends.py new file mode 100755 index 0000000..703562b --- /dev/null +++ b/at_django_boilerplate/accounts/auth_backends.py @@ -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 diff --git a/at_django_boilerplate/accounts/forms.py b/at_django_boilerplate/accounts/forms.py new file mode 100755 index 0000000..a32962a --- /dev/null +++ b/at_django_boilerplate/accounts/forms.py @@ -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.
") + elif isinstance(validator, UserAttributeSimilarityValidator): + requirements.append("*Password cannot be too similar to your other personal information.
") + elif isinstance(validator, CommonPasswordValidator): + requirements.append("*Password cannot be a commonly used password.
") + elif isinstance(validator, NumericPasswordValidator): + requirements.append("*Password cannot be entirely numeric.
") + 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'] diff --git a/at_django_boilerplate/accounts/management/__init__.py b/at_django_boilerplate/accounts/management/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/at_django_boilerplate/accounts/management/__pycache__/__init__.cpython-312.pyc b/at_django_boilerplate/accounts/management/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..707928c Binary files /dev/null and b/at_django_boilerplate/accounts/management/__pycache__/__init__.cpython-312.pyc differ diff --git a/at_django_boilerplate/accounts/management/commands/__init__.py b/at_django_boilerplate/accounts/management/commands/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/at_django_boilerplate/accounts/management/commands/__pycache__/__init__.cpython-312.pyc b/at_django_boilerplate/accounts/management/commands/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..bb3f795 Binary files /dev/null and b/at_django_boilerplate/accounts/management/commands/__pycache__/__init__.cpython-312.pyc differ diff --git a/at_django_boilerplate/accounts/management/commands/__pycache__/seed_admin_account.cpython-312.pyc b/at_django_boilerplate/accounts/management/commands/__pycache__/seed_admin_account.cpython-312.pyc new file mode 100644 index 0000000..1fd4cb4 Binary files /dev/null and b/at_django_boilerplate/accounts/management/commands/__pycache__/seed_admin_account.cpython-312.pyc differ diff --git a/at_django_boilerplate/accounts/management/commands/seed_admin_account.py b/at_django_boilerplate/accounts/management/commands/seed_admin_account.py new file mode 100755 index 0000000..ae649fd --- /dev/null +++ b/at_django_boilerplate/accounts/management/commands/seed_admin_account.py @@ -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")) diff --git a/at_django_boilerplate/accounts/migrations/0001_initial.py b/at_django_boilerplate/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..aed6b55 --- /dev/null +++ b/at_django_boilerplate/accounts/migrations/0001_initial.py @@ -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, + }, + ), + ] diff --git a/at_django_boilerplate/accounts/migrations/0002_alter_customuser_email.py b/at_django_boilerplate/accounts/migrations/0002_alter_customuser_email.py new file mode 100644 index 0000000..4a562f1 --- /dev/null +++ b/at_django_boilerplate/accounts/migrations/0002_alter_customuser_email.py @@ -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), + ), + ] diff --git a/at_django_boilerplate/accounts/migrations/__init__.py b/at_django_boilerplate/accounts/migrations/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/at_django_boilerplate/accounts/migrations/__pycache__/0001_initial.cpython-312.pyc b/at_django_boilerplate/accounts/migrations/__pycache__/0001_initial.cpython-312.pyc new file mode 100644 index 0000000..59c0a9e Binary files /dev/null and b/at_django_boilerplate/accounts/migrations/__pycache__/0001_initial.cpython-312.pyc differ diff --git a/at_django_boilerplate/accounts/migrations/__pycache__/0002_alter_customuser_email.cpython-312.pyc b/at_django_boilerplate/accounts/migrations/__pycache__/0002_alter_customuser_email.cpython-312.pyc new file mode 100644 index 0000000..d37a379 Binary files /dev/null and b/at_django_boilerplate/accounts/migrations/__pycache__/0002_alter_customuser_email.cpython-312.pyc differ diff --git a/at_django_boilerplate/accounts/migrations/__pycache__/__init__.cpython-312.pyc b/at_django_boilerplate/accounts/migrations/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..cb1203c Binary files /dev/null and b/at_django_boilerplate/accounts/migrations/__pycache__/__init__.cpython-312.pyc differ diff --git a/at_django_boilerplate/accounts/models.py b/at_django_boilerplate/accounts/models.py new file mode 100755 index 0000000..c877741 --- /dev/null +++ b/at_django_boilerplate/accounts/models.py @@ -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 \ No newline at end of file diff --git a/at_django_boilerplate/accounts/templates/change_password.html b/at_django_boilerplate/accounts/templates/change_password.html new file mode 100755 index 0000000..5a86ba5 --- /dev/null +++ b/at_django_boilerplate/accounts/templates/change_password.html @@ -0,0 +1,75 @@ +{% extends "public_base.html" %} +{% block content %} +
+
+
+
+
+
+

Change Your Password

+
+ {% if form.errors %} + + {% endif %} + +
+
+ {% csrf_token %} +
+
+
+ + + + +
+
+
+
+
+
+ + + + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+{% endblock content %} \ No newline at end of file diff --git a/at_django_boilerplate/accounts/templates/login.html b/at_django_boilerplate/accounts/templates/login.html new file mode 100755 index 0000000..e7ee588 --- /dev/null +++ b/at_django_boilerplate/accounts/templates/login.html @@ -0,0 +1,596 @@ +{% extends 'public_base.html' %} +{% block title %} + Login +{% endblock %} + +{% block content %} +{% load static %} + + + + + +
+ +
+ + +
+
+
+
Login with OTP
+ +
+
+
+

Enter your registered email or mobile number

+ + +
+ + +
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/at_django_boilerplate/accounts/templates/otp/otp_email.html b/at_django_boilerplate/accounts/templates/otp/otp_email.html new file mode 100755 index 0000000..fb3c615 --- /dev/null +++ b/at_django_boilerplate/accounts/templates/otp/otp_email.html @@ -0,0 +1,31 @@ +{% load static %} + + + + + Your OTP for {{ purpose|capfirst }} + + + +
+

Hello,

+

You requested an OTP for {{ purpose|capfirst }}.

+

Your One-Time Password (OTP) is:

+
{{ otp }}
+

This OTP is valid for {{ validity_minutes }} minutes.

+ +

⚠️ Do not share this OTP with anyone.

+ +

If you did not request this OTP, please ignore this email or contact support immediately.

+ + +
+ + diff --git a/at_django_boilerplate/accounts/templates/profile/profile.html b/at_django_boilerplate/accounts/templates/profile/profile.html new file mode 100755 index 0000000..6f5faf2 --- /dev/null +++ b/at_django_boilerplate/accounts/templates/profile/profile.html @@ -0,0 +1,66 @@ +{% extends 'base.html' %} +{% block title %}User Profile{% endblock %} +{% load crispy_forms_tags %} +{% load static %} + +{% block content %} + + + + + +
+
+
+ + +
+ Profile Image +

{{ user }}

+ Update Profile + +
+ + +
+

Account Information

+
+
+

Email

+

{{ user.email }}

+
+
+

Phone

+

{{ user.get_decrypted_contact_number }}

+
+
+ +

Social Media

+
+ + + +
+
+ +
+
+
+ +{% endblock %} diff --git a/at_django_boilerplate/accounts/templates/profile/update_profile.html b/at_django_boilerplate/accounts/templates/profile/update_profile.html new file mode 100755 index 0000000..444b3fd --- /dev/null +++ b/at_django_boilerplate/accounts/templates/profile/update_profile.html @@ -0,0 +1,21 @@ +{% extends 'base.html' %} +{% load crispy_forms_tags %} + +{% block title %} + Update Profile +{% endblock %} + +{% block content %} +
+

Update Profile

+ +
+ {% csrf_token %} + {{ form|crispy }} + + +
+
+{% endblock %} diff --git a/at_django_boilerplate/accounts/templates/profile/update_profile_picture.html b/at_django_boilerplate/accounts/templates/profile/update_profile_picture.html new file mode 100755 index 0000000..18bb230 --- /dev/null +++ b/at_django_boilerplate/accounts/templates/profile/update_profile_picture.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} +{% block title %} + Update Profile Picture +{% endblock %} +{% block content %} +
+

Update Profile Picture

+
+ {% csrf_token %} + {{ form.as_p }} + +
+
+{% endblock %} diff --git a/at_django_boilerplate/accounts/templates/registration/create_user.html b/at_django_boilerplate/accounts/templates/registration/create_user.html new file mode 100755 index 0000000..f52a981 --- /dev/null +++ b/at_django_boilerplate/accounts/templates/registration/create_user.html @@ -0,0 +1,21 @@ + + + + Create User + + +

Create User

+
+ {% csrf_token %} + {{ form.as_p }} + +
+ + + diff --git a/at_django_boilerplate/accounts/templates/registration/signup.html b/at_django_boilerplate/accounts/templates/registration/signup.html new file mode 100755 index 0000000..1983bd2 --- /dev/null +++ b/at_django_boilerplate/accounts/templates/registration/signup.html @@ -0,0 +1,316 @@ +{% extends 'public_base.html' %} +{% load static %} +{% load custom_tags %} + +{% block title %}Sign Up | Register your startup{% endblock %} + +{% block content %} + +
+
+
+
+ + + + +
+ +
+
+
+ +
+
+

Create Your Account

+

Join thousands of successful startups

+
+ + + + + + {% if form.errors %} +
+
+ + Please correct the following errors: +
+
    + {% for field, errors in form.errors.items %} + {% for error in errors %} +
  • {{ error }}
  • + {% endfor %} + {% endfor %} +
+
+ {% endif %} + + + + +
+ {% csrf_token %} + + +
+
+
+ +
+

Personal Information

+
+ + + +
+ +
+ +
+ + + + {{ 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" }} +
+
+ + +
+ +
+ + + + {{ 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" }} +
+
+
+ + +
+ +
+ +
+ + + + {{ 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" }} +
+
+ + +
+ +
+ + + + {{ 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" }} +
+
+
+
+ + +
+
+
+ +
+

Security

+
+ + +
+ +
+ + + + {{ 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" }} +
+ + +
+ + + +
+
+ + +
+ +
+ + + + {{ 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" }} +
+
+
+ + +
+
+ {{ form.terms_accepted|add_class:"h-5 w-5 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded mt-0.5" }} + +
+ {% if form.terms_accepted.errors %} +
+ {{ form.terms_accepted.errors }} +
+ {% endif %} +
+ + +
+ +
+ + +
+

+ Already have an account? + + Sign in here + +

+
+
+
+
+
+
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/at_django_boilerplate/accounts/templates/registration/verify_email.html b/at_django_boilerplate/accounts/templates/registration/verify_email.html new file mode 100755 index 0000000..a0435ae --- /dev/null +++ b/at_django_boilerplate/accounts/templates/registration/verify_email.html @@ -0,0 +1,15 @@ +{% extends 'public_base.html' %} + +{% block title %} +Verify +{% endblock %} + +{% block content %} + +

You need to verify your email

+
+ {% csrf_token %} + +
+ +{% endblock %} diff --git a/at_django_boilerplate/accounts/templates/registration/verify_email_complete.html b/at_django_boilerplate/accounts/templates/registration/verify_email_complete.html new file mode 100755 index 0000000..2a9a066 --- /dev/null +++ b/at_django_boilerplate/accounts/templates/registration/verify_email_complete.html @@ -0,0 +1,14 @@ + +{% extends 'public_base.html' %} + +{% block title %} +Verify +{% endblock %} + +{% block content %} + + +
+ You have successfully verified your e-mail +
+{% endblock %} diff --git a/at_django_boilerplate/accounts/templates/registration/verify_email_confirm.html b/at_django_boilerplate/accounts/templates/registration/verify_email_confirm.html new file mode 100755 index 0000000..4d8fde4 --- /dev/null +++ b/at_django_boilerplate/accounts/templates/registration/verify_email_confirm.html @@ -0,0 +1,18 @@ +{% extends 'public_base.html' %} + +{% block title %} +Verify +{% endblock %} + +{% block content %} +
+ {% if messages %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} +
+ +{% endblock %} diff --git a/at_django_boilerplate/accounts/templates/registration/verify_email_done.html b/at_django_boilerplate/accounts/templates/registration/verify_email_done.html new file mode 100755 index 0000000..d6dbbc8 --- /dev/null +++ b/at_django_boilerplate/accounts/templates/registration/verify_email_done.html @@ -0,0 +1,12 @@ +{% extends 'public_base.html' %} + +{% block title %} +Verify +{% endblock %} + +{% block content %} + +
An email has been sent with instructions to verify your email
+
If you have not received the email. Please check the spam folder
+{% endblock %} + diff --git a/at_django_boilerplate/accounts/templates/registration/verify_email_message.html b/at_django_boilerplate/accounts/templates/registration/verify_email_message.html new file mode 100755 index 0000000..445d2a9 --- /dev/null +++ b/at_django_boilerplate/accounts/templates/registration/verify_email_message.html @@ -0,0 +1,20 @@ +
+

Verify Email

+
+

Hi {{ user.name }},

+

You created an account on we-kwick, you need to verify your email. Please click on the button below to verify your email.

+ + + Verify Email + + +

Or you can copy the link below to your browser

+

{{ request.scheme }}://{{ domain }}{% url 'verify-email-confirm' uidb64=uid token=token %}

+

The We-Kwick Team

+
+
+

© {% now 'Y' %} Blog

+

Follow us on Twitter

+
+
diff --git a/at_django_boilerplate/accounts/templates/registration/verify_otp.html b/at_django_boilerplate/accounts/templates/registration/verify_otp.html new file mode 100755 index 0000000..d97faf3 --- /dev/null +++ b/at_django_boilerplate/accounts/templates/registration/verify_otp.html @@ -0,0 +1,49 @@ +{% extends 'public_base.html' %} +{% load static %} +{% block title %}Verify OTP{% endblock %} + +{% block content %} +
+
+

Verify OTP

+ + {% if messages %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + +
+ {% csrf_token %} +
+ + +
+ +
+ +
+
+ +
+ Didn’t receive the OTP? + Resend +
+
+
+{% endblock %} diff --git a/at_django_boilerplate/accounts/templates/reset/password_reset.html b/at_django_boilerplate/accounts/templates/reset/password_reset.html new file mode 100755 index 0000000..e6b7d63 --- /dev/null +++ b/at_django_boilerplate/accounts/templates/reset/password_reset.html @@ -0,0 +1,62 @@ +{% extends "public_base.html" %} + +{% block content %} +
+
+
+ + +
+

Forgot Password?

+
+ + + {% if form.errors %} +
+
    + {% for key, value in form.errors.items %} +
  • {{ value }}
  • + {% endfor %} +
+
+ {% endif %} + + +
+
+ {% csrf_token %} + + +
+ + +
+ + +
+ +
+
+
+ + + + +
+
+
+{% endblock content %} diff --git a/at_django_boilerplate/accounts/templates/reset/password_reset_complete.html b/at_django_boilerplate/accounts/templates/reset/password_reset_complete.html new file mode 100755 index 0000000..c8e11b8 --- /dev/null +++ b/at_django_boilerplate/accounts/templates/reset/password_reset_complete.html @@ -0,0 +1,9 @@ + +{% extends 'base.html' %} + +{% block title %}Password reset complete{% endblock %} + +{% block content %} +

Password reset complete

+

Your new password has been set. You can log in now on the log in page.

+{% endblock %} \ No newline at end of file diff --git a/at_django_boilerplate/accounts/templates/reset/password_reset_confirm.html b/at_django_boilerplate/accounts/templates/reset/password_reset_confirm.html new file mode 100755 index 0000000..509132d --- /dev/null +++ b/at_django_boilerplate/accounts/templates/reset/password_reset_confirm.html @@ -0,0 +1,42 @@ +{% extends "base.html" %} + +{% block title %}Password Reset{% endblock %} + +{% block content %} +{% load crispy_forms_tags %} + + + + +{% if validlink %} +
+
+
+
+

Set a New Password

+
+ {% csrf_token %} + {{form|crispy}} + + +
+
+
+
+
+ +{% else %} +

The password reset link was invalid, possibly because it has already been used. Please request a new password reset.

+{% endif %} +{% endblock %} diff --git a/at_django_boilerplate/accounts/templates/reset/password_reset_done.html b/at_django_boilerplate/accounts/templates/reset/password_reset_done.html new file mode 100755 index 0000000..45c6ca8 --- /dev/null +++ b/at_django_boilerplate/accounts/templates/reset/password_reset_done.html @@ -0,0 +1,26 @@ + +{% extends "base.html" %} + +{% block title %}Email Sent{% endblock %} + +{% block content %} + +
+ {% if messages %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} +
+ +

Check your inbox.

+

We've emailed you instructions for setting your password. You should receive the email shortly!

+ +{% endblock %} \ No newline at end of file diff --git a/at_django_boilerplate/accounts/templates/reset/password_reset_email.html b/at_django_boilerplate/accounts/templates/reset/password_reset_email.html new file mode 100755 index 0000000..5e4e76e --- /dev/null +++ b/at_django_boilerplate/accounts/templates/reset/password_reset_email.html @@ -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 %} + diff --git a/at_django_boilerplate/accounts/templates/reset/password_reset_email_html.html b/at_django_boilerplate/accounts/templates/reset/password_reset_email_html.html new file mode 100755 index 0000000..05ae679 --- /dev/null +++ b/at_django_boilerplate/accounts/templates/reset/password_reset_email_html.html @@ -0,0 +1,24 @@ +
+

+ Password Reset +

+
+

Hi {{ user.name }},

+

+ To initiate the password reset process for your we-kwick account, + click the link below: + + + Reset Password + + +

Or you can copy the link below to your browser

+

{{ request.scheme }}://{{ domain }}{% url 'verify-email-confirm' uidb64=uid token=token %}

+

The We-Kwick Team

+
+
+

© {% now 'Y' %} Blog

+

Follow us on Twitter

+
+
diff --git a/at_django_boilerplate/accounts/templates/reset/password_reset_subject.txt b/at_django_boilerplate/accounts/templates/reset/password_reset_subject.txt new file mode 100755 index 0000000..d26e69a --- /dev/null +++ b/at_django_boilerplate/accounts/templates/reset/password_reset_subject.txt @@ -0,0 +1 @@ +We-Kwick Password Reset \ No newline at end of file diff --git a/at_django_boilerplate/accounts/templates/user_list.html b/at_django_boilerplate/accounts/templates/user_list.html new file mode 100755 index 0000000..de2317f --- /dev/null +++ b/at_django_boilerplate/accounts/templates/user_list.html @@ -0,0 +1,25 @@ + + + + User List + + +

User List

+ + + + diff --git a/at_django_boilerplate/accounts/templatetags/__init__.py b/at_django_boilerplate/accounts/templatetags/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/at_django_boilerplate/accounts/templatetags/__pycache__/__init__.cpython-312.pyc b/at_django_boilerplate/accounts/templatetags/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..21733db Binary files /dev/null and b/at_django_boilerplate/accounts/templatetags/__pycache__/__init__.cpython-312.pyc differ diff --git a/at_django_boilerplate/accounts/templatetags/__pycache__/custom_tags.cpython-312.pyc b/at_django_boilerplate/accounts/templatetags/__pycache__/custom_tags.cpython-312.pyc new file mode 100644 index 0000000..89e8c90 Binary files /dev/null and b/at_django_boilerplate/accounts/templatetags/__pycache__/custom_tags.cpython-312.pyc differ diff --git a/at_django_boilerplate/accounts/templatetags/__pycache__/forms_tags.cpython-312.pyc b/at_django_boilerplate/accounts/templatetags/__pycache__/forms_tags.cpython-312.pyc new file mode 100644 index 0000000..144791e Binary files /dev/null and b/at_django_boilerplate/accounts/templatetags/__pycache__/forms_tags.cpython-312.pyc differ diff --git a/at_django_boilerplate/accounts/templatetags/custom_tags.py b/at_django_boilerplate/accounts/templatetags/custom_tags.py new file mode 100755 index 0000000..1d19690 --- /dev/null +++ b/at_django_boilerplate/accounts/templatetags/custom_tags.py @@ -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}) diff --git a/at_django_boilerplate/accounts/templatetags/forms_tags.py b/at_django_boilerplate/accounts/templatetags/forms_tags.py new file mode 100755 index 0000000..9c16ac9 --- /dev/null +++ b/at_django_boilerplate/accounts/templatetags/forms_tags.py @@ -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) diff --git a/at_django_boilerplate/accounts/tests.py b/at_django_boilerplate/accounts/tests.py new file mode 100755 index 0000000..7ce503c --- /dev/null +++ b/at_django_boilerplate/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/at_django_boilerplate/accounts/tokens.py b/at_django_boilerplate/accounts/tokens.py new file mode 100755 index 0000000..a9c4d5f --- /dev/null +++ b/at_django_boilerplate/accounts/tokens.py @@ -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() \ No newline at end of file diff --git a/at_django_boilerplate/accounts/urls.py b/at_django_boilerplate/accounts/urls.py new file mode 100755 index 0000000..6b3a245 --- /dev/null +++ b/at_django_boilerplate/accounts/urls.py @@ -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/', 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///', + 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///', 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///', api_v1.ResetPasswordConfirmAPIView.as_view(), name='reset-password-confirm'), + path('api/profile/', api_v1.UserProfileView.as_view(), name='profile-api'), +] + + +urlpatterns +=api_v1 \ No newline at end of file diff --git a/at_django_boilerplate/accounts/utils.py b/at_django_boilerplate/accounts/utils.py new file mode 100755 index 0000000..f3cf3bc --- /dev/null +++ b/at_django_boilerplate/accounts/utils.py @@ -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 diff --git a/at_django_boilerplate/accounts/views.py b/at_django_boilerplate/accounts/views.py new file mode 100755 index 0000000..46bf034 --- /dev/null +++ b/at_django_boilerplate/accounts/views.py @@ -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') diff --git a/at_django_boilerplate/backend_admin/__init__.py b/at_django_boilerplate/backend_admin/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/at_django_boilerplate/backend_admin/__pycache__/__init__.cpython-312.pyc b/at_django_boilerplate/backend_admin/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..79c6015 Binary files /dev/null and b/at_django_boilerplate/backend_admin/__pycache__/__init__.cpython-312.pyc differ diff --git a/at_django_boilerplate/backend_admin/__pycache__/admin.cpython-312.pyc b/at_django_boilerplate/backend_admin/__pycache__/admin.cpython-312.pyc new file mode 100644 index 0000000..1a3e383 Binary files /dev/null and b/at_django_boilerplate/backend_admin/__pycache__/admin.cpython-312.pyc differ diff --git a/at_django_boilerplate/backend_admin/__pycache__/apps.cpython-312.pyc b/at_django_boilerplate/backend_admin/__pycache__/apps.cpython-312.pyc new file mode 100644 index 0000000..f5506a9 Binary files /dev/null and b/at_django_boilerplate/backend_admin/__pycache__/apps.cpython-312.pyc differ diff --git a/at_django_boilerplate/backend_admin/__pycache__/forms.cpython-312.pyc b/at_django_boilerplate/backend_admin/__pycache__/forms.cpython-312.pyc new file mode 100644 index 0000000..9c16f43 Binary files /dev/null and b/at_django_boilerplate/backend_admin/__pycache__/forms.cpython-312.pyc differ diff --git a/at_django_boilerplate/backend_admin/__pycache__/models.cpython-312.pyc b/at_django_boilerplate/backend_admin/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000..d360596 Binary files /dev/null and b/at_django_boilerplate/backend_admin/__pycache__/models.cpython-312.pyc differ diff --git a/at_django_boilerplate/backend_admin/__pycache__/urls.cpython-312.pyc b/at_django_boilerplate/backend_admin/__pycache__/urls.cpython-312.pyc new file mode 100644 index 0000000..77cf8ba Binary files /dev/null and b/at_django_boilerplate/backend_admin/__pycache__/urls.cpython-312.pyc differ diff --git a/at_django_boilerplate/backend_admin/__pycache__/utils.cpython-312.pyc b/at_django_boilerplate/backend_admin/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000..ac71f1b Binary files /dev/null and b/at_django_boilerplate/backend_admin/__pycache__/utils.cpython-312.pyc differ diff --git a/at_django_boilerplate/backend_admin/__pycache__/views.cpython-312.pyc b/at_django_boilerplate/backend_admin/__pycache__/views.cpython-312.pyc new file mode 100644 index 0000000..d9c982c Binary files /dev/null and b/at_django_boilerplate/backend_admin/__pycache__/views.cpython-312.pyc differ diff --git a/at_django_boilerplate/backend_admin/admin.py b/at_django_boilerplate/backend_admin/admin.py new file mode 100755 index 0000000..b1148cd --- /dev/null +++ b/at_django_boilerplate/backend_admin/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin +from .models import SEOConfiguration +# Register your models here. + +admin.site.register(SEOConfiguration) \ No newline at end of file diff --git a/at_django_boilerplate/backend_admin/apps.py b/at_django_boilerplate/backend_admin/apps.py new file mode 100755 index 0000000..366e507 --- /dev/null +++ b/at_django_boilerplate/backend_admin/apps.py @@ -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' diff --git a/at_django_boilerplate/backend_admin/forms.py b/at_django_boilerplate/backend_admin/forms.py new file mode 100755 index 0000000..35fa87f --- /dev/null +++ b/at_django_boilerplate/backend_admin/forms.py @@ -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"}), + # } diff --git a/at_django_boilerplate/backend_admin/migrations/0001_initial.py b/at_django_boilerplate/backend_admin/migrations/0001_initial.py new file mode 100644 index 0000000..626469b --- /dev/null +++ b/at_django_boilerplate/backend_admin/migrations/0001_initial.py @@ -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)), + ], + ), + ] diff --git a/at_django_boilerplate/backend_admin/migrations/__init__.py b/at_django_boilerplate/backend_admin/migrations/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/at_django_boilerplate/backend_admin/migrations/__pycache__/0001_initial.cpython-312.pyc b/at_django_boilerplate/backend_admin/migrations/__pycache__/0001_initial.cpython-312.pyc new file mode 100644 index 0000000..d87c362 Binary files /dev/null and b/at_django_boilerplate/backend_admin/migrations/__pycache__/0001_initial.cpython-312.pyc differ diff --git a/at_django_boilerplate/backend_admin/migrations/__pycache__/__init__.cpython-312.pyc b/at_django_boilerplate/backend_admin/migrations/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..e7e94a1 Binary files /dev/null and b/at_django_boilerplate/backend_admin/migrations/__pycache__/__init__.cpython-312.pyc differ diff --git a/at_django_boilerplate/backend_admin/models.py b/at_django_boilerplate/backend_admin/models.py new file mode 100755 index 0000000..0658cb9 --- /dev/null +++ b/at_django_boilerplate/backend_admin/models.py @@ -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.") diff --git a/at_django_boilerplate/backend_admin/templates/admin_dashboard.html b/at_django_boilerplate/backend_admin/templates/admin_dashboard.html new file mode 100755 index 0000000..8164126 --- /dev/null +++ b/at_django_boilerplate/backend_admin/templates/admin_dashboard.html @@ -0,0 +1,25 @@ +{% extends 'base.html' %} +{% block content %} +{% load crispy_forms_tags %} + +

Admin Dashboard

+ + + +{% endblock %} diff --git a/at_django_boilerplate/backend_admin/templates/appointment_list.html b/at_django_boilerplate/backend_admin/templates/appointment_list.html new file mode 100755 index 0000000..57b192e --- /dev/null +++ b/at_django_boilerplate/backend_admin/templates/appointment_list.html @@ -0,0 +1,188 @@ +{% extends 'base.html' %} +{% load static %} +{% block title %}Appointment Leads - Admin{% endblock %} + +{% block content %} +
+
+ + +
+

Appointment Requests

+

Manage appointments: update status, reschedule, and track progress

+
+ + {% if appointments %} +
+ + + + + + + + + + + + + + + + + + {% for apt in appointments %} + + + + + + + + + + + + + + + + + + + + + {% endfor %} + +
#Ticket IDNameContactExpertScheduled ForStatusActions
{{ forloop.counter }} + APP-{{ apt.pk|stringformat:"06d" }} + + {{ apt.full_name }} + + {{ apt.email }}
+ {{ apt.phone }} +
+ {{ apt.get_meeting_with_display }} + + {{ apt.appointment_datetime|date:"d M Y" }} at {{ apt.appointment_datetime|time:"h:i A" }} + + + {{ apt.get_status_display }} + + + + + + +
+
+ + {% else %} +
+

No appointment requests yet.

+
+ {% endif %} +
+
+ + + + + + +{% endblock %} \ No newline at end of file diff --git a/at_django_boilerplate/backend_admin/templates/contact_list.html b/at_django_boilerplate/backend_admin/templates/contact_list.html new file mode 100755 index 0000000..3601dee --- /dev/null +++ b/at_django_boilerplate/backend_admin/templates/contact_list.html @@ -0,0 +1,239 @@ +{% extends 'base.html' %} +{% load crispy_forms_tags %} + +{% block content %} + + + + +
+ +

All Contacts

+ + + +
+ + + + + + + + + +
+ + + +
+ + + + + + + + + + + + + + + {% for c in contacts %} + + + + + + + + + + + + + + + {% empty %} + + + + {% endfor %} + + +
NameEmailMessageDateActions
+ {{ c.name }} + + {{ c.email }} + + {{ c.message }} + + {{ c.created_at|date:"M d, Y H:i" }} + + + + + + +{% if not c.is_junk %} +
+ {% csrf_token %} + +
+{% else %} +Junk +{% endif %} + +
+ No contacts found. +
+
+ + + {% if is_paginated %} +
+ + {% if page_obj.has_previous %} + + Prev + + {% endif %} + + + {{ page_obj.number }} / {{ page_obj.paginator.num_pages }} + + + {% if page_obj.has_next %} + + Next + + {% endif %} +
+ {% endif %} + +
+ + + + + + + + + + + + +{% endblock %} diff --git a/at_django_boilerplate/backend_admin/templates/seo_config.html b/at_django_boilerplate/backend_admin/templates/seo_config.html new file mode 100755 index 0000000..a4789c3 --- /dev/null +++ b/at_django_boilerplate/backend_admin/templates/seo_config.html @@ -0,0 +1,22 @@ +{% extends 'base.html' %} +{% block content %} +{% load crispy_forms_tags %} + +{% load static %} + +

SEO Configuration

+ + {% if messages %} + {% for message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + +
+ {% csrf_token %} + {{ form.as_p }} + +
+ + +{% endblock %} \ No newline at end of file diff --git a/at_django_boilerplate/backend_admin/templates/subscriber.html b/at_django_boilerplate/backend_admin/templates/subscriber.html new file mode 100755 index 0000000..e78184e --- /dev/null +++ b/at_django_boilerplate/backend_admin/templates/subscriber.html @@ -0,0 +1,60 @@ +{% extends 'base.html' %} +{% block content %} +{% load crispy_forms_tags %} + + + + +
+ + +

Subscriber List

+ +
+ + + + + + + + + + + + {% for subscriber in subscribers %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
IDEmailSubscribed AtStatusAction
{{ subscriber.id }}{{ subscriber.email }}{{ subscriber.subscribed_at|date:"M d, Y H:i" }} + {% if subscriber.is_active %} + ✅ Active + {% else %} + ❌ Inactive + {% endif %} + + + {% if subscriber.is_active %}Deactivate{% else %}Activate{% endif %} + +
No subscribers yet.
+
+ +
+ +{% endblock %} diff --git a/at_django_boilerplate/backend_admin/tests.py b/at_django_boilerplate/backend_admin/tests.py new file mode 100755 index 0000000..7ce503c --- /dev/null +++ b/at_django_boilerplate/backend_admin/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/at_django_boilerplate/backend_admin/urls.py b/at_django_boilerplate/backend_admin/urls.py new file mode 100755 index 0000000..6411dc4 --- /dev/null +++ b/at_django_boilerplate/backend_admin/urls.py @@ -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//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//', views.toggle_subscriber, name='toggle_subscriber'), + + path('appointments/', views.appointment_list, name='appointment_list'), + + + path("seo/config/", views.SEOConfigView.as_view(), name="seo_config"), + +] diff --git a/at_django_boilerplate/backend_admin/utils.py b/at_django_boilerplate/backend_admin/utils.py new file mode 100755 index 0000000..ddd4da2 --- /dev/null +++ b/at_django_boilerplate/backend_admin/utils.py @@ -0,0 +1,49 @@ +from datetime import timedelta +from django.utils import timezone +# from business.models import Subscription, SubscriptionPackage + + +# def assign_free_trial_subscription(business): +# """ +# Assigns a free trial subscription to a business, if allowed. +# Handles expiration of old trials and avoids duplicates. +# """ +# try: +# # Find the first free trial plan +# plan = SubscriptionPackage.objects.filter( +# provider='trial', +# is_free_plan=True, +# free_trial_days__gt=0 +# ).first() + +# if not plan: +# return {"success": False, "message": "No free trial plan available."} + +# # Check for existing active trial +# existing_trial = Subscription.objects.filter( +# business=business, +# plan__provider='trial', +# status='trial' +# ).order_by('-subscribed_on').first() + +# if existing_trial and existing_trial.current_period_end > timezone.now(): +# return {"success": False, "message": "Active trial already exists."} + +# # Mark existing trial as expired (if any) +# if existing_trial: +# existing_trial.status = 'expired' +# existing_trial.save() + +# # Create new free trial +# Subscription.objects.create( +# business=business, +# plan=plan, +# status='trial', +# subscribed_on=timezone.now(), +# current_period_end=timezone.now() + timedelta(days=plan.free_trial_days) +# ) + +# return {"success": True, "message": f"{plan.free_trial_days}-day free trial assigned."} + +# except Exception as e: +# return {"success": False, "message": f"Error assigning trial: {str(e)}"} \ No newline at end of file diff --git a/at_django_boilerplate/backend_admin/views.py b/at_django_boilerplate/backend_admin/views.py new file mode 100755 index 0000000..3ba5817 --- /dev/null +++ b/at_django_boilerplate/backend_admin/views.py @@ -0,0 +1,156 @@ +from at_django_boilerplate.communications.models import FeedbackModel,AppointmentModel +from at_django_boilerplate.core.models import Subscriber +from django.shortcuts import render, redirect,get_object_or_404 +from django.contrib import messages +from django.views import View +from .models import SEOConfiguration +from .forms import SEOConfigurationForm +from django.contrib.auth.decorators import login_required, user_passes_test +from django.contrib.auth import get_user_model +from django.utils.decorators import method_decorator +from django.views.generic import ListView +from django.db.models import Q +from .utils import * +from django.utils import timezone +from django.contrib.auth.mixins import UserPassesTestMixin +from django.http import HttpResponseForbidden + +# Keep your custom decorator +def superuser_required(view_func): + def wrapper(request, *args, **kwargs): + if not request.user.is_authenticated: + return redirect('login') + if not request.user.is_superuser: + return HttpResponseForbidden() + return view_func(request, *args, **kwargs) + return wrapper + + + +class SuperuserRequiredMixin(UserPassesTestMixin): + def test_func(self): + return self.request.user.is_superuser +User = get_user_model() + +@superuser_required +def admin_dashboard(request): + return render(request, "admin_dashboard.html") + + + +class ContactListView(SuperuserRequiredMixin ,ListView): + model = FeedbackModel + template_name = 'contact_list.html' + context_object_name = 'contacts' + paginate_by = 50 + + def get_queryset(self): + queryset = FeedbackModel.objects.all().order_by("-created_at") + + search = self.request.GET.get("search") + status = self.request.GET.get("status") # NEW + + if search: + queryset = queryset.filter( + Q(name__icontains=search) | + Q(email__icontains=search) | + Q(message__icontains=search) + ) + + # ================= JUNK FILTER ================= + if status == "junk": + queryset = queryset.filter(is_junk=True) + elif status == "inbox": + queryset = queryset.filter(is_junk=False) + + return queryset + +@superuser_required +def set_message_to_junk(request, pk): # Change parameter from 'uuid' to 'pk' + contact = get_object_or_404(FeedbackModel, pk=pk) # Use 'id=pk' instead of 'uuid=uuid' + contact.is_junk = True + contact.save(update_fields=["is_junk"]) + + messages.success(request, "Message marked as junk.") + return redirect(request.META.get("HTTP_REFERER", "contact_list")) # Better fallback + +@superuser_required +# Show all subscribers +def subscriber_list(request): + subscribers = Subscriber.objects.all().order_by("-subscribed_at") + return render(request, "subscriber.html", {"subscribers": subscribers}) + + + +@superuser_required +def toggle_subscriber(request, pk): # Changed from pk to uuid + subscriber = get_object_or_404(Subscriber, pk=pk) # Assuming Subscriber also uses UUID as PK + subscriber.is_active = not subscriber.is_active + subscriber.save() + return redirect("subscriber_list") + +class SEOConfigView(View): + template_name = "seo_config.html" + + def get(self, request): + seo_config = SEOConfiguration.objects.first() + if not seo_config: + seo_config = SEOConfiguration.objects.create() # ensure one record exists + + form = SEOConfigurationForm(instance=seo_config) + return render(request, self.template_name, {"form": form}) + + def post(self, request): + seo_config = SEOConfiguration.objects.first() + form = SEOConfigurationForm(request.POST, instance=seo_config) + if form.is_valid(): + form.save() + messages.success(request, "SEO configuration updated successfully.") + return redirect("seo_config") + return render(request, self.template_name, {"form": form}) + + +@superuser_required +def appointment_list(request): + if request.method == "POST": + appointment_id = request.POST.get("appointment_id") + action = request.POST.get("action") + appointment = get_object_or_404(AppointmentModel, id=appointment_id) + + if action == "update_status": + new_status = request.POST.get("status") + if new_status in dict(AppointmentModel.STATUS_CHOICES): + appointment.status = new_status + appointment.save() + messages.success(request, f"Status updated to '{appointment.get_status_display()}' for {appointment.full_name}") + + elif action == "reschedule": + date_str = request.POST.get("new_date") + time_str = request.POST.get("new_time") + if date_str and time_str: + try: + new_datetime_str = f"{date_str} {time_str}" + new_datetime = timezone.datetime.strptime(new_datetime_str, "%Y-%m-%d %H:%M") + new_datetime = timezone.make_aware(new_datetime) + + if new_datetime > timezone.now(): + appointment.appointment_datetime = new_datetime + appointment.status = 'rescheduled' + appointment.save() + messages.success(request, f"Appointment rescheduled to {new_datetime.strftime('%d %B %Y, %I:%M %p')}") + else: + messages.error(request, "New time must be in the future.") + except ValueError: + messages.error(request, "Invalid date/time format.") + else: + messages.error(request, "Please select both date and time.") + + return redirect('appointment_list') + + appointments = AppointmentModel.objects.all().order_by('-appointment_datetime') + status_choices = AppointmentModel.STATUS_CHOICES + + return render(request, "appointment_list.html", { + "appointments": appointments, + "status_choices": status_choices, + }) \ No newline at end of file diff --git a/at_django_boilerplate/blogs/__init__.py b/at_django_boilerplate/blogs/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/at_django_boilerplate/blogs/__pycache__/__init__.cpython-312.pyc b/at_django_boilerplate/blogs/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..632d135 Binary files /dev/null and b/at_django_boilerplate/blogs/__pycache__/__init__.cpython-312.pyc differ diff --git a/at_django_boilerplate/blogs/__pycache__/admin.cpython-312.pyc b/at_django_boilerplate/blogs/__pycache__/admin.cpython-312.pyc new file mode 100644 index 0000000..c8b4cdd Binary files /dev/null and b/at_django_boilerplate/blogs/__pycache__/admin.cpython-312.pyc differ diff --git a/at_django_boilerplate/blogs/__pycache__/apps.cpython-312.pyc b/at_django_boilerplate/blogs/__pycache__/apps.cpython-312.pyc new file mode 100644 index 0000000..aa6ac95 Binary files /dev/null and b/at_django_boilerplate/blogs/__pycache__/apps.cpython-312.pyc differ diff --git a/at_django_boilerplate/blogs/__pycache__/models.cpython-312.pyc b/at_django_boilerplate/blogs/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000..3975652 Binary files /dev/null and b/at_django_boilerplate/blogs/__pycache__/models.cpython-312.pyc differ diff --git a/at_django_boilerplate/blogs/__pycache__/urls.cpython-312.pyc b/at_django_boilerplate/blogs/__pycache__/urls.cpython-312.pyc new file mode 100644 index 0000000..24864fe Binary files /dev/null and b/at_django_boilerplate/blogs/__pycache__/urls.cpython-312.pyc differ diff --git a/at_django_boilerplate/blogs/__pycache__/views.cpython-312.pyc b/at_django_boilerplate/blogs/__pycache__/views.cpython-312.pyc new file mode 100644 index 0000000..723b5e9 Binary files /dev/null and b/at_django_boilerplate/blogs/__pycache__/views.cpython-312.pyc differ diff --git a/at_django_boilerplate/blogs/admin.py b/at_django_boilerplate/blogs/admin.py new file mode 100755 index 0000000..c7c3e16 --- /dev/null +++ b/at_django_boilerplate/blogs/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from .models import (Blog) + +@admin.register(Blog) +class BlogModelAdmin(admin.ModelAdmin): + list_display = ('title','published_on','is_draft','is_active') diff --git a/at_django_boilerplate/blogs/apps.py b/at_django_boilerplate/blogs/apps.py new file mode 100755 index 0000000..e151552 --- /dev/null +++ b/at_django_boilerplate/blogs/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class BlogsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'at_django_boilerplate.blogs' + diff --git a/at_django_boilerplate/blogs/generate_dummy_blogs.py b/at_django_boilerplate/blogs/generate_dummy_blogs.py new file mode 100755 index 0000000..9cf4be5 --- /dev/null +++ b/at_django_boilerplate/blogs/generate_dummy_blogs.py @@ -0,0 +1,45 @@ +import random +import requests +from django.utils import timezone +from django.core.files.base import ContentFile +from faker import Faker +from .models import Blog +from accounts.models import CustomUser + +fake = Faker() + +def create_dummy_blogs(n=20): + users = list(CustomUser.objects.filter(is_active=True)) + if not users: + print("No active users found. Please create users first.") + return + + for i in range(n): + title = fake.sentence(nb_words=6) + content = fake.paragraph(nb_sentences=10) + meta_title = title + meta_description = fake.text(max_nb_chars=180) + meta_keywords = ', '.join(fake.words(nb=5)) + + # Download a random image + image_url = "https://source.unsplash.com/600x400/?realestate,home,apartment" + response = requests.get(image_url) + + blog = Blog( + title=title, + content=content, + meta_title=meta_title, + meta_description=meta_description, + meta_keywords=meta_keywords, + published_on=timezone.now(), + published_by=random.choice(users), + is_draft=False, + is_active=True, + ) + + if response.status_code == 200: + image_name = f"blog_image_{i}.jpg" + blog.cover_image.save(image_name, ContentFile(response.content), save=False) + + blog.save() + print(f"Created blog: {title}") diff --git a/at_django_boilerplate/blogs/migrations/0001_initial.py b/at_django_boilerplate/blogs/migrations/0001_initial.py new file mode 100644 index 0000000..e3968d2 --- /dev/null +++ b/at_django_boilerplate/blogs/migrations/0001_initial.py @@ -0,0 +1,34 @@ +# Generated by Django 5.2.6 on 2025-12-29 05:20 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Blog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('published_on', models.DateTimeField(auto_created=True, default=django.utils.timezone.now)), + ('title', models.CharField(blank=True, max_length=80, null=True)), + ('content', models.TextField(blank=True, null=True)), + ('cover_image', models.ImageField(blank=True, null=True, upload_to='blogs')), + ('meta_title', models.CharField(blank=True, max_length=80, null=True)), + ('meta_description', models.CharField(blank=True, max_length=180, null=True)), + ('meta_keywords', models.CharField(blank=True, max_length=100, null=True)), + ('is_draft', models.BooleanField(default=True)), + ('is_active', models.BooleanField(default=False)), + ('published_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/at_django_boilerplate/blogs/migrations/0002_alter_blog_id.py b/at_django_boilerplate/blogs/migrations/0002_alter_blog_id.py new file mode 100644 index 0000000..6e89b2a --- /dev/null +++ b/at_django_boilerplate/blogs/migrations/0002_alter_blog_id.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.6 on 2026-01-05 04:33 + +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('blogs', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='blog', + name='id', + field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False), + ), + ] diff --git a/at_django_boilerplate/blogs/migrations/0003_alter_blog_options_and_more.py b/at_django_boilerplate/blogs/migrations/0003_alter_blog_options_and_more.py new file mode 100644 index 0000000..d1741b3 --- /dev/null +++ b/at_django_boilerplate/blogs/migrations/0003_alter_blog_options_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.6 on 2026-01-05 05:39 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('blogs', '0002_alter_blog_id'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterModelOptions( + name='blog', + options={'ordering': ['-published_on'], 'verbose_name': 'Blog Post', 'verbose_name_plural': 'Blog Posts'}, + ), + migrations.AddIndex( + model_name='blog', + index=models.Index(fields=['-published_on'], name='blogs_blog_publish_fdee8e_idx'), + ), + ] diff --git a/at_django_boilerplate/blogs/migrations/0004_alter_blog_options_and_more.py b/at_django_boilerplate/blogs/migrations/0004_alter_blog_options_and_more.py new file mode 100644 index 0000000..ea69d2f --- /dev/null +++ b/at_django_boilerplate/blogs/migrations/0004_alter_blog_options_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.6 on 2026-01-07 04:42 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('blogs', '0003_alter_blog_options_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='blog', + options={}, + ), + migrations.RemoveIndex( + model_name='blog', + name='blogs_blog_publish_fdee8e_idx', + ), + ] diff --git a/at_django_boilerplate/blogs/migrations/__init__.py b/at_django_boilerplate/blogs/migrations/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/at_django_boilerplate/blogs/migrations/__pycache__/0001_initial.cpython-312.pyc b/at_django_boilerplate/blogs/migrations/__pycache__/0001_initial.cpython-312.pyc new file mode 100644 index 0000000..9b10f77 Binary files /dev/null and b/at_django_boilerplate/blogs/migrations/__pycache__/0001_initial.cpython-312.pyc differ diff --git a/at_django_boilerplate/blogs/migrations/__pycache__/0002_alter_blog_id.cpython-312.pyc b/at_django_boilerplate/blogs/migrations/__pycache__/0002_alter_blog_id.cpython-312.pyc new file mode 100644 index 0000000..063c054 Binary files /dev/null and b/at_django_boilerplate/blogs/migrations/__pycache__/0002_alter_blog_id.cpython-312.pyc differ diff --git a/at_django_boilerplate/blogs/migrations/__pycache__/0003_alter_blog_options_and_more.cpython-312.pyc b/at_django_boilerplate/blogs/migrations/__pycache__/0003_alter_blog_options_and_more.cpython-312.pyc new file mode 100644 index 0000000..d5ac04a Binary files /dev/null and b/at_django_boilerplate/blogs/migrations/__pycache__/0003_alter_blog_options_and_more.cpython-312.pyc differ diff --git a/at_django_boilerplate/blogs/migrations/__pycache__/0004_alter_blog_options_and_more.cpython-312.pyc b/at_django_boilerplate/blogs/migrations/__pycache__/0004_alter_blog_options_and_more.cpython-312.pyc new file mode 100644 index 0000000..86a7854 Binary files /dev/null and b/at_django_boilerplate/blogs/migrations/__pycache__/0004_alter_blog_options_and_more.cpython-312.pyc differ diff --git a/at_django_boilerplate/blogs/migrations/__pycache__/__init__.cpython-312.pyc b/at_django_boilerplate/blogs/migrations/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..9308038 Binary files /dev/null and b/at_django_boilerplate/blogs/migrations/__pycache__/__init__.cpython-312.pyc differ diff --git a/at_django_boilerplate/blogs/models.py b/at_django_boilerplate/blogs/models.py new file mode 100755 index 0000000..6717da6 --- /dev/null +++ b/at_django_boilerplate/blogs/models.py @@ -0,0 +1,17 @@ +from django.db import models +from django.utils import timezone +from at_django_boilerplate.utils.mixins import UUIDMixin + +class Blog(UUIDMixin): + title = models.CharField(max_length=80,null=True,blank=True) + content = models.TextField(null=True,blank=True) + cover_image = models.ImageField(upload_to='blogs',null=True,blank=True) + meta_title = models.CharField(max_length=80,null=True,blank=True) + meta_description = models.CharField(max_length=180,null=True,blank=True) + meta_keywords = models.CharField(max_length=100,null=True,blank=True) + published_on = models.DateTimeField(auto_created=True,default=timezone.now) + published_by = models.ForeignKey('accounts.CustomUser',null=True,blank=True,on_delete=models.DO_NOTHING) + is_draft = models.BooleanField(default=True) + is_active = models.BooleanField(default=False) + + diff --git a/at_django_boilerplate/blogs/templates/blog_detail.html b/at_django_boilerplate/blogs/templates/blog_detail.html new file mode 100755 index 0000000..0b612ab --- /dev/null +++ b/at_django_boilerplate/blogs/templates/blog_detail.html @@ -0,0 +1,45 @@ +{% extends "public_base.html" %} +{% load static %} +{% block title %}{{ meta_title }}{% endblock %} + +{% block meta %} + + + + + +{% endblock %} + +{% block content %} +
+ + + + +
+
+

+ {{ blog.title }} +

+ +
+ + {% if blog.cover_image %} +
+ {{ blog.title }} +
+ {% endif %} + +
+ {{ blog.content|linebreaks }} +
+
+
+{% endblock %} diff --git a/at_django_boilerplate/blogs/templates/blogs_list.html b/at_django_boilerplate/blogs/templates/blogs_list.html new file mode 100755 index 0000000..f0911e4 --- /dev/null +++ b/at_django_boilerplate/blogs/templates/blogs_list.html @@ -0,0 +1,233 @@ +{% extends 'public_base.html' %} + +{% block title %}{{ meta_title|default:"Our Blog - RegisterYourStartup" }}{% endblock %} +{% block meta_description %}{{ meta_description|default:"Read the latest articles, guides, and updates on company registration, compliance, startups, and business growth from RegisterYourStartup." }}{% endblock %} + +{% block content %} + + + +
+
+
+

+ Our Blog +

+

+ Insights, guides, and updates on company registration, startup compliance, taxation, funding, and business growth in India. +

+
+
+
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+ + + +
+
+ +
+
+
+ {% for blog in blogs %} + + {% empty %} +
+ +

No blog posts available yet. Stay tuned!

+
+ {% endfor %} +
+ + + +
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/at_django_boilerplate/blogs/tests.py b/at_django_boilerplate/blogs/tests.py new file mode 100755 index 0000000..7ce503c --- /dev/null +++ b/at_django_boilerplate/blogs/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/at_django_boilerplate/blogs/urls.py b/at_django_boilerplate/blogs/urls.py new file mode 100755 index 0000000..34beac9 --- /dev/null +++ b/at_django_boilerplate/blogs/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from .import views + +urlpatterns = [ + + path('', views.BlogsListView.as_view(), name='blogs_list'), + path('blogs//', views.BlogDetailView.as_view(), name='blog_detail'), +] \ No newline at end of file diff --git a/at_django_boilerplate/blogs/views.py b/at_django_boilerplate/blogs/views.py new file mode 100755 index 0000000..a4dca40 --- /dev/null +++ b/at_django_boilerplate/blogs/views.py @@ -0,0 +1,29 @@ +from django.views.generic import ListView,DetailView +from at_django_boilerplate.backend_admin.models import SEOConfiguration +# Create your views here. + +from at_django_boilerplate.blogs.models import Blog +class BlogsListView(ListView): + model = Blog + template_name = 'blogs_list.html' + context_object_name = 'blogs' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + seo = SEOConfiguration.objects.first() + context["meta_title"] = seo.blog_meta_title if seo else "Blog - RegisterYourStartup" + context["meta_description"] = seo.blog_meta_description if seo else "Read articles and updates from Zestato." + return context + + +class BlogDetailView(DetailView): + model = Blog + template_name = 'blog_detail.html' + context_object_name = 'blog' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + blog = self.get_object() + context["meta_title"] = blog.title + context["meta_description"] = blog.meta_description if hasattr(blog, 'meta_description') else blog.short_description[:160] + return context \ No newline at end of file diff --git a/at_django_boilerplate/communications/__init__.py b/at_django_boilerplate/communications/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/at_django_boilerplate/communications/__pycache__/__init__.cpython-312.pyc b/at_django_boilerplate/communications/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..4928a9d Binary files /dev/null and b/at_django_boilerplate/communications/__pycache__/__init__.cpython-312.pyc differ diff --git a/at_django_boilerplate/communications/__pycache__/admin.cpython-312.pyc b/at_django_boilerplate/communications/__pycache__/admin.cpython-312.pyc new file mode 100644 index 0000000..3b71370 Binary files /dev/null and b/at_django_boilerplate/communications/__pycache__/admin.cpython-312.pyc differ diff --git a/at_django_boilerplate/communications/__pycache__/apps.cpython-312.pyc b/at_django_boilerplate/communications/__pycache__/apps.cpython-312.pyc new file mode 100644 index 0000000..7e89bc1 Binary files /dev/null and b/at_django_boilerplate/communications/__pycache__/apps.cpython-312.pyc differ diff --git a/at_django_boilerplate/communications/__pycache__/models.cpython-312.pyc b/at_django_boilerplate/communications/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000..e4ef8fd Binary files /dev/null and b/at_django_boilerplate/communications/__pycache__/models.cpython-312.pyc differ diff --git a/at_django_boilerplate/communications/__pycache__/urls.cpython-312.pyc b/at_django_boilerplate/communications/__pycache__/urls.cpython-312.pyc new file mode 100644 index 0000000..524b5e3 Binary files /dev/null and b/at_django_boilerplate/communications/__pycache__/urls.cpython-312.pyc differ diff --git a/at_django_boilerplate/communications/__pycache__/views.cpython-312.pyc b/at_django_boilerplate/communications/__pycache__/views.cpython-312.pyc new file mode 100644 index 0000000..7a271b7 Binary files /dev/null and b/at_django_boilerplate/communications/__pycache__/views.cpython-312.pyc differ diff --git a/at_django_boilerplate/communications/admin.py b/at_django_boilerplate/communications/admin.py new file mode 100755 index 0000000..cc1008a --- /dev/null +++ b/at_django_boilerplate/communications/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from .models import FeedbackModel + + + +admin.site.register(FeedbackModel) diff --git a/at_django_boilerplate/communications/apps.py b/at_django_boilerplate/communications/apps.py new file mode 100755 index 0000000..8310888 --- /dev/null +++ b/at_django_boilerplate/communications/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CommunicationsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'at_django_boilerplate.communications' diff --git a/at_django_boilerplate/communications/migrations/0001_initial.py b/at_django_boilerplate/communications/migrations/0001_initial.py new file mode 100644 index 0000000..673740c --- /dev/null +++ b/at_django_boilerplate/communications/migrations/0001_initial.py @@ -0,0 +1,50 @@ +# Generated by Django 5.2.6 on 2025-12-29 05:20 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='AppointmentModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('full_name', models.CharField(max_length=255)), + ('email', models.EmailField(max_length=254)), + ('phone', models.CharField(max_length=20)), + ('meeting_with', models.CharField(choices=[('business-consultant', 'Meet to Lawyer'), ('compliance-specialist', 'Meet to CA'), ('incorporation-expert', 'Meet to Secretary')], max_length=50)), + ('appointment_datetime', models.DateTimeField()), + ('notes', models.TextField(blank=True, null=True)), + ('status', models.CharField(choices=[('pending', 'Pending'), ('contacted', 'Contacted'), ('in_process', 'In Process'), ('rescheduled', 'Rescheduled'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], default='pending', max_length=20)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='appointments', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-appointment_datetime'], + }, + ), + migrations.CreateModel( + name='FeedbackModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('email', models.EmailField(blank=True, max_length=254, null=True)), + ('phone', models.CharField(blank=True, max_length=20, null=True)), + ('name', models.CharField(blank=True, max_length=255, null=True)), + ('subject', models.TextField(blank=True, null=True)), + ('message', models.TextField(blank=True, null=True)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='feedbacks', to=settings.AUTH_USER_MODEL)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='feedback_from', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/at_django_boilerplate/communications/migrations/0002_feedbackmodel_is_junk.py b/at_django_boilerplate/communications/migrations/0002_feedbackmodel_is_junk.py new file mode 100644 index 0000000..44b72af --- /dev/null +++ b/at_django_boilerplate/communications/migrations/0002_feedbackmodel_is_junk.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.6 on 2026-01-03 06:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('communications', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='feedbackmodel', + name='is_junk', + field=models.BooleanField(default=False), + ), + ] diff --git a/at_django_boilerplate/communications/migrations/0003_alter_appointmentmodel_id_alter_feedbackmodel_id.py b/at_django_boilerplate/communications/migrations/0003_alter_appointmentmodel_id_alter_feedbackmodel_id.py new file mode 100644 index 0000000..f995bba --- /dev/null +++ b/at_django_boilerplate/communications/migrations/0003_alter_appointmentmodel_id_alter_feedbackmodel_id.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.6 on 2026-01-03 06:18 + +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('communications', '0002_feedbackmodel_is_junk'), + ] + + operations = [ + migrations.AlterField( + model_name='appointmentmodel', + name='id', + field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='feedbackmodel', + name='id', + field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False), + ), + ] diff --git a/at_django_boilerplate/communications/migrations/__init__.py b/at_django_boilerplate/communications/migrations/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/at_django_boilerplate/communications/migrations/__pycache__/0001_initial.cpython-312.pyc b/at_django_boilerplate/communications/migrations/__pycache__/0001_initial.cpython-312.pyc new file mode 100644 index 0000000..1042c4d Binary files /dev/null and b/at_django_boilerplate/communications/migrations/__pycache__/0001_initial.cpython-312.pyc differ diff --git a/at_django_boilerplate/communications/migrations/__pycache__/0002_feedbackmodel_is_junk.cpython-312.pyc b/at_django_boilerplate/communications/migrations/__pycache__/0002_feedbackmodel_is_junk.cpython-312.pyc new file mode 100644 index 0000000..0ce6d4d Binary files /dev/null and b/at_django_boilerplate/communications/migrations/__pycache__/0002_feedbackmodel_is_junk.cpython-312.pyc differ diff --git a/at_django_boilerplate/communications/migrations/__pycache__/0003_alter_appointmentmodel_id_alter_feedbackmodel_id.cpython-312.pyc b/at_django_boilerplate/communications/migrations/__pycache__/0003_alter_appointmentmodel_id_alter_feedbackmodel_id.cpython-312.pyc new file mode 100644 index 0000000..a568aee Binary files /dev/null and b/at_django_boilerplate/communications/migrations/__pycache__/0003_alter_appointmentmodel_id_alter_feedbackmodel_id.cpython-312.pyc differ diff --git a/at_django_boilerplate/communications/migrations/__pycache__/__init__.cpython-312.pyc b/at_django_boilerplate/communications/migrations/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..dcbb2b4 Binary files /dev/null and b/at_django_boilerplate/communications/migrations/__pycache__/__init__.cpython-312.pyc differ diff --git a/at_django_boilerplate/communications/models.py b/at_django_boilerplate/communications/models.py new file mode 100755 index 0000000..5ef4bc6 --- /dev/null +++ b/at_django_boilerplate/communications/models.py @@ -0,0 +1,63 @@ +from django.db import models +from django.utils import timezone + +from at_django_boilerplate.utils.mixins import UUIDMixin + + + +class FeedbackModel(UUIDMixin): + user = models.ForeignKey('accounts.CustomUser',related_name='feedback_from',null=True,blank=True,on_delete=models.DO_NOTHING) + email = models.EmailField(null=True,blank=True) + phone = models.CharField(max_length=20, null=True, blank=True) # Corrected + name = models.CharField(max_length=255, blank=True, null=True) + to = models.ForeignKey('accounts.CustomUser',related_name='feedbacks',null=True,blank=True,on_delete=models.DO_NOTHING) + is_junk = models.BooleanField(default= False) + + subject = models.TextField(null=True,blank=True) + message = models.TextField(null=True,blank=True) + + created_at = models.DateTimeField(default=timezone.now) + + +# communications/models.py + +class AppointmentModel(UUIDMixin): + MEETING_CHOICES = ( + ('business-consultant', 'Meet to Lawyer'), + ('compliance-specialist', 'Meet to CA'), + ('incorporation-expert', 'Meet to Secretary'), + ) + + STATUS_CHOICES = ( + ('pending', 'Pending'), + ('contacted', 'Contacted'), + ('in_process', 'In Process'), + ('rescheduled', 'Rescheduled'), + ('completed', 'Completed'), + ('cancelled', 'Cancelled'), + ) + + user = models.ForeignKey( + 'accounts.CustomUser', + related_name='appointments', + null=True, + blank=True, + on_delete=models.DO_NOTHING + ) + full_name = models.CharField(max_length=255) + email = models.EmailField() + phone = models.CharField(max_length=20) + meeting_with = models.CharField(max_length=50, choices=MEETING_CHOICES) + appointment_datetime = models.DateTimeField() + notes = models.TextField(blank=True, null=True) + + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending') + + created_at = models.DateTimeField(auto_now_add=True) + + + def __str__(self): + return f"APP-{self.pk:06d} - {self.full_name}" + + class Meta: + ordering = ['-appointment_datetime'] \ No newline at end of file diff --git a/at_django_boilerplate/communications/templates/book_appointment.html b/at_django_boilerplate/communications/templates/book_appointment.html new file mode 100755 index 0000000..979aa8d --- /dev/null +++ b/at_django_boilerplate/communications/templates/book_appointment.html @@ -0,0 +1,437 @@ +{% extends 'public_base.html' %} +{% load static %} + +{% block title %}Book an Appointment - RegisterYourStartup.com{% endblock %} +{% block meta_description %}Schedule a consultation with our business experts. Book an appointment for personalized guidance on incorporation, compliance, and business growth strategies.{% endblock %} + +{% block content %} + + + +
+
+
+
+

+ Book an Appointment +

+

+ Schedule a consultation with our business experts. Get personalized guidance on incorporation, compliance, and growth strategies tailored to your needs. +

+
+ Book Now +
+
+
+
+ +
+
+
+
+
+
+
+ + +
+
+ + + {% if messages %} +
+ {% for message in messages %} +
+ {{ message|safe }} +
+ {% endfor %} +
+ {% endif %} + + +
+

Schedule Your Consultation

+
+ {% csrf_token %} + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + +
+ + +
+
+ + +
+

+ Book an appointment with us +

+

+ Fill out the form to connect with us. During this session, our experts will: +

+ +
+
+
+
+
+
Walk you through our services
+

We provide solutions tailored to your business needs

+
+
+ +
+
+
+
+
Answer your specific questions
+

Get actionable insights for your business

+
+
+ +
+
+
+
+
Expert Guidance
+

Receive personalized advice from specialists in incorporation, compliance, and business growth

+
+
+ +
+
+
+
+
Time-Saving Efficiency
+

Streamlined consultations help you get clear answers quickly

+
+
+ +
+
+
+
+
Comprehensive Support
+

From starting a business to managing operations globally, we've got you covered

+
+
+
+
+
+
+ + +
+
+

Ready to Grow Your Business?

+

Book a consultation today and let our expert team guide you through seamless business setup and expansion

+ +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/at_django_boilerplate/communications/templates/contact_us_form.html b/at_django_boilerplate/communications/templates/contact_us_form.html new file mode 100755 index 0000000..87e39d1 --- /dev/null +++ b/at_django_boilerplate/communications/templates/contact_us_form.html @@ -0,0 +1,518 @@ +{% extends 'public_base.html' %} +{% load crispy_forms_tags %} + +{% block title %}Contact Us - RegisterYourStartup.com{% endblock %} +{% block meta_description %}Get in touch with RegisterYourStartup.com. Contact our global offices in India, UAE, USA, UK, Saudi Arabia, Malaysia, Singapore, Indonesia, Kenya, and Bangalore.{% endblock %} + +{% block content %} + + + +
+
+
+
+

+ Contact Us +

+

+ Get in touch with our global team of experts. We're here to help you with business incorporation, compliance, and expansion worldwide. +

+ +
+
+
+ +
+
+
+
+
+
+
+ + +
+
+ + +
+

Get In Touch

+ {% if messages %} + {% for message in messages %} +
+ {{ message|safe }} +
+ {% endfor %} + {% endif %} +
+ {% csrf_token %} + + + + + + + + + + + + {% if not user.is_authenticated %} + + + + + + + + {% endif %} + + +
+ + + {% if feedback_submitted %} +
+ Thank you for your feedback! We will get back to you shortly. Please use Ticket ID: {{ ticket_id }} for future reference. +
+ {% endif %} +
+ + +
+

Contact us

+

+ It is very important for us to keep in touch with you, so we are always ready to answer any questions that interest you. Shoot! +

+ + +
+
+
+ +
+
+

HEAD OFFICE

+

ADDRESS:

+

D - 878, LGF, New Friends Colony,

+

New Delhi - 110025, India

+
+
+ +
+
+ +
+
+

PHONE NUMBERS

+

Customer Care: +91 92204 33466

+
+
+ +
+
+ +
+
+

EMAIL

+

info@registeryourstartup.com

+
+
+
+
+
+
+ + + +
+
+

Our Global Offices

+
+ +
+

U.A.E Office

+

Visalite Global FZCO

+

Scality Office No 63, Building No 9WC 523

+

PO Box 491, Dubai Airport Freezone

+

Dubai, UAE

+
+ + +
+

U.S. Office

+

3240 E-State Street, Hamilton

+

New Jersey 08619

+

United States

+
+ + +
+

U.K. Office

+

8 Alexandra Road, Worthing

+

West Sussex BN 11 2DX

+

United Kingdom

+
+ + +
+

Saudi Arabia Office

+

Building No. 4219, Al Izdihar Street

+

Unit No. 4301, Riyadh 12486

+

Saudi Arabia

+
+ + +
+

Malaysia Office

+

Landmark, Suite 1705, Level 17

+

12 Jalan Ngee Heng

+

80000 Johor Bahru, Johor

+

Malaysia

+
+ + +
+

Singapore Office

+

3 Shenton Way, #09-07

+

Shenton House

+

Singapore 068805

+
+ + +
+

Indonesia Office

+

Cyber 2 Tower, Rasuna Said

+

Kuningan, Jakarta

+

Phone: +62 812-863-18349

+
+ + +
+

Kenya Office

+

#7 Vakaria Investment

+

Mombasa Road, Nairobi

+

Kenya

+
+ + +
+

Bangalore Office

+

Bizcon Services Toyama Bizhub

+

Second Floor, Near Manyata Tech Park

+

Thannisandra Main Road

+

Bangalore 560077, India

+
+
+
+
+ + +
+
+

Ready to Start Your Business Journey?

+

Contact us today and let our expert team guide you through seamless business setup and expansion

+ +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/at_django_boilerplate/communications/tests.py b/at_django_boilerplate/communications/tests.py new file mode 100755 index 0000000..7ce503c --- /dev/null +++ b/at_django_boilerplate/communications/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/at_django_boilerplate/communications/urls.py b/at_django_boilerplate/communications/urls.py new file mode 100755 index 0000000..44f80aa --- /dev/null +++ b/at_django_boilerplate/communications/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('contact/', views.ContactView.as_view(), name='contact_us_form'), + path('book-appointment/', views.AppointmentView.as_view(), name='book_appointment'), + ] \ No newline at end of file diff --git a/at_django_boilerplate/communications/views.py b/at_django_boilerplate/communications/views.py new file mode 100755 index 0000000..7a52bdc --- /dev/null +++ b/at_django_boilerplate/communications/views.py @@ -0,0 +1,190 @@ +from django.shortcuts import render, redirect +from django.core.mail import send_mail +from django.conf import settings +from django.views.generic import View +from django.contrib import messages +from django.utils import timezone # Ensure this is imported + +from at_django_boilerplate.accounts.models import CustomUser +from at_django_boilerplate.communications.models import FeedbackModel, AppointmentModel +from at_django_boilerplate.backend_admin.models import SEOConfiguration + + +class ContactView(View): + template_name = 'contact_us_form.html' + + def get(self, request, *args, **kwargs): + seo = SEOConfiguration.objects.first() + context = { + 'user': request.user, + 'meta_title': seo.contact_meta_title if seo else "Contact Us - RegisterYourStartup", + 'meta_description': seo.contact_meta_description if seo else "Get in touch with our team." + } + return render(request, self.template_name, context) + + def post(self, request, *args, **kwargs): + custom_user = None + if request.user.is_authenticated: + try: + custom_user = request.user + except CustomUser.DoesNotExist: + pass + + subject = request.POST.get('subject') + message = request.POST.get('message') + name = request.POST.get('name') + + email = '' + phone = '' + full_name = name or "Anonymous User" + + if custom_user: + email = custom_user.get_decrypted_email() + phone = custom_user.get_decrypted_contact_number() + full_name = custom_user.get_full_name() or name or "Authenticated User" + else: + email = request.POST.get('email') + phone = request.POST.get('phone') + + # Save feedback + feedback = FeedbackModel( + user=custom_user, + name=full_name, + email=email, + phone=phone, + subject=subject, + message=message + ) + feedback.save() + ticket_id = feedback.id + + # Prepare and send email + email_message = f''' + Ticket ID:\t{ticket_id} + Name:\t{full_name} + Email:\t{email} + Contact Number:\t{phone or 'Not provided'} + Subject:\t{subject} + Message:\t{message} + ''' + + try: + send_mail( + subject='You have a new message on RegisterYourStartup', + message=email_message, + from_email=settings.EMAIL_HOST_USER, + recipient_list=[settings.EMAIL_HOST_USER], + fail_silently=False, + ) + except Exception as e: + print("Email sending failed:", e) + + messages.success( + request, + f"Thank you, {full_name.split()[0] if full_name.split() else 'there'}! " + f"Your message has been sent successfully. " + f"Your Ticket ID is {ticket_id}. We will get back to you shortly." + ) + + return redirect('contact_us_form') + + +class AppointmentView(View): + template_name = 'book_appointment.html' + + def get(self, request, *args, **kwargs): + seo = SEOConfiguration.objects.first() + context = { + 'meta_title': seo.appointment_meta_title if seo and seo.appointment_meta_title else "Book an Appointment - RegisterYourStartup.com", + 'meta_description': seo.appointment_meta_description if seo and seo.appointment_meta_description else "Schedule a consultation with our business experts.", + } + return render(request, self.template_name, context) + + def post(self, request, *args, **kwargs): + custom_user = None + if request.user.is_authenticated: + try: + custom_user = request.user + except CustomUser.DoesNotExist: + pass + + full_name = request.POST.get('fullName') + email = request.POST.get('email') + phone = request.POST.get('phone') + meeting_with = request.POST.get('meetingWith') + appointment_datetime_str = request.POST.get('appointmentDateTime') + notes = request.POST.get('notes', '') + + # Basic server-side validation + if not all([full_name, email, phone, meeting_with, appointment_datetime_str]): + messages.error(request, "All required fields must be filled.") + return redirect('book_appointment') + + try: + # Parse the datetime-local input (format: YYYY-MM-DDTHH:MM) + # Replace 'T' with space to make it compatible with fromisoformat + naive_datetime = timezone.datetime.fromisoformat( + appointment_datetime_str.replace('T', ' ') + ) + + # Convert to timezone-aware datetime + appointment_datetime = timezone.make_aware(naive_datetime) + + if appointment_datetime <= timezone.now(): + messages.error(request, "Appointment time must be in the future.") + return redirect('book_appointment') + except ValueError: + messages.error(request, "Invalid date/time format.") + return redirect('book_appointment') + + # Save appointment + appointment = AppointmentModel( + user=custom_user, + full_name=full_name, + email=email, + phone=phone, + meeting_with=meeting_with, + appointment_datetime=appointment_datetime, + notes=notes + ) + appointment.save() + + ticket_id = f"APP-{appointment.pk:06d}" + + # Send email notification to admin + email_subject = f"New Appointment Booking - {ticket_id}" + email_message = f""" +New Appointment Request + +Ticket ID: {ticket_id} +Name: {full_name} +Email: {email} +Phone: {phone} +Meet With: {appointment.get_meeting_with_display()} +Date & Time: {appointment_datetime.strftime('%d %B %Y, %I:%M %p')} +Notes: {notes or 'No additional notes'} + +Please confirm or follow up with the user. + """ + + try: + send_mail( + subject=email_subject, + message=email_message, + from_email=settings.EMAIL_HOST_USER, + recipient_list=[settings.EMAIL_HOST_USER], + fail_silently=False, + ) + except Exception as e: + print("Appointment email failed:", e) + + # Success message via Django messages (shown on redirect) + messages.success( + request, + f"Thank you, {full_name.split()[0] if full_name.split() else 'there'}! " + f"Your appointment has been booked successfully. " + f"Your Ticket ID is {ticket_id}. We will contact you shortly to confirm." + ) + + # Redirect to same page to show success message and prevent resubmission + return redirect('book_appointment') \ No newline at end of file diff --git a/at_django_boilerplate/core/__init__.py b/at_django_boilerplate/core/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/at_django_boilerplate/core/__pycache__/__init__.cpython-312.pyc b/at_django_boilerplate/core/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..e6ab27f Binary files /dev/null and b/at_django_boilerplate/core/__pycache__/__init__.cpython-312.pyc differ diff --git a/at_django_boilerplate/core/__pycache__/admin.cpython-312.pyc b/at_django_boilerplate/core/__pycache__/admin.cpython-312.pyc new file mode 100644 index 0000000..fa95b4e Binary files /dev/null and b/at_django_boilerplate/core/__pycache__/admin.cpython-312.pyc differ diff --git a/at_django_boilerplate/core/__pycache__/apps.cpython-312.pyc b/at_django_boilerplate/core/__pycache__/apps.cpython-312.pyc new file mode 100644 index 0000000..07fd23a Binary files /dev/null and b/at_django_boilerplate/core/__pycache__/apps.cpython-312.pyc differ diff --git a/at_django_boilerplate/core/__pycache__/models.cpython-312.pyc b/at_django_boilerplate/core/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000..043663d Binary files /dev/null and b/at_django_boilerplate/core/__pycache__/models.cpython-312.pyc differ diff --git a/at_django_boilerplate/core/__pycache__/scheduler.cpython-312.pyc b/at_django_boilerplate/core/__pycache__/scheduler.cpython-312.pyc new file mode 100644 index 0000000..26ed44f Binary files /dev/null and b/at_django_boilerplate/core/__pycache__/scheduler.cpython-312.pyc differ diff --git a/at_django_boilerplate/core/__pycache__/urls.cpython-312.pyc b/at_django_boilerplate/core/__pycache__/urls.cpython-312.pyc new file mode 100644 index 0000000..a2b1eb1 Binary files /dev/null and b/at_django_boilerplate/core/__pycache__/urls.cpython-312.pyc differ diff --git a/at_django_boilerplate/core/__pycache__/views.cpython-312.pyc b/at_django_boilerplate/core/__pycache__/views.cpython-312.pyc new file mode 100644 index 0000000..e2c9bbc Binary files /dev/null and b/at_django_boilerplate/core/__pycache__/views.cpython-312.pyc differ diff --git a/at_django_boilerplate/core/admin.py b/at_django_boilerplate/core/admin.py new file mode 100755 index 0000000..43ec650 --- /dev/null +++ b/at_django_boilerplate/core/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from .models import Subscriber + +@admin.register(Subscriber) +class SubscriberAdmin(admin.ModelAdmin): + list_display = ("email", "subscribed_at") + search_fields = ("email",) diff --git a/at_django_boilerplate/core/apps.py b/at_django_boilerplate/core/apps.py new file mode 100755 index 0000000..97d5b9f --- /dev/null +++ b/at_django_boilerplate/core/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'at_django_boilerplate.core' + diff --git a/at_django_boilerplate/core/context_processors.py b/at_django_boilerplate/core/context_processors.py new file mode 100755 index 0000000..93b7d4b --- /dev/null +++ b/at_django_boilerplate/core/context_processors.py @@ -0,0 +1,14 @@ +from django.conf import settings + +def default_meta_attributes(request): + data = {'default_meta_title':'','default_meta_description':''} + meta_title = settings.APP_META_TITLE + meta_description = settings.APP_META_DESCRIPTION + + if meta_title: + data['default_meta_title'] = meta_title + + if meta_description: + data['default_meta_description'] = meta_description + + return data diff --git a/at_django_boilerplate/core/logging/__init__.py b/at_django_boilerplate/core/logging/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/at_django_boilerplate/core/logging/__pycache__/__init__.cpython-312.pyc b/at_django_boilerplate/core/logging/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..4837a39 Binary files /dev/null and b/at_django_boilerplate/core/logging/__pycache__/__init__.cpython-312.pyc differ diff --git a/at_django_boilerplate/core/logging/__pycache__/config.cpython-312.pyc b/at_django_boilerplate/core/logging/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000..0004d3b Binary files /dev/null and b/at_django_boilerplate/core/logging/__pycache__/config.cpython-312.pyc differ diff --git a/at_django_boilerplate/core/logging/__pycache__/views.cpython-312.pyc b/at_django_boilerplate/core/logging/__pycache__/views.cpython-312.pyc new file mode 100644 index 0000000..f0c0983 Binary files /dev/null and b/at_django_boilerplate/core/logging/__pycache__/views.cpython-312.pyc differ diff --git a/at_django_boilerplate/core/logging/config.py b/at_django_boilerplate/core/logging/config.py new file mode 100755 index 0000000..adaef59 --- /dev/null +++ b/at_django_boilerplate/core/logging/config.py @@ -0,0 +1,46 @@ +from pathlib import Path + +def get_logging_config(base_dir: Path): + log_dir = base_dir / "logs" + log_dir.mkdir(exist_ok=True) + + return { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "default": { + "format": "[{asctime}] [{levelname}] {name}: {message}", + "style": "{", + }, + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "default", + }, + "file": { + "class": "logging.handlers.TimedRotatingFileHandler", + "filename": log_dir / "django.log", + "when": "midnight", + "backupCount": 7, + "formatter": "default", + "delay": True, + }, + "mail_admins": { + "class": "django.utils.log.AdminEmailHandler", + "level": "ERROR", + }, + }, + "loggers": { + "django": { + "handlers": ["console", "file"], + "level": "INFO", + "propagate": True, + }, + "django.request": { + "handlers": ["console", "file", "mail_admins"], + "level": "ERROR", + "propagate": False, + }, + }, + } diff --git a/at_django_boilerplate/core/logging/views.py b/at_django_boilerplate/core/logging/views.py new file mode 100755 index 0000000..dfead62 --- /dev/null +++ b/at_django_boilerplate/core/logging/views.py @@ -0,0 +1,128 @@ +# utils/log_viewer.py +from pathlib import Path +from django.conf import settings +from django.contrib.admin.views.decorators import staff_member_required +from django.http import Http404 +from django.shortcuts import render +from datetime import datetime +from django.utils import timezone + + +def tail(filepath, lines=500, chunk_size=1024): + with open(filepath, "rb") as f: + f.seek(0, 2) + file_size = f.tell() + + buffer = b"" + pointer = file_size + + while pointer > 0 and buffer.count(b"\n") < lines: + read_size = min(chunk_size, pointer) + pointer -= read_size + f.seek(pointer) + buffer = f.read(read_size) + buffer + + return buffer.decode(errors="ignore").splitlines()[-lines:] + + + +@staff_member_required +def log_list(request): + log_dir = Path(settings.BASE_DIR) / "logs" + + files = [] + for f in log_dir.glob("*.log"): + stat = f.stat() + files.append({ + "name": f.name, + "size": stat.st_size, + "mtime": timezone.make_aware( + datetime.fromtimestamp(stat.st_mtime)) + }) + + return render(request, "logging/logs_list.html", {"files": files}) + + + + + +# @staff_member_required +# def log_detail(request, filename): +# log_dir = Path(settings.LOG_VIEWER_DIR) +# file_path = log_dir / filename + +# if not file_path.exists(): +# raise Http404 + +# lines = tail(file_path, settings.LOG_VIEWER_MAX_LINES) + +# return render( +# request, +# "logging/log_detail.html", +# {"filename": filename, "lines": lines}, +# ) + + +import re +from pathlib import Path +from django.conf import settings +from django.shortcuts import render +from django.http import Http404 + +LOG_PATTERN = re.compile( + r""" + ^\[ + (?P