Compare commits
12 Commits
eef12c8579
...
9d20583a78
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d20583a78 | ||
|
|
5693ec3c48 | ||
|
|
bec8f9d608 | ||
|
|
b559c62926 | ||
|
|
24949ade3e | ||
|
|
3425d82891 | ||
|
|
8ccec7012c | ||
|
|
3f263b2af5 | ||
|
|
76ac9413c4 | ||
|
|
76323a1ea1 | ||
|
|
9ba960fcad | ||
|
|
6f07fe4877 |
@@ -362,6 +362,7 @@ class IAmPrincipalResourceLinkForm(IAmPrincipalForm):
|
||||
|
||||
|
||||
class CreateCustomerForm(forms.Form):
|
||||
profile_photo = forms.ImageField(label="Profile Image", widget=forms.ClearableFileInput(attrs={'class': 'filepond'}),)
|
||||
first_name = forms.CharField(max_length=255, required=True, label='First Name')
|
||||
last_name = forms.CharField(max_length=255, required=True, label='Last Name')
|
||||
business_name = forms.CharField(max_length=200, required=True, label="Business Name")
|
||||
@@ -402,6 +403,7 @@ class CreateCustomerForm(forms.Form):
|
||||
self.fields['preferences'].queryset = EventCategory.objects.all()
|
||||
|
||||
class UpdateCustomerForm(forms.Form):
|
||||
profile_photo = forms.ImageField(label="Profile Image")
|
||||
first_name = forms.CharField(max_length=255, required=True, label='First Name')
|
||||
last_name = forms.CharField(max_length=255, required=True, label='Last Name')
|
||||
business_name = forms.CharField(max_length=200, required=True, label="Business Name")
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.2 on 2024-12-20 12:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0015_iamprincipal_twitter_profile'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='iamprincipalextendeddata',
|
||||
name='encrypted_pass',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -13,6 +13,7 @@ from django.utils.text import slugify
|
||||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
|
||||
# from manage_subscriptions.models import Subscription
|
||||
|
||||
from goodtimes.utils import RandomGenerator
|
||||
from .resource_action import (
|
||||
PRINCIPAL_TYPE_EVENT_USER,
|
||||
@@ -334,8 +335,15 @@ class IAmPrincipal(AbstractUser):
|
||||
def __str__(self):
|
||||
return f"{self.email}"
|
||||
|
||||
@staticmethod
|
||||
def generate_random_password():
|
||||
"""Generate a password in the format 'GoodTimes@xxxx'."""
|
||||
random_number = random.randint(1000, 9999) # Generate a 4-digit random number
|
||||
return f"GoodTimes@{random_number}"
|
||||
|
||||
|
||||
class IAmPrincipalExtendedData(models.Model):
|
||||
|
||||
principal = models.OneToOneField(
|
||||
IAmPrincipal,
|
||||
related_name="extended_data",
|
||||
@@ -356,7 +364,7 @@ class IAmPrincipalExtendedData(models.Model):
|
||||
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.")
|
||||
|
||||
encrypted_pass = models.TextField(blank=True, null=True)
|
||||
class Meta:
|
||||
db_table = "iam_principal_extended_data"
|
||||
|
||||
@@ -368,6 +376,12 @@ class IAmPrincipalExtendedData(models.Model):
|
||||
self.transferred_on = datetime.datetime.now()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def decrypted_field(self):
|
||||
from goodtimes.services import Encryptor
|
||||
encryptor = Encryptor()
|
||||
return encryptor.decrypt(self.encrypted_pass)
|
||||
|
||||
class IAmPrincipalResourceLink(models.Model):
|
||||
principal = models.ForeignKey(
|
||||
IAmPrincipal,
|
||||
|
||||
@@ -42,6 +42,7 @@ 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/get-decrypted-password/<int:customer_id>/', views.GetDecryptedPasswordView.as_view(), name='get_decrypted_password'),
|
||||
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"),
|
||||
|
||||
@@ -24,7 +24,7 @@ from django.utils import timezone
|
||||
import phonenumbers
|
||||
from accounts import permission
|
||||
from goodtimes import constants
|
||||
from goodtimes.services import EmailService
|
||||
from goodtimes.services import EmailService, Encryptor
|
||||
from goodtimes.utils import JsonResponseUtil
|
||||
from manage_events.models import EventCategory, PrincipalPreference
|
||||
from manage_referrals.models import ReferralCode
|
||||
@@ -608,7 +608,7 @@ class CustomerCreateView(LoginRequiredMixin, generic.View):
|
||||
def post(self, request, *args, **kwargs):
|
||||
print(request.POST)
|
||||
# return redirect(self.success_url)
|
||||
form = self.form_class(request.POST)
|
||||
form = self.form_class(request.POST, request.FILES)
|
||||
context = self.get_context_data(form=form)
|
||||
if not form.is_valid():
|
||||
return render(request, self.template_name, context=context)
|
||||
@@ -621,14 +621,21 @@ class CustomerCreateView(LoginRequiredMixin, generic.View):
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
random_password = IAmPrincipal.generate_random_password()
|
||||
|
||||
# Encrypt the password
|
||||
encryptor = Encryptor()
|
||||
encrypted_password = encryptor.encrypt(random_password)
|
||||
|
||||
# save principal data
|
||||
principal_obj = IAmPrincipal.objects.create(
|
||||
profile_photo = form.cleaned_data.get("profile_photo"),
|
||||
email=form.cleaned_data.get('email'),
|
||||
first_name=form.cleaned_data.get('first_name'),
|
||||
last_name=form.cleaned_data.get('last_name'),
|
||||
business_name=form.cleaned_data.get('business_name'),
|
||||
phone_no=form.cleaned_data.get('phone_no'),
|
||||
password=make_password("goodtimes#2024"),
|
||||
password=make_password(random_password),
|
||||
username=form.cleaned_data.get("email"),
|
||||
email_verified=True,
|
||||
register_complete=True,
|
||||
@@ -651,6 +658,7 @@ class CustomerCreateView(LoginRequiredMixin, generic.View):
|
||||
IAmPrincipalExtendedData.objects.create(
|
||||
principal=principal_obj,
|
||||
is_onboarded=True,
|
||||
encrypted_pass=encrypted_password, # Save encrypted password
|
||||
)
|
||||
|
||||
# save principal preferences record
|
||||
@@ -665,7 +673,6 @@ class CustomerCreateView(LoginRequiredMixin, generic.View):
|
||||
is_paid=True,
|
||||
subscription=free_subscription
|
||||
)
|
||||
|
||||
messages.success(self.request, constants.REGISTRATION_SUCCESS)
|
||||
return redirect(self.success_url)
|
||||
except Exception as e:
|
||||
@@ -704,6 +711,7 @@ class CustomerUpdateView(LoginRequiredMixin, generic.View):
|
||||
print(f"principal address is {principal_obj.address_line1}")
|
||||
|
||||
initial_data = {
|
||||
"profile_photo": principal_obj.profile_photo,
|
||||
"first_name": principal_obj.first_name,
|
||||
"last_name": principal_obj.last_name,
|
||||
"email": principal_obj.email,
|
||||
@@ -746,13 +754,15 @@ class CustomerUpdateView(LoginRequiredMixin, generic.View):
|
||||
except Exception as e:
|
||||
messages.error(request, f"No Record of customer id {principal_id} is found")
|
||||
return redirect(self.success_url)
|
||||
form = self.form_class(request.POST)
|
||||
form = self.form_class(request.POST, request.FILES)
|
||||
print(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.profile_photo = form.cleaned_data.get('profile_photo')
|
||||
principal_obj.first_name = form.cleaned_data.get('first_name')
|
||||
principal_obj.last_name = form.cleaned_data.get('last_name')
|
||||
principal_obj.business_name = form.cleaned_data.get("business_name")
|
||||
@@ -863,6 +873,27 @@ class CustomerListView(LoginRequiredMixin, generic.ListView):
|
||||
context["page_name"] = self.page_name
|
||||
return context
|
||||
|
||||
|
||||
class GetDecryptedPasswordView(generic.View):
|
||||
def get(self, request, customer_id):
|
||||
try:
|
||||
# Fetch the extended data for the customer
|
||||
extended_data = IAmPrincipalExtendedData.objects.get(principal_id=customer_id)
|
||||
|
||||
if not extended_data.encrypted_pass:
|
||||
return JsonResponse({"success": False, "message": "No password found."})
|
||||
|
||||
# Use Encryptor to decrypt the password
|
||||
encryptor = Encryptor()
|
||||
decrypted_password = encryptor.decrypt(extended_data.encrypted_pass)
|
||||
|
||||
return JsonResponse({"success": True, "decrypted_password": decrypted_password})
|
||||
except IAmPrincipalExtendedData.DoesNotExist:
|
||||
return JsonResponse({"success": False, "message": "Customer not found."})
|
||||
except Exception as e:
|
||||
return JsonResponse({"success": False, "message": str(e)})
|
||||
|
||||
|
||||
import pandas as pd
|
||||
from openpyxl import Workbook, load_workbook
|
||||
from openpyxl.worksheet.datavalidation import DataValidation
|
||||
@@ -1022,17 +1053,23 @@ class CustomerTransferView(LoginRequiredMixin, generic.View):
|
||||
|
||||
# Send the email
|
||||
try:
|
||||
temp_password = "goodtimes#2024"
|
||||
principal_preference = IAmPrincipalExtendedData.objects.get(principal=principal_obj)
|
||||
|
||||
# Use Encryptor to decrypt the password
|
||||
encryptor = Encryptor()
|
||||
temp_password = encryptor.decrypt(principal_preference.encrypted_pass)
|
||||
|
||||
# updating password
|
||||
principal_obj.password = make_password(temp_password)
|
||||
principal_obj.save()
|
||||
|
||||
principal_preference.is_transferred = True
|
||||
principal_preference.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)}")
|
||||
@@ -1159,12 +1196,18 @@ class CustomerImportView(LoginRequiredMixin, generic.View):
|
||||
error_log.append(f"Row {idx}: One or more preferences are invalid.")
|
||||
continue
|
||||
|
||||
random_password = IAmPrincipal.generate_random_password()
|
||||
|
||||
# Encrypt the password
|
||||
encryptor = Encryptor()
|
||||
encrypted_password = encryptor.encrypt(random_password)
|
||||
|
||||
# collect the principals
|
||||
principal = IAmPrincipal(
|
||||
first_name=first_name.strip().capitalize(),
|
||||
last_name=last_name.strip().capitalize(),
|
||||
email=email.strip(),
|
||||
password=make_password("goodtimes#2024"),
|
||||
password=make_password(random_password),
|
||||
username=email.strip(),
|
||||
email_verified=True,
|
||||
register_complete=True,
|
||||
@@ -1207,7 +1250,7 @@ class CustomerImportView(LoginRequiredMixin, generic.View):
|
||||
ReferralCode.create_referral_code_for_user_manager(principal=principal, principal_type=principal_type)
|
||||
|
||||
# Create IAmPrincipalExtendedData record
|
||||
IAmPrincipalExtendedData.objects.create(principal=principal, is_onboarded=True)
|
||||
IAmPrincipalExtendedData.objects.create(principal=principal, is_onboarded=True,encrypted_pass=encrypted_password,)
|
||||
|
||||
# Create PrincipalSubscription record
|
||||
subscription = PrincipalSubscription(
|
||||
|
||||
@@ -8,13 +8,15 @@ https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
import django
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "goodtimes.settings")
|
||||
django.setup()
|
||||
from django.urls import path
|
||||
from django.core.asgi import get_asgi_application
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
from chat.routing import websocket_urlpatterns
|
||||
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "goodtimes.settings")
|
||||
django_asgi_app = get_asgi_application()
|
||||
application = ProtocolTypeRouter(
|
||||
{
|
||||
|
||||
@@ -695,58 +695,19 @@ class FacebookAPI:
|
||||
self.app_id = settings.FACEBOOK_APP_ID
|
||||
self.app_secret = settings.FACEBOOK_APP_SECRET
|
||||
self.page_id = settings.FACEBOOK_PAGE_ID
|
||||
self.page_access_token = None
|
||||
|
||||
def _get_short_lived_user_access_token(self):
|
||||
try:
|
||||
url = f"https://graph.facebook.com/oauth/access_token?grant_type=client_credentials&client_id={self.app_id}&client_secret={self.app_secret}"
|
||||
response = requests.get(url)
|
||||
# response.raise_for_status()
|
||||
print(f"short lived token {response.json()}")
|
||||
return response.json()['access_token']
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error getting short-lived user access token: {e}")
|
||||
return None
|
||||
|
||||
def _get_long_lived_user_access_token(self, short_lived_token):
|
||||
try:
|
||||
url = f"https://graph.facebook.com/v20.0/oauth/access_token?grant_type=fb_exchange_token&client_id={self.app_id}&client_secret={self.app_secret}&fb_exchange_token={short_lived_token}"
|
||||
response = requests.get(url)
|
||||
# response.raise_for_status()
|
||||
print(f"long lived access token : {response.json()}")
|
||||
return response.json()['access_token']
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error getting long-lived user access token: {e}")
|
||||
return None
|
||||
|
||||
def _get_page_access_token(self, long_lived_token):
|
||||
url = f"https://graph.facebook.com/{self.page_id}?fields=access_token&access_token={long_lived_token}"
|
||||
response = requests.get(url)
|
||||
# response.raise_for_status()
|
||||
print(f"page access token is {response.json()}")
|
||||
# self.page_access_token = response.json()["access_token"]
|
||||
|
||||
def authenticate(self):
|
||||
# short_lived_token = self._get_short_lived_user_access_token()
|
||||
# if not short_lived_token:
|
||||
# return False
|
||||
# long_lived_token = self._get_long_lived_user_access_token(short_lived_token)
|
||||
# if not long_lived_token:
|
||||
# return False
|
||||
# self._get_page_access_token(short_lived_token)
|
||||
self.page_access_token = settings.FACEBOOK_ACCESS_TOKEN
|
||||
return True
|
||||
self.graph_api_version = settings.FACEBOOK_GRAPH_VERSION_API
|
||||
self.access_token = settings.FACEBOOK_ACCESS_TOKEN # long live access token
|
||||
|
||||
def post_photo(self, image_url, caption):
|
||||
if not self.page_access_token:
|
||||
if not self.access_token:
|
||||
print("Page access token not obtained. Call authenticate() first.")
|
||||
return False
|
||||
try:
|
||||
url = f"https://graph.facebook.com/v20.0/{self.page_id}/photos"
|
||||
url = f"https://graph.facebook.com/{self.graph_api_version}/{self.page_id}/photos"
|
||||
params = {
|
||||
"message": caption,
|
||||
"url": image_url,
|
||||
"access_token": self.page_access_token,
|
||||
"access_token": self.access_token,
|
||||
}
|
||||
response = requests.post(url, params=params)
|
||||
# response.raise_for_status()
|
||||
@@ -765,9 +726,6 @@ class FacebookPoster:
|
||||
self.facebook_api = facebook_api
|
||||
|
||||
def post_photo(self, image_url, caption):
|
||||
if not self.facebook_api.authenticate():
|
||||
print("Authentication failed. Please try again.")
|
||||
return {'success': False, 'message': 'Error posting photo. Authenticate failed'}
|
||||
result = self.facebook_api.post_photo(image_url, caption)
|
||||
if not result:
|
||||
return {'success': False, 'message': 'Error posting photo in Facebook'}
|
||||
@@ -1169,4 +1127,18 @@ class StripeService:
|
||||
)
|
||||
return {'success': True, 'data': subscription}
|
||||
except stripe.error.StripeError as e:
|
||||
|
||||
return {'success': False, 'message': f'Error cancelling subscription auto-renewal: {e}'}
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
class Encryptor:
|
||||
def __init__(self):
|
||||
self.key = "paMSf3Ny8KAMs1tRLcVOQQhRxTnInHLwP7WtVdm8O_4="
|
||||
self.fernet = Fernet(self.key)
|
||||
|
||||
def encrypt(self, plaintext):
|
||||
return self.fernet.encrypt(plaintext.encode()).decode()
|
||||
|
||||
def decrypt(self, encrypted_text):
|
||||
return self.fernet.decrypt(encrypted_text.encode()).decode()
|
||||
|
||||
|
||||
@@ -77,13 +77,14 @@ THIRD_PARTY_APPS = [
|
||||
"taggit",
|
||||
"django_quill",
|
||||
"corsheaders",
|
||||
'django_extensions',
|
||||
"allauth",
|
||||
"allauth.account",
|
||||
"allauth.socialaccount",
|
||||
"allauth.socialaccount.providers.apple",
|
||||
"allauth.socialaccount.providers.google",
|
||||
"django_filters",
|
||||
# "django_crontab",
|
||||
"django_crontab",
|
||||
# "django_celery_results",
|
||||
# "django_celery_beat",
|
||||
]
|
||||
@@ -211,6 +212,7 @@ TIME_FORMAT = "H:i p"
|
||||
|
||||
# otp expire time limit
|
||||
OTP_EXPIRE_TIME = 1 # mins
|
||||
DEFAULT_CHARSET = 'utf-8'
|
||||
|
||||
|
||||
# Default primary key field type
|
||||
@@ -336,7 +338,7 @@ CHANNEL_LAYERS = {
|
||||
WEBSOCKET_TIMEOUT = 30
|
||||
|
||||
CRONJOBS = [
|
||||
# ("0 9 * * 1-5", "manage_games.cron.update_game_status_live"),
|
||||
# ('0 0 * * *', 'myapp.cron.daily_task >> /path/to/logfile.log 2>&1'),
|
||||
]
|
||||
|
||||
GOOGLE_MAPS_API_KEY = env.str("GOOGLE_MAPS_API_KEY")
|
||||
@@ -356,6 +358,7 @@ TWITTER_ACCESS_TOKEN_SECRET = env.str("TWITTER_ACCESS_TOKEN_SECRET")
|
||||
FACEBOOK_APP_ID = env.str("FACEBOOK_APP_ID")
|
||||
FACEBOOK_APP_SECRET = env.str("FACEBOOK_APP_SECRET")
|
||||
FACEBOOK_PAGE_ID = env.str("FACEBOOK_PAGE_ID")
|
||||
FACEBOOK_GRAPH_VERSION_API = env.str("FACEBOOK_GRAPH_VERSION_API")
|
||||
FACEBOOK_ACCESS_TOKEN = env.str("FACEBOOK_ACCESS_TOKEN")
|
||||
|
||||
# Instagram Key
|
||||
|
||||
@@ -6,7 +6,7 @@ import colorlog
|
||||
# from logging.handlers import TimedRotatingFileHandler
|
||||
|
||||
DEBUG = False
|
||||
ALLOWED_HOSTS = ["staging.goodtimesltd.co.uk", "77.68.8.229"]
|
||||
ALLOWED_HOSTS = ["staging.goodtimesltd.co.uk", "77.68.8.229",".staging.goodtimesltd.co.uk"]
|
||||
|
||||
|
||||
LOGGING_DIR = os.path.join(
|
||||
|
||||
@@ -63,9 +63,9 @@ urlpatterns = [
|
||||
# path('api/', include("accounts.api.urls")),
|
||||
]
|
||||
|
||||
# if settings.DEBUG:
|
||||
# import debug_toolbar
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
if settings.DEBUG:
|
||||
import debug_toolbar
|
||||
|
||||
# urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
# urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
# urlpatterns += [path("__debug__/", include(debug_toolbar.urls))]
|
||||
urlpatterns += [path("__debug__/", include(debug_toolbar.urls))]
|
||||
|
||||
@@ -7,7 +7,7 @@ class EventFilter(filters.FilterSet):
|
||||
"""
|
||||
FilterSet for Event model.
|
||||
"""
|
||||
title = filters.CharFilter(field_name="title", lookup_expr="icontains")
|
||||
title = filters.CharFilter(method="filter_title")
|
||||
location = filters.CharFilter(field_name="venue__address", lookup_expr="icontains")
|
||||
category = filters.CharFilter(method="filter_category")
|
||||
start_date = filters.DateFilter(field_name="start_date", lookup_expr="gte")
|
||||
@@ -29,6 +29,13 @@ class EventFilter(filters.FilterSet):
|
||||
'age_group',
|
||||
]
|
||||
|
||||
def filter_title(self, queryset, name, value):
|
||||
if value:
|
||||
return queryset.filter(
|
||||
Q(title__icontains=value) | Q(tags__name__icontains=value)
|
||||
).distinct()
|
||||
return queryset
|
||||
|
||||
def filter_category(self, queryset, name, value):
|
||||
category = value.split(',')
|
||||
return queryset.filter(category__title__in=category)
|
||||
|
||||
@@ -99,6 +99,7 @@ class EventListSerializer(serializers.ModelSerializer):
|
||||
"entry_fee",
|
||||
"key_guest",
|
||||
"age_group",
|
||||
"link",
|
||||
# "images",
|
||||
# "is_favorited",
|
||||
# "reviews",
|
||||
@@ -107,6 +108,12 @@ class EventListSerializer(serializers.ModelSerializer):
|
||||
# "draft",
|
||||
]
|
||||
|
||||
# def to_representation(self, instance):
|
||||
# """Customize the representation of the instance."""
|
||||
# representation = super().to_representation(instance)
|
||||
# representation['key_guest'] = instance.get_key_guests() # Return as a list
|
||||
# return representation
|
||||
|
||||
|
||||
class EventDetailSerializer(serializers.ModelSerializer):
|
||||
tags = TagSerializer(many=True, read_only=True)
|
||||
@@ -138,6 +145,7 @@ class EventDetailSerializer(serializers.ModelSerializer):
|
||||
"key_guest",
|
||||
"coupon_code",
|
||||
"coupon_description",
|
||||
"link",
|
||||
"age_group",
|
||||
"images",
|
||||
"is_favorited",
|
||||
@@ -179,6 +187,12 @@ class EventDetailSerializer(serializers.ModelSerializer):
|
||||
}
|
||||
return None
|
||||
|
||||
# def to_representation(self, instance):
|
||||
# """Customize the representation of the instance."""
|
||||
# representation = super().to_representation(instance)
|
||||
# representation['key_guest'] = instance.get_key_guests() # Return as a list
|
||||
# return representation
|
||||
|
||||
|
||||
class CreateEventSerializer(serializers.ModelSerializer):
|
||||
tags = TagListSerializerField(required=False)
|
||||
@@ -210,11 +224,19 @@ class CreateEventSerializer(serializers.ModelSerializer):
|
||||
"tags",
|
||||
"coupon_code",
|
||||
"coupon_description",
|
||||
"link"
|
||||
]
|
||||
|
||||
def validate_key_guest(self, value):
|
||||
if value and not isinstance(value, str):
|
||||
raise serializers.ValidationError("key_guest must be a string")
|
||||
return value
|
||||
|
||||
def create(self, validated_data):
|
||||
tags = validated_data.pop("tags", None)
|
||||
images_data = validated_data.pop("images", None)
|
||||
key_guest = validated_data.pop("key_guest", None)
|
||||
|
||||
event = Event.objects.create(**validated_data)
|
||||
|
||||
if tags:
|
||||
@@ -224,11 +246,15 @@ class CreateEventSerializer(serializers.ModelSerializer):
|
||||
for image_data in images_data:
|
||||
EventImage.objects.create(event=event, image=image_data)
|
||||
|
||||
if key_guest:
|
||||
event.set_key_guests(key_guest)
|
||||
event.save()
|
||||
return event
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
tags = validated_data.pop("tags", None)
|
||||
images_data = validated_data.pop("images", None)
|
||||
key_guest = validated_data.pop("key_guest", None)
|
||||
|
||||
# Update fields if there is any change.
|
||||
if tags is not None:
|
||||
@@ -241,6 +267,9 @@ class CreateEventSerializer(serializers.ModelSerializer):
|
||||
for image_data in images_data:
|
||||
EventImage.objects.create(event=instance, image=image_data)
|
||||
|
||||
if key_guest is not None:
|
||||
instance.set_key_guests(key_guest)
|
||||
|
||||
# Update other fields
|
||||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
@@ -248,6 +277,12 @@ class CreateEventSerializer(serializers.ModelSerializer):
|
||||
|
||||
return instance
|
||||
|
||||
# def to_representation(self, instance):
|
||||
# """Customize the representation of the instance."""
|
||||
# representation = super().to_representation(instance)
|
||||
# representation['key_guest'] = instance.get_key_guests() # Return as a list
|
||||
# return representation
|
||||
|
||||
|
||||
class CreateVenueSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
|
||||
@@ -11,7 +11,7 @@ urlpatterns = [
|
||||
name="add_event",
|
||||
),
|
||||
path("edit-event/<int:pk>/", views.EventEditAPIView.as_view(), name="event-edit"),
|
||||
path("get-events/", views.EventsAPIView.as_view(), name="events"),
|
||||
|
||||
path(
|
||||
"event/<int:pk>/",
|
||||
views.EventDetailAPIView.as_view(),
|
||||
@@ -135,6 +135,8 @@ urlpatterns = [
|
||||
name="age_group_list"
|
||||
),
|
||||
|
||||
path("get-events/calendar/", views.EventsCalenderAPIView.as_view(), name="events-calendar"),
|
||||
|
||||
# event list with filter
|
||||
path(
|
||||
"events/",
|
||||
|
||||
@@ -139,6 +139,7 @@ class CreateVenueApi(APIView):
|
||||
def post(self, request):
|
||||
|
||||
data = request.data.copy()
|
||||
print("prindata is ", data)
|
||||
|
||||
# Convert latitude and longitude to float and round to 8 decimal places
|
||||
data["latitude"] = round(float(data["latitude"]), 8)
|
||||
@@ -199,67 +200,6 @@ class VenueDeleteAPIView(APIView):
|
||||
)
|
||||
|
||||
|
||||
class EventsAPIView(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
filter = request.query_params.get("filter", None)
|
||||
query = request.query_params.get("query", None)
|
||||
category_id = request.query_params.get("category_id", None)
|
||||
params = [
|
||||
"expensive",
|
||||
"cheap",
|
||||
"preference",
|
||||
"today",
|
||||
"tomorrow",
|
||||
"category",
|
||||
"key_guest",
|
||||
"tags",
|
||||
]
|
||||
if filter not in params:
|
||||
return ApiResponse.error(
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
message=constants.FAILURE,
|
||||
errors="No filter found",
|
||||
)
|
||||
|
||||
try:
|
||||
if filter == "today":
|
||||
events = services.EventFilterService.filter_events_for_today()
|
||||
elif filter == "tomorrow":
|
||||
events = services.EventFilterService.filter_events_for_tomorrow()
|
||||
elif filter == "key_guest":
|
||||
events = services.EventFilterService.filter_events_by_search(
|
||||
search_query=query
|
||||
)
|
||||
elif filter == "category" and category_id is not None:
|
||||
events = services.EventFilterService.filter_events_by_category(
|
||||
int(category_id)
|
||||
)
|
||||
else:
|
||||
events = services.EventFilterService.filter_events(
|
||||
filter_type=filter, principal=request.user
|
||||
)
|
||||
# serializer = EventDetailSerializer(
|
||||
# events, context={"request": request}, many=True
|
||||
# )
|
||||
serializer = EventListSerializer(
|
||||
events, context={"request": request}, many=True
|
||||
)
|
||||
return ApiResponse.success(
|
||||
status=status.HTTP_200_OK,
|
||||
message=constants.SUCCESS,
|
||||
data=serializer.data,
|
||||
)
|
||||
except Exception as e:
|
||||
return ApiResponse.error(
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
message=constants.FAILURE,
|
||||
errors=str(e),
|
||||
)
|
||||
|
||||
|
||||
class MyEventsAPIView(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
@@ -1040,6 +980,46 @@ class AgeGroupListView(APIView):
|
||||
return ApiResponse.success(message=constants.SUCCESS, data=serializer.data)
|
||||
|
||||
|
||||
class EventsCalenderAPIView(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
try:
|
||||
principal = request.user
|
||||
queryset = Event.objects.filter(
|
||||
active=True,
|
||||
draft=False,
|
||||
deleted=False,
|
||||
end_date__gte=timezone.now().date()
|
||||
)
|
||||
|
||||
# queryset = Event.objects.filter(
|
||||
# ((Q(active=True) & Q(draft=False) & Q(deleted=False) & Q(end_date__gte=timezone.now().date())))
|
||||
# )
|
||||
|
||||
preferences = PrincipalPreference.objects.get(principal=principal)
|
||||
preferred_categories_ids = preferences.preferred_categories.values_list("id", flat=True)
|
||||
|
||||
# Filter the queryset to only include events in the user's preferred categories
|
||||
queryset = queryset.filter(Q(category__in=preferred_categories_ids) | Q(principal=principal))
|
||||
|
||||
serializer = EventListSerializer(
|
||||
queryset, context={"request": request}, many=True
|
||||
)
|
||||
return ApiResponse.success(
|
||||
status=status.HTTP_200_OK,
|
||||
message=constants.SUCCESS,
|
||||
data=serializer.data,
|
||||
)
|
||||
except Exception as e:
|
||||
return ApiResponse.error(
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
message=constants.FAILURE,
|
||||
errors=str(e),
|
||||
)
|
||||
|
||||
|
||||
class EventListView(generics.ListAPIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@@ -46,6 +46,7 @@ class EventForm(forms.ModelForm):
|
||||
"title",
|
||||
# "event_master",
|
||||
"description",
|
||||
"link",
|
||||
"image",
|
||||
"event_images",
|
||||
# "status",
|
||||
@@ -148,6 +149,7 @@ class VenueForm(forms.ModelForm):
|
||||
required=True
|
||||
)
|
||||
image = forms.ImageField(required=True)
|
||||
postcode = forms.CharField(required=True, max_length=10)
|
||||
latitude = forms.DecimalField(
|
||||
widget=forms.NumberInput()
|
||||
)
|
||||
@@ -161,6 +163,7 @@ class VenueForm(forms.ModelForm):
|
||||
"principal",
|
||||
"title",
|
||||
"address",
|
||||
"postcode",
|
||||
"image",
|
||||
"latitude",
|
||||
"longitude",
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
from django.core.mail import EmailMessage
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from datetime import datetime, timedelta
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.utils.timezone import now
|
||||
from django.contrib.auth import get_user_model
|
||||
import calendar
|
||||
|
||||
from manage_events.report import generate_event_report, generate_event_report_pdf_three
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Getting event reports of specific event managers'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('month', type=int, help='Month number (1-12)')
|
||||
parser.add_argument('email', type=str, help='User email address')
|
||||
parser.add_argument('mail_send_on', type=str, help='Email to send the report')
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
month = kwargs['month']
|
||||
email = kwargs['email']
|
||||
mail_send_on = kwargs['mail_send_on']
|
||||
|
||||
# Validate the month
|
||||
if month < 1 or month > 12:
|
||||
self.stdout.write(self.style.ERROR("Invalid month. Must be between 1 and 12."))
|
||||
return
|
||||
|
||||
User = get_user_model()
|
||||
try:
|
||||
user = User.objects.get(email=email)
|
||||
except User.DoesNotExist:
|
||||
self.stdout.write(self.style.ERROR(f"User with email {email} does not exist."))
|
||||
return
|
||||
|
||||
# Calculate start and end dates of the month
|
||||
year = now().year # Assuming the current year
|
||||
start_date = datetime(year, month, 1)
|
||||
last_day = calendar.monthrange(year, month)[1]
|
||||
end_date = datetime(year, month, last_day)
|
||||
|
||||
report_data = generate_event_report(user.id, start_date, end_date)
|
||||
if report_data:
|
||||
pdf_data, filename = generate_event_report_pdf_three(user, report_data, start_date)
|
||||
self.send_email_with_attachment(mail_send_on, pdf_data, filename)
|
||||
|
||||
def send_email_with_attachment(self, email, pdf_data, filename):
|
||||
try:
|
||||
email_message = EmailMessage(
|
||||
subject="Monthly Event Report",
|
||||
body="Please find the attached report for the last month.",
|
||||
to=[email],
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
)
|
||||
email_message.attach(filename, pdf_data, "application/pdf")
|
||||
email_message.send()
|
||||
self.stdout.write(self.style.SUCCESS(f"Email successfully sent to {email} with attachment {filename}."))
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f"Failed to send email to {email}. Error: {str(e)}"))
|
||||
@@ -17,7 +17,7 @@ class Command(BaseCommand):
|
||||
users = event_managers()
|
||||
|
||||
for user in users:
|
||||
report_data = generate_event_report(user.id)
|
||||
report_data = generate_event_report(user.id, start_date, end_date)
|
||||
if report_data:
|
||||
pdf_data, filename = generate_event_report_pdf_three(user, report_data)
|
||||
self.send_email_with_attachment(user.email, pdf_data, filename)
|
||||
|
||||
103
manage_events/management/commands/update_facebook_tokens.py
Normal file
103
manage_events/management/commands/update_facebook_tokens.py
Normal file
@@ -0,0 +1,103 @@
|
||||
|
||||
import os
|
||||
from django.conf import settings
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
# Load .env variables
|
||||
load_dotenv()
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Update Facebook long-lived access tokens and page access token'
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.app_id = settings.FACEBOOK_APP_ID
|
||||
self.app_secret = settings.FACEBOOK_APP_SECRET
|
||||
self.page_id = settings.FACEBOOK_PAGE_ID
|
||||
self.graph_api_version = settings.FACEBOOK_GRAPH_VERSION_API
|
||||
self.page_access_token = settings.FACEBOOK_ACCESS_TOKEN
|
||||
self.long_lived_token = settings.FACEBOOK_ACCESS_TOKEN
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
"""Handle the token refresh and update .env file."""
|
||||
if self.refresh_access_tokens():
|
||||
self.stdout.write(self.style.SUCCESS("Successfully refreshed Facebook tokens."))
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR("Failed to refresh Facebook tokens."))
|
||||
|
||||
def _exchange_short_to_long_lived_token(self, short_lived_token):
|
||||
"""Exchange short-lived token for long-lived token."""
|
||||
try:
|
||||
url = f"https://graph.facebook.com/{self.graph_api_version}/oauth/access_token"
|
||||
params = {
|
||||
"grant_type": "fb_exchange_token",
|
||||
"client_id": self.app_id,
|
||||
"client_secret": self.app_secret,
|
||||
"fb_exchange_token": short_lived_token,
|
||||
}
|
||||
response = requests.get(url, params=params)
|
||||
response.raise_for_status()
|
||||
long_lived_token = response.json().get("access_token")
|
||||
self.stdout.write(self.style.SUCCESS("Successfully exchanged for long-lived user access token."))
|
||||
return long_lived_token
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.stdout.write(self.style.ERROR(f"Error exchanging short-lived token: {e}"))
|
||||
return None
|
||||
|
||||
def _get_page_access_token(self, user_token):
|
||||
"""Retrieve Page Access Token."""
|
||||
try:
|
||||
url = f"https://graph.facebook.com/{self.graph_api_version}/{self.page_id}"
|
||||
params = {
|
||||
"fields": "access_token",
|
||||
"access_token": user_token,
|
||||
}
|
||||
response = requests.get(url, params=params)
|
||||
response.raise_for_status()
|
||||
page_access_token = response.json().get("access_token")
|
||||
self.stdout.write(self.style.SUCCESS("Successfully obtained page access token."))
|
||||
return page_access_token
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.stdout.write(self.style.ERROR(f"Error retrieving page access token: {e}"))
|
||||
return None
|
||||
|
||||
def _update_env_variable(self, key, value):
|
||||
"""Update a variable in the .env file."""
|
||||
with open('.env', 'r') as file:
|
||||
lines = file.readlines()
|
||||
|
||||
with open('.env', 'w') as file:
|
||||
updated = False
|
||||
for line in lines:
|
||||
if line.startswith(key):
|
||||
file.write(f"{key}={value}\n")
|
||||
updated = True
|
||||
else:
|
||||
file.write(line)
|
||||
if not updated:
|
||||
file.write(f"{key}={value}\n")
|
||||
|
||||
def refresh_access_tokens(self):
|
||||
"""Refresh long-lived user access token and page access token."""
|
||||
if not self.long_lived_token:
|
||||
self.stdout.write(self.style.ERROR("No valid long-lived user access token found."))
|
||||
return False
|
||||
|
||||
# Refresh long-lived user token (optional, based on expiry)
|
||||
refreshed_user_token = self._exchange_short_to_long_lived_token(self.long_lived_token)
|
||||
if refreshed_user_token:
|
||||
self.long_lived_token = refreshed_user_token
|
||||
|
||||
# Refresh page access token
|
||||
# page_access_token = self._get_page_access_token(self.long_lived_token)
|
||||
# if page_access_token:
|
||||
# self.page_access_token = page_access_token
|
||||
# # Update tokens in .env file
|
||||
self._update_env_variable("FACEBOOK_ACCESS_TOKEN", self.long_lived_token)
|
||||
# self._update_env_variable("FACEBOOK_PAGE_ACCESS_TOKEN", self.page_access_token)
|
||||
return True
|
||||
|
||||
self.stdout.write(self.style.ERROR("Failed to refresh page access token."))
|
||||
return False
|
||||
18
manage_events/migrations/0017_venue_postcode.py
Normal file
18
manage_events/migrations/0017_venue_postcode.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.2 on 2024-12-20 09:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('manage_events', '0016_freeusagefeaturelimit'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='venue',
|
||||
name='postcode',
|
||||
field=models.CharField(blank=True, max_length=20, null=True),
|
||||
),
|
||||
]
|
||||
18
manage_events/migrations/0018_event_link.py
Normal file
18
manage_events/migrations/0018_event_link.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.2 on 2024-12-24 11:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('manage_events', '0017_venue_postcode'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='link',
|
||||
field=models.URLField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -51,6 +51,7 @@ class Venue(BaseModel):
|
||||
longitude = models.DecimalField(
|
||||
max_digits=14, decimal_places=8, blank=True, null=True
|
||||
)
|
||||
postcode = models.CharField(max_length=20, blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
@@ -124,11 +125,25 @@ class Event(BaseModel):
|
||||
social_media_shares_count = models.IntegerField(default=0)
|
||||
coupon_code = models.CharField(max_length=255, blank=True, null=True)
|
||||
coupon_description = models.TextField(blank=True, null=True)
|
||||
link = models.URLField(max_length=255, blank=True, null=True)
|
||||
|
||||
def increment_shares(self):
|
||||
self.social_media_shares_count += 1
|
||||
self.save()
|
||||
|
||||
def set_key_guests(self, guests):
|
||||
"""Set the key guests as a comma-seperated string."""
|
||||
if isinstance(guests, list):
|
||||
self.key_guest = ",".join(guests)
|
||||
elif isinstance(guests, str):
|
||||
self.key_guest = guests
|
||||
else:
|
||||
raise ValueError("Guests must be a comma-seperated string")
|
||||
|
||||
def get_key_guests(self):
|
||||
"""Return the key guests as a list of strings."""
|
||||
return self.key_guest.split(",") if self.key_guest else []
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
@@ -52,8 +52,8 @@ def get_previous_month_date_range():
|
||||
return first_day_of_previous_month, last_day_of_previous_month
|
||||
|
||||
|
||||
def generate_event_report(user_id):
|
||||
start_date, end_date = get_previous_month_date_range()
|
||||
def generate_event_report(user_id, start_date, end_date):
|
||||
# start_date, end_date = get_previous_month_date_range()
|
||||
user = User.objects.get(id=user_id)
|
||||
|
||||
# events = Event.objects.filter(
|
||||
@@ -167,8 +167,8 @@ def generate_event_report(user_id):
|
||||
return report_data
|
||||
|
||||
|
||||
def generate_event_report_pdf_three(user, report_data):
|
||||
start_date, _ = get_previous_month_date_range()
|
||||
def generate_event_report_pdf_three(user, report_data, start_date):
|
||||
# start_date, _ = get_previous_month_date_range()
|
||||
filename = generate_filename(user.email, start_date)
|
||||
|
||||
buffer = BytesIO()
|
||||
|
||||
@@ -584,7 +584,7 @@ class SocialMediaPostView(generic.View):
|
||||
'success_messages': success_messages
|
||||
}, status=400)
|
||||
|
||||
caption = f"{event.title}\nDuration: {event.start_date} to {event.end_date}\nAddress: {event.venue.address}"
|
||||
caption = f"Venue: {event.venue.title} \n Event: {event.title}\n Description: {event.description} \n Date: {event.start_date} to {event.end_date}\n Time: {event.from_time} - {event.to_time} \n Address: {event.venue.address}"
|
||||
|
||||
if platform in ['instagram', 'facebook', 'twitter', 'all']:
|
||||
if platform in ['twitter', 'all']:
|
||||
@@ -599,6 +599,8 @@ class SocialMediaPostView(generic.View):
|
||||
|
||||
image_url = request.build_absolute_uri(event.image.url)
|
||||
if platform in ['facebook', 'all']:
|
||||
try:
|
||||
print("facebook is called")
|
||||
facebook_api = FacebookAPI()
|
||||
facebook_poster = FacebookPoster(facebook_api)
|
||||
result = facebook_poster.post_photo(image_url, caption)
|
||||
@@ -606,6 +608,9 @@ class SocialMediaPostView(generic.View):
|
||||
success_messages.append("Posted to Facebook successfully")
|
||||
else:
|
||||
errors.append("Fail to post on Facebook")
|
||||
except Exception as e:
|
||||
print(f"facebook error {e}")
|
||||
errors.append("Fail to post on Facebook")
|
||||
|
||||
if platform in ['instagram', 'all']:
|
||||
instagram_api = InstagramAPI()
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.0.2 on 2024-12-20 09:29
|
||||
|
||||
import django_quill.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('manage_subscriptions', '0014_alter_subscription_long_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='subscription',
|
||||
name='long_description',
|
||||
field=django_quill.fields.QuillField(),
|
||||
),
|
||||
]
|
||||
@@ -19,6 +19,7 @@ defusedxml==0.7.1
|
||||
Django==5.0.2
|
||||
django-allauth==0.61.1
|
||||
django-cors-headers==4.3.1
|
||||
django-crontab==0.7.1
|
||||
django-debug-toolbar==4.3.0
|
||||
django-environ==0.11.2
|
||||
django-extensions==3.2.3
|
||||
@@ -32,6 +33,7 @@ djangorestframework==3.14.0
|
||||
djangorestframework-simplejwt==5.3.1
|
||||
et-xmlfile==1.1.0
|
||||
googlemaps==4.10.0
|
||||
gunicorn==23.0.0
|
||||
h11==0.14.0
|
||||
httpcore==1.0.4
|
||||
httpx==0.27.0
|
||||
@@ -46,6 +48,7 @@ oauthlib==3.2.2
|
||||
onesignal-sdk==2.0.0
|
||||
openpyxl==3.1.4
|
||||
orjson==3.9.15
|
||||
packaging==24.2
|
||||
pandas==2.2.2
|
||||
phonenumbers==8.13.30
|
||||
pillow==10.2.0
|
||||
@@ -56,6 +59,7 @@ PyJWT==2.8.0
|
||||
pyngrok==7.1.2
|
||||
pyOpenSSL==24.0.0
|
||||
python-dateutil==2.9.0.post0
|
||||
python-dotenv==1.0.1
|
||||
python3-openid==3.2.0
|
||||
pytz==2024.1
|
||||
PyYAML==6.0.1
|
||||
|
||||
@@ -4,6 +4,27 @@
|
||||
<!-- 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" %}
|
||||
{% include "cdn_through_html/filepond_cdn_css.html" %}
|
||||
|
||||
<style>
|
||||
/* Adjust the width and height of the FilePond container */
|
||||
.filepond--root {
|
||||
max-width: 200px; /* Set your desired width */
|
||||
height: auto; /* Adjust height based on content */
|
||||
margin: auto; /* Center the input */
|
||||
}
|
||||
|
||||
/* Control the size of the preview panel */
|
||||
.filepond--panel {
|
||||
height: 200px !important; /* Set a fixed height for the panel */
|
||||
}
|
||||
|
||||
/* Optionally customize the labels or placeholders */
|
||||
.filepond--label-action {
|
||||
font-size: 14px; /* Adjust font size */
|
||||
}
|
||||
</style>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@@ -12,8 +33,10 @@
|
||||
<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">
|
||||
<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>
|
||||
@@ -24,11 +47,12 @@
|
||||
<div class="statbox widget box box-shadow">
|
||||
<div class="widget-content widget-content-area">
|
||||
|
||||
<form method="post" id="addCustomer">
|
||||
<form method="post" id="addCustomer" 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 class="d-grid"><button class="btn btn-primary btn-block"
|
||||
type="submit">Submit</button></div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -44,14 +68,75 @@
|
||||
|
||||
{% block javascript %}
|
||||
<!-- include required js cdn link through html here -->
|
||||
{% include "cdn_through_html/filepond_cdn_js.html" %}
|
||||
{% 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>
|
||||
|
||||
|
||||
// Register required FilePond plugins
|
||||
FilePond.registerPlugin(
|
||||
FilePondPluginFileValidateType,
|
||||
FilePondPluginImageExifOrientation,
|
||||
FilePondPluginImagePreview,
|
||||
FilePondPluginImageCrop,
|
||||
FilePondPluginImageResize,
|
||||
FilePondPluginImageTransform,
|
||||
);
|
||||
|
||||
|
||||
|
||||
// Initialize FilePond for profile photo input
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const profileInput = FilePond.create(
|
||||
document.querySelector('.filepond'), // Target elements with class "filepond"
|
||||
{
|
||||
storeAsFile: true,
|
||||
imagePreviewHeight: 170,
|
||||
imageCropAspectRatio: '1:1',
|
||||
imageResizeTargetWidth: 200,
|
||||
imageResizeTargetHeight: 200,
|
||||
stylePanelLayout: 'compact circle',
|
||||
styleLoadIndicatorPosition: 'center bottom',
|
||||
styleProgressIndicatorPosition: 'right bottom',
|
||||
styleButtonRemoveItemPosition: 'left bottom',
|
||||
styleButtonProcessItemPosition: 'right bottom',
|
||||
labelIdle: `
|
||||
<span class="no-image-placeholder">
|
||||
<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-user">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
</span>
|
||||
<p class="drag-para">Drag & Drop your picture or
|
||||
<span class="filepond--label-action" tabindex="0">Browse</span>
|
||||
</p>
|
||||
`,
|
||||
// files: [
|
||||
// {
|
||||
// // Preload the server file reference if it exists
|
||||
// source: "{% if form.profile_photo.value %}{{ form.profile_photo.value.url }}{% endif %}",
|
||||
// options: {
|
||||
// type: 'local',
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$(document).ready(function () {
|
||||
|
||||
// Initialize FilePond
|
||||
// var profile = initializeFilePond('id_profile_photo');
|
||||
// var profileUrl = "{% if form.profile_photo.value %}{{ form.profile_photo.value.url }}{% endif %}";
|
||||
// if (profileUrl) {
|
||||
// profile.addFile(profileUrl)
|
||||
// }
|
||||
|
||||
var start_date = flatpickr(document.getElementById('id_free_start_date'), {
|
||||
// minDate: "today",
|
||||
onChange: function (selectedDates, dateStr, instance) {
|
||||
|
||||
@@ -4,6 +4,26 @@
|
||||
<!-- 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" %}
|
||||
{% include "cdn_through_html/filepond_cdn_css.html" %}
|
||||
|
||||
<style>
|
||||
/* Adjust the width and height of the FilePond container */
|
||||
.filepond--root {
|
||||
max-width: 200px; /* Set your desired width */
|
||||
height: auto; /* Adjust height based on content */
|
||||
margin: auto; /* Center the input */
|
||||
}
|
||||
|
||||
/* Control the size of the preview panel */
|
||||
.filepond--panel {
|
||||
height: 200px !important; /* Set a fixed height for the panel */
|
||||
}
|
||||
|
||||
/* Optionally customize the labels or placeholders */
|
||||
.filepond--label-action {
|
||||
font-size: 14px; /* Adjust font size */
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@@ -33,7 +53,7 @@
|
||||
<div class="statbox widget box box-shadow">
|
||||
<div class="widget-content widget-content-area">
|
||||
|
||||
<form method="post" id="addCustomer">
|
||||
<form method="post" id="addCustomer" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% include 'includes/dynamic_template_form.html' with form=form %}
|
||||
<div class="mt-4 mb-0">
|
||||
@@ -53,12 +73,73 @@
|
||||
|
||||
{% block javascript %}
|
||||
<!-- include required js cdn link through html here -->
|
||||
{% include "cdn_through_html/filepond_cdn_js.html" %}
|
||||
{% 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>
|
||||
|
||||
// Register required FilePond plugins
|
||||
FilePond.registerPlugin(
|
||||
FilePondPluginFileValidateType,
|
||||
FilePondPluginImageExifOrientation,
|
||||
FilePondPluginImagePreview,
|
||||
FilePondPluginImageCrop,
|
||||
FilePondPluginImageResize,
|
||||
FilePondPluginImageTransform
|
||||
);
|
||||
|
||||
// Initialize FilePond for profile photo input
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const existingImageUrl = "{% if form.profile_photo.value %}{{ form.profile_photo.value.url }}{% endif %}";
|
||||
|
||||
const profileInput = FilePond.create(
|
||||
document.querySelector('#id_profile_photo'), // Target the file input by its ID
|
||||
{
|
||||
storeAsFile: true,
|
||||
imagePreviewHeight: 170,
|
||||
imageCropAspectRatio: '1:1', // Square aspect ratio for cropping
|
||||
imageResizeTargetWidth: 200, // Resize to 200px width
|
||||
imageResizeTargetHeight: 200, // Resize to 200px height
|
||||
stylePanelLayout: 'compact circle', // Circular layout for the preview
|
||||
styleLoadIndicatorPosition: 'center bottom',
|
||||
styleProgressIndicatorPosition: 'right bottom',
|
||||
styleButtonRemoveItemPosition: 'left bottom',
|
||||
styleButtonProcessItemPosition: 'right bottom',
|
||||
labelIdle: `
|
||||
<span class="no-image-placeholder">
|
||||
<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-user">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
</span>
|
||||
<p class="drag-para">Drag & Drop your picture or
|
||||
<span class="filepond--label-action" tabindex="0">Browse</span>
|
||||
</p>
|
||||
`,
|
||||
allowImageCrop: true, // Enable cropping
|
||||
allowImageResize: true, // Enable resizing
|
||||
imageTransformOutputMimeType: 'image/jpeg', // Ensure the output is a JPEG
|
||||
imageTransformOutputQuality: 90, // Adjust the quality of the output image
|
||||
allowProcess: false, // Prevent automatic processing to maintain cropping
|
||||
}
|
||||
);
|
||||
|
||||
// If there's an existing image, preload it
|
||||
if (existingImageUrl) {
|
||||
fetch(existingImageUrl)
|
||||
.then((response) => response.blob())
|
||||
.then((blob) => {
|
||||
const file = new File([blob], "profile_photo.jpg", { type: blob.type });
|
||||
profileInput.addFile(file);
|
||||
})
|
||||
.catch((error) => console.error("Error loading existing image:", error));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
var start_date = flatpickr(document.getElementById('id_free_start_date'), {
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
{% block stylesheet %}
|
||||
<!-- include required css cdn link through html here -->
|
||||
{% include "cdn_through_html/datatable_cdn_css.html" %}
|
||||
{% include "cdn_through_html/modal_cdn_css.html" %}
|
||||
{% include "cdn_through_html/sweetalert2_cdn_css.html" %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -46,39 +48,39 @@
|
||||
<th class="checkbox-column text-center sorting_asc" tabindex="0"
|
||||
aria-controls="style-3" rowspan="1" colspan="1" aria-sort="ascending"
|
||||
style="width: 69.2656px;"> Record Id </th>
|
||||
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1"
|
||||
style="width: 44.2344px;">Image</th>
|
||||
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
|
||||
style="width: 79.7969px;">First Name</th>
|
||||
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
|
||||
style="width: 77.3281px;">Last Name</th>
|
||||
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
|
||||
style="width: 143.516px;">Email</th>
|
||||
<!-- <th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
|
||||
style="width: 98.875px;">Mobile No.</th> -->
|
||||
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
|
||||
style="width: 98.875px;">Principal Type</th>
|
||||
<th class="text-center sorting" tabindex="0" aria-controls="style-3"
|
||||
rowspan="1" style="width: 44.2344px;">Image</th>
|
||||
<th class="text-center sorting" tabindex="0" aria-controls="style-3"
|
||||
rowspan="1" colspan="1" style="width: 79.7969px;">First Name</th>
|
||||
<th class="text-center sorting" tabindex="0" aria-controls="style-3"
|
||||
rowspan="1" colspan="1" style="width: 77.3281px;">Last Name</th>
|
||||
<th class="text-center sorting" tabindex="0" aria-controls="style-3"
|
||||
rowspan="1" colspan="1" style="width: 143.516px;">Email</th>
|
||||
<th class="text-center sorting" tabindex="0" aria-controls="style-3"
|
||||
rowspan="1" colspan="1" style="width: 98.875px;">#</th>
|
||||
<th class="text-center sorting" tabindex="0" aria-controls="style-3"
|
||||
rowspan="1" colspan="1" style="width: 98.875px;">Principal Type</th>
|
||||
<!-- <th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
|
||||
style="width: 98.875px;">Phone Verified</th> -->
|
||||
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
|
||||
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"
|
||||
style="width: 98.875px;">Modified On</th>
|
||||
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
|
||||
style="width: 98.875px;">Active</th>
|
||||
<th class="text-center sorting" tabindex="0" aria-controls="style-3"
|
||||
rowspan="1" colspan="1" 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" style="width: 98.875px;">Modified On</th>
|
||||
<th class="text-center sorting" tabindex="0" aria-controls="style-3"
|
||||
rowspan="1" colspan="1" style="width: 98.875px;">Active</th>
|
||||
<!-- <th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
|
||||
style="width: 98.875px;">Delete</th> -->
|
||||
<th class="text-center dt-no-sorting" tabindex="0"
|
||||
aria-controls="style-3" rowspan="1" colspan="1"
|
||||
style="width: 51.625px;">Action</th>
|
||||
<th class="text-center dt-no-sorting" tabindex="0" aria-controls="style-3"
|
||||
rowspan="1" colspan="1" style="width: 51.625px;">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -86,19 +88,31 @@
|
||||
<tr role="row">
|
||||
<td class="checkbox-column text-center sorting_1"> {{ data_obj.id }}</td>
|
||||
<td class="text-center">
|
||||
<span><img src="../src/assets/img/profile-17.jpeg" class="profile-img"
|
||||
alt="avatar"></span>
|
||||
<span>
|
||||
<img
|
||||
src="{% if data_obj.profile_photo %}{{ data_obj.profile_photo.url }}{% else %}{% endif %}"
|
||||
class="profile-img"
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ data_obj.first_name }}</td>
|
||||
<td>{{ data_obj.last_name }}</td>
|
||||
<td>{{ data_obj.email }}</td>
|
||||
<!-- <td>{{ data_obj.phone_no }}</td> -->
|
||||
<td>
|
||||
{% if data_obj.extended_data and data_obj.extended_data.is_onboarded and not data_obj.extended_data.is_transferred %}
|
||||
<button class="btn btn-primary btn-sm view-password-btn"
|
||||
data-id="{{ data_obj.id }}" data-email="{{ data_obj.email }}">
|
||||
View
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ data_obj.principal_type.name }}</td>
|
||||
<!-- <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 and data_obj.extended_data.is_onboarded %}badge-primary{% else %}badge-danger{% endif %}">
|
||||
<span
|
||||
class="shadow-none badge {% if data_obj.extended_data and data_obj.extended_data.is_onboarded %}badge-primary{% else %}badge-danger{% endif %}">
|
||||
{% if data_obj.extended_data %}
|
||||
{{ data_obj.extended_data.is_onboarded }}
|
||||
{% else %}
|
||||
@@ -107,7 +121,8 @@
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="shadow-none badge {% if data_obj.extended_data and data_obj.extended_data.is_transferred %}badge-primary{% else %}badge-danger{% endif %}">
|
||||
<span
|
||||
class="shadow-none badge {% if data_obj.extended_data and data_obj.extended_data.is_transferred %}badge-primary{% else %}badge-danger{% endif %}">
|
||||
{% if data_obj.extended_data %}
|
||||
{{ data_obj.extended_data.is_transferred }}
|
||||
{% else %}
|
||||
@@ -118,7 +133,8 @@
|
||||
<td>{{ data_obj.created_on }}</td>
|
||||
<td>{{ data_obj.modified_on }}</td>
|
||||
<td class="text-center">
|
||||
<span class="shadow-none badge {% if data_obj.is_active %}badge-primary{% else %}badge-danger{% endif %}">
|
||||
<span
|
||||
class="shadow-none badge {% if data_obj.is_active %}badge-primary{% else %}badge-danger{% endif %}">
|
||||
{{ data_obj.is_active }}
|
||||
</span>
|
||||
</td>
|
||||
@@ -129,11 +145,12 @@
|
||||
</td> -->
|
||||
<td class="text-center">
|
||||
<ul class="table-controls">
|
||||
<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"
|
||||
width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
<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" 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-edit-2 p-1 br-8 mb-1">
|
||||
@@ -141,8 +158,19 @@
|
||||
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="{% 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>
|
||||
<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>
|
||||
@@ -159,11 +187,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal fade" id="passwordModal" tabindex="-1" role="dialog" aria-labelledby="passwordModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="passwordModalLabel">Customer Details</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p><strong>Email:</strong> <span id="modalEmail"></span></p>
|
||||
<p><strong>Password:</strong> <span id="modalPassword"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block javascript %}
|
||||
<!-- include required js cdn link through html here -->
|
||||
{% include "cdn_through_html/datatable_cdn_js.html" %}
|
||||
{% include "cdn_through_html/sweetalert2_cdn_js.html" %}
|
||||
|
||||
<script>
|
||||
c3 = $('#style-3').DataTable({
|
||||
@@ -185,5 +230,46 @@
|
||||
|
||||
multiCheck(c3);
|
||||
|
||||
var viewUrl = "{% url 'accounts:get_decrypted_password' customer_id=0 %}"
|
||||
$("#style-3").on("click", ".view-password-btn", function () {
|
||||
console.log("passowrd calling")
|
||||
const customerId = $(this).data("id");
|
||||
const email = $(this).data("email");
|
||||
|
||||
// Make the AJAX call
|
||||
$.ajax({
|
||||
// url: `accounts//get-decrypted-password/${customerId}/`,
|
||||
url: viewUrl.replace('0',customerId),
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
// Populate modal with email and decrypted password
|
||||
$("#modalEmail").text(email);
|
||||
$("#modalPassword").text(response.decrypted_password);
|
||||
|
||||
// Show the modal
|
||||
$("#passwordModal").modal("show");
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: 'Error!',
|
||||
text: response.message || "Failed to fetch the password. Please try again.",
|
||||
icon: 'error',
|
||||
showConfirmButton: true
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
console.error("AJAX Error:", error);
|
||||
Swal.fire({
|
||||
title: 'Error!',
|
||||
text: "An error occurred. Please try again.",
|
||||
icon: 'error',
|
||||
showConfirmButton: true
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -57,22 +57,19 @@
|
||||
{% include "cdn_through_html/sweetalert2_cdn_js.html" %}
|
||||
{% include "cdn_through_html/jquery_validate_cdn_js.html" %}
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
// Function to initialize FilePond
|
||||
function initializeFilePond(selector, allowMultiple = false) {
|
||||
FilePond.registerPlugin(
|
||||
FilePondPluginImagePreview,
|
||||
FilePondPluginImageExifOrientation,
|
||||
FilePondPluginFileValidateSize
|
||||
FilePondPluginFileValidateSize,
|
||||
);
|
||||
return FilePond.create(document.getElementById(selector), {
|
||||
allowMultiple: allowMultiple,
|
||||
acceptedFileTypes: ['image/*'],
|
||||
storeAsFile: true,
|
||||
dropOnPage: true
|
||||
dropOnPage: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -195,6 +192,7 @@
|
||||
|
||||
// Initialize Tagify
|
||||
initializeTagify('#id_tags');
|
||||
initializeTagify('#id_key_guest');
|
||||
|
||||
// Handle principal change
|
||||
handlePrincipalChange();
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
aria-sort="ascending" style="width: 69.2656px;"> Title </th>
|
||||
<th class="checkbox-column sorting_asc" tabindex="0" aria-controls="style-3"
|
||||
aria-sort="ascending" style="width: 69.2656px;"> Address </th>
|
||||
<th class="checkbox-column sorting_asc" tabindex="0" aria-controls="style-3"
|
||||
aria-sort="ascending" style="width: 69.2656px;"> Postcode </th>
|
||||
<th class="checkbox-column sorting_asc" tabindex="0" aria-controls="style-3"
|
||||
aria-sort="ascending" style="width: 69.2656px;"> Latitude </th>
|
||||
<th class="checkbox-column sorting_asc" tabindex="0" aria-controls="style-3"
|
||||
@@ -68,6 +70,7 @@
|
||||
</td>
|
||||
<td>{{data_obj.title}}</td>
|
||||
<td>{{data_obj.address}}</td>
|
||||
<td>{{data_obj.postcode}}</td>
|
||||
<td>{{data_obj.latitude}}</td>
|
||||
<td>{{data_obj.longitude}}</td>
|
||||
<td class="text-center">
|
||||
|
||||
Reference in New Issue
Block a user