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 %} + +
+
+ +
+
+
+
+ +
+ {% csrf_token %} + {% include 'includes/dynamic_template_form.html' with form=form %} +
+
+
+
+ +
+
+
+
+
+
+ + +{% endblock content %} + +{% block javascript %} + + {% include "cdn_through_html/flatpicker_cdn_js.html" %} + {% include "cdn_through_html/jquery_validate_cdn_js.html" %} + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/accounts/customer/customer_bulk_template.html b/templates/accounts/customer/customer_bulk_template.html new file mode 100644 index 0000000..4b45115 --- /dev/null +++ b/templates/accounts/customer/customer_bulk_template.html @@ -0,0 +1,80 @@ +{% extends 'layout/base_template.html' %} +{% load static %} +{% block stylesheet %} + +{% endblock %} + +{% block content %} + +
+
+ +
+
+
+
+ +
+ {% csrf_token %} + {% include 'includes/dynamic_template_form.html' with form=form %} +
+
+
+
+ +
+ {% if error_log %} +

Error Log:

+
    + {% for error in error_log %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} +
+ +
+
+
+
+
+
+ + +{% endblock content %} + +{% block javascript %} + + +{% endblock %} \ No newline at end of file diff --git a/templates/accounts/customer/customer_detail.html b/templates/accounts/customer/customer_detail.html new file mode 100644 index 0000000..9d48e5c --- /dev/null +++ b/templates/accounts/customer/customer_detail.html @@ -0,0 +1,101 @@ +{% extends 'layout/base_template.html' %} +{% load static %} +{% block stylesheet %} + + +{% endblock %} + +{% block content %} + +
+
+ + + +
+
+
+
+
+ +
First Name
+ +
{{principal_obj.first_name}}
+ +
+ +
+ +
Last Name
+ +
{{principal_obj.last_name}}
+ +
+ +
+ +
Email Address
+ +
{{principal_obj.email}}
+ +
+ +
+ +
Preferences
+ +
+ {% for category in principal_preference.preferred_categories.all %} + + {{ category.title }} + + {% empty %} + + No preferred categories. + + {% endfor %} +
+ +
+ +
+ +
Start Date
+ +
{{principal_subscription.start_date}}
+ +
+
+ +
End Date
+ +
{{principal_subscription.end_date}}
+ +
+ {% if not principal_obj.extended_data.is_transferred %} + + {% endif %} +
+
+
+
+
+
+ + {% endblock content %} + + {% block javascript %} + + {% endblock %} \ No newline at end of file diff --git a/templates/accounts/customer/customer_edit.html b/templates/accounts/customer/customer_edit.html new file mode 100644 index 0000000..8cebe76 --- /dev/null +++ b/templates/accounts/customer/customer_edit.html @@ -0,0 +1,220 @@ +{% extends 'layout/base_template.html' %} +{% load static %} +{% block stylesheet %} + + + {% include "cdn_through_html/flatpicker_cdn_css.html" %} +{% endblock %} + +{% block content %} + +
+
+
+ + + {% if not principal_obj.extended_data.is_transferred %} + + {% endif %} +
+
+
+
+
+ +
+ {% csrf_token %} + {% include 'includes/dynamic_template_form.html' with form=form %} +
+
+
+
+ +
+
+
+
+
+
+ + +{% endblock content %} + +{% block javascript %} + + {% include "cdn_through_html/flatpicker_cdn_js.html" %} + {% include "cdn_through_html/jquery_validate_cdn_js.html" %} + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/accounts/customer/customer_list.html b/templates/accounts/customer/customer_list.html index 442c97d..4056566 100644 --- a/templates/accounts/customer/customer_list.html +++ b/templates/accounts/customer/customer_list.html @@ -11,18 +11,24 @@
-
+

Manage Customer

- @@ -58,6 +64,10 @@ style="width: 98.875px;">Email Verified Referral Count + Onboarded by Admin + Transferred to Customer Created On {{ data_obj.phone_verified }} --> {{ data_obj.email_verified }} {{ data_obj.referral_count }} + + + {{ data_obj.extended_data.is_onboarded }} + + + + + {{ data_obj.extended_data.is_transferred }} + + {{ data_obj.created_on }} {{ data_obj.modified_on }} @@ -101,7 +121,7 @@ --> diff --git a/templates/cdn_through_html/jquery_validate_cdn_js.html b/templates/cdn_through_html/jquery_validate_cdn_js.html new file mode 100644 index 0000000..96f8d24 --- /dev/null +++ b/templates/cdn_through_html/jquery_validate_cdn_js.html @@ -0,0 +1,2 @@ +{% load static%} + \ No newline at end of file diff --git a/templates/layout/base_template.html b/templates/layout/base_template.html index 899c53e..c806a2b 100644 --- a/templates/layout/base_template.html +++ b/templates/layout/base_template.html @@ -81,7 +81,7 @@ diff --git a/templates/manage_events/event_add.html b/templates/manage_events/event_add.html index 479da2f..66abf4b 100644 --- a/templates/manage_events/event_add.html +++ b/templates/manage_events/event_add.html @@ -16,15 +16,12 @@
-

{{operation}} {{page_title}}

+ +

+ arrow_back + {{operation}} Event

+
- -
- -
diff --git a/templates/manage_events/event_list.html b/templates/manage_events/event_list.html index bc3f027..bebfe2c 100644 --- a/templates/manage_events/event_list.html +++ b/templates/manage_events/event_list.html @@ -41,6 +41,8 @@ Record Id + Customer Title Start Time End Time - Principal + Draft {{data_obj.id}} + + {% if data_obj.principal %} + {{ data_obj.principal }} + {% elif data_obj.created_by %} + {{ data_obj.created_by }} + {% endif %} + {{data_obj.title}} {{data_obj.start_date}} {{data_obj.end_date}} {{data_obj.from_time}} {{data_obj.to_time}} - {{data_obj.created_by}} + {{data_obj.draft}} diff --git a/templates/manage_subscriptions/subscription_list.html b/templates/manage_subscriptions/subscription_list.html index 3429f6d..1639afb 100644 --- a/templates/manage_subscriptions/subscription_list.html +++ b/templates/manage_subscriptions/subscription_list.html @@ -50,6 +50,9 @@ Amount + Free for Admin Active {{data_obj.title}} {{data_obj.plan.days}} {{data_obj.amount}} + + {{data_obj.is_free}} + {{data_obj.active}} diff --git a/templates/manage_venues/venue_add.html b/templates/manage_venues/venue_add.html index 7af23b8..c7e1081 100644 --- a/templates/manage_venues/venue_add.html +++ b/templates/manage_venues/venue_add.html @@ -16,14 +16,11 @@
-

{{operation}} {{page_title}}

-
- -
diff --git a/templates/manage_venues/venue_list.html b/templates/manage_venues/venue_list.html index 1305d9c..31e8a2b 100644 --- a/templates/manage_venues/venue_list.html +++ b/templates/manage_venues/venue_list.html @@ -39,6 +39,8 @@ Record Id + Customer Title {{data_obj.id}} + + {% if data_obj.principal %} + {{ data_obj.principal }} + {% elif data_obj.created_by %} + {{ data_obj.created_by }} + {% endif %} + {{data_obj.title}} {{data_obj.address}} {{data_obj.latitude}}