feat(module 1): add customer, non transferlist, import , export

This commit is contained in:
bobbyvish
2024-06-27 17:24:52 +05:30
parent ed92443e36
commit 8e7f2d6d69
35 changed files with 1599 additions and 56 deletions

View File

@@ -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",
]
]
class IAmPrincipalExtendedDataSerializer(serializers.ModelSerializer):
class Meta:
model = IAmPrincipalExtendedData
fields = "__all__"

View File

@@ -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'),
]

View File

@@ -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)

View File

@@ -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()
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()

View File

@@ -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',
},
),
]

View File

@@ -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.'),
),
]

View File

@@ -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,

View File

@@ -42,6 +42,14 @@ urlpatterns = [
path('principal/role/delete/<int:pk>/', 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/<int:pk>/', views.CustomerUpdateView.as_view(), name="customer_edit"),
path('customer/detail/<int:pk>/', views.CustomerDetailView.as_view(), name="customer_detail"),
path('customer/transfer/<int:pk>/', 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

View File

@@ -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

32
goodtimes/mixins.py Normal file
View File

@@ -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)

View File

@@ -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):

View File

@@ -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(

View File

@@ -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",

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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"

View File

@@ -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)

View File

@@ -30,6 +30,7 @@ class SubscriptionForm(forms.ModelForm):
"referral_percentage",
"active",
"deleted",
"is_free",
] # Include all fields you want from the model

View File

@@ -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.'),
),
]

View File

@@ -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):

View File

@@ -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():

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<title>Your Exclusive Account Access Details with Good Times!</title>
<style>
body {
font-family: Arial, sans-serif;
}
</style>
</head>
<body>
<h2>Dear Valued Customer,</h2>
<p>Greetings from Good Times! We trust this correspondence finds you in splendid spirits.</p>
<p>We are pleased to provide you with your account credentials for seamless access:</p>
<table border="0" cellpadding="5" cellspacing="0">
<tr>
<td><strong>Username:</strong></td>
<td>{{ principal_obj.email }}</td>
</tr>
<tr>
<td><strong>Password:</strong></td>
<td>{{ temp_password }}</td>
</tr>
</table>
<p>Please utilize the temporary password to access your account promptly. Upon your initial login, we recommend changing your password to further enhance security measures.</p>
<p>We sincerely hope your experience with Good Times has been delightful thus far and look forward to continuing to exceed your expectations!</p>
<p>Warmest regards,</p>
Good Times<br>
{{ settings.EMAIL_HOST_USER }}</p>
</body>
</html>

View File

@@ -0,0 +1,212 @@
{% extends 'layout/base_template.html' %}
{% load static %}
{% block stylesheet %}
<!-- include required css cdn link through html here -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
{% include "cdn_through_html/flatpicker_cdn_css.html" %}
{% endblock %}
{% block content %}
<div class="row layout-top-spacing">
<div class="col-lg-12">
<div class="row mb-2">
<div class="col">
<a href="{% url 'accounts:customer_list'%}" style="height: fit-content;width: fit-content;display: inline-block;">
<h3 class="card-title m-2 d-flex align-items-center gap-2" style="width: fit-content;"><span class="fw-bold material-symbols-outlined">
arrow_back
</span><span>{{operation}} Customer</span></h3>
</a>
</div>
</div>
<div class="row layout-spacing">
<div class="col-lg-12">
<div class="statbox widget box box-shadow">
<div class="widget-content widget-content-area">
<form method="post" id="addCustomer">
{% csrf_token %}
{% include 'includes/dynamic_template_form.html' with form=form %}
<div class="mt-4 mb-0">
<div class="d-grid"><button class="btn btn-primary btn-block" type="submit">Submit</button></div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}
{% block javascript %}
<!-- include required js cdn link through html here -->
{% include "cdn_through_html/flatpicker_cdn_js.html" %}
{% include "cdn_through_html/jquery_validate_cdn_js.html" %}
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.3/jquery.validate.min.js"></script>
-->
<script>
$(document).ready(function() {
// var f3 = flatpickr(document.getElementById('id_free_period'), {
// mode: "range"
// });
var start_date = flatpickr(document.getElementById('id_free_start_date'), {
minDate: "today",
onChange: function(selectedDates, dateStr, instance) {
end_date.set('minDate', selectedDates[0]);
}
});
var end_date = flatpickr(document.getElementById('id_free_end_date'), {
minDate: null // initialize with no minimum date
});
$('.js-example-basic-multiple').select2({
placeholder: 'Select options',
allowClear: true,
tags: true, // Allow the user to enter custom tags
tokenSeparators: [',', ' '], // Customize token separators
closeOnSelect: false // Keep the dropdown open after selection
});
// Add custom validation method for email
$.validator.addMethod("validEmail", function(value, element) {
return /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/.test(value);
}, "Please enter a valid email address.");
// Add custom validation method to check for special characters
$.validator.addMethod("noSpecialChars", function(value, element) {
return /^[a-zA-Z\s]*$/.test(value); // Allow only letters and whitespace
}, "Please enter only letters and spaces.");
// Add custom validation method to check for starting with a letter
$.validator.addMethod("startsWithLetter", function(value, element) {
return /^[a-zA-Z]/.test(value); // Check if the value starts with a letter
}, "Please start with a letter.");
$("#addCustomer").validate({
rules: {
first_name: {
required: true,
minlength: 2,
maxlength:15,
noSpecialChars: true,
startsWithLetter: true,
noSpace: true
},
last_name: {
required: true,
minlength: 2,
maxlength: 15,
noSpecialChars: true,
startsWithLetter: true,
noSpace: true
},
email: {
required: true,
validEmail: true,
remote: {
url: "{% url 'accounts:customer_check_email' %}", // Replace with your actual URL for the view
type: "POST",
data: {
email: function() {
return $("#id_email").val();
}
},
beforeSend: function(xhr) {
xhr.setRequestHeader('X-CSRFToken', $('input[name="csrfmiddlewaretoken"]').val());
},
success: function(data) {
console.log(date)
// Handle successful email check (optional)
// You can remove this if you only need to display the error message
},
error: function(response) {
console.log(response)
}
},
},
preferences: {
required: true,
minlength: 3
},
free_start_date: {
required: true,
// You can add a custom validation method for date format (optional)
},
free_end_date: {
required: true,
// You can add a custom validation method for date format (optional)
}
},
messages: {
first_name: {
required: "Please enter your first name.",
minlength: "First name must be at least 2 characters.",
maxlength: "First name must not exceed 20 characters.",
noSpecialChars: "Please enter only letters and spaces.",
startsWithLetter: "First name must start with a letter."
},
last_name: {
required: "Please enter your last name.",
minlength: "Last name must be at least 2 characters.",
maxlength: "First name must not exceed 20 characters.",
noSpecialChars: "Please enter only letters and spaces.",
startsWithLetter: "Last name must start with a letter."
},
email: {
required: "Please enter your email address.",
validEmail: "Please enter a valid email address."
},
preferences: {
required: "Please select at least one preference.",
minlength: "Please select at least one preference."
},
free_start_date: {
required: "Please select a start date for the free period."
},
free_end_date: {
required: "Please select an end date for the free period.",
greaterThanStartDate: "The end date must be after the start date."
}
},
customMethods: {
greaterThanStartDate: function(element) {
var startDate = $("#id_free_start_date").datepicker("getDate"); // Assuming you're using datepicker
var endDate = $(element).datepicker("getDate");
if (!endDate || !startDate) {
return true; // Allow if either date is not selected (prevents errors)
}
return endDate > startDate;
}
},
errorElement: 'div',
errorPlacement: function(error, element) {
error.addClass('invalid-feedback');
$(element).closest('.form-group').append(error);
},
highlight: function(element, errorClass, validClass) {
$(element).addClass('is-invalid').removeClass('is-valid');
},
unhighlight: function(element, errorClass, validClass) {
$(element).removeClass('is-invalid').addClass('is-valid');
},
submitHandler: function(form) {
// Disable the submit button to prevent multiple submissions
$('button[type="submit"]').prop('disabled', true);
form.submit();
}
});
})
</script>
{% endblock %}

View File

@@ -0,0 +1,80 @@
{% extends 'layout/base_template.html' %}
{% load static %}
{% block stylesheet %}
<!-- include required css cdn link through html here -->
{% endblock %}
{% block content %}
<div class="row layout-top-spacing">
<div class="col-lg-12">
<div class="row mb-2">
<div class="col">
<a href="{% url 'accounts:customer_list'%}" style="height: fit-content;width: fit-content;display: inline-block;">
<h3 class="card-title m-2 d-flex align-items-center gap-2" style="width: fit-content;"><span class="fw-bold material-symbols-outlined">
arrow_back
</span><span>Customer</span></h3>
</a>
</div>
</div>
<div class="row layout-spacing">
<div class="col-lg-12">
<div class="statbox widget box box-shadow">
<div class="widget-content widget-content-area">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% include 'includes/dynamic_template_form.html' with form=form %}
<div class="mt-4 mb-0">
<div class="d-grid"><button class="btn btn-primary btn-block" type="submit">Submit</button></div>
</div>
</form>
<div class="mt-3">
{% if error_log %}
<h3>Error Log:</h3>
<ul>
{% for error in error_log %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}
{% block javascript %}
<script>
// $('form').submit(function(event) {
// event.preventDefault(); // prevent default form submission
// var formData = new FormData(this); // create a FormData object from the form
// if ($('#excel_file').val() == '') {
// alert('Please select a file to upload');
// return;
// }
// $.ajax({
// type: 'POST',
// url: '{% url "accounts:import_customer_data" %}', // replace with your view URL
// data: formData,
// contentType: false,
// processData: false,
// success: function(data) {
// console.log('File uploaded successfully!');
// },
// error: function(xhr, status, error) {
// console.log('Error uploading file: ' + error);
// }
// });
// });
</script>
{% endblock %}

View File

@@ -0,0 +1,101 @@
{% extends 'layout/base_template.html' %}
{% load static %}
{% block stylesheet %}
<!-- include required css cdn link through html here -->
{% endblock %}
{% block content %}
<div class="row layout-top-spacing">
<div class="col-lg-12">
<div class="row">
<div class="col">
<a href="{% url 'accounts:customer_list'%}" style="height: fit-content;width: fit-content;display: inline-block;">
<h3 class="card-title m-2 d-flex align-items-center gap-2" style="width: fit-content;"><span class="fw-bold material-symbols-outlined">
arrow_back
</span><span>Customer Details</span></h3>
</a>
</div>
</div>
<div class="row layout-spacing">
<div class="col-lg-12">
<div class="statbox widget box box-shadow">
<div class="widget-content widget-content-area">
<div class="row mb-3">
<div class="col-md-3">First Name</div>
<div class="col-md-9">{{principal_obj.first_name}}</div>
</div>
<div class="row mb-3">
<div class="col-md-3">Last Name</div>
<div class="col-md-9">{{principal_obj.last_name}}</div>
</div>
<div class="row mb-3">
<div class="col-md-3">Email Address</div>
<div class="col-md-9">{{principal_obj.email}}</div>
</div>
<div class="row mb-3">
<div class="col-md-3">Preferences</div>
<div class="col-md-9">
{% for category in principal_preference.preferred_categories.all %}
<span class="shadow-none badge badge-primary mb-1">
{{ category.title }}
</span>
{% empty %}
<span class="shadow-none badge badge-primary">
No preferred categories.
</span>
{% endfor %}
</div>
</div>
<div class="row mb-3">
<div class="col-md-3">Start Date</div>
<div class="col-md-9">{{principal_subscription.start_date}}</div>
</div>
<div class="row mb-3">
<div class="col-md-3">End Date</div>
<div class="col-md-9">{{principal_subscription.end_date}}</div>
</div>
{% if not principal_obj.extended_data.is_transferred %}
<div class="col text-end">
<a class="btn btn-primary mb-3" href="{% url 'accounts:customer_transfer' principal_obj.id%}">
Transfer Account
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}
{% block javascript %}
{% endblock %}

View File

@@ -0,0 +1,220 @@
{% extends 'layout/base_template.html' %}
{% load static %}
{% block stylesheet %}
<!-- include required css cdn link through html here -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
{% include "cdn_through_html/flatpicker_cdn_css.html" %}
{% endblock %}
{% block content %}
<div class="row layout-top-spacing">
<div class="col-lg-12">
<div class="row mb-2">
<div class="col">
<a href="{% url 'accounts:customer_list'%}" style="height: fit-content;width: fit-content;display: inline-block;">
<h3 class="card-title m-2 d-flex align-items-center gap-2" style="width: fit-content;"><span class="fw-bold material-symbols-outlined">
arrow_back
</span><span>{{operation}} Customer</span></h3>
</a>
</div>
{% if not principal_obj.extended_data.is_transferred %}
<div class="col text-end">
<a class="btn btn-dark mb-2 me-4" href="{% url 'accounts:customer_transfer' principal_obj.id%}">
Transfer Account
</a>
</div>
{% endif %}
</div>
<div class="row layout-spacing">
<div class="col-lg-12">
<div class="statbox widget box box-shadow">
<div class="widget-content widget-content-area">
<form method="post" id="addCustomer">
{% csrf_token %}
{% include 'includes/dynamic_template_form.html' with form=form %}
<div class="mt-4 mb-0">
<div class="d-grid"><button class="btn btn-primary btn-block" type="submit">Submit</button></div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}
{% block javascript %}
<!-- include required js cdn link through html here -->
{% include "cdn_through_html/flatpicker_cdn_js.html" %}
{% include "cdn_through_html/jquery_validate_cdn_js.html" %}
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.3/jquery.validate.min.js"></script>
-->
<script>
$(document).ready(function() {
// var f3 = flatpickr(document.getElementById('id_free_period'), {
// mode: "range"
// });
var start_date = flatpickr(document.getElementById('id_free_start_date'), {
minDate: "today",
onChange: function(selectedDates, dateStr, instance) {
end_date.set('minDate', selectedDates[0]);
}
});
var end_date = flatpickr(document.getElementById('id_free_end_date'), {
minDate: null // initialize with no minimum date
});
$('.js-example-basic-multiple').select2({
placeholder: 'Select options',
allowClear: true,
tags: true, // Allow the user to enter custom tags
tokenSeparators: [',', ' '], // Customize token separators
closeOnSelect: false // Keep the dropdown open after selection
});
// Add custom validation method for email
$.validator.addMethod("validEmail", function(value, element) {
return /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/.test(value);
}, "Please enter a valid email address.");
// Add custom validation method to check for special characters
$.validator.addMethod("noSpecialChars", function(value, element) {
return /^[a-zA-Z\s]*$/.test(value); // Allow only letters and whitespace
}, "Please enter only letters and spaces.");
// Add custom validation method to check for starting with a letter
$.validator.addMethod("startsWithLetter", function(value, element) {
return /^[a-zA-Z]/.test(value); // Check if the value starts with a letter
}, "Please start with a letter.");
$("#addCustomer").validate({
rules: {
first_name: {
required: true,
minlength: 2,
maxlength:15,
noSpecialChars: true,
startsWithLetter: true,
noSpace: true
},
last_name: {
required: true,
minlength: 2,
maxlength: 15,
noSpecialChars: true,
startsWithLetter: true,
noSpace: true
},
email: {
required: true,
validEmail: true,
remote: {
url: "{% url 'accounts:customer_check_email' %}", // Replace with your actual URL for the view
type: "POST",
data: {
email: function() {
return $("#id_email").val();
}
},
beforeSend: function(xhr) {
xhr.setRequestHeader('X-CSRFToken', $('input[name="csrfmiddlewaretoken"]').val());
},
success: function(data) {
console.log(date)
// Handle successful email check (optional)
// You can remove this if you only need to display the error message
},
error: function(response) {
console.log(response)
}
},
},
preferences: {
required: true,
minlength: 3
},
free_start_date: {
required: true,
// You can add a custom validation method for date format (optional)
},
free_end_date: {
required: true,
// You can add a custom validation method for date format (optional)
}
},
messages: {
first_name: {
required: "Please enter your first name.",
minlength: "First name must be at least 2 characters.",
maxlength: "First name must not exceed 20 characters.",
noSpecialChars: "Please enter only letters and spaces.",
startsWithLetter: "First name must start with a letter."
},
last_name: {
required: "Please enter your last name.",
minlength: "Last name must be at least 2 characters.",
maxlength: "First name must not exceed 20 characters.",
noSpecialChars: "Please enter only letters and spaces.",
startsWithLetter: "Last name must start with a letter."
},
email: {
required: "Please enter your email address.",
validEmail: "Please enter a valid email address."
},
preferences: {
required: "Please select at least one preference.",
minlength: "Please select at least one preference."
},
free_start_date: {
required: "Please select a start date for the free period."
},
free_end_date: {
required: "Please select an end date for the free period.",
greaterThanStartDate: "The end date must be after the start date."
}
},
customMethods: {
greaterThanStartDate: function(element) {
var startDate = $("#id_free_start_date").datepicker("getDate"); // Assuming you're using datepicker
var endDate = $(element).datepicker("getDate");
if (!endDate || !startDate) {
return true; // Allow if either date is not selected (prevents errors)
}
return endDate > startDate;
}
},
errorElement: 'div',
errorPlacement: function(error, element) {
error.addClass('invalid-feedback');
$(element).closest('.form-group').append(error);
},
highlight: function(element, errorClass, validClass) {
$(element).addClass('is-invalid').removeClass('is-valid');
},
unhighlight: function(element, errorClass, validClass) {
$(element).removeClass('is-invalid').addClass('is-valid');
},
submitHandler: function(form) {
// Disable the submit button to prevent multiple submissions
$('button[type="submit"]').prop('disabled', true);
form.submit();
}
});
})
</script>
{% endblock %}

View File

@@ -11,18 +11,24 @@
<div class="row layout-top-spacing">
<div class="col-lg-12">
<div class="row">
<div class="col-sm-6">
<div class="col-sm-4">
<h3>Manage Customer</h3>
</div>
<div class="col-sm-6 text-md-end">
<!--
<button class="btn btn-dark mb-2 me-md-4" onclick="history.back()">
<i class="fa fa-arrow-left"></i>
Back
</button>
-->
{% comment %} <a class="btn btn-primary mb-2 me-md-4" href="{% url 'accounts:example_add' %}">Example form</a>
<a class="btn btn-primary mb-2" href="{% url 'accounts:principal_add' %}">Add Principal</a> {% endcomment %}
<div class="col-sm-8 text-md-end">
<a class="btn btn-dark mb-2 me-md-4" href="{% url 'accounts:download_excel_template' %}">
Download Excel Template
</a>
<a class="btn btn-dark mb-2 me-md-4" href="{% url 'accounts:import_customer_data' %}">
import
</a>
<a class="btn btn-dark mb-2 me-md-4" href="{% url 'accounts:export_customer_data' %}">
Export
</a>
<a class="btn btn-primary mb-2 me-4" href="{% url 'accounts:customer_add' %}">Add Customer</a>
</div>
</div>
@@ -58,6 +64,10 @@
style="width: 98.875px;">Email Verified</th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
style="width: 98.875px;">Referral Count</th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
style="width: 98.875px;">Onboarded by Admin</th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
style="width: 98.875px;">Transferred to Customer</th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
style="width: 98.875px;">Created On</th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
@@ -87,6 +97,16 @@
<!-- <td>{{ data_obj.phone_verified }}</td> -->
<td>{{ data_obj.email_verified }}</td>
<td>{{ data_obj.referral_count }}</td>
<td class="text-center">
<span class="shadow-none badge {% if data_obj.extended_data.is_onboarded %}badge-primary{% else %}badge-danger{% endif %}">
{{ data_obj.extended_data.is_onboarded }}
</span>
</td>
<td class="text-center">
<span class="shadow-none badge {% if data_obj.extended_data.is_transferred %}badge-primary{% else %}badge-danger{% endif %}">
{{ data_obj.extended_data.is_transferred }}
</span>
</td>
<td>{{ data_obj.created_on }}</td>
<td>{{ data_obj.modified_on }}</td>
<td class="text-center">
@@ -101,7 +121,7 @@
</td> -->
<td class="text-center">
<ul class="table-controls">
<li><a href="{% url 'accounts:principal_edit' data_obj.id%}" class="bs-tooltip"
<li><a href="{% url 'accounts:customer_edit' data_obj.id%}" class="bs-tooltip"
data-bs-toggle="tooltip" data-bs-placement="top" title=""
data-original-title="Edit" data-bs-original-title="Edit"
aria-label="Edit"><svg xmlns="http://www.w3.org/2000/svg"
@@ -113,19 +133,9 @@
d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z">
</path>
</svg></a></li>
<!-- <li><a href="javascript:void(0);" class="bs-tooltip"
data-bs-toggle="tooltip" data-bs-placement="top" title=""
data-original-title="Delete" data-bs-original-title="Delete"
aria-label="Delete"><svg xmlns="http://www.w3.org/2000/svg"
width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
class="feather feather-trash p-1 br-8 mb-1">
<polyline points="3 6 5 6 21 6"></polyline>
<path
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2">
</path>
</svg></a></li> -->
<li><a href="{% url 'accounts:customer_detail' data_obj.id%}" class="bs-tooltip" data-bs-toggle="tooltip" data-bs-placement="top" title="" data-original-title="View" data-bs-original-title="View" aria-label="View">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-eye"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
</a></li>
</ul>
</td>
</tr>

View File

@@ -0,0 +1,2 @@
{% load static%}
<script src="{% static 'src/plugins/src/jquery-validate/jquery.validate.min.js' %}"></script>

View File

@@ -81,7 +81,7 @@
<script src="{% static 'src/assets/js/dashboard/dash_1.js' %}"></script>
<!-- BEGIN PAGE LEVEL PLUGINS/CUSTOM SCRIPTS -->
<script>
$(".messgae-alert").fadeTo(2000, 500).slideUp(500, function () {
$(".messgae-alert").fadeTo(5000, 500).slideUp(500, function () {
$(".messgae-alert").slideUp(500);
});
</script>

View File

@@ -16,15 +16,12 @@
<div class="col-lg-12">
<div class="row mb-2">
<div class="col">
<h3>{{operation}} {{page_title}}</h3>
<a href="{% url 'manage_events:event_list'%}" style="height: fit-content;width: fit-content;display: inline-block;">
<h3 class="card-title m-2 d-flex align-items-center gap-2" style="width: fit-content;"><span class="fw-bold material-symbols-outlined">
arrow_back
</span><span>{{operation}} Event</span></h3>
</a>
</div>
<div class="col text-end">
<button class="btn btn-dark mb-2 me-4" onclick="history.back()">
<i class="fa fa-arrow-left"></i>
Back
</button>
</div>
</div>
<div class="row layout-spacing">
<div class="col-lg-12">

View File

@@ -41,6 +41,8 @@
<tr role="row">
<th class="checkbox-column sorting_asc" tabindex="0" aria-controls="style-3"
aria-sort="ascending" style="width: 69.2656px;"> Record Id </th>
<th class="checkbox-column sorting_asc" tabindex="0" aria-controls="style-3"
aria-sort="ascending" style="width: 69.2656px;"> Customer </th>
<th class="checkbox-column sorting_asc" tabindex="0" aria-controls="style-3"
aria-sort="ascending" style="width: 69.2656px;"> Title </th>
<th class="checkbox-column sorting_asc" tabindex="0" aria-controls="style-3"
@@ -51,8 +53,7 @@
aria-sort="ascending" style="width: 69.2656px;"> Start Time </th>
<th class="checkbox-column sorting_asc" tabindex="0" aria-controls="style-3"
aria-sort="ascending" style="width: 69.2656px;"> End Time </th>
<th class="checkbox-column sorting_asc" tabindex="0" aria-controls="style-3"
aria-sort="ascending" style="width: 69.2656px;"> Principal </th>
<th class="sorting" tabindex="7" aria-controls="style-3"
style="width: 79.7969px;">Draft</th>
<th class="sorting" tabindex="7" aria-controls="style-3"
@@ -67,12 +68,19 @@
<td class="checkbox-column text-center sorting_1"><a
href="{% url 'manage_events:event_detail' data_obj.id %}">{{data_obj.id}}</a>
</td>
<td>
{% if data_obj.principal %}
{{ data_obj.principal }}
{% elif data_obj.created_by %}
{{ data_obj.created_by }}
{% endif %}
</td>
<td>{{data_obj.title}}</td>
<td>{{data_obj.start_date}}</td>
<td>{{data_obj.end_date}}</td>
<td>{{data_obj.from_time}}</td>
<td>{{data_obj.to_time}}</td>
<td>{{data_obj.created_by}}</td>
<td class="text-center">
<span
class="shadow-none badge {% if data_obj.draft %}badge-primary{% else %}badge-danger{% endif %}">{{data_obj.draft}}</span>

View File

@@ -50,6 +50,9 @@
<th class="checkbox-column sorting_asc" tabindex="0"
aria-controls="style-3" aria-sort="ascending"
style="width: 69.2656px;"> Amount </th>
<th class="checkbox-column sorting_asc" tabindex="0"
aria-controls="style-3" aria-sort="ascending"
style="width: 69.2656px;"> Free for Admin </th>
<th class="sorting" tabindex="7" aria-controls="style-3"
style="width: 79.7969px;">Active</th>
<th class="dt-no-sorting sorting" tabindex="8"
@@ -64,6 +67,9 @@
<td>{{data_obj.title}}</td>
<td>{{data_obj.plan.days}}</td>
<td>{{data_obj.amount}}</td>
<td class="text-center">
<span class="shadow-none badge {% if data_obj.is_free %}badge-primary{% else %}badge-danger{% endif %}">{{data_obj.is_free}}</span>
</td>
<td class="text-center">
<span class="shadow-none badge {% if data_obj.active %}badge-primary{% else %}badge-danger{% endif %}">{{data_obj.active}}</span>
</td>

View File

@@ -16,14 +16,11 @@
<div class="col-lg-12">
<div class="row mb-2">
<div class="col">
<h3>{{operation}} {{page_title}}</h3>
</div>
<div class="col text-end">
<button class="btn btn-dark mb-2 me-4" onclick="history.back()">
<i class="fa fa-arrow-left"></i>
Back
</button>
<a href="{% url 'manage_events:venue_list'%}" style="height: fit-content;width: fit-content;display: inline-block;">
<h3 class="card-title m-2 d-flex align-items-center gap-2" style="width: fit-content;"><span class="fw-bold material-symbols-outlined">
arrow_back
</span><span>{{operation}} Venue</span></h3>
</a>
</div>
</div>
<div class="row layout-spacing">

View File

@@ -39,6 +39,8 @@
<tr role="row">
<th class="checkbox-column sorting_asc" tabindex="0" aria-controls="style-3"
aria-sort="ascending" style="width: 69.2656px;"> Record Id </th>
<th class="checkbox-column sorting_asc" tabindex="0" aria-controls="style-3"
aria-sort="ascending" style="width: 69.2656px;"> Customer </th>
<th class="checkbox-column sorting_asc" tabindex="0" aria-controls="style-3"
aria-sort="ascending" style="width: 69.2656px;"> Title </th>
<th class="checkbox-column sorting_asc" tabindex="0" aria-controls="style-3"
@@ -57,6 +59,13 @@
{% for data_obj in venue_obj %}
<tr role="row">
<td>{{data_obj.id}}</td>
<td>
{% if data_obj.principal %}
{{ data_obj.principal }}
{% elif data_obj.created_by %}
{{ data_obj.created_by }}
{% endif %}
</td>
<td>{{data_obj.title}}</td>
<td>{{data_obj.address}}</td>
<td>{{data_obj.latitude}}</td>