Compare commits

...

12 Commits

Author SHA1 Message Date
bobbyvish
9d20583a78 fix: event multi image upload 2025-01-23 12:04:24 +05:30
bobbyvish
5693ec3c48 fixed:(customer): fixed issue of veiw button 2025-01-08 13:41:42 +05:30
bobbyvish
bec8f9d608 Update(Customer): added profile photo field 2025-01-08 12:37:07 +05:30
bobbyvish
b559c62926 updated(event): added link field in event 2024-12-24 17:00:59 +05:30
bobbyvish
24949ade3e feat(customer): customer cred and postcode for address 2024-12-20 19:43:44 +05:30
bobbyvish
3425d82891 updated : requirement.txt 2024-12-02 12:05:45 +05:30
bobbyvish
8ccec7012c refactor: custom command to update social key 2024-12-02 11:51:51 +05:30
bobbyvish
3f263b2af5 fixed: filter title with tag and calendar issue 2024-11-28 16:05:32 +05:30
bobbyvish
76ac9413c4 fix: report isssue and custom command to generate individual marchant report 2024-11-28 15:56:20 +05:30
bobbyvish
76323a1ea1 Merge branch 'development' of https://github.com/WDI-Ideas/goodtimes into development 2024-11-06 14:40:28 +05:30
BOBBY VISHWAKARMA
9ba960fcad Merge pull request #102 from WDI-Ideas/feature/key-guest-multiple
Updated : key guest for multiple
2024-10-31 17:49:19 +05:30
bobbyvish
6f07fe4877 Updated : key guest for multiple 2024-10-30 12:36:05 +05:30
30 changed files with 959 additions and 382 deletions

View File

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

View File

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

View File

@@ -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,
@@ -333,9 +334,16 @@ 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,

View File

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

View File

@@ -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")
@@ -862,6 +872,27 @@ class CustomerListView(LoginRequiredMixin, generic.ListView):
context = super().get_context_data(**kwargs)
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
@@ -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(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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",
@@ -178,6 +186,12 @@ class EventDetailSerializer(serializers.ModelSerializer):
"status_display": interaction.get_status_display(),
}
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):
@@ -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:

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

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

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

View File

@@ -51,13 +51,14 @@ 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
class AgeGroups(BaseModel):
name = models.CharField(max_length=10, unique=True)
class Meta:
db_table = "age_group"
@@ -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

View File

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

View File

@@ -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,12 +599,17 @@ class SocialMediaPostView(generic.View):
image_url = request.build_absolute_uri(event.image.url)
if platform in ['facebook', 'all']:
facebook_api = FacebookAPI()
facebook_poster = FacebookPoster(facebook_api)
result = facebook_poster.post_photo(image_url, caption)
if result["success"]:
success_messages.append("Posted to Facebook successfully")
else:
try:
print("facebook is called")
facebook_api = FacebookAPI()
facebook_poster = FacebookPoster(facebook_api)
result = facebook_poster.post_photo(image_url, caption)
if result["success"]:
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']:

View File

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

View File

@@ -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
@@ -71,7 +75,7 @@ stripe==8.2.0
tqdm==4.66.2
tweepy==4.14.0
Twisted==23.10.0
#twisted-iocpsupport==1.0.4
# twisted-iocpsupport==1.0.4
txaio==23.1.1
typing_extensions==4.9.0
tzdata==2024.1

View File

@@ -1,9 +1,30 @@
{% 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" %}
<!-- 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,10 +33,12 @@
<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 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>
@@ -24,14 +47,15 @@
<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>
</div>
</div>
</div>
@@ -43,155 +67,216 @@
{% 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>
<!-- 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>
$(document).ready(function() {
<script>
var start_date = flatpickr(document.getElementById('id_free_start_date'), {
// minDate: "today",
onChange: function(selectedDates, dateStr, instance) {
end_date.set('minDate', selectedDates[0]);
// 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) {
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,
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.");
// Add custom validation method for greater than start date
$.validator.addMethod("greaterThanStartDate", function (value, element) {
var startDate = $('#id_free_start_date').val();
if (!startDate || !value) {
return true;
}
return new Date(value) > new Date(startDate);
}, "The end date must be after the start date.");
$("#addCustomer").validate({
rules: {
first_name: {
required: true,
maxlength: 15,
noSpecialChars: true,
startsWithLetter: true,
},
last_name: {
required: true,
maxlength: 15,
noSpecialChars: true,
startsWithLetter: true,
},
email: {
required: true,
validEmail: true,
},
preferences: {
required: true,
minlength: 1
},
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)
}
});
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,
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.");
// Add custom validation method for greater than start date
$.validator.addMethod("greaterThanStartDate", function(value, element) {
var startDate = $('#id_free_start_date').val();
if (!startDate || !value) {
return true;
},
messages: {
first_name: {
required: "Please enter your first name.",
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.",
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."
}
return new Date(value) > new Date(startDate);
}, "The end date must be after the start date.");
},
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) {
$("#addCustomer").validate({
rules: {
first_name: {
required: true,
maxlength:15,
noSpecialChars: true,
startsWithLetter: true,
},
last_name: {
required: true,
maxlength: 15,
noSpecialChars: true,
startsWithLetter: true,
},
email: {
required: true,
validEmail: true,
},
preferences: {
required: true,
minlength: 1
},
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.",
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.",
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."
}
},
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) {
// Check if form is valid before submission
if ($(form).valid()) {
// Disable the submit button to prevent multiple submissions
$('button[type="submit"]').prop('disabled', true);
form.submit();
}
// Check if form is valid before submission
if ($(form).valid()) {
// Disable the submit button to prevent multiple submissions
$('button[type="submit"]').prop('disabled', true);
form.submit();
}
});
}
});
// Trigger validation for select2 fields on change
$('#id_preferences').on('change', function() {
$(this).valid();
});
// Trigger validation for select2 fields on change
$('#id_preferences').on('change', function () {
$(this).valid();
});
// Trigger validation for flatpickr fields on change
$('#id_free_start_date').on('change', function() {
$(this).valid();
});
$('#id_free_end_date').on('change', function() {
$(this).valid();
});
})
</script>
// Trigger validation for flatpickr fields on change
$('#id_free_start_date').on('change', function () {
$(this).valid();
});
$('#id_free_end_date').on('change', function () {
$(this).valid();
});
})
</script>
{% endblock %}

View File

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

View File

@@ -1,8 +1,10 @@
{% extends 'layout/base_template.html' %}
{% load static %}
{% block stylesheet %}
<!-- include required css cdn link through html here -->
{% include "cdn_through_html/datatable_cdn_css.html" %}
<!-- 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 %}
@@ -19,7 +21,7 @@
<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>
@@ -27,12 +29,12 @@
<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>
<div class="row layout-spacing">
<div class="col-lg-12">
<div class="statbox widget box box-shadow">
@@ -46,79 +48,93 @@
<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>
{% for data_obj in data_objs %}
<tr role="row">
<td class="checkbox-column text-center sorting_1"> {{ data_obj.id }} </td>
<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 }}
{{ data_obj.extended_data.is_onboarded }}
{% else %}
False
False
{% endif %}
</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 }}
{{ data_obj.extended_data.is_transferred }}
{% else %}
False
False
{% endif %}
</span>
</td>
<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,9 +158,20 @@
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>
</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>
@@ -159,31 +187,89 @@
</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" %}
<script>
c3 = $('#style-3').DataTable({
"dom": "<'dt--top-section'<'row'<'col-12 col-sm-6 d-flex justify-content-sm-start justify-content-center'l><'col-12 col-sm-6 d-flex justify-content-sm-end justify-content-center mt-sm-0 mt-3'f>>>" +
"<'table-responsive'tr>" +
"<'dt--bottom-section d-sm-flex justify-content-sm-between text-center'<'dt--pages-count mb-sm-0 mb-3'i><'dt--pagination'p>>",
"oLanguage": {
"oPaginate": { "sPrevious": '<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-arrow-left"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>', "sNext": '<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-arrow-right"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>' },
"sInfo": "Showing page _PAGE_ of _PAGES_",
"sSearch": '<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-search"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>',
"sSearchPlaceholder": "Search...",
"sLengthMenu": "Results : _MENU_",
},
"order": [[ 0, "desc" ]],
"stripeClasses": [],
"lengthMenu": [5, 10, 20, 50],
"pageLength": 10
<!-- 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({
"dom": "<'dt--top-section'<'row'<'col-12 col-sm-6 d-flex justify-content-sm-start justify-content-center'l><'col-12 col-sm-6 d-flex justify-content-sm-end justify-content-center mt-sm-0 mt-3'f>>>" +
"<'table-responsive'tr>" +
"<'dt--bottom-section d-sm-flex justify-content-sm-between text-center'<'dt--pages-count mb-sm-0 mb-3'i><'dt--pagination'p>>",
"oLanguage": {
"oPaginate": { "sPrevious": '<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-arrow-left"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>', "sNext": '<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-arrow-right"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>' },
"sInfo": "Showing page _PAGE_ of _PAGES_",
"sSearch": '<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-search"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>',
"sSearchPlaceholder": "Search...",
"sLengthMenu": "Results : _MENU_",
},
"order": [[0, "desc"]],
"stripeClasses": [],
"lengthMenu": [5, 10, 20, 50],
"pageLength": 10
});
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
});
},
});
});
multiCheck(c3);
</script>
</script>
{% endblock %}

View File

@@ -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),{
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();

View File

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