From 8e7f2d6d693be8ab958d866a921002ec1c4bb9cc Mon Sep 17 00:00:00 2001
From: bobbyvish
Date: Thu, 27 Jun 2024 17:24:52 +0530
Subject: [PATCH] feat(module 1): add customer, non transferlist, import ,
export
---
accounts/api/serializers.py | 9 +-
accounts/api/urls.py | 1 +
accounts/api/views.py | 43 ++
accounts/forms.py | 62 ++-
.../0012_iamprincipalextendeddata.py | 28 +
...lextendeddata_pwd_changed_post_transfer.py | 18 +
accounts/models.py | 33 ++
accounts/urls.py | 8 +
accounts/views.py | 503 +++++++++++++++++-
goodtimes/mixins.py | 32 ++
goodtimes/utils.py | 21 +
manage_events/api/views.py | 8 +-
manage_events/forms.py | 29 +
.../migrations/0013_venue_principal.py | 21 +
.../migrations/0014_event_principal.py | 21 +
manage_events/models.py | 4 +-
manage_events/views.py | 11 +-
manage_subscriptions/forms.py | 1 +
.../migrations/0008_subscription_is_free.py | 18 +
manage_subscriptions/models.py | 8 +
manage_subscriptions/views.py | 11 +-
.../jquery-validate/jquery.validate.min.js | 4 +
.../account_transfer_email_template.html | 31 ++
templates/accounts/customer/customer_add.html | 212 ++++++++
.../customer/customer_bulk_template.html | 80 +++
.../accounts/customer/customer_detail.html | 101 ++++
.../accounts/customer/customer_edit.html | 220 ++++++++
.../accounts/customer/customer_list.html | 58 +-
.../jquery_validate_cdn_js.html | 2 +
templates/layout/base_template.html | 2 +-
templates/manage_events/event_add.html | 13 +-
templates/manage_events/event_list.html | 14 +-
.../subscription_list.html | 6 +
templates/manage_venues/venue_add.html | 13 +-
templates/manage_venues/venue_list.html | 9 +
35 files changed, 1599 insertions(+), 56 deletions(-)
create mode 100644 accounts/migrations/0012_iamprincipalextendeddata.py
create mode 100644 accounts/migrations/0013_iamprincipalextendeddata_pwd_changed_post_transfer.py
create mode 100644 goodtimes/mixins.py
create mode 100644 manage_events/migrations/0013_venue_principal.py
create mode 100644 manage_events/migrations/0014_event_principal.py
create mode 100644 manage_subscriptions/migrations/0008_subscription_is_free.py
create mode 100644 static/src/plugins/src/jquery-validate/jquery.validate.min.js
create mode 100644 templates/accounts/customer/account_transfer_email_template.html
create mode 100644 templates/accounts/customer/customer_add.html
create mode 100644 templates/accounts/customer/customer_bulk_template.html
create mode 100644 templates/accounts/customer/customer_detail.html
create mode 100644 templates/accounts/customer/customer_edit.html
create mode 100644 templates/cdn_through_html/jquery_validate_cdn_js.html
diff --git a/accounts/api/serializers.py b/accounts/api/serializers.py
index fcfff7d..34cab6e 100644
--- a/accounts/api/serializers.py
+++ b/accounts/api/serializers.py
@@ -6,6 +6,7 @@ from rest_framework import serializers
from accounts.models import (
AppVersion,
IAmPrincipal,
+ IAmPrincipalExtendedData,
IAmPrincipalType,
# IAmPrincipalKYCDetails,
)
@@ -348,4 +349,10 @@ class AppVersionSerializer(serializers.ModelSerializer):
"version",
"force_upgrade",
"recommend_upgrade",
- ]
\ No newline at end of file
+ ]
+
+
+class IAmPrincipalExtendedDataSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = IAmPrincipalExtendedData
+ fields = "__all__"
\ No newline at end of file
diff --git a/accounts/api/urls.py b/accounts/api/urls.py
index 1428632..6a51de5 100644
--- a/accounts/api/urls.py
+++ b/accounts/api/urls.py
@@ -38,5 +38,6 @@ urlpatterns = [
name="delete_account",
),
path('version-check/', views.VersionCheck.as_view(), name='version_check'),
+ path('transfer-check/', views.AccountTransferCheckView.as_view(), name='transfer_check'),
]
diff --git a/accounts/api/views.py b/accounts/api/views.py
index 54160be..8aac098 100644
--- a/accounts/api/views.py
+++ b/accounts/api/views.py
@@ -17,6 +17,7 @@ from django.views.decorators.http import require_http_methods
from accounts.models import (
AppVersion,
IAmPrincipal,
+ IAmPrincipalExtendedData,
IAmPrincipalOtp,
IAmPrincipalSource,
IAmPrincipalType,
@@ -41,6 +42,7 @@ from .serializers import (
# RegistrationPasswordSerializer,
# PhoneSerializer,
EmailSerializer,
+ IAmPrincipalExtendedDataSerializer,
PlayerIDSerializer,
RegistrationPasswordSerializer,
RegistrationSerializer,
@@ -971,3 +973,44 @@ class VersionCheck(APIView):
version_data = AppVersionSerializer(version)
return ApiResponse.success(message=constants.SUCCESS, data=version_data.data)
+
+
+class AccountTransferCheckView(APIView):
+ authentication_classes = [JWTAuthentication]
+ permission_classes = [IsAuthenticated]
+ model = IAmPrincipalExtendedData
+ serializer_class = IAmPrincipalExtendedDataSerializer
+
+ def get(self, request, *args, **kwargs):
+ print("request.user is ", request.user)
+ try:
+ obj = IAmPrincipalExtendedData.objects.get(principal=request.user)
+ except Exception as e:
+ error_response = {
+ "status": status.HTTP_404_NOT_FOUND,
+ "message": constants.RECORD_NOT_FOUND,
+ "errors": str(e),
+ }
+ return ApiResponse.error(**error_response)
+
+ serializer = self.serializer_class(obj)
+ print("serializer data", serializer)
+ return ApiResponse.success(message=constants.SUCCESS, data=serializer.data)
+
+ def post(self, request, *args, **kwargs):
+ try:
+ obj = IAmPrincipalExtendedData.objects.get(principal=request.user)
+ except Exception as e:
+ error_response = {
+ "status": status.HTTP_404_NOT_FOUND,
+ "message": constants.RECORD_NOT_FOUND,
+ "errors": str(e),
+ }
+ return ApiResponse.error(**error_response)
+
+ obj.pwd_changed_post_transfer = True
+ obj.save()
+ serializer = self.serializer_class(obj)
+ return ApiResponse.success(message=constants.SUCCESS, data=serializer.data)
+
+
diff --git a/accounts/forms.py b/accounts/forms.py
index d4d90ee..fbc6897 100644
--- a/accounts/forms.py
+++ b/accounts/forms.py
@@ -6,6 +6,7 @@ from django.core import validators
from django.utils.translation import gettext_lazy as _
from goodtimes import constants
+from manage_events.models import EventCategory
from . import models
# from .backend import EmailBackend
@@ -357,4 +358,63 @@ class IAmPrincipalResourceLinkForm(IAmPrincipalForm):
principal.principal_resource.set(principal_resource_data)
# Save the instance to the database
if commit:
- principal.save()
\ No newline at end of file
+ principal.save()
+
+
+class CreateCustomerForm(forms.Form):
+ first_name = forms.CharField(max_length=255, required=True, label='First Name')
+ last_name = forms.CharField(max_length=255, required=True, label='Last Name')
+ email = forms.EmailField(required=True, label='Email')
+ preferences = forms.ModelMultipleChoiceField(
+ queryset=EventCategory.objects.all(),
+ widget=forms.widgets.SelectMultiple(
+ attrs={"class": "form_select js-example-basic-multiple"}
+ ),
+ required=False,
+ label='Preferences'
+ )
+ free_start_date = forms.DateField(
+ required=True,
+ label=_('Free period start date'),
+ help_text=_('Enter the start date of the free period')
+ )
+ free_end_date = forms.DateField(
+ required=True,
+ label=_('Free period end date'),
+ help_text=_('Enter the end date of the free period')
+ )
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields['preferences'].queryset = EventCategory.objects.all()
+
+class UpdateCustomerForm(forms.Form):
+ first_name = forms.CharField(max_length=255, required=True, label='First Name')
+ last_name = forms.CharField(max_length=255, required=True, label='Last Name')
+ email = forms.EmailField(required=True, label='Email', widget=forms.TextInput(attrs={'readonly': 'readonly'}))
+ preferences = forms.ModelMultipleChoiceField(
+ queryset=EventCategory.objects.all(),
+ widget=forms.widgets.SelectMultiple(
+ attrs={"class": "form_select js-example-basic-multiple"}
+ ),
+ required=False,
+ label='Preferences'
+ )
+ free_start_date = forms.DateField(
+ required=True,
+ label=_('Free period start date'),
+ help_text=_('Enter the start date of the free period')
+ )
+ free_end_date = forms.DateField(
+ required=True,
+ label=_('Free period end date'),
+ help_text=_('Enter the end date of the free period')
+ )
+ active = forms.BooleanField(required=False, label='Active', help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",)
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields['preferences'].queryset = EventCategory.objects.all()
+
+class UploadExcelForm(forms.Form):
+ file = forms.FileField()
diff --git a/accounts/migrations/0012_iamprincipalextendeddata.py b/accounts/migrations/0012_iamprincipalextendeddata.py
new file mode 100644
index 0000000..220b1e2
--- /dev/null
+++ b/accounts/migrations/0012_iamprincipalextendeddata.py
@@ -0,0 +1,28 @@
+# Generated by Django 5.0.2 on 2024-06-25 07:28
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0011_alter_iamprincipallocation_principal'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='IAmPrincipalExtendedData',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('is_onboarded', models.BooleanField(default=False, help_text='Indicates whether the user was onboarded by an admin.')),
+ ('is_transferred', models.BooleanField(default=False, help_text='Indicates whether the account has been transferred to the user.')),
+ ('transferred_on', models.DateTimeField(blank=True, help_text='The date and time when the account was transferred to the user.', null=True)),
+ ('principal', models.OneToOneField(help_text='The principal user to which this extended data is related.', on_delete=django.db.models.deletion.CASCADE, related_name='extended_data', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'db_table': 'iam_principal_extended_data',
+ },
+ ),
+ ]
diff --git a/accounts/migrations/0013_iamprincipalextendeddata_pwd_changed_post_transfer.py b/accounts/migrations/0013_iamprincipalextendeddata_pwd_changed_post_transfer.py
new file mode 100644
index 0000000..6815936
--- /dev/null
+++ b/accounts/migrations/0013_iamprincipalextendeddata_pwd_changed_post_transfer.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.2 on 2024-06-27 08:11
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0012_iamprincipalextendeddata'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='iamprincipalextendeddata',
+ name='pwd_changed_post_transfer',
+ field=models.BooleanField(default=False, help_text='Indicates if the user changed their password after the account was transferred.'),
+ ),
+ ]
diff --git a/accounts/models.py b/accounts/models.py
index 1bc9b89..d7c1dd4 100644
--- a/accounts/models.py
+++ b/accounts/models.py
@@ -333,6 +333,39 @@ class IAmPrincipal(AbstractUser):
return f"{self.email}"
+class IAmPrincipalExtendedData(models.Model):
+ principal = models.OneToOneField(
+ IAmPrincipal,
+ related_name="extended_data",
+ on_delete=models.CASCADE,
+ help_text="The principal user to which this extended data is related."
+ )
+ is_onboarded = models.BooleanField(
+ default=False,
+ help_text="Indicates whether the user was onboarded by an admin."
+ )
+ is_transferred = models.BooleanField(
+ default=False,
+ help_text="Indicates whether the account has been transferred to the user."
+ )
+ transferred_on = models.DateTimeField(
+ null=True,
+ blank=True,
+ help_text="The date and time when the account was transferred to the user."
+ )
+ pwd_changed_post_transfer = models.BooleanField(default=False, help_text="Indicates if the user changed their password after the account was transferred.")
+
+ class Meta:
+ db_table = "iam_principal_extended_data"
+
+ def __str__(self):
+ return f"Extended Data for {self.principal}"
+
+ def save(self, *args, **kwargs):
+ if self.is_transferred and self.transferred_on is None:
+ self.transferred_on = datetime.datetime.now()
+ super().save(*args, **kwargs)
+
class IAmPrincipalResourceLink(models.Model):
principal = models.ForeignKey(
IAmPrincipal,
diff --git a/accounts/urls.py b/accounts/urls.py
index d9dc83a..c51db74 100644
--- a/accounts/urls.py
+++ b/accounts/urls.py
@@ -42,6 +42,14 @@ urlpatterns = [
path('principal/role/delete//', views.AppRoleDeleteView.as_view(), name="role_delete"),
path('customer/', views.CustomerListView.as_view(), name="customer_list"),
+ path('customer/add/', views.CustomerCreateView.as_view(), name="customer_add"),
+ path('customer/edit//', views.CustomerUpdateView.as_view(), name="customer_edit"),
+ path('customer/detail//', views.CustomerDetailView.as_view(), name="customer_detail"),
+ path('customer/transfer//', views.CustomerTransferView.as_view(), name="customer_transfer"),
+ path('customer/check-email/', views.CustomerCheckEmail.as_view(), name="customer_check_email"),
+ path('customer/download-excel-template/', views.export_excel_template, name='download_excel_template'),
+ path('customer/import-customer-data/', views.CustomerImportView.as_view(), name='import_customer_data'),
+ path('customer/export-customer-data/', views.CustomerExportView.as_view(), name='export_customer_data'),
# ignore this to path this for example setup
diff --git a/accounts/views.py b/accounts/views.py
index 6f5c89d..9e450c0 100644
--- a/accounts/views.py
+++ b/accounts/views.py
@@ -1,10 +1,12 @@
import logging
+from django.conf import settings
from django.db.models import Count, Q
from django.contrib import messages
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.views import LogoutView
from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.mixins import LoginRequiredMixin
+from django.contrib.auth.hashers import make_password
from django.contrib.auth.views import (
LoginView,
PasswordResetCompleteView,
@@ -13,6 +15,7 @@ from django.contrib.auth.views import (
PasswordResetView,
)
from django.core.exceptions import ValidationError
+from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse_lazy
from django.views import generic
@@ -20,9 +23,15 @@ from django.db import models, transaction, IntegrityError
from django.utils import timezone
from accounts import permission
from goodtimes import constants
+from goodtimes.services import EmailService
+from goodtimes.utils import JsonResponseUtil
+from manage_events.models import EventCategory, PrincipalPreference
+from manage_referrals.models import ReferralCode
+from manage_subscriptions.models import PrincipalSubscription, Subscription
from . import resource_action
from .forms import (
+ CreateCustomerForm,
CustomAuthenticationForm,
IAmPrincipalForm,
IAmPrincipalGroupRoleLinkForm,
@@ -30,9 +39,12 @@ from .forms import (
IAmPrincipalRoleAppResourceActionLinkForm,
IAmPrincipalGroupLinkForm,
ProfileEditForm,
+ UpdateCustomerForm,
+ UploadExcelForm,
)
from .models import (
IAmPrincipal,
+ IAmPrincipalExtendedData,
IAmPrincipalType,
IAmAppResourceActionLink,
IAmPrincipalGroup,
@@ -557,6 +569,193 @@ class AppRoleDeleteView(LoginRequiredMixin, generic.View):
"""Customer"""
+class CustomerCheckEmail(generic.View):
+ model = IAmPrincipal
+
+ def post(self, request, *args, **kwargs):
+ email = request.POST.get('email')
+ print("check email is cllaed ", email)
+ if self.model.objects.filter(email=email).exists():
+ print("exist called")
+ return JsonResponse({'message': 'This email address is already in use.'}, status=400)
+ else:
+ print("email is valid")
+ return JsonResponse({'message': 'Email is available.'}, status=200)
+
+
+class CustomerCreateView(LoginRequiredMixin, generic.View):
+ page_name = resource_action.RESOURCE_MANAGE_CUSTOMER
+ resource = resource_action.RESOURCE_MANAGE_CUSTOMER
+ model = IAmPrincipal
+ form_class = CreateCustomerForm
+ template_name = "accounts/customer/customer_add.html"
+ success_url = reverse_lazy("accounts:customer_list")
+ success_message = "Saved Successfully"
+ error_message = "An error occurred while saving the data."
+
+ def get_context_data(self, **kwargs):
+ context = {
+ "page_name": self.page_name,
+ "operation": "Add",
+ }
+ context.update(kwargs) # Include any additional context data passed to the view
+ return context
+
+ def get(self, request, *args, **kwargs):
+ form = self.form_class()
+ context = self.get_context_data(form=form)
+ return render(request, self.template_name, context=context)
+
+ def post(self, request, *args, **kwargs):
+ print(request.POST)
+ # return redirect(self.success_url)
+ form = self.form_class(request.POST)
+ if not form.is_valid():
+ context = self.get_context_data(form=form)
+ return render(request, self.template_name, context=context)
+ try:
+ with transaction.atomic():
+ # save principal data
+ principal_obj = IAmPrincipal.objects.create(
+ email=form.cleaned_data.get('email'),
+ first_name=form.cleaned_data.get('first_name'),
+ last_name=form.cleaned_data.get('last_name'),
+ password=make_password("goodtimes#2024"),
+ username=form.cleaned_data.get("email"),
+ email_verified=True,
+ register_complete=True,
+ principal_type=IAmPrincipalType.objects.get(name=resource_action.PRINCIPAL_TYPE_EVENT_MANAGER),
+ )
+
+ # generate referralcode of manager
+ ReferralCode.create_referral_code_for_user_manager(
+ principal=principal_obj, principal_type=principal_obj.principal_type
+ )
+
+ IAmPrincipalExtendedData.objects.create(
+ principal=principal_obj,
+ is_onboarded=True,
+ )
+
+ # save principal preferences record
+ principal_preference = PrincipalPreference.objects.create(principal=principal_obj)
+ principal_preference.preferred_categories.set(form.cleaned_data.get("preferences"))
+
+ principal_subscription= PrincipalSubscription.objects.create(
+ start_date=form.cleaned_data.get("free_start_date"),
+ end_date=form.cleaned_data.get("free_end_date"),
+ principal=principal_obj,
+ grace_period_end_date=PrincipalSubscription.generate_grace_period_end_date(form.cleaned_data.get("free_end_date")),
+ is_paid=True,
+ subscription=Subscription.objects.filter(is_free=True, active=True).first()
+ )
+
+ messages.success(self.request, constants.REGISTRATION_SUCCESS)
+ return redirect(self.success_url)
+ except Exception as e:
+ messages.error(self.request, str(e))
+ context = self.get_context_data(form=form)
+ return render(request, self.template_name, context=context)
+
+
+class CustomerUpdateView(LoginRequiredMixin, generic.View):
+ page_name = resource_action.RESOURCE_MANAGE_CUSTOMER
+ resource = resource_action.RESOURCE_MANAGE_CUSTOMER
+ model = IAmPrincipal
+ form_class = UpdateCustomerForm
+ template_name = "accounts/customer/customer_edit.html"
+ success_url = reverse_lazy("accounts:customer_list")
+ success_message = "Updated Successfully"
+ error_message = "An error occurred while saving the data."
+
+ def get_context_data(self, **kwargs):
+ context = {
+ "page_name": self.page_name,
+ "operation": "Edit",
+ }
+ context.update(kwargs) # Include any additional context data passed to the view
+ return context
+
+ def get(self, request, *args, **kwargs):
+ principal_id = kwargs.get("pk")
+ principal_obj = IAmPrincipal.objects.get(pk=principal_id)
+
+ initial_data = {
+ "first_name": principal_obj.first_name,
+ "last_name": principal_obj.last_name,
+ "email": principal_obj.email,
+ "active": principal_obj.is_active
+ }
+
+ try:
+ principal_preference = PrincipalPreference.objects.get(principal=principal_obj)
+ initial_data["preferences"] = list(principal_preference.preferred_categories.all().values_list("id", flat=True))
+ except PrincipalPreference.DoesNotExist:
+ initial_data["preferences"] = []
+
+ try:
+ subscription = PrincipalSubscription.objects.filter(principal=principal_obj).latest("created_on")
+ initial_data["free_start_date"] = subscription.start_date
+ initial_data["free_end_date"] = subscription.end_date
+ except PrincipalSubscription.DoesNotExist:
+ initial_data["free_start_date"] = None
+ initial_data["free_end_date"] = None
+
+ form = self.form_class(initial=initial_data)
+ context = self.get_context_data(form=form, principal_obj=principal_obj)
+ print("context dta is ", context)
+ return render(request, self.template_name, context=context)
+
+ def post(self, request, *args, **kwargs):
+ customer_id = kwargs.get("pk")
+ principal_obj = IAmPrincipal.objects.get(pk=customer_id)
+ form = self.form_class(request.POST)
+ if not form.is_valid():
+ context = self.get_context_data(form=form)
+ return render(request, self.template_name, context=context)
+ try:
+ with transaction.atomic():
+ # update principal data
+ principal_obj.first_name = form.cleaned_data.get('first_name')
+ principal_obj.last_name = form.cleaned_data.get('last_name')
+ principal_obj.save()
+
+ # update principal preferences record
+ principal_preference, _ = PrincipalPreference.objects.get_or_create(principal=principal_obj)
+ principal_preference.preferred_categories.set(form.cleaned_data.get("preferences"))
+
+ # update principal subscription record
+ principal_subscription = PrincipalSubscription.objects.filter(principal=principal_obj).order_by('-end_date').first()
+ if principal_subscription:
+ principal_subscription.start_date = form.cleaned_data.get("free_start_date")
+ principal_subscription.end_date = form.cleaned_data.get("free_end_date")
+ principal_subscription.grace_period_end_date = form.cleaned_data.get("free_end_date") + datetime.timedelta(days=15)
+ principal_subscription.save()
+ else:
+ PrincipalSubscription.objects.create(
+ principal=principal_obj,
+ start_date=form.cleaned_data.get("free_start_date"),
+ end_date=form.cleaned_data.get("free_end_date"),
+ grace_period_end_date=PrincipalSubscription.generate_grace_period_end_date(form.cleaned_data.get("free_end_date")),
+ is_paid=True,
+ subscription=Subscription.objects.filter().first() # Assuming you want to link a default subscription
+ )
+
+ messages.success(self.request, self.success_message)
+ return redirect(self.success_url)
+ except Exception as e:
+ messages.error(self.request, str(e))
+ context = self.get_context_data(form=form)
+ return render(request, self.template_name, context=context)
+
+class CustomerDetailView(LoginRequiredMixin, generic.DetailView):
+ template_name = 'accounts/customer/customer_detail.html'
+
+ def get(self, request, *args, **kwargs):
+ principal_obj = IAmPrincipal.objects.get(pk=kwargs.get("pk"))
+ principal_preference = PrincipalPreference.objects.get(principal_id=principal_obj.id)
+ principal_subscription = PrincipalSubscription.objects.filter(principal=principal_obj).latest("start_date")
+ return render(request, self.template_name, locals())
class CustomerListView(LoginRequiredMixin, generic.ListView):
page_name = resource_action.RESOURCE_MANAGE_CUSTOMER
@@ -570,7 +769,7 @@ class CustomerListView(LoginRequiredMixin, generic.ListView):
queryset = (
super()
.get_queryset()
- .select_related("principal_type", "principal_source")
+ .select_related("principal_type", "principal_source", "extended_data")
.filter(
models.Q(
principal_type__name=resource_action.PRINCIPAL_TYPE_EVENT_MANAGER
@@ -602,11 +801,311 @@ class CustomerListView(LoginRequiredMixin, generic.ListView):
context["page_name"] = self.page_name
return context
+import pandas as pd
+from openpyxl import Workbook, load_workbook
+from openpyxl.worksheet.datavalidation import DataValidation
+from openpyxl.styles import Font
+from django.http import HttpResponse
+
+# def export_excel_template(request):
+# # Define the columns and create an empty DataFrame
+# columns = ['First Name', 'Last Name', 'Email', 'Preferences', 'Free period start date', 'Free period end date']
+# df = pd.DataFrame(columns=columns)
+
+# # Create a workbook and select the active worksheet
+# wb = Workbook()
+# ws = wb.active
+# ws.title = 'Customer Registration'
+
+# # # Write the column headers
+# # for col_num, column_title in enumerate(df.columns, 1):
+# # cell = ws.cell(row=1, column=col_num, value=column_title)
+# # cell.font = Font(bold=True)
+
+# # # Create a hidden sheet for preferences
+# # ws_prefs = wb.create_sheet(title="Preferences")
+# # ws_prefs.sheet_state = 'hidden'
+
+# # # Fetch preferences options from the EventCategory model
+# # preferences_options = EventCategory.objects.values_list('title', flat=True)
+
+# # # Write preferences to the hidden sheet
+# # for row_num, preference in enumerate(preferences_options, 1):
+# # ws_prefs.cell(row=row_num, column=1, value=preference)
+
+# # # Define the range for preferences in the hidden sheet
+# # preferences_range = f"Preferences!$A$1:$A${len(preferences_options)}"
+
+# # # Add Data Validation for preferences (drop-down list)
+# # dv_preferences = DataValidation(
+# # type="list",
+# # formula1=preferences_range,
+# # allow_blank=True,
+# # showDropDown=True
+# # )
+# # ws.add_data_validation(dv_preferences)
+# # dv_preferences.add(f'D2:D1048576') # Apply to the whole column
+
+# # # Add Data Validation for date comparison
+# # dv_start_date = DataValidation(
+# # type="date",
+# # operator="greaterThan",
+# # formula1='"1900-01-01"',
+# # showErrorMessage=True
+# # )
+# # dv_end_date = DataValidation(
+# # type="custom",
+# # formula1="=AND(ISNUMBER(F2), F2>E2)",
+# # showErrorMessage=True,
+# # errorTitle="Invalid Date",
+# # error="End date must be greater than start date."
+# # )
+# # ws.add_data_validation(dv_start_date)
+# # ws.add_data_validation(dv_end_date)
+# # dv_start_date.add(f'E2:E1048576')
+# # dv_end_date.add(f'F2:F1048576')
+
+# # Save the workbook to a bytes buffer
+# response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
+# response['Content-Disposition'] = 'attachment; filename=customer_registration_template.xlsx'
+# wb.save(response)
+# return response
+
+
+def export_excel_template(request):
+ # Define the columns and create an empty DataFrame
+ columns = ['First Name', 'Last Name', 'Email', 'Preferences(should be seperated by comma)', 'Free period start date(YYYY-MM-DD)', 'Free period end date(YYYY-MM-DD)']
+ df = pd.DataFrame(columns=columns)
+
+ # Create a workbook and select the active worksheet
+ wb = Workbook()
+ ws = wb.active
+ ws.title = 'Customer Registration'
+
+ # Write the column headers
+ for col_num, column_title in enumerate(df.columns, 1):
+ cell = ws.cell(row=1, column=col_num, value=column_title)
+ cell.font = Font(bold=True)
+
+ # Save the workbook to a bytes buffer
+ response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
+ response['Content-Disposition'] = 'attachment; filename=customer_registration_template.xlsx'
+ wb.save(response)
+ return response
+
+class CustomerTransferView(LoginRequiredMixin, generic.View):
+ model = IAmPrincipal
+
+ def get(self, request, *args, **kwargs):
+ try:
+ principal_obj = self.model.objects.get(pk=kwargs.get("pk"))
+ except self.model.DoesNotExist:
+ messages.error(request, "Something went wrong")
+ return redirect(reverse_lazy("accounts:customer_detail"))
+
+ email_service = EmailService(
+ subject="Your Exclusive Account Access Details with Good Times!",
+ to=principal_obj.email,
+ from_email=settings.EMAIL_HOST_USER,
+ )
+
+ # Send the email
+ try:
+ temp_password="goodtimes#2024"
+ principal_obj.password = make_password(temp_password)
+ principal_obj.save()
+ email_service.load_template(
+ "accounts/customer/account_transfer_email_template.html", locals()
+ )
+ email_service.send()
+
+ principal_preference = IAmPrincipalExtendedData.objects.get(principal=principal_obj)
+ principal_preference.is_transferred = True
+ principal_preference.save()
+ messages.success(request, "Account Transfer mail send successfully")
+ except Exception as e:
+ messages.error(request, f"{str(e)}")
+
+ return redirect(reverse_lazy("accounts:customer_detail", kwargs={"pk": kwargs.get("pk")}))
+
+
+class CustomerImportView(LoginRequiredMixin, generic.View):
+ page_name = resource_action.RESOURCE_MANAGE_CUSTOMER
+ resource = resource_action.RESOURCE_MANAGE_CUSTOMER
+ action = resource_action.ACTION_READ
+ template_name = "accounts/customer/customer_bulk_template.html"
+ form_class = UploadExcelForm
+
+ def get_context_data(self, **kwargs):
+ context = {
+ "page_name": self.page_name,
+ }
+ context.update(kwargs) # Include any additional context data passed to the view
+ return context
+
+ def get(self, request, *args, **kwargs):
+ form = self.form_class()
+ context = self.get_context_data(form=form)
+ return render(request, self.template_name, context=context)
+
+ def post(self, request, *args, **kwargs):
+ form = self.form_class(request.POST, request.FILES)
+ context = self.get_context_data(form=form)
+ if not form.is_valid():
+ print(form.errors)
+ return render(request, self.template_name, context=context)
+
+ excel_file = request.FILES['file']
+
+ wb = load_workbook(filename=excel_file)
+ ws = wb.active
+
+ error_log = []
+
+ principals = []
+ preferences_l = []
+ subscriptions = []
+ principal_type = IAmPrincipalType.objects.get(name=resource_action.PRINCIPAL_TYPE_EVENT_MANAGER)
+ free_subscription = Subscription.objects.filter(is_free=True, active=True).first()
+
+ for idx, row in enumerate(ws.iter_rows(min_row=2, values_only=True), start=2):
+ first_name, last_name, email, preferences, start_date, end_date = row
+ print(f"{first_name}, {last_name, email, preferences, start_date, end_date}")
+
+ # validate all data
+ if not first_name or not last_name or not email or not preferences or not start_date or not end_date:
+ error_log.append(f"Row {idx}: Missing data.")
+ continue
+
+ # validate email existence
+ if IAmPrincipal.objects.filter(email=email).exists():
+ error_log.append(f"Row {idx}: Email {email} already exists.")
+ continue
+
+ # validate date rnage
+ if end_date < start_date:
+ error_log.append(f"Row {idx}: End date {end_date} must greater then start date {start_date}.")
+ continue
+
+ # validate preferences
+ preference_list = [pref.strip() for pref in preferences.split(',')]
+ event_categories = EventCategory.objects.filter(title__in=preference_list)
+ if len(event_categories) != len(preference_list):
+ error_log.append(f"Row {idx}: One or more preferences are invalid.")
+ continue
+
+ # collect the principals
+ principal = IAmPrincipal(
+ first_name=first_name,
+ last_name=last_name,
+ email=email,
+ password=make_password("goodtimes#2024"),
+ username=email,
+ email_verified=True,
+ register_complete=True,
+ principal_type=principal_type
+ )
+ principals.append(principal)
+
+ # Collect preferences to be set later
+ preferences_l.append((principal, event_categories, start_date, end_date))
+
+ if error_log:
+ context = self.get_context_data(form=form, error_log=error_log)
+ messages.error(request, "No recore is created check error log and fix the error in the file ")
+ return render(request, self.template_name, context=context)
+
+ # Use transaction.atomic to ensure all-or-nothing
+ with transaction.atomic():
+ # Bulk create principals
+ IAmPrincipal.objects.bulk_create(principals)
+
+ # Now we need to refresh principals from the DB to get their IDs
+ principals = IAmPrincipal.objects.filter(email__in=[p.email for p in principals])
+
+ # Create subscriptions and preferences
+ for principal, event_categories, start_date, end_date in preferences_l:
+ principal = principals.get(email=principal.email)
+
+ # Generate referral code for the manager
+ ReferralCode.create_referral_code_for_user_manager(principal=principal, principal_type=principal_type)
+
+ # Create IAmPrincipalExtendedData record
+ IAmPrincipalExtendedData.objects.create(principal=principal, is_onboarded=True)
+
+ # Create PrincipalSubscription record
+ subscription = PrincipalSubscription(
+ principal=principal,
+ start_date=start_date,
+ end_date=end_date,
+ grace_period_end_date=PrincipalSubscription.generate_grace_period_end_date(end_date),
+ is_paid=True,
+ subscription=free_subscription
+ )
+ subscriptions.append(subscription)
+
+ # Create PrincipalPreferences record
+ preference = PrincipalPreference(principal=principal)
+ preference.save()
+ preference.preferred_categories.set(event_categories)
+
+ # Bulk create subscriptions
+ PrincipalSubscription.objects.bulk_create(subscriptions)
+
+ messages.success(request, "Data imported successfully")
+ return render(request, self.template_name, context=context)
+
+
+class CustomerExportView(LoginRequiredMixin, generic.View):
+ model = IAmPrincipal
+
+ def get(self, request, *args, **kwargs):
+ princiapls = IAmPrincipal.objects.select_related("extended_data").filter(principal_type__name=resource_action.PRINCIPAL_TYPE_EVENT_MANAGER)
+
+ # prepare data for excel file
+ data = []
+ for principal in princiapls:
+ data.append([
+ principal.email,
+ principal.first_name,
+ principal.last_name,
+ str(principal.phone_no),
+ principal.email_verified,
+ principal.is_active,
+ principal.extended_data.is_onboarded if principal.extended_data else 'N/A',
+ principal.extended_data.is_transferred if principal.extended_data else 'N/A',
+ principal.created_on.replace(tzinfo=None) if principal.created_on else 'N/A'
+ ])
+
+ # Define the columns for the Excel file
+ columns = ["Email", "First Name", "Last Name", "Phone Number", "Email Verified", "Active", "Onboarde by Admin", "Transferred to Customer", "Created Date"]
+
+ # Create a workbook and select the active worksheet
+ wb = Workbook()
+ ws = wb.active
+ ws.title = "Event Managers List"
+
+ # Write the column headers
+ for col_num, column_title in enumerate(columns, 1):
+ cell = ws.cell(row=1, column=col_num, value=column_title)
+ cell.font = Font(bold=True)
+
+ # write the data rows
+
+ for row_num, row_data in enumerate(data, 2):
+ for col_num, cell_value in enumerate(row_data, 1):
+ ws.cell(row=row_num, column=col_num, value=cell_value)
+
+ response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
+ response['Content-Disposition'] = 'attachment; filename=event_managers.xlsx'
+ wb.save(response)
+
+ return response
+
class DatatableListView(LoginRequiredMixin, generic.ListView):
pass
-
class PrincipalProfileView(LoginRequiredMixin, generic.ListView):
page_name = resource_action.RESOURCE_MANAGE_DASHBOARD
model = IAmPrincipal
diff --git a/goodtimes/mixins.py b/goodtimes/mixins.py
new file mode 100644
index 0000000..af0d2ce
--- /dev/null
+++ b/goodtimes/mixins.py
@@ -0,0 +1,32 @@
+from django.views import generic
+from goodtimes.utils import JsonResponseUtil
+
+
+class ActionMixin(generic.View):
+ model = None
+
+ def post(self, request, *args, **kwargs):
+
+ if self.model is None:
+ raise NotImplementedError("Subclasses of BaseActionView must define a 'model' attribute.")
+
+ action = request.POST.get('action') # 'archive', 'active', or 'unarchive'
+ ids = request.POST.getlist('ids[]') # List of IDs to perform action on
+ active = request.POST.get('active')
+ print(f"arhive action {action} and id is {ids} and active data is {active}")
+ if action == 'archive':
+ # Update 'deleted' field to True for the selected users
+ self.model.objects.filter(id__in=ids).update(deleted=True, active=False)
+ message = 'Record archived successfully.'
+ elif action == 'active':
+ # Update 'active' field to True for the selected users
+ self.model.objects.filter(id__in=ids).update(active=active.capitalize())
+ message = 'Record updated successfully.'
+ elif action == 'unarchive':
+ # Update 'deleted' field to False for the selected users
+ self.model.objects.filter(id__in=ids).update(deleted=False)
+ message = 'Record unarchived successfully.'
+ else:
+ return JsonResponseUtil.error(message="Invalid Action")
+
+ return JsonResponseUtil.success(message=message)
\ No newline at end of file
diff --git a/goodtimes/utils.py b/goodtimes/utils.py
index d56def8..9fd4742 100644
--- a/goodtimes/utils.py
+++ b/goodtimes/utils.py
@@ -1,3 +1,4 @@
+from django.http import JsonResponse
from rest_framework.response import Response
from rest_framework.renderers import JSONRenderer
from rest_framework import status
@@ -40,6 +41,26 @@ class ApiResponse:
# return ApiResponse.error("Validation error", errors, status_code)
+class JsonResponseUtil:
+ """
+ A utility class for creating JSON responses with a standardized format.
+ """
+ @staticmethod
+ def success(message, data=None, status=200):
+ response_data = {"success": True, "status": status, "message": message}
+ if data is not None:
+ response_data["data"] = data
+ return JsonResponse(response_data, status=status)
+
+ @staticmethod
+ def error(message, errors=None, status=403):
+ response_data = {"success": False, "status": status, "message": message}
+ if errors is not None:
+ response_data["errors"] = errors
+ return JsonResponse(response_data, status=status)
+
+
+
class RandomGenerator:
@staticmethod
def number(start, end):
diff --git a/manage_events/api/views.py b/manage_events/api/views.py
index ac54d17..b5c2a79 100644
--- a/manage_events/api/views.py
+++ b/manage_events/api/views.py
@@ -54,7 +54,7 @@ class CreateEventApi(APIView):
serializer = CreateEventSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
- serializer.save(created_by=self.request.user)
+ serializer.save(created_by=self.request.user, principal=self.request.user)
# Add additional logic for handling other relationships (e.g., Venue)
return ApiResponse.success(
@@ -122,7 +122,7 @@ class CreateVenueApi(APIView):
serializer = VenueSerializer(data=data, context={"request": request})
serializer.is_valid(raise_exception=True)
- serializer.save(created_by=self.request.user, active=True)
+ serializer.save(created_by=self.request.user, principal=self.request.user, active=True)
# Add additional logic for handling other relationships (e.g., Venue)
return ApiResponse.success(
@@ -688,7 +688,7 @@ class EventFilterByLocationAPIView(APIView):
)
max_distance_km = 10 # Set your desired maximum distance
- current_and_future_events_query = Q(active=True, deleted=False, draft=False) & (
+ current_and_future_events_query = Q(active=True, deleted=False, draft=False, created_by__is_active=True,) & (
Q(end_date__gte=today)
)
@@ -714,7 +714,7 @@ class EventFilterByLocationAPIView(APIView):
venues_within_range.append(venue.id)
print("venues_within_range: ", venues_within_range)
# venues_data = [venue_to_dict(venue) for venue in venues_within_range]
- events = Event.objects.filter(venue__id__in=venues_within_range)
+ events = events_queryset.filter(venue__id__in=venues_within_range)
# Serialize and return the filtered events
serializer = EventDetailSerializer(
diff --git a/manage_events/forms.py b/manage_events/forms.py
index 01924b8..71c80a9 100644
--- a/manage_events/forms.py
+++ b/manage_events/forms.py
@@ -1,4 +1,5 @@
from django import forms
+from accounts.models import IAmPrincipal, IAmPrincipalExtendedData
from manage_events.models import EventMaster, Event, EventCategory, Venue
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
@@ -16,9 +17,25 @@ class EventCategoryForm(forms.ModelForm):
class EventForm(forms.ModelForm):
+ principal = forms.ModelChoiceField(
+ queryset=IAmPrincipal.objects.select_related("extended_data").filter(
+ extended_data__is_onboarded=True,
+ extended_data__is_transferred=False
+ ),
+ label="Non-transfer user list",
+ required=True
+ )
+ venue = forms.ModelChoiceField(
+ queryset=Venue.objects.filter(
+ active=True
+ ),
+ label="venue",
+ required=True
+ )
class Meta:
model = Event
fields = [
+ "principal",
"title",
# "event_master",
"description",
@@ -36,6 +53,8 @@ class EventForm(forms.ModelForm):
"entry_fee",
"key_guest",
"age_group",
+ "coupon_code",
+ "coupon_description",
"tags",
"draft",
"active",
@@ -101,9 +120,19 @@ class EventMasterForm(forms.ModelForm):
class VenueForm(forms.ModelForm):
+ principal = forms.ModelChoiceField(
+ queryset=IAmPrincipal.objects.select_related("extended_data").filter(
+ extended_data__is_onboarded=True,
+ extended_data__is_transferred=False
+ ),
+ label="Non-transfer user list",
+ required=True
+ )
+
class Meta:
model = Venue
fields = [
+ "principal",
"title",
"description",
"address",
diff --git a/manage_events/migrations/0013_venue_principal.py b/manage_events/migrations/0013_venue_principal.py
new file mode 100644
index 0000000..6a32b53
--- /dev/null
+++ b/manage_events/migrations/0013_venue_principal.py
@@ -0,0 +1,21 @@
+# Generated by Django 5.0.2 on 2024-06-25 17:04
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('manage_events', '0012_event_coupon_code_event_coupon_description'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='venue',
+ name='principal',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='venues_principal', to=settings.AUTH_USER_MODEL),
+ ),
+ ]
diff --git a/manage_events/migrations/0014_event_principal.py b/manage_events/migrations/0014_event_principal.py
new file mode 100644
index 0000000..15109c4
--- /dev/null
+++ b/manage_events/migrations/0014_event_principal.py
@@ -0,0 +1,21 @@
+# Generated by Django 5.0.2 on 2024-06-25 17:09
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('manage_events', '0013_venue_principal'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='event',
+ name='principal',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='events_principal', to=settings.AUTH_USER_MODEL),
+ ),
+ ]
diff --git a/manage_events/models.py b/manage_events/models.py
index 6811f2f..54619dc 100644
--- a/manage_events/models.py
+++ b/manage_events/models.py
@@ -18,6 +18,7 @@ class EventCategory(BaseModel):
class Venue(BaseModel):
+ principal = models.ForeignKey(IAmPrincipal, related_name="venues_principal", on_delete=models.CASCADE, null=True)
title = models.CharField(max_length=255)
description = models.TextField(null=True, blank=True)
address = models.TextField(null=True, blank=True)
@@ -59,6 +60,7 @@ class Event(BaseModel):
("free", "Free"),
("paid", "Paid"),
]
+ principal = models.ForeignKey(IAmPrincipal, related_name="events_principal", on_delete=models.CASCADE, null=True)
title = models.CharField(max_length=255)
category = models.ForeignKey(EventCategory, on_delete=models.CASCADE)
event_master = models.ForeignKey(
@@ -144,7 +146,7 @@ class PrincipalPreference(BaseModel):
)
def __str__(self):
- return str(self.preferred_categories)
+ return str(self.preferred_categories.name)
class Meta:
db_table = "user_preference"
diff --git a/manage_events/views.py b/manage_events/views.py
index 080e7bc..56b42a5 100644
--- a/manage_events/views.py
+++ b/manage_events/views.py
@@ -281,7 +281,9 @@ class EventCreateOrUpdateView(LoginRequiredMixin, generic.View):
print(form.errors)
context = self.get_context_data(form=form)
return render(request, self.template_name, context=context)
- form.save()
+ instance = form.save()
+ instance.created_by = form.cleaned_data.get("principal")
+ instance.save()
messages.success(self.request, self.get_success_message())
return redirect(self.success_url)
@@ -414,6 +416,7 @@ class VenueCreateOrUpdateView(LoginRequiredMixin, generic.View):
def get(self, request, *args, **kwargs):
self.object = self.get_object()
+ print(f"self.object is {self.object}")
# If an object is found, change action to ACTION_UPDATE
if self.object is not None:
@@ -425,6 +428,7 @@ class VenueCreateOrUpdateView(LoginRequiredMixin, generic.View):
def post(self, request, *args, **kwargs):
self.object = self.get_object()
+ print(f"form data is {request.POST} and self.object is {self.object}")
# If an object is found, change action to ACTION_UPDATE
if self.object is not None:
@@ -435,7 +439,10 @@ class VenueCreateOrUpdateView(LoginRequiredMixin, generic.View):
print(form.errors)
context = self.get_context_data(form=form)
return render(request, self.template_name, context=context)
- form.save()
+
+ instance = form.save()
+ instance.created_by = form.cleaned_data.get("principal")
+ instance.save()
messages.success(self.request, self.get_success_message())
return redirect(self.success_url)
diff --git a/manage_subscriptions/forms.py b/manage_subscriptions/forms.py
index 064a35d..79ed3ac 100644
--- a/manage_subscriptions/forms.py
+++ b/manage_subscriptions/forms.py
@@ -30,6 +30,7 @@ class SubscriptionForm(forms.ModelForm):
"referral_percentage",
"active",
"deleted",
+ "is_free",
] # Include all fields you want from the model
diff --git a/manage_subscriptions/migrations/0008_subscription_is_free.py b/manage_subscriptions/migrations/0008_subscription_is_free.py
new file mode 100644
index 0000000..d86bd47
--- /dev/null
+++ b/manage_subscriptions/migrations/0008_subscription_is_free.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.2 on 2024-06-25 07:28
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('manage_subscriptions', '0007_alter_subscription_referral_percentage'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='subscription',
+ name='is_free',
+ field=models.BooleanField(default=False, help_text='Indicates whether this subscription is free and only accessible by administrators, not visible to regular users.'),
+ ),
+ ]
diff --git a/manage_subscriptions/models.py b/manage_subscriptions/models.py
index ae99b31..326f7df 100644
--- a/manage_subscriptions/models.py
+++ b/manage_subscriptions/models.py
@@ -1,3 +1,4 @@
+from datetime import timedelta, timezone
from django.db import models
from accounts.models import BaseModel, IAmPrincipal, IAmPrincipalType
from django.utils.translation import gettext_lazy as _
@@ -30,6 +31,7 @@ class Subscription(BaseModel):
IAmPrincipalType, related_name="principal_type_subscriptions", blank=True
)
referral_percentage = models.DecimalField(max_digits=5, decimal_places=2)
+ is_free = models.BooleanField(default=False, help_text="Indicates whether this subscription is free and only accessible by administrators, not visible to regular users.")
class Meta:
db_table = "subscription"
@@ -75,6 +77,12 @@ class PrincipalSubscription(BaseModel):
def __str__(self):
return f"{self.subscription} - {self.principal.first_name}"
+
+ def generate_order_id(email):
+ return f"order_{str(timezone.localtime().timestamp())}{str(email)}"
+
+ def generate_grace_period_end_date(date):
+ return date + timedelta(days=15)
class WebhookEvent(BaseModel):
diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py
index 480e4e0..0050df9 100644
--- a/manage_subscriptions/views.py
+++ b/manage_subscriptions/views.py
@@ -37,6 +37,7 @@ from django.views.generic.base import TemplateView
# Create your views here.
+from django.db.models import Q
class SubscriptionCreateOrUpdateView(LoginRequiredMixin, generic.View):
# Set the page_name and resource
@@ -96,6 +97,14 @@ class SubscriptionCreateOrUpdateView(LoginRequiredMixin, generic.View):
print(form.errors)
context = self.get_context_data(form=form)
return render(request, self.template_name, context=context)
+
+ # This code ensures that only one free plan can be created by checking for existing free plans before saving a new one.
+ if form.cleaned_data.get("is_free"):
+ if self.model.objects.filter(Q(is_free=True) & Q(active=True)).exists:
+ messages.error(self.request, "A free plan is already available. Please deactivate the existing one before creating a new one.")
+ context = self.get_context_data(form=form)
+ return render(request, self.template_name, context=context)
+
form.save()
messages.success(self.request, self.get_success_message())
return redirect(self.success_url)
@@ -386,7 +395,7 @@ class SubscriptionPageView(TemplateView):
if request.user.is_authenticated:
print("request.user: ", request.user)
subscriptions = Subscription.objects.filter(
- principal_types=request.user.principal_type, active=True, deleted=False
+ principal_types=request.user.principal_type, active=True, deleted=False, is_free=False
)
if subscriptions.exists():
diff --git a/static/src/plugins/src/jquery-validate/jquery.validate.min.js b/static/src/plugins/src/jquery-validate/jquery.validate.min.js
new file mode 100644
index 0000000..7f5f510
--- /dev/null
+++ b/static/src/plugins/src/jquery-validate/jquery.validate.min.js
@@ -0,0 +1,4 @@
+/*! jQuery Validation Plugin - v1.19.3 - 1/9/2021
+ * https://jqueryvalidation.org/
+ * Copyright (c) 2021 Jörn Zaefferer; Licensed MIT */
+!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof module&&module.exports?module.exports=a(require("jquery")):a(jQuery)}(function(a){a.extend(a.fn,{validate:function(b){if(!this.length)return void(b&&b.debug&&window.console&&console.warn("Nothing selected, can't validate, returning nothing."));var c=a.data(this[0],"validator");return c?c:(this.attr("novalidate","novalidate"),c=new a.validator(b,this[0]),a.data(this[0],"validator",c),c.settings.onsubmit&&(this.on("click.validate",":submit",function(b){c.submitButton=b.currentTarget,a(this).hasClass("cancel")&&(c.cancelSubmit=!0),void 0!==a(this).attr("formnovalidate")&&(c.cancelSubmit=!0)}),this.on("submit.validate",function(b){function d(){var d,e;return c.submitButton&&(c.settings.submitHandler||c.formSubmitted)&&(d=a("").attr("name",c.submitButton.name).val(a(c.submitButton).val()).appendTo(c.currentForm)),!(c.settings.submitHandler&&!c.settings.debug)||(e=c.settings.submitHandler.call(c,c.currentForm,b),d&&d.remove(),void 0!==e&&e)}return c.settings.debug&&b.preventDefault(),c.cancelSubmit?(c.cancelSubmit=!1,d()):c.form()?c.pendingRequest?(c.formSubmitted=!0,!1):d():(c.focusInvalid(),!1)})),c)},valid:function(){var b,c,d;return a(this[0]).is("form")?b=this.validate().form():(d=[],b=!0,c=a(this[0].form).validate(),this.each(function(){b=c.element(this)&&b,b||(d=d.concat(c.errorList))}),c.errorList=d),b},rules:function(b,c){var d,e,f,g,h,i,j=this[0],k="undefined"!=typeof this.attr("contenteditable")&&"false"!==this.attr("contenteditable");if(null!=j&&(!j.form&&k&&(j.form=this.closest("form")[0],j.name=this.attr("name")),null!=j.form)){if(b)switch(d=a.data(j.form,"validator").settings,e=d.rules,f=a.validator.staticRules(j),b){case"add":a.extend(f,a.validator.normalizeRule(c)),delete f.messages,e[j.name]=f,c.messages&&(d.messages[j.name]=a.extend(d.messages[j.name],c.messages));break;case"remove":return c?(i={},a.each(c.split(/\s/),function(a,b){i[b]=f[b],delete f[b]}),i):(delete e[j.name],f)}return g=a.validator.normalizeRules(a.extend({},a.validator.classRules(j),a.validator.attributeRules(j),a.validator.dataRules(j),a.validator.staticRules(j)),j),g.required&&(h=g.required,delete g.required,g=a.extend({required:h},g)),g.remote&&(h=g.remote,delete g.remote,g=a.extend(g,{remote:h})),g}}});var b=function(a){return a.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")};a.extend(a.expr.pseudos||a.expr[":"],{blank:function(c){return!b(""+a(c).val())},filled:function(c){var d=a(c).val();return null!==d&&!!b(""+d)},unchecked:function(b){return!a(b).prop("checked")}}),a.validator=function(b,c){this.settings=a.extend(!0,{},a.validator.defaults,b),this.currentForm=c,this.init()},a.validator.format=function(b,c){return 1===arguments.length?function(){var c=a.makeArray(arguments);return c.unshift(b),a.validator.format.apply(this,c)}:void 0===c?b:(arguments.length>2&&c.constructor!==Array&&(c=a.makeArray(arguments).slice(1)),c.constructor!==Array&&(c=[c]),a.each(c,function(a,c){b=b.replace(new RegExp("\\{"+a+"\\}","g"),function(){return c})}),b)},a.extend(a.validator,{defaults:{messages:{},groups:{},rules:{},errorClass:"error",pendingClass:"pending",validClass:"valid",errorElement:"label",focusCleanup:!1,focusInvalid:!0,errorContainer:a([]),errorLabelContainer:a([]),onsubmit:!0,ignore:":hidden",ignoreTitle:!1,onfocusin:function(a){this.lastActive=a,this.settings.focusCleanup&&(this.settings.unhighlight&&this.settings.unhighlight.call(this,a,this.settings.errorClass,this.settings.validClass),this.hideThese(this.errorsFor(a)))},onfocusout:function(a){this.checkable(a)||!(a.name in this.submitted)&&this.optional(a)||this.element(a)},onkeyup:function(b,c){var d=[16,17,18,20,35,36,37,38,39,40,45,144,225];9===c.which&&""===this.elementValue(b)||a.inArray(c.keyCode,d)!==-1||(b.name in this.submitted||b.name in this.invalid)&&this.element(b)},onclick:function(a){a.name in this.submitted?this.element(a):a.parentNode.name in this.submitted&&this.element(a.parentNode)},highlight:function(b,c,d){"radio"===b.type?this.findByName(b.name).addClass(c).removeClass(d):a(b).addClass(c).removeClass(d)},unhighlight:function(b,c,d){"radio"===b.type?this.findByName(b.name).removeClass(c).addClass(d):a(b).removeClass(c).addClass(d)}},setDefaults:function(b){a.extend(a.validator.defaults,b)},messages:{required:"This field is required.",remote:"Please fix this field.",email:"Please enter a valid email address.",url:"Please enter a valid URL.",date:"Please enter a valid date.",dateISO:"Please enter a valid date (ISO).",number:"Please enter a valid number.",digits:"Please enter only digits.",equalTo:"Please enter the same value again.",maxlength:a.validator.format("Please enter no more than {0} characters."),minlength:a.validator.format("Please enter at least {0} characters."),rangelength:a.validator.format("Please enter a value between {0} and {1} characters long."),range:a.validator.format("Please enter a value between {0} and {1}."),max:a.validator.format("Please enter a value less than or equal to {0}."),min:a.validator.format("Please enter a value greater than or equal to {0}."),step:a.validator.format("Please enter a multiple of {0}.")},autoCreateRanges:!1,prototype:{init:function(){function b(b){var c="undefined"!=typeof a(this).attr("contenteditable")&&"false"!==a(this).attr("contenteditable");if(!this.form&&c&&(this.form=a(this).closest("form")[0],this.name=a(this).attr("name")),d===this.form){var e=a.data(this.form,"validator"),f="on"+b.type.replace(/^validate/,""),g=e.settings;g[f]&&!a(this).is(g.ignore)&&g[f].call(e,this,b)}}this.labelContainer=a(this.settings.errorLabelContainer),this.errorContext=this.labelContainer.length&&this.labelContainer||a(this.currentForm),this.containers=a(this.settings.errorContainer).add(this.settings.errorLabelContainer),this.submitted={},this.valueCache={},this.pendingRequest=0,this.pending={},this.invalid={},this.reset();var c,d=this.currentForm,e=this.groups={};a.each(this.settings.groups,function(b,c){"string"==typeof c&&(c=c.split(/\s/)),a.each(c,function(a,c){e[c]=b})}),c=this.settings.rules,a.each(c,function(b,d){c[b]=a.validator.normalizeRule(d)}),a(this.currentForm).on("focusin.validate focusout.validate keyup.validate",":text, [type='password'], [type='file'], select, textarea, [type='number'], [type='search'], [type='tel'], [type='url'], [type='email'], [type='datetime'], [type='date'], [type='month'], [type='week'], [type='time'], [type='datetime-local'], [type='range'], [type='color'], [type='radio'], [type='checkbox'], [contenteditable], [type='button']",b).on("click.validate","select, option, [type='radio'], [type='checkbox']",b),this.settings.invalidHandler&&a(this.currentForm).on("invalid-form.validate",this.settings.invalidHandler)},form:function(){return this.checkForm(),a.extend(this.submitted,this.errorMap),this.invalid=a.extend({},this.errorMap),this.valid()||a(this.currentForm).triggerHandler("invalid-form",[this]),this.showErrors(),this.valid()},checkForm:function(){this.prepareForm();for(var a=0,b=this.currentElements=this.elements();b[a];a++)this.check(b[a]);return this.valid()},element:function(b){var c,d,e=this.clean(b),f=this.validationTargetFor(e),g=this,h=!0;return void 0===f?delete this.invalid[e.name]:(this.prepareElement(f),this.currentElements=a(f),d=this.groups[f.name],d&&a.each(this.groups,function(a,b){b===d&&a!==f.name&&(e=g.validationTargetFor(g.clean(g.findByName(a))),e&&e.name in g.invalid&&(g.currentElements.push(e),h=g.check(e)&&h))}),c=this.check(f)!==!1,h=h&&c,c?this.invalid[f.name]=!1:this.invalid[f.name]=!0,this.numberOfInvalids()||(this.toHide=this.toHide.add(this.containers)),this.showErrors(),a(b).attr("aria-invalid",!c)),h},showErrors:function(b){if(b){var c=this;a.extend(this.errorMap,b),this.errorList=a.map(this.errorMap,function(a,b){return{message:a,element:c.findByName(b)[0]}}),this.successList=a.grep(this.successList,function(a){return!(a.name in b)})}this.settings.showErrors?this.settings.showErrors.call(this,this.errorMap,this.errorList):this.defaultShowErrors()},resetForm:function(){a.fn.resetForm&&a(this.currentForm).resetForm(),this.invalid={},this.submitted={},this.prepareForm(),this.hideErrors();var b=this.elements().removeData("previousValue").removeAttr("aria-invalid");this.resetElements(b)},resetElements:function(a){var b;if(this.settings.unhighlight)for(b=0;a[b];b++)this.settings.unhighlight.call(this,a[b],this.settings.errorClass,""),this.findByName(a[b].name).removeClass(this.settings.validClass);else a.removeClass(this.settings.errorClass).removeClass(this.settings.validClass)},numberOfInvalids:function(){return this.objectLength(this.invalid)},objectLength:function(a){var b,c=0;for(b in a)void 0!==a[b]&&null!==a[b]&&a[b]!==!1&&c++;return c},hideErrors:function(){this.hideThese(this.toHide)},hideThese:function(a){a.not(this.containers).text(""),this.addWrapper(a).hide()},valid:function(){return 0===this.size()},size:function(){return this.errorList.length},focusInvalid:function(){if(this.settings.focusInvalid)try{a(this.findLastActive()||this.errorList.length&&this.errorList[0].element||[]).filter(":visible").trigger("focus").trigger("focusin")}catch(b){}},findLastActive:function(){var b=this.lastActive;return b&&1===a.grep(this.errorList,function(a){return a.element.name===b.name}).length&&b},elements:function(){var b=this,c={};return a(this.currentForm).find("input, select, textarea, [contenteditable]").not(":submit, :reset, :image, :disabled").not(this.settings.ignore).filter(function(){var d=this.name||a(this).attr("name"),e="undefined"!=typeof a(this).attr("contenteditable")&&"false"!==a(this).attr("contenteditable");return!d&&b.settings.debug&&window.console&&console.error("%o has no name assigned",this),e&&(this.form=a(this).closest("form")[0],this.name=d),this.form===b.currentForm&&(!(d in c||!b.objectLength(a(this).rules()))&&(c[d]=!0,!0))})},clean:function(b){return a(b)[0]},errors:function(){var b=this.settings.errorClass.split(" ").join(".");return a(this.settings.errorElement+"."+b,this.errorContext)},resetInternals:function(){this.successList=[],this.errorList=[],this.errorMap={},this.toShow=a([]),this.toHide=a([])},reset:function(){this.resetInternals(),this.currentElements=a([])},prepareForm:function(){this.reset(),this.toHide=this.errors().add(this.containers)},prepareElement:function(a){this.reset(),this.toHide=this.errorsFor(a)},elementValue:function(b){var c,d,e=a(b),f=b.type,g="undefined"!=typeof e.attr("contenteditable")&&"false"!==e.attr("contenteditable");return"radio"===f||"checkbox"===f?this.findByName(b.name).filter(":checked").val():"number"===f&&"undefined"!=typeof b.validity?b.validity.badInput?"NaN":e.val():(c=g?e.text():e.val(),"file"===f?"C:\\fakepath\\"===c.substr(0,12)?c.substr(12):(d=c.lastIndexOf("/"),d>=0?c.substr(d+1):(d=c.lastIndexOf("\\"),d>=0?c.substr(d+1):c)):"string"==typeof c?c.replace(/\r/g,""):c)},check:function(b){b=this.validationTargetFor(this.clean(b));var c,d,e,f,g=a(b).rules(),h=a.map(g,function(a,b){return b}).length,i=!1,j=this.elementValue(b);"function"==typeof g.normalizer?f=g.normalizer:"function"==typeof this.settings.normalizer&&(f=this.settings.normalizer),f&&(j=f.call(b,j),delete g.normalizer);for(d in g){e={method:d,parameters:g[d]};try{if(c=a.validator.methods[d].call(this,j,b,e.parameters),"dependency-mismatch"===c&&1===h){i=!0;continue}if(i=!1,"pending"===c)return void(this.toHide=this.toHide.not(this.errorsFor(b)));if(!c)return this.formatAndAdd(b,e),!1}catch(k){throw this.settings.debug&&window.console&&console.log("Exception occurred when checking element "+b.id+", check the '"+e.method+"' method.",k),k instanceof TypeError&&(k.message+=". Exception occurred when checking element "+b.id+", check the '"+e.method+"' method."),k}}if(!i)return this.objectLength(g)&&this.successList.push(b),!0},customDataMessage:function(b,c){return a(b).data("msg"+c.charAt(0).toUpperCase()+c.substring(1).toLowerCase())||a(b).data("msg")},customMessage:function(a,b){var c=this.settings.messages[a];return c&&(c.constructor===String?c:c[b])},findDefined:function(){for(var a=0;aWarning: No message defined for "+b.name+""),e=/\$?\{(\d+)\}/g;return"function"==typeof d?d=d.call(this,c.parameters,b):e.test(d)&&(d=a.validator.format(d.replace(e,"{$1}"),c.parameters)),d},formatAndAdd:function(a,b){var c=this.defaultMessage(a,b);this.errorList.push({message:c,element:a,method:b.method}),this.errorMap[a.name]=c,this.submitted[a.name]=c},addWrapper:function(a){return this.settings.wrapper&&(a=a.add(a.parent(this.settings.wrapper))),a},defaultShowErrors:function(){var a,b,c;for(a=0;this.errorList[a];a++)c=this.errorList[a],this.settings.highlight&&this.settings.highlight.call(this,c.element,this.settings.errorClass,this.settings.validClass),this.showLabel(c.element,c.message);if(this.errorList.length&&(this.toShow=this.toShow.add(this.containers)),this.settings.success)for(a=0;this.successList[a];a++)this.showLabel(this.successList[a]);if(this.settings.unhighlight)for(a=0,b=this.validElements();b[a];a++)this.settings.unhighlight.call(this,b[a],this.settings.errorClass,this.settings.validClass);this.toHide=this.toHide.not(this.toShow),this.hideErrors(),this.addWrapper(this.toShow).show()},validElements:function(){return this.currentElements.not(this.invalidElements())},invalidElements:function(){return a(this.errorList).map(function(){return this.element})},showLabel:function(b,c){var d,e,f,g,h=this.errorsFor(b),i=this.idOrName(b),j=a(b).attr("aria-describedby");h.length?(h.removeClass(this.settings.validClass).addClass(this.settings.errorClass),h.html(c)):(h=a("<"+this.settings.errorElement+">").attr("id",i+"-error").addClass(this.settings.errorClass).html(c||""),d=h,this.settings.wrapper&&(d=h.hide().show().wrap("<"+this.settings.wrapper+"/>").parent()),this.labelContainer.length?this.labelContainer.append(d):this.settings.errorPlacement?this.settings.errorPlacement.call(this,d,a(b)):d.insertAfter(b),h.is("label")?h.attr("for",i):0===h.parents("label[for='"+this.escapeCssMeta(i)+"']").length&&(f=h.attr("id"),j?j.match(new RegExp("\\b"+this.escapeCssMeta(f)+"\\b"))||(j+=" "+f):j=f,a(b).attr("aria-describedby",j),e=this.groups[b.name],e&&(g=this,a.each(g.groups,function(b,c){c===e&&a("[name='"+g.escapeCssMeta(b)+"']",g.currentForm).attr("aria-describedby",h.attr("id"))})))),!c&&this.settings.success&&(h.text(""),"string"==typeof this.settings.success?h.addClass(this.settings.success):this.settings.success(h,b)),this.toShow=this.toShow.add(h)},errorsFor:function(b){var c=this.escapeCssMeta(this.idOrName(b)),d=a(b).attr("aria-describedby"),e="label[for='"+c+"'], label[for='"+c+"'] *";return d&&(e=e+", #"+this.escapeCssMeta(d).replace(/\s+/g,", #")),this.errors().filter(e)},escapeCssMeta:function(a){return a.replace(/([\\!"#$%&'()*+,.\/:;<=>?@\[\]^`{|}~])/g,"\\$1")},idOrName:function(a){return this.groups[a.name]||(this.checkable(a)?a.name:a.id||a.name)},validationTargetFor:function(b){return this.checkable(b)&&(b=this.findByName(b.name)),a(b).not(this.settings.ignore)[0]},checkable:function(a){return/radio|checkbox/i.test(a.type)},findByName:function(b){return a(this.currentForm).find("[name='"+this.escapeCssMeta(b)+"']")},getLength:function(b,c){switch(c.nodeName.toLowerCase()){case"select":return a("option:selected",c).length;case"input":if(this.checkable(c))return this.findByName(c.name).filter(":checked").length}return b.length},depend:function(a,b){return!this.dependTypes[typeof a]||this.dependTypes[typeof a](a,b)},dependTypes:{"boolean":function(a){return a},string:function(b,c){return!!a(b,c.form).length},"function":function(a,b){return a(b)}},optional:function(b){var c=this.elementValue(b);return!a.validator.methods.required.call(this,c,b)&&"dependency-mismatch"},startRequest:function(b){this.pending[b.name]||(this.pendingRequest++,a(b).addClass(this.settings.pendingClass),this.pending[b.name]=!0)},stopRequest:function(b,c){this.pendingRequest--,this.pendingRequest<0&&(this.pendingRequest=0),delete this.pending[b.name],a(b).removeClass(this.settings.pendingClass),c&&0===this.pendingRequest&&this.formSubmitted&&this.form()?(a(this.currentForm).submit(),this.submitButton&&a("input:hidden[name='"+this.submitButton.name+"']",this.currentForm).remove(),this.formSubmitted=!1):!c&&0===this.pendingRequest&&this.formSubmitted&&(a(this.currentForm).triggerHandler("invalid-form",[this]),this.formSubmitted=!1)},previousValue:function(b,c){return c="string"==typeof c&&c||"remote",a.data(b,"previousValue")||a.data(b,"previousValue",{old:null,valid:!0,message:this.defaultMessage(b,{method:c})})},destroy:function(){this.resetForm(),a(this.currentForm).off(".validate").removeData("validator").find(".validate-equalTo-blur").off(".validate-equalTo").removeClass("validate-equalTo-blur").find(".validate-lessThan-blur").off(".validate-lessThan").removeClass("validate-lessThan-blur").find(".validate-lessThanEqual-blur").off(".validate-lessThanEqual").removeClass("validate-lessThanEqual-blur").find(".validate-greaterThanEqual-blur").off(".validate-greaterThanEqual").removeClass("validate-greaterThanEqual-blur").find(".validate-greaterThan-blur").off(".validate-greaterThan").removeClass("validate-greaterThan-blur")}},classRuleSettings:{required:{required:!0},email:{email:!0},url:{url:!0},date:{date:!0},dateISO:{dateISO:!0},number:{number:!0},digits:{digits:!0},creditcard:{creditcard:!0}},addClassRules:function(b,c){b.constructor===String?this.classRuleSettings[b]=c:a.extend(this.classRuleSettings,b)},classRules:function(b){var c={},d=a(b).attr("class");return d&&a.each(d.split(" "),function(){this in a.validator.classRuleSettings&&a.extend(c,a.validator.classRuleSettings[this])}),c},normalizeAttributeRule:function(a,b,c,d){/min|max|step/.test(c)&&(null===b||/number|range|text/.test(b))&&(d=Number(d),isNaN(d)&&(d=void 0)),d||0===d?a[c]=d:b===c&&"range"!==b&&(a[c]=!0)},attributeRules:function(b){var c,d,e={},f=a(b),g=b.getAttribute("type");for(c in a.validator.methods)"required"===c?(d=b.getAttribute(c),""===d&&(d=!0),d=!!d):d=f.attr(c),this.normalizeAttributeRule(e,g,c,d);return e.maxlength&&/-1|2147483647|524288/.test(e.maxlength)&&delete e.maxlength,e},dataRules:function(b){var c,d,e={},f=a(b),g=b.getAttribute("type");for(c in a.validator.methods)d=f.data("rule"+c.charAt(0).toUpperCase()+c.substring(1).toLowerCase()),""===d&&(d=!0),this.normalizeAttributeRule(e,g,c,d);return e},staticRules:function(b){var c={},d=a.data(b.form,"validator");return d.settings.rules&&(c=a.validator.normalizeRule(d.settings.rules[b.name])||{}),c},normalizeRules:function(b,c){return a.each(b,function(d,e){if(e===!1)return void delete b[d];if(e.param||e.depends){var f=!0;switch(typeof e.depends){case"string":f=!!a(e.depends,c.form).length;break;case"function":f=e.depends.call(c,c)}f?b[d]=void 0===e.param||e.param:(a.data(c.form,"validator").resetElements(a(c)),delete b[d])}}),a.each(b,function(a,d){b[a]="function"==typeof d&&"normalizer"!==a?d(c):d}),a.each(["minlength","maxlength"],function(){b[this]&&(b[this]=Number(b[this]))}),a.each(["rangelength","range"],function(){var a;b[this]&&(Array.isArray(b[this])?b[this]=[Number(b[this][0]),Number(b[this][1])]:"string"==typeof b[this]&&(a=b[this].replace(/[\[\]]/g,"").split(/[\s,]+/),b[this]=[Number(a[0]),Number(a[1])]))}),a.validator.autoCreateRanges&&(null!=b.min&&null!=b.max&&(b.range=[b.min,b.max],delete b.min,delete b.max),null!=b.minlength&&null!=b.maxlength&&(b.rangelength=[b.minlength,b.maxlength],delete b.minlength,delete b.maxlength)),b},normalizeRule:function(b){if("string"==typeof b){var c={};a.each(b.split(/\s/),function(){c[this]=!0}),b=c}return b},addMethod:function(b,c,d){a.validator.methods[b]=c,a.validator.messages[b]=void 0!==d?d:a.validator.messages[b],c.length<3&&a.validator.addClassRules(b,a.validator.normalizeRule(b))},methods:{required:function(b,c,d){if(!this.depend(d,c))return"dependency-mismatch";if("select"===c.nodeName.toLowerCase()){var e=a(c).val();return e&&e.length>0}return this.checkable(c)?this.getLength(b,c)>0:void 0!==b&&null!==b&&b.length>0},email:function(a,b){return this.optional(b)||/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(a)},url:function(a,b){return this.optional(b)||/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[\/?#]\S*)?$/i.test(a)},date:function(){var a=!1;return function(b,c){return a||(a=!0,this.settings.debug&&window.console&&console.warn("The `date` method is deprecated and will be removed in version '2.0.0'.\nPlease don't use it, since it relies on the Date constructor, which\nbehaves very differently across browsers and locales. Use `dateISO`\ninstead or one of the locale specific methods in `localizations/`\nand `additional-methods.js`.")),this.optional(c)||!/Invalid|NaN/.test(new Date(b).toString())}}(),dateISO:function(a,b){return this.optional(b)||/^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test(a)},number:function(a,b){return this.optional(b)||/^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(a)},digits:function(a,b){return this.optional(b)||/^\d+$/.test(a)},minlength:function(a,b,c){var d=Array.isArray(a)?a.length:this.getLength(a,b);return this.optional(b)||d>=c},maxlength:function(a,b,c){var d=Array.isArray(a)?a.length:this.getLength(a,b);return this.optional(b)||d<=c},rangelength:function(a,b,c){var d=Array.isArray(a)?a.length:this.getLength(a,b);return this.optional(b)||d>=c[0]&&d<=c[1]},min:function(a,b,c){return this.optional(b)||a>=c},max:function(a,b,c){return this.optional(b)||a<=c},range:function(a,b,c){return this.optional(b)||a>=c[0]&&a<=c[1]},step:function(b,c,d){var e,f=a(c).attr("type"),g="Step attribute on input type "+f+" is not supported.",h=["text","number","range"],i=new RegExp("\\b"+f+"\\b"),j=f&&!i.test(h.join()),k=function(a){var b=(""+a).match(/(?:\.(\d+))?$/);return b&&b[1]?b[1].length:0},l=function(a){return Math.round(a*Math.pow(10,e))},m=!0;if(j)throw new Error(g);return e=k(d),(k(b)>e||l(b)%l(d)!==0)&&(m=!1),this.optional(c)||m},equalTo:function(b,c,d){var e=a(d);return this.settings.onfocusout&&e.not(".validate-equalTo-blur").length&&e.addClass("validate-equalTo-blur").on("blur.validate-equalTo",function(){a(c).valid()}),b===e.val()},remote:function(b,c,d,e){if(this.optional(c))return"dependency-mismatch";e="string"==typeof e&&e||"remote";var f,g,h,i=this.previousValue(c,e);return this.settings.messages[c.name]||(this.settings.messages[c.name]={}),i.originalMessage=i.originalMessage||this.settings.messages[c.name][e],this.settings.messages[c.name][e]=i.message,d="string"==typeof d&&{url:d}||d,h=a.param(a.extend({data:b},d.data)),i.old===h?i.valid:(i.old=h,f=this,this.startRequest(c),g={},g[c.name]=b,a.ajax(a.extend(!0,{mode:"abort",port:"validate"+c.name,dataType:"json",data:g,context:f.currentForm,success:function(a){var d,g,h,j=a===!0||"true"===a;f.settings.messages[c.name][e]=i.originalMessage,j?(h=f.formSubmitted,f.resetInternals(),f.toHide=f.errorsFor(c),f.formSubmitted=h,f.successList.push(c),f.invalid[c.name]=!1,f.showErrors()):(d={},g=a||f.defaultMessage(c,{method:e,parameters:b}),d[c.name]=i.message=g,f.invalid[c.name]=!0,f.showErrors(d)),i.valid=j,f.stopRequest(c,j)}},d)),"pending")}}});var c,d={};return a.ajaxPrefilter?a.ajaxPrefilter(function(a,b,c){var e=a.port;"abort"===a.mode&&(d[e]&&d[e].abort(),d[e]=c)}):(c=a.ajax,a.ajax=function(b){var e=("mode"in b?b:a.ajaxSettings).mode,f=("port"in b?b:a.ajaxSettings).port;return"abort"===e?(d[f]&&d[f].abort(),d[f]=c.apply(this,arguments),d[f]):c.apply(this,arguments)}),a});
\ No newline at end of file
diff --git a/templates/accounts/customer/account_transfer_email_template.html b/templates/accounts/customer/account_transfer_email_template.html
new file mode 100644
index 0000000..3c9423a
--- /dev/null
+++ b/templates/accounts/customer/account_transfer_email_template.html
@@ -0,0 +1,31 @@
+
+
+
+ Your Exclusive Account Access Details with Good Times!
+
+
+
+
Dear Valued Customer,
+
Greetings from Good Times! We trust this correspondence finds you in splendid spirits.
+
We are pleased to provide you with your account credentials for seamless access:
+
+
+
Username:
+
{{ principal_obj.email }}
+
+
+
Password:
+
{{ temp_password }}
+
+
+
Please utilize the temporary password to access your account promptly. Upon your initial login, we recommend changing your password to further enhance security measures.
+
We sincerely hope your experience with Good Times has been delightful thus far and look forward to continuing to exceed your expectations!
+
Warmest regards,
+ Good Times
+ {{ settings.EMAIL_HOST_USER }}
+
+
\ No newline at end of file
diff --git a/templates/accounts/customer/customer_add.html b/templates/accounts/customer/customer_add.html
new file mode 100644
index 0000000..77bd818
--- /dev/null
+++ b/templates/accounts/customer/customer_add.html
@@ -0,0 +1,212 @@
+{% extends 'layout/base_template.html' %}
+{% load static %}
+{% block stylesheet %}
+
+
+ {% include "cdn_through_html/flatpicker_cdn_css.html" %}
+{% endblock %}
+
+{% block content %}
+
+