From e124e4c6c7d457cda94ce15eadd704be59889ac2 Mon Sep 17 00:00:00 2001 From: DevilsAutumn Date: Thu, 12 Dec 2024 17:03:34 +0530 Subject: [PATCH 1/4] WIP --- backend/djangoindia/db/models/user.py | 71 +++++++++++++++++++++++++++ contributing.md | 2 +- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/backend/djangoindia/db/models/user.py b/backend/djangoindia/db/models/user.py index 46b959da..83afa542 100644 --- a/backend/djangoindia/db/models/user.py +++ b/backend/djangoindia/db/models/user.py @@ -1,3 +1,4 @@ +<<<<<<< HEAD # Python imports import uuid @@ -17,6 +18,15 @@ class User(AbstractBaseUser, PermissionsMixin): +======= +from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin +from django.db import models + +from .base import BaseModel + + +class User(PermissionsMixin, AbstractBaseUser): +>>>>>>> 0e67722 (WIP) class GENDER: CHOICES = ( ("male", "male"), @@ -25,6 +35,7 @@ class GENDER: ("not_to_specify", "not_to_specify"), ) +<<<<<<< HEAD USER_TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones)) username = models.CharField(max_length=128, unique=True) @@ -98,3 +109,63 @@ def save(self, *args, **kwargs): self.is_staff = True super().save(*args, **kwargs) +======= + first_name = models.CharField(max_length=255, blank=False, null=False) + last_name = models.CharField(max_length=255, blank=False, null=False) + unique_username = models.CharField(max_length=50, null=True, blank=True) + gender = models.CharField(choices=GENDER.CHOICES, max_length=50) + email = models.EmailField(max_length=255, unique=True, blank=False, null=False) + phone_number = models.CharField(max_length=50, blank=True, null=True) + date_of_birth = models.DateTimeField(blank=True, null=True) + organization = models.CharField(max_length=500, blank=True, null=True) + year_of_experience = models.IntegerField(default=None, blank=True, null=True) + college = models.CharField(max_length=500, blank=True, null=True) + year_of_passing = models.DateField(blank=True, null=True) + is_active = models.BooleanField(default=True) + is_staff = models.BooleanField(default=False, verbose_name="community volunteer") + is_superuser = models.BooleanField(default=False) + is_professional = models.BooleanField(default=False) + + +class UserSocials: + class SoialMediaType: + CHOICES = ( + ("linkedin", "linkedin"), + ("github", "github"), + ("twitter", "twitter"), + ) + + user = models.ForeignKey(User, null=False, blank=False) + social_media_type = models.CharField(choices=SoialMediaType.CHOICES, max_length=50) + profile_link = models.CharField(max_length=500, blank=True, null=True) + + +class LogHistory(BaseModel): + user = models.ForeignKey(User, null=False, blank=False) + login_timestamp = models.DateTimeField(auto_now=True) + + +class UserEventHistory(BaseModel): + class RegistrationStatusType: + CHOICES = ( + ("interested", "interested"), + ("rsvped", "rsvped"), + ("confirmed", "confirmed"), + ("shortlisted", "shortlisted")("waiting", "waiting")( + "cancelled", "cancelled" + ), + ) + + user = models.ForeignKey("User", null=False, blank=False) + event = models.ForeignKey("Event", null=False, blank=False) + registration_time = models.DateTimeField(auto_now=True) + registration_status = models.CharField( + choices=RegistrationStatusType.CHOICES, max_length=50 + ) + + +# register for event +# rsvp for event -> going/ not going +# show events on my profile +# django stories +>>>>>>> 0e67722 (WIP) diff --git a/contributing.md b/contributing.md index ca45f57b..c8267c30 100644 --- a/contributing.md +++ b/contributing.md @@ -104,7 +104,7 @@ Remember, every contribution, no matter how small, is valuable and appreciated! 2. **Clone the repository:** ``` - git clone https://github.com//djangoindia.org + git clone https://github.com/djangoindia/djangoindia.org.git ``` ### Without Docker From 4add410ed4a2d985e40fd2c1a2db2cd44e14e82b Mon Sep 17 00:00:00 2001 From: DevilsAutumn Date: Sun, 29 Dec 2024 20:42:17 +0530 Subject: [PATCH 2/4] WIP --- .env.example | 2 + .../djangoindia/api/serializers/__init__.py | 6 + backend/djangoindia/api/serializers/base.py | 5 + backend/djangoindia/api/serializers/user.py | 113 +++ backend/djangoindia/api/urls/__init__.py | 9 +- .../djangoindia/api/urls/authentication.py | 39 + backend/djangoindia/api/urls/user.py | 39 + backend/djangoindia/api/views/__init__.py | 25 + .../djangoindia/api/views/authentication.py | 500 +++++++++++ backend/djangoindia/api/views/base.py | 72 ++ backend/djangoindia/api/views/user.py | 36 + backend/djangoindia/bg_tasks/auth/__init__.py | 0 .../bg_tasks/auth/email_verification_task.py | 37 + .../bg_tasks/auth/forgot_password_task.py | 45 + backend/djangoindia/db/admin.py | 642 ++++++------- ...or_remove_event_event_end_date_and_more.py | 131 +++ ...inconnection_user_eventuserregistration.py | 847 ++++++++++++++++++ backend/djangoindia/db/models/__init__.py | 11 +- backend/djangoindia/db/models/event.py | 31 + backend/djangoindia/db/models/user.py | 137 +-- backend/djangoindia/settings/base.py | 41 + backend/requirements/base.txt | 7 +- .../templates/auth/email_verification.html | 97 ++ backend/templates/auth/forgot_password.html | 103 +++ 24 files changed, 2590 insertions(+), 385 deletions(-) create mode 100644 backend/djangoindia/api/serializers/base.py create mode 100644 backend/djangoindia/api/serializers/user.py create mode 100644 backend/djangoindia/api/urls/authentication.py create mode 100644 backend/djangoindia/api/urls/user.py create mode 100644 backend/djangoindia/api/views/authentication.py create mode 100644 backend/djangoindia/api/views/base.py create mode 100644 backend/djangoindia/api/views/user.py create mode 100644 backend/djangoindia/bg_tasks/auth/__init__.py create mode 100644 backend/djangoindia/bg_tasks/auth/email_verification_task.py create mode 100644 backend/djangoindia/bg_tasks/auth/forgot_password_task.py create mode 100644 backend/djangoindia/db/migrations/0007_sponsor_remove_event_event_end_date_and_more.py create mode 100644 backend/djangoindia/db/migrations/0014_socialloginconnection_user_eventuserregistration.py create mode 100644 backend/templates/auth/email_verification.html create mode 100644 backend/templates/auth/forgot_password.html diff --git a/.env.example b/.env.example index d5827b72..ef598ffa 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,8 @@ EMAIL_PORT=port EMAIL_HOST_USER=user EMAIL_HOST_PASSWORD=password +GOOGLE_CLIENT_ID=id + CELERY_BROKER_URL= amqp://guest:guest@rabbitmq:5672// #pragma: allowlist secret CELERY_RESULT_BACKEND= db+postgresql://postgres:postgres@postgres:5432/djangoindia-db #pragma: allowlist secret diff --git a/backend/djangoindia/api/serializers/__init__.py b/backend/djangoindia/api/serializers/__init__.py index 811063a5..91a17c43 100644 --- a/backend/djangoindia/api/serializers/__init__.py +++ b/backend/djangoindia/api/serializers/__init__.py @@ -2,6 +2,8 @@ from .event import EventRegistrationSerializer, EventSerializer from .media_library import FolderSerializer from .partner_and_sponsor import CommunityPartnerAndSponsorSerializer +from .user import ChangePasswordSerializer, UserMeSerializer, UserSerializer +from .volunteer import VolunteerSerializer __all__ = [ @@ -11,4 +13,8 @@ "EventSerializer", "FolderSerializer", "CommunityPartnerAndSponsorSerializer", + "UserSerializer", + "UserMeSerializer", + "ChangePasswordSerializer", + "VolunteerSerializer", ] diff --git a/backend/djangoindia/api/serializers/base.py b/backend/djangoindia/api/serializers/base.py new file mode 100644 index 00000000..0c6bba46 --- /dev/null +++ b/backend/djangoindia/api/serializers/base.py @@ -0,0 +1,5 @@ +from rest_framework import serializers + + +class BaseSerializer(serializers.ModelSerializer): + id = serializers.PrimaryKeyRelatedField(read_only=True) diff --git a/backend/djangoindia/api/serializers/user.py b/backend/djangoindia/api/serializers/user.py new file mode 100644 index 00000000..1e8b14ee --- /dev/null +++ b/backend/djangoindia/api/serializers/user.py @@ -0,0 +1,113 @@ +# Module import +from rest_framework import serializers + +from djangoindia.db.models import User + +from .base import BaseSerializer + + +class UserSerializer(BaseSerializer): + class Meta: + model = User + fields = "__all__" + read_only_fields = [ + "id", + "created_at", + "updated_at", + "is_superuser", + "is_staff", + "is_onboarded", + "is_password_autoset", + "is_email_verified", + ] + extra_kwargs = {"password": {"write_only": True}} + + +class UserMeSerializer(BaseSerializer): + class Meta: + model = User + fields = [ + "id", + "avatar", + "created_at", + "email", + "first_name", + "last_name", + "is_active", + "is_email_verified", + "is_onboarded", + "mobile_number", + "user_timezone", + "username", + "is_password_autoset", + "gender", + "organization", + ] + read_only_fields = fields + + +class ChangePasswordSerializer(serializers.Serializer): + model = User + + """ + Serializer for password change endpoint. + """ + old_password = serializers.CharField(required=True) + new_password = serializers.CharField(required=True, min_length=8) + confirm_password = serializers.CharField(required=True, min_length=8) + + def validate(self, data): + if data.get("old_password") == data.get("new_password"): + raise serializers.ValidationError( + {"message": "New password cannot be same as old password."} + ) + + if data.get("new_password") != data.get("confirm_password"): + raise serializers.ValidationError( + {"message": "Confirm password should be same as the new password."} + ) + + return data + + +class ResetPasswordSerializer(serializers.Serializer): + """ + Serializer for password change endpoint. + """ + + new_password = serializers.CharField(required=True, min_length=8) + + +class UserLiteSerializer(BaseSerializer): + class Meta: + model = User + fields = [ + "id", + "first_name", + "last_name", + "avatar", + "username", + "email", + "role", + ] + read_only_fields = [ + "id", + "username", + "email", + ] + + +class UserAdminLiteSerializer(BaseSerializer): + class Meta: + model = User + fields = [ + "id", + "first_name", + "last_name", + "avatar", + "username", + "email", + ] + read_only_fields = [ + "id", + ] diff --git a/backend/djangoindia/api/urls/__init__.py b/backend/djangoindia/api/urls/__init__.py index 6fb8f4a0..a9113df0 100644 --- a/backend/djangoindia/api/urls/__init__.py +++ b/backend/djangoindia/api/urls/__init__.py @@ -1,9 +1,16 @@ +from .authentication import urlpatterns as auth_urls from .communication import urlpatterns as communication_urls from .event import urlpatterns as event_urls from .media_library import urlpatterns as media_library_urls from .partner_and_sponsor import urlpatterns as community_partner_urls +from .user import urlpatterns as user_urls urlpatterns = ( - event_urls + communication_urls + community_partner_urls + media_library_urls + event_urls + + communication_urls + + community_partner_urls + + media_library_urls + + user_urls + + auth_urls ) diff --git a/backend/djangoindia/api/urls/authentication.py b/backend/djangoindia/api/urls/authentication.py new file mode 100644 index 00000000..e7e250ba --- /dev/null +++ b/backend/djangoindia/api/urls/authentication.py @@ -0,0 +1,39 @@ +from django.urls import path + +from djangoindia.api.views import ( + ForgotPasswordEndpoint, + OauthEndpoint, + RequestEmailVerificationEndpoint, + ResetPasswordEndpoint, + SignInEndpoint, + SignOutEndpoint, + SignUpEndpoint, + VerifyEmailEndpoint, +) + + +urlpatterns = [ + path("social-auth/", OauthEndpoint.as_view(), name="oauth"), + # Auth + path("sign-up/", SignUpEndpoint.as_view(), name="sign-up"), + path("sign-in/", SignInEndpoint.as_view(), name="sign-in"), + path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"), + # email verification + path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"), + path( + "request-email-verify/", + RequestEmailVerificationEndpoint.as_view(), + name="request-reset-email", + ), + # Password Manipulation + path( + "reset-password///", + ResetPasswordEndpoint.as_view(), + name="password-reset", + ), + path( + "forgot-password/", + ForgotPasswordEndpoint.as_view(), + name="forgot-password", + ), +] diff --git a/backend/djangoindia/api/urls/user.py b/backend/djangoindia/api/urls/user.py new file mode 100644 index 00000000..2db549ec --- /dev/null +++ b/backend/djangoindia/api/urls/user.py @@ -0,0 +1,39 @@ +from django.urls import path + +from djangoindia.api.views import ( + ChangePasswordEndpoint, + SetUserPasswordEndpoint, + UpdateUserOnBoardedEndpoint, + UserEndpoint, +) + + +urlpatterns = [ + # User Profile + path( + "users/me/", + UserEndpoint.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "deactivate", + } + ), + name="users", + ), + path( + "users/me/onboard/", + UpdateUserOnBoardedEndpoint.as_view(), + name="user-onboard", + ), + path( + "users/me/set-password/", + SetUserPasswordEndpoint.as_view(), + name="set-password", + ), + path( + "users/me/change-password/", + ChangePasswordEndpoint.as_view(), + name="change-password", + ), +] diff --git a/backend/djangoindia/api/views/__init__.py b/backend/djangoindia/api/views/__init__.py index 85fcbc9f..187ed452 100644 --- a/backend/djangoindia/api/views/__init__.py +++ b/backend/djangoindia/api/views/__init__.py @@ -1,7 +1,20 @@ +from .authentication import ( + ChangePasswordEndpoint, + ForgotPasswordEndpoint, + OauthEndpoint, + RequestEmailVerificationEndpoint, + ResetPasswordEndpoint, + SetUserPasswordEndpoint, + SignInEndpoint, + SignOutEndpoint, + SignUpEndpoint, + VerifyEmailEndpoint, +) from .communication import ContactUsAPIView, SubscriberAPIView from .event import EventAPIView, EventAttendeeViewSet from .media_library import FolderLiteSerializer, FolderSerializer from .partner_and_sponsor import CommunityPartnerAndSponsorAPIView +from .user import UpdateUserOnBoardedEndpoint, UserEndpoint __all__ = [ @@ -12,4 +25,16 @@ "FolderLiteSerializer", "FolderSerializer", "CommunityPartnerAndSponsorAPIView", + "UserEndpoint", + "UpdateUserOnBoardedEndpoint", + "ChangePasswordEndpoint", + "ForgotPasswordEndpoint", + "ResetPasswordEndpoint", + "SetUserPasswordEndpoint", + "SignInEndpoint", + "SignOutEndpoint", + "SignUpEndpoint", + "VerifyEmailEndpoint", + "RequestEmailVerificationEndpoint", + "OauthEndpoint", ] diff --git a/backend/djangoindia/api/views/authentication.py b/backend/djangoindia/api/views/authentication.py new file mode 100644 index 00000000..18a7cb12 --- /dev/null +++ b/backend/djangoindia/api/views/authentication.py @@ -0,0 +1,500 @@ +# Python imports +import os +import uuid + +import jwt + +from google.auth.transport import requests as google_auth_request +from google.oauth2 import id_token +from rest_framework import exceptions, status +from rest_framework.permissions import AllowAny, IsAuthenticated + +# Third Party modules +from rest_framework.response import Response +from rest_framework_simplejwt.tokens import RefreshToken + +## Django imports +from django.conf import settings +from django.contrib.auth.hashers import make_password +from django.contrib.auth.tokens import PasswordResetTokenGenerator +from django.core.exceptions import ValidationError +from django.core.validators import validate_email +from django.utils import timezone +from django.utils.encoding import DjangoUnicodeDecodeError, smart_bytes, smart_str +from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode + +from djangoindia.api.serializers.user import ( + ChangePasswordSerializer, + ResetPasswordSerializer, + UserSerializer, +) +from djangoindia.api.views.base import BaseAPIView +from djangoindia.bg_tasks.auth.email_verification_task import email_verification_task +from djangoindia.bg_tasks.auth.forgot_password_task import forgot_password_task + +# Module imports +from djangoindia.db.models import SocialLoginConnection, User + + +def generate_password_token(user): + uidb64 = urlsafe_base64_encode(smart_bytes(user.uuid)) + token = PasswordResetTokenGenerator().make_token(user) + + return uidb64, token + + +def get_tokens_for_user(user): + refresh = RefreshToken.for_user(user) + return ( + str(refresh.access_token), + str(refresh), + ) + + +def validate_google_token(token, client_id): + try: + id_info = id_token.verify_oauth2_token( + token, google_auth_request.Request(), client_id + ) + email = id_info.get("email") + first_name = id_info.get("given_name") + last_name = id_info.get("family_name", "") + picture = id_info.get("picture", "") + data = { + "email": email, + "first_name": first_name, + "last_name": last_name, + "picture": picture, + } + return data + except Exception as e: + raise exceptions.AuthenticationFailed("detail with Google connection.") + + +class OauthEndpoint(BaseAPIView): + permission_classes = [AllowAny] + + def post(self, request): + try: + medium = request.data.get("medium", False) + id_token = request.data.get("credential", False) + client_id = request.data.get("clientId", False) + data = {} + GOOGLE_CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID") + + if not medium or not id_token: + return Response( + { + "message": "Something went wrong. Please try again later or contact the support team." + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + if medium == "google": + if not GOOGLE_CLIENT_ID: + return Response( + {"message": "Google login is not configured"}, + status=status.HTTP_400_BAD_REQUEST, + ) + data = validate_google_token(id_token, client_id) + + email = data.get("email", None) + if email is None: + return Response( + { + "message": "Something went wrong. Please try again later or contact the support team." + }, + status=status.HTTP_400_BAD_REQUEST, + ) + if "@" in email: + user = User.objects.get(email=email) + email = data["email"] + email_verified = True + else: + return Response( + { + "message": "Something went wrong. Please try again later or contact the support team." + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + user.is_active = True + user.is_email_verified = email_verified + user.save() + + SocialLoginConnection.objects.update_or_create( + medium=medium, + extra_data={}, + user=user, + defaults={ + "token_data": {"id_token": id_token}, + "last_login_at": timezone.now(), + }, + ) + + access_token, refresh_token = get_tokens_for_user(user) + + data = { + "access_token": access_token, + "refresh_token": refresh_token, + } + return Response(data, status=status.HTTP_200_OK) + + except User.DoesNotExist: + if "@" in email: + email = data["email"] + email_verified = True + username = email.split("@")[0] + else: + return Response( + { + "message": "Something went wrong. Please try again later or contact the support team." + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + user = User.objects.create( + username=username, + email=email, + first_name=data.get("first_name"), + last_name=data.get("last_name"), + avatar=data.get("picture"), + is_email_verified=email_verified, + is_password_autoset=True, + ) + + user.set_password(uuid.uuid4().hex) + user.save() + + SocialLoginConnection.objects.update_or_create( + medium=medium, + extra_data={}, + user=user, + defaults={ + "token_data": {"id_token": id_token}, + "last_login_at": timezone.now(), + }, + ) + + access_token, refresh_token = get_tokens_for_user(user) + data = { + "access_token": access_token, + "refresh_token": refresh_token, + } + + return Response(data, status=status.HTTP_201_CREATED) + + +class SignUpEndpoint(BaseAPIView): + permission_classes = (AllowAny,) + + def post(self, request): + email = request.data.get("email", False) + password = request.data.get("password", False) + confirm_password = request.data.get("confirm_password", False) + + if not password == confirm_password: + return Response( + {"message": "Passwords does not match."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Raise exception if any of the above are missing + if not email or not password: + return Response( + {"message": "Both email and password are required."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + email = email.strip().lower() + + try: + validate_email(email) + except ValidationError: + return Response( + {"message": "Please provide a valid email address."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Check if the user already exists + if User.objects.filter(email=email).exists(): + return Response( + {"message": "User with this email already exists."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + user = User.objects.create( + email=email, + username=email.split("@")[0], + password=make_password(password), + ) + + user.save() + + access_token, refresh_token = get_tokens_for_user(user) + + data = { + "access_token": access_token, + "refresh_token": refresh_token, + } + + return Response(data, status=status.HTTP_200_OK) + + +class SignInEndpoint(BaseAPIView): + permission_classes = (AllowAny,) + + def post(self, request): + email = request.data.get("email", False) + password = request.data.get("password", False) + + # Raise exception if any of the above are missing + if not email or not password: + return Response( + {"message": "Both email and password are required."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + email = email.strip().lower() + + try: + validate_email(email) + except ValidationError: + return Response( + {"message": "Please provide a valid email address."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + user = User.objects.filter(email=email).first() + + if user is None: + return Response( + { + "message": "Sorry, we could not find a user with the provided credentials. Please try again." + }, + status=status.HTTP_403_FORBIDDEN, + ) + + # Sign up Process + if not user.check_password(password): + return Response( + {"message": "Incorrect password."}, + status=status.HTTP_403_FORBIDDEN, + ) + if not user.is_active: + return Response( + { + "message": "Your account has been deactivated. Please contact your site administrator." + }, + status=status.HTTP_403_FORBIDDEN, + ) + + user.save() + access_token, refresh_token = get_tokens_for_user(user) + data = { + "access_token": access_token, + "refresh_token": refresh_token, + } + + return Response(data, status=status.HTTP_200_OK) + + +class SignOutEndpoint(BaseAPIView): + def post(self, request): + refresh_token = request.data.get("refresh_token", False) + + if not refresh_token: + return Response( + {"message": "No refresh token provided"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + user = User.objects.get(pk=request.user.id) + + user.save() + + token = RefreshToken(refresh_token) + token.blacklist() + return Response({"message": "success"}, status=status.HTTP_200_OK) + + +class RequestEmailVerificationEndpoint(BaseAPIView): + permission_classes = [ + IsAuthenticated, + ] + + def get(self, request): + refresh_token = RefreshToken.for_user(request.user) + token = str(refresh_token.access_token) + current_site = settings.WEB_URL + email_verification_task.delay( + request.user.first_name, request.user.email, token, current_site + ) + return Response( + {"message": "Email sent successfully."}, status=status.HTTP_200_OK + ) + + +class VerifyEmailEndpoint(BaseAPIView): + permission_classes = [ + IsAuthenticated, + ] + + def get(self, request): + token = request.GET.get("token") + try: + payload = jwt.decode(token, settings.SECRET_KEY, algorithms="HS256") + user = User.objects.get(id=payload["user_id"]) + + if not user.is_email_verified: + user.is_email_verified = True + user.save() + return Response( + {"message": "Successfully activated."}, status=status.HTTP_200_OK + ) + else: + return Response( + {"message": "Email already verified."}, status=status.HTTP_200_OK + ) + except jwt.ExpiredSignatureError: + return Response( + {"message": "Activation expired."}, status=status.HTTP_400_BAD_REQUEST + ) + except jwt.exceptions.DecodeError: + return Response( + {"message": "Invalid token."}, status=status.HTTP_400_BAD_REQUEST + ) + + +class ForgotPasswordEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def post(self, request): + email = request.data.get("email") + + try: + validate_email(email) + except ValidationError: + return Response( + {"message": "Please enter a valid email"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Get the user + user = User.objects.filter(email=email).first() + if user: + # Get the reset token for user + uidb64, token = generate_password_token(user=user) + # send the forgot password email + forgot_password_task.delay(user.first_name, user.email, uidb64, token) + return Response( + {"message": "Check your email to reset your password"}, + status=status.HTTP_200_OK, + ) + return Response( + {"message": "User with the provided email does not exist."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + +class ResetPasswordEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def post(self, request, uidb64, token): + try: + # Decode the id from the uidb64 + id = smart_str(urlsafe_base64_decode(uidb64)) + user = User.objects.get(uuid=id) + + # check if the token is valid for the user + if not PasswordResetTokenGenerator().check_token(user, token): + return Response( + {"message": "Token is invalid"}, + status=status.HTTP_401_UNAUTHORIZED, + ) + + # Reset the password + serializer = ResetPasswordSerializer(data=request.data) + if serializer.is_valid(): + # set_password also hashes the password that the user will get + user.set_password(serializer.data.get("new_password")) + user.is_password_autoset = False + user.save() + + # Log the user in + # Generate access token for the user + access_token, refresh_token = get_tokens_for_user(user) + data = { + "access_token": access_token, + "refresh_token": refresh_token, + } + + return Response(data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + except DjangoUnicodeDecodeError as indentifier: + return Response( + {"message": "token is not valid, please check the new one"}, + status=status.HTTP_401_UNAUTHORIZED, + ) + + +class ChangePasswordEndpoint(BaseAPIView): + def post(self, request): + serializer = ChangePasswordSerializer(data=request.data) + user = User.objects.get(pk=request.user.id) + if serializer.is_valid(): + if not user.check_password(serializer.data.get("old_password")): + return Response( + {"message": "Old password is not correct"}, + status=status.HTTP_400_BAD_REQUEST, + ) + # set_password also hashes the password that the user will get + user.set_password(serializer.data.get("new_password")) + user.is_password_autoset = False + user.save() + return Response( + {"message": "Password updated successfully"}, + status=status.HTTP_200_OK, + ) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class SetUserPasswordEndpoint(BaseAPIView): + def post(self, request): + user = User.objects.get(pk=request.user.id) + new_password = request.data.get("new_password", False) + confirm_password = request.data.get("confirm_password", False) + + if not new_password == confirm_password: + return Response( + {"message": "Passwords does not match."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # If the user password is not autoset then return detail + if not user.is_password_autoset: + return Response( + { + "message": "Your password is already set please change your password from profile" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Check password validation + if not new_password and len(str(new_password)) < 8: + return Response( + {"message": "Password is not valid"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Set the user password + user.set_password(new_password) + user.is_password_autoset = False + user.save() + serializer = UserSerializer(user) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/backend/djangoindia/api/views/base.py b/backend/djangoindia/api/views/base.py new file mode 100644 index 00000000..526fb822 --- /dev/null +++ b/backend/djangoindia/api/views/base.py @@ -0,0 +1,72 @@ +# Python imports +import zoneinfo + +from django_filters.rest_framework import DjangoFilterBackend + +# Third part imports +from rest_framework import status +from rest_framework.exceptions import APIException +from rest_framework.filters import SearchFilter +from rest_framework.permissions import IsAuthenticated +from rest_framework.views import APIView +from rest_framework.viewsets import ModelViewSet + +# Django imports +from django.utils import timezone + + +class TimezoneMixin: + """ + This enables timezone conversion according + to the user set timezone + """ + + def initial(self, request, *args, **kwargs): + super().initial(request, *args, **kwargs) + if request.user.is_authenticated: + timezone.activate(zoneinfo.ZoneInfo(request.user.user_timezone)) + else: + timezone.deactivate() + + +class BaseViewSet(TimezoneMixin, ModelViewSet): + model = None + + permission_classes = [ + IsAuthenticated, + ] + + filter_backends = ( + DjangoFilterBackend, + SearchFilter, + ) + + filterset_fields = [] + + search_fields = [] + + def get_queryset(self): + try: + return self.model.objects.all() + except Exception as e: + raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST) + + +class BaseAPIView(TimezoneMixin, APIView): + permission_classes = [ + IsAuthenticated, + ] + + filter_backends = ( + DjangoFilterBackend, + SearchFilter, + ) + + filterset_fields = [] + + search_fields = [] + + def filter_queryset(self, queryset): + for backend in list(self.filter_backends): + queryset = backend().filter_queryset(self.request, queryset, self) + return queryset diff --git a/backend/djangoindia/api/views/user.py b/backend/djangoindia/api/views/user.py new file mode 100644 index 00000000..23616148 --- /dev/null +++ b/backend/djangoindia/api/views/user.py @@ -0,0 +1,36 @@ +# Module imports +from rest_framework import status +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from djangoindia.api.serializers import UserMeSerializer, UserSerializer +from djangoindia.db.models import User + +from .base import BaseAPIView, BaseViewSet + + +class UserEndpoint(BaseViewSet): + serializer_class = UserSerializer + model = User + permission_classes = [IsAuthenticated] + + def get_object(self): + return self.request.user + + def retrieve(self, request): + serialized_data = UserMeSerializer(request.user).data + return Response( + serialized_data, + status=status.HTTP_200_OK, + ) + + def deactivate(self, request): + pass + + +class UpdateUserOnBoardedEndpoint(BaseAPIView): + def patch(self, request): + user = User.objects.get(pk=request.user.id, is_active=True) + user.is_onboarded = request.data.get("is_onboarded", False) + user.save() + return Response({"message": "Updated successfully"}, status=status.HTTP_200_OK) diff --git a/backend/djangoindia/bg_tasks/auth/__init__.py b/backend/djangoindia/bg_tasks/auth/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/djangoindia/bg_tasks/auth/email_verification_task.py b/backend/djangoindia/bg_tasks/auth/email_verification_task.py new file mode 100644 index 00000000..0ed3b5c7 --- /dev/null +++ b/backend/djangoindia/bg_tasks/auth/email_verification_task.py @@ -0,0 +1,37 @@ +# Third party imports +from celery import shared_task + +from django.conf import settings +from django.core.mail import EmailMultiAlternatives +from django.template.loader import render_to_string +from django.utils.html import strip_tags + + +@shared_task +def email_verification_task(first_name, email, token, current_site): + try: + realtivelink = "/email-verify/" + "?token=" + str(token) + abs_url = current_site + realtivelink + + from_email_string = settings.DEFAULT_FROM_EMAIL + + subject = "Verify your Email!" + + context = { + "first_name": first_name, + "verification_url": abs_url, + } + + html_content = render_to_string("auth/email_verification.html", context) + + text_content = strip_tags(html_content) + + msg = EmailMultiAlternatives(subject, text_content, from_email_string, [email]) + msg.attach_alternative(html_content, "text/html") + msg.send() + return + except Exception as e: + # Print logs if in DEBUG mode + if settings.DEBUG: + print(e) + return diff --git a/backend/djangoindia/bg_tasks/auth/forgot_password_task.py b/backend/djangoindia/bg_tasks/auth/forgot_password_task.py new file mode 100644 index 00000000..b6b78ca5 --- /dev/null +++ b/backend/djangoindia/bg_tasks/auth/forgot_password_task.py @@ -0,0 +1,45 @@ +# Django imports +# Third party imports +from celery import shared_task + +from django.conf import settings +from django.core.mail import EmailMultiAlternatives +from django.template.loader import render_to_string +from django.utils.html import strip_tags + + +@shared_task +def forgot_password_task(first_name, email, uidb64, token): + try: + current_site = settings.WEB_URL + + realtivelink = "/reset-password/?uidb64=" + uidb64 + "&token=" + token + abs_url = str(current_site) + realtivelink + from_email_string = settings.DEFAULT_FROM_EMAIL + + subject = "A new password to your Django India account has been requested" + + context = { + "first_name": first_name, + "forgot_password_url": abs_url, + "email": email, + } + + html_content = render_to_string("auth/forgot_password.html", context) + + text_content = strip_tags(html_content) + + msg = EmailMultiAlternatives( + subject=subject, + body=text_content, + from_email=from_email_string, + to=[email], + ) + msg.attach_alternative(html_content, "text/html") + msg.send() + return + except Exception as e: + # Print logs if in DEBUG mode + if settings.DEBUG: + print(e) + return diff --git a/backend/djangoindia/db/admin.py b/backend/djangoindia/db/admin.py index 828d42cb..f99e276e 100644 --- a/backend/djangoindia/db/admin.py +++ b/backend/djangoindia/db/admin.py @@ -1,321 +1,321 @@ -# from django.conf import settings -from import_export import fields, resources -from import_export.admin import ImportExportModelAdmin -from import_export.widgets import ForeignKeyWidget - -from django.conf import settings -from django.contrib import admin, messages -from django.db import transaction -from django.db.models import Count, F -from django.shortcuts import redirect -from django.template.response import TemplateResponse -from django.urls import path - -from djangoindia.bg_tasks.event_registration import send_mass_mail_task -from djangoindia.db.models.communication import ContactUs, Subscriber -from djangoindia.db.models.event import Event, EventRegistration -from djangoindia.db.models.partner_and_sponsor import ( - CommunityPartner, - Sponsor, - Sponsorship, -) -from djangoindia.db.models.update import Update -from djangoindia.db.models.user import User -from djangoindia.db.models.volunteer import Volunteer - -from .forms import EmailForm, EventForm, UpdateForm - - -@admin.action(description="Send email to selected users") -def send_email_to_selected_users(modeladmin, request, queryset): - ids = queryset.values_list("id", flat=True) - return redirect(f'send_email/?ids={",".join(map(str, ids))}') - - -class SponsorInline(admin.TabularInline): - model = Sponsorship - extra = 1 - - -@admin.register(Event) -class EventAdmin(admin.ModelAdmin): - list_display = ("name", "city", "start_date", "event_mode", "created_at") - readonly_fields = ("created_at", "updated_at", "slug") - search_fields = ["name", "city"] - form = EventForm - inlines = [SponsorInline] - filter_horizontal = ("volunteers",) - - -class EventRegistrationResource(resources.ModelResource): - class Meta: - model = EventRegistration - - -@admin.register(EventRegistration) -class EventRegistrationAdmin(ImportExportModelAdmin): - list_display = ( - "event", - "first_name", - "email", - "created_at", - "attendee_type", - "first_time_attendee", - ) - readonly_fields = ("created_at", "updated_at", "first_time_attendee") - list_filter = ("event__name", "attendee_type", "first_time_attendee") - search_fields = [ - "email", - "event__name", - "first_name", - "last_name", - "first_time_attendee", - "attendee_type", - ] - raw_id_fields = ("event",) - actions = [send_email_to_selected_users] - resource_class = EventRegistrationResource - - def get_urls(self): - urls = super().get_urls() - custom_urls = [ - path( - "send_email/", - self.admin_site.admin_view(self.send_email_view), - name="send_email", - ), - ] - return custom_urls + urls - - @transaction.atomic - def delete_model(self, request, obj): - if obj.event.seats_left < obj.event.max_seats: - obj.event.seats_left += 1 - obj.event.save() - super().delete_model(request, obj) - - @transaction.atomic - def delete_queryset(self, request, queryset): - # Group registrations by event and count them - event_counts = queryset.values("event").annotate(count=Count("id")) - - # Update seats_left for each affected event - for event_count in event_counts: - Event.objects.filter(id=event_count["event"]).update( - seats_left=F("seats_left") + event_count["count"] - ) - - # Perform the actual deletion - super().delete_queryset(request, queryset) - - def send_email_view(self, request): - if request.method == "POST": - form = EmailForm(request.POST) - if form.is_valid(): - try: - subject = form.cleaned_data["subject"] - message = form.cleaned_data["message"] - emails = [] - from_email = settings.DEFAULT_FROM_EMAIL - - registration_ids = request.GET.get("ids").split(",") - queryset = EventRegistration.objects.filter(id__in=registration_ids) - - for registration in queryset: - recipient_email = registration.email - emails.append((subject, message, from_email, [recipient_email])) - - send_mass_mail_task(emails, fail_silently=False) - messages.success( - request, f"{len(emails)} emails sent successfully." - ) - return redirect("../") - except Exception as e: - messages.error(request, f"Error sending emails: {str(e)}") - else: - form = EmailForm() - - context = { - "form": form, - "opts": self.model._meta, - "queryset": request.GET.get("ids").split(","), - } - return TemplateResponse(request, "admin/send_email.html", context) - - -@admin.register(Subscriber) -class SubscriberAdmin(admin.ModelAdmin): - list_display = ("email", "created_at") - readonly_fields = ("created_at", "updated_at") - ordering = ("-created_at",) - search_fields = [ - "name", - "email", - ] - - -@admin.register(ContactUs) -class ContactUsAdmin(admin.ModelAdmin): - list_display = ("first_name", "email", "created_at") - readonly_fields = ("created_at", "updated_at") - ordering = ("-created_at",) - search_fields = [ - "email", - ] - - -class SponsorshipResource(resources.ModelResource): - sponsor_name = fields.Field( - column_name="sponsor_name", - attribute="sponsor_details", - widget=ForeignKeyWidget(Sponsor, "name"), - ) - sponsor_email = fields.Field( - column_name="sponsor_email", attribute="sponsor_details__email" - ) - sponsor_url = fields.Field( - column_name="sponsor_url", attribute="sponsor_details__url" - ) - - class Meta: - model = Sponsorship - fields = ( - "id", - "sponsor_name", - "sponsor_email", - "sponsor_url", - "tier", - "type", - "amount_inr", - "created_at", - "updated_at", - ) - export_order = fields - - -@admin.register(Sponsorship) -class SponsorshipAdmin(ImportExportModelAdmin): - list_display = ("sponsor_details", "tier", "type", "event") - list_filter = ("type", "event", "tier") - search_fields = [ - "sponsor_details__name", - ] - readonly_fields = ("created_at", "updated_at") - resource_class = SponsorshipResource - - def get_export_queryset(self, request): - return super().get_export_queryset(request).select_related("sponsor_details") - - -@admin.register(Sponsor) -class SponsorAdmin(admin.ModelAdmin): - list_display = ["name", "type", "email"] - search_fields = [ - "name", - ] - readonly_fields = ("created_at", "updated_at") - - -# email sending functionality and update registration -@admin.register(Update) -class UpdateAdmin(admin.ModelAdmin): - form = UpdateForm - list_display = ("email_subject", "type", "created_at", "mail_sent") - search_fields = ["email_subject", "type"] - readonly_fields = ("created_at", "updated_at") - actions = ["send_update"] - - @admin.action(description="Send selected updates to subscribers") - def send_update(self, request, queryset): - for update in queryset: - update.send_bulk_emails() - self.message_user(request, "Update emails sent.") - - -@admin.register(CommunityPartner) -class CommunityPartnerAdmin(admin.ModelAdmin): - list_display = [ - "name", - "website", - "contact_name", - "contact_email", - "contact_number", - "description", - ] - search_fields = ["name"] - readonly_fields = ("created_at", "updated_at") - - -class EventVolunteerResource(resources.ModelResource): - class Meta: - model = Volunteer - fields = ("id", "name", "about", "email", "twitter", " linkedin") - - -@admin.register(Volunteer) -class EventVolunteerAdmin(ImportExportModelAdmin): - list_display = ["name", "about", "email"] - search_fields = ["events__name", "name", "email"] - readonly_fields = ("created_at", "updated_at") - list_filter = ("events__name",) - resource_class = EventVolunteerResource - filter_horizontal = ("events",) - - -@admin.register(User) -class UserAdmin(admin.ModelAdmin): - list_display = ( - "username", - "email", - "first_name", - "last_name", - "is_active", - "is_superuser", - "is_email_verified", - ) - list_filter = ( - "is_active", - "is_staff", - "is_superuser", - "is_email_verified", - "gender", - ) - search_fields = ("username", "email", "first_name", "last_name") - readonly_fields = ("created_at", "updated_at") - filter_horizontal = ( - "groups", - "user_permissions", - ) - fieldsets = ( - (None, {"fields": ("username", "email", "password")}), - ( - "Personal info", - { - "fields": ( - "first_name", - "last_name", - "avatar", - "gender", - "organization", - "mobile_number", - ) - }, - ), - ( - "Permissions", - { - "fields": ( - "is_active", - "is_staff", - "is_superuser", - "groups", - "user_permissions", - "is_email_verified", - "is_password_expired", - "is_onboarded", - ), - }, - ), - ("Important dates", {"fields": ("created_at", "updated_at")}), - ) - ordering = ("-created_at",) +# from django.conf import settings +from import_export import fields, resources +from import_export.admin import ImportExportModelAdmin +from import_export.widgets import ForeignKeyWidget + +from django.conf import settings +from django.contrib import admin, messages +from django.db import transaction +from django.db.models import Count, F +from django.shortcuts import redirect +from django.template.response import TemplateResponse +from django.urls import path + +from djangoindia.bg_tasks.event_registration import send_mass_mail_task +from djangoindia.db.models.communication import ContactUs, Subscriber +from djangoindia.db.models.event import Event, EventRegistration +from djangoindia.db.models.partner_and_sponsor import ( + CommunityPartner, + Sponsor, + Sponsorship, +) +from djangoindia.db.models.update import Update +from djangoindia.db.models.user import User +from djangoindia.db.models.volunteer import Volunteer + +from .forms import EmailForm, EventForm, UpdateForm + + +@admin.action(description="Send email to selected users") +def send_email_to_selected_users(modeladmin, request, queryset): + ids = queryset.values_list("id", flat=True) + return redirect(f'send_email/?ids={",".join(map(str, ids))}') + + +class SponsorInline(admin.TabularInline): + model = Sponsorship + extra = 1 + + +@admin.register(Event) +class EventAdmin(admin.ModelAdmin): + list_display = ("name", "city", "start_date", "event_mode", "created_at") + readonly_fields = ("created_at", "updated_at", "slug") + search_fields = ["name", "city"] + form = EventForm + inlines = [SponsorInline] + filter_horizontal = ("volunteers",) + + +class EventRegistrationResource(resources.ModelResource): + class Meta: + model = EventRegistration + + +@admin.register(EventRegistration) +class EventRegistrationAdmin(ImportExportModelAdmin): + list_display = ( + "event", + "first_name", + "email", + "created_at", + "attendee_type", + "first_time_attendee", + ) + readonly_fields = ("created_at", "updated_at", "first_time_attendee") + list_filter = ("event__name", "attendee_type", "first_time_attendee") + search_fields = [ + "email", + "event__name", + "first_name", + "last_name", + "first_time_attendee", + "attendee_type", + ] + raw_id_fields = ("event",) + actions = [send_email_to_selected_users] + resource_class = EventRegistrationResource + + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path( + "send_email/", + self.admin_site.admin_view(self.send_email_view), + name="send_email", + ), + ] + return custom_urls + urls + + @transaction.atomic + def delete_model(self, request, obj): + if obj.event.seats_left < obj.event.max_seats: + obj.event.seats_left += 1 + obj.event.save() + super().delete_model(request, obj) + + @transaction.atomic + def delete_queryset(self, request, queryset): + # Group registrations by event and count them + event_counts = queryset.values("event").annotate(count=Count("id")) + + # Update seats_left for each affected event + for event_count in event_counts: + Event.objects.filter(id=event_count["event"]).update( + seats_left=F("seats_left") + event_count["count"] + ) + + # Perform the actual deletion + super().delete_queryset(request, queryset) + + def send_email_view(self, request): + if request.method == "POST": + form = EmailForm(request.POST) + if form.is_valid(): + try: + subject = form.cleaned_data["subject"] + message = form.cleaned_data["message"] + emails = [] + from_email = settings.DEFAULT_FROM_EMAIL + + registration_ids = request.GET.get("ids").split(",") + queryset = EventRegistration.objects.filter(id__in=registration_ids) + + for registration in queryset: + recipient_email = registration.email + emails.append((subject, message, from_email, [recipient_email])) + + send_mass_mail_task(emails, fail_silently=False) + messages.success( + request, f"{len(emails)} emails sent successfully." + ) + return redirect("../") + except Exception as e: + messages.error(request, f"Error sending emails: {str(e)}") + else: + form = EmailForm() + + context = { + "form": form, + "opts": self.model._meta, + "queryset": request.GET.get("ids").split(","), + } + return TemplateResponse(request, "admin/send_email.html", context) + + +@admin.register(Subscriber) +class SubscriberAdmin(admin.ModelAdmin): + list_display = ("email", "created_at") + readonly_fields = ("created_at", "updated_at") + ordering = ("-created_at",) + search_fields = [ + "name", + "email", + ] + + +@admin.register(ContactUs) +class ContactUsAdmin(admin.ModelAdmin): + list_display = ("first_name", "email", "created_at") + readonly_fields = ("created_at", "updated_at") + ordering = ("-created_at",) + search_fields = [ + "email", + ] + + +class SponsorshipResource(resources.ModelResource): + sponsor_name = fields.Field( + column_name="sponsor_name", + attribute="sponsor_details", + widget=ForeignKeyWidget(Sponsor, "name"), + ) + sponsor_email = fields.Field( + column_name="sponsor_email", attribute="sponsor_details__email" + ) + sponsor_url = fields.Field( + column_name="sponsor_url", attribute="sponsor_details__url" + ) + + class Meta: + model = Sponsorship + fields = ( + "id", + "sponsor_name", + "sponsor_email", + "sponsor_url", + "tier", + "type", + "amount_inr", + "created_at", + "updated_at", + ) + export_order = fields + + +@admin.register(Sponsorship) +class SponsorshipAdmin(ImportExportModelAdmin): + list_display = ("sponsor_details", "tier", "type", "event") + list_filter = ("type", "event", "tier") + search_fields = [ + "sponsor_details__name", + ] + readonly_fields = ("created_at", "updated_at") + resource_class = SponsorshipResource + + def get_export_queryset(self, request): + return super().get_export_queryset(request).select_related("sponsor_details") + + +@admin.register(Sponsor) +class SponsorAdmin(admin.ModelAdmin): + list_display = ["name", "type", "email"] + search_fields = [ + "name", + ] + readonly_fields = ("created_at", "updated_at") + + +# email sending functionality and update registration +@admin.register(Update) +class UpdateAdmin(admin.ModelAdmin): + form = UpdateForm + list_display = ("email_subject", "type", "created_at", "mail_sent") + search_fields = ["email_subject", "type"] + readonly_fields = ("created_at", "updated_at") + actions = ["send_update"] + + @admin.action(description="Send selected updates to subscribers") + def send_update(self, request, queryset): + for update in queryset: + update.send_bulk_emails() + self.message_user(request, "Update emails sent.") + + +@admin.register(CommunityPartner) +class CommunityPartnerAdmin(admin.ModelAdmin): + list_display = [ + "name", + "website", + "contact_name", + "contact_email", + "contact_number", + "description", + ] + search_fields = ["name"] + readonly_fields = ("created_at", "updated_at") + + +class EventVolunteerResource(resources.ModelResource): + class Meta: + model = Volunteer + fields = ("id", "name", "about", "email", "twitter", " linkedin") + + +@admin.register(Volunteer) +class EventVolunteerAdmin(ImportExportModelAdmin): + list_display = ["name", "about", "email"] + search_fields = ["events__name", "name", "email"] + readonly_fields = ("created_at", "updated_at") + list_filter = ("events__name",) + resource_class = EventVolunteerResource + filter_horizontal = ("events",) + + +@admin.register(User) +class UserAdmin(admin.ModelAdmin): + list_display = ( + "username", + "email", + "first_name", + "last_name", + "is_active", + "is_superuser", + "is_email_verified", + ) + list_filter = ( + "is_active", + "is_staff", + "is_superuser", + "is_email_verified", + "gender", + ) + search_fields = ("username", "email", "first_name", "last_name") + readonly_fields = ("created_at", "updated_at") + filter_horizontal = ( + "groups", + "user_permissions", + ) + fieldsets = ( + (None, {"fields": ("username", "email", "password")}), + ( + "Personal info", + { + "fields": ( + "first_name", + "last_name", + "avatar", + "gender", + "organization", + "mobile_number", + ) + }, + ), + ( + "Permissions", + { + "fields": ( + "is_active", + "is_staff", + "is_superuser", + "groups", + "user_permissions", + "is_email_verified", + "is_password_expired", + "is_onboarded", + ), + }, + ), + ("Important dates", {"fields": ("created_at", "updated_at")}), + ) + ordering = ("-created_at",) diff --git a/backend/djangoindia/db/migrations/0007_sponsor_remove_event_event_end_date_and_more.py b/backend/djangoindia/db/migrations/0007_sponsor_remove_event_event_end_date_and_more.py new file mode 100644 index 00000000..1ec8f43b --- /dev/null +++ b/backend/djangoindia/db/migrations/0007_sponsor_remove_event_event_end_date_and_more.py @@ -0,0 +1,131 @@ +# Generated by Django 4.2.5 on 2024-09-19 13:50 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import djangoindia.db.models.event +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0006_contactus_created_at_contactus_updated_at_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Sponsor', + fields=[ + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=255)), + ('email', models.CharField(max_length=100)), + ('type', models.CharField(choices=[('individual', 'individual'), ('organization', 'organization')], max_length=20)), + ('logo', models.ImageField(upload_to='sponsors/logos/')), + ('url', models.URLField(blank=True, max_length=500, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.RemoveField( + model_name='event', + name='event_end_date', + ), + migrations.RemoveField( + model_name='event', + name='event_start_date', + ), + migrations.RemoveField( + model_name='eventregistration', + name='occupation', + ), + migrations.AddField( + model_name='event', + name='end_date', + field=models.DateTimeField(blank=True, null=True, validators=[djangoindia.db.models.event.validate_future_date]), + ), + migrations.AddField( + model_name='event', + name='start_date', + field=models.DateTimeField(blank=True, null=True, validators=[djangoindia.db.models.event.validate_future_date]), + ), + migrations.AddField( + model_name='eventregistration', + name='description', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='eventregistration', + name='organization', + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AddField( + model_name='eventregistration', + name='professional_status', + field=models.CharField(choices=[('working_professional', 'working_professional'), ('student', 'student'), ('freelancer', 'freelancer'), ('other', 'other')], default='other', max_length=100), + ), + migrations.AlterField( + model_name='event', + name='registration_end_date', + field=models.DateTimeField(blank=True, null=True, validators=[djangoindia.db.models.event.validate_future_date]), + ), + migrations.AlterField( + model_name='eventregistration', + name='gender', + field=models.CharField(choices=[('male', 'male'), ('female', 'female'), ('other', 'other')], default='Male', max_length=15), + preserve_default=False, + ), + migrations.AlterField( + model_name='eventregistration', + name='github', + field=models.URLField(blank=True, null=True), + ), + migrations.AlterField( + model_name='eventregistration', + name='other_links', + field=models.URLField(blank=True, null=True), + ), + migrations.AlterField( + model_name='eventregistration', + name='twitter', + field=models.URLField(blank=True, null=True), + ), + migrations.RenameModel( + old_name='NewsletterSubscription', + new_name='Subscriber', + ), + migrations.CreateModel( + name='Update', + fields=[ + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('title', models.CharField(max_length=255)), + ('type', models.CharField(choices=[('django_update', 'Django Update'), ('event_update', 'Event Update'), ('newsletter', 'Newsletter'), ('community_update', 'Community Update')], max_length=20)), + ('html_template', models.TextField()), + ('mail_sent', models.BooleanField(default=False)), + ('recipients', models.ManyToManyField(related_name='received_updates', to='db.subscriber')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Sponsorship', + fields=[ + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('tier', models.CharField(choices=[('platinum', 'platinum'), ('gold', 'gold'), ('silver', 'silver'), ('individual', 'individual'), ('organization', 'organization')], max_length=20)), + ('type', models.CharField(choices=[('community_sponsorship', 'community_sponsorship'), ('event_sponsorship', 'event_sponsorship')], max_length=30)), + ('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sponsors', to='db.event')), + ('sponsor_details', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sponsorships', to='db.sponsor')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/backend/djangoindia/db/migrations/0014_socialloginconnection_user_eventuserregistration.py b/backend/djangoindia/db/migrations/0014_socialloginconnection_user_eventuserregistration.py new file mode 100644 index 00000000..723538a8 --- /dev/null +++ b/backend/djangoindia/db/migrations/0014_socialloginconnection_user_eventuserregistration.py @@ -0,0 +1,847 @@ +# Generated by Django 4.2.5 on 2024-12-29 14:22 + +import django.contrib.auth.models +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ("db", "0013_alter_event_end_date_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="SocialLoginConnection", + fields=[ + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "medium", + models.CharField( + choices=[("Google", "google")], default=None, max_length=20 + ), + ), + ( + "last_login_at", + models.DateTimeField(default=django.utils.timezone.now, null=True), + ), + ( + "last_received_at", + models.DateTimeField(default=django.utils.timezone.now, null=True), + ), + ("token_data", models.JSONField(null=True)), + ("extra_data", models.JSONField(null=True)), + ], + options={ + "verbose_name": "Social Login Connection", + "verbose_name_plural": "Social Login Connections", + "db_table": "social_login_connections", + "ordering": ("-created_at",), + }, + ), + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ("username", models.CharField(max_length=128, unique=True)), + ( + "uuid", + models.UUIDField(default=uuid.uuid4, editable=False, unique=True), + ), + ( + "mobile_number", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "email", + models.CharField( + blank=True, max_length=255, null=True, unique=True + ), + ), + ("first_name", models.CharField(blank=True, max_length=255)), + ("last_name", models.CharField(blank=True, max_length=255)), + ("avatar", models.CharField(blank=True, max_length=255)), + ( + "organization", + models.CharField(blank=True, max_length=500, null=True), + ), + ( + "gender", + models.CharField( + choices=[ + ("male", "male"), + ("female", "female"), + ("non-binary", "non-binary"), + ("not_to_specify", "not_to_specify"), + ], + max_length=50, + ), + ), + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ("is_superuser", models.BooleanField(default=False)), + ("is_password_expired", models.BooleanField(default=False)), + ("is_active", models.BooleanField(default=True)), + ("is_staff", models.BooleanField(default=False)), + ("is_email_verified", models.BooleanField(default=False)), + ("is_password_autoset", models.BooleanField(default=False)), + ("is_onboarded", models.BooleanField(default=False)), + ( + "user_timezone", + models.CharField( + choices=[ + ("Africa/Abidjan", "Africa/Abidjan"), + ("Africa/Accra", "Africa/Accra"), + ("Africa/Addis_Ababa", "Africa/Addis_Ababa"), + ("Africa/Algiers", "Africa/Algiers"), + ("Africa/Asmara", "Africa/Asmara"), + ("Africa/Asmera", "Africa/Asmera"), + ("Africa/Bamako", "Africa/Bamako"), + ("Africa/Bangui", "Africa/Bangui"), + ("Africa/Banjul", "Africa/Banjul"), + ("Africa/Bissau", "Africa/Bissau"), + ("Africa/Blantyre", "Africa/Blantyre"), + ("Africa/Brazzaville", "Africa/Brazzaville"), + ("Africa/Bujumbura", "Africa/Bujumbura"), + ("Africa/Cairo", "Africa/Cairo"), + ("Africa/Casablanca", "Africa/Casablanca"), + ("Africa/Ceuta", "Africa/Ceuta"), + ("Africa/Conakry", "Africa/Conakry"), + ("Africa/Dakar", "Africa/Dakar"), + ("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"), + ("Africa/Djibouti", "Africa/Djibouti"), + ("Africa/Douala", "Africa/Douala"), + ("Africa/El_Aaiun", "Africa/El_Aaiun"), + ("Africa/Freetown", "Africa/Freetown"), + ("Africa/Gaborone", "Africa/Gaborone"), + ("Africa/Harare", "Africa/Harare"), + ("Africa/Johannesburg", "Africa/Johannesburg"), + ("Africa/Juba", "Africa/Juba"), + ("Africa/Kampala", "Africa/Kampala"), + ("Africa/Khartoum", "Africa/Khartoum"), + ("Africa/Kigali", "Africa/Kigali"), + ("Africa/Kinshasa", "Africa/Kinshasa"), + ("Africa/Lagos", "Africa/Lagos"), + ("Africa/Libreville", "Africa/Libreville"), + ("Africa/Lome", "Africa/Lome"), + ("Africa/Luanda", "Africa/Luanda"), + ("Africa/Lubumbashi", "Africa/Lubumbashi"), + ("Africa/Lusaka", "Africa/Lusaka"), + ("Africa/Malabo", "Africa/Malabo"), + ("Africa/Maputo", "Africa/Maputo"), + ("Africa/Maseru", "Africa/Maseru"), + ("Africa/Mbabane", "Africa/Mbabane"), + ("Africa/Mogadishu", "Africa/Mogadishu"), + ("Africa/Monrovia", "Africa/Monrovia"), + ("Africa/Nairobi", "Africa/Nairobi"), + ("Africa/Ndjamena", "Africa/Ndjamena"), + ("Africa/Niamey", "Africa/Niamey"), + ("Africa/Nouakchott", "Africa/Nouakchott"), + ("Africa/Ouagadougou", "Africa/Ouagadougou"), + ("Africa/Porto-Novo", "Africa/Porto-Novo"), + ("Africa/Sao_Tome", "Africa/Sao_Tome"), + ("Africa/Timbuktu", "Africa/Timbuktu"), + ("Africa/Tripoli", "Africa/Tripoli"), + ("Africa/Tunis", "Africa/Tunis"), + ("Africa/Windhoek", "Africa/Windhoek"), + ("America/Adak", "America/Adak"), + ("America/Anchorage", "America/Anchorage"), + ("America/Anguilla", "America/Anguilla"), + ("America/Antigua", "America/Antigua"), + ("America/Araguaina", "America/Araguaina"), + ( + "America/Argentina/Buenos_Aires", + "America/Argentina/Buenos_Aires", + ), + ( + "America/Argentina/Catamarca", + "America/Argentina/Catamarca", + ), + ( + "America/Argentina/ComodRivadavia", + "America/Argentina/ComodRivadavia", + ), + ("America/Argentina/Cordoba", "America/Argentina/Cordoba"), + ("America/Argentina/Jujuy", "America/Argentina/Jujuy"), + ( + "America/Argentina/La_Rioja", + "America/Argentina/La_Rioja", + ), + ("America/Argentina/Mendoza", "America/Argentina/Mendoza"), + ( + "America/Argentina/Rio_Gallegos", + "America/Argentina/Rio_Gallegos", + ), + ("America/Argentina/Salta", "America/Argentina/Salta"), + ( + "America/Argentina/San_Juan", + "America/Argentina/San_Juan", + ), + ( + "America/Argentina/San_Luis", + "America/Argentina/San_Luis", + ), + ("America/Argentina/Tucuman", "America/Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"), + ("America/Aruba", "America/Aruba"), + ("America/Asuncion", "America/Asuncion"), + ("America/Atikokan", "America/Atikokan"), + ("America/Atka", "America/Atka"), + ("America/Bahia", "America/Bahia"), + ("America/Bahia_Banderas", "America/Bahia_Banderas"), + ("America/Barbados", "America/Barbados"), + ("America/Belem", "America/Belem"), + ("America/Belize", "America/Belize"), + ("America/Blanc-Sablon", "America/Blanc-Sablon"), + ("America/Boa_Vista", "America/Boa_Vista"), + ("America/Bogota", "America/Bogota"), + ("America/Boise", "America/Boise"), + ("America/Buenos_Aires", "America/Buenos_Aires"), + ("America/Cambridge_Bay", "America/Cambridge_Bay"), + ("America/Campo_Grande", "America/Campo_Grande"), + ("America/Cancun", "America/Cancun"), + ("America/Caracas", "America/Caracas"), + ("America/Catamarca", "America/Catamarca"), + ("America/Cayenne", "America/Cayenne"), + ("America/Cayman", "America/Cayman"), + ("America/Chicago", "America/Chicago"), + ("America/Chihuahua", "America/Chihuahua"), + ("America/Ciudad_Juarez", "America/Ciudad_Juarez"), + ("America/Coral_Harbour", "America/Coral_Harbour"), + ("America/Cordoba", "America/Cordoba"), + ("America/Costa_Rica", "America/Costa_Rica"), + ("America/Creston", "America/Creston"), + ("America/Cuiaba", "America/Cuiaba"), + ("America/Curacao", "America/Curacao"), + ("America/Danmarkshavn", "America/Danmarkshavn"), + ("America/Dawson", "America/Dawson"), + ("America/Dawson_Creek", "America/Dawson_Creek"), + ("America/Denver", "America/Denver"), + ("America/Detroit", "America/Detroit"), + ("America/Dominica", "America/Dominica"), + ("America/Edmonton", "America/Edmonton"), + ("America/Eirunepe", "America/Eirunepe"), + ("America/El_Salvador", "America/El_Salvador"), + ("America/Ensenada", "America/Ensenada"), + ("America/Fort_Nelson", "America/Fort_Nelson"), + ("America/Fort_Wayne", "America/Fort_Wayne"), + ("America/Fortaleza", "America/Fortaleza"), + ("America/Glace_Bay", "America/Glace_Bay"), + ("America/Godthab", "America/Godthab"), + ("America/Goose_Bay", "America/Goose_Bay"), + ("America/Grand_Turk", "America/Grand_Turk"), + ("America/Grenada", "America/Grenada"), + ("America/Guadeloupe", "America/Guadeloupe"), + ("America/Guatemala", "America/Guatemala"), + ("America/Guayaquil", "America/Guayaquil"), + ("America/Guyana", "America/Guyana"), + ("America/Halifax", "America/Halifax"), + ("America/Havana", "America/Havana"), + ("America/Hermosillo", "America/Hermosillo"), + ( + "America/Indiana/Indianapolis", + "America/Indiana/Indianapolis", + ), + ("America/Indiana/Knox", "America/Indiana/Knox"), + ("America/Indiana/Marengo", "America/Indiana/Marengo"), + ( + "America/Indiana/Petersburg", + "America/Indiana/Petersburg", + ), + ("America/Indiana/Tell_City", "America/Indiana/Tell_City"), + ("America/Indiana/Vevay", "America/Indiana/Vevay"), + ("America/Indiana/Vincennes", "America/Indiana/Vincennes"), + ("America/Indiana/Winamac", "America/Indiana/Winamac"), + ("America/Indianapolis", "America/Indianapolis"), + ("America/Inuvik", "America/Inuvik"), + ("America/Iqaluit", "America/Iqaluit"), + ("America/Jamaica", "America/Jamaica"), + ("America/Jujuy", "America/Jujuy"), + ("America/Juneau", "America/Juneau"), + ( + "America/Kentucky/Louisville", + "America/Kentucky/Louisville", + ), + ( + "America/Kentucky/Monticello", + "America/Kentucky/Monticello", + ), + ("America/Knox_IN", "America/Knox_IN"), + ("America/Kralendijk", "America/Kralendijk"), + ("America/La_Paz", "America/La_Paz"), + ("America/Lima", "America/Lima"), + ("America/Los_Angeles", "America/Los_Angeles"), + ("America/Louisville", "America/Louisville"), + ("America/Lower_Princes", "America/Lower_Princes"), + ("America/Maceio", "America/Maceio"), + ("America/Managua", "America/Managua"), + ("America/Manaus", "America/Manaus"), + ("America/Marigot", "America/Marigot"), + ("America/Martinique", "America/Martinique"), + ("America/Matamoros", "America/Matamoros"), + ("America/Mazatlan", "America/Mazatlan"), + ("America/Mendoza", "America/Mendoza"), + ("America/Menominee", "America/Menominee"), + ("America/Merida", "America/Merida"), + ("America/Metlakatla", "America/Metlakatla"), + ("America/Mexico_City", "America/Mexico_City"), + ("America/Miquelon", "America/Miquelon"), + ("America/Moncton", "America/Moncton"), + ("America/Monterrey", "America/Monterrey"), + ("America/Montevideo", "America/Montevideo"), + ("America/Montreal", "America/Montreal"), + ("America/Montserrat", "America/Montserrat"), + ("America/Nassau", "America/Nassau"), + ("America/New_York", "America/New_York"), + ("America/Nipigon", "America/Nipigon"), + ("America/Nome", "America/Nome"), + ("America/Noronha", "America/Noronha"), + ( + "America/North_Dakota/Beulah", + "America/North_Dakota/Beulah", + ), + ( + "America/North_Dakota/Center", + "America/North_Dakota/Center", + ), + ( + "America/North_Dakota/New_Salem", + "America/North_Dakota/New_Salem", + ), + ("America/Nuuk", "America/Nuuk"), + ("America/Ojinaga", "America/Ojinaga"), + ("America/Panama", "America/Panama"), + ("America/Pangnirtung", "America/Pangnirtung"), + ("America/Paramaribo", "America/Paramaribo"), + ("America/Phoenix", "America/Phoenix"), + ("America/Port-au-Prince", "America/Port-au-Prince"), + ("America/Port_of_Spain", "America/Port_of_Spain"), + ("America/Porto_Acre", "America/Porto_Acre"), + ("America/Porto_Velho", "America/Porto_Velho"), + ("America/Puerto_Rico", "America/Puerto_Rico"), + ("America/Punta_Arenas", "America/Punta_Arenas"), + ("America/Rainy_River", "America/Rainy_River"), + ("America/Rankin_Inlet", "America/Rankin_Inlet"), + ("America/Recife", "America/Recife"), + ("America/Regina", "America/Regina"), + ("America/Resolute", "America/Resolute"), + ("America/Rio_Branco", "America/Rio_Branco"), + ("America/Rosario", "America/Rosario"), + ("America/Santa_Isabel", "America/Santa_Isabel"), + ("America/Santarem", "America/Santarem"), + ("America/Santiago", "America/Santiago"), + ("America/Santo_Domingo", "America/Santo_Domingo"), + ("America/Sao_Paulo", "America/Sao_Paulo"), + ("America/Scoresbysund", "America/Scoresbysund"), + ("America/Shiprock", "America/Shiprock"), + ("America/Sitka", "America/Sitka"), + ("America/St_Barthelemy", "America/St_Barthelemy"), + ("America/St_Johns", "America/St_Johns"), + ("America/St_Kitts", "America/St_Kitts"), + ("America/St_Lucia", "America/St_Lucia"), + ("America/St_Thomas", "America/St_Thomas"), + ("America/St_Vincent", "America/St_Vincent"), + ("America/Swift_Current", "America/Swift_Current"), + ("America/Tegucigalpa", "America/Tegucigalpa"), + ("America/Thule", "America/Thule"), + ("America/Thunder_Bay", "America/Thunder_Bay"), + ("America/Tijuana", "America/Tijuana"), + ("America/Toronto", "America/Toronto"), + ("America/Tortola", "America/Tortola"), + ("America/Vancouver", "America/Vancouver"), + ("America/Virgin", "America/Virgin"), + ("America/Whitehorse", "America/Whitehorse"), + ("America/Winnipeg", "America/Winnipeg"), + ("America/Yakutat", "America/Yakutat"), + ("America/Yellowknife", "America/Yellowknife"), + ("Antarctica/Casey", "Antarctica/Casey"), + ("Antarctica/Davis", "Antarctica/Davis"), + ("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"), + ("Antarctica/Macquarie", "Antarctica/Macquarie"), + ("Antarctica/Mawson", "Antarctica/Mawson"), + ("Antarctica/McMurdo", "Antarctica/McMurdo"), + ("Antarctica/Palmer", "Antarctica/Palmer"), + ("Antarctica/Rothera", "Antarctica/Rothera"), + ("Antarctica/South_Pole", "Antarctica/South_Pole"), + ("Antarctica/Syowa", "Antarctica/Syowa"), + ("Antarctica/Troll", "Antarctica/Troll"), + ("Antarctica/Vostok", "Antarctica/Vostok"), + ("Arctic/Longyearbyen", "Arctic/Longyearbyen"), + ("Asia/Aden", "Asia/Aden"), + ("Asia/Almaty", "Asia/Almaty"), + ("Asia/Amman", "Asia/Amman"), + ("Asia/Anadyr", "Asia/Anadyr"), + ("Asia/Aqtau", "Asia/Aqtau"), + ("Asia/Aqtobe", "Asia/Aqtobe"), + ("Asia/Ashgabat", "Asia/Ashgabat"), + ("Asia/Ashkhabad", "Asia/Ashkhabad"), + ("Asia/Atyrau", "Asia/Atyrau"), + ("Asia/Baghdad", "Asia/Baghdad"), + ("Asia/Bahrain", "Asia/Bahrain"), + ("Asia/Baku", "Asia/Baku"), + ("Asia/Bangkok", "Asia/Bangkok"), + ("Asia/Barnaul", "Asia/Barnaul"), + ("Asia/Beirut", "Asia/Beirut"), + ("Asia/Bishkek", "Asia/Bishkek"), + ("Asia/Brunei", "Asia/Brunei"), + ("Asia/Calcutta", "Asia/Calcutta"), + ("Asia/Chita", "Asia/Chita"), + ("Asia/Choibalsan", "Asia/Choibalsan"), + ("Asia/Chongqing", "Asia/Chongqing"), + ("Asia/Chungking", "Asia/Chungking"), + ("Asia/Colombo", "Asia/Colombo"), + ("Asia/Dacca", "Asia/Dacca"), + ("Asia/Damascus", "Asia/Damascus"), + ("Asia/Dhaka", "Asia/Dhaka"), + ("Asia/Dili", "Asia/Dili"), + ("Asia/Dubai", "Asia/Dubai"), + ("Asia/Dushanbe", "Asia/Dushanbe"), + ("Asia/Famagusta", "Asia/Famagusta"), + ("Asia/Gaza", "Asia/Gaza"), + ("Asia/Harbin", "Asia/Harbin"), + ("Asia/Hebron", "Asia/Hebron"), + ("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"), + ("Asia/Hong_Kong", "Asia/Hong_Kong"), + ("Asia/Hovd", "Asia/Hovd"), + ("Asia/Irkutsk", "Asia/Irkutsk"), + ("Asia/Istanbul", "Asia/Istanbul"), + ("Asia/Jakarta", "Asia/Jakarta"), + ("Asia/Jayapura", "Asia/Jayapura"), + ("Asia/Jerusalem", "Asia/Jerusalem"), + ("Asia/Kabul", "Asia/Kabul"), + ("Asia/Kamchatka", "Asia/Kamchatka"), + ("Asia/Karachi", "Asia/Karachi"), + ("Asia/Kashgar", "Asia/Kashgar"), + ("Asia/Kathmandu", "Asia/Kathmandu"), + ("Asia/Katmandu", "Asia/Katmandu"), + ("Asia/Khandyga", "Asia/Khandyga"), + ("Asia/Kolkata", "Asia/Kolkata"), + ("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"), + ("Asia/Kuching", "Asia/Kuching"), + ("Asia/Kuwait", "Asia/Kuwait"), + ("Asia/Macao", "Asia/Macao"), + ("Asia/Macau", "Asia/Macau"), + ("Asia/Magadan", "Asia/Magadan"), + ("Asia/Makassar", "Asia/Makassar"), + ("Asia/Manila", "Asia/Manila"), + ("Asia/Muscat", "Asia/Muscat"), + ("Asia/Nicosia", "Asia/Nicosia"), + ("Asia/Novokuznetsk", "Asia/Novokuznetsk"), + ("Asia/Novosibirsk", "Asia/Novosibirsk"), + ("Asia/Omsk", "Asia/Omsk"), + ("Asia/Oral", "Asia/Oral"), + ("Asia/Phnom_Penh", "Asia/Phnom_Penh"), + ("Asia/Pontianak", "Asia/Pontianak"), + ("Asia/Pyongyang", "Asia/Pyongyang"), + ("Asia/Qatar", "Asia/Qatar"), + ("Asia/Qostanay", "Asia/Qostanay"), + ("Asia/Qyzylorda", "Asia/Qyzylorda"), + ("Asia/Rangoon", "Asia/Rangoon"), + ("Asia/Riyadh", "Asia/Riyadh"), + ("Asia/Saigon", "Asia/Saigon"), + ("Asia/Sakhalin", "Asia/Sakhalin"), + ("Asia/Samarkand", "Asia/Samarkand"), + ("Asia/Seoul", "Asia/Seoul"), + ("Asia/Shanghai", "Asia/Shanghai"), + ("Asia/Singapore", "Asia/Singapore"), + ("Asia/Srednekolymsk", "Asia/Srednekolymsk"), + ("Asia/Taipei", "Asia/Taipei"), + ("Asia/Tashkent", "Asia/Tashkent"), + ("Asia/Tbilisi", "Asia/Tbilisi"), + ("Asia/Tehran", "Asia/Tehran"), + ("Asia/Tel_Aviv", "Asia/Tel_Aviv"), + ("Asia/Thimbu", "Asia/Thimbu"), + ("Asia/Thimphu", "Asia/Thimphu"), + ("Asia/Tokyo", "Asia/Tokyo"), + ("Asia/Tomsk", "Asia/Tomsk"), + ("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"), + ("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"), + ("Asia/Ulan_Bator", "Asia/Ulan_Bator"), + ("Asia/Urumqi", "Asia/Urumqi"), + ("Asia/Ust-Nera", "Asia/Ust-Nera"), + ("Asia/Vientiane", "Asia/Vientiane"), + ("Asia/Vladivostok", "Asia/Vladivostok"), + ("Asia/Yakutsk", "Asia/Yakutsk"), + ("Asia/Yangon", "Asia/Yangon"), + ("Asia/Yekaterinburg", "Asia/Yekaterinburg"), + ("Asia/Yerevan", "Asia/Yerevan"), + ("Atlantic/Azores", "Atlantic/Azores"), + ("Atlantic/Bermuda", "Atlantic/Bermuda"), + ("Atlantic/Canary", "Atlantic/Canary"), + ("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"), + ("Atlantic/Faeroe", "Atlantic/Faeroe"), + ("Atlantic/Faroe", "Atlantic/Faroe"), + ("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"), + ("Atlantic/Madeira", "Atlantic/Madeira"), + ("Atlantic/Reykjavik", "Atlantic/Reykjavik"), + ("Atlantic/South_Georgia", "Atlantic/South_Georgia"), + ("Atlantic/St_Helena", "Atlantic/St_Helena"), + ("Atlantic/Stanley", "Atlantic/Stanley"), + ("Australia/ACT", "Australia/ACT"), + ("Australia/Adelaide", "Australia/Adelaide"), + ("Australia/Brisbane", "Australia/Brisbane"), + ("Australia/Broken_Hill", "Australia/Broken_Hill"), + ("Australia/Canberra", "Australia/Canberra"), + ("Australia/Currie", "Australia/Currie"), + ("Australia/Darwin", "Australia/Darwin"), + ("Australia/Eucla", "Australia/Eucla"), + ("Australia/Hobart", "Australia/Hobart"), + ("Australia/LHI", "Australia/LHI"), + ("Australia/Lindeman", "Australia/Lindeman"), + ("Australia/Lord_Howe", "Australia/Lord_Howe"), + ("Australia/Melbourne", "Australia/Melbourne"), + ("Australia/NSW", "Australia/NSW"), + ("Australia/North", "Australia/North"), + ("Australia/Perth", "Australia/Perth"), + ("Australia/Queensland", "Australia/Queensland"), + ("Australia/South", "Australia/South"), + ("Australia/Sydney", "Australia/Sydney"), + ("Australia/Tasmania", "Australia/Tasmania"), + ("Australia/Victoria", "Australia/Victoria"), + ("Australia/West", "Australia/West"), + ("Australia/Yancowinna", "Australia/Yancowinna"), + ("Brazil/Acre", "Brazil/Acre"), + ("Brazil/DeNoronha", "Brazil/DeNoronha"), + ("Brazil/East", "Brazil/East"), + ("Brazil/West", "Brazil/West"), + ("CET", "CET"), + ("CST6CDT", "CST6CDT"), + ("Canada/Atlantic", "Canada/Atlantic"), + ("Canada/Central", "Canada/Central"), + ("Canada/Eastern", "Canada/Eastern"), + ("Canada/Mountain", "Canada/Mountain"), + ("Canada/Newfoundland", "Canada/Newfoundland"), + ("Canada/Pacific", "Canada/Pacific"), + ("Canada/Saskatchewan", "Canada/Saskatchewan"), + ("Canada/Yukon", "Canada/Yukon"), + ("Chile/Continental", "Chile/Continental"), + ("Chile/EasterIsland", "Chile/EasterIsland"), + ("Cuba", "Cuba"), + ("EET", "EET"), + ("EST", "EST"), + ("EST5EDT", "EST5EDT"), + ("Egypt", "Egypt"), + ("Eire", "Eire"), + ("Etc/GMT", "Etc/GMT"), + ("Etc/GMT+0", "Etc/GMT+0"), + ("Etc/GMT+1", "Etc/GMT+1"), + ("Etc/GMT+10", "Etc/GMT+10"), + ("Etc/GMT+11", "Etc/GMT+11"), + ("Etc/GMT+12", "Etc/GMT+12"), + ("Etc/GMT+2", "Etc/GMT+2"), + ("Etc/GMT+3", "Etc/GMT+3"), + ("Etc/GMT+4", "Etc/GMT+4"), + ("Etc/GMT+5", "Etc/GMT+5"), + ("Etc/GMT+6", "Etc/GMT+6"), + ("Etc/GMT+7", "Etc/GMT+7"), + ("Etc/GMT+8", "Etc/GMT+8"), + ("Etc/GMT+9", "Etc/GMT+9"), + ("Etc/GMT-0", "Etc/GMT-0"), + ("Etc/GMT-1", "Etc/GMT-1"), + ("Etc/GMT-10", "Etc/GMT-10"), + ("Etc/GMT-11", "Etc/GMT-11"), + ("Etc/GMT-12", "Etc/GMT-12"), + ("Etc/GMT-13", "Etc/GMT-13"), + ("Etc/GMT-14", "Etc/GMT-14"), + ("Etc/GMT-2", "Etc/GMT-2"), + ("Etc/GMT-3", "Etc/GMT-3"), + ("Etc/GMT-4", "Etc/GMT-4"), + ("Etc/GMT-5", "Etc/GMT-5"), + ("Etc/GMT-6", "Etc/GMT-6"), + ("Etc/GMT-7", "Etc/GMT-7"), + ("Etc/GMT-8", "Etc/GMT-8"), + ("Etc/GMT-9", "Etc/GMT-9"), + ("Etc/GMT0", "Etc/GMT0"), + ("Etc/Greenwich", "Etc/Greenwich"), + ("Etc/UCT", "Etc/UCT"), + ("Etc/UTC", "Etc/UTC"), + ("Etc/Universal", "Etc/Universal"), + ("Etc/Zulu", "Etc/Zulu"), + ("Europe/Amsterdam", "Europe/Amsterdam"), + ("Europe/Andorra", "Europe/Andorra"), + ("Europe/Astrakhan", "Europe/Astrakhan"), + ("Europe/Athens", "Europe/Athens"), + ("Europe/Belfast", "Europe/Belfast"), + ("Europe/Belgrade", "Europe/Belgrade"), + ("Europe/Berlin", "Europe/Berlin"), + ("Europe/Bratislava", "Europe/Bratislava"), + ("Europe/Brussels", "Europe/Brussels"), + ("Europe/Bucharest", "Europe/Bucharest"), + ("Europe/Budapest", "Europe/Budapest"), + ("Europe/Busingen", "Europe/Busingen"), + ("Europe/Chisinau", "Europe/Chisinau"), + ("Europe/Copenhagen", "Europe/Copenhagen"), + ("Europe/Dublin", "Europe/Dublin"), + ("Europe/Gibraltar", "Europe/Gibraltar"), + ("Europe/Guernsey", "Europe/Guernsey"), + ("Europe/Helsinki", "Europe/Helsinki"), + ("Europe/Isle_of_Man", "Europe/Isle_of_Man"), + ("Europe/Istanbul", "Europe/Istanbul"), + ("Europe/Jersey", "Europe/Jersey"), + ("Europe/Kaliningrad", "Europe/Kaliningrad"), + ("Europe/Kiev", "Europe/Kiev"), + ("Europe/Kirov", "Europe/Kirov"), + ("Europe/Kyiv", "Europe/Kyiv"), + ("Europe/Lisbon", "Europe/Lisbon"), + ("Europe/Ljubljana", "Europe/Ljubljana"), + ("Europe/London", "Europe/London"), + ("Europe/Luxembourg", "Europe/Luxembourg"), + ("Europe/Madrid", "Europe/Madrid"), + ("Europe/Malta", "Europe/Malta"), + ("Europe/Mariehamn", "Europe/Mariehamn"), + ("Europe/Minsk", "Europe/Minsk"), + ("Europe/Monaco", "Europe/Monaco"), + ("Europe/Moscow", "Europe/Moscow"), + ("Europe/Nicosia", "Europe/Nicosia"), + ("Europe/Oslo", "Europe/Oslo"), + ("Europe/Paris", "Europe/Paris"), + ("Europe/Podgorica", "Europe/Podgorica"), + ("Europe/Prague", "Europe/Prague"), + ("Europe/Riga", "Europe/Riga"), + ("Europe/Rome", "Europe/Rome"), + ("Europe/Samara", "Europe/Samara"), + ("Europe/San_Marino", "Europe/San_Marino"), + ("Europe/Sarajevo", "Europe/Sarajevo"), + ("Europe/Saratov", "Europe/Saratov"), + ("Europe/Simferopol", "Europe/Simferopol"), + ("Europe/Skopje", "Europe/Skopje"), + ("Europe/Sofia", "Europe/Sofia"), + ("Europe/Stockholm", "Europe/Stockholm"), + ("Europe/Tallinn", "Europe/Tallinn"), + ("Europe/Tirane", "Europe/Tirane"), + ("Europe/Tiraspol", "Europe/Tiraspol"), + ("Europe/Ulyanovsk", "Europe/Ulyanovsk"), + ("Europe/Uzhgorod", "Europe/Uzhgorod"), + ("Europe/Vaduz", "Europe/Vaduz"), + ("Europe/Vatican", "Europe/Vatican"), + ("Europe/Vienna", "Europe/Vienna"), + ("Europe/Vilnius", "Europe/Vilnius"), + ("Europe/Volgograd", "Europe/Volgograd"), + ("Europe/Warsaw", "Europe/Warsaw"), + ("Europe/Zagreb", "Europe/Zagreb"), + ("Europe/Zaporozhye", "Europe/Zaporozhye"), + ("Europe/Zurich", "Europe/Zurich"), + ("GB", "GB"), + ("GB-Eire", "GB-Eire"), + ("GMT", "GMT"), + ("GMT+0", "GMT+0"), + ("GMT-0", "GMT-0"), + ("GMT0", "GMT0"), + ("Greenwich", "Greenwich"), + ("HST", "HST"), + ("Hongkong", "Hongkong"), + ("Iceland", "Iceland"), + ("Indian/Antananarivo", "Indian/Antananarivo"), + ("Indian/Chagos", "Indian/Chagos"), + ("Indian/Christmas", "Indian/Christmas"), + ("Indian/Cocos", "Indian/Cocos"), + ("Indian/Comoro", "Indian/Comoro"), + ("Indian/Kerguelen", "Indian/Kerguelen"), + ("Indian/Mahe", "Indian/Mahe"), + ("Indian/Maldives", "Indian/Maldives"), + ("Indian/Mauritius", "Indian/Mauritius"), + ("Indian/Mayotte", "Indian/Mayotte"), + ("Indian/Reunion", "Indian/Reunion"), + ("Iran", "Iran"), + ("Israel", "Israel"), + ("Jamaica", "Jamaica"), + ("Japan", "Japan"), + ("Kwajalein", "Kwajalein"), + ("Libya", "Libya"), + ("MET", "MET"), + ("MST", "MST"), + ("MST7MDT", "MST7MDT"), + ("Mexico/BajaNorte", "Mexico/BajaNorte"), + ("Mexico/BajaSur", "Mexico/BajaSur"), + ("Mexico/General", "Mexico/General"), + ("NZ", "NZ"), + ("NZ-CHAT", "NZ-CHAT"), + ("Navajo", "Navajo"), + ("PRC", "PRC"), + ("PST8PDT", "PST8PDT"), + ("Pacific/Apia", "Pacific/Apia"), + ("Pacific/Auckland", "Pacific/Auckland"), + ("Pacific/Bougainville", "Pacific/Bougainville"), + ("Pacific/Chatham", "Pacific/Chatham"), + ("Pacific/Chuuk", "Pacific/Chuuk"), + ("Pacific/Easter", "Pacific/Easter"), + ("Pacific/Efate", "Pacific/Efate"), + ("Pacific/Enderbury", "Pacific/Enderbury"), + ("Pacific/Fakaofo", "Pacific/Fakaofo"), + ("Pacific/Fiji", "Pacific/Fiji"), + ("Pacific/Funafuti", "Pacific/Funafuti"), + ("Pacific/Galapagos", "Pacific/Galapagos"), + ("Pacific/Gambier", "Pacific/Gambier"), + ("Pacific/Guadalcanal", "Pacific/Guadalcanal"), + ("Pacific/Guam", "Pacific/Guam"), + ("Pacific/Honolulu", "Pacific/Honolulu"), + ("Pacific/Johnston", "Pacific/Johnston"), + ("Pacific/Kanton", "Pacific/Kanton"), + ("Pacific/Kiritimati", "Pacific/Kiritimati"), + ("Pacific/Kosrae", "Pacific/Kosrae"), + ("Pacific/Kwajalein", "Pacific/Kwajalein"), + ("Pacific/Majuro", "Pacific/Majuro"), + ("Pacific/Marquesas", "Pacific/Marquesas"), + ("Pacific/Midway", "Pacific/Midway"), + ("Pacific/Nauru", "Pacific/Nauru"), + ("Pacific/Niue", "Pacific/Niue"), + ("Pacific/Norfolk", "Pacific/Norfolk"), + ("Pacific/Noumea", "Pacific/Noumea"), + ("Pacific/Pago_Pago", "Pacific/Pago_Pago"), + ("Pacific/Palau", "Pacific/Palau"), + ("Pacific/Pitcairn", "Pacific/Pitcairn"), + ("Pacific/Pohnpei", "Pacific/Pohnpei"), + ("Pacific/Ponape", "Pacific/Ponape"), + ("Pacific/Port_Moresby", "Pacific/Port_Moresby"), + ("Pacific/Rarotonga", "Pacific/Rarotonga"), + ("Pacific/Saipan", "Pacific/Saipan"), + ("Pacific/Samoa", "Pacific/Samoa"), + ("Pacific/Tahiti", "Pacific/Tahiti"), + ("Pacific/Tarawa", "Pacific/Tarawa"), + ("Pacific/Tongatapu", "Pacific/Tongatapu"), + ("Pacific/Truk", "Pacific/Truk"), + ("Pacific/Wake", "Pacific/Wake"), + ("Pacific/Wallis", "Pacific/Wallis"), + ("Pacific/Yap", "Pacific/Yap"), + ("Poland", "Poland"), + ("Portugal", "Portugal"), + ("ROC", "ROC"), + ("ROK", "ROK"), + ("Singapore", "Singapore"), + ("Turkey", "Turkey"), + ("UCT", "UCT"), + ("US/Alaska", "US/Alaska"), + ("US/Aleutian", "US/Aleutian"), + ("US/Arizona", "US/Arizona"), + ("US/Central", "US/Central"), + ("US/East-Indiana", "US/East-Indiana"), + ("US/Eastern", "US/Eastern"), + ("US/Hawaii", "US/Hawaii"), + ("US/Indiana-Starke", "US/Indiana-Starke"), + ("US/Michigan", "US/Michigan"), + ("US/Mountain", "US/Mountain"), + ("US/Pacific", "US/Pacific"), + ("US/Samoa", "US/Samoa"), + ("UTC", "UTC"), + ("Universal", "Universal"), + ("W-SU", "W-SU"), + ("WET", "WET"), + ("Zulu", "Zulu"), + ], + default="Asia/Kolkata", + max_length=255, + ), + ), + ( + "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_account_set", + related_query_name="user_account", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_account_set", + related_query_name="user_account", + to="auth.permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "verbose_name": "User", + "verbose_name_plural": "Users", + "db_table": "users", + "ordering": ("-created_at",), + }, + managers=[ + ("objects", django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name="EventUserRegistration", + fields=[ + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "status", + models.CharField( + choices=[ + ("going", "going"), + ("waiting", "waiting"), + ("not_going", "not_going"), + ], + max_length=50, + ), + ), + ("first_time_attendee", models.BooleanField(default=True)), + ( + "event", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="db.event" + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/backend/djangoindia/db/models/__init__.py b/backend/djangoindia/db/models/__init__.py index beb172b5..16870a39 100644 --- a/backend/djangoindia/db/models/__init__.py +++ b/backend/djangoindia/db/models/__init__.py @@ -1,8 +1,9 @@ -from djangoindia.db.models.communication import ContactUs, Subscriber -from djangoindia.db.models.event import Event, EventRegistration -from djangoindia.db.models.partner_and_sponsor import CommunityPartner, Sponsorship -from djangoindia.db.models.update import Update -from djangoindia.db.models.volunteer import Volunteer +from .communication import ContactUs, Subscriber +from .event import Event, EventRegistration +from .partner_and_sponsor import CommunityPartner, Sponsorship +from .update import Update +from .user import SocialLoginConnection, User +from .volunteer import Volunteer from .user import User diff --git a/backend/djangoindia/db/models/event.py b/backend/djangoindia/db/models/event.py index 489944d4..e36e048d 100644 --- a/backend/djangoindia/db/models/event.py +++ b/backend/djangoindia/db/models/event.py @@ -1,5 +1,6 @@ from cabinet.models import Folder +from django.conf import settings from django.core.exceptions import ValidationError from django.db import models from django.utils import timezone @@ -45,6 +46,7 @@ def save(self, *args, **kwargs): super().save(*args, **kwargs) +# Deprecated model class EventRegistration(BaseModel): class ProfessionalStatus(models.TextChoices): WORKING_PROFESSIONAL = "working_professional" @@ -117,3 +119,32 @@ def __str__(self) -> str: return ( f"{self.first_name} {self.last_name} ({self.email}) --- {self.event.name}" ) + + +class EventUserRegistration(BaseModel): + class RegistrationStatusType: + CHOICES = ( + ("going", "going"), + ("waiting", "waiting"), + ("not_going", "not_going"), + ) + + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + event = models.ForeignKey(Event, on_delete=models.CASCADE) + status = models.CharField(choices=RegistrationStatusType.CHOICES, max_length=50) + first_time_attendee = models.BooleanField(default=True) + + def save(self, *args, **kwargs): + # This is a new registration + if self._state.adding: + user_has_registered_before = EventUserRegistration.objects.filter( + user=self.user + ).exists() + self.first_time_attendee = not user_has_registered_before + + if self.event.seats_left > 0: + self.event.seats_left -= 1 + self.event.save() + else: + raise ValueError("No seats left for this event.") + super().save(*args, **kwargs) diff --git a/backend/djangoindia/db/models/user.py b/backend/djangoindia/db/models/user.py index 83afa542..6465d752 100644 --- a/backend/djangoindia/db/models/user.py +++ b/backend/djangoindia/db/models/user.py @@ -1,4 +1,4 @@ -<<<<<<< HEAD + # Python imports import uuid @@ -13,20 +13,15 @@ ) # Django imports -from django.db import models -from django.utils.translation import gettext_lazy as _ - - -class User(AbstractBaseUser, PermissionsMixin): -======= from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin from django.db import models +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ from .base import BaseModel -class User(PermissionsMixin, AbstractBaseUser): ->>>>>>> 0e67722 (WIP) +class User(AbstractBaseUser, PermissionsMixin): class GENDER: CHOICES = ( ("male", "male"), @@ -35,7 +30,6 @@ class GENDER: ("not_to_specify", "not_to_specify"), ) -<<<<<<< HEAD USER_TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones)) username = models.CharField(max_length=128, unique=True) @@ -109,63 +103,92 @@ def save(self, *args, **kwargs): self.is_staff = True super().save(*args, **kwargs) -======= - first_name = models.CharField(max_length=255, blank=False, null=False) - last_name = models.CharField(max_length=255, blank=False, null=False) - unique_username = models.CharField(max_length=50, null=True, blank=True) - gender = models.CharField(choices=GENDER.CHOICES, max_length=50) - email = models.EmailField(max_length=255, unique=True, blank=False, null=False) - phone_number = models.CharField(max_length=50, blank=True, null=True) - date_of_birth = models.DateTimeField(blank=True, null=True) organization = models.CharField(max_length=500, blank=True, null=True) - year_of_experience = models.IntegerField(default=None, blank=True, null=True) - college = models.CharField(max_length=500, blank=True, null=True) - year_of_passing = models.DateField(blank=True, null=True) - is_active = models.BooleanField(default=True) - is_staff = models.BooleanField(default=False, verbose_name="community volunteer") + gender = models.CharField(choices=GENDER.CHOICES, max_length=50) + + # tracking metrics + created_at = models.DateTimeField(auto_now_add=True, verbose_name="Created At") + updated_at = models.DateTimeField(auto_now=True, verbose_name="Last Modified At") + + # the is' es is_superuser = models.BooleanField(default=False) - is_professional = models.BooleanField(default=False) + is_password_expired = models.BooleanField(default=False) + is_active = models.BooleanField(default=True) + is_staff = models.BooleanField(default=False) + is_email_verified = models.BooleanField(default=False) + is_password_autoset = models.BooleanField(default=False) + is_onboarded = models.BooleanField(default=False) + user_timezone = models.CharField( + max_length=255, default="Asia/Kolkata", choices=USER_TIMEZONE_CHOICES + ) -class UserSocials: - class SoialMediaType: - CHOICES = ( - ("linkedin", "linkedin"), - ("github", "github"), - ("twitter", "twitter"), - ) + groups = models.ManyToManyField( + Group, + verbose_name=_("groups"), + 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_account_set", + related_query_name="user_account", + ) + user_permissions = models.ManyToManyField( + Permission, + verbose_name=_("user permissions"), + blank=True, + help_text=_("Specific permissions for this user."), + related_name="user_account_set", + related_query_name="user_account", + ) + + USERNAME_FIELD = "email" + + REQUIRED_FIELDS = ["username"] + + objects = UserManager() + + class Meta: + verbose_name = "User" + verbose_name_plural = "Users" + db_table = "users" + ordering = ("-created_at",) - user = models.ForeignKey(User, null=False, blank=False) - social_media_type = models.CharField(choices=SoialMediaType.CHOICES, max_length=50) - profile_link = models.CharField(max_length=500, blank=True, null=True) + def __str__(self): + return f"{self.username} <{self.email}>" + def save(self, *args, **kwargs): + self.email = self.email.lower().strip() -class LogHistory(BaseModel): - user = models.ForeignKey(User, null=False, blank=False) - login_timestamp = models.DateTimeField(auto_now=True) + if self.is_superuser: + self.is_staff = True + super().save(*args, **kwargs) -class UserEventHistory(BaseModel): - class RegistrationStatusType: - CHOICES = ( - ("interested", "interested"), - ("rsvped", "rsvped"), - ("confirmed", "confirmed"), - ("shortlisted", "shortlisted")("waiting", "waiting")( - "cancelled", "cancelled" - ), - ) - user = models.ForeignKey("User", null=False, blank=False) - event = models.ForeignKey("Event", null=False, blank=False) - registration_time = models.DateTimeField(auto_now=True) - registration_status = models.CharField( - choices=RegistrationStatusType.CHOICES, max_length=50 +class SocialLoginConnection(BaseModel): + medium = models.CharField( + max_length=20, + choices=(("Google", "google"),), + default=None, ) + last_login_at = models.DateTimeField(default=timezone.now, null=True) + last_received_at = models.DateTimeField(default=timezone.now, null=True) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="user_login_connections", + ) + token_data = models.JSONField(null=True) + extra_data = models.JSONField(null=True) + class Meta: + verbose_name = "Social Login Connection" + verbose_name_plural = "Social Login Connections" + db_table = "social_login_connections" + ordering = ("-created_at",) -# register for event -# rsvp for event -> going/ not going -# show events on my profile -# django stories ->>>>>>> 0e67722 (WIP) + def __str__(self): + """Return name of the user and medium""" + return f"{self.medium} >" diff --git a/backend/djangoindia/settings/base.py b/backend/djangoindia/settings/base.py index 8701ae5e..f110abe3 100644 --- a/backend/djangoindia/settings/base.py +++ b/backend/djangoindia/settings/base.py @@ -12,6 +12,7 @@ import os +from datetime import timedelta from pathlib import Path from dotenv import load_dotenv @@ -69,6 +70,7 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + "crum.CurrentRequestUserMiddleware", ] ROOT_URLCONF = "djangoindia.urls" @@ -168,6 +170,15 @@ CELERY_RESULT_BACKEND = os.environ.get("CELERY_RESULT_BACKEND") REST_FRAMEWORK = { + "DEFAULT_RENDERER_CLASSES": [ + "rest_framework.renderers.JSONRenderer", + "rest_framework.renderers.BrowsableAPIRenderer", + ], + "DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",), + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework_simplejwt.authentication.JWTAuthentication", + ), + "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", } @@ -183,3 +194,33 @@ FILE_UPLOAD_MAX_MEMORY_SIZE = 104857600 # 100 mb DATA_UPLOAD_MAX_MEMORY_SIZE = 104857600 # 100 mb + +# JWT Auth Configuration +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(days=7), + "REFRESH_TOKEN_LIFETIME": timedelta(days=43200), + "ROTATE_REFRESH_TOKENS": False, + "BLACKLIST_AFTER_ROTATION": False, + "UPDATE_LAST_LOGIN": False, + "ALGORITHM": "HS256", + "SIGNING_KEY": SECRET_KEY, + "VERIFYING_KEY": None, + "AUDIENCE": None, + "ISSUER": None, + "JWK_URL": None, + "LEEWAY": 0, + "AUTH_HEADER_TYPES": ("Bearer",), + "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION", + "USER_ID_FIELD": "id", + "USER_ID_CLAIM": "user_id", + "USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule", + "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), + "TOKEN_TYPE_CLAIM": "token_type", + "TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser", + "JTI_CLAIM": "jti", + "SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp", + "SLIDING_TOKEN_LIFETIME": timedelta(minutes=5), + "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1), +} + +WEB_URL = os.environ.get("NEXT_PUBLIC_FRONTEND_URL", "http://localhost:3000") diff --git a/backend/requirements/base.txt b/backend/requirements/base.txt index 6f8aa84d..fd983e4c 100644 --- a/backend/requirements/base.txt +++ b/backend/requirements/base.txt @@ -3,12 +3,17 @@ celery==5.3.4 Django==4.2.5 django-cabinet==0.17.0 django-cors-headers==4.3.1 # https://github.com/adamchainz/django-cors-headers +django-crum==0.7.9 +django-filter==23.5 django-import-export==4.1.1 django-import-export[all] django-prose-editor==0.9.0 -django-prose-editor[sanitize] +django-prose-editor[sanitize] djangorestframework==3.14.0 # https://github.com/encode/django-rest-framework +djangorestframework-simplejwt==5.3.0 drf-spectacular +google-api-python-client==2.97.0 +google-auth==2.22.0 Pillow==10.4.0 psycopg2-binary==2.9.9 python-dotenv==1.0.1 diff --git a/backend/templates/auth/email_verification.html b/backend/templates/auth/email_verification.html new file mode 100644 index 00000000..3d65ba04 --- /dev/null +++ b/backend/templates/auth/email_verification.html @@ -0,0 +1,97 @@ + + + + + Email Verification - Django India + + + + +
+ OD +

Email Verification

+ {% if first_name %} +

Hi {{first_name}}!

+ {% else %} +

Hi there!

+ {% endif %} +

Thank you for joining us at Django India. We're excited to have you on board! Before you get started, please + verify your email address by clicking the button below:

+ Verify Email +

If you did not sign up for an account at Django India, please disregard this email.

+

Best regards,
The Django India Team

+
+ + + diff --git a/backend/templates/auth/forgot_password.html b/backend/templates/auth/forgot_password.html new file mode 100644 index 00000000..3893e06c --- /dev/null +++ b/backend/templates/auth/forgot_password.html @@ -0,0 +1,103 @@ + + + + + Password Reset - Django India + + + + +
+ OD +

Password Reset

+ {% if first_name %} +

Hi {{first_name}}!

+ {% else %} +

Hi there!

+ {% endif %} +

We have received a request to reset your password for your account at Django India. Don't worry, we've got you + covered! Just click the button below to reset your password and get back to using our awesome services:

+ Reset Password +
+

Stay Secure

+

Your security is our top priority. We recommend choosing a strong, unique password and keeping it confidential. + Avoid using common words or easily guessable information to protect your account from unauthorized access.

+
+

If you did not request a password reset, please disregard this email.

+

Thank you for being a valued member of Django India Community. We appreciate your continued support!

+

Best regards,
The Django India Team

+
+ + + From 343d7dcd5864f2c1029aa9e75d8f0ddb1de81755 Mon Sep 17 00:00:00 2001 From: DevilsAutumn Date: Fri, 10 Jan 2025 01:49:28 +0530 Subject: [PATCH 3/4] removed extra migrations --- ...or_remove_event_event_end_date_and_more.py | 131 --- ...inconnection_user_eventuserregistration.py | 847 ------------------ 2 files changed, 978 deletions(-) delete mode 100644 backend/djangoindia/db/migrations/0007_sponsor_remove_event_event_end_date_and_more.py delete mode 100644 backend/djangoindia/db/migrations/0014_socialloginconnection_user_eventuserregistration.py diff --git a/backend/djangoindia/db/migrations/0007_sponsor_remove_event_event_end_date_and_more.py b/backend/djangoindia/db/migrations/0007_sponsor_remove_event_event_end_date_and_more.py deleted file mode 100644 index 1ec8f43b..00000000 --- a/backend/djangoindia/db/migrations/0007_sponsor_remove_event_event_end_date_and_more.py +++ /dev/null @@ -1,131 +0,0 @@ -# Generated by Django 4.2.5 on 2024-09-19 13:50 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import djangoindia.db.models.event -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ('db', '0006_contactus_created_at_contactus_updated_at_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='Sponsor', - fields=[ - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('name', models.CharField(max_length=255)), - ('email', models.CharField(max_length=100)), - ('type', models.CharField(choices=[('individual', 'individual'), ('organization', 'organization')], max_length=20)), - ('logo', models.ImageField(upload_to='sponsors/logos/')), - ('url', models.URLField(blank=True, max_length=500, null=True)), - ], - options={ - 'abstract': False, - }, - ), - migrations.RemoveField( - model_name='event', - name='event_end_date', - ), - migrations.RemoveField( - model_name='event', - name='event_start_date', - ), - migrations.RemoveField( - model_name='eventregistration', - name='occupation', - ), - migrations.AddField( - model_name='event', - name='end_date', - field=models.DateTimeField(blank=True, null=True, validators=[djangoindia.db.models.event.validate_future_date]), - ), - migrations.AddField( - model_name='event', - name='start_date', - field=models.DateTimeField(blank=True, null=True, validators=[djangoindia.db.models.event.validate_future_date]), - ), - migrations.AddField( - model_name='eventregistration', - name='description', - field=models.TextField(blank=True, null=True), - ), - migrations.AddField( - model_name='eventregistration', - name='organization', - field=models.CharField(blank=True, max_length=100, null=True), - ), - migrations.AddField( - model_name='eventregistration', - name='professional_status', - field=models.CharField(choices=[('working_professional', 'working_professional'), ('student', 'student'), ('freelancer', 'freelancer'), ('other', 'other')], default='other', max_length=100), - ), - migrations.AlterField( - model_name='event', - name='registration_end_date', - field=models.DateTimeField(blank=True, null=True, validators=[djangoindia.db.models.event.validate_future_date]), - ), - migrations.AlterField( - model_name='eventregistration', - name='gender', - field=models.CharField(choices=[('male', 'male'), ('female', 'female'), ('other', 'other')], default='Male', max_length=15), - preserve_default=False, - ), - migrations.AlterField( - model_name='eventregistration', - name='github', - field=models.URLField(blank=True, null=True), - ), - migrations.AlterField( - model_name='eventregistration', - name='other_links', - field=models.URLField(blank=True, null=True), - ), - migrations.AlterField( - model_name='eventregistration', - name='twitter', - field=models.URLField(blank=True, null=True), - ), - migrations.RenameModel( - old_name='NewsletterSubscription', - new_name='Subscriber', - ), - migrations.CreateModel( - name='Update', - fields=[ - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('title', models.CharField(max_length=255)), - ('type', models.CharField(choices=[('django_update', 'Django Update'), ('event_update', 'Event Update'), ('newsletter', 'Newsletter'), ('community_update', 'Community Update')], max_length=20)), - ('html_template', models.TextField()), - ('mail_sent', models.BooleanField(default=False)), - ('recipients', models.ManyToManyField(related_name='received_updates', to='db.subscriber')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='Sponsorship', - fields=[ - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('tier', models.CharField(choices=[('platinum', 'platinum'), ('gold', 'gold'), ('silver', 'silver'), ('individual', 'individual'), ('organization', 'organization')], max_length=20)), - ('type', models.CharField(choices=[('community_sponsorship', 'community_sponsorship'), ('event_sponsorship', 'event_sponsorship')], max_length=30)), - ('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sponsors', to='db.event')), - ('sponsor_details', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sponsorships', to='db.sponsor')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/backend/djangoindia/db/migrations/0014_socialloginconnection_user_eventuserregistration.py b/backend/djangoindia/db/migrations/0014_socialloginconnection_user_eventuserregistration.py deleted file mode 100644 index 723538a8..00000000 --- a/backend/djangoindia/db/migrations/0014_socialloginconnection_user_eventuserregistration.py +++ /dev/null @@ -1,847 +0,0 @@ -# Generated by Django 4.2.5 on 2024-12-29 14:22 - -import django.contrib.auth.models -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ("auth", "0012_alter_user_first_name_max_length"), - ("db", "0013_alter_event_end_date_and_more"), - ] - - operations = [ - migrations.CreateModel( - name="SocialLoginConnection", - fields=[ - ( - "id", - models.UUIDField( - db_index=True, - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - unique=True, - ), - ), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ( - "medium", - models.CharField( - choices=[("Google", "google")], default=None, max_length=20 - ), - ), - ( - "last_login_at", - models.DateTimeField(default=django.utils.timezone.now, null=True), - ), - ( - "last_received_at", - models.DateTimeField(default=django.utils.timezone.now, null=True), - ), - ("token_data", models.JSONField(null=True)), - ("extra_data", models.JSONField(null=True)), - ], - options={ - "verbose_name": "Social Login Connection", - "verbose_name_plural": "Social Login Connections", - "db_table": "social_login_connections", - "ordering": ("-created_at",), - }, - ), - migrations.CreateModel( - name="User", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("password", models.CharField(max_length=128, verbose_name="password")), - ( - "last_login", - models.DateTimeField( - blank=True, null=True, verbose_name="last login" - ), - ), - ("username", models.CharField(max_length=128, unique=True)), - ( - "uuid", - models.UUIDField(default=uuid.uuid4, editable=False, unique=True), - ), - ( - "mobile_number", - models.CharField(blank=True, max_length=255, null=True), - ), - ( - "email", - models.CharField( - blank=True, max_length=255, null=True, unique=True - ), - ), - ("first_name", models.CharField(blank=True, max_length=255)), - ("last_name", models.CharField(blank=True, max_length=255)), - ("avatar", models.CharField(blank=True, max_length=255)), - ( - "organization", - models.CharField(blank=True, max_length=500, null=True), - ), - ( - "gender", - models.CharField( - choices=[ - ("male", "male"), - ("female", "female"), - ("non-binary", "non-binary"), - ("not_to_specify", "not_to_specify"), - ], - max_length=50, - ), - ), - ( - "created_at", - models.DateTimeField(auto_now_add=True, verbose_name="Created At"), - ), - ( - "updated_at", - models.DateTimeField( - auto_now=True, verbose_name="Last Modified At" - ), - ), - ("is_superuser", models.BooleanField(default=False)), - ("is_password_expired", models.BooleanField(default=False)), - ("is_active", models.BooleanField(default=True)), - ("is_staff", models.BooleanField(default=False)), - ("is_email_verified", models.BooleanField(default=False)), - ("is_password_autoset", models.BooleanField(default=False)), - ("is_onboarded", models.BooleanField(default=False)), - ( - "user_timezone", - models.CharField( - choices=[ - ("Africa/Abidjan", "Africa/Abidjan"), - ("Africa/Accra", "Africa/Accra"), - ("Africa/Addis_Ababa", "Africa/Addis_Ababa"), - ("Africa/Algiers", "Africa/Algiers"), - ("Africa/Asmara", "Africa/Asmara"), - ("Africa/Asmera", "Africa/Asmera"), - ("Africa/Bamako", "Africa/Bamako"), - ("Africa/Bangui", "Africa/Bangui"), - ("Africa/Banjul", "Africa/Banjul"), - ("Africa/Bissau", "Africa/Bissau"), - ("Africa/Blantyre", "Africa/Blantyre"), - ("Africa/Brazzaville", "Africa/Brazzaville"), - ("Africa/Bujumbura", "Africa/Bujumbura"), - ("Africa/Cairo", "Africa/Cairo"), - ("Africa/Casablanca", "Africa/Casablanca"), - ("Africa/Ceuta", "Africa/Ceuta"), - ("Africa/Conakry", "Africa/Conakry"), - ("Africa/Dakar", "Africa/Dakar"), - ("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"), - ("Africa/Djibouti", "Africa/Djibouti"), - ("Africa/Douala", "Africa/Douala"), - ("Africa/El_Aaiun", "Africa/El_Aaiun"), - ("Africa/Freetown", "Africa/Freetown"), - ("Africa/Gaborone", "Africa/Gaborone"), - ("Africa/Harare", "Africa/Harare"), - ("Africa/Johannesburg", "Africa/Johannesburg"), - ("Africa/Juba", "Africa/Juba"), - ("Africa/Kampala", "Africa/Kampala"), - ("Africa/Khartoum", "Africa/Khartoum"), - ("Africa/Kigali", "Africa/Kigali"), - ("Africa/Kinshasa", "Africa/Kinshasa"), - ("Africa/Lagos", "Africa/Lagos"), - ("Africa/Libreville", "Africa/Libreville"), - ("Africa/Lome", "Africa/Lome"), - ("Africa/Luanda", "Africa/Luanda"), - ("Africa/Lubumbashi", "Africa/Lubumbashi"), - ("Africa/Lusaka", "Africa/Lusaka"), - ("Africa/Malabo", "Africa/Malabo"), - ("Africa/Maputo", "Africa/Maputo"), - ("Africa/Maseru", "Africa/Maseru"), - ("Africa/Mbabane", "Africa/Mbabane"), - ("Africa/Mogadishu", "Africa/Mogadishu"), - ("Africa/Monrovia", "Africa/Monrovia"), - ("Africa/Nairobi", "Africa/Nairobi"), - ("Africa/Ndjamena", "Africa/Ndjamena"), - ("Africa/Niamey", "Africa/Niamey"), - ("Africa/Nouakchott", "Africa/Nouakchott"), - ("Africa/Ouagadougou", "Africa/Ouagadougou"), - ("Africa/Porto-Novo", "Africa/Porto-Novo"), - ("Africa/Sao_Tome", "Africa/Sao_Tome"), - ("Africa/Timbuktu", "Africa/Timbuktu"), - ("Africa/Tripoli", "Africa/Tripoli"), - ("Africa/Tunis", "Africa/Tunis"), - ("Africa/Windhoek", "Africa/Windhoek"), - ("America/Adak", "America/Adak"), - ("America/Anchorage", "America/Anchorage"), - ("America/Anguilla", "America/Anguilla"), - ("America/Antigua", "America/Antigua"), - ("America/Araguaina", "America/Araguaina"), - ( - "America/Argentina/Buenos_Aires", - "America/Argentina/Buenos_Aires", - ), - ( - "America/Argentina/Catamarca", - "America/Argentina/Catamarca", - ), - ( - "America/Argentina/ComodRivadavia", - "America/Argentina/ComodRivadavia", - ), - ("America/Argentina/Cordoba", "America/Argentina/Cordoba"), - ("America/Argentina/Jujuy", "America/Argentina/Jujuy"), - ( - "America/Argentina/La_Rioja", - "America/Argentina/La_Rioja", - ), - ("America/Argentina/Mendoza", "America/Argentina/Mendoza"), - ( - "America/Argentina/Rio_Gallegos", - "America/Argentina/Rio_Gallegos", - ), - ("America/Argentina/Salta", "America/Argentina/Salta"), - ( - "America/Argentina/San_Juan", - "America/Argentina/San_Juan", - ), - ( - "America/Argentina/San_Luis", - "America/Argentina/San_Luis", - ), - ("America/Argentina/Tucuman", "America/Argentina/Tucuman"), - ("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"), - ("America/Aruba", "America/Aruba"), - ("America/Asuncion", "America/Asuncion"), - ("America/Atikokan", "America/Atikokan"), - ("America/Atka", "America/Atka"), - ("America/Bahia", "America/Bahia"), - ("America/Bahia_Banderas", "America/Bahia_Banderas"), - ("America/Barbados", "America/Barbados"), - ("America/Belem", "America/Belem"), - ("America/Belize", "America/Belize"), - ("America/Blanc-Sablon", "America/Blanc-Sablon"), - ("America/Boa_Vista", "America/Boa_Vista"), - ("America/Bogota", "America/Bogota"), - ("America/Boise", "America/Boise"), - ("America/Buenos_Aires", "America/Buenos_Aires"), - ("America/Cambridge_Bay", "America/Cambridge_Bay"), - ("America/Campo_Grande", "America/Campo_Grande"), - ("America/Cancun", "America/Cancun"), - ("America/Caracas", "America/Caracas"), - ("America/Catamarca", "America/Catamarca"), - ("America/Cayenne", "America/Cayenne"), - ("America/Cayman", "America/Cayman"), - ("America/Chicago", "America/Chicago"), - ("America/Chihuahua", "America/Chihuahua"), - ("America/Ciudad_Juarez", "America/Ciudad_Juarez"), - ("America/Coral_Harbour", "America/Coral_Harbour"), - ("America/Cordoba", "America/Cordoba"), - ("America/Costa_Rica", "America/Costa_Rica"), - ("America/Creston", "America/Creston"), - ("America/Cuiaba", "America/Cuiaba"), - ("America/Curacao", "America/Curacao"), - ("America/Danmarkshavn", "America/Danmarkshavn"), - ("America/Dawson", "America/Dawson"), - ("America/Dawson_Creek", "America/Dawson_Creek"), - ("America/Denver", "America/Denver"), - ("America/Detroit", "America/Detroit"), - ("America/Dominica", "America/Dominica"), - ("America/Edmonton", "America/Edmonton"), - ("America/Eirunepe", "America/Eirunepe"), - ("America/El_Salvador", "America/El_Salvador"), - ("America/Ensenada", "America/Ensenada"), - ("America/Fort_Nelson", "America/Fort_Nelson"), - ("America/Fort_Wayne", "America/Fort_Wayne"), - ("America/Fortaleza", "America/Fortaleza"), - ("America/Glace_Bay", "America/Glace_Bay"), - ("America/Godthab", "America/Godthab"), - ("America/Goose_Bay", "America/Goose_Bay"), - ("America/Grand_Turk", "America/Grand_Turk"), - ("America/Grenada", "America/Grenada"), - ("America/Guadeloupe", "America/Guadeloupe"), - ("America/Guatemala", "America/Guatemala"), - ("America/Guayaquil", "America/Guayaquil"), - ("America/Guyana", "America/Guyana"), - ("America/Halifax", "America/Halifax"), - ("America/Havana", "America/Havana"), - ("America/Hermosillo", "America/Hermosillo"), - ( - "America/Indiana/Indianapolis", - "America/Indiana/Indianapolis", - ), - ("America/Indiana/Knox", "America/Indiana/Knox"), - ("America/Indiana/Marengo", "America/Indiana/Marengo"), - ( - "America/Indiana/Petersburg", - "America/Indiana/Petersburg", - ), - ("America/Indiana/Tell_City", "America/Indiana/Tell_City"), - ("America/Indiana/Vevay", "America/Indiana/Vevay"), - ("America/Indiana/Vincennes", "America/Indiana/Vincennes"), - ("America/Indiana/Winamac", "America/Indiana/Winamac"), - ("America/Indianapolis", "America/Indianapolis"), - ("America/Inuvik", "America/Inuvik"), - ("America/Iqaluit", "America/Iqaluit"), - ("America/Jamaica", "America/Jamaica"), - ("America/Jujuy", "America/Jujuy"), - ("America/Juneau", "America/Juneau"), - ( - "America/Kentucky/Louisville", - "America/Kentucky/Louisville", - ), - ( - "America/Kentucky/Monticello", - "America/Kentucky/Monticello", - ), - ("America/Knox_IN", "America/Knox_IN"), - ("America/Kralendijk", "America/Kralendijk"), - ("America/La_Paz", "America/La_Paz"), - ("America/Lima", "America/Lima"), - ("America/Los_Angeles", "America/Los_Angeles"), - ("America/Louisville", "America/Louisville"), - ("America/Lower_Princes", "America/Lower_Princes"), - ("America/Maceio", "America/Maceio"), - ("America/Managua", "America/Managua"), - ("America/Manaus", "America/Manaus"), - ("America/Marigot", "America/Marigot"), - ("America/Martinique", "America/Martinique"), - ("America/Matamoros", "America/Matamoros"), - ("America/Mazatlan", "America/Mazatlan"), - ("America/Mendoza", "America/Mendoza"), - ("America/Menominee", "America/Menominee"), - ("America/Merida", "America/Merida"), - ("America/Metlakatla", "America/Metlakatla"), - ("America/Mexico_City", "America/Mexico_City"), - ("America/Miquelon", "America/Miquelon"), - ("America/Moncton", "America/Moncton"), - ("America/Monterrey", "America/Monterrey"), - ("America/Montevideo", "America/Montevideo"), - ("America/Montreal", "America/Montreal"), - ("America/Montserrat", "America/Montserrat"), - ("America/Nassau", "America/Nassau"), - ("America/New_York", "America/New_York"), - ("America/Nipigon", "America/Nipigon"), - ("America/Nome", "America/Nome"), - ("America/Noronha", "America/Noronha"), - ( - "America/North_Dakota/Beulah", - "America/North_Dakota/Beulah", - ), - ( - "America/North_Dakota/Center", - "America/North_Dakota/Center", - ), - ( - "America/North_Dakota/New_Salem", - "America/North_Dakota/New_Salem", - ), - ("America/Nuuk", "America/Nuuk"), - ("America/Ojinaga", "America/Ojinaga"), - ("America/Panama", "America/Panama"), - ("America/Pangnirtung", "America/Pangnirtung"), - ("America/Paramaribo", "America/Paramaribo"), - ("America/Phoenix", "America/Phoenix"), - ("America/Port-au-Prince", "America/Port-au-Prince"), - ("America/Port_of_Spain", "America/Port_of_Spain"), - ("America/Porto_Acre", "America/Porto_Acre"), - ("America/Porto_Velho", "America/Porto_Velho"), - ("America/Puerto_Rico", "America/Puerto_Rico"), - ("America/Punta_Arenas", "America/Punta_Arenas"), - ("America/Rainy_River", "America/Rainy_River"), - ("America/Rankin_Inlet", "America/Rankin_Inlet"), - ("America/Recife", "America/Recife"), - ("America/Regina", "America/Regina"), - ("America/Resolute", "America/Resolute"), - ("America/Rio_Branco", "America/Rio_Branco"), - ("America/Rosario", "America/Rosario"), - ("America/Santa_Isabel", "America/Santa_Isabel"), - ("America/Santarem", "America/Santarem"), - ("America/Santiago", "America/Santiago"), - ("America/Santo_Domingo", "America/Santo_Domingo"), - ("America/Sao_Paulo", "America/Sao_Paulo"), - ("America/Scoresbysund", "America/Scoresbysund"), - ("America/Shiprock", "America/Shiprock"), - ("America/Sitka", "America/Sitka"), - ("America/St_Barthelemy", "America/St_Barthelemy"), - ("America/St_Johns", "America/St_Johns"), - ("America/St_Kitts", "America/St_Kitts"), - ("America/St_Lucia", "America/St_Lucia"), - ("America/St_Thomas", "America/St_Thomas"), - ("America/St_Vincent", "America/St_Vincent"), - ("America/Swift_Current", "America/Swift_Current"), - ("America/Tegucigalpa", "America/Tegucigalpa"), - ("America/Thule", "America/Thule"), - ("America/Thunder_Bay", "America/Thunder_Bay"), - ("America/Tijuana", "America/Tijuana"), - ("America/Toronto", "America/Toronto"), - ("America/Tortola", "America/Tortola"), - ("America/Vancouver", "America/Vancouver"), - ("America/Virgin", "America/Virgin"), - ("America/Whitehorse", "America/Whitehorse"), - ("America/Winnipeg", "America/Winnipeg"), - ("America/Yakutat", "America/Yakutat"), - ("America/Yellowknife", "America/Yellowknife"), - ("Antarctica/Casey", "Antarctica/Casey"), - ("Antarctica/Davis", "Antarctica/Davis"), - ("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"), - ("Antarctica/Macquarie", "Antarctica/Macquarie"), - ("Antarctica/Mawson", "Antarctica/Mawson"), - ("Antarctica/McMurdo", "Antarctica/McMurdo"), - ("Antarctica/Palmer", "Antarctica/Palmer"), - ("Antarctica/Rothera", "Antarctica/Rothera"), - ("Antarctica/South_Pole", "Antarctica/South_Pole"), - ("Antarctica/Syowa", "Antarctica/Syowa"), - ("Antarctica/Troll", "Antarctica/Troll"), - ("Antarctica/Vostok", "Antarctica/Vostok"), - ("Arctic/Longyearbyen", "Arctic/Longyearbyen"), - ("Asia/Aden", "Asia/Aden"), - ("Asia/Almaty", "Asia/Almaty"), - ("Asia/Amman", "Asia/Amman"), - ("Asia/Anadyr", "Asia/Anadyr"), - ("Asia/Aqtau", "Asia/Aqtau"), - ("Asia/Aqtobe", "Asia/Aqtobe"), - ("Asia/Ashgabat", "Asia/Ashgabat"), - ("Asia/Ashkhabad", "Asia/Ashkhabad"), - ("Asia/Atyrau", "Asia/Atyrau"), - ("Asia/Baghdad", "Asia/Baghdad"), - ("Asia/Bahrain", "Asia/Bahrain"), - ("Asia/Baku", "Asia/Baku"), - ("Asia/Bangkok", "Asia/Bangkok"), - ("Asia/Barnaul", "Asia/Barnaul"), - ("Asia/Beirut", "Asia/Beirut"), - ("Asia/Bishkek", "Asia/Bishkek"), - ("Asia/Brunei", "Asia/Brunei"), - ("Asia/Calcutta", "Asia/Calcutta"), - ("Asia/Chita", "Asia/Chita"), - ("Asia/Choibalsan", "Asia/Choibalsan"), - ("Asia/Chongqing", "Asia/Chongqing"), - ("Asia/Chungking", "Asia/Chungking"), - ("Asia/Colombo", "Asia/Colombo"), - ("Asia/Dacca", "Asia/Dacca"), - ("Asia/Damascus", "Asia/Damascus"), - ("Asia/Dhaka", "Asia/Dhaka"), - ("Asia/Dili", "Asia/Dili"), - ("Asia/Dubai", "Asia/Dubai"), - ("Asia/Dushanbe", "Asia/Dushanbe"), - ("Asia/Famagusta", "Asia/Famagusta"), - ("Asia/Gaza", "Asia/Gaza"), - ("Asia/Harbin", "Asia/Harbin"), - ("Asia/Hebron", "Asia/Hebron"), - ("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"), - ("Asia/Hong_Kong", "Asia/Hong_Kong"), - ("Asia/Hovd", "Asia/Hovd"), - ("Asia/Irkutsk", "Asia/Irkutsk"), - ("Asia/Istanbul", "Asia/Istanbul"), - ("Asia/Jakarta", "Asia/Jakarta"), - ("Asia/Jayapura", "Asia/Jayapura"), - ("Asia/Jerusalem", "Asia/Jerusalem"), - ("Asia/Kabul", "Asia/Kabul"), - ("Asia/Kamchatka", "Asia/Kamchatka"), - ("Asia/Karachi", "Asia/Karachi"), - ("Asia/Kashgar", "Asia/Kashgar"), - ("Asia/Kathmandu", "Asia/Kathmandu"), - ("Asia/Katmandu", "Asia/Katmandu"), - ("Asia/Khandyga", "Asia/Khandyga"), - ("Asia/Kolkata", "Asia/Kolkata"), - ("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"), - ("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"), - ("Asia/Kuching", "Asia/Kuching"), - ("Asia/Kuwait", "Asia/Kuwait"), - ("Asia/Macao", "Asia/Macao"), - ("Asia/Macau", "Asia/Macau"), - ("Asia/Magadan", "Asia/Magadan"), - ("Asia/Makassar", "Asia/Makassar"), - ("Asia/Manila", "Asia/Manila"), - ("Asia/Muscat", "Asia/Muscat"), - ("Asia/Nicosia", "Asia/Nicosia"), - ("Asia/Novokuznetsk", "Asia/Novokuznetsk"), - ("Asia/Novosibirsk", "Asia/Novosibirsk"), - ("Asia/Omsk", "Asia/Omsk"), - ("Asia/Oral", "Asia/Oral"), - ("Asia/Phnom_Penh", "Asia/Phnom_Penh"), - ("Asia/Pontianak", "Asia/Pontianak"), - ("Asia/Pyongyang", "Asia/Pyongyang"), - ("Asia/Qatar", "Asia/Qatar"), - ("Asia/Qostanay", "Asia/Qostanay"), - ("Asia/Qyzylorda", "Asia/Qyzylorda"), - ("Asia/Rangoon", "Asia/Rangoon"), - ("Asia/Riyadh", "Asia/Riyadh"), - ("Asia/Saigon", "Asia/Saigon"), - ("Asia/Sakhalin", "Asia/Sakhalin"), - ("Asia/Samarkand", "Asia/Samarkand"), - ("Asia/Seoul", "Asia/Seoul"), - ("Asia/Shanghai", "Asia/Shanghai"), - ("Asia/Singapore", "Asia/Singapore"), - ("Asia/Srednekolymsk", "Asia/Srednekolymsk"), - ("Asia/Taipei", "Asia/Taipei"), - ("Asia/Tashkent", "Asia/Tashkent"), - ("Asia/Tbilisi", "Asia/Tbilisi"), - ("Asia/Tehran", "Asia/Tehran"), - ("Asia/Tel_Aviv", "Asia/Tel_Aviv"), - ("Asia/Thimbu", "Asia/Thimbu"), - ("Asia/Thimphu", "Asia/Thimphu"), - ("Asia/Tokyo", "Asia/Tokyo"), - ("Asia/Tomsk", "Asia/Tomsk"), - ("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"), - ("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"), - ("Asia/Ulan_Bator", "Asia/Ulan_Bator"), - ("Asia/Urumqi", "Asia/Urumqi"), - ("Asia/Ust-Nera", "Asia/Ust-Nera"), - ("Asia/Vientiane", "Asia/Vientiane"), - ("Asia/Vladivostok", "Asia/Vladivostok"), - ("Asia/Yakutsk", "Asia/Yakutsk"), - ("Asia/Yangon", "Asia/Yangon"), - ("Asia/Yekaterinburg", "Asia/Yekaterinburg"), - ("Asia/Yerevan", "Asia/Yerevan"), - ("Atlantic/Azores", "Atlantic/Azores"), - ("Atlantic/Bermuda", "Atlantic/Bermuda"), - ("Atlantic/Canary", "Atlantic/Canary"), - ("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"), - ("Atlantic/Faeroe", "Atlantic/Faeroe"), - ("Atlantic/Faroe", "Atlantic/Faroe"), - ("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"), - ("Atlantic/Madeira", "Atlantic/Madeira"), - ("Atlantic/Reykjavik", "Atlantic/Reykjavik"), - ("Atlantic/South_Georgia", "Atlantic/South_Georgia"), - ("Atlantic/St_Helena", "Atlantic/St_Helena"), - ("Atlantic/Stanley", "Atlantic/Stanley"), - ("Australia/ACT", "Australia/ACT"), - ("Australia/Adelaide", "Australia/Adelaide"), - ("Australia/Brisbane", "Australia/Brisbane"), - ("Australia/Broken_Hill", "Australia/Broken_Hill"), - ("Australia/Canberra", "Australia/Canberra"), - ("Australia/Currie", "Australia/Currie"), - ("Australia/Darwin", "Australia/Darwin"), - ("Australia/Eucla", "Australia/Eucla"), - ("Australia/Hobart", "Australia/Hobart"), - ("Australia/LHI", "Australia/LHI"), - ("Australia/Lindeman", "Australia/Lindeman"), - ("Australia/Lord_Howe", "Australia/Lord_Howe"), - ("Australia/Melbourne", "Australia/Melbourne"), - ("Australia/NSW", "Australia/NSW"), - ("Australia/North", "Australia/North"), - ("Australia/Perth", "Australia/Perth"), - ("Australia/Queensland", "Australia/Queensland"), - ("Australia/South", "Australia/South"), - ("Australia/Sydney", "Australia/Sydney"), - ("Australia/Tasmania", "Australia/Tasmania"), - ("Australia/Victoria", "Australia/Victoria"), - ("Australia/West", "Australia/West"), - ("Australia/Yancowinna", "Australia/Yancowinna"), - ("Brazil/Acre", "Brazil/Acre"), - ("Brazil/DeNoronha", "Brazil/DeNoronha"), - ("Brazil/East", "Brazil/East"), - ("Brazil/West", "Brazil/West"), - ("CET", "CET"), - ("CST6CDT", "CST6CDT"), - ("Canada/Atlantic", "Canada/Atlantic"), - ("Canada/Central", "Canada/Central"), - ("Canada/Eastern", "Canada/Eastern"), - ("Canada/Mountain", "Canada/Mountain"), - ("Canada/Newfoundland", "Canada/Newfoundland"), - ("Canada/Pacific", "Canada/Pacific"), - ("Canada/Saskatchewan", "Canada/Saskatchewan"), - ("Canada/Yukon", "Canada/Yukon"), - ("Chile/Continental", "Chile/Continental"), - ("Chile/EasterIsland", "Chile/EasterIsland"), - ("Cuba", "Cuba"), - ("EET", "EET"), - ("EST", "EST"), - ("EST5EDT", "EST5EDT"), - ("Egypt", "Egypt"), - ("Eire", "Eire"), - ("Etc/GMT", "Etc/GMT"), - ("Etc/GMT+0", "Etc/GMT+0"), - ("Etc/GMT+1", "Etc/GMT+1"), - ("Etc/GMT+10", "Etc/GMT+10"), - ("Etc/GMT+11", "Etc/GMT+11"), - ("Etc/GMT+12", "Etc/GMT+12"), - ("Etc/GMT+2", "Etc/GMT+2"), - ("Etc/GMT+3", "Etc/GMT+3"), - ("Etc/GMT+4", "Etc/GMT+4"), - ("Etc/GMT+5", "Etc/GMT+5"), - ("Etc/GMT+6", "Etc/GMT+6"), - ("Etc/GMT+7", "Etc/GMT+7"), - ("Etc/GMT+8", "Etc/GMT+8"), - ("Etc/GMT+9", "Etc/GMT+9"), - ("Etc/GMT-0", "Etc/GMT-0"), - ("Etc/GMT-1", "Etc/GMT-1"), - ("Etc/GMT-10", "Etc/GMT-10"), - ("Etc/GMT-11", "Etc/GMT-11"), - ("Etc/GMT-12", "Etc/GMT-12"), - ("Etc/GMT-13", "Etc/GMT-13"), - ("Etc/GMT-14", "Etc/GMT-14"), - ("Etc/GMT-2", "Etc/GMT-2"), - ("Etc/GMT-3", "Etc/GMT-3"), - ("Etc/GMT-4", "Etc/GMT-4"), - ("Etc/GMT-5", "Etc/GMT-5"), - ("Etc/GMT-6", "Etc/GMT-6"), - ("Etc/GMT-7", "Etc/GMT-7"), - ("Etc/GMT-8", "Etc/GMT-8"), - ("Etc/GMT-9", "Etc/GMT-9"), - ("Etc/GMT0", "Etc/GMT0"), - ("Etc/Greenwich", "Etc/Greenwich"), - ("Etc/UCT", "Etc/UCT"), - ("Etc/UTC", "Etc/UTC"), - ("Etc/Universal", "Etc/Universal"), - ("Etc/Zulu", "Etc/Zulu"), - ("Europe/Amsterdam", "Europe/Amsterdam"), - ("Europe/Andorra", "Europe/Andorra"), - ("Europe/Astrakhan", "Europe/Astrakhan"), - ("Europe/Athens", "Europe/Athens"), - ("Europe/Belfast", "Europe/Belfast"), - ("Europe/Belgrade", "Europe/Belgrade"), - ("Europe/Berlin", "Europe/Berlin"), - ("Europe/Bratislava", "Europe/Bratislava"), - ("Europe/Brussels", "Europe/Brussels"), - ("Europe/Bucharest", "Europe/Bucharest"), - ("Europe/Budapest", "Europe/Budapest"), - ("Europe/Busingen", "Europe/Busingen"), - ("Europe/Chisinau", "Europe/Chisinau"), - ("Europe/Copenhagen", "Europe/Copenhagen"), - ("Europe/Dublin", "Europe/Dublin"), - ("Europe/Gibraltar", "Europe/Gibraltar"), - ("Europe/Guernsey", "Europe/Guernsey"), - ("Europe/Helsinki", "Europe/Helsinki"), - ("Europe/Isle_of_Man", "Europe/Isle_of_Man"), - ("Europe/Istanbul", "Europe/Istanbul"), - ("Europe/Jersey", "Europe/Jersey"), - ("Europe/Kaliningrad", "Europe/Kaliningrad"), - ("Europe/Kiev", "Europe/Kiev"), - ("Europe/Kirov", "Europe/Kirov"), - ("Europe/Kyiv", "Europe/Kyiv"), - ("Europe/Lisbon", "Europe/Lisbon"), - ("Europe/Ljubljana", "Europe/Ljubljana"), - ("Europe/London", "Europe/London"), - ("Europe/Luxembourg", "Europe/Luxembourg"), - ("Europe/Madrid", "Europe/Madrid"), - ("Europe/Malta", "Europe/Malta"), - ("Europe/Mariehamn", "Europe/Mariehamn"), - ("Europe/Minsk", "Europe/Minsk"), - ("Europe/Monaco", "Europe/Monaco"), - ("Europe/Moscow", "Europe/Moscow"), - ("Europe/Nicosia", "Europe/Nicosia"), - ("Europe/Oslo", "Europe/Oslo"), - ("Europe/Paris", "Europe/Paris"), - ("Europe/Podgorica", "Europe/Podgorica"), - ("Europe/Prague", "Europe/Prague"), - ("Europe/Riga", "Europe/Riga"), - ("Europe/Rome", "Europe/Rome"), - ("Europe/Samara", "Europe/Samara"), - ("Europe/San_Marino", "Europe/San_Marino"), - ("Europe/Sarajevo", "Europe/Sarajevo"), - ("Europe/Saratov", "Europe/Saratov"), - ("Europe/Simferopol", "Europe/Simferopol"), - ("Europe/Skopje", "Europe/Skopje"), - ("Europe/Sofia", "Europe/Sofia"), - ("Europe/Stockholm", "Europe/Stockholm"), - ("Europe/Tallinn", "Europe/Tallinn"), - ("Europe/Tirane", "Europe/Tirane"), - ("Europe/Tiraspol", "Europe/Tiraspol"), - ("Europe/Ulyanovsk", "Europe/Ulyanovsk"), - ("Europe/Uzhgorod", "Europe/Uzhgorod"), - ("Europe/Vaduz", "Europe/Vaduz"), - ("Europe/Vatican", "Europe/Vatican"), - ("Europe/Vienna", "Europe/Vienna"), - ("Europe/Vilnius", "Europe/Vilnius"), - ("Europe/Volgograd", "Europe/Volgograd"), - ("Europe/Warsaw", "Europe/Warsaw"), - ("Europe/Zagreb", "Europe/Zagreb"), - ("Europe/Zaporozhye", "Europe/Zaporozhye"), - ("Europe/Zurich", "Europe/Zurich"), - ("GB", "GB"), - ("GB-Eire", "GB-Eire"), - ("GMT", "GMT"), - ("GMT+0", "GMT+0"), - ("GMT-0", "GMT-0"), - ("GMT0", "GMT0"), - ("Greenwich", "Greenwich"), - ("HST", "HST"), - ("Hongkong", "Hongkong"), - ("Iceland", "Iceland"), - ("Indian/Antananarivo", "Indian/Antananarivo"), - ("Indian/Chagos", "Indian/Chagos"), - ("Indian/Christmas", "Indian/Christmas"), - ("Indian/Cocos", "Indian/Cocos"), - ("Indian/Comoro", "Indian/Comoro"), - ("Indian/Kerguelen", "Indian/Kerguelen"), - ("Indian/Mahe", "Indian/Mahe"), - ("Indian/Maldives", "Indian/Maldives"), - ("Indian/Mauritius", "Indian/Mauritius"), - ("Indian/Mayotte", "Indian/Mayotte"), - ("Indian/Reunion", "Indian/Reunion"), - ("Iran", "Iran"), - ("Israel", "Israel"), - ("Jamaica", "Jamaica"), - ("Japan", "Japan"), - ("Kwajalein", "Kwajalein"), - ("Libya", "Libya"), - ("MET", "MET"), - ("MST", "MST"), - ("MST7MDT", "MST7MDT"), - ("Mexico/BajaNorte", "Mexico/BajaNorte"), - ("Mexico/BajaSur", "Mexico/BajaSur"), - ("Mexico/General", "Mexico/General"), - ("NZ", "NZ"), - ("NZ-CHAT", "NZ-CHAT"), - ("Navajo", "Navajo"), - ("PRC", "PRC"), - ("PST8PDT", "PST8PDT"), - ("Pacific/Apia", "Pacific/Apia"), - ("Pacific/Auckland", "Pacific/Auckland"), - ("Pacific/Bougainville", "Pacific/Bougainville"), - ("Pacific/Chatham", "Pacific/Chatham"), - ("Pacific/Chuuk", "Pacific/Chuuk"), - ("Pacific/Easter", "Pacific/Easter"), - ("Pacific/Efate", "Pacific/Efate"), - ("Pacific/Enderbury", "Pacific/Enderbury"), - ("Pacific/Fakaofo", "Pacific/Fakaofo"), - ("Pacific/Fiji", "Pacific/Fiji"), - ("Pacific/Funafuti", "Pacific/Funafuti"), - ("Pacific/Galapagos", "Pacific/Galapagos"), - ("Pacific/Gambier", "Pacific/Gambier"), - ("Pacific/Guadalcanal", "Pacific/Guadalcanal"), - ("Pacific/Guam", "Pacific/Guam"), - ("Pacific/Honolulu", "Pacific/Honolulu"), - ("Pacific/Johnston", "Pacific/Johnston"), - ("Pacific/Kanton", "Pacific/Kanton"), - ("Pacific/Kiritimati", "Pacific/Kiritimati"), - ("Pacific/Kosrae", "Pacific/Kosrae"), - ("Pacific/Kwajalein", "Pacific/Kwajalein"), - ("Pacific/Majuro", "Pacific/Majuro"), - ("Pacific/Marquesas", "Pacific/Marquesas"), - ("Pacific/Midway", "Pacific/Midway"), - ("Pacific/Nauru", "Pacific/Nauru"), - ("Pacific/Niue", "Pacific/Niue"), - ("Pacific/Norfolk", "Pacific/Norfolk"), - ("Pacific/Noumea", "Pacific/Noumea"), - ("Pacific/Pago_Pago", "Pacific/Pago_Pago"), - ("Pacific/Palau", "Pacific/Palau"), - ("Pacific/Pitcairn", "Pacific/Pitcairn"), - ("Pacific/Pohnpei", "Pacific/Pohnpei"), - ("Pacific/Ponape", "Pacific/Ponape"), - ("Pacific/Port_Moresby", "Pacific/Port_Moresby"), - ("Pacific/Rarotonga", "Pacific/Rarotonga"), - ("Pacific/Saipan", "Pacific/Saipan"), - ("Pacific/Samoa", "Pacific/Samoa"), - ("Pacific/Tahiti", "Pacific/Tahiti"), - ("Pacific/Tarawa", "Pacific/Tarawa"), - ("Pacific/Tongatapu", "Pacific/Tongatapu"), - ("Pacific/Truk", "Pacific/Truk"), - ("Pacific/Wake", "Pacific/Wake"), - ("Pacific/Wallis", "Pacific/Wallis"), - ("Pacific/Yap", "Pacific/Yap"), - ("Poland", "Poland"), - ("Portugal", "Portugal"), - ("ROC", "ROC"), - ("ROK", "ROK"), - ("Singapore", "Singapore"), - ("Turkey", "Turkey"), - ("UCT", "UCT"), - ("US/Alaska", "US/Alaska"), - ("US/Aleutian", "US/Aleutian"), - ("US/Arizona", "US/Arizona"), - ("US/Central", "US/Central"), - ("US/East-Indiana", "US/East-Indiana"), - ("US/Eastern", "US/Eastern"), - ("US/Hawaii", "US/Hawaii"), - ("US/Indiana-Starke", "US/Indiana-Starke"), - ("US/Michigan", "US/Michigan"), - ("US/Mountain", "US/Mountain"), - ("US/Pacific", "US/Pacific"), - ("US/Samoa", "US/Samoa"), - ("UTC", "UTC"), - ("Universal", "Universal"), - ("W-SU", "W-SU"), - ("WET", "WET"), - ("Zulu", "Zulu"), - ], - default="Asia/Kolkata", - max_length=255, - ), - ), - ( - "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_account_set", - related_query_name="user_account", - to="auth.group", - verbose_name="groups", - ), - ), - ( - "user_permissions", - models.ManyToManyField( - blank=True, - help_text="Specific permissions for this user.", - related_name="user_account_set", - related_query_name="user_account", - to="auth.permission", - verbose_name="user permissions", - ), - ), - ], - options={ - "verbose_name": "User", - "verbose_name_plural": "Users", - "db_table": "users", - "ordering": ("-created_at",), - }, - managers=[ - ("objects", django.contrib.auth.models.UserManager()), - ], - ), - migrations.CreateModel( - name="EventUserRegistration", - fields=[ - ( - "id", - models.UUIDField( - db_index=True, - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - unique=True, - ), - ), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ( - "status", - models.CharField( - choices=[ - ("going", "going"), - ("waiting", "waiting"), - ("not_going", "not_going"), - ], - max_length=50, - ), - ), - ("first_time_attendee", models.BooleanField(default=True)), - ( - "event", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="db.event" - ), - ), - ], - options={ - "abstract": False, - }, - ), - ] From 7150c22e0bc8d3085d25ef032b4c9d85662fd202 Mon Sep 17 00:00:00 2001 From: DevilsAutumn Date: Fri, 10 Jan 2025 20:55:51 +0530 Subject: [PATCH 4/4] added complete user account endpoints --- .../djangoindia/api/views/authentication.py | 27 ++-- backend/djangoindia/db/admin.py | 36 ++++- ..._user_avatar_alter_user_gender_and_more.py | 128 ++++++++++++++++++ backend/djangoindia/db/models/__init__.py | 9 +- backend/djangoindia/db/models/user.py | 72 +--------- backend/djangoindia/settings/base.py | 1 + 6 files changed, 184 insertions(+), 89 deletions(-) create mode 100644 backend/djangoindia/db/migrations/0002_alter_user_avatar_alter_user_gender_and_more.py diff --git a/backend/djangoindia/api/views/authentication.py b/backend/djangoindia/api/views/authentication.py index 18a7cb12..b786339e 100644 --- a/backend/djangoindia/api/views/authentication.py +++ b/backend/djangoindia/api/views/authentication.py @@ -301,22 +301,27 @@ def post(self, request): class SignOutEndpoint(BaseAPIView): def post(self, request): - refresh_token = request.data.get("refresh_token", False) + try: + refresh_token = request.data.get("refresh_token") + + if not refresh_token: + return Response( + {"message": "No refresh token provided"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + token = RefreshToken(refresh_token) + token.blacklist() - if not refresh_token: return Response( - {"message": "No refresh token provided"}, + {"message": "Successfully logged out"}, status=status.HTTP_200_OK + ) + except Exception as e: + return Response( + {"message": str(e)}, status=status.HTTP_400_BAD_REQUEST, ) - user = User.objects.get(pk=request.user.id) - - user.save() - - token = RefreshToken(refresh_token) - token.blacklist() - return Response({"message": "success"}, status=status.HTTP_200_OK) - class RequestEmailVerificationEndpoint(BaseAPIView): permission_classes = [ diff --git a/backend/djangoindia/db/admin.py b/backend/djangoindia/db/admin.py index f99e276e..5f7a86e8 100644 --- a/backend/djangoindia/db/admin.py +++ b/backend/djangoindia/db/admin.py @@ -12,16 +12,20 @@ from django.urls import path from djangoindia.bg_tasks.event_registration import send_mass_mail_task -from djangoindia.db.models.communication import ContactUs, Subscriber -from djangoindia.db.models.event import Event, EventRegistration -from djangoindia.db.models.partner_and_sponsor import ( +from djangoindia.db.models import ( CommunityPartner, + ContactUs, + Event, + EventRegistration, + EventUserRegistration, + SocialLoginConnection, Sponsor, Sponsorship, + Subscriber, + Update, + User, + Volunteer, ) -from djangoindia.db.models.update import Update -from djangoindia.db.models.user import User -from djangoindia.db.models.volunteer import Volunteer from .forms import EmailForm, EventForm, UpdateForm @@ -319,3 +323,23 @@ class UserAdmin(admin.ModelAdmin): ("Important dates", {"fields": ("created_at", "updated_at")}), ) ordering = ("-created_at",) + + +@admin.register(SocialLoginConnection) +class SocialLoginConnectionAdmin(admin.ModelAdmin): + list_display = ["user", "provider", "created_at"] + search_fields = ["user__username", "user__email"] + readonly_fields = ("created_at", "updated_at") + ordering = ("-created_at",) + + def provider(self, obj): + return obj.medium + + +@admin.register(EventUserRegistration) +class EventUserRegistrationAdmin(admin.ModelAdmin): + list_display = ["user", "event", "created_at"] + search_fields = ["user__username", "user__email", "event__name"] + readonly_fields = ("created_at", "updated_at") + list_filter = ("event__name",) + ordering = ("-created_at",) diff --git a/backend/djangoindia/db/migrations/0002_alter_user_avatar_alter_user_gender_and_more.py b/backend/djangoindia/db/migrations/0002_alter_user_avatar_alter_user_gender_and_more.py new file mode 100644 index 00000000..b5b1db47 --- /dev/null +++ b/backend/djangoindia/db/migrations/0002_alter_user_avatar_alter_user_gender_and_more.py @@ -0,0 +1,128 @@ +# Generated by Django 4.2.5 on 2025-01-10 15:24 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ("db", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="avatar", + field=models.ImageField(upload_to="users/avatars/"), + ), + migrations.AlterField( + model_name="user", + name="gender", + field=models.CharField( + choices=[ + ("male", "male"), + ("female", "female"), + ("not_to_specify", "not_to_specify"), + ], + max_length=50, + ), + ), + migrations.CreateModel( + name="SocialLoginConnection", + fields=[ + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "medium", + models.CharField( + choices=[("Google", "google")], default=None, max_length=20 + ), + ), + ( + "last_login_at", + models.DateTimeField(default=django.utils.timezone.now, null=True), + ), + ( + "last_received_at", + models.DateTimeField(default=django.utils.timezone.now, null=True), + ), + ("token_data", models.JSONField(null=True)), + ("extra_data", models.JSONField(null=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="user_login_connections", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "Social Login Connection", + "verbose_name_plural": "Social Login Connections", + "db_table": "social_login_connections", + "ordering": ("-created_at",), + }, + ), + migrations.CreateModel( + name="EventUserRegistration", + fields=[ + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "status", + models.CharField( + choices=[ + ("going", "going"), + ("waiting", "waiting"), + ("not_going", "not_going"), + ], + max_length=50, + ), + ), + ("first_time_attendee", models.BooleanField(default=True)), + ( + "event", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="db.event" + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/backend/djangoindia/db/models/__init__.py b/backend/djangoindia/db/models/__init__.py index 16870a39..99581aa2 100644 --- a/backend/djangoindia/db/models/__init__.py +++ b/backend/djangoindia/db/models/__init__.py @@ -1,12 +1,10 @@ from .communication import ContactUs, Subscriber -from .event import Event, EventRegistration -from .partner_and_sponsor import CommunityPartner, Sponsorship +from .event import Event, EventRegistration, EventUserRegistration +from .partner_and_sponsor import CommunityPartner, Sponsor, Sponsorship from .update import Update from .user import SocialLoginConnection, User from .volunteer import Volunteer -from .user import User - __all__ = [ "ContactUs", @@ -18,4 +16,7 @@ "Update", "Volunteer", "User", + "SocialLoginConnection", + "Sponsor", + "EventUserRegistration", ] diff --git a/backend/djangoindia/db/models/user.py b/backend/djangoindia/db/models/user.py index 6465d752..10d2e16a 100644 --- a/backend/djangoindia/db/models/user.py +++ b/backend/djangoindia/db/models/user.py @@ -1,9 +1,11 @@ - # Python imports import uuid import pytz +from django.conf import settings + +# Django imports from django.contrib.auth.models import ( AbstractBaseUser, Group, @@ -11,9 +13,6 @@ PermissionsMixin, UserManager, ) - -# Django imports -from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ @@ -26,7 +25,6 @@ class GENDER: CHOICES = ( ("male", "male"), ("female", "female"), - ("non-binary", "non-binary"), ("not_to_specify", "not_to_specify"), ) @@ -40,69 +38,7 @@ class GENDER: email = models.CharField(max_length=255, null=True, blank=True, unique=True) first_name = models.CharField(max_length=255, blank=True) last_name = models.CharField(max_length=255, blank=True) - avatar = models.CharField(max_length=255, blank=True) - organization = models.CharField(max_length=500, blank=True, null=True) - gender = models.CharField(choices=GENDER.CHOICES, max_length=50) - - # tracking metrics - created_at = models.DateTimeField(auto_now_add=True, verbose_name="Created At") - updated_at = models.DateTimeField(auto_now=True, verbose_name="Last Modified At") - - # the is' es - is_superuser = models.BooleanField(default=False) - is_password_expired = models.BooleanField(default=False) - is_active = models.BooleanField(default=True) - is_staff = models.BooleanField(default=False) - is_email_verified = models.BooleanField(default=False) - is_password_autoset = models.BooleanField(default=False) - is_onboarded = models.BooleanField(default=False) - - user_timezone = models.CharField( - max_length=255, default="Asia/Kolkata", choices=USER_TIMEZONE_CHOICES - ) - - groups = models.ManyToManyField( - Group, - verbose_name=_("groups"), - 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_account_set", - related_query_name="user_account", - ) - user_permissions = models.ManyToManyField( - Permission, - verbose_name=_("user permissions"), - blank=True, - help_text=_("Specific permissions for this user."), - related_name="user_account_set", - related_query_name="user_account", - ) - - USERNAME_FIELD = "email" - - REQUIRED_FIELDS = ["username"] - - objects = UserManager() - - class Meta: - verbose_name = "User" - verbose_name_plural = "Users" - db_table = "users" - ordering = ("-created_at",) - - def __str__(self): - return f"{self.username} <{self.email}>" - - def save(self, *args, **kwargs): - self.email = self.email.lower().strip() - - if self.is_superuser: - self.is_staff = True - - super().save(*args, **kwargs) + avatar = models.ImageField(upload_to="users/avatars/") organization = models.CharField(max_length=500, blank=True, null=True) gender = models.CharField(choices=GENDER.CHOICES, max_length=50) diff --git a/backend/djangoindia/settings/base.py b/backend/djangoindia/settings/base.py index f110abe3..9556f90c 100644 --- a/backend/djangoindia/settings/base.py +++ b/backend/djangoindia/settings/base.py @@ -54,6 +54,7 @@ "djangoindia.bg_tasks", "djangoindia.db", "rest_framework", + "rest_framework_simplejwt.token_blacklist", "drf_spectacular", "import_export", "cabinet",