From e5017899e3db461f50c8d906cffe8ff5eca711a4 Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Fri, 24 May 2024 12:18:58 +0530 Subject: [PATCH 001/187] refactor(setting): structure setting for different env --- goodtimes/settings/production.py | 8 ++++---- goodtimes/settings/staging.py | 4 ++-- manage_communications/utils.py | 5 +++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/goodtimes/settings/production.py b/goodtimes/settings/production.py index b2413da..6675744 100644 --- a/goodtimes/settings/production.py +++ b/goodtimes/settings/production.py @@ -6,7 +6,7 @@ from logging.handlers import TimedRotatingFileHandler DEBUG = False -ALLOWED_HOSTS = ["goodtimes.betadelivery.com", "154.41.254.33"] +ALLOWED_HOSTS = ["admin.goodtimesltd.co.uk", "77.68.29.148"] LOGGING_DIR = os.path.join( @@ -61,7 +61,7 @@ LOGGING = { } -BASE_DOMAIN = "https://goodtimes.betadelivery.com" +BASE_DOMAIN = "https://admin.goodtimesltd.co.uk" # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ @@ -77,8 +77,8 @@ STATIC_URL = "/static/" STATICFILES_DIRS = [BASE_DIR.joinpath("static")] STRIPE_CHECKOUT_URL = ( - "https://staging.goodtimesltd.co.uk/subscriptions/stripe-subscription/" + "https://admin.goodtimesltd.co.uk/subscriptions/stripe-subscription/" ) STRIPE_FINAL_URL = ( - "https://staging.goodtimesltd.co.uk/subscriptions/create-checkout-session/" + "https://admin.goodtimesltd.co.uk/subscriptions/create-checkout-session/" ) diff --git a/goodtimes/settings/staging.py b/goodtimes/settings/staging.py index 7d4d773..c3aa480 100644 --- a/goodtimes/settings/staging.py +++ b/goodtimes/settings/staging.py @@ -6,7 +6,7 @@ import colorlog # from logging.handlers import TimedRotatingFileHandler DEBUG = False -ALLOWED_HOSTS = ["127.0.0.1", "77.68.8.229", "staging.goodtimesltd.co.uk"] +ALLOWED_HOSTS = ["staging.goodtimesltd.co.uk", "77.68.8.229"] # LOGGING_DIR = os.path.join( @@ -60,7 +60,7 @@ ALLOWED_HOSTS = ["127.0.0.1", "77.68.8.229", "staging.goodtimesltd.co.uk"] # }, # } -# BASE_DOMAIN = "https://goodtimes.betadelivery.com" +BASE_DOMAIN = "https://staging.goodtimesltd.co.uk" # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ diff --git a/manage_communications/utils.py b/manage_communications/utils.py index 1c7f74a..a9edfd2 100644 --- a/manage_communications/utils.py +++ b/manage_communications/utils.py @@ -2,7 +2,8 @@ import logging from threading import Thread # Configure logging at the beginning of your application -logging.basicConfig(level=logging.INFO, filename='app.log', filemode='a', format='%(name)s - %(levelname)s - %(message)s') +# logging.basicConfig(level=logging.INFO, filename='app.log', filemode='a', format='%(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) def send_email_async(email_service): try: @@ -11,6 +12,6 @@ def send_email_async(email_service): except Exception as e: # Log the exception print(f"Failed to send email: {e}") - logging.error(f"Failed to send email: {e}") + logger.error(f"Failed to send email: {e}") # Optionally, you could use other means to notify you of the failure, # such as sending an alert to an admin email, or using a monitoring service. \ No newline at end of file From 348ed6e91020d047a39138b2df09d9f5841915d7 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 24 May 2024 12:29:18 +0530 Subject: [PATCH 002/187] production url updated for stripe --- goodtimes/settings/production.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/goodtimes/settings/production.py b/goodtimes/settings/production.py index b2413da..37f9bf9 100644 --- a/goodtimes/settings/production.py +++ b/goodtimes/settings/production.py @@ -77,8 +77,8 @@ STATIC_URL = "/static/" STATICFILES_DIRS = [BASE_DIR.joinpath("static")] STRIPE_CHECKOUT_URL = ( - "https://staging.goodtimesltd.co.uk/subscriptions/stripe-subscription/" + "https://admin.goodtimesltd.co.uk/subscriptions/stripe-subscription/" ) STRIPE_FINAL_URL = ( - "https://staging.goodtimesltd.co.uk/subscriptions/create-checkout-session/" + "https://admin.goodtimesltd.co.uk/subscriptions/create-checkout-session/" ) From 5b98584c576c6745022ad891d60e4b199281d733 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 24 May 2024 15:59:39 +0530 Subject: [PATCH 003/187] handling active subscription if exist --- goodtimes/settings/base.py | 1 + goodtimes/settings/staging.py | 90 +++++++++++++++---------------- manage_subscriptions/api/views.py | 10 ++-- manage_subscriptions/views.py | 35 ++++++------ 4 files changed, 71 insertions(+), 65 deletions(-) diff --git a/goodtimes/settings/base.py b/goodtimes/settings/base.py index 75e1ce9..7a4febd 100644 --- a/goodtimes/settings/base.py +++ b/goodtimes/settings/base.py @@ -304,6 +304,7 @@ SIMPLE_JWT = { STRIPE_SECRET_KEY = env.str("STRIPE_SECRET_KEY") STRIPE_PUBLISH_KEY = env.str("STRIPE_PUBLISH_KEY") + ONE_SIGNAL_APP_ID = env.str("ONE_SIGNAL_APP_ID") ONE_SIGNAL_API_KEY = env.str("ONE_SIGNAL_API_KEY") diff --git a/goodtimes/settings/staging.py b/goodtimes/settings/staging.py index c3aa480..30b9c16 100644 --- a/goodtimes/settings/staging.py +++ b/goodtimes/settings/staging.py @@ -9,56 +9,56 @@ DEBUG = False ALLOWED_HOSTS = ["staging.goodtimesltd.co.uk", "77.68.8.229"] -# LOGGING_DIR = os.path.join( -# BASE_DIR, "logs" -# ) # Define the directory where log files will be stored +LOGGING_DIR = os.path.join( + BASE_DIR, "logs" +) # Define the directory where log files will be stored # Ensure the directory exists; create it if it doesn't -# if not os.path.exists(LOGGING_DIR): -# os.makedirs(LOGGING_DIR) +if not os.path.exists(LOGGING_DIR): + os.makedirs(LOGGING_DIR) -# LOGGING_LEVEL = env.str( -# "LOG_LEVEL", "INFO" -# ) # Set your desired log level (e.g., DEBUG, INFO, WARNING, ERROR) in the env file +LOGGING_LEVEL = env.str( + "LOG_LEVEL", "INFO" +) # Set your desired log level (e.g., DEBUG, INFO, WARNING, ERROR) -# LOGGING = { -# "version": 1, -# "disable_existing_loggers": False, -# "formatters": { -# "verbose": { -# "()": colorlog.ColoredFormatter, -# "format": "%(cyan)s%(asctime)s%(reset)s | %(red)s[%(levelname)8s]%(reset)s | [ %(yellow)s%(name)s.%(module)s:%(white)s%(lineno)d%(reset)s - %(green)s%(funcName)10s()%(reset)s ] --> %(message)s", -# "datefmt": "%Y-%m-%d %H:%M:%S", -# "log_colors": { -# "DEBUG": "white", -# "INFO": "green", -# "WARNING": "yellow", -# "ERROR": "red", -# "CRITICAL": "bold_red", -# }, -# "secondary_log_colors": {}, -# "style": "%", -# } -# }, -# "handlers": { -# "logfile": { -# "level": LOGGING_LEVEL, -# "class": "logging.handlers.RotatingFileHandler", -# "filename": os.path.join(LOGGING_DIR, "goodtimes_staging_error.log"), -# 'maxBytes': 5242880, # 5*1024*1024 bytes (5MB) -# "backupCount": 10, # Number of log files to keep (15 days' worth of logs) -# "formatter": "verbose", -# }, -# }, -# "loggers": { -# "django": { -# "handlers": ["logfile"], -# "level": LOGGING_LEVEL, -# "propagate": False, -# }, -# }, -# } +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": { + "()": colorlog.ColoredFormatter, + "format": "%(cyan)s%(asctime)s%(reset)s | %(red)s[%(levelname)8s]%(reset)s | [ %(yellow)s%(name)s.%(module)s:%(white)s%(lineno)d%(reset)s - %(green)s%(funcName)10s()%(reset)s ] --> %(message)s", + "datefmt": "%Y-%m-%d %H:%M:%S", + "log_colors": { + "DEBUG": "white", + "INFO": "green", + "WARNING": "yellow", + "ERROR": "red", + "CRITICAL": "bold_red", + }, + "secondary_log_colors": {}, + "style": "%", + } + }, + "handlers": { + "logfile": { + "level": LOGGING_LEVEL, + "class": "logging.handlers.RotatingFileHandler", + "filename": os.path.join(LOGGING_DIR, "godtimes_pro_error.log"), + "maxBytes": 5242880, # 5*1024*1024 bytes (5MB) + "backupCount": 10, # Number of log files to keep (15 days' worth of logs) + "formatter": "verbose", + }, + }, + "loggers": { + "django": { + "handlers": ["logfile"], + "level": LOGGING_LEVEL, + "propagate": False, + }, + }, +} BASE_DOMAIN = "https://staging.goodtimesltd.co.uk" diff --git a/manage_subscriptions/api/views.py b/manage_subscriptions/api/views.py index 05cd2e6..c583aa6 100644 --- a/manage_subscriptions/api/views.py +++ b/manage_subscriptions/api/views.py @@ -195,11 +195,11 @@ class StripeWebhookTest(APIView): ) # Check if there is an active principal subscription - if self._has_active_principal_subscription(principal_id): - return ApiResponse.success( - status=status.HTTP_208_ALREADY_REPORTED, - message="Active principal subscription already exists", - ) + # if self._has_active_principal_subscription(principal_id): + # return ApiResponse.success( + # status=status.HTTP_208_ALREADY_REPORTED, + # message="Active principal subscription already exists", + # ) # payment_service = services.PaymentProcessingService(webhook_data=event) payment_service = PaymentProcessingService(webhook_data=event) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index dbbdcec..24933d6 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -405,6 +405,17 @@ def stripe_config(request): return JsonResponse(stripe_config, safe=False) +def _has_active_principal_subscription(principal_id): + return PrincipalSubscription.objects.filter( + principal__id=principal_id, + active=True, + deleted=False, + cancelled=False, + is_paid=True, + end_date__gte=timezone.now().date(), + ).exists() + + @csrf_exempt @require_POST def create_checkout_session(request): @@ -412,6 +423,13 @@ def create_checkout_session(request): data = json.loads(request.body) print("data: ", data) subscription_id = data.get("subscriptionId", None) + principal_id = request.user.id + + if _has_active_principal_subscription(principal_id): + return ApiResponse.success( + status=status.HTTP_208_ALREADY_REPORTED, + message="Active principal subscription already exists", + ) try: subscription = Subscription.objects.get(id=subscription_id) @@ -437,21 +455,6 @@ def create_checkout_session(request): comment="Principal Subscription Initiated", ) - # subscription_days = subscription.plan.days - # today = timezone.now().date() - # last_date = today + timedelta(days=int(subscription_days)) - - # To Avoid Duplicacy of Principal Subscription - # principal_subscription = PrincipalSubscription.objects.create( - # principal=request.user, - # subscription=subscription, - # is_paid=False, - # order_id=order_id, - # start_date=today, - # end_date=last_date, - # grace_period_end_date=last_date + timedelta(days=15), - # ) - try: # customer = stripe.Customer.create( # email=request.user.email, @@ -485,7 +488,9 @@ def create_checkout_session(request): "quantity": 1, } ], + # allow_promotion_codes=True, mode="payment", + # discounts=[{"coupon": "VLMAsicx"}], success_url=request.build_absolute_uri("/subscriptions/success/"), cancel_url=request.build_absolute_uri("/subscriptions/cancel/"), metadata={ From c5e3a8afd14a6041ad53bfae51bba5b185c3044b Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 24 May 2024 16:12:27 +0530 Subject: [PATCH 004/187] handling active subscription if exist 2 --- templates/stripe_html/index.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/templates/stripe_html/index.html b/templates/stripe_html/index.html index 34789d0..152a687 100644 --- a/templates/stripe_html/index.html +++ b/templates/stripe_html/index.html @@ -52,6 +52,13 @@
+
+ {% for message in messages %} + + {% endfor %} +
From ff433e4a765205a62938a924322228c6134d68c9 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 24 May 2024 16:22:45 +0530 Subject: [PATCH 005/187] handling active subscription if exist 3 --- manage_subscriptions/views.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 24933d6..f6e47dd 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -419,6 +419,7 @@ def _has_active_principal_subscription(principal_id): @csrf_exempt @require_POST def create_checkout_session(request): + success_url = reverse_lazy("manage_subscriptions:stripe") stripe.api_key = settings.STRIPE_SECRET_KEY data = json.loads(request.body) print("data: ", data) @@ -426,10 +427,8 @@ def create_checkout_session(request): principal_id = request.user.id if _has_active_principal_subscription(principal_id): - return ApiResponse.success( - status=status.HTTP_208_ALREADY_REPORTED, - message="Active principal subscription already exists", - ) + messages.success(request, "Active principal subscription already exists") + return redirect(success_url) try: subscription = Subscription.objects.get(id=subscription_id) From f35ffded815e6ef917e54ef187565029a83b2599 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 24 May 2024 16:39:00 +0530 Subject: [PATCH 006/187] html web view --- manage_subscriptions/views.py | 6 +- templates/stripe_html/index.html | 176 +++++++++++++++++++------------ 2 files changed, 109 insertions(+), 73 deletions(-) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index f6e47dd..cf1c929 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -1,6 +1,6 @@ from datetime import timedelta import json -from django.http import HttpResponseBadRequest, JsonResponse +from django.http import HttpResponseBadRequest, JsonResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect, render import stripe from accounts import resource_action @@ -427,8 +427,8 @@ def create_checkout_session(request): principal_id = request.user.id if _has_active_principal_subscription(principal_id): - messages.success(request, "Active principal subscription already exists") - return redirect(success_url) + messages.error(request, "Active principal subscription already exists") + return HttpResponseRedirect(success_url) try: subscription = Subscription.objects.get(id=subscription_id) diff --git a/templates/stripe_html/index.html b/templates/stripe_html/index.html index 152a687..40ab87f 100644 --- a/templates/stripe_html/index.html +++ b/templates/stripe_html/index.html @@ -52,14 +52,7 @@
-
- {% for message in messages %} - - {% endfor %} -
- +
@@ -81,46 +74,7 @@
--> - {% for subscription in subscriptions %} -
-
- -
{{ subscription.title }}
- {% if subscription.image %} - {{ subscription.title }} - {% endif %} -
-
- {% if subscription.short_description %} -

{{ subscription.short_description }}

- {% endif %} - {% if subscription.long_description %} -

{{ subscription.long_description|truncatewords:20 }}

- {% endif %} -
Subscription Amount
- {% if subscription.high_amount and subscription.high_amount > subscription.amount %} -

£ {{ subscription.high_amount }} £ {{ subscription.amount }}

- {% else %} -

£ {{ subscription.amount }}

- {% endif %} - {% if subscription.plan.days %} -

Days of Subscription: {{ subscription.plan.days }}

- {% else %} -

Days of Subscription: Not available

- {% endif %} - -
-
- - -
-
-
- {% empty %} -

No subscriptions available.

- {% endfor %} +
@@ -136,6 +90,55 @@
+
+
+
+ {% for subscription in subscriptions %} +
+
+ +
{{ subscription.title }} +
+ {% if subscription.image %} + {{ subscription.title }} + {% endif %} +
+
+ {% if subscription.short_description %} +

{{ subscription.short_description }}

+ {% endif %} + {% if subscription.long_description %} +

{{ subscription.long_description|truncatewords:20 }}

+ {% endif %} +
Subscription Amount
+ {% if subscription.high_amount and subscription.high_amount > subscription.amount %} +

£ {{ subscription.high_amount }} £ {{ subscription.amount }} +

+ {% else %} +

£ {{ subscription.amount }}

+ {% endif %} + {% if subscription.plan.days %} +

Days of Subscription: {{ subscription.plan.days }}

+ {% else %} +

Days of Subscription: Not available

+ {% endif %} + +
+
+ + +
+
+
+ {% empty %} +

No subscriptions available.

+ {% endfor %} +
+ +
+
+
@@ -276,11 +279,13 @@

-

-
+
Good Times covers a wide range of events, including concerts, sporting events, @@ -292,125 +297,156 @@

-

- The Good Times App is a revolutionary mobile application designed to bring people together by providing a comprehensive listing of local and regional events. It caters to a wide range of interests and passions, making it easier for users to find and connect with events they love. + The Good Times App is a revolutionary mobile application designed to bring people + together by providing a comprehensive listing of local and regional events. It caters to + a wide range of interests and passions, making it easier for users to find and connect + with events they love.

-

- For every new user that signs up for the Good Times App using a referral code, the referrer earns 1 G-Token. The referrer continues to earn G-Tokens for each monthly subscription payment made by the referred user. These tokens can be sold back to Good Times Ltd for cash or used towards event payments. + For every new user that signs up for the Good Times App using a referral code, the + referrer earns 1 G-Token. The referrer continues to earn G-Tokens for each monthly + subscription payment made by the referred user. These tokens can be sold back to Good + Times Ltd for cash or used towards event payments.

-

- G-Tokens are valued at 25% of the minimum monthly subscription cost. The funds from sold G-Tokens are paid into the user's bank account of choice, according to the app's terms and conditions. + G-Tokens are valued at 25% of the minimum monthly subscription cost. The funds from sold + G-Tokens are paid into the user's bank account of choice, according to the app's terms + and conditions.

-

- No, there's no limit to the number of G-Tokens you can sell back to Good Times. However, withdrawals can only be made once per month. + No, there's no limit to the number of G-Tokens you can sell back to Good Times. However, + withdrawals can only be made once per month.

-

- The Good Times App partners with local event organizers and venues, granting them 24-hour control over their event profiles and promotions. This ensures that customers receive the most recent and accurate event information directly from the source. + The Good Times App partners with local event organizers and venues, granting them + 24-hour control over their event profiles and promotions. This ensures that customers + receive the most recent and accurate event information directly from the source.

-

- Events are ranked based on a unique algorithm that considers customer interaction, website traffic to the event's page, and the event's 1-to-5 star reviews. This helps users find popular and highly regarded events tailored to their interests. + Events are ranked based on a unique algorithm that considers customer interaction, + website traffic to the event's page, and the event's 1-to-5 star reviews. This helps + users find popular and highly regarded events tailored to their interests.

-

- No, the app does not facilitate direct event bookings. However, it provides links to event owners' or promoters' websites, where users can make bookings. + No, the app does not facilitate direct event bookings. However, it provides links to + event owners' or promoters' websites, where users can make bookings.

-

- No, Good Times does not charge partners for traffic data to their websites. The app focuses on facilitating easy access to events for users without additional costs for traffic. + No, Good Times does not charge partners for traffic data to their websites. The app + focuses on facilitating easy access to events for users without additional costs for + traffic.

-

- Safety and security are paramount for Good Times. The app employs the latest technologies and practices to protect user data and privacy. While it vets each event and organizer, it also encourages users to perform their due diligence for added safety. + Safety and security are paramount for Good Times. The app employs the latest + technologies and practices to protect user data and privacy. While it vets each event + and organizer, it also encourages users to perform their due diligence for added safety.

-

- If you cancel your subscription, Good Times offers account recovery, allowing you to pick up where you left off, including any referrals made previously. There is also a 15-day grace period for missed payments, after which wallet funds will be lost if the payment is not made. + If you cancel your subscription, Good Times offers account recovery, allowing you to + pick up where you left off, including any referrals made previously. There is also a + 15-day grace period for missed payments, after which wallet funds will be lost if the + payment is not made.
From 446535855b77231fed571fd17777d6d3a8e4ad28 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 24 May 2024 16:43:02 +0530 Subject: [PATCH 007/187] html web view 2 --- templates/stripe_html/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/stripe_html/index.html b/templates/stripe_html/index.html index 40ab87f..d3182d7 100644 --- a/templates/stripe_html/index.html +++ b/templates/stripe_html/index.html @@ -87,6 +87,7 @@
+
From 478783054b43813f1853828d812e6b5a6007d7bf Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 24 May 2024 16:51:50 +0530 Subject: [PATCH 008/187] html web view 3 --- templates/stripe_html/index.html | 81 +++++++++++++++++--------------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/templates/stripe_html/index.html b/templates/stripe_html/index.html index d3182d7..eced950 100644 --- a/templates/stripe_html/index.html +++ b/templates/stripe_html/index.html @@ -92,51 +92,54 @@
-
-
+
+
{% for subscription in subscriptions %} -
-
- -
{{ subscription.title }} -
- {% if subscription.image %} - {{ subscription.title }} - {% endif %} -
-
- {% if subscription.short_description %} -

{{ subscription.short_description }}

- {% endif %} - {% if subscription.long_description %} -

{{ subscription.long_description|truncatewords:20 }}

- {% endif %} -
Subscription Amount
- {% if subscription.high_amount and subscription.high_amount > subscription.amount %} -

£ {{ subscription.high_amount }} £ {{ subscription.amount }} -

- {% else %} -

£ {{ subscription.amount }}

- {% endif %} - {% if subscription.plan.days %} -

Days of Subscription: {{ subscription.plan.days }}

- {% else %} -

Days of Subscription: Not available

- {% endif %} +
+
+
+ +
{{ subscription.title }} +
+ {% if subscription.image %} + {{ subscription.title }} + {% endif %} +
+
+ {% if subscription.short_description %} +

{{ subscription.short_description }}

+ {% endif %} + {% if subscription.long_description %} +

{{ subscription.long_description|truncatewords:20 }}

+ {% endif %} +
Subscription Amount
+ {% if subscription.high_amount and subscription.high_amount > subscription.amount %} +

£ {{ subscription.high_amount }} £ {{ subscription.amount + }} +

+ {% else %} +

£ {{ subscription.amount }}

+ {% endif %} + {% if subscription.plan.days %} +

Days of Subscription: {{ subscription.plan.days }}

+ {% else %} +

Days of Subscription: Not available

+ {% endif %} +
+
+ + +
-
- - -
+
+ {% empty %} +

No subscriptions available.

-
- {% empty %} -

No subscriptions available.

{% endfor %} -
+
From a6979aba61bc37785920aba059cf576b8786d5e0 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 24 May 2024 17:44:38 +0530 Subject: [PATCH 009/187] html web view --- static/src/assets/css/payment/style.css | 1481 ++++++++++++----------- templates/stripe_html/index.html | 30 +- 2 files changed, 766 insertions(+), 745 deletions(-) diff --git a/static/src/assets/css/payment/style.css b/static/src/assets/css/payment/style.css index e20914f..43ef5f8 100644 --- a/static/src/assets/css/payment/style.css +++ b/static/src/assets/css/payment/style.css @@ -1,808 +1,821 @@ +@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap') @import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap') * { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +:root { + --black: #000000; + --light-black: #050505; + --main-yellow: rgba(209, 170, 88, 1); + --light-yellow: rgba(229, 25, 94, 0.2); + --white: #ffffff; + --light-white: #f8f8f8; + --white-other: rgba(207, 207, 207, 1); + --white-mix: #cecece; + --border: #ff72a285; +} + +body { + font-family: "Poppins", sans-serif; +} + +.ptb { + padding: 40px 0; +} + +.sec-heading { + font-size: 38px; + font-weight: 600; + text-align: center; + /* padding-top: 40px; */ + color: var(--main-yellow); +} + +.sec-subheading { + font-size: 18px; + font-weight: 400; + text-align: center; + color: var(--white); +} - @import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap') @import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap') * { - margin: 0; - padding: 0; - box-sizing: border-box; - font-family: "Poppins", sans-serif; +.big-heading { + font-size: 52px; + font-weight: 700; + color: var(--white); + letter-spacing: 1.8px; +} + + +.para { + font-size: 18px; + font-weight: 400; + color: rgba(255, 255, 255, 1); +} + +.sec-mini-heading { + font-size: 24px; + font-weight: 600; +} + + +.para-dark { + font-size: 24px; + /* font-weight: 600; */ +} + +.para-mid { + font-size: 18px; + color: rgba(255, 255, 255, 0.69); +} + +li { + list-style: none; +} + +.pt { + padding: 40px 0; +} + + +/* header */ + +header { + border-bottom: 1px solid var(--main-yellow); + position: absolute; + background-color: transparent; + top: 0; + left: 0; + width: 100%; + padding: 22px 0; + display: flex; + align-items: center; + z-index: 9999; +} + +header .header-main-inner { + display: flex; + align-items: center; + justify-content: space-between; +} + +header nav ul { + display: flex; + gap: 80px; + align-items: center; + margin: 0; +} + + +.header-main-inner .logo { + width: 212px; + height: 52px; +} + +.header-main-inner .logo img { + width: 100%; +} + + +header nav ul .menu-btn img { + width: 24px; + height: 24px; +} + +header nav ul li a { + color: var(--white); + font-size: 18px; + /* font-weight: 600; */ + position: relative; + text-decoration: none; +} + +.sticky { + background-color: var(--black); + position: fixed; + animation: slideDown 0.8s ease-out; + -webkit-animation: slideDown 0.8s ease-out; +} + +@keyframes slideDown { + 0% { + transform: translateY(-100%); } - :root { - --black: #000000; - --light-black: #050505; - --main-yellow: rgba(209, 170, 88, 1); - --light-yellow: rgba(229, 25, 94, 0.2); - --white: #ffffff; - --light-white: #f8f8f8; - --white-other: rgba(207, 207, 207, 1); - --white-mix: #cecece; - --border: #ff72a285; + 100% { + transform: translateY(0); } +} - body { - font-family: "Poppins", sans-serif; +header a.active { + color: var(--main-yellow); +} + +header nav ul li a::after { + content: ""; + position: absolute; + left: 50%; + width: 0%; + bottom: -10px; + border-bottom: 2px solid var(--main-yellow); + transition: all 0.3s; + transform: translateX(-50%); +} + +header nav ul li a:hover { + color: var(--main-yellow); +} + +header nav ul li a:hover:after { + width: 100%; +} + +.hamburger { + display: none; +} + +.hamburger { + position: relative; + width: 25px; + height: 25px; + display: none; + cursor: pointer; +} + +.hamburger img { + width: 25px; + height: 25px; +} + + +.overlay { + display: none; +} + +.cross-btn { + padding: 2px 20px; + text-align: right; + display: none; + font-size: 40px; + cursor: pointer; + color: var(--main-yellow); +} + +.cross-btn i { + font-size: 20px; + font-weight: 500; +} + +/* about-head */ +.head-sec header, +.terms-sec header { + /* position: inherit; */ + background-color: var(--black); +} + + + + +/* baner */ +.baner-section { + background-image: linear-gradient(rgba(4, 9, 10, 0.7), rgba(4, 9, 10, 0.7)), + url("https://goodtimes.betadelivery.com/static/images/baner.jpg"); + background-position: center; + background-size: cover; + height: 100vh; + display: flex; + align-items: center; +} + +.baner-section .row { + align-items: center; + padding-top: 100px; +} + +.baner-section .store-app { + display: flex; + align-items: center; + gap: 15px; + margin: 30px 0; +} + +.baner-img { + text-align: center; + padding-top: 20px; +} + +.baner-section .baner-img img { + width: 75%; +} + +.baner-content .big-heading span { + color: var(--main-yellow); +} + +.baner-content .grey-para { + margin-top: 24px; + font-size: 20px; + color: var(--white-other); +} + + +.baner-btn { + margin-top: 24px; +} + + +/* plan */ + +.modal-body .your-plan { + margin-top: 15px; +} + +.modal-body .your-plans-main { + padding: 0px 10px 30px; +} + +.modal-body .your-plans-main .head { + font-size: 25px; + color: black; + font-weight: 500; +} + +.modal-body .your-plans-main .monthly-div-main { + box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; + padding: 15px 20px; + border-radius: 8px; +} + +.modal-body .your-plans-main .monthly-div-main .monthly-div { + box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; + padding: 6px 10px; + border-radius: 8px; + border: 1px solid rgb(215 169 72 / 28%); +} + +.modal-body .your-plans-main .your-heading { + color: var(--main-yellow); + font-size: 24px; + font-weight: 500; +} + +.modal-body .your-plans-main .your-subheading { + font-size: 24px; + font-weight: 500; + color: black; +} + +.modal-body .your-plans-main .your-subheading span { + font-size: 18px; + font-weight: 400; +} + +.modal-body .your-plans-btn { + text-align: center; + margin-top: 20px; +} + +.modal-body .your-plans-btn .common-btn { + width: 100%; +} + +/* plan end */ + +.common-btn { + background: linear-gradient(90.02deg, #CDA34C 0.02%, #F1D6A0 52%, #D1A956 98.68%); + font-weight: 500; + border: none; + font-size: 18px; + font-weight: 600; + padding: 10px 40px; + border-radius: 5px; +} + + +/* key-feature */ +.key-features { + background-color: #10100e; + color: white; +} + +.key-features .row { + align-items: center; + padding-bottom: 40px; +} + +.key-features .key-main-img img { + width: 75%; +} + +.key-features .key-right-first, +.key-right-second { + display: flex; + align-items: start; + gap: 20px; +} + +.key-right-second { + margin-top: 60px; +} + +/* easy-steps */ +.easy-steps { + background-color: var(--black); +} + +.easy-steps .para-dark { + margin-top: 50px; + color: var(--white); +} + +.easy-steps .para { + color: rgba(192, 192, 192, 1); + text-align: center; +} + +.easy-steps-main { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 25px; + margin-top: 100px; + padding-bottom: 40px; +} + +.easy-steps-first { + background-color: rgba(255, 255, 255, 0.05); + border: 1px solid var(--main-yellow); + border-top-left-radius: 14px; + border-top-right-radius: 14px; + display: flex; + align-items: center; + flex-direction: column; + position: relative; + height: 400px; + padding: 0px 20px; +} + +.easy-steps-first-img-num { + width: 75px; + height: 75px; + position: absolute; + top: -50px; +} + +img.easy-steps-first-img-bot { + position: absolute; + bottom: -1px; +} + +/* Adventure + */ + + +.Adventure { + background-color: #10100e; + color: white; +} + +.Adventure .row { + align-items: center; + padding-top: 40px; +} + +.Adventure-rti { + display: flex; + align-items: center; + gap: 20px; + margin-bottom: 40px; +} + +.Adventure-right img { + width: 75%; +} + +.Adventure-left .store-app { + display: flex; + gap: 15px; +} + +.Adventure-btn { + margin-top: 40px; +} + + +/* faq */ +.faq { + background-color: black; +} + +.main-faq { + padding: 40px 0 80px; +} + +div#accordionExample { + display: flex; + flex-direction: column; + gap: 30px; +} + +.faq .accordion-item { + border: 1px solid var(--main-yellow); + background-color: transparent; + color: #fff; + border-radius: 5px; +} + +.faq button.accordion-button:focus { + box-shadow: inherit; +} + +.faq button.accordion-button { + background-color: transparent; + color: var(--white); +} + +.accordion-button:not(.collapsed) { + color: var(--main-yellow) !important; + font-family: "Nunito Sans", sans-serif; + font-weight: 600; +} + +.accordion-item:first-of-type .accordion-button { + box-shadow: none; +} + +.accordion-button::after { + background-image: url(images/ab.png); +} + +.accordion-button:not(.collapsed)::after { + background-image: url(images/at.png); + transform: none; +} + + + +/* footer */ +.footer { + background-color: rgba(16, 16, 14, 1); +} + +.footer .footer-main-img { + width: 212px; + height: 52px; + padding: 40px 0; +} + +.footer .footer-main-img img { + width: 100%; +} + +.footer .footer-main-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + color: var(--white); + padding: 3rem 0 2rem; +} + +.footer .footer-main-grid .para-dark { + font-size: 18px; + font-weight: 600; +} + +.footer .footer-main-grid .para { + font-size: 16px; +} + + +.footer .store-app { + display: flex; + gap: 15px; + margin: 0; + flex-direction: column; +} + +.footer-btn .common-btn { + margin-bottom: 16px; +} + +.footer-main-grid-fourth { + margin-top: -20px; +} + +.copy-right { + color: var(--main-yellow); + text-decoration: none; + font-size: 16px; + display: flex; + align-items: center; + justify-content: center; + padding-bottom: 20px; +} + +.store-app img { + width: 165px; +} + + +/* About us */ + +.about-us { + background-image: url(images/About\ Us\ Banner.png); + background-position: top; + background-size: cover; + background-repeat: no-repeat; + height: 500px; + display: flex; + align-items: center; + margin-top: 90px; +} + +.about-us .row { + align-items: center; +} + + +.about, +.terms { + background-color: var(--black); + color: var(--white); + padding: 40px 0 70px; +} + + +.terms-main { + background-image: url(images/terms.png); + background-position: top; + background-size: cover; + background-repeat: no-repeat; + height: 500px; + display: flex; + align-items: center; + margin-top: 90px; +} + +.card_design { + background: #000; + padding: 40px 0; +} + +.gold-text { + color: rgb(209 170 88); + margin: 0; + padding: 12px; +} + +.bg_color { + background-color: #e5e2e2; + border: none; + color: #000; + font-weight: 600; + border-radius: 5px; +} + +.feat-card { + border: 1px solid #d1aa588c; + padding: 35px; + border-radius: 6px; + background-color: #00000080; + text-align: center; +} + +/* mediascreen */ +@media (max-width: 1199px) { + .big-heading br { + display: none; } +} - .ptb { - padding: 40px 0; - } - - .sec-heading { - font-size: 38px; - font-weight: 600; - text-align: center; - /* padding-top: 40px; */ - color: var(--main-yellow); - } - - .sec-subheading { - font-size: 18px; - font-weight: 400; - text-align: center; - color: var(--white); +@media (max-width: 1024px) { + .big-heading { + font-size: 42px; } +} +@media (max-width: 991px) { + /* .big-heading br { + display: none; + } */ .big-heading { - font-size: 52px; - font-weight: 700; - color: var(--white); - letter-spacing: 1.8px; + font-size: 35px; } - - .para { - font-size: 18px; - font-weight: 400; - color: rgba(255, 255, 255, 1); + .store-app img { + width: 142px; } .sec-mini-heading { - font-size: 24px; - font-weight: 600; - } - - - .para-dark { - font-size: 24px; - /* font-weight: 600; */ - } - - .para-mid { font-size: 18px; - color: rgba(255, 255, 255, 0.69); } - li { - list-style: none; + .para { + font-size: 14px; } - .pt { - padding: 40px 0; + .easy-steps-main { + grid-template-columns: repeat(2, 1fr); + gap: 70px 20px; + margin-top: 70px; } - - /* header */ - - header { - border-bottom: 1px solid var(--main-yellow); - position: absolute; - background-color: transparent; - top: 0; - left: 0; - width: 100%; - padding: 22px 0; - display: flex; - align-items: center; - z-index: 9999; + .footer .footer-main-grid { + grid-template-columns: repeat(2, 1fr); } - header .header-main-inner { - display: flex; - align-items: center; - justify-content: space-between; + .footer-main-grid-fourth { + margin-top: 0px; } - header nav ul { - display: flex; - gap: 80px; - align-items: center; - margin: 0; +} + + + +@media (max-width: 767px) { + .ptb { + padding: 20px 0 40px 0; } - - .header-main-inner .logo { - width: 212px; - height: 52px; - } - - .header-main-inner .logo img { - width: 100%; - } - - - header nav ul .menu-btn img { - width: 24px; - height: 24px; - } - - header nav ul li a { - color: var(--white); - font-size: 18px; - /* font-weight: 600; */ - position: relative; - text-decoration: none; - } - - .sticky { - background-color: var(--black); - position: fixed; - animation: slideDown 0.8s ease-out; - -webkit-animation: slideDown 0.8s ease-out; - } - - @keyframes slideDown { - 0% { - transform: translateY(-100%); - } - - 100% { - transform: translateY(0); - } - } - - header a.active { - color: var(--main-yellow); - } - - header nav ul li a::after { - content: ""; - position: absolute; - left: 50%; - width: 0%; - bottom: -10px; - border-bottom: 2px solid var(--main-yellow); - transition: all 0.3s; - transform: translateX(-50%); - } - - header nav ul li a:hover { - color: var(--main-yellow); - } - - header nav ul li a:hover:after { - width: 100%; - } - - .hamburger { - display: none; - } - - .hamburger { - position: relative; - width: 25px; - height: 25px; - display: none; - cursor: pointer; - } - - .hamburger img { - width: 25px; - height: 25px; - } - - .overlay { - display: none; + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: -100%; + background-color: #60606054; + } .cross-btn { - padding: 2px 20px; - text-align: right; - display: none; - font-size: 40px; - cursor: pointer; - color: var(--main-yellow); + display: block; } - .cross-btn i { - font-size: 20px; - font-weight: 500; + .hamburger, + .overlay { + display: block; } - /* about-head */ - .head-sec header, - .terms-sec header { - /* position: inherit; */ - background-color: var(--black); + .navs { + position: fixed; + top: 0; + left: -100%; + width: 300px; + height: 100%; + background: rgb(0 0 0); + transition: all .3s; + z-index: 1; + } + + .navs ul { + flex-direction: column; + padding: 00px 20px; + align-items: start; + gap: 14px; + } + + .navs ul li a { + color: whitesmoke; + } + + .common-btn { + width: 175px; + height: 40px; } - - - /* baner */ .baner-section { - background-image: linear-gradient(rgba(4, 9, 10, 0.7), rgba(4, 9, 10, 0.7)), - url("https://goodtimes.betadelivery.com/static/images/baner.jpg"); - background-position: center; - background-size: cover; - height: 100vh; - display: flex; - align-items: center; + height: inherit; + padding: 40px 0; } .baner-section .row { - align-items: center; - padding-top: 100px; - } - - .baner-section .store-app { - display: flex; - align-items: center; - gap: 15px; - margin: 30px 0; - } - - .baner-img { - text-align: center; - padding-top: 20px; - } - - .baner-section .baner-img img { - width: 75%; - } - - .baner-content .big-heading span { - color: var(--main-yellow); - } - - .baner-content .grey-para { - margin-top: 24px; - font-size: 20px; - color: var(--white-other); + flex-direction: column-reverse; } + /* .baner-section .store-app { + justify-content: center; + } */ .baner-btn { - margin-top: 24px; - } - - - /* plan */ - - .modal-body .your-plan { - margin-top: 15px; - } - - .modal-body .your-plans-main { - padding: 0px 10px 30px; - } - - .modal-body .your-plans-main .head { - font-size: 25px; - color: black; - font-weight: 500; - } - - .modal-body .your-plans-main .monthly-div-main { - box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; - padding: 15px 20px; - border-radius: 8px; - } - - .modal-body .your-plans-main .monthly-div-main .monthly-div { - box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; - padding: 6px 10px; - border-radius: 8px; - border: 1px solid rgb(215 169 72 / 28%); - } - - .modal-body .your-plans-main .your-heading { - color: var(--main-yellow); - font-size: 24px; - font-weight: 500; - } - - .modal-body .your-plans-main .your-subheading { - font-size: 24px; - font-weight: 500; - color: black; - } - - .modal-body .your-plans-main .your-subheading span { - font-size: 18px; - font-weight: 400; - } - - .modal-body .your-plans-btn { - text-align: center; - margin-top: 20px; - } - - .modal-body .your-plans-btn .common-btn { - width: 100%; - } - - /* plan end */ - - .common-btn { - background: linear-gradient(90.02deg, #CDA34C 0.02%, #F1D6A0 52%, #D1A956 98.68%); - width: 252px; - /* height: 50px; */ - font-weight: 500; - border: none; - font-size: 18px; - font-weight: 600; - padding: 12px 0; - } - - - /* key-feature */ - .key-features { - background-color: #10100e; - color: white; - } - - .key-features .row { - align-items: center; - padding-bottom: 40px; - } - - .key-features .key-main-img img { - width: 75%; - } - - .key-features .key-right-first, - .key-right-second { - display: flex; - align-items: start; - gap: 20px; - } - - .key-right-second { - margin-top: 60px; - } - - /* easy-steps */ - .easy-steps { - background-color: var(--black); - } - - .easy-steps .para-dark { - margin-top: 50px; - color: var(--white); - } - - .easy-steps .para { - color: rgba(192, 192, 192, 1); text-align: center; } .easy-steps-main { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 25px; - margin-top: 100px; - padding-bottom: 40px; - } - - .easy-steps-first { - background-color: rgba(255, 255, 255, 0.05); - border: 1px solid var(--main-yellow); - border-top-left-radius: 14px; - border-top-right-radius: 14px; - display: flex; - align-items: center; - flex-direction: column; - position: relative; - height: 400px; - padding: 0px 20px; - } - - .easy-steps-first-img-num { - width: 75px; - height: 75px; - position: absolute; - top: -50px; - } - - img.easy-steps-first-img-bot { - position: absolute; - bottom: -1px; - } - - /* Adventure - */ - - - .Adventure { - background-color: #10100e; - color: white; - } - - .Adventure .row { - align-items: center; - padding-top: 40px; - } - - .Adventure-rti { - display: flex; - align-items: center; - gap: 20px; - margin-bottom: 40px; - } - - .Adventure-right img { - width: 75%; - } - - .Adventure-left .store-app { - display: flex; - gap: 15px; - } - - .Adventure-btn { - margin-top: 40px; - } - - - /* faq */ - .faq { - background-color: black; - } - - .main-faq { - padding: 40px 0 80px; - } - - div#accordionExample { - display: flex; - flex-direction: column; - gap: 30px; - } - - .faq .accordion-item { - border: 1px solid var(--main-yellow); - background-color: transparent; - color: #fff; - border-radius: 5px; - } - - .faq button.accordion-button:focus { - box-shadow: inherit; - } - - .faq button.accordion-button { - background-color: transparent; - color: var(--white); - } - - .accordion-button:not(.collapsed) { - color: var(--main-yellow) !important; - font-family: "Nunito Sans", sans-serif; - font-weight: 600; - } - - .accordion-item:first-of-type .accordion-button { - box-shadow: none; - } - - .accordion-button::after { - background-image: url(images/ab.png); - } - - .accordion-button:not(.collapsed)::after { - background-image: url(images/at.png); - transform: none; - } - - - - /* footer */ - .footer { - background-color: rgba(16, 16, 14, 1); - } - - .footer .footer-main-img { - width: 212px; - height: 52px; - padding: 40px 0; - } - - .footer .footer-main-img img { - width: 100%; + grid-template-columns: repeat(1, 1fr); } .footer .footer-main-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - color: var(--white); - padding: 3rem 0 2rem; + grid-template-columns: repeat(2, 1fr); + gap: 20px; } - .footer .footer-main-grid .para-dark { - font-size: 18px; - font-weight: 600; + .easy-steps-first { + height: 400px; } - .footer .footer-main-grid .para { + .key-features .key-main-img img { + width: 50%; + margin-bottom: 20px; + } + + .Adventure-rti { + margin-bottom: 20px; + } + + .Adventure .row { + flex-direction: column-reverse; + gap: 40px; + padding: 0; + } + + .Adventure { + padding: 30px 0; + } + + .Adventure-btn { + margin-top: 30px; + } + + .Adventure-right img { + width: 50%; + } + + .sec-heading { + font-size: 30px; + padding-top: 0; + } + + .about .para-mid, + .terms .para-mid { font-size: 16px; } - - .footer .store-app { - display: flex; - gap: 15px; - margin: 0; - flex-direction: column; + .faq { + padding: 30px 0; } - .footer-btn .common-btn { - margin-bottom: 16px; + .main-faq { + padding: 20px 0 30px; } - .footer-main-grid-fourth { - margin-top: -20px; - } - - .copy-right { - color: var(--main-yellow); - text-decoration: none; - font-size: 16px; - display: flex; - align-items: center; - justify-content: center; - padding-bottom: 20px; - } - - .store-app img { - width: 165px; - } - - - /* About us */ - - .about-us { - background-image: url(images/About\ Us\ Banner.png); - background-position: top; - background-size: cover; - background-repeat: no-repeat; - height: 500px; - display: flex; - align-items: center; - margin-top: 90px; - } - - .about-us .row { - align-items: center; - } - - - .about, - .terms { - background-color: var(--black); - color: var(--white); - padding: 40px 0 70px; - } - - - .terms-main { - background-image: url(images/terms.png); - background-position: top; - background-size: cover; - background-repeat: no-repeat; - height: 500px; - display: flex; - align-items: center; - margin-top: 90px; - } - - .gold-text { - color: rgb(209 170 88); - } - .feat-card { - border: 1px solid #d1aa588c; - padding: 18px; - width: 300px; - border-radius: 6px; - background-color: #00000080; - } - - /* mediascreen */ - @media (max-width: 1199px) { - .big-heading br { - display: none; - } - } - - @media (max-width: 1024px) { - .big-heading { - font-size: 42px; - } - } - - @media (max-width: 991px) { - /* .big-heading br { + br { display: none; - } */ - - .big-heading { - font-size: 35px; - } - - .store-app img { - width: 142px; - } - - .sec-mini-heading { - font-size: 18px; - } - - .para { - font-size: 14px; - } - - .easy-steps-main { - grid-template-columns: repeat(2, 1fr); - gap: 70px 20px; - margin-top: 70px; - } - - .footer .footer-main-grid { - grid-template-columns: repeat(2, 1fr); - } - - .footer-main-grid-fourth { - margin-top: 0px; - } - } +} - - - @media (max-width: 767px) { - .ptb { - padding: 20px 0 40px 0; - } - - .overlay { - position: fixed; - width: 100%; - height: 100%; - top: 0; - left: -100%; - background-color: #60606054; - - } - - .cross-btn { - display: block; - } - - .hamburger, - .overlay { - display: block; - } - - .navs { - position: fixed; - top: 0; - left: -100%; - width: 300px; - height: 100%; - background: rgb(0 0 0); - transition: all .3s; - z-index: 1; - } - - .navs ul { - flex-direction: column; - padding: 00px 20px; - align-items: start; - gap: 14px; - } - - .navs ul li a { - color: whitesmoke; - } - - .common-btn { - width: 175px; - height: 40px; - } - - - .baner-section { - height: inherit; - padding: 40px 0; - } - - .baner-section .row { - flex-direction: column-reverse; - } - - /* .baner-section .store-app { - justify-content: center; - } */ - - .baner-btn { - text-align: center; - } - - .easy-steps-main { - grid-template-columns: repeat(1, 1fr); - } - - .footer .footer-main-grid { - grid-template-columns: repeat(2, 1fr); - gap: 20px; - } - - .easy-steps-first { - height: 400px; - } - - .key-features .key-main-img img { - width: 50%; - margin-bottom: 20px; - } - - .Adventure-rti { - margin-bottom: 20px; - } - - .Adventure .row { - flex-direction: column-reverse; - gap: 40px; - padding: 0; - } - - .Adventure { - padding: 30px 0; - } - - .Adventure-btn { - margin-top: 30px; - } - - .Adventure-right img { - width: 50%; - } - - .sec-heading { - font-size: 30px; - padding-top: 0; - } - - .about .para-mid, - .terms .para-mid { - font-size: 16px; - } - - .faq { - padding: 30px 0; - } - - .main-faq { - padding: 20px 0 30px; - } - - br { - display: none; - } - } - - @media (max-width: 444px) { - /* .easy-steps-main { +@media (max-width: 444px) { + /* .easy-steps-main { grid-template-columns: repeat(1, 1fr); } */ - .footer .footer-main-grid { - grid-template-columns: repeat(1, 1fr); - gap: 0px; - } + .footer .footer-main-grid { + grid-template-columns: repeat(1, 1fr); + gap: 0px; + } - .footer .store-app { - margin-bottom: 16px; - } - } \ No newline at end of file + .footer .store-app { + margin-bottom: 16px; + } +} \ No newline at end of file diff --git a/templates/stripe_html/index.html b/templates/stripe_html/index.html index eced950..671b7de 100644 --- a/templates/stripe_html/index.html +++ b/templates/stripe_html/index.html @@ -52,7 +52,13 @@
- +
+ {% for message in messages %} + + {% endfor %} +
@@ -91,7 +97,7 @@
-
+
{% for subscription in subscriptions %} @@ -99,13 +105,13 @@
-
{{ subscription.title }} +
{{ subscription.title }}
{% if subscription.image %} {{ subscription.title }} {% endif %}
-
+
{% if subscription.short_description %}

{{ subscription.short_description }}

{% endif %} @@ -114,16 +120,16 @@ {% endif %}
Subscription Amount
{% if subscription.high_amount and subscription.high_amount > subscription.amount %} -

£ {{ subscription.high_amount }} £ {{ subscription.amount +

£ {{ subscription.high_amount }} £ {{ subscription.amount }}

{% else %} -

£ {{ subscription.amount }}

+

£ {{ subscription.amount }}

{% endif %} {% if subscription.plan.days %} -

Days of Subscription: {{ subscription.plan.days }}

+

Days of Subscription: {{ subscription.plan.days }}

{% else %} -

Days of Subscription: Not available

+

Days of Subscription: Not available

{% endif %}
@@ -133,10 +139,12 @@ now
-
- {% empty %} -

No subscriptions available.

+ +
+
+ {% empty %} +

No subscriptions available.

{% endfor %}
From 899c2c16858ede4a8b1023cd60c0a7e486223794 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 24 May 2024 19:51:55 +0530 Subject: [PATCH 010/187] existing subscription validation --- manage_subscriptions/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index cf1c929..6614a8b 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -405,7 +405,7 @@ def stripe_config(request): return JsonResponse(stripe_config, safe=False) -def _has_active_principal_subscription(principal_id): +def has_active_principal_subscription(principal_id): return PrincipalSubscription.objects.filter( principal__id=principal_id, active=True, @@ -426,7 +426,8 @@ def create_checkout_session(request): subscription_id = data.get("subscriptionId", None) principal_id = request.user.id - if _has_active_principal_subscription(principal_id): + if has_active_principal_subscription(principal_id): + print("Active principal subscription already exists.") messages.error(request, "Active principal subscription already exists") return HttpResponseRedirect(success_url) From c13f992e364663c778e94ccb905ca0c881e6ac0f Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 24 May 2024 20:10:34 +0530 Subject: [PATCH 011/187] existing subscription validation 2 --- manage_subscriptions/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 6614a8b..e0058ef 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -429,7 +429,7 @@ def create_checkout_session(request): if has_active_principal_subscription(principal_id): print("Active principal subscription already exists.") messages.error(request, "Active principal subscription already exists") - return HttpResponseRedirect(success_url) + return redirect(success_url) try: subscription = Subscription.objects.get(id=subscription_id) From 4040ddfd43723f42c1f8ca88a8105225545cde94 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 24 May 2024 20:28:51 +0530 Subject: [PATCH 012/187] existing subscription validation 3 --- templates/stripe_html/index.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/templates/stripe_html/index.html b/templates/stripe_html/index.html index 671b7de..9d1f1c2 100644 --- a/templates/stripe_html/index.html +++ b/templates/stripe_html/index.html @@ -120,8 +120,7 @@ {% endif %}
Subscription Amount
{% if subscription.high_amount and subscription.high_amount > subscription.amount %} -

£ {{ subscription.high_amount }} £ {{ subscription.amount - }} +

£ {{ subscription.high_amount }} £ {{ subscription.amount }}

{% else %}

£ {{ subscription.amount }}

From cb0b0a3e68f556b92d13f7a95354cb17d6607e9a Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 24 May 2024 20:54:35 +0530 Subject: [PATCH 013/187] existing subscription validation 4 --- manage_subscriptions/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index e0058ef..3f7fa56 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -350,10 +350,11 @@ class SubscriptionPageView(TemplateView): def get(self, request, *args, **kwargs): # Example of extracting the token from a query parameter or cookie - token = request.GET.get("token") - # token = request.GET.get("token") or request.COOKIES.get("jwt") + token = request.GET.get("token") or request.session.get("jwt") print("token: ", token) if token: + request.session['jwt'] = token + print("request.session: ", request.session) try: # Decode and validate token payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) From ed5d5253f03dce048dcb014afffbc1a0c254ce92 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 24 May 2024 20:58:40 +0530 Subject: [PATCH 014/187] existing subscription validation 5 --- manage_subscriptions/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 3f7fa56..8c11b60 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -430,7 +430,7 @@ def create_checkout_session(request): if has_active_principal_subscription(principal_id): print("Active principal subscription already exists.") messages.error(request, "Active principal subscription already exists") - return redirect(success_url) + return redirect("manage_subscriptions:stripe") try: subscription = Subscription.objects.get(id=subscription_id) From d4a49fcf568f5388e2caf5fa0a6501108cdc333b Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 24 May 2024 21:08:36 +0530 Subject: [PATCH 015/187] existing subscription validation 6 --- manage_subscriptions/views.py | 3 +-- templates/stripe_html/index.html | 8 ++------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 8c11b60..383e465 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -429,8 +429,7 @@ def create_checkout_session(request): if has_active_principal_subscription(principal_id): print("Active principal subscription already exists.") - messages.error(request, "Active principal subscription already exists") - return redirect("manage_subscriptions:stripe") + return JsonResponse({"error-message": "Active principal subscription already exists"}, status=400) try: subscription = Subscription.objects.get(id=subscription_id) diff --git a/templates/stripe_html/index.html b/templates/stripe_html/index.html index 9d1f1c2..1914b80 100644 --- a/templates/stripe_html/index.html +++ b/templates/stripe_html/index.html @@ -52,12 +52,8 @@
-
- {% for message in messages %} - - {% endfor %} +
+
From ca08d0aeb0cd6ad3935d6e5098a0d971f0575ec1 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 24 May 2024 21:16:30 +0530 Subject: [PATCH 016/187] existing subscription validation 7 --- manage_subscriptions/views.py | 10 +++++----- templates/stripe_html/index.html | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 383e465..b943887 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -353,7 +353,7 @@ class SubscriptionPageView(TemplateView): token = request.GET.get("token") or request.session.get("jwt") print("token: ", token) if token: - request.session['jwt'] = token + request.session["jwt"] = token print("request.session: ", request.session) try: # Decode and validate token @@ -429,14 +429,14 @@ def create_checkout_session(request): if has_active_principal_subscription(principal_id): print("Active principal subscription already exists.") - return JsonResponse({"error-message": "Active principal subscription already exists"}, status=400) + return JsonResponse( + {"error": "Active principal subscription already exists"}, status=400 + ) try: subscription = Subscription.objects.get(id=subscription_id) except Subscription.DoesNotExist: - return ApiResponse.error( - status=status.HTTP_404_NOT_FOUND, message="Subscription not found." - ) + return JsonResponse({"error": "Subscription not found."}, status=404) order_id = ( "order_" + str(timezone.localtime().timestamp()) + str(request.user.email) diff --git a/templates/stripe_html/index.html b/templates/stripe_html/index.html index 1914b80..75ff006 100644 --- a/templates/stripe_html/index.html +++ b/templates/stripe_html/index.html @@ -548,6 +548,7 @@ }) .catch((error) => { console.error("Error:", error); + document.getElementById('error-message').innerText = "Error: " + error.message; button.disabled = false; }); }); From 1afda70bf112e0357f21fc58886b0cdea33d0be1 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 24 May 2024 21:19:14 +0530 Subject: [PATCH 017/187] existing subscription validation 8 --- templates/stripe_html/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/stripe_html/index.html b/templates/stripe_html/index.html index 75ff006..23afde9 100644 --- a/templates/stripe_html/index.html +++ b/templates/stripe_html/index.html @@ -548,7 +548,7 @@ }) .catch((error) => { console.error("Error:", error); - document.getElementById('error-message').innerText = "Error: " + error.message; + document.getElementById('already-active-subscription').innerText = "Error: " + error.message; button.disabled = false; }); }); From 685ee8a9fb9ac657b37dadb78276fbf7831883b3 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 24 May 2024 21:49:08 +0530 Subject: [PATCH 018/187] existing subscription validation 8 --- templates/stripe_html/index.html | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/templates/stripe_html/index.html b/templates/stripe_html/index.html index 23afde9..42b505e 100644 --- a/templates/stripe_html/index.html +++ b/templates/stripe_html/index.html @@ -52,9 +52,6 @@
-
- -
@@ -92,7 +89,9 @@
- +
+ +
@@ -548,7 +547,12 @@ }) .catch((error) => { console.error("Error:", error); - document.getElementById('already-active-subscription').innerText = "Error: " + error.message; + const errorMessageElement = document.getElementById('error-message'); + if (errorMessageElement) { + errorMessageElement.innerText = "Error: " + error.message; // Display the error in the specific div + errorMessageElement.style.color = 'red'; // Set text color to red + errorMessageElement.style.fontWeight = 'bold'; // Set text to bold + } button.disabled = false; }); }); From 3cab0dbbc4be5ed9487bfa4d03856821bbdbf506 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 24 May 2024 21:51:07 +0530 Subject: [PATCH 019/187] existing subscription validation 9 --- templates/stripe_html/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/stripe_html/index.html b/templates/stripe_html/index.html index 42b505e..01f07b6 100644 --- a/templates/stripe_html/index.html +++ b/templates/stripe_html/index.html @@ -547,7 +547,7 @@ }) .catch((error) => { console.error("Error:", error); - const errorMessageElement = document.getElementById('error-message'); + const errorMessageElement = document.getElementById('already-active-subscription'); if (errorMessageElement) { errorMessageElement.innerText = "Error: " + error.message; // Display the error in the specific div errorMessageElement.style.color = 'red'; // Set text color to red From 496e8a3fbad16ad585d921a40f0058e9fb8bc28c Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 27 May 2024 12:48:06 +0530 Subject: [PATCH 020/187] updating to event list serializer --- manage_events/api/serializers.py | 20 ++++++++++---------- manage_events/api/views.py | 8 ++++---- manage_subscriptions/views.py | 10 +++++----- templates/stripe_html/index.html | 12 ++++++------ 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/manage_events/api/serializers.py b/manage_events/api/serializers.py index 2a36c82..f7ba42d 100644 --- a/manage_events/api/serializers.py +++ b/manage_events/api/serializers.py @@ -59,23 +59,23 @@ class EventListSerializer(serializers.ModelSerializer): "description", "start_date", "end_date", - "from_time", - "to_time", - "category", - "venue", - "venue_capacity", + # "from_time", + # "to_time", + # "category", + # "venue", + # "venue_capacity", "image", # "video_url", - "entry_type", + # "entry_type", "entry_fee", - "key_guest", - "age_group", + # "key_guest", + # "age_group", # "images", # "is_favorited", # "reviews", - "tags", + # "tags", # "principal_interaction", - "draft", + # "draft", ] diff --git a/manage_events/api/views.py b/manage_events/api/views.py index 6c632d5..7a53e93 100644 --- a/manage_events/api/views.py +++ b/manage_events/api/views.py @@ -191,12 +191,12 @@ class EventsAPIView(APIView): events = services.EventFilterService.filter_events( filter_type=filter, principal=request.user ) - serializer = EventDetailSerializer( - events, context={"request": request}, many=True - ) - # serializer = EventListSerializer( + # 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, diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index b943887..6420e25 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -427,11 +427,11 @@ def create_checkout_session(request): subscription_id = data.get("subscriptionId", None) principal_id = request.user.id - if has_active_principal_subscription(principal_id): - print("Active principal subscription already exists.") - return JsonResponse( - {"error": "Active principal subscription already exists"}, status=400 - ) + # if has_active_principal_subscription(principal_id): + # print("Active principal subscription already exists.") + # return JsonResponse( + # {"error": "Active principal subscription already exists"}, status=400 + # ) try: subscription = Subscription.objects.get(id=subscription_id) diff --git a/templates/stripe_html/index.html b/templates/stripe_html/index.html index 01f07b6..5016178 100644 --- a/templates/stripe_html/index.html +++ b/templates/stripe_html/index.html @@ -547,12 +547,12 @@ }) .catch((error) => { console.error("Error:", error); - const errorMessageElement = document.getElementById('already-active-subscription'); - if (errorMessageElement) { - errorMessageElement.innerText = "Error: " + error.message; // Display the error in the specific div - errorMessageElement.style.color = 'red'; // Set text color to red - errorMessageElement.style.fontWeight = 'bold'; // Set text to bold - } + // const errorMessageElement = document.getElementById('already-active-subscription'); + // if (errorMessageElement) { + // errorMessageElement.innerText = "Error: " + error.message; // Display the error in the specific div + // errorMessageElement.style.color = 'red'; // Set text color to red + // errorMessageElement.style.fontWeight = 'bold'; // Set text to bold + // } button.disabled = false; }); }); From ff74aad3e59cf5ac81447db3e48d8d03bd2ae4cf Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 27 May 2024 15:44:00 +0530 Subject: [PATCH 021/187] optimized event api query and added events till 7 days --- goodtimes/services.py | 149 ++++++++++--------------------- manage_events/api/serializers.py | 8 +- manage_events/api/views.py | 5 +- 3 files changed, 55 insertions(+), 107 deletions(-) diff --git a/goodtimes/services.py b/goodtimes/services.py index 0468a27..cc22c0e 100644 --- a/goodtimes/services.py +++ b/goodtimes/services.py @@ -147,7 +147,9 @@ class SMSService: raise SMSError(message=str(e)) def create_otp(self, principal: IAmPrincipal, opt_purpose: str): - old_otp_change = IAmPrincipalOtp.objects.filter(principal=principal).update(is_used=True) + old_otp_change = IAmPrincipalOtp.objects.filter(principal=principal).update( + is_used=True + ) print("Everything Is Used..!!") otp = IAmPrincipalOtp.objects.create( principal=principal, otp_purpose=opt_purpose @@ -436,19 +438,22 @@ class InteractionCalculator: class EventFilterService: + today = timezone.now().date() + one_week_ago = today - timedelta(days=7) + + # Base query for events that are active, not deleted, not draft, created by active users, and visible up to 1 week after their end date + base_event_query = Event.objects.filter( + active=True, + deleted=False, + draft=False, + end_date__gte=one_week_ago, + created_by__is_active=True, + ).distinct() + @staticmethod def filter_events_by_search(search_query=None): - today = timezone.now().date() - # Filter events that are active, not deleted, not draft, and created by active users - filtered_events = Event.objects.filter( - deleted=False, - active=True, - draft=False, - created_by__is_active=True, - end_date__gte=today, # Only include events that end today or in the future - ) + filtered_events = EventFilterService.base_event_query - # Optional search filtering on title, key_guest, venue address, and tags if search_query: print("search_query: ", search_query) filtered_events = filtered_events.filter( @@ -458,136 +463,76 @@ class EventFilterService: | Q(tags__name__icontains=search_query) ) print("filtered_events: ", filtered_events) + else: + # Filter events where key_guest is not null if no search query is provided + filtered_events = filtered_events.filter( + Q(key_guest__isnull=False) & ~Q(key_guest__exact="") + ) - # Ensure results are distinct - filtered_events = filtered_events.distinct() - - return filtered_events + return filtered_events.distinct() @staticmethod def filter_events_by_tags(search_query=None): - today = timezone.now().date() + filtered_events = EventFilterService.base_event_query + if search_query: print("search_query: ", search_query) - filtered_events = Event.objects.filter( + filtered_events = filtered_events.filter( tags__isnull=False, - deleted=False, - active=True, - draft=False, - created_by__is_active=True, tags__name__icontains=search_query, ) - - # filtered_events = ( - # filtered_events.annotate( - # matched_tags=Count( - # "tags", - # filter=Q(tags__name__icontains=search_query), - # distinct=True, - # ) - # ) - # .filter(matched_tags__gt=0) - # .distinct() - # ) print("filtered_events: ", filtered_events) - # Filter for current, future, or ongoing events - current_and_future_events_query = Q( - start_date__lte=today, end_date__gte=today - ) | Q(start_date__gt=today) - filtered_events = filtered_events.filter(current_and_future_events_query) - - return filtered_events + return filtered_events.distinct() @staticmethod def filter_events(filter_type, principal=None): - today = timezone.now().date() - events = Event.objects.none() - - current_and_future_events_query = Q( - active=True, deleted=False, draft=False, created_by__is_active=True - ) & (Q(start_date__lte=today, end_date__gte=today) | Q(start_date__gt=today)) + events = EventFilterService.base_event_query if filter_type == "expensive": - events = Event.objects.filter(current_and_future_events_query).order_by( - "-entry_fee" - ) + events = events.order_by("-entry_fee") elif filter_type == "cheap": - events = Event.objects.filter(current_and_future_events_query).order_by( - "entry_fee" - ) + events = events.order_by("entry_fee") elif filter_type == "preference" and principal is not None: preferences = PrincipalPreference.objects.get(principal=principal) - preferred_categories_ids = preferences.preferred_categories.values_list( - "id", flat=True - ) - events = Event.objects.filter( - category__in=preferred_categories_ids, - end_date__gte=today, - draft=False, - active=True, - deleted=False, - created_by__is_active=True, - ).distinct() + preferred_categories_ids = preferences.preferred_categories.values_list("id", flat=True) + events = events.filter(category__in=preferred_categories_ids) - return events + return events.distinct() @staticmethod def filter_events_by_category(category_id): - today = timezone.now().date() + events = EventFilterService.base_event_query - current_and_future_events_query = Q( - active=True, deleted=False, draft=False, created_by__is_active=True - ) & (Q(start_date__lte=today, end_date__gte=today) | Q(start_date__gt=today)) - - # Ensure the category_id is valid and within the specified range (1-8) + # Ensure the category_id is valid and within the specified range (1-10) if 1 <= category_id <= 10: - events = Event.objects.filter( - current_and_future_events_query, category_id=category_id - ).distinct() + events = events.filter(category_id=category_id) else: - events = ( - Event.objects.none() - ) # Return an empty queryset if the category_id is not valid + events = Event.objects.none() # Return an empty queryset if the category_id is not valid - return events + return events.distinct() @staticmethod def filter_events_for_tomorrow(): - today = timezone.now().date() - tomorrow = today + timezone.timedelta(days=1) + tomorrow = EventFilterService.today + timezone.timedelta(days=1) - # Events that are starting tomorrow, ending tomorrow, or have an end date greater than tomorrow - events_query = ( - Q(start_date=tomorrow) - | Q(end_date=tomorrow) - | (Q(start_date__lte=tomorrow) & Q(end_date__gte=tomorrow)) + events = EventFilterService.base_event_query.filter( + start_date__lte=tomorrow, + end_date__gte=tomorrow, ) - events = Event.objects.filter( - events_query, - active=True, - deleted=False, - draft=False, - created_by__is_active=True, - ).distinct() - return events + return events.distinct() @staticmethod def filter_events_for_today(): - today = timezone.now().date() - print("Today: ", today) + print("Today: ", EventFilterService.today) - events = Event.objects.filter( - active=True, - deleted=False, - draft=False, - start_date__lte=today, - end_date__gte=today, - created_by__is_active=True, + events = EventFilterService.base_event_query.filter( + start_date__lte=EventFilterService.today, + end_date__gte=EventFilterService.today, ) - return events + return events.distinct() # ye package ka naam hai diff --git a/manage_events/api/serializers.py b/manage_events/api/serializers.py index f7ba42d..a371140 100644 --- a/manage_events/api/serializers.py +++ b/manage_events/api/serializers.py @@ -61,19 +61,19 @@ class EventListSerializer(serializers.ModelSerializer): "end_date", # "from_time", # "to_time", - # "category", - # "venue", + "category", + "venue", # "venue_capacity", "image", # "video_url", # "entry_type", "entry_fee", - # "key_guest", + "key_guest", # "age_group", # "images", # "is_favorited", # "reviews", - # "tags", + "tags", # "principal_interaction", # "draft", ] diff --git a/manage_events/api/views.py b/manage_events/api/views.py index 7a53e93..0c72a3a 100644 --- a/manage_events/api/views.py +++ b/manage_events/api/views.py @@ -245,7 +245,10 @@ class MyEventsAPIView(APIView): errors="Invalid filter parameter", ) - serializer = EventDetailSerializer( + # serializer = EventDetailSerializer( + # events, context={"request": request}, many=True + # ) + serializer = EventListSerializer( events, context={"request": request}, many=True ) return ApiResponse.success( From 679abb78fe48c346cbe2da9b44c099cb1d95368d Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 27 May 2024 15:51:11 +0530 Subject: [PATCH 022/187] updating to event list serializer 2 --- manage_events/api/serializers.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/manage_events/api/serializers.py b/manage_events/api/serializers.py index a371140..7a455ef 100644 --- a/manage_events/api/serializers.py +++ b/manage_events/api/serializers.py @@ -46,10 +46,10 @@ class EventCategorySerializer(serializers.ModelSerializer): class EventListSerializer(serializers.ModelSerializer): - category = EventCategorySerializer(read_only=True) - venue = VenueSerializer(read_only=True) - draft = serializers.BooleanField(read_only=True) - tags = TagSerializer(many=True, read_only=True) + # category = EventCategorySerializer(read_only=True) + # venue = VenueSerializer(read_only=True) + # draft = serializers.BooleanField(read_only=True) + # tags = TagSerializer(many=True, read_only=True) class Meta: model = Event @@ -61,8 +61,8 @@ class EventListSerializer(serializers.ModelSerializer): "end_date", # "from_time", # "to_time", - "category", - "venue", + # "category", + # "venue", # "venue_capacity", "image", # "video_url", @@ -73,7 +73,7 @@ class EventListSerializer(serializers.ModelSerializer): # "images", # "is_favorited", # "reviews", - "tags", + # "tags", # "principal_interaction", # "draft", ] From df59f0714eaad1b50b8adf3651a5c1fa9dee58bc Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 27 May 2024 16:00:02 +0530 Subject: [PATCH 023/187] added deleted in the principal form --- accounts/forms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/accounts/forms.py b/accounts/forms.py index d4d90ee..3dbb6ce 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -72,6 +72,7 @@ class IAmPrincipalForm(forms.ModelForm): "password", "confirm_password", "is_active", + "deleted", ] def __init__(self, *args, **kwargs): From 97bae9e940cb8d59cacdedf15a75a4a69dfd719b Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 27 May 2024 16:03:41 +0530 Subject: [PATCH 024/187] solved account deleted bug --- accounts/api/views.py | 3 +-- accounts/forms.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/accounts/api/views.py b/accounts/api/views.py index 0c7b59e..f51223f 100644 --- a/accounts/api/views.py +++ b/accounts/api/views.py @@ -937,7 +937,7 @@ class SoftDeletePrincipalAPIView(APIView): def delete(self, request, format=None): principal = request.user - if principal.deleted: # Check if already deleted + if not principal.is_active: # Check if already deleted return ApiResponse.error( status=status.HTTP_400_BAD_REQUEST, message="Account already deleted.", @@ -945,7 +945,6 @@ class SoftDeletePrincipalAPIView(APIView): ) principal.is_active = False - principal.deleted = True principal.save() return ApiResponse.success( status=status.HTTP_200_OK, diff --git a/accounts/forms.py b/accounts/forms.py index 3dbb6ce..d4d90ee 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -72,7 +72,6 @@ class IAmPrincipalForm(forms.ModelForm): "password", "confirm_password", "is_active", - "deleted", ] def __init__(self, *args, **kwargs): From a3d31f8dd7402e8e9d398a7ca9af662e13a3743e Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 27 May 2024 17:07:36 +0530 Subject: [PATCH 025/187] transaction filters only success and fail --- accounts/api/views.py | 10 ++++++---- manage_wallets/api/views.py | 8 +++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/accounts/api/views.py b/accounts/api/views.py index f51223f..54160be 100644 --- a/accounts/api/views.py +++ b/accounts/api/views.py @@ -959,13 +959,15 @@ class VersionCheck(APIView): model = AppVersion def get(self, request, *args, **kwargs): - - device_type = request.GET.get('type') + + device_type = request.GET.get("type") if not device_type: - return ApiResponse.error(message=constants.FAILURE, errors="device type is required") + return ApiResponse.error( + message=constants.FAILURE, errors="device type is required" + ) # Query the database to retrieve the upgrade flags based on the app version version = self.model.objects.filter(app_type=device_type).last() version_data = AppVersionSerializer(version) - return ApiResponse.success(message=constants.SUCCESS, data=version_data.data) \ No newline at end of file + return ApiResponse.success(message=constants.SUCCESS, data=version_data.data) diff --git a/manage_wallets/api/views.py b/manage_wallets/api/views.py index 85deee9..f477a22 100644 --- a/manage_wallets/api/views.py +++ b/manage_wallets/api/views.py @@ -140,7 +140,13 @@ class TransactionView(APIView): permission_classes = [IsAuthenticated] def get(self, request): - queryset = models.Transaction.objects.filter(principal_id=request.user.id) + queryset = models.Transaction.objects.filter( + principal_id=request.user.id, + transaction_status__in=[ + models.TransactionStatus.SUCCESS, + models.TransactionStatus.FAIL, + ], + ) serializer = serializers.TransactionSerializer(queryset, many=True) response = { From a9725dd0fc2d98df2f65ec4c1f03d48d71f06ab2 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 27 May 2024 20:23:48 +0530 Subject: [PATCH 026/187] solved event notifications bug --- manage_notifications/management/commands/interested_going.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manage_notifications/management/commands/interested_going.py b/manage_notifications/management/commands/interested_going.py index ce33b70..077d4d8 100644 --- a/manage_notifications/management/commands/interested_going.py +++ b/manage_notifications/management/commands/interested_going.py @@ -66,4 +66,6 @@ class Command(BaseCommand): principal__is_active=True, principal__deleted=False, status__in=[EventInteractionType.GOING, EventInteractionType.INTERESTED], + principal__notifications_principal__notification_category=NotificationCategoryChoices.EVENT, + principal__notifications_principal__is_enabled=True, ).select_related("principal", "event") From e7b5b37e586f9e5578a49018f855583371994d19 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 27 May 2024 20:31:48 +0530 Subject: [PATCH 027/187] cron event bug solved --- manage_notifications/management/commands/interested_going.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/manage_notifications/management/commands/interested_going.py b/manage_notifications/management/commands/interested_going.py index 077d4d8..1d8ba6d 100644 --- a/manage_notifications/management/commands/interested_going.py +++ b/manage_notifications/management/commands/interested_going.py @@ -62,9 +62,7 @@ class Command(BaseCommand): event__deleted=False, event__active=True, event__created_by__is_active=True, - event__created_by__deleted=False, principal__is_active=True, - principal__deleted=False, status__in=[EventInteractionType.GOING, EventInteractionType.INTERESTED], principal__notifications_principal__notification_category=NotificationCategoryChoices.EVENT, principal__notifications_principal__is_enabled=True, From 051d8788bde7aa9cfe46dc7b9cfaac236fe607e2 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Tue, 28 May 2024 13:17:52 +0530 Subject: [PATCH 028/187] removed eventlistserializer in my events api --- manage_events/api/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/manage_events/api/views.py b/manage_events/api/views.py index 0c72a3a..889f0ba 100644 --- a/manage_events/api/views.py +++ b/manage_events/api/views.py @@ -245,12 +245,12 @@ class MyEventsAPIView(APIView): errors="Invalid filter parameter", ) - # serializer = EventDetailSerializer( - # events, context={"request": request}, many=True - # ) - serializer = EventListSerializer( + 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, From 6262afea17578703201b934849d1271856610e52 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 31 May 2024 17:16:30 +0530 Subject: [PATCH 029/187] event view count --- .../0010_alter_appversion_app_type.py | 20 ++ manage_events/admin.py | 11 +- manage_events/api/urls.py | 6 + manage_events/api/views.py | 41 ++++ manage_events/migrations/0009_eventview.py | 75 ++++++ manage_events/models.py | 14 ++ manage_events/report.py | 213 ++++++++++++++++++ manage_events/urls.py | 5 + manage_events/views.py | 25 ++ requirements.txt | 3 + 10 files changed, 412 insertions(+), 1 deletion(-) create mode 100644 accounts/migrations/0010_alter_appversion_app_type.py create mode 100644 manage_events/migrations/0009_eventview.py create mode 100644 manage_events/report.py diff --git a/accounts/migrations/0010_alter_appversion_app_type.py b/accounts/migrations/0010_alter_appversion_app_type.py new file mode 100644 index 0000000..7997524 --- /dev/null +++ b/accounts/migrations/0010_alter_appversion_app_type.py @@ -0,0 +1,20 @@ +# Generated by Django 5.0.2 on 2024-05-31 11:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0009_appversion_app_type"), + ] + + operations = [ + migrations.AlterField( + model_name="appversion", + name="app_type", + field=models.CharField( + choices=[("android", "android"), ("ios", "ios")], max_length=10 + ), + ), + ] diff --git a/manage_events/admin.py b/manage_events/admin.py index ac72d3d..ec4bfc8 100644 --- a/manage_events/admin.py +++ b/manage_events/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import EventCategory, Venue, EventMaster, Event, EventPrincipalInteraction +from .models import EventCategory, EventView, Venue, EventMaster, Event, EventPrincipalInteraction # Register your models here. @@ -91,6 +91,15 @@ class EventPrincipalInteractionAdmin(admin.ModelAdmin): ) # Adjust these field lookups according to your models. +class EventViewAdmin(admin.ModelAdmin): + list_display = ('id', 'event', 'principal', 'view_date', 'location') + search_fields = ('event__title', 'principal__email', 'location') + list_filter = ('view_date', 'location', 'event__title', 'principal__email') + ordering = ('-view_date',) + readonly_fields = ('id',) + + +admin.site.register(EventView, EventViewAdmin) admin.site.register(EventPrincipalInteraction, EventPrincipalInteractionAdmin) admin.site.register(Event, EventAdmin) admin.site.register(EventCategory, EventCategoryAdmin) diff --git a/manage_events/api/urls.py b/manage_events/api/urls.py index 4e632f9..f9c3e7d 100644 --- a/manage_events/api/urls.py +++ b/manage_events/api/urls.py @@ -111,4 +111,10 @@ urlpatterns = [ name="principal-events", ), path("tags/", views.TagListView.as_view(), name="tag-list"), + # For counting event views + path( + "event//view/", + views.CaptureEventViewAPIView.as_view(), + name="capture_event_view", + ), ] diff --git a/manage_events/api/views.py b/manage_events/api/views.py index 889f0ba..08f3c6d 100644 --- a/manage_events/api/views.py +++ b/manage_events/api/views.py @@ -34,6 +34,7 @@ from manage_events.models import ( EventCategory, EventPrincipalInteraction, EventReview, + EventView, Favorites, PrincipalPreference, Venue, @@ -123,6 +124,20 @@ class CreateVenueApi(APIView): ) +# # Prepare the email +# subject = f"Your Event Report for {start_date.month} {start_date.year}." +# body = f"Please find attached the event report for {start_date.month} {start_date.month}." +# email_service = EmailService( +# subject="Good Times - Report", +# to=[user.email], +# from_email=settings.EMAIL_HOST_USER, +# ) +# email_service.attach(filename, buffer.getvalue(), "application/pdf") + +# # Send the email +# email_service.send() + + class VenueDeleteAPIView(APIView): authentication_classes = [JWTAuthentication] permission_classes = [IsAuthenticated] @@ -849,3 +864,29 @@ class TagListView(generics.ListAPIView): data=serializer.data, status=status.HTTP_200_OK, ) + + +class CaptureEventViewAPIView(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + def post(self, request, pk): + try: + event = Event.objects.get(pk=pk) + user = request.user + location_parts = [user.city, user.state, user.country] + location = " ".join(part for part in location_parts if part) + + EventView.objects.create(event=event, principal=user, location=location) + + return ApiResponse.success( + message=constants.SUCCESS, + data="Event view recorded successfully.", + status=status.HTTP_200_OK, + ) + except Event.DoesNotExist: + return ApiResponse.error( + message=constants.FAILURE, + errors="Event not found.", + status=status.HTTP_400_BAD_REQUEST, + ) diff --git a/manage_events/migrations/0009_eventview.py b/manage_events/migrations/0009_eventview.py new file mode 100644 index 0000000..ee512af --- /dev/null +++ b/manage_events/migrations/0009_eventview.py @@ -0,0 +1,75 @@ +# Generated by Django 5.0.2 on 2024-05-31 11:31 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("manage_events", "0008_alter_eventprincipalinteraction_event_and_more"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="EventView", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("active", models.BooleanField(default=True)), + ("deleted", models.BooleanField(default=False)), + ("created_on", models.DateTimeField(auto_now_add=True)), + ("modified_on", models.DateTimeField(auto_now=True)), + ("view_date", models.DateTimeField(auto_now_add=True)), + ("location", models.CharField(blank=True, max_length=255, null=True)), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_created", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "event", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="views", + to="manage_events.event", + ), + ), + ( + "modified_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_modified", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "principal", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="event_views", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/manage_events/models.py b/manage_events/models.py index 4f876cb..1b4c55e 100644 --- a/manage_events/models.py +++ b/manage_events/models.py @@ -173,3 +173,17 @@ class EventReview(BaseModel): def __str__(self): return f"Review by {self.principal} on {self.event}" + + +class EventView(BaseModel): + event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="views") + principal = models.ForeignKey( + IAmPrincipal, on_delete=models.CASCADE, related_name="event_views" + ) + view_date = models.DateTimeField(auto_now_add=True) + location = models.CharField( + max_length=255, blank=True, null=True + ) # Or use a more complex field for location data + + def __str__(self): + return f"{self.principal.email} viewed {self.event.title} from {self.location}" diff --git a/manage_events/report.py b/manage_events/report.py new file mode 100644 index 0000000..f435fe5 --- /dev/null +++ b/manage_events/report.py @@ -0,0 +1,213 @@ +from django.core.mail import EmailMessage +from django.db.models import Count, Q +from django.utils import timezone +from datetime import timedelta +from reportlab.pdfgen import canvas +from reportlab.lib.pagesizes import letter +from reportlab.graphics.shapes import Drawing +from reportlab.graphics.charts.piecharts import Pie +from io import BytesIO +from django.conf import settings +from reportlab.graphics import renderPDF +from django.contrib.auth import get_user_model +from goodtimes.services import EmailService +from manage_events.models import Event, EventInteractionType + + +User = get_user_model() + + +def generate_filename(email, date): + # Extract the username from the email address + username = email.split("@")[0] + # Get the full month name from the date + month_name = date.strftime("%B") + # Create the filename + filename = f"{username}_{month_name}_report.pdf" + return filename + + +def get_previous_month_date_range(): + today = timezone.now() + first_day_of_current_month = today.replace(day=1) + last_day_of_previous_month = first_day_of_current_month - timedelta(days=1) + first_day_of_previous_month = last_day_of_previous_month.replace(day=1) + return first_day_of_previous_month, last_day_of_previous_month + + +def generate_event_report(user_id): + # Calculate the start and end dates for the previous month + start_date, end_date = get_previous_month_date_range() + + # Get the user (manager) + user = User.objects.get(id=user_id) + + # Filter events created by the user in the previous month + events = Event.objects.filter( + created_by=user, created_on__gte=start_date, created_on__lte=end_date + ).annotate( + favorites_count=Count( + "favorites", filter=Q(favorites__active=True, favorites__deleted=False) + ), + interested_count=Count( + "interaction_event", + filter=Q(interaction_event__status=EventInteractionType.INTERESTED), + ), + going_count=Count( + "interaction_event", + filter=Q(interaction_event__status=EventInteractionType.GOING), + ), + reviews_count=Count( + "reviews", filter=Q(reviews__active=True, reviews__deleted=False) + ), + ) + + # Generate the report + report_data = [] + for event in events: + report_data.append( + { + "event_name": event.title, + "favorites_count": event.favorites_count, + "interested_count": event.interested_count, + "going_count": event.going_count, + "reviews_count": event.reviews_count, + } + ) + + return report_data + + +def generate_event_report_pdf(user, report_data): + start_date, _ = get_previous_month_date_range() + filename = generate_filename(user.email, start_date) + + buffer = BytesIO() + pdf = canvas.Canvas(buffer, pagesize=letter) + width, height = letter + + # Add a title and user information + pdf.setFont("Helvetica-Bold", 16) + pdf.drawString(100, 750, "Event Report - April 2024") + + user_name = user.email.split("@")[0] # Use part of email before @ as username + pdf.setFont("Helvetica", 12) + pdf.drawString(100, 730, f"For Event Manager: {user_name}") + + # Add a table header + pdf.setFont("Helvetica-Bold", 10) + pdf.drawString(50, 700, "Event Name") + pdf.drawString(250, 700, "Favorites") + pdf.drawString(350, 700, "Interested") + pdf.drawString(450, 700, "Going") + pdf.drawString(550, 700, "Reviews") + + # Loop through data and add table rows + y_pos = 680 + for event in report_data: + pdf.drawString(50, y_pos, event["event_name"]) + pdf.drawString(250, y_pos, str(event["favorites_count"])) + pdf.drawString(350, y_pos, str(event["interested_count"])) + pdf.drawString(450, y_pos, str(event["going_count"])) + pdf.drawString(550, y_pos, str(event["reviews_count"])) + y_pos -= 15 # Adjust position for next row + + # Draw the pie chart + if report_data: + pie_data = [ + sum(event[key] for event in report_data) + for key in ("favorites_count", "interested_count", "going_count") + ] + pie_labels = ["Favorites", "Interested", "Going"] + + drawing = Drawing(width, height) + pie = Pie() + pie.x = 150 + pie.y = 200 + pie.width = 300 + pie.height = 150 + pie.data = pie_data + pie.labels = pie_labels + pie.slices.strokeWidth = 0.5 + + drawing.add(pie) + renderPDF.draw(drawing, pdf, 150, 200) + + # Close the PDF object and write the buffer content to the PDF file + pdf.save() + buffer.seek(0) + pdf_data = buffer.read() + buffer.close() + return pdf_data, filename + + +def generate_event_report_pdf_two(user, report_data): + start_date, _ = get_previous_month_date_range() + filename = generate_filename(user.email, start_date) + + buffer = BytesIO() + pdf = canvas.Canvas(buffer, pagesize=letter) + width, height = letter + + # Add a title and user information + pdf.setFont("Helvetica-Bold", 16) + pdf.drawString(100, 750, "Event Report - April 2024") + + user_name = user.email.split("@")[0] # Use part of email before @ as username + pdf.setFont("Helvetica", 12) + pdf.drawString(100, 730, f"For Event Manager: {user_name}") + + # Event loop with row handling + y_pos = 650 # Starting position for charts (adjust as needed) + chart_width = 250 # Width of each pie chart + chart_height = 150 # Height of each pie chart + chart_spacing = 50 # Spacing between charts in a row + + for i, event in enumerate(report_data): + # Add event name as a header + pdf.setFont("Helvetica-Bold", 12) + pdf.drawString(50, y_pos + 20, event["event_name"]) + + # Check if this is the first event in a row + if i % 2 == 0: + x_pos = 75 # Starting position for charts in the first column + else: + x_pos = ( + width - chart_width - 75 + ) # Starting position for charts in the second column + + # Draw pie charts for the event + for key, value in [ + ("favorites_count", "Favorites"), + ("interested_count", "Interested"), + ("going_count", "Going"), + ]: + pie_data = [value] + pie_labels = [value] + + drawing = Drawing(chart_width, chart_height) + pie = Pie() + pie.x = 0 + pie.y = 0 + pie.width = chart_width + pie.height = chart_height + pie.data = pie_data + pie.labels = pie_labels + pie.slices.strokeWidth = 0.5 + + drawing.add(pie) + renderPDF.draw(drawing, pdf, x_pos, y_pos) + + x_pos += chart_width + chart_spacing # Adjust x position for the next chart + + y_pos -= ( + 100 # Adjust y position for the next event (adjust based on chart heights) + ) + + # Close the PDF object and write the buffer content to the PDF file + pdf.save() + buffer.seek(0) + pdf_data = buffer.read() + buffer.close() + + return pdf_data, filename diff --git a/manage_events/urls.py b/manage_events/urls.py index 543d40b..6ce1536 100644 --- a/manage_events/urls.py +++ b/manage_events/urls.py @@ -89,4 +89,9 @@ urlpatterns = [ views.VenueDeleteView.as_view(), name="venue_delete", ), + path( + "generate-event-report//", + views.GenerateEventReportView.as_view(), + name="generate_event_report", + ), ] diff --git a/manage_events/views.py b/manage_events/views.py index 5952aca..05c53e5 100644 --- a/manage_events/views.py +++ b/manage_events/views.py @@ -13,6 +13,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse_lazy from django.contrib import messages from goodtimes import constants +from django.contrib.auth import get_user_model # Create your views here. @@ -477,3 +478,27 @@ class VenueDeleteView(LoginRequiredMixin, generic.View): messages.success(request, self.error_message) return redirect(self.success_url) + + +User = get_user_model() +from .report import generate_event_report, generate_event_report_pdf +from django.http import HttpResponse + + +class GenerateEventReportView(generic.View): + + def get(self, request, user_id): + # Generate the event report + report_data = generate_event_report(user_id) + + # Get the user + user = get_object_or_404(User, id=user_id) + + # Generate the PDF + pdf_data, filename = generate_event_report_pdf(user, report_data) + + # Create the HttpResponse object with the PDF data + response = HttpResponse(pdf_data, content_type="application/pdf") + response["Content-Disposition"] = f'attachment; filename="{filename}"' + + return response diff --git a/requirements.txt b/requirements.txt index 08ccda6..094748b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,7 @@ certifi==2024.2.2 cffi==1.16.0 channels==4.0.0 channels-redis==4.2.0 +chardet==5.2.0 charset-normalizer==3.3.2 colorama==0.4.6 colorlog==6.8.2 @@ -17,6 +18,7 @@ daphne==4.1.0 defusedxml==0.7.1 Django==5.0.2 django-allauth==0.61.1 +django-channels==0.7.0 django-cors-headers==4.3.1 django-debug-toolbar==4.3.0 django-environ==0.11.2 @@ -56,6 +58,7 @@ python3-openid==3.2.0 pytz==2024.1 PyYAML==6.0.1 redis==5.0.2 +reportlab==4.2.0 requests==2.31.0 requests-oauthlib==1.3.1 service-identity==24.1.0 From 10171170f47524ee5883009327773853a13ff589 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 31 May 2024 19:58:50 +0530 Subject: [PATCH 030/187] reports pdf --- manage_events/report.py | 177 ++++++++++++++++++++++++++++++++-------- manage_events/views.py | 4 +- 2 files changed, 147 insertions(+), 34 deletions(-) diff --git a/manage_events/report.py b/manage_events/report.py index f435fe5..94d237c 100644 --- a/manage_events/report.py +++ b/manage_events/report.py @@ -8,10 +8,11 @@ from reportlab.graphics.shapes import Drawing from reportlab.graphics.charts.piecharts import Pie from io import BytesIO from django.conf import settings +from collections import defaultdict from reportlab.graphics import renderPDF from django.contrib.auth import get_user_model from goodtimes.services import EmailService -from manage_events.models import Event, EventInteractionType +from manage_events.models import Event, EventInteractionType, EventView User = get_user_model() @@ -36,44 +37,35 @@ def get_previous_month_date_range(): def generate_event_report(user_id): - # Calculate the start and end dates for the previous month start_date, end_date = get_previous_month_date_range() - - # Get the user (manager) user = User.objects.get(id=user_id) - # Filter events created by the user in the previous month - events = Event.objects.filter( - created_by=user, created_on__gte=start_date, created_on__lte=end_date - ).annotate( - favorites_count=Count( - "favorites", filter=Q(favorites__active=True, favorites__deleted=False) - ), - interested_count=Count( - "interaction_event", - filter=Q(interaction_event__status=EventInteractionType.INTERESTED), - ), - going_count=Count( - "interaction_event", - filter=Q(interaction_event__status=EventInteractionType.GOING), - ), - reviews_count=Count( - "reviews", filter=Q(reviews__active=True, reviews__deleted=False) - ), + events = Event.objects.filter(created_by=user, created_on__gte=start_date, created_on__lte=end_date).annotate( + favorites_count=Count("favorites", filter=Q(favorites__active=True, favorites__deleted=False)), + interested_count=Count("interaction_event", filter=Q(interaction_event__status=EventInteractionType.INTERESTED)), + going_count=Count("interaction_event", filter=Q(interaction_event__status=EventInteractionType.GOING)), + reviews_count=Count("reviews", filter=Q(reviews__active=True, reviews__deleted=False)), + views_count=Count("views", filter=Q(views__active=True, views__deleted=False)), ) - # Generate the report report_data = [] for event in events: - report_data.append( - { - "event_name": event.title, - "favorites_count": event.favorites_count, - "interested_count": event.interested_count, - "going_count": event.going_count, - "reviews_count": event.reviews_count, - } - ) + views = EventView.objects.filter(event=event) + locations = defaultdict(int) + for view in views: + locations[view.location] += 1 + + report_data.append({ + "event_name": event.title, + "event_type": event.category.title, + "event_date": str(event.start_date), + "favorites_count": event.favorites_count, + "interested_count": event.interested_count, + "going_count": event.going_count, + "reviews_count": event.reviews_count, + "views_count": event.views_count, + "locations": dict(locations), + }) return report_data @@ -211,3 +203,124 @@ def generate_event_report_pdf_two(user, report_data): buffer.close() return pdf_data, filename + + +def generate_event_report_pdf_three(user, report_data): + start_date, _ = get_previous_month_date_range() + filename = generate_filename(user.email, start_date) + + buffer = BytesIO() + pdf = canvas.Canvas(buffer, pagesize=letter) + width, height = letter + + # Header Section + title = "Good Times Ltd. Monthly Report" + report_for_month = f"Report for the month of - {start_date.strftime('%B %Y')}" + organiser_name = f"Name of the Organiser - {user.get_full_name()}" + contact_name = f"Contact Name - {user.get_full_name()}" + + pdf.setFont("Helvetica-Bold", 20) + title_width = pdf.stringWidth(title, "Helvetica-Bold", 20) + pdf.drawString((width - title_width) / 2, 750, title) + + pdf.setFont("Helvetica", 16) + report_for_month_width = pdf.stringWidth(report_for_month, "Helvetica", 16) + pdf.drawString((width - report_for_month_width) / 2, 720, report_for_month) + + organiser_name_width = pdf.stringWidth(organiser_name, "Helvetica", 16) + pdf.drawString((width - organiser_name_width) / 2, 690, organiser_name) + + contact_name_width = pdf.stringWidth(contact_name, "Helvetica", 16) + pdf.drawString((width - contact_name_width) / 2, 660, contact_name) + pdf.showPage() + # Summary Section + pdf.setFont("Helvetica-Bold", 12) + pdf.drawString( + 100, + 640, + f"Number of Events added in {start_date.strftime('%B %Y')} - {len(report_data)}", + ) + + y_pos = 620 + pdf.setFont("Helvetica-Bold", 10) + pdf.drawString(50, y_pos, "Event Name") + pdf.drawString(200, y_pos, "Event Type") + pdf.drawString(350, y_pos, "Date") + y_pos -= 20 + + pdf.setFont("Helvetica", 10) + for event in report_data: + pdf.drawString(50, y_pos, event["event_name"]) + pdf.drawString(200, y_pos, event["event_type"]) + pdf.drawString(350, y_pos, event["event_date"]) + y_pos -= 20 + + # Start a new page for Traffic Details Section + pdf.showPage() + + pdf.setFont("Helvetica-Bold", 12) + pdf.drawString( + 50, height - 50, "Traffic Details for profile - Event Organisers London Ltd." + ) + + # Detailed Review of Each Event + for event in report_data: + pdf.showPage() + pdf.setFont("Helvetica-Bold", 12) + pdf.drawString(50, height - 50, f"Event Name - {event['event_name']}") + pdf.setFont("Helvetica", 10) + pdf.drawString(50, height - 70, f"Event Type - {event['event_type']}") + pdf.drawString(50, height - 90, f"Event Date - {event['event_date']}") + + y_pos = height - 120 + pdf.drawString(50, y_pos, f"Number of Views - {event['views_count']}") + pdf.drawString(200, y_pos, f"Favorites Count - {event['favorites_count']}") + pdf.drawString(350, y_pos, f"Interested in Going - {event['interested_count']}") + pdf.drawString(500, y_pos, f"Going - {event['going_count']}") + pdf.drawString(650, y_pos, f"Reviews - {event['reviews_count']}") + y_pos -= 40 + + pdf.setFont("Helvetica-Bold", 10) + pdf.drawString(50, y_pos, "View Details:") + y_pos -= 20 + pdf.setFont("Helvetica", 10) + pdf.drawString(100, y_pos, "User Email") + pdf.drawString(300, y_pos, "Location") + pdf.drawString(500, y_pos, "View Date") + y_pos -= 20 + for view in event["locations"]: + pdf.drawString(100, y_pos, view["user__email"]) + pdf.drawString(300, y_pos, view["location"]) + pdf.drawString(500, y_pos, view["view_date"].strftime("%d %B %Y %H:%M")) + y_pos -= 20 + + # Draw pie chart on a new page if there's not enough space + if y_pos < 150: + pdf.showPage() + y_pos = height - 150 + + pie_data = [ + event["views_count"], + event["favorites_count"], + event["interested_count"], + event["going_count"], + ] + pie_labels = ["Views", "Favorites", "Interested", "Going"] + + drawing = Drawing(200, 100) + pie = Pie() + pie.x = 50 + pie.y = y_pos - 100 + pie.data = pie_data + pie.labels = pie_labels + pie.width = 100 + pie.height = 100 + drawing.add(pie) + renderPDF.draw(drawing, pdf, 50, y_pos - 100) + y_pos -= 150 # Adjust for pie chart height + + pdf.save() + buffer.seek(0) + pdf_data = buffer.read() + buffer.close() + return pdf_data, filename diff --git a/manage_events/views.py b/manage_events/views.py index 05c53e5..89c3336 100644 --- a/manage_events/views.py +++ b/manage_events/views.py @@ -481,7 +481,7 @@ class VenueDeleteView(LoginRequiredMixin, generic.View): User = get_user_model() -from .report import generate_event_report, generate_event_report_pdf +from .report import generate_event_report, generate_event_report_pdf_three from django.http import HttpResponse @@ -495,7 +495,7 @@ class GenerateEventReportView(generic.View): user = get_object_or_404(User, id=user_id) # Generate the PDF - pdf_data, filename = generate_event_report_pdf(user, report_data) + pdf_data, filename = generate_event_report_pdf_three(user, report_data) # Create the HttpResponse object with the PDF data response = HttpResponse(pdf_data, content_type="application/pdf") From 5ddccc0860ff7270817bdb03ea4e5907bc49ad64 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 3 Jun 2024 16:49:19 +0530 Subject: [PATCH 031/187] report --- manage_events/admin.py | 31 +- manage_events/api/urls.py | 6 + manage_events/api/views.py | 28 ++ ...nt_social_media_shares_count_eventshare.py | 78 ++++ manage_events/models.py | 14 + manage_events/report.py | 423 ++++++++---------- 6 files changed, 344 insertions(+), 236 deletions(-) create mode 100644 manage_events/migrations/0010_event_social_media_shares_count_eventshare.py diff --git a/manage_events/admin.py b/manage_events/admin.py index ec4bfc8..ff0a938 100644 --- a/manage_events/admin.py +++ b/manage_events/admin.py @@ -1,5 +1,13 @@ from django.contrib import admin -from .models import EventCategory, EventView, Venue, EventMaster, Event, EventPrincipalInteraction +from .models import ( + EventCategory, + EventShare, + EventView, + Venue, + EventMaster, + Event, + EventPrincipalInteraction, +) # Register your models here. @@ -78,7 +86,7 @@ class EventAdmin(admin.ModelAdmin): }, ), ) - filter_horizontal = () # Use this if there are many-to-many fields + filter_horizontal = () # if there are many-to-many fields raw_id_fields = ("venue", "category", "event_master") @@ -88,17 +96,24 @@ class EventPrincipalInteractionAdmin(admin.ModelAdmin): search_fields = ( "principal__name", "event__title", - ) # Adjust these field lookups according to your models. + ) class EventViewAdmin(admin.ModelAdmin): - list_display = ('id', 'event', 'principal', 'view_date', 'location') - search_fields = ('event__title', 'principal__email', 'location') - list_filter = ('view_date', 'location', 'event__title', 'principal__email') - ordering = ('-view_date',) - readonly_fields = ('id',) + list_display = ("id", "event", "principal", "view_date", "location") + search_fields = ("event__title", "principal__email", "location") + list_filter = ("id", "view_date", "location", "event__title", "principal__email") + ordering = ("-view_date",) + readonly_fields = ("id",) +class EventShareAdmin(admin.ModelAdmin): + list_display = ("id", "event", "principal", "created_on") + search_fields = ("event__title", "principal__username") + list_filter = ("id", "event", "principal", "created_on") + + +admin.site.register(EventShare, EventShareAdmin) admin.site.register(EventView, EventViewAdmin) admin.site.register(EventPrincipalInteraction, EventPrincipalInteractionAdmin) admin.site.register(Event, EventAdmin) diff --git a/manage_events/api/urls.py b/manage_events/api/urls.py index f9c3e7d..5b63eb7 100644 --- a/manage_events/api/urls.py +++ b/manage_events/api/urls.py @@ -117,4 +117,10 @@ urlpatterns = [ views.CaptureEventViewAPIView.as_view(), name="capture_event_view", ), + # For counting event shares + path( + "event//share/", + views.EventShareView.as_view(), + name="capture_event_share", + ), ] diff --git a/manage_events/api/views.py b/manage_events/api/views.py index 08f3c6d..9bc35ce 100644 --- a/manage_events/api/views.py +++ b/manage_events/api/views.py @@ -34,6 +34,7 @@ from manage_events.models import ( EventCategory, EventPrincipalInteraction, EventReview, + EventShare, EventView, Favorites, PrincipalPreference, @@ -890,3 +891,30 @@ class CaptureEventViewAPIView(APIView): errors="Event not found.", status=status.HTTP_400_BAD_REQUEST, ) + + +class EventShareView(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + def post(self, request, pk): + try: + event = Event.objects.get(id=pk) + except Event.DoesNotExist: + return ApiResponse.error( + message=constants.FAILURE, + errors="Event not found.", + status=status.HTTP_400_BAD_REQUEST, + ) + + # Incrementing the social media shares count + event.increment_shares() + + user = request.user # Assuming the user is authenticated + EventShare.objects.create(principal=user, event=event) + + return ApiResponse.success( + message=constants.SUCCESS, + data="Event shared successfully.", + status=status.HTTP_200_OK, + ) diff --git a/manage_events/migrations/0010_event_social_media_shares_count_eventshare.py b/manage_events/migrations/0010_event_social_media_shares_count_eventshare.py new file mode 100644 index 0000000..4c6c7a4 --- /dev/null +++ b/manage_events/migrations/0010_event_social_media_shares_count_eventshare.py @@ -0,0 +1,78 @@ +# Generated by Django 5.0.2 on 2024-06-01 15:06 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("manage_events", "0009_eventview"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name="event", + name="social_media_shares_count", + field=models.IntegerField(default=0), + ), + migrations.CreateModel( + name="EventShare", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("active", models.BooleanField(default=True)), + ("deleted", models.BooleanField(default=False)), + ("created_on", models.DateTimeField(auto_now_add=True)), + ("modified_on", models.DateTimeField(auto_now=True)), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_created", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "event", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="social_media_shares", + to="manage_events.event", + ), + ), + ( + "modified_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_modified", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "principal", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="event_shares", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/manage_events/models.py b/manage_events/models.py index 1b4c55e..a961ab5 100644 --- a/manage_events/models.py +++ b/manage_events/models.py @@ -84,6 +84,11 @@ class Event(BaseModel): tags = TaggableManager(blank=True) age_group = models.CharField(max_length=100, blank=True, null=True) draft = models.BooleanField(default=False) + social_media_shares_count = models.IntegerField(default=0) + + def increment_shares(self): + self.social_media_shares_count += 1 + self.save() def __str__(self): return self.title @@ -187,3 +192,12 @@ class EventView(BaseModel): def __str__(self): return f"{self.principal.email} viewed {self.event.title} from {self.location}" + + +class EventShare(BaseModel): + event = models.ForeignKey( + Event, on_delete=models.CASCADE, related_name="social_media_shares" + ) + principal = models.ForeignKey( + IAmPrincipal, on_delete=models.CASCADE, related_name="event_shares" + ) diff --git a/manage_events/report.py b/manage_events/report.py index 94d237c..be32646 100644 --- a/manage_events/report.py +++ b/manage_events/report.py @@ -5,14 +5,24 @@ from datetime import timedelta from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import letter from reportlab.graphics.shapes import Drawing +from reportlab.lib import colors +from reportlab.lib.styles import getSampleStyleSheet from reportlab.graphics.charts.piecharts import Pie +from reportlab.platypus import ( + SimpleDocTemplate, + Table, + TableStyle, + Paragraph, + Spacer, + PageBreak, +) from io import BytesIO from django.conf import settings from collections import defaultdict from reportlab.graphics import renderPDF from django.contrib.auth import get_user_model from goodtimes.services import EmailService -from manage_events.models import Event, EventInteractionType, EventView +from manage_events.models import Event, EventInteractionType, EventShare, EventView User = get_user_model() @@ -40,13 +50,26 @@ def generate_event_report(user_id): start_date, end_date = get_previous_month_date_range() user = User.objects.get(id=user_id) - events = Event.objects.filter(created_by=user, created_on__gte=start_date, created_on__lte=end_date).annotate( - favorites_count=Count("favorites", filter=Q(favorites__active=True, favorites__deleted=False)), - interested_count=Count("interaction_event", filter=Q(interaction_event__status=EventInteractionType.INTERESTED)), - going_count=Count("interaction_event", filter=Q(interaction_event__status=EventInteractionType.GOING)), - reviews_count=Count("reviews", filter=Q(reviews__active=True, reviews__deleted=False)), + events = Event.objects.filter( + created_by=user, start_date__gte=start_date, start_date__lte=end_date + ).annotate( + favorites_count=Count( + "favorites", filter=Q(favorites__active=True, favorites__deleted=False) + ), + interested_count=Count( + "interaction_event", + filter=Q(interaction_event__status=EventInteractionType.INTERESTED), + ), + going_count=Count( + "interaction_event", + filter=Q(interaction_event__status=EventInteractionType.GOING), + ), + reviews_count=Count( + "reviews", filter=Q(reviews__active=True, reviews__deleted=False) + ), views_count=Count("views", filter=Q(views__active=True, views__deleted=False)), ) + print("events: ", events) report_data = [] for event in events: @@ -55,154 +78,35 @@ def generate_event_report(user_id): for view in views: locations[view.location] += 1 - report_data.append({ - "event_name": event.title, - "event_type": event.category.title, - "event_date": str(event.start_date), - "favorites_count": event.favorites_count, - "interested_count": event.interested_count, - "going_count": event.going_count, - "reviews_count": event.reviews_count, - "views_count": event.views_count, - "locations": dict(locations), - }) - - return report_data - - -def generate_event_report_pdf(user, report_data): - start_date, _ = get_previous_month_date_range() - filename = generate_filename(user.email, start_date) - - buffer = BytesIO() - pdf = canvas.Canvas(buffer, pagesize=letter) - width, height = letter - - # Add a title and user information - pdf.setFont("Helvetica-Bold", 16) - pdf.drawString(100, 750, "Event Report - April 2024") - - user_name = user.email.split("@")[0] # Use part of email before @ as username - pdf.setFont("Helvetica", 12) - pdf.drawString(100, 730, f"For Event Manager: {user_name}") - - # Add a table header - pdf.setFont("Helvetica-Bold", 10) - pdf.drawString(50, 700, "Event Name") - pdf.drawString(250, 700, "Favorites") - pdf.drawString(350, 700, "Interested") - pdf.drawString(450, 700, "Going") - pdf.drawString(550, 700, "Reviews") - - # Loop through data and add table rows - y_pos = 680 - for event in report_data: - pdf.drawString(50, y_pos, event["event_name"]) - pdf.drawString(250, y_pos, str(event["favorites_count"])) - pdf.drawString(350, y_pos, str(event["interested_count"])) - pdf.drawString(450, y_pos, str(event["going_count"])) - pdf.drawString(550, y_pos, str(event["reviews_count"])) - y_pos -= 15 # Adjust position for next row - - # Draw the pie chart - if report_data: - pie_data = [ - sum(event[key] for event in report_data) - for key in ("favorites_count", "interested_count", "going_count") - ] - pie_labels = ["Favorites", "Interested", "Going"] - - drawing = Drawing(width, height) - pie = Pie() - pie.x = 150 - pie.y = 200 - pie.width = 300 - pie.height = 150 - pie.data = pie_data - pie.labels = pie_labels - pie.slices.strokeWidth = 0.5 - - drawing.add(pie) - renderPDF.draw(drawing, pdf, 150, 200) - - # Close the PDF object and write the buffer content to the PDF file - pdf.save() - buffer.seek(0) - pdf_data = buffer.read() - buffer.close() - return pdf_data, filename - - -def generate_event_report_pdf_two(user, report_data): - start_date, _ = get_previous_month_date_range() - filename = generate_filename(user.email, start_date) - - buffer = BytesIO() - pdf = canvas.Canvas(buffer, pagesize=letter) - width, height = letter - - # Add a title and user information - pdf.setFont("Helvetica-Bold", 16) - pdf.drawString(100, 750, "Event Report - April 2024") - - user_name = user.email.split("@")[0] # Use part of email before @ as username - pdf.setFont("Helvetica", 12) - pdf.drawString(100, 730, f"For Event Manager: {user_name}") - - # Event loop with row handling - y_pos = 650 # Starting position for charts (adjust as needed) - chart_width = 250 # Width of each pie chart - chart_height = 150 # Height of each pie chart - chart_spacing = 50 # Spacing between charts in a row - - for i, event in enumerate(report_data): - # Add event name as a header - pdf.setFont("Helvetica-Bold", 12) - pdf.drawString(50, y_pos + 20, event["event_name"]) - - # Check if this is the first event in a row - if i % 2 == 0: - x_pos = 75 # Starting position for charts in the first column - else: - x_pos = ( - width - chart_width - 75 - ) # Starting position for charts in the second column - - # Draw pie charts for the event - for key, value in [ - ("favorites_count", "Favorites"), - ("interested_count", "Interested"), - ("going_count", "Going"), - ]: - pie_data = [value] - pie_labels = [value] - - drawing = Drawing(chart_width, chart_height) - pie = Pie() - pie.x = 0 - pie.y = 0 - pie.width = chart_width - pie.height = chart_height - pie.data = pie_data - pie.labels = pie_labels - pie.slices.strokeWidth = 0.5 - - drawing.add(pie) - renderPDF.draw(drawing, pdf, x_pos, y_pos) - - x_pos += chart_width + chart_spacing # Adjust x position for the next chart - - y_pos -= ( - 100 # Adjust y position for the next event (adjust based on chart heights) + shares = ( + EventShare.objects.filter(event=event) + .values("principal") + .annotate(share_count=Count("principal")) ) + shares_data = { + User.objects.get(id=share["principal"]).get_full_name(): share[ + "share_count" + ] + for share in shares + } - # Close the PDF object and write the buffer content to the PDF file - pdf.save() - buffer.seek(0) - pdf_data = buffer.read() - buffer.close() - - return pdf_data, filename + report_data.append( + { + "event_name": event.title, + "event_type": event.category.title, + "event_date": str(event.start_date), + "favorites_count": event.favorites_count, + "interested_count": event.interested_count, + "going_count": event.going_count, + "reviews_count": event.reviews_count, + "views_count": event.views_count, + "locations": dict(locations), + "social_media_shares": event.social_media_shares_count, + "shares_data": shares_data, + } + ) + print("report_data: ", report_data) + return report_data def generate_event_report_pdf_three(user, report_data): @@ -210,116 +114,179 @@ def generate_event_report_pdf_three(user, report_data): filename = generate_filename(user.email, start_date) buffer = BytesIO() - pdf = canvas.Canvas(buffer, pagesize=letter) + # pdf = canvas.Canvas(buffer, pagesize=letter) + pdf = SimpleDocTemplate(buffer, pagesize=letter) width, height = letter + elements = [] + + styles = getSampleStyleSheet() # Header Section - title = "Good Times Ltd. Monthly Report" - report_for_month = f"Report for the month of - {start_date.strftime('%B %Y')}" - organiser_name = f"Name of the Organiser - {user.get_full_name()}" - contact_name = f"Contact Name - {user.get_full_name()}" + title = Paragraph("Good Times Ltd. Monthly Report", styles["Title"]) + report_for_month = Paragraph( + f"Report for the month of - {start_date.strftime('%B %Y')}", styles["Title"] + ) + organiser_name = Paragraph( + f"Name of the Organiser - {user.get_full_name()}", styles["Title"] + ) + contact_name = Paragraph(f"Contact Name - {user.get_full_name()}", styles["Title"]) - pdf.setFont("Helvetica-Bold", 20) - title_width = pdf.stringWidth(title, "Helvetica-Bold", 20) - pdf.drawString((width - title_width) / 2, 750, title) + elements.extend( + [ + title, + Spacer(1, 12), + report_for_month, + Spacer(1, 12), + organiser_name, + Spacer(1, 12), + contact_name, + PageBreak(), + ] + ) - pdf.setFont("Helvetica", 16) - report_for_month_width = pdf.stringWidth(report_for_month, "Helvetica", 16) - pdf.drawString((width - report_for_month_width) / 2, 720, report_for_month) - - organiser_name_width = pdf.stringWidth(organiser_name, "Helvetica", 16) - pdf.drawString((width - organiser_name_width) / 2, 690, organiser_name) - - contact_name_width = pdf.stringWidth(contact_name, "Helvetica", 16) - pdf.drawString((width - contact_name_width) / 2, 660, contact_name) - pdf.showPage() # Summary Section - pdf.setFont("Helvetica-Bold", 12) - pdf.drawString( - 100, - 640, - f"Number of Events added in {start_date.strftime('%B %Y')} - {len(report_data)}", + summary_text = ( + f"Number of Events added in {start_date.strftime('%B %Y')} - {len(report_data)}" ) + elements.append(Paragraph(summary_text, styles["Normal"])) + elements.append(Spacer(1, 12)) - y_pos = 620 - pdf.setFont("Helvetica-Bold", 10) - pdf.drawString(50, y_pos, "Event Name") - pdf.drawString(200, y_pos, "Event Type") - pdf.drawString(350, y_pos, "Date") - y_pos -= 20 + data = [["Sr No.", "Name of the Event", "Event Type", "Date"]] + for idx, event in enumerate(report_data, start=1): + data.append( + [ + idx, + event["event_name"], + event["event_type"], + event["event_date"], + ] + ) - pdf.setFont("Helvetica", 10) + table = Table(data, colWidths=[50, 200, 150, 100]) + style = TableStyle( + [ + ("BACKGROUND", (0, 0), (-1, 0), colors.grey), + ("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke), + ("ALIGN", (0, 0), (-1, -1), "CENTER"), + ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), + ("BOTTOMPADDING", (0, 0), (-1, 0), 12), + ("BACKGROUND", (0, 1), (-1, -1), colors.beige), + ("GRID", (0, 0), (-1, -1), 1, colors.black), + ] + ) + table.setStyle(style) + + elements.append(table) + elements.append(PageBreak()) + + # Traffic Details Section + traffic_header = "Traffic Details for profile - Event Organisers London Ltd." + elements.append(Paragraph(traffic_header, styles["Heading2"])) + elements.append(Spacer(1, 12)) + + views_count = sum(event["views_count"] for event in report_data) + favorites_count = sum(event["favorites_count"] for event in report_data) + social_media_shares = sum(event["social_media_shares"] for event in report_data) + + elements.append( + Paragraph(f"Number of Event Views - {views_count}", styles["Normal"]) + ) + elements.append(Spacer(1, 12)) + elements.append( + Paragraph(f"Number of Event Favorites - {favorites_count}", styles["Normal"]) + ) + elements.append(Spacer(1, 12)) + elements.append( + Paragraph(f"Social Media Shares - {social_media_shares}", styles["Normal"]) + ) + # elements.append(PageBreak()) + elements.append(Spacer(1, 60)) + + # Top 5 Locations and Top 5 Viewed Events + all_locations = defaultdict(int) for event in report_data: - pdf.drawString(50, y_pos, event["event_name"]) - pdf.drawString(200, y_pos, event["event_type"]) - pdf.drawString(350, y_pos, event["event_date"]) - y_pos -= 20 + for location, count in event["locations"].items(): + all_locations[location] += count - # Start a new page for Traffic Details Section - pdf.showPage() + top_locations = sorted(all_locations.items(), key=lambda x: x[1], reverse=True)[:5] + top_events = sorted(report_data, key=lambda x: x["views_count"], reverse=True)[:5] - pdf.setFont("Helvetica-Bold", 12) - pdf.drawString( - 50, height - 50, "Traffic Details for profile - Event Organisers London Ltd." + data = [ + ["Top 5 Locations Viewed From", "Top 5 Viewed Events"], + ] + + for i in range(5): + location = top_locations[i][0] if i < len(top_locations) else "" + event = top_events[i]["event_name"] if i < len(top_events) else "" + data.append([location, event]) + + table = Table(data, colWidths=[200, 200]) + style = TableStyle( + [ + ("BACKGROUND", (0, 0), (-1, 0), colors.grey), + ("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke), + ("ALIGN", (0, 0), (-1, -1), "CENTER"), + ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), + ("BOTTOMPADDING", (0, 0), (-1, 0), 12), + ("BACKGROUND", (0, 1), (-1, -1), colors.beige), + ("GRID", (0, 0), (-1, -1), 1, colors.black), + ] ) + table.setStyle(style) + + elements.append(table) + elements.append(PageBreak()) # Detailed Review of Each Event for event in report_data: - pdf.showPage() - pdf.setFont("Helvetica-Bold", 12) - pdf.drawString(50, height - 50, f"Event Name - {event['event_name']}") - pdf.setFont("Helvetica", 10) - pdf.drawString(50, height - 70, f"Event Type - {event['event_type']}") - pdf.drawString(50, height - 90, f"Event Date - {event['event_date']}") + elements.append(Paragraph(f"Event Name - {event['event_name']}", styles['Heading2'])) + elements.append(Spacer(1, 12)) + elements.append(Paragraph(f"Event Type - {event['event_type']}", styles['Normal'])) + elements.append(Paragraph(f"Event Date - {event['event_date']}", styles['Normal'])) - y_pos = height - 120 - pdf.drawString(50, y_pos, f"Number of Views - {event['views_count']}") - pdf.drawString(200, y_pos, f"Favorites Count - {event['favorites_count']}") - pdf.drawString(350, y_pos, f"Interested in Going - {event['interested_count']}") - pdf.drawString(500, y_pos, f"Going - {event['going_count']}") - pdf.drawString(650, y_pos, f"Reviews - {event['reviews_count']}") - y_pos -= 40 + views = f"Number of Views - {event['views_count']}" + favorites = f"Favorites Count - {event['favorites_count']}" + interested = f"Interested in Going - {event['interested_count']}" + going = f"Going - {event['going_count']}" + reviews = f"Reviews - {event['reviews_count']}" + shares = f"Social Media Shares - {event['social_media_shares']}" - pdf.setFont("Helvetica-Bold", 10) - pdf.drawString(50, y_pos, "View Details:") - y_pos -= 20 - pdf.setFont("Helvetica", 10) - pdf.drawString(100, y_pos, "User Email") - pdf.drawString(300, y_pos, "Location") - pdf.drawString(500, y_pos, "View Date") - y_pos -= 20 - for view in event["locations"]: - pdf.drawString(100, y_pos, view["user__email"]) - pdf.drawString(300, y_pos, view["location"]) - pdf.drawString(500, y_pos, view["view_date"].strftime("%d %B %Y %H:%M")) - y_pos -= 20 + elements.append(Paragraph(views, styles['Normal'])) + elements.append(Paragraph(shares, styles['Normal'])) + elements.append(Paragraph(favorites, styles['Normal'])) + elements.append(Paragraph(interested, styles['Normal'])) + elements.append(Paragraph(going, styles['Normal'])) + elements.append(Paragraph(reviews, styles['Normal'])) + elements.append(Spacer(1, 12)) - # Draw pie chart on a new page if there's not enough space - if y_pos < 150: - pdf.showPage() - y_pos = height - 150 + location_details = [] + for location, count in event["locations"].items(): + location_details.append(f"{location}: {count}") + + elements.append(Paragraph("Event viewed from:", styles['Normal'])) + elements.append(Paragraph(", ".join(location_details), styles['Normal'])) + # elements.append(PageBreak()) + elements.append(Spacer(1, 60)) pie_data = [ event["views_count"], + event["social_media_shares"], event["favorites_count"], event["interested_count"], event["going_count"], ] - pie_labels = ["Views", "Favorites", "Interested", "Going"] + pie_labels = ["Views", "Shares", "Favorites", "Interested", "Going"] drawing = Drawing(200, 100) pie = Pie() - pie.x = 50 - pie.y = y_pos - 100 pie.data = pie_data pie.labels = pie_labels pie.width = 100 pie.height = 100 drawing.add(pie) - renderPDF.draw(drawing, pdf, 50, y_pos - 100) - y_pos -= 150 # Adjust for pie chart height - - pdf.save() + elements.append(drawing) + elements.append(PageBreak()) + pdf.build(elements) buffer.seek(0) pdf_data = buffer.read() buffer.close() From d09ff0f9735d9b54050dcea92f457b4d30999f02 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 3 Jun 2024 19:30:33 +0530 Subject: [PATCH 032/187] report and share api --- .../management/commands/manager_report.py | 33 ++++++++++++ manage_events/report.py | 50 ++++++++++++------- manage_events/views.py | 2 +- requirements.txt | 2 +- 4 files changed, 68 insertions(+), 19 deletions(-) create mode 100644 manage_events/management/commands/manager_report.py diff --git a/manage_events/management/commands/manager_report.py b/manage_events/management/commands/manager_report.py new file mode 100644 index 0000000..8da35e6 --- /dev/null +++ b/manage_events/management/commands/manager_report.py @@ -0,0 +1,33 @@ +from django.core.management.base import BaseCommand +from django.core.mail import EmailMessage +from django.conf import settings +from manage_events.report import ( + get_previous_month_date_range, + event_managers, + generate_event_report, + generate_event_report_pdf_three, +) + + +class Command(BaseCommand): + help = "Send monthly event reports to event managers" + + def handle(self, *args, **kwargs): + start_date, end_date = get_previous_month_date_range() + users = event_managers() + + for user in users: + report_data = generate_event_report(user.id) + if report_data: + pdf_data, filename = generate_event_report_pdf_three(user, report_data) + self.send_email_with_attachment(user.email, pdf_data, filename) + + def send_email_with_attachment(self, email, pdf_data, filename): + email_message = EmailMessage( + subject="Monthly Event Report", + body="Please find the attached report for the last month.", + to=[email], + from_email=settings.EMAIL_HOST_USER, + ) + email_message.attach(filename, pdf_data, "application/pdf") + email_message.send() diff --git a/manage_events/report.py b/manage_events/report.py index be32646..19619f5 100644 --- a/manage_events/report.py +++ b/manage_events/report.py @@ -6,7 +6,7 @@ from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import letter from reportlab.graphics.shapes import Drawing from reportlab.lib import colors -from reportlab.lib.styles import getSampleStyleSheet +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.graphics.charts.piecharts import Pie from reportlab.platypus import ( SimpleDocTemplate, @@ -21,6 +21,7 @@ from django.conf import settings from collections import defaultdict from reportlab.graphics import renderPDF from django.contrib.auth import get_user_model +from accounts.models import IAmPrincipalType from goodtimes.services import EmailService from manage_events.models import Event, EventInteractionType, EventShare, EventView @@ -37,6 +38,10 @@ def generate_filename(email, date): filename = f"{username}_{month_name}_report.pdf" return filename +def event_managers(): + principal_type = IAmPrincipalType.objects.filter(name="event_manager").first() + return User.objects.filter(principal_type=principal_type, is_active=True) + def get_previous_month_date_range(): today = timezone.now() @@ -69,7 +74,7 @@ def generate_event_report(user_id): ), views_count=Count("views", filter=Q(views__active=True, views__deleted=False)), ) - print("events: ", events) + # print("events: ", events) report_data = [] for event in events: @@ -105,7 +110,7 @@ def generate_event_report(user_id): "shares_data": shares_data, } ) - print("report_data: ", report_data) + # print("report_data: ", report_data) return report_data @@ -121,6 +126,14 @@ def generate_event_report_pdf_three(user, report_data): styles = getSampleStyleSheet() + custom_style = ParagraphStyle( + name="Custom", + parent=styles["Normal"], + fontName="Helvetica", + fontSize=14, + leading=18, + spaceAfter=12, + ) # Header Section title = Paragraph("Good Times Ltd. Monthly Report", styles["Title"]) report_for_month = Paragraph( @@ -148,8 +161,8 @@ def generate_event_report_pdf_three(user, report_data): summary_text = ( f"Number of Events added in {start_date.strftime('%B %Y')} - {len(report_data)}" ) - elements.append(Paragraph(summary_text, styles["Normal"])) - elements.append(Spacer(1, 12)) + elements.append(Paragraph(summary_text, styles["Title"])) + elements.append(Spacer(1, 24)) data = [["Sr No.", "Name of the Event", "Event Type", "Date"]] for idx, event in enumerate(report_data, start=1): @@ -204,12 +217,15 @@ def generate_event_report_pdf_three(user, report_data): # Top 5 Locations and Top 5 Viewed Events all_locations = defaultdict(int) + print("all_locations: ", all_locations) for event in report_data: for location, count in event["locations"].items(): all_locations[location] += count top_locations = sorted(all_locations.items(), key=lambda x: x[1], reverse=True)[:5] + print("top_locations: ", top_locations) top_events = sorted(report_data, key=lambda x: x["views_count"], reverse=True)[:5] + print("top_events: ", top_events) data = [ ["Top 5 Locations Viewed From", "Top 5 Viewed Events"], @@ -239,10 +255,10 @@ def generate_event_report_pdf_three(user, report_data): # Detailed Review of Each Event for event in report_data: - elements.append(Paragraph(f"Event Name - {event['event_name']}", styles['Heading2'])) + elements.append(Paragraph(f"Event Name - {event['event_name']}", styles['Heading1'])) elements.append(Spacer(1, 12)) - elements.append(Paragraph(f"Event Type - {event['event_type']}", styles['Normal'])) - elements.append(Paragraph(f"Event Date - {event['event_date']}", styles['Normal'])) + elements.append(Paragraph(f"Event Type - {event['event_type']}", custom_style)) + elements.append(Paragraph(f"Event Date - {event['event_date']}", custom_style)) views = f"Number of Views - {event['views_count']}" favorites = f"Favorites Count - {event['favorites_count']}" @@ -251,22 +267,22 @@ def generate_event_report_pdf_three(user, report_data): reviews = f"Reviews - {event['reviews_count']}" shares = f"Social Media Shares - {event['social_media_shares']}" - elements.append(Paragraph(views, styles['Normal'])) - elements.append(Paragraph(shares, styles['Normal'])) - elements.append(Paragraph(favorites, styles['Normal'])) - elements.append(Paragraph(interested, styles['Normal'])) - elements.append(Paragraph(going, styles['Normal'])) - elements.append(Paragraph(reviews, styles['Normal'])) + elements.append(Paragraph(views, custom_style)) + elements.append(Paragraph(shares, custom_style)) + elements.append(Paragraph(favorites, custom_style)) + elements.append(Paragraph(interested, custom_style)) + elements.append(Paragraph(going, custom_style)) + elements.append(Paragraph(reviews, custom_style)) elements.append(Spacer(1, 12)) location_details = [] for location, count in event["locations"].items(): location_details.append(f"{location}: {count}") - elements.append(Paragraph("Event viewed from:", styles['Normal'])) - elements.append(Paragraph(", ".join(location_details), styles['Normal'])) + elements.append(Paragraph("Event viewed from:", custom_style)) + elements.append(Paragraph(", ".join(location_details), custom_style)) # elements.append(PageBreak()) - elements.append(Spacer(1, 60)) + elements.append(Spacer(1, 48)) pie_data = [ event["views_count"], diff --git a/manage_events/views.py b/manage_events/views.py index 89c3336..0bfcc9e 100644 --- a/manage_events/views.py +++ b/manage_events/views.py @@ -500,5 +500,5 @@ class GenerateEventReportView(generic.View): # Create the HttpResponse object with the PDF data response = HttpResponse(pdf_data, content_type="application/pdf") response["Content-Disposition"] = f'attachment; filename="{filename}"' - + return response diff --git a/requirements.txt b/requirements.txt index 094748b..e80e07b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -68,7 +68,7 @@ sqlparse==0.4.4 stripe==8.2.0 tqdm==4.66.2 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 From b6a4567ff80a501d369a5dc450c281439f9d4f1f Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 3 Jun 2024 21:06:33 +0530 Subject: [PATCH 033/187] report and share api 2 --- manage_events/report.py | 71 ++++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/manage_events/report.py b/manage_events/report.py index 19619f5..c485293 100644 --- a/manage_events/report.py +++ b/manage_events/report.py @@ -1,4 +1,3 @@ -from django.core.mail import EmailMessage from django.db.models import Count, Q from django.utils import timezone from datetime import timedelta @@ -15,6 +14,7 @@ from reportlab.platypus import ( Paragraph, Spacer, PageBreak, + Image, ) from io import BytesIO from django.conf import settings @@ -22,8 +22,8 @@ from collections import defaultdict from reportlab.graphics import renderPDF from django.contrib.auth import get_user_model from accounts.models import IAmPrincipalType -from goodtimes.services import EmailService from manage_events.models import Event, EventInteractionType, EventShare, EventView +import os User = get_user_model() @@ -38,6 +38,7 @@ def generate_filename(email, date): filename = f"{username}_{month_name}_report.pdf" return filename + def event_managers(): principal_type = IAmPrincipalType.objects.filter(name="event_manager").first() return User.objects.filter(principal_type=principal_type, is_active=True) @@ -142,7 +143,7 @@ def generate_event_report_pdf_three(user, report_data): organiser_name = Paragraph( f"Name of the Organiser - {user.get_full_name()}", styles["Title"] ) - contact_name = Paragraph(f"Contact Name - {user.get_full_name()}", styles["Title"]) + contact_name = Paragraph(f"Contact Name - {user.email}", styles["Title"]) elements.extend( [ @@ -153,10 +154,23 @@ def generate_event_report_pdf_three(user, report_data): organiser_name, Spacer(1, 12), contact_name, - PageBreak(), + Spacer(1, 72), # Add space before the logo ] ) + # Insert company logo + logo_path = os.path.join("static", "images", "icon.png") # Path to the logo + logo = Image(logo_path) + logo.drawWidth = 300 # Adjust the width as needed + logo.drawHeight = 150 # Adjust the height as needed + logo.hAlign = "CENTER" # Center the logo + + elements.append(logo) + elements.append(Spacer(1, 12)) # Add space after the logo + + # Page break after header and logo + elements.append(PageBreak()) + # Summary Section summary_text = ( f"Number of Events added in {start_date.strftime('%B %Y')} - {len(report_data)}" @@ -194,38 +208,37 @@ def generate_event_report_pdf_three(user, report_data): # Traffic Details Section traffic_header = "Traffic Details for profile - Event Organisers London Ltd." - elements.append(Paragraph(traffic_header, styles["Heading2"])) + elements.append(Paragraph(traffic_header, styles["Title"])) elements.append(Spacer(1, 12)) views_count = sum(event["views_count"] for event in report_data) favorites_count = sum(event["favorites_count"] for event in report_data) social_media_shares = sum(event["social_media_shares"] for event in report_data) + elements.append(Paragraph(f"Number of Event Views - {views_count}", custom_style)) + elements.append(Spacer(1, 12)) elements.append( - Paragraph(f"Number of Event Views - {views_count}", styles["Normal"]) + Paragraph(f"Number of Event Favorites - {favorites_count}", custom_style) ) elements.append(Spacer(1, 12)) elements.append( - Paragraph(f"Number of Event Favorites - {favorites_count}", styles["Normal"]) - ) - elements.append(Spacer(1, 12)) - elements.append( - Paragraph(f"Social Media Shares - {social_media_shares}", styles["Normal"]) + Paragraph(f"Social Media Shares - {social_media_shares}", custom_style) ) # elements.append(PageBreak()) elements.append(Spacer(1, 60)) # Top 5 Locations and Top 5 Viewed Events all_locations = defaultdict(int) - print("all_locations: ", all_locations) for event in report_data: for location, count in event["locations"].items(): all_locations[location] += count - + # top 5 events by location top_locations = sorted(all_locations.items(), key=lambda x: x[1], reverse=True)[:5] - print("top_locations: ", top_locations) - top_events = sorted(report_data, key=lambda x: x["views_count"], reverse=True)[:5] - print("top_events: ", top_events) + + # top 5 events overall by views + top_events_by_views = sorted( + report_data, key=lambda x: x["views_count"], reverse=True + )[:5] data = [ ["Top 5 Locations Viewed From", "Top 5 Viewed Events"], @@ -233,7 +246,9 @@ def generate_event_report_pdf_three(user, report_data): for i in range(5): location = top_locations[i][0] if i < len(top_locations) else "" - event = top_events[i]["event_name"] if i < len(top_events) else "" + event = ( + top_events_by_views[i]["event_name"] if i < len(top_events_by_views) else "" + ) data.append([location, event]) table = Table(data, colWidths=[200, 200]) @@ -255,7 +270,9 @@ def generate_event_report_pdf_three(user, report_data): # Detailed Review of Each Event for event in report_data: - elements.append(Paragraph(f"Event Name - {event['event_name']}", styles['Heading1'])) + elements.append( + Paragraph(f"Event Name - {event['event_name']}", styles["Heading1"]) + ) elements.append(Spacer(1, 12)) elements.append(Paragraph(f"Event Type - {event['event_type']}", custom_style)) elements.append(Paragraph(f"Event Date - {event['event_date']}", custom_style)) @@ -275,12 +292,12 @@ def generate_event_report_pdf_three(user, report_data): elements.append(Paragraph(reviews, custom_style)) elements.append(Spacer(1, 12)) - location_details = [] - for location, count in event["locations"].items(): - location_details.append(f"{location}: {count}") + # location_details = [] + # for location, count in event["locations"].items(): + # location_details.append(f"{location}: {count}") - elements.append(Paragraph("Event viewed from:", custom_style)) - elements.append(Paragraph(", ".join(location_details), custom_style)) + # elements.append(Paragraph("Event viewed from:", custom_style)) + # elements.append(Paragraph(", ".join(location_details), custom_style)) # elements.append(PageBreak()) elements.append(Spacer(1, 48)) @@ -293,12 +310,14 @@ def generate_event_report_pdf_three(user, report_data): ] pie_labels = ["Views", "Shares", "Favorites", "Interested", "Going"] - drawing = Drawing(200, 100) + drawing = Drawing(300, 200) pie = Pie() pie.data = pie_data pie.labels = pie_labels - pie.width = 100 - pie.height = 100 + pie.width = 150 + pie.height = 150 + pie.x = 75 + pie.y = 25 drawing.add(pie) elements.append(drawing) elements.append(PageBreak()) From ecd924637550da5726bae06f6c8f44bb84219e95 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Tue, 4 Jun 2024 14:01:18 +0530 Subject: [PATCH 034/187] referral and reports --- goodtimes/webhook.py | 15 +++++++++++++-- manage_events/report.py | 18 ++++-------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/goodtimes/webhook.py b/goodtimes/webhook.py index 9b54998..df5703d 100644 --- a/goodtimes/webhook.py +++ b/goodtimes/webhook.py @@ -1,11 +1,11 @@ from django.conf import settings from django.db import transaction -import requests +from django.shortcuts import get_object_or_404 from datetime import timedelta from django.utils import timezone from onesignal_sdk.client import Client as OneSignalClient from accounts.models import IAmPrincipal, IAmPrincipalOtp, IAmPrincipalType -from manage_notifications.models import InAppNotification, NotificationCategoryChoices +from manage_notifications.models import IAmPrincipalNotificationSettings, InAppNotification, NotificationCategoryChoices from manage_referrals.models import ( GoodTimeCoins, ReferralRecord, @@ -71,11 +71,22 @@ class NotificationService: def payment_failed_notification(self, principal, subscription, amount): print("payment_failed_notification: ", principal.player_id) + if not self.should_send_referral_notification(principal): + print("Referral notifications are disabled for this user") + return title = "Payment Failed!" message = f"Your payment for {subscription} of ${amount} was failed." self.send_notification(title, message, principal.player_id) self.save_notification(principal, title, message, NotificationCategoryChoices.TRANSACTION) + def should_send_referral_notification(self, principal): + notification_settings = get_object_or_404( + IAmPrincipalNotificationSettings, + principal=principal, + notification_category=NotificationCategoryChoices.REFERRAL, + ) + return notification_settings.is_enabled + class WebhookService: def __init__(self, webhook_data): diff --git a/manage_events/report.py b/manage_events/report.py index c485293..10a63b5 100644 --- a/manage_events/report.py +++ b/manage_events/report.py @@ -1,7 +1,6 @@ from django.db.models import Count, Q from django.utils import timezone from datetime import timedelta -from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import letter from reportlab.graphics.shapes import Drawing from reportlab.lib import colors @@ -143,7 +142,7 @@ def generate_event_report_pdf_three(user, report_data): organiser_name = Paragraph( f"Name of the Organiser - {user.get_full_name()}", styles["Title"] ) - contact_name = Paragraph(f"Contact Name - {user.email}", styles["Title"]) + contact_email = Paragraph(f"Contact Email - {user.email}", styles["Title"]) elements.extend( [ @@ -153,7 +152,7 @@ def generate_event_report_pdf_three(user, report_data): Spacer(1, 12), organiser_name, Spacer(1, 12), - contact_name, + contact_email, Spacer(1, 72), # Add space before the logo ] ) @@ -161,8 +160,8 @@ def generate_event_report_pdf_three(user, report_data): # Insert company logo logo_path = os.path.join("static", "images", "icon.png") # Path to the logo logo = Image(logo_path) - logo.drawWidth = 300 # Adjust the width as needed - logo.drawHeight = 150 # Adjust the height as needed + logo.drawWidth = 150 # Adjust the width as needed + logo.drawHeight = 300 # Adjust the height as needed logo.hAlign = "CENTER" # Center the logo elements.append(logo) @@ -290,15 +289,6 @@ def generate_event_report_pdf_three(user, report_data): elements.append(Paragraph(interested, custom_style)) elements.append(Paragraph(going, custom_style)) elements.append(Paragraph(reviews, custom_style)) - elements.append(Spacer(1, 12)) - - # location_details = [] - # for location, count in event["locations"].items(): - # location_details.append(f"{location}: {count}") - - # elements.append(Paragraph("Event viewed from:", custom_style)) - # elements.append(Paragraph(", ".join(location_details), custom_style)) - # elements.append(PageBreak()) elements.append(Spacer(1, 48)) pie_data = [ From d43d135123d0ed7a1e1ee8a0d7bc0f7faa3d37e0 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Tue, 4 Jun 2024 15:01:20 +0530 Subject: [PATCH 035/187] corrected code --- goodtimes/webhook.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/goodtimes/webhook.py b/goodtimes/webhook.py index df5703d..4a5a52f 100644 --- a/goodtimes/webhook.py +++ b/goodtimes/webhook.py @@ -64,6 +64,9 @@ class NotificationService: def referral_received_notification(self, principal, amount, email): print("referral_received_notification: ", principal.player_id) + if not self.should_send_referral_notification(principal): + print("Referral notifications are disabled for this user") + return title = "Congratulations! You got a referral G-Token." message = f"Your referral {email} has subscribed to GoodTimesApp. You have received {amount} (£)" self.send_notification(title, message, principal.player_id) @@ -71,9 +74,6 @@ class NotificationService: def payment_failed_notification(self, principal, subscription, amount): print("payment_failed_notification: ", principal.player_id) - if not self.should_send_referral_notification(principal): - print("Referral notifications are disabled for this user") - return title = "Payment Failed!" message = f"Your payment for {subscription} of ${amount} was failed." self.send_notification(title, message, principal.player_id) From 9b37e0ee5b10c6830b3d53cef1915413be9f0a10 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Tue, 4 Jun 2024 15:35:14 +0530 Subject: [PATCH 036/187] referral notifications correction --- goodtimes/webhook.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/goodtimes/webhook.py b/goodtimes/webhook.py index 4a5a52f..7824ddf 100644 --- a/goodtimes/webhook.py +++ b/goodtimes/webhook.py @@ -5,7 +5,11 @@ from datetime import timedelta from django.utils import timezone from onesignal_sdk.client import Client as OneSignalClient from accounts.models import IAmPrincipal, IAmPrincipalOtp, IAmPrincipalType -from manage_notifications.models import IAmPrincipalNotificationSettings, InAppNotification, NotificationCategoryChoices +from manage_notifications.models import ( + IAmPrincipalNotificationSettings, + InAppNotification, + NotificationCategoryChoices, +) from manage_referrals.models import ( GoodTimeCoins, ReferralRecord, @@ -60,24 +64,30 @@ class NotificationService: end_date = principal_subscription.end_date message = f"Your payment for {subscription} of ${amount} was successfully processed. Your subscription is valid till {end_date}" self.send_notification(title, message, principal.player_id) - self.save_notification(principal, title, message, NotificationCategoryChoices.TRANSACTION) + self.save_notification( + principal, title, message, NotificationCategoryChoices.TRANSACTION + ) def referral_received_notification(self, principal, amount, email): print("referral_received_notification: ", principal.player_id) + title = "Congratulations! You got a referral G-Token." + message = f"Your referral {email} has subscribed to GoodTimesApp. You have received {amount} (£)" + self.save_notification( + principal, title, message, NotificationCategoryChoices.REFERRAL + ) if not self.should_send_referral_notification(principal): print("Referral notifications are disabled for this user") return - title = "Congratulations! You got a referral G-Token." - message = f"Your referral {email} has subscribed to GoodTimesApp. You have received {amount} (£)" self.send_notification(title, message, principal.player_id) - self.save_notification(principal, title, message, NotificationCategoryChoices.REFERRAL) def payment_failed_notification(self, principal, subscription, amount): print("payment_failed_notification: ", principal.player_id) title = "Payment Failed!" message = f"Your payment for {subscription} of ${amount} was failed." self.send_notification(title, message, principal.player_id) - self.save_notification(principal, title, message, NotificationCategoryChoices.TRANSACTION) + self.save_notification( + principal, title, message, NotificationCategoryChoices.TRANSACTION + ) def should_send_referral_notification(self, principal): notification_settings = get_object_or_404( From 6ae855cfe134d3bd344ff6113ae112fa19f692b1 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Tue, 4 Jun 2024 16:12:52 +0530 Subject: [PATCH 037/187] testing --- manage_events/forms.py | 4 ++-- .../migrations/0011_alter_event_entry_type.py | 20 +++++++++++++++++++ manage_events/models.py | 12 +++++++++-- manage_subscriptions/forms.py | 2 ++ manage_subscriptions/views.py | 2 +- templates/manage_events/event_list.html | 2 +- 6 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 manage_events/migrations/0011_alter_event_entry_type.py diff --git a/manage_events/forms.py b/manage_events/forms.py index 02d2e90..01924b8 100644 --- a/manage_events/forms.py +++ b/manage_events/forms.py @@ -31,7 +31,7 @@ class EventForm(forms.ModelForm): "category", "venue", "venue_capacity", - "video_url", + # "video_url", "entry_type", "entry_fee", "key_guest", @@ -57,7 +57,7 @@ class EventForm(forms.ModelForm): "to_time": forms.TimeInput(attrs={"class": "form-control", "type": "time"}), "venue_capacity": forms.NumberInput(attrs={"class": "form-control"}), "video_url": forms.URLInput(attrs={"class": "form-control"}), - "entry_type": forms.TextInput(attrs={"class": "form-control"}), + "entry_type": forms.Select(attrs={"class": "form-control"}), "entry_fee": forms.NumberInput(attrs={"class": "form-control"}), "key_guest": forms.Textarea(attrs={"class": "form-control", "rows": 3}), "age_group": forms.TextInput(attrs={"class": "form-control"}), diff --git a/manage_events/migrations/0011_alter_event_entry_type.py b/manage_events/migrations/0011_alter_event_entry_type.py new file mode 100644 index 0000000..f5b8390 --- /dev/null +++ b/manage_events/migrations/0011_alter_event_entry_type.py @@ -0,0 +1,20 @@ +# Generated by Django 5.0.2 on 2024-06-04 10:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("manage_events", "0010_event_social_media_shares_count_eventshare"), + ] + + operations = [ + migrations.AlterField( + model_name="event", + name="entry_type", + field=models.CharField( + choices=[("free", "Free"), ("paid", "Paid")], max_length=10 + ), + ), + ] diff --git a/manage_events/models.py b/manage_events/models.py index a961ab5..b9d122b 100644 --- a/manage_events/models.py +++ b/manage_events/models.py @@ -55,6 +55,10 @@ class EventMaster(BaseModel): class Event(BaseModel): + ENTRY_TYPE_CHOICES = [ + ("free", "Free"), + ("paid", "Paid"), + ] title = models.CharField(max_length=255) category = models.ForeignKey(EventCategory, on_delete=models.CASCADE) event_master = models.ForeignKey( @@ -74,9 +78,13 @@ class Event(BaseModel): venue_capacity = models.IntegerField() video_url = models.URLField(max_length=200, blank=True, null=True) + # entry_type = models.CharField( + # max_length=100 + # ) entry_type = models.CharField( - max_length=100 - ) # Assuming entry type is a string (e.g., Free, Ticketed) + max_length=10, + choices=ENTRY_TYPE_CHOICES, + ) entry_fee = models.DecimalField( max_digits=14, decimal_places=2, default=0.00 ) # Assuming it's an integer. Use DecimalField if you need to handle cents. diff --git a/manage_subscriptions/forms.py b/manage_subscriptions/forms.py index 3579fc4..064a35d 100644 --- a/manage_subscriptions/forms.py +++ b/manage_subscriptions/forms.py @@ -28,6 +28,8 @@ class SubscriptionForm(forms.ModelForm): # "image", "principal_types", "referral_percentage", + "active", + "deleted", ] # Include all fields you want from the model diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 6420e25..480e4e0 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -386,7 +386,7 @@ class SubscriptionPageView(TemplateView): if request.user.is_authenticated: print("request.user: ", request.user) subscriptions = Subscription.objects.filter( - principal_types=request.user.principal_type + principal_types=request.user.principal_type, active=True, deleted=False ) if subscriptions.exists(): diff --git a/templates/manage_events/event_list.html b/templates/manage_events/event_list.html index 525db2b..bc3f027 100644 --- a/templates/manage_events/event_list.html +++ b/templates/manage_events/event_list.html @@ -21,7 +21,7 @@ Back --> - + Add Event Event Category From 9d502d5b602bf1862095e55746587439273c1801 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Wed, 5 Jun 2024 19:16:23 +0530 Subject: [PATCH 038/187] report problem --- manage_events/report.py | 2 +- manage_events/views.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/manage_events/report.py b/manage_events/report.py index 10a63b5..afc893a 100644 --- a/manage_events/report.py +++ b/manage_events/report.py @@ -160,7 +160,7 @@ def generate_event_report_pdf_three(user, report_data): # Insert company logo logo_path = os.path.join("static", "images", "icon.png") # Path to the logo logo = Image(logo_path) - logo.drawWidth = 150 # Adjust the width as needed + logo.drawWidth = 200 # Adjust the width as needed logo.drawHeight = 300 # Adjust the height as needed logo.hAlign = "CENTER" # Center the logo diff --git a/manage_events/views.py b/manage_events/views.py index 0bfcc9e..080e7bc 100644 --- a/manage_events/views.py +++ b/manage_events/views.py @@ -488,6 +488,7 @@ from django.http import HttpResponse class GenerateEventReportView(generic.View): def get(self, request, user_id): + print("INside GET GenerateEventReportView") # Generate the event report report_data = generate_event_report(user_id) From a303d1056df6712817c0d22f72c105311ee79a06 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Wed, 5 Jun 2024 19:39:23 +0530 Subject: [PATCH 039/187] report problem 2 --- goodtimes/settings/staging.py | 4 +++- manage_events/report.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/goodtimes/settings/staging.py b/goodtimes/settings/staging.py index 30b9c16..0ecc8f2 100644 --- a/goodtimes/settings/staging.py +++ b/goodtimes/settings/staging.py @@ -73,7 +73,8 @@ MEDIA_ROOT = os.path.join(BASE_DIR, "media") STATIC_URL = "/static/" -STATICFILES_DIRS = [BASE_DIR.joinpath("static")] +# STATICFILES_DIRS = [BASE_DIR.joinpath("static")] +STATICFILES_DIRS = os.path.join(BASE_DIR, "static") STRIPE_CHECKOUT_URL = ( "https://staging.goodtimesltd.co.uk/subscriptions/stripe-subscription/" @@ -81,3 +82,4 @@ STRIPE_CHECKOUT_URL = ( STRIPE_FINAL_URL = ( "https://staging.goodtimesltd.co.uk/subscriptions/create-checkout-session/" ) + diff --git a/manage_events/report.py b/manage_events/report.py index afc893a..0ebce15 100644 --- a/manage_events/report.py +++ b/manage_events/report.py @@ -158,7 +158,7 @@ def generate_event_report_pdf_three(user, report_data): ) # Insert company logo - logo_path = os.path.join("static", "images", "icon.png") # Path to the logo + logo_path = os.path.join("static/images/icon.png") # Path to the logo logo = Image(logo_path) logo.drawWidth = 200 # Adjust the width as needed logo.drawHeight = 300 # Adjust the height as needed From 2126660d9b2e1d8f33010faa90a7821937bbb2bc Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Wed, 5 Jun 2024 19:47:48 +0530 Subject: [PATCH 040/187] report problem 3 --- goodtimes/settings/development.py | 2 ++ goodtimes/settings/production.py | 2 ++ goodtimes/settings/staging.py | 5 +++-- manage_events/report.py | 3 ++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/goodtimes/settings/development.py b/goodtimes/settings/development.py index f362626..5846cde 100644 --- a/goodtimes/settings/development.py +++ b/goodtimes/settings/development.py @@ -56,3 +56,5 @@ STATICFILES_DIRS = [BASE_DIR.joinpath("static")] STRIPE_CHECKOUT_URL = "http://localhost:8000/subscriptions/stripe-subscription/" STRIPE_FINAL_URL = "http://localhost:8000/subscriptions/create-checkout-session/" + +LOGO_PATH = "/static/images" \ No newline at end of file diff --git a/goodtimes/settings/production.py b/goodtimes/settings/production.py index 6675744..5460bca 100644 --- a/goodtimes/settings/production.py +++ b/goodtimes/settings/production.py @@ -82,3 +82,5 @@ STRIPE_CHECKOUT_URL = ( STRIPE_FINAL_URL = ( "https://admin.goodtimesltd.co.uk/subscriptions/create-checkout-session/" ) + +LOGO_PATH = "/var/www/goodtimes_prod/goodtimes/static/images" \ No newline at end of file diff --git a/goodtimes/settings/staging.py b/goodtimes/settings/staging.py index 0ecc8f2..440e5ba 100644 --- a/goodtimes/settings/staging.py +++ b/goodtimes/settings/staging.py @@ -73,8 +73,8 @@ MEDIA_ROOT = os.path.join(BASE_DIR, "media") STATIC_URL = "/static/" -# STATICFILES_DIRS = [BASE_DIR.joinpath("static")] -STATICFILES_DIRS = os.path.join(BASE_DIR, "static") +STATICFILES_DIRS = [BASE_DIR.joinpath("static")] + STRIPE_CHECKOUT_URL = ( "https://staging.goodtimesltd.co.uk/subscriptions/stripe-subscription/" @@ -83,3 +83,4 @@ STRIPE_FINAL_URL = ( "https://staging.goodtimesltd.co.uk/subscriptions/create-checkout-session/" ) +LOGO_PATH = "/var/www/goodtimes/static/images" diff --git a/manage_events/report.py b/manage_events/report.py index 0ebce15..9273c0a 100644 --- a/manage_events/report.py +++ b/manage_events/report.py @@ -158,7 +158,8 @@ def generate_event_report_pdf_three(user, report_data): ) # Insert company logo - logo_path = os.path.join("static/images/icon.png") # Path to the logo + logo_path = settings.LOGO_PATH + logo_path = os.path.join(str(logo_path), "icon.png") # Path to the logo logo = Image(logo_path) logo.drawWidth = 200 # Adjust the width as needed logo.drawHeight = 300 # Adjust the height as needed From 59a9a5eb989e199cd0417be19e4c56b7069677ec Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Wed, 5 Jun 2024 19:48:48 +0530 Subject: [PATCH 041/187] report problem 4 --- manage_events/report.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manage_events/report.py b/manage_events/report.py index 9273c0a..85b70b9 100644 --- a/manage_events/report.py +++ b/manage_events/report.py @@ -159,6 +159,7 @@ def generate_event_report_pdf_three(user, report_data): # Insert company logo logo_path = settings.LOGO_PATH + print("logo_path: ", logo_path) logo_path = os.path.join(str(logo_path), "icon.png") # Path to the logo logo = Image(logo_path) logo.drawWidth = 200 # Adjust the width as needed From e6ca0c5c71d4f6cc81886e6113a1f0fe20cf6706 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Wed, 5 Jun 2024 19:54:40 +0530 Subject: [PATCH 042/187] report problem 5 --- goodtimes/settings/development.py | 2 +- goodtimes/settings/production.py | 2 +- goodtimes/settings/staging.py | 2 +- manage_events/report.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/goodtimes/settings/development.py b/goodtimes/settings/development.py index 5846cde..f33f818 100644 --- a/goodtimes/settings/development.py +++ b/goodtimes/settings/development.py @@ -57,4 +57,4 @@ STATICFILES_DIRS = [BASE_DIR.joinpath("static")] STRIPE_CHECKOUT_URL = "http://localhost:8000/subscriptions/stripe-subscription/" STRIPE_FINAL_URL = "http://localhost:8000/subscriptions/create-checkout-session/" -LOGO_PATH = "/static/images" \ No newline at end of file +LOGO_PATH = "static" diff --git a/goodtimes/settings/production.py b/goodtimes/settings/production.py index 5460bca..c3ed2bf 100644 --- a/goodtimes/settings/production.py +++ b/goodtimes/settings/production.py @@ -83,4 +83,4 @@ STRIPE_FINAL_URL = ( "https://admin.goodtimesltd.co.uk/subscriptions/create-checkout-session/" ) -LOGO_PATH = "/var/www/goodtimes_prod/goodtimes/static/images" \ No newline at end of file +LOGO_PATH = "/var/www/goodtimes_prod/goodtimes/static" diff --git a/goodtimes/settings/staging.py b/goodtimes/settings/staging.py index 440e5ba..315c495 100644 --- a/goodtimes/settings/staging.py +++ b/goodtimes/settings/staging.py @@ -83,4 +83,4 @@ STRIPE_FINAL_URL = ( "https://staging.goodtimesltd.co.uk/subscriptions/create-checkout-session/" ) -LOGO_PATH = "/var/www/goodtimes/static/images" +LOGO_PATH = "/var/www/goodtimes/static" diff --git a/manage_events/report.py b/manage_events/report.py index 85b70b9..fbc94b0 100644 --- a/manage_events/report.py +++ b/manage_events/report.py @@ -160,7 +160,7 @@ def generate_event_report_pdf_three(user, report_data): # Insert company logo logo_path = settings.LOGO_PATH print("logo_path: ", logo_path) - logo_path = os.path.join(str(logo_path), "icon.png") # Path to the logo + logo_path = os.path.join(str(logo_path), "images/icon.png") # Path to the logo logo = Image(logo_path) logo.drawWidth = 200 # Adjust the width as needed logo.drawHeight = 300 # Adjust the height as needed From f746f0c29aee8920d5e190e785ccc3c31f0cd643 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Wed, 5 Jun 2024 20:52:54 +0530 Subject: [PATCH 043/187] report problem 6 --- manage_events/report.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/manage_events/report.py b/manage_events/report.py index fbc94b0..c3cb821 100644 --- a/manage_events/report.py +++ b/manage_events/report.py @@ -2,7 +2,8 @@ from django.db.models import Count, Q from django.utils import timezone from datetime import timedelta from reportlab.lib.pagesizes import letter -from reportlab.graphics.shapes import Drawing +from reportlab.lib.units import mm +from reportlab.graphics.shapes import Drawing, Rect from reportlab.lib import colors from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.graphics.charts.piecharts import Pie @@ -134,6 +135,11 @@ def generate_event_report_pdf_three(user, report_data): leading=18, spaceAfter=12, ) + + def add_page_number(canvas, doc): + page_num_text = f"Page {doc.page}" + canvas.drawRightString(200 * mm, 10 * mm, page_num_text) + # Header Section title = Paragraph("Good Times Ltd. Monthly Report", styles["Title"]) report_for_month = Paragraph( @@ -313,7 +319,7 @@ def generate_event_report_pdf_three(user, report_data): drawing.add(pie) elements.append(drawing) elements.append(PageBreak()) - pdf.build(elements) + pdf.build(elements, onFirstPage=add_page_number, onLaterPages=add_page_number) buffer.seek(0) pdf_data = buffer.read() buffer.close() From e9079f6c828dabb9bf31c179f0d7014f3d89f115 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 7 Jun 2024 15:23:58 +0530 Subject: [PATCH 044/187] added model admin for favorites --- manage_events/admin.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/manage_events/admin.py b/manage_events/admin.py index ff0a938..abaf588 100644 --- a/manage_events/admin.py +++ b/manage_events/admin.py @@ -3,6 +3,7 @@ from .models import ( EventCategory, EventShare, EventView, + Favorites, Venue, EventMaster, Event, @@ -113,6 +114,14 @@ class EventShareAdmin(admin.ModelAdmin): list_filter = ("id", "event", "principal", "created_on") +class FavoritesAdmin(admin.ModelAdmin): + list_display = ("id", "principal", "event") + search_fields = ("principal__username", "event__title") + list_filter = ("principal", "event") + ordering = ("id",) + + +admin.site.register(Favorites, FavoritesAdmin) admin.site.register(EventShare, EventShareAdmin) admin.site.register(EventView, EventViewAdmin) admin.site.register(EventPrincipalInteraction, EventPrincipalInteractionAdmin) From 9a3ff98253f2160ec824a8ad1c8e6b58987b191a Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 7 Jun 2024 15:51:03 +0530 Subject: [PATCH 045/187] removed the active and deleted from views and reviews --- manage_events/report.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/manage_events/report.py b/manage_events/report.py index c3cb821..ed8474c 100644 --- a/manage_events/report.py +++ b/manage_events/report.py @@ -59,9 +59,7 @@ def generate_event_report(user_id): events = Event.objects.filter( created_by=user, start_date__gte=start_date, start_date__lte=end_date ).annotate( - favorites_count=Count( - "favorites", filter=Q(favorites__active=True, favorites__deleted=False) - ), + favorites_count=Count("favorites"), interested_count=Count( "interaction_event", filter=Q(interaction_event__status=EventInteractionType.INTERESTED), @@ -73,7 +71,7 @@ def generate_event_report(user_id): reviews_count=Count( "reviews", filter=Q(reviews__active=True, reviews__deleted=False) ), - views_count=Count("views", filter=Q(views__active=True, views__deleted=False)), + views_count=Count("views"), ) # print("events: ", events) From d1f24f08673c331702a0d50d5be9d6eec1d89053 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 7 Jun 2024 15:59:35 +0530 Subject: [PATCH 046/187] updated report count --- manage_events/report.py | 96 ++++++++++++++++++++++++++++++++--------- 1 file changed, 75 insertions(+), 21 deletions(-) diff --git a/manage_events/report.py b/manage_events/report.py index ed8474c..7d78216 100644 --- a/manage_events/report.py +++ b/manage_events/report.py @@ -56,32 +56,85 @@ def generate_event_report(user_id): start_date, end_date = get_previous_month_date_range() user = User.objects.get(id=user_id) + # events = Event.objects.filter( + # created_by=user, start_date__gte=start_date, start_date__lte=end_date + # ).annotate( + # favorites_count=Count("favorites"), + # interested_count=Count( + # "interaction_event", + # filter=Q(interaction_event__status=EventInteractionType.INTERESTED), + # ), + # going_count=Count( + # "interaction_event", + # filter=Q(interaction_event__status=EventInteractionType.GOING), + # ), + # reviews_count=Count( + # "reviews", filter=Q(reviews__active=True, reviews__deleted=False) + # ), + # views_count=Count("views"), + # ) + # # print("events: ", events) + + # report_data = [] + # for event in events: + # views = EventView.objects.filter(event=event) + # locations = defaultdict(int) + # for view in views: + # locations[view.location] += 1 + + # shares = ( + # EventShare.objects.filter(event=event) + # .values("principal") + # .annotate(share_count=Count("principal")) + # ) + # shares_data = { + # User.objects.get(id=share["principal"]).get_full_name(): share[ + # "share_count" + # ] + # for share in shares + # } + + # report_data.append( + # { + # "event_name": event.title, + # "event_type": event.category.title, + # "event_date": str(event.start_date), + # "favorites_count": event.favorites_count, + # "interested_count": event.interested_count, + # "going_count": event.going_count, + # "reviews_count": event.reviews_count, + # "views_count": event.views_count, + # "locations": dict(locations), + # "social_media_shares": event.social_media_shares_count, + # "shares_data": shares_data, + # } + # ) + # # print("report_data: ", report_data) + # return report_data events = Event.objects.filter( created_by=user, start_date__gte=start_date, start_date__lte=end_date - ).annotate( - favorites_count=Count("favorites"), - interested_count=Count( - "interaction_event", - filter=Q(interaction_event__status=EventInteractionType.INTERESTED), - ), - going_count=Count( - "interaction_event", - filter=Q(interaction_event__status=EventInteractionType.GOING), - ), - reviews_count=Count( - "reviews", filter=Q(reviews__active=True, reviews__deleted=False) - ), - views_count=Count("views"), ) - # print("events: ", events) report_data = [] for event in events: + # Collecting individual counts for each event + favorites_count = event.favorites_set.count() + interested_count = event.interaction_event.filter( + status=EventInteractionType.INTERESTED + ).count() + going_count = event.interaction_event.filter( + status=EventInteractionType.GOING + ).count() + reviews_count = event.reviews_set.filter(active=True, deleted=False).count() + views_count = event.views_set.count() + + # Collecting views and locations views = EventView.objects.filter(event=event) locations = defaultdict(int) for view in views: locations[view.location] += 1 + # Collecting shares data shares = ( EventShare.objects.filter(event=event) .values("principal") @@ -94,22 +147,23 @@ def generate_event_report(user_id): for share in shares } + # Appending event data to report report_data.append( { "event_name": event.title, "event_type": event.category.title, "event_date": str(event.start_date), - "favorites_count": event.favorites_count, - "interested_count": event.interested_count, - "going_count": event.going_count, - "reviews_count": event.reviews_count, - "views_count": event.views_count, + "favorites_count": favorites_count, + "interested_count": interested_count, + "going_count": going_count, + "reviews_count": reviews_count, + "views_count": views_count, "locations": dict(locations), "social_media_shares": event.social_media_shares_count, "shares_data": shares_data, } ) - # print("report_data: ", report_data) + return report_data From 15265b191456e9afc72d7109b477802fe9e752e7 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 7 Jun 2024 17:06:47 +0530 Subject: [PATCH 047/187] saving interested and going notifications into database if enabled False --- manage_events/report.py | 6 +-- .../management/commands/interested_going.py | 40 +++++++++++-------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/manage_events/report.py b/manage_events/report.py index 7d78216..5f571b4 100644 --- a/manage_events/report.py +++ b/manage_events/report.py @@ -118,15 +118,15 @@ def generate_event_report(user_id): report_data = [] for event in events: # Collecting individual counts for each event - favorites_count = event.favorites_set.count() + favorites_count = event.favorites.count() interested_count = event.interaction_event.filter( status=EventInteractionType.INTERESTED ).count() going_count = event.interaction_event.filter( status=EventInteractionType.GOING ).count() - reviews_count = event.reviews_set.filter(active=True, deleted=False).count() - views_count = event.views_set.count() + reviews_count = event.reviews.filter(active=True, deleted=False).count() + views_count = event.views.count() # Collecting views and locations views = EventView.objects.filter(event=event) diff --git a/manage_notifications/management/commands/interested_going.py b/manage_notifications/management/commands/interested_going.py index 1d8ba6d..bddc788 100644 --- a/manage_notifications/management/commands/interested_going.py +++ b/manage_notifications/management/commands/interested_going.py @@ -29,21 +29,7 @@ class Command(BaseCommand): principal_interaction.event.title ) # Accessing event title correctly message = f"{event_title} is going live tomorrow." - - try: - player_id = ( - principal_interaction.principal.player_id - ) # Correctly access player_id from principal - except AttributeError: - continue - notification_title = "Event Reminder" - notification_payload = { - "headings": {"en": notification_title}, - "contents": {"en": message}, - "include_player_ids": [player_id], - } - response = client.send_notification(notification_payload) in_app_notification = InAppNotification( principal=principal_interaction.principal, @@ -53,6 +39,28 @@ class Command(BaseCommand): ) in_app_notification.save() + notification_settings = IAmPrincipalNotificationSettings.objects.filter( + principal=principal_interaction.principal, + notification_category=NotificationCategoryChoices.EVENT, + is_enabled=True, + ).exists() + + if notification_settings: + try: + player_id = ( + principal_interaction.principal.player_id + ) # Correctly access player_id from principal + if player_id: + notification_payload = { + "headings": {"en": notification_title}, + "contents": {"en": message}, + "include_player_ids": [player_id], + } + response = client.send_notification(notification_payload) + except AttributeError: + # Handle the case where player_id does not exist + continue + self.stdout.write(self.style.SUCCESS("Event reminders sent successfully")) def eligible_event_interactions(self): @@ -64,6 +72,6 @@ class Command(BaseCommand): event__created_by__is_active=True, principal__is_active=True, status__in=[EventInteractionType.GOING, EventInteractionType.INTERESTED], - principal__notifications_principal__notification_category=NotificationCategoryChoices.EVENT, - principal__notifications_principal__is_enabled=True, + # principal__notifications_principal__notification_category=NotificationCategoryChoices.EVENT, + # principal__notifications_principal__is_enabled=True, ).select_related("principal", "event") From b6a0b0b537ac1fcb1be54869ed4afb766d72b9c4 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 7 Jun 2024 20:34:45 +0530 Subject: [PATCH 048/187] selling selected rewards --- manage_referrals/api/serializers.py | 1 + manage_referrals/api/urls.py | 5 +++ manage_referrals/api/views.py | 69 ++++++++++++++++++++++++++++- 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/manage_referrals/api/serializers.py b/manage_referrals/api/serializers.py index 48dfa83..53c559a 100644 --- a/manage_referrals/api/serializers.py +++ b/manage_referrals/api/serializers.py @@ -50,4 +50,5 @@ class ReferralRecordRewardSerializer(serializers.ModelSerializer): "coins", "unique_token", "value", + "created_on", ] diff --git a/manage_referrals/api/urls.py b/manage_referrals/api/urls.py index 6d2f4c0..569815c 100644 --- a/manage_referrals/api/urls.py +++ b/manage_referrals/api/urls.py @@ -24,4 +24,9 @@ urlpatterns = [ views.RedeemRewardView.as_view(), name="redeem_reward", ), + path( + "redeem-selected-rewards/", + views.RedeemSelectedRewardsView.as_view(), + name="redeem_selected_rewards", + ), ] diff --git a/manage_referrals/api/views.py b/manage_referrals/api/views.py index c748556..f131bea 100644 --- a/manage_referrals/api/views.py +++ b/manage_referrals/api/views.py @@ -62,7 +62,9 @@ class RewardListView(APIView): def get(self, request): # Filter rewards based on specified conditions - current_principal = request.user # Adjust based on how user is linked to principal + current_principal = ( + request.user + ) # Adjust based on how user is linked to principal # Filter rewards based on the authenticated referrer rewards_query = ReferralRecordReward.objects.filter( @@ -126,3 +128,68 @@ class RedeemRewardView(APIView): message=constants.SUCCESS, data="Token sold successfully.", ) + + +class RedeemSelectedRewardsView(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + def patch(self, request): + # Extract the number of tokens from the request data + num_tokens_to_sell = request.data.get("num_tokens", None) + + if num_tokens_to_sell is None: + return ApiResponse.error( + status=status.HTTP_400_BAD_REQUEST, + message=constants.FAILURE, + errors="Number of tokens to sell is required.", + ) + + try: + num_tokens_to_sell = int(num_tokens_to_sell) + except ValueError: + return ApiResponse.error( + status=status.HTTP_400_BAD_REQUEST, + message=constants.FAILURE, + errors="Invalid number of tokens.", + ) + + # Retrieve the rewards for the authenticated user + rewards = ReferralRecordReward.objects.filter( + referral_record__referrer_principal=request.user, sell=False + ).order_by( + "id" + ) # FIFO method + + if rewards.count() < num_tokens_to_sell: + return ApiResponse.error( + status=status.HTTP_404_NOT_FOUND, + message=constants.FAILURE, + errors="Not enough unsold rewards available.", + ) + + # Select the required number of rewards + rewards_to_sell = rewards[:num_tokens_to_sell] + total_value = sum(reward.value for reward in rewards_to_sell) + total_coins = sum(reward.coins for reward in rewards_to_sell) + tokens = ",".join(str(reward.unique_token) for reward in rewards_to_sell) + + with transaction.atomic(): + # Create a new withdrawal request + withdrawal_request = WithdrawalRequest.objects.create( + principal=request.user, + coins=total_coins, + amount=total_value, + token=tokens, + ) + + # Update each reward to mark it as sold + for reward in rewards_to_sell: + reward.sell = True + reward.save() + + return ApiResponse.success( + status=status.HTTP_200_OK, + message=constants.SUCCESS, + data="Selected tokens sold successfully.", + ) From c87cf3eea88b9ca2cbf50d4be40aebf1b7e49124 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 7 Jun 2024 20:40:45 +0530 Subject: [PATCH 049/187] selling selected rewards 2 --- templates/manage_wallets/withdrawal_list.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/manage_wallets/withdrawal_list.html b/templates/manage_wallets/withdrawal_list.html index f05e2ef..c57d8da 100644 --- a/templates/manage_wallets/withdrawal_list.html +++ b/templates/manage_wallets/withdrawal_list.html @@ -40,8 +40,8 @@ colspan="1" style="width: 69.2656px;"> Record Id Principal - + Qty Amount {{withdrawal_obj.id}} {{withdrawal_obj.principal}} - + {{withdrawal_obj.coins}} {{withdrawal_obj.amount}} {{withdrawal_obj.token}} {{withdrawal_obj.status}} From 8ee574de5238fbe97b1bf11ae18581bc02a6eab0 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 7 Jun 2024 20:45:55 +0530 Subject: [PATCH 050/187] text wrapping token field --- templates/manage_wallets/withdrawal_list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/manage_wallets/withdrawal_list.html b/templates/manage_wallets/withdrawal_list.html index c57d8da..cffbfe8 100644 --- a/templates/manage_wallets/withdrawal_list.html +++ b/templates/manage_wallets/withdrawal_list.html @@ -63,7 +63,7 @@ {{withdrawal_obj.principal}} {{withdrawal_obj.coins}} {{withdrawal_obj.amount}} - {{withdrawal_obj.token}} + {{withdrawal_obj.token}} {{withdrawal_obj.status}} {{withdrawal_obj.created_on}} + + {% include "cdn_through_html/flatpicker_cdn_css.html" %} +{% endblock %} + +{% block content %} + +
+
+ +
+
+
+
+ +
+ {% csrf_token %} + {% include 'includes/dynamic_template_form.html' with form=form %} +
+
+
+
+ +
+
+
+
+
+
+ + +{% endblock content %} + +{% block javascript %} + + {% include "cdn_through_html/flatpicker_cdn_js.html" %} + {% include "cdn_through_html/jquery_validate_cdn_js.html" %} + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/accounts/customer/customer_bulk_template.html b/templates/accounts/customer/customer_bulk_template.html new file mode 100644 index 0000000..4b45115 --- /dev/null +++ b/templates/accounts/customer/customer_bulk_template.html @@ -0,0 +1,80 @@ +{% extends 'layout/base_template.html' %} +{% load static %} +{% block stylesheet %} + +{% endblock %} + +{% block content %} + +
+
+ +
+
+
+
+ +
+ {% csrf_token %} + {% include 'includes/dynamic_template_form.html' with form=form %} +
+
+
+
+ +
+ {% if error_log %} +

Error Log:

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

Manage Customer

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

{{operation}} {{page_title}}

+ +

+ arrow_back + {{operation}} Event

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

{{operation}} {{page_title}}

-
- -
diff --git a/templates/manage_venues/venue_list.html b/templates/manage_venues/venue_list.html index 1305d9c..31e8a2b 100644 --- a/templates/manage_venues/venue_list.html +++ b/templates/manage_venues/venue_list.html @@ -39,6 +39,8 @@ Record Id + Customer Title {{data_obj.id}} + + {% if data_obj.principal %} + {{ data_obj.principal }} + {% elif data_obj.created_by %} + {{ data_obj.created_by }} + {% endif %} + {{data_obj.title}} {{data_obj.address}} {{data_obj.latitude}} From a5ce725b3006fc34b1b971fa1c508a8ef39e60c5 Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Tue, 2 Jul 2024 15:37:50 +0530 Subject: [PATCH 057/187] feat(module 1):multiple image input, form validation --- accounts/api/views.py | 10 + accounts/urls.py | 1 - accounts/views.py | 29 +- manage_events/api/serializers.py | 5 + manage_events/forms.py | 98 ++--- manage_events/urls.py | 5 + manage_events/views.py | 55 ++- templates/accounts/customer/customer_add.html | 46 +-- .../accounts/customer/customer_detail.html | 52 +-- .../accounts/customer/customer_edit.html | 69 ++-- .../accounts/customer/customer_list.html | 16 +- .../cdn_through_html/sweetalert2_cdn_css.html | 3 + .../cdn_through_html/sweetalert2_cdn_js.html | 3 + templates/manage_events/event_add.html | 376 ++++++++++++++---- templates/manage_venues/venue_add.html | 147 ++++--- 15 files changed, 623 insertions(+), 292 deletions(-) create mode 100644 templates/cdn_through_html/sweetalert2_cdn_css.html create mode 100644 templates/cdn_through_html/sweetalert2_cdn_js.html diff --git a/accounts/api/views.py b/accounts/api/views.py index 8aac098..b8ebb56 100644 --- a/accounts/api/views.py +++ b/accounts/api/views.py @@ -1000,6 +1000,16 @@ class AccountTransferCheckView(APIView): def post(self, request, *args, **kwargs): try: obj = IAmPrincipalExtendedData.objects.get(principal=request.user) + except IAmPrincipalExtendedData.DoesNotExist: + # Create a dummy serializer record + obj = { + 'principal': request.user.id, + 'is_onboarded': False, + 'is_transferred': False, + 'transferred_on': None, + 'pwd_changed_post_transfer': False + } + return ApiResponse.success(message=constants.SUCCESS, data=obj) except Exception as e: error_response = { "status": status.HTTP_404_NOT_FOUND, diff --git a/accounts/urls.py b/accounts/urls.py index c51db74..f8f0d1b 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -51,7 +51,6 @@ urlpatterns = [ path('customer/import-customer-data/', views.CustomerImportView.as_view(), name='import_customer_data'), path('customer/export-customer-data/', views.CustomerExportView.as_view(), name='export_customer_data'), - # ignore this to path this for example setup path('principal/example/', TemplateView.as_view(template_name="accounts/iam_module/example_form.html"), name="example_add"), path('datatable/', views.DatatableListView.as_view(), name="serverside_list"), diff --git a/accounts/views.py b/accounts/views.py index 9e450c0..cce5f54 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -754,7 +754,7 @@ class CustomerDetailView(LoginRequiredMixin, generic.DetailView): def get(self, request, *args, **kwargs): principal_obj = IAmPrincipal.objects.get(pk=kwargs.get("pk")) principal_preference = PrincipalPreference.objects.get(principal_id=principal_obj.id) - principal_subscription = PrincipalSubscription.objects.filter(principal=principal_obj).latest("start_date") + principal_subscription = PrincipalSubscription.objects.filter(principal=principal_obj).order_by("-start_date").first() return render(request, self.template_name, locals()) class CustomerListView(LoginRequiredMixin, generic.ListView): @@ -947,7 +947,7 @@ class CustomerImportView(LoginRequiredMixin, generic.View): form = self.form_class() context = self.get_context_data(form=form) return render(request, self.template_name, context=context) - + def post(self, request, *args, **kwargs): form = self.form_class(request.POST, request.FILES) context = self.get_context_data(form=form) @@ -996,11 +996,11 @@ class CustomerImportView(LoginRequiredMixin, generic.View): # collect the principals principal = IAmPrincipal( - first_name=first_name, - last_name=last_name, - email=email, + first_name=first_name.strip().capitalize(), + last_name=last_name.strip().capitalize(), + email=email.strip(), password=make_password("goodtimes#2024"), - username=email, + username=email.strip(), email_verified=True, register_complete=True, principal_type=principal_type @@ -1014,11 +1014,12 @@ class CustomerImportView(LoginRequiredMixin, generic.View): context = self.get_context_data(form=form, error_log=error_log) messages.error(request, "No recore is created check error log and fix the error in the file ") return render(request, self.template_name, context=context) - + # Use transaction.atomic to ensure all-or-nothing with transaction.atomic(): # Bulk create principals - IAmPrincipal.objects.bulk_create(principals) + for principal in principals: + principal.save() # Now we need to refresh principals from the DB to get their IDs principals = IAmPrincipal.objects.filter(email__in=[p.email for p in principals]) @@ -1054,7 +1055,7 @@ class CustomerImportView(LoginRequiredMixin, generic.View): messages.success(request, "Data imported successfully") return render(request, self.template_name, context=context) - + class CustomerExportView(LoginRequiredMixin, generic.View): model = IAmPrincipal @@ -1069,16 +1070,16 @@ class CustomerExportView(LoginRequiredMixin, generic.View): principal.email, principal.first_name, principal.last_name, - str(principal.phone_no), + str(principal.phone_no) if principal.phone_no else "N/A", principal.email_verified, principal.is_active, - principal.extended_data.is_onboarded if principal.extended_data else 'N/A', - principal.extended_data.is_transferred if principal.extended_data else 'N/A', - principal.created_on.replace(tzinfo=None) if principal.created_on else 'N/A' + # principal.extended_data.is_onboarded if principal.extended_data else 'N/A', + # principal.extended_data.is_transferred if principal.extended_data else 'N/A', + # principal.created_on.replace(tzinfo=None) if principal.created_on else 'N/A' ]) # Define the columns for the Excel file - columns = ["Email", "First Name", "Last Name", "Phone Number", "Email Verified", "Active", "Onboarde by Admin", "Transferred to Customer", "Created Date"] + columns = ["Email", "First Name", "Last Name", "Phone Number", "Email Verified", "Active"] # Create a workbook and select the active worksheet wb = Workbook() diff --git a/manage_events/api/serializers.py b/manage_events/api/serializers.py index 5bb1756..17daa07 100644 --- a/manage_events/api/serializers.py +++ b/manage_events/api/serializers.py @@ -38,6 +38,11 @@ class VenueSerializer(serializers.ModelSerializer): fields = "__all__" read_only_fields = ("created_by",) +class VenueShortSerializer(serializers.ModelSerializer): + class Meta: + model = Venue + fields= ["id","title"] + class EventCategorySerializer(serializers.ModelSerializer): class Meta: diff --git a/manage_events/forms.py b/manage_events/forms.py index 71c80a9..9b68fb5 100644 --- a/manage_events/forms.py +++ b/manage_events/forms.py @@ -1,6 +1,6 @@ from django import forms from accounts.models import IAmPrincipal, IAmPrincipalExtendedData -from manage_events.models import EventMaster, Event, EventCategory, Venue +from manage_events.models import EventImage, EventMaster, Event, EventCategory, Venue from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ @@ -17,6 +17,12 @@ class EventCategoryForm(forms.ModelForm): class EventForm(forms.ModelForm): + AGE_GROUP_CHOICES = [ + ('18 & Under', '18 & Under'), + ('18-30', '18-30'), + ('30+', '30+'), + ('Family Event', 'Family Event'), + ] principal = forms.ModelChoiceField( queryset=IAmPrincipal.objects.select_related("extended_data").filter( extended_data__is_onboarded=True, @@ -26,27 +32,30 @@ class EventForm(forms.ModelForm): required=True ) venue = forms.ModelChoiceField( - queryset=Venue.objects.filter( - active=True - ), + queryset=Venue.objects.none(), label="venue", required=True ) + image = forms.ImageField(label="Thumbnail") + event_images = forms.ImageField(label="Event Images") + age_group = forms.ChoiceField(choices=AGE_GROUP_CHOICES, label="Age Group", required=True) + class Meta: model = Event fields = [ "principal", + "venue", "title", # "event_master", "description", "image", - "status", + "event_images", + # "status", "start_date", "end_date", "from_time", "to_time", "category", - "venue", "venue_capacity", # "video_url", "entry_type", @@ -61,57 +70,61 @@ class EventForm(forms.ModelForm): "deleted", ] widgets = { - "title": forms.TextInput(attrs={"class": "form-control"}), - "description": forms.Textarea(attrs={"class": "form-control", "rows": 4}), - "status": forms.Select(attrs={"class": "form-control"}), + "description": forms.Textarea(attrs={"rows": 4}), "start_date": forms.DateInput( - attrs={"class": "form-control", "type": "date"} + attrs={"type": "date"} ), "end_date": forms.DateInput( - attrs={"class": "form-control", "type": "date"} + attrs={"type": "date"} ), "from_time": forms.TimeInput( - attrs={"class": "form-control", "type": "time"} + attrs={"type": "time"} ), - "to_time": forms.TimeInput(attrs={"class": "form-control", "type": "time"}), - "venue_capacity": forms.NumberInput(attrs={"class": "form-control"}), - "video_url": forms.URLInput(attrs={"class": "form-control"}), - "entry_type": forms.Select(attrs={"class": "form-control"}), - "entry_fee": forms.NumberInput(attrs={"class": "form-control"}), - "key_guest": forms.Textarea(attrs={"class": "form-control", "rows": 3}), - "age_group": forms.TextInput(attrs={"class": "form-control"}), - "draft": forms.CheckboxInput(attrs={"class": "form-check-input"}), - # For the 'image' field, you might not need to specify a widget since the default is appropriate. - # However, if you want to add specific classes or attributes, you can do it like this: - "image": forms.FileInput(attrs={"class": "form-control-file"}), - # For ForeignKey fields like 'EventMaster' and 'venue', Django uses a select widget by default. - # You can customize it further if needed: - # "event_master": forms.Select(attrs={"class": "form-control"}), - "venue": forms.Select(attrs={"class": "form-control"}), - "category": forms.Select(attrs={"class": "form-control"}), + "to_time": forms.TimeInput(attrs={"type": "time"}), + "key_guest": forms.Textarea(attrs={"rows": 3}), + "coupon_description": forms.Textarea(attrs={"rows": 3}), } + def __init__(self, *args, **kwargs): + + instance = kwargs.get('instance') + principal_id = kwargs.pop('principal_id', None) + super().__init__(*args, **kwargs) + + if instance: + event_images = EventImage.objects.filter(event=instance) + if event_images.exists(): + self.fields['event_images'].initial = [image.image.url for image in event_images] + + if principal_id: + self.fields['venue'].queryset = Venue.objects.filter(principal_id=principal_id, active=True) + else: + self.fields['venue'].queryset = Venue.objects.none() + def clean(self): cleaned_data = super().clean() # Get the start and end dates from cleaned_data start_date = cleaned_data.get("start_date") end_date = cleaned_data.get("end_date") - - # Validation 1: end_date should not be less than start_date - if end_date and start_date and end_date < start_date: - self.add_error("end_date", _("End date cannot be before the start date.")) - # Get the from and to times from cleaned_data from_time = cleaned_data.get("from_time") to_time = cleaned_data.get("to_time") - # Validation 2: to_time should not be less than or equal to from_time - if to_time and from_time and to_time <= from_time: - self.add_error("to_time", _("End time must be after the start time.")) + # Validation 1: end_date should not be less than start_date + if start_date and end_date and end_date < start_date: + self.add_error("end_date", _("End date cannot be before the start date.")) + + if end_date == start_date: + if to_time and from_time and to_time <= from_time: + self.add_error("to_time", _("End time must be after the start time.")) return cleaned_data +class EventImageForm(forms.ModelForm): + class Meta: + model = EventImage + fields = ['image'] class EventMasterForm(forms.ModelForm): class Meta: @@ -134,19 +147,14 @@ class VenueForm(forms.ModelForm): fields = [ "principal", "title", - "description", + # "description", "address", "image", - "url", + # "url", "latitude", "longitude", ] widgets = { - "title": forms.TextInput(attrs={"class": "form-control"}), - "description": forms.Textarea(attrs={"class": "form-control", "rows": 4}), - "address": forms.Textarea(attrs={"class": "form-control", "rows": 3}), - "image": forms.FileInput(attrs={"class": "form-control"}), - "url": forms.URLInput(attrs={"class": "form-control"}), - "latitude": forms.NumberInput(attrs={"class": "form-control"}), - "longitude": forms.NumberInput(attrs={"class": "form-control"}), + "description": forms.Textarea(attrs={"rows": 4}), + "address": forms.Textarea(attrs={"rows": 3}), } diff --git a/manage_events/urls.py b/manage_events/urls.py index 6ce1536..bcc3182 100644 --- a/manage_events/urls.py +++ b/manage_events/urls.py @@ -89,6 +89,11 @@ urlpatterns = [ views.VenueDeleteView.as_view(), name="venue_delete", ), + path( + "venue/customer/", + views.CustomerVenueFilterView.as_view(), + name="venue_customer_filter", + ), path( "generate-event-report//", views.GenerateEventReportView.as_view(), diff --git a/manage_events/views.py b/manage_events/views.py index 56b42a5..85e9f28 100644 --- a/manage_events/views.py +++ b/manage_events/views.py @@ -1,5 +1,7 @@ from django.shortcuts import get_object_or_404, redirect, render from accounts import resource_action +from goodtimes.utils import JsonResponseUtil +from manage_events.api.serializers import VenueSerializer, VenueShortSerializer from manage_events.forms import ( EventMasterForm, EventCategoryForm, @@ -7,7 +9,7 @@ from manage_events.forms import ( VenueForm, ) from django.core.paginator import Paginator -from .models import EventMaster, Event, EventCategory, EventPrincipalInteraction, Venue +from .models import EventImage, EventMaster, Event, EventCategory, EventPrincipalInteraction, Venue from django.views import generic from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse_lazy @@ -222,7 +224,7 @@ class EventMasterDeleteView(LoginRequiredMixin, generic.View): return redirect(self.success_url) - +from django.core.files.storage import default_storage class EventCreateOrUpdateView(LoginRequiredMixin, generic.View): # Set the page_name and resource page_name = resource_action.RESOURCE_MANAGE_EVENTS @@ -257,6 +259,9 @@ class EventCreateOrUpdateView(LoginRequiredMixin, generic.View): } context.update(kwargs) # Include any additional context data passed to the view return context + + def get_event_images(self): + return [image.image.url for image in EventImage.objects.filter(event=self.object)] def get(self, request, *args, **kwargs): self.object = self.get_object() @@ -266,25 +271,46 @@ class EventCreateOrUpdateView(LoginRequiredMixin, generic.View): self.action = resource_action.ACTION_UPDATE form = self.form_class(instance=self.object) - context = self.get_context_data(form=form) + + context = self.get_context_data(form=form, event_images_urls=self.get_event_images()) return render(request, self.template_name, context=context) def post(self, request, *args, **kwargs): + print(request.POST) + print(request.FILES) self.object = self.get_object() # If an object is found, change action to ACTION_UPDATE if self.object is not None: self.action = resource_action.ACTION_UPDATE - form = self.form_class(request.POST, request.FILES, instance=self.object) + principal_id = request.POST.get('principal') + + form = self.form_class(request.POST, request.FILES, instance=self.object, principal_id=principal_id) if not form.is_valid(): - print(form.errors) - context = self.get_context_data(form=form) + print(f"form error is {form.errors}") + context = self.get_context_data(form=form, event_images_urls=self.get_event_images()) return render(request, self.template_name, context=context) instance = form.save() instance.created_by = form.cleaned_data.get("principal") instance.save() + + # Delete old images from storage + old_images = EventImage.objects.filter(event=instance) + for old_image in old_images: + if default_storage.exists(old_image.image.name): + default_storage.delete(old_image.image.name) + + # Delete old images from database + old_images.delete() + + event_images = request.FILES.getlist("event_images") + event_image_objects = [EventImage(event=instance, image=image) for image in event_images] + + EventImage.objects.bulk_create(event_image_objects) + messages.success(self.request, self.get_success_message()) + return redirect(self.success_url) @@ -487,6 +513,23 @@ class VenueDeleteView(LoginRequiredMixin, generic.View): return redirect(self.success_url) +class CustomerVenueFilterView(LoginRequiredMixin, generic.View): + model = Venue + serializer_class = VenueShortSerializer + + def get(self, request, *args, **kwargs): + pk = request.GET.get("pk", None) + if not pk: + return JsonResponseUtil.error(message="Non transfer user list field is required") + obj = self.model.objects.filter(principal=pk) + if not obj.exists(): + return JsonResponseUtil.error(message="No venue found for the given user.") + + serializer = self.serializer_class(obj, many=True) + + return JsonResponseUtil.success(message=constants.SUCCESS, data=serializer.data) + + User = get_user_model() from .report import generate_event_report, generate_event_report_pdf_three from django.http import HttpResponse diff --git a/templates/accounts/customer/customer_add.html b/templates/accounts/customer/customer_add.html index 77bd818..723c9fb 100644 --- a/templates/accounts/customer/customer_add.html +++ b/templates/accounts/customer/customer_add.html @@ -57,7 +57,7 @@ // }); var start_date = flatpickr(document.getElementById('id_free_start_date'), { - minDate: "today", + // minDate: "today", onChange: function(selectedDates, dateStr, instance) { end_date.set('minDate', selectedDates[0]); } @@ -113,30 +113,30 @@ email: { required: true, validEmail: true, - remote: { - url: "{% url 'accounts:customer_check_email' %}", // Replace with your actual URL for the view - type: "POST", - data: { - email: function() { - return $("#id_email").val(); - } - }, - beforeSend: function(xhr) { - xhr.setRequestHeader('X-CSRFToken', $('input[name="csrfmiddlewaretoken"]').val()); - }, - success: function(data) { - console.log(date) - // Handle successful email check (optional) - // You can remove this if you only need to display the error message - }, - error: function(response) { - console.log(response) - } - }, + // remote: { + // url: "{% url 'accounts:customer_check_email' %}", // Replace with your actual URL for the view + // type: "POST", + // data: { + // email: function() { + // return $("#id_email").val(); + // } + // }, + // beforeSend: function(xhr) { + // xhr.setRequestHeader('X-CSRFToken', $('input[name="csrfmiddlewaretoken"]').val()); + // }, + // success: function(data) { + // console.log(date) + // // Handle successful email check (optional) + // // You can remove this if you only need to display the error message + // }, + // error: function(response) { + // console.log(response) + // } + // }, }, preferences: { required: true, - minlength: 3 + minlength: 1 }, free_start_date: { required: true, @@ -183,7 +183,7 @@ var startDate = $("#id_free_start_date").datepicker("getDate"); // Assuming you're using datepicker var endDate = $(element).datepicker("getDate"); if (!endDate || !startDate) { - return true; // Allow if either date is not selected (prevents errors) + return true; // Allow if either date is not selected (prevents errors) } return endDate > startDate; } diff --git a/templates/accounts/customer/customer_detail.html b/templates/accounts/customer/customer_detail.html index 9d48e5c..438b117 100644 --- a/templates/accounts/customer/customer_detail.html +++ b/templates/accounts/customer/customer_detail.html @@ -30,28 +30,17 @@
{{principal_obj.first_name}}
-
- -
- +
+
Last Name
-
{{principal_obj.last_name}}
- -
- -
- +
+
Email Address
-
{{principal_obj.email}}
- -
- -
- +
+
Preferences
-
{% for category in principal_preference.preferred_categories.all %} @@ -62,31 +51,24 @@ No preferred categories. {% endfor %} +
- -
- -
- -
Start Date
- -
{{principal_subscription.start_date}}
- -
-
- +
+
Start Date
+
{% if principal_subscription %}{{ principal_subscription.start_date }}{% else %}No subscription found{% endif %}
+
+
End Date
- -
{{principal_subscription.end_date}}
- -
- {% if not principal_obj.extended_data.is_transferred %} +
{% if principal_subscription %}{{ principal_subscription.end_date }}{% else %}No subscription found{% endif %}
+
+ {% if principal_obj.extended_data and not principal_obj.extended_data.transferred and principal_obj.extended_data.onboarded and principal_obj.principal_type.name == 'event_manager' %} {% endif %} +
diff --git a/templates/accounts/customer/customer_edit.html b/templates/accounts/customer/customer_edit.html index 8cebe76..d5b3009 100644 --- a/templates/accounts/customer/customer_edit.html +++ b/templates/accounts/customer/customer_edit.html @@ -19,13 +19,14 @@
- {% if not principal_obj.extended_data.is_transferred %} + {% if principal_obj.extended_data and not principal_obj.extended_data.transferred and principal_obj.extended_data.onboarded and principal_obj.principal_type.name == 'event_manager' %} {% endif %} +
@@ -65,7 +66,7 @@ // }); var start_date = flatpickr(document.getElementById('id_free_start_date'), { - minDate: "today", + // minDate: "today", onChange: function(selectedDates, dateStr, instance) { end_date.set('minDate', selectedDates[0]); } @@ -121,30 +122,30 @@ email: { required: true, validEmail: true, - remote: { - url: "{% url 'accounts:customer_check_email' %}", // Replace with your actual URL for the view - type: "POST", - data: { - email: function() { - return $("#id_email").val(); - } - }, - beforeSend: function(xhr) { - xhr.setRequestHeader('X-CSRFToken', $('input[name="csrfmiddlewaretoken"]').val()); - }, - success: function(data) { - console.log(date) - // Handle successful email check (optional) - // You can remove this if you only need to display the error message - }, - error: function(response) { - console.log(response) - } - }, + // remote: { + // url: "{% url 'accounts:customer_check_email' %}", // Replace with your actual URL for the view + // type: "POST", + // data: { + // email: function() { + // return $("#id_email").val(); + // } + // }, + // beforeSend: function(xhr) { + // xhr.setRequestHeader('X-CSRFToken', $('input[name="csrfmiddlewaretoken"]').val()); + // }, + // success: function(data) { + // console.log(date) + // // Handle successful email check (optional) + // // You can remove this if you only need to display the error message + // }, + // error: function(response) { + // console.log(response) + // } + // }, }, preferences: { required: true, - minlength: 3 + minlength: 1 }, free_start_date: { required: true, @@ -186,16 +187,16 @@ greaterThanStartDate: "The end date must be after the start date." } }, - customMethods: { - greaterThanStartDate: function(element) { - var startDate = $("#id_free_start_date").datepicker("getDate"); // Assuming you're using datepicker - var endDate = $(element).datepicker("getDate"); - if (!endDate || !startDate) { - return true; // Allow if either date is not selected (prevents errors) - } - return endDate > startDate; - } - }, + // customMethods: { + // greaterThanStartDate: function(element) { + // var startDate = $("#id_free_start_date").datepicker("getDate"); // Assuming you're using datepicker + // var endDate = $(element).datepicker("getDate"); + // if (!endDate || !startDate) { + // return true; // Allow if either date is not selected (prevents errors) + // } + // return endDate > startDate; + // } + // }, errorElement: 'div', errorPlacement: function(error, element) { error.addClass('invalid-feedback'); diff --git a/templates/accounts/customer/customer_list.html b/templates/accounts/customer/customer_list.html index 4056566..ad1825f 100644 --- a/templates/accounts/customer/customer_list.html +++ b/templates/accounts/customer/customer_list.html @@ -98,13 +98,21 @@ {{ data_obj.email_verified }} {{ data_obj.referral_count }} - - {{ data_obj.extended_data.is_onboarded }} + + {% if data_obj.extended_data %} + {{ data_obj.extended_data.is_onboarded }} + {% else %} + False + {% endif %} - - {{ data_obj.extended_data.is_transferred }} + + {% if data_obj.extended_data %} + {{ data_obj.extended_data.is_transferred }} + {% else %} + False + {% endif %} {{ data_obj.created_on }} diff --git a/templates/cdn_through_html/sweetalert2_cdn_css.html b/templates/cdn_through_html/sweetalert2_cdn_css.html new file mode 100644 index 0000000..01bab28 --- /dev/null +++ b/templates/cdn_through_html/sweetalert2_cdn_css.html @@ -0,0 +1,3 @@ +{% load static%} + + diff --git a/templates/cdn_through_html/sweetalert2_cdn_js.html b/templates/cdn_through_html/sweetalert2_cdn_js.html new file mode 100644 index 0000000..00a21b9 --- /dev/null +++ b/templates/cdn_through_html/sweetalert2_cdn_js.html @@ -0,0 +1,3 @@ +{% load static%} + + \ No newline at end of file diff --git a/templates/manage_events/event_add.html b/templates/manage_events/event_add.html index 66abf4b..2a598cb 100644 --- a/templates/manage_events/event_add.html +++ b/templates/manage_events/event_add.html @@ -4,8 +4,10 @@ {% include "cdn_through_html/filepond_cdn_css.html" %} + {% include "cdn_through_html/flatpicker_cdn_css.html" %} {% include "cdn_through_html/quill_cdn_css.html" %} {% include "cdn_through_html/tagify_cdn_css.html" %} + {% include "cdn_through_html/sweetalert2_cdn_css.html" %} {{form.media}} {% endblock %} @@ -28,32 +30,13 @@
-
+ {% csrf_token %} {% include 'includes/dynamic_template_form.html' with form=form %} +
-
+
- {% comment %}
- - -
We'll never share your email with anyone else.
-
-
- -
-
-
- -
-
    -
    -
    -
    - - -
    - {% endcomment %}
    @@ -70,64 +53,305 @@ {% include "cdn_through_html/filepond_cdn_js.html" %} - {% include "cdn_through_html/quill_cdn_js.html" %} + {% include "cdn_through_html/flatpicker_cdn_js.html" %} {% include "cdn_through_html/tagify_cdn_js.html" %} + {% include "cdn_through_html/sweetalert2_cdn_js.html" %} + {% include "cdn_through_html/jquery_validate_cdn_js.html" %} + {% endblock %} \ No newline at end of file diff --git a/templates/manage_venues/venue_add.html b/templates/manage_venues/venue_add.html index c7e1081..deb9b5c 100644 --- a/templates/manage_venues/venue_add.html +++ b/templates/manage_venues/venue_add.html @@ -4,8 +4,6 @@ {% include "cdn_through_html/filepond_cdn_css.html" %} -{% include "cdn_through_html/quill_cdn_css.html" %} -{% include "cdn_through_html/tagify_cdn_css.html" %} {{form.media}} {% endblock %} @@ -28,7 +26,7 @@
    -
    + {% csrf_token %} {% include 'includes/dynamic_template_form.html' with form=form %}
    @@ -56,64 +54,105 @@ {% include "cdn_through_html/filepond_cdn_js.html" %} -{% include "cdn_through_html/quill_cdn_js.html" %} -{% include "cdn_through_html/tagify_cdn_js.html" %} + +{% include "cdn_through_html/jquery_validate_cdn_js.html" %} {% endblock %} \ No newline at end of file From 04e428fcf6cd5b16a4388612d349bd934e124b18 Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Tue, 2 Jul 2024 18:54:18 +0530 Subject: [PATCH 058/187] fix(module_1):event form validation, venue validation --- manage_events/forms.py | 9 ++++++-- manage_subscriptions/views.py | 2 +- .../accounts/customer/customer_detail.html | 14 ++++++------- .../accounts/customer/customer_edit.html | 2 +- .../accounts/customer/customer_list.html | 2 +- templates/manage_events/event_add.html | 21 ++----------------- .../subscription_list.html | 10 +++++++-- templates/manage_venues/venue_add.html | 19 +++++++++++------ 8 files changed, 40 insertions(+), 39 deletions(-) diff --git a/manage_events/forms.py b/manage_events/forms.py index 9b68fb5..dc5777d 100644 --- a/manage_events/forms.py +++ b/manage_events/forms.py @@ -141,16 +141,21 @@ class VenueForm(forms.ModelForm): label="Non-transfer user list", required=True ) + image = forms.ImageField(required=True) + latitude = forms.DecimalField( + widget=forms.NumberInput() + ) + longitude = forms.DecimalField( + widget=forms.NumberInput() + ) class Meta: model = Venue fields = [ "principal", "title", - # "description", "address", "image", - # "url", "latitude", "longitude", ] diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 0050df9..bf7dda6 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -100,7 +100,7 @@ class SubscriptionCreateOrUpdateView(LoginRequiredMixin, generic.View): # This code ensures that only one free plan can be created by checking for existing free plans before saving a new one. if form.cleaned_data.get("is_free"): - if self.model.objects.filter(Q(is_free=True) & Q(active=True)).exists: + if self.model.objects.filter(Q(is_free=True) & Q(active=True)).exists(): messages.error(self.request, "A free plan is already available. Please deactivate the existing one before creating a new one.") context = self.get_context_data(form=form) return render(request, self.template_name, context=context) diff --git a/templates/accounts/customer/customer_detail.html b/templates/accounts/customer/customer_detail.html index 438b117..1cec652 100644 --- a/templates/accounts/customer/customer_detail.html +++ b/templates/accounts/customer/customer_detail.html @@ -61,13 +61,13 @@
    End Date
    {% if principal_subscription %}{{ principal_subscription.end_date }}{% else %}No subscription found{% endif %}
    - {% if principal_obj.extended_data and not principal_obj.extended_data.transferred and principal_obj.extended_data.onboarded and principal_obj.principal_type.name == 'event_manager' %} - - {% endif %} + {% if principal_obj.extended_data and not principal_obj.extended_data.is_transferred and principal_obj.extended_data.is_onboarded and principal_obj.principal_type.name == 'event_manager' %} + + {% endif %}
    diff --git a/templates/accounts/customer/customer_edit.html b/templates/accounts/customer/customer_edit.html index d5b3009..57c6774 100644 --- a/templates/accounts/customer/customer_edit.html +++ b/templates/accounts/customer/customer_edit.html @@ -19,7 +19,7 @@
    - {% if principal_obj.extended_data and not principal_obj.extended_data.transferred and principal_obj.extended_data.onboarded and principal_obj.principal_type.name == 'event_manager' %} + {% if principal_obj.extended_data and not principal_obj.extended_data.is_transferred and principal_obj.extended_data.is_onboarded and principal_obj.principal_type.name == 'event_manager' %}
    Transfer Account diff --git a/templates/accounts/customer/customer_list.html b/templates/accounts/customer/customer_list.html index ad1825f..caf4463 100644 --- a/templates/accounts/customer/customer_list.html +++ b/templates/accounts/customer/customer_list.html @@ -21,7 +21,7 @@ - import + Import diff --git a/templates/manage_events/event_add.html b/templates/manage_events/event_add.html index 2a598cb..60c74c1 100644 --- a/templates/manage_events/event_add.html +++ b/templates/manage_events/event_add.html @@ -5,7 +5,6 @@ {% include "cdn_through_html/filepond_cdn_css.html" %} {% include "cdn_through_html/flatpicker_cdn_css.html" %} - {% include "cdn_through_html/quill_cdn_css.html" %} {% include "cdn_through_html/tagify_cdn_css.html" %} {% include "cdn_through_html/sweetalert2_cdn_css.html" %} {{form.media}} @@ -60,20 +59,7 @@ diff --git a/templates/accounts/customer/customer_edit.html b/templates/accounts/customer/customer_edit.html index 57c6774..7a45364 100644 --- a/templates/accounts/customer/customer_edit.html +++ b/templates/accounts/customer/customer_edit.html @@ -61,10 +61,6 @@ diff --git a/templates/manage_events/event_add.html b/templates/manage_events/event_add.html index 6321a39..950a41e 100644 --- a/templates/manage_events/event_add.html +++ b/templates/manage_events/event_add.html @@ -70,6 +70,7 @@ ); return FilePond.create(document.getElementById(selector),{ allowMultiple: allowMultiple, + acceptedFileTypes: ['image/*'], storeAsFile: true, dropOnPage: true }); @@ -198,6 +199,29 @@ // Handle principal change handlePrincipalChange(); + // 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."); + + $.validator.addMethod("greaterThanFromTime", function(value, element){ + var startDateVal = $("#id_start_date").val(); + var endDateVal = $("#id_end_date").val(); + var fromTime = $("#id_from_time").val(); + var toTime = $(element).val(); + if (!toTime || !fromTime) { + return true; // Allow if either time is not selected (prevents errors) + } + if (startDateVal !== endDateVal) { + return true + } + return toTime > fromTime; + },"End time must be greater than start time on the same day") + // Initialize jQuery Validate $("#eventForm").validate({ rules: { @@ -215,25 +239,23 @@ }, image: { required: true, - accept: "image/*" }, event_images: { required: true, - accept: "image/*" }, start_date: { required: true, - date: true }, end_date: { required: true, - date: true + greaterThanStartDate: true }, from_time: { required: true }, to_time: { - required: true + required: true, + greaterThanFromTime: true }, category:{ required: true @@ -267,19 +289,15 @@ }, image: { required: "Please upload a thumbnail", - accept: "Please upload a valid image file" }, event_images: { required: "Please upload event images", - accept: "Please upload valid image files" }, start_date: { required: "Please select a start date", - date: "Please enter a valid date" }, end_date: { required: "Please select an end date", - date: "Please enter a valid date", greaterThanStartDate: "The end date must be after or equal to the start date." }, from_time: { @@ -302,30 +320,7 @@ required: "Please select a age group" }, tags: { - required: "Please enter tags" - } - }, - customMethods: { - greaterThanStartDate: function(element) { - var startDate = $("#id_start_date").datepicker("getDate"); // Assuming you're using datepicker - var endDate = $(element).datepicker("getDate"); - if (!endDate || !startDate) { - return true; // Allow if either date is not selected (prevents errors) - } - return endDate >= startDate; - }, - greaterThanFromTime: function(element){ - var startDateVal = $("#id_start_date").val(); - var endDateVal = $("#id_end_date").val(); - var fromTime = $("#id_from_time").val(); - var toTime = $(element).val(); - if (!toTime || !fromTime) { - return true; // Allow if either time is not selected (prevents errors) - } - if (startDateVal !== endDateVal) { - return true - } - return toTime > fromTime; + required: "Please enter tags" } }, errorElement: "div", @@ -344,7 +339,6 @@ $(element).addClass("is-valid").removeClass("is-invalid"); }, submitHandler: function(form) { - // Disable the submit button to prevent multiple submissions $('button[type="submit"]').prop('disabled', true); form.submit(); diff --git a/templates/manage_subscriptions/subscription_list.html b/templates/manage_subscriptions/subscription_list.html index 6e6896d..6a5a8b7 100644 --- a/templates/manage_subscriptions/subscription_list.html +++ b/templates/manage_subscriptions/subscription_list.html @@ -50,9 +50,9 @@ Amount - {% comment %} Customer Type {% endcomment %} + style="width: 69.2656px;"> Customer Type Free for Admin @@ -70,7 +70,15 @@ {{data_obj.title}} {{data_obj.plan.days}} {{data_obj.amount}} - {% comment %} {{data_obj.principal_types.name}} {% endcomment %} + + {% if data_obj.principal_types.all %} + {% for data in data_obj.principal_types.all %} + {{ data.name }} + {% endfor %} + {% else %} + No user type + {% endif %} + {{data_obj.is_free}} diff --git a/templates/manage_venues/venue_add.html b/templates/manage_venues/venue_add.html index 922cafe..d6c9c03 100644 --- a/templates/manage_venues/venue_add.html +++ b/templates/manage_venues/venue_add.html @@ -67,13 +67,29 @@ {% endblock %} \ No newline at end of file From 1ebf7f68385e7a86b5b7b91ada2193d0e09227d1 Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Wed, 10 Jul 2024 00:42:38 +0530 Subject: [PATCH 061/187] feat(module_2_filter):Home page advance filter api --- goodtimes/services.py | 141 ++++++++++++++++++++++++++++++- goodtimes/settings/base.py | 1 + manage_events/api/filters.py | 34 ++++++++ manage_events/api/serializers.py | 10 +-- manage_events/api/urls.py | 11 +++ manage_events/api/views.py | 92 ++++++++++++++++++-- 6 files changed, 277 insertions(+), 12 deletions(-) create mode 100644 manage_events/api/filters.py diff --git a/goodtimes/services.py b/goodtimes/services.py index cc22c0e..e2faeb2 100644 --- a/goodtimes/services.py +++ b/goodtimes/services.py @@ -1,4 +1,5 @@ import random +import googlemaps from django.conf import settings from django.core.files.uploadedfile import UploadedFile from django.core.mail import EmailMessage @@ -6,7 +7,9 @@ from django.utils.html import strip_tags import math from django.template.loader import render_to_string from django.shortcuts import get_object_or_404 +from django.db.models import Case, When from smtplib import SMTPException + from accounts.models import IAmPrincipal, IAmPrincipalOtp, IAmPrincipalType from manage_referrals.models import ( GoodTimeCoins, @@ -495,7 +498,9 @@ class EventFilterService: events = events.order_by("entry_fee") elif filter_type == "preference" and principal is not None: preferences = PrincipalPreference.objects.get(principal=principal) - preferred_categories_ids = preferences.preferred_categories.values_list("id", flat=True) + preferred_categories_ids = preferences.preferred_categories.values_list( + "id", flat=True + ) events = events.filter(category__in=preferred_categories_ids) return events.distinct() @@ -508,7 +513,9 @@ class EventFilterService: if 1 <= category_id <= 10: events = events.filter(category_id=category_id) else: - events = Event.objects.none() # Return an empty queryset if the category_id is not valid + events = ( + Event.objects.none() + ) # Return an empty queryset if the category_id is not valid return events.distinct() @@ -680,3 +687,133 @@ class MyEventFilterService: ) return events + + +class GoogleMapsservice: + def __init__(self, api_key=None): + self.api_key = api_key or settings.GOOGLE_MAPS_API_KEY + self.client = googlemaps.Client(key=self.api_key) + + def get_distance_matrix(self, origin: list, destination: list): + """ + Get the distance matrix from Google Maps API for the given origins and destinations. + + Args: + origins (list): List of origin coordinates (latitude, longitude). + destinations (list): List of destination coordinates (latitude, longitude). + + Returns: + dict: Distance matrix response from Google Maps API. + """ + return self.client.distance_matrix(origin, destination) + + def search_address(self, address): + """ + Search for a list of addresses matching the given address string. + + :param address: Address string to search for + :return: List of matching addresses + """ + geocode_result = self.client.geocode(address) + return geocode_result + + def get_coordinates_from_address(self, address): + """ + Get the coordinates (latitude and longitude) of the given address. + + :param address: Address string to get coordinates for + :return: Coordinates as a tuple (latitude, longitude) + """ + geocode_result = self.client.geocode(address) + if geocode_result: + location = geocode_result[0]['geometry']['location'] + return location['lat'], location['lng'] + return None + + def get_place_id_from_address(self, address): + """ + Get the place ID of the given address. + + :param address: Address string to get the place ID for + :return: Place ID + """ + geocode_result = self.client.geocode(address) + if geocode_result: + return geocode_result[0]['place_id'] + return None + + def get_place_id_from_coordinates(self, latitude, longitude): + """ + Get the place ID of the given coordinates. + + :param latitude: Latitude of the location + :param longitude: Longitude of the location + :return: Place ID + """ + reverse_geocode_result = self.client.reverse_geocode((latitude, longitude)) + if reverse_geocode_result: + return reverse_geocode_result[0]['place_id'] + return None + + def search_addresses_containing(self, keyword): + """ + Search for a list of addresses containing the given keyword. + + :param keyword: Keyword to search for in addresses + :return: List of matching addresses containing the keyword + """ + geocode_result = self.client.geocode(keyword) + matching_addresses = [result['formatted_address'] for result in geocode_result if keyword.lower() in result['formatted_address'].lower()] + return matching_addresses + + def get_nearest_events(self, queryset, latitude, longitude, radius_km=10): + """ + Filter and sort events by their distance to the given latitude and longitude. + + Args: + queryset (QuerySet): The queryset of events to filter and sort. + latitude (float): The latitude of the origin point. + longitude (float): The longitude of the origin point. + radius_km (int): The radius in kilometers within which to filter events. + + Returns: + QuerySet: The filtered and sorted queryset of events. + """ + + # Set the origin to the provided latitude and longitude + origins = [(latitude, longitude)] + + # Create a list of destination coordinates for all events with valid venues + destinations = [ + (event.venue.latitude, event.venue.longitude) + for event in queryset + if event.venue.latitude and event.venue.longitude + ] + + # If there is no destination coordinates + if not destinations: + return queryset + + # Get the distance matrix from the Google Maps API + matrix = self.get_distance_matrix(origins, destinations) + + # Create a dictionary of event IDs and their corresponding distances + distances = { + event.id: element["distance"]["value"] + for event, element in zip(queryset, matrix["rows"][0]["elements"]) + if element["status"] == "OK" and element["distance"]["value"] <= radius_km * 1000 # Convert km to meters + } + + # Filter the queryset to include only events within the specified radius + queryset = queryset.filter(id__in=distances.keys()) + + # # Sort the event IDs by their distances in ascending order + # event_ids_by_distance = sorted(distances, key=distances.get) + + # # Create a Case/When expression to preserve the order of events by distance + # preserved_order = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(event_ids_by_distance)]) + # print(f"preserved_order is {preserved_order}") + # # Order the queryset based on the preserved order + # queryset = queryset.order_by(preserved_order) + + return queryset diff --git a/goodtimes/settings/base.py b/goodtimes/settings/base.py index 7a4febd..5476155 100644 --- a/goodtimes/settings/base.py +++ b/goodtimes/settings/base.py @@ -81,6 +81,7 @@ THIRD_PARTY_APPS = [ "allauth.socialaccount", "allauth.socialaccount.providers.apple", "allauth.socialaccount.providers.google", + "django_filters", # "django_crontab", # "django_celery_results", # "django_celery_beat", diff --git a/manage_events/api/filters.py b/manage_events/api/filters.py new file mode 100644 index 0000000..aeb164c --- /dev/null +++ b/manage_events/api/filters.py @@ -0,0 +1,34 @@ +from django_filters import rest_framework as filters +from django.db.models import Count, Q +from ..models import Event, EventInteractionType + + +class EventFilter(filters.FilterSet): + """ + FilterSet for Event model. + """ + title = filters.CharFilter(field_name="title", lookup_expr="icontains") + 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") + end_date = filters.DateFilter(field_name="end_date", lookup_expr="lte") + price_from = filters.DateFilter(field_name="entry_fee", lookup_expr="gte") + price_to = filters.DateFilter(field_name="entry_fee", lookup_expr="lte") + age_group = filters.CharFilter(field_name="age_group", lookup_expr="icontains") + + class Meta: + model = Event + fields = [ + 'title', + 'location', + 'category', + 'start_date', + 'end_date', + 'price_from', + 'price_to', + 'age_group', + ] + + def filter_category(self, queryset, name, value): + category = value.split(',') + return queryset.filter(category__title__in=category) \ No newline at end of file diff --git a/manage_events/api/serializers.py b/manage_events/api/serializers.py index 9dd9c25..b198381 100644 --- a/manage_events/api/serializers.py +++ b/manage_events/api/serializers.py @@ -51,7 +51,7 @@ class EventCategorySerializer(serializers.ModelSerializer): class EventListSerializer(serializers.ModelSerializer): - # category = EventCategorySerializer(read_only=True) + category = EventCategorySerializer(read_only=True) # venue = VenueSerializer(read_only=True) # draft = serializers.BooleanField(read_only=True) # tags = TagSerializer(many=True, read_only=True) @@ -64,9 +64,9 @@ class EventListSerializer(serializers.ModelSerializer): "description", "start_date", "end_date", - # "from_time", - # "to_time", - # "category", + "from_time", + "to_time", + "category", # "venue", # "venue_capacity", "image", @@ -74,7 +74,7 @@ class EventListSerializer(serializers.ModelSerializer): # "entry_type", "entry_fee", "key_guest", - # "age_group", + "age_group", # "images", # "is_favorited", # "reviews", diff --git a/manage_events/api/urls.py b/manage_events/api/urls.py index 5b63eb7..1f657d3 100644 --- a/manage_events/api/urls.py +++ b/manage_events/api/urls.py @@ -56,6 +56,11 @@ urlpatterns = [ views.PrincipalPreferenceDetailView.as_view(), name="principal-preferences", ), + path( + "preferences/", + views.EventPreferencesView.as_view(), + name="preferences", + ), # Principal Location path( "add-location/", @@ -123,4 +128,10 @@ urlpatterns = [ views.EventShareView.as_view(), name="capture_event_share", ), + + path( + "events/", + views.EventListView.as_view(), + name="event_filter", + ), ] diff --git a/manage_events/api/views.py b/manage_events/api/views.py index b5c2a79..d15be1a 100644 --- a/manage_events/api/views.py +++ b/manage_events/api/views.py @@ -1,14 +1,18 @@ +import datetime from django.shortcuts import get_object_or_404 from django.utils import timezone +from django_filters.rest_framework import DjangoFilterBackend +import googlemaps from rest_framework import status, generics, mixins from rest_framework.views import APIView from django.conf import settings from accounts.models import IAmPrincipalLocation from goodtimes import constants -from django.db.models import Q +from django.db.models import Q, Count from taggit.models import Tag from django.utils.dateparse import parse_date from goodtimes import services +from goodtimes.services import GoogleMapsservice from goodtimes.utils import ApiResponse, CapacityError from rest_framework.permissions import IsAuthenticated from rest_framework_simplejwt.authentication import JWTAuthentication @@ -43,6 +47,7 @@ from manage_events.models import ( import requests from manage_events.utils import haversine_one, update_principal_location +from .filters import EventFilter class CreateEventApi(APIView): @@ -122,7 +127,9 @@ class CreateVenueApi(APIView): serializer = VenueSerializer(data=data, context={"request": request}) serializer.is_valid(raise_exception=True) - serializer.save(created_by=self.request.user, principal=self.request.user, active=True) + serializer.save( + created_by=self.request.user, principal=self.request.user, active=True + ) # Add additional logic for handling other relationships (e.g., Venue) return ApiResponse.success( @@ -568,6 +575,21 @@ class PrincipalPreferenceDetailView(generics.RetrieveAPIView): ) +class EventPreferencesView(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + model = EventCategory + serializer_class = EventCategorySerializer + + def get(self, request, *args, **kwargs): + """Get all event categories for the authenticated user.""" + obj = self.model.objects.filter(active=True, deleted=False) + serializer = self.serializer_class(obj, many=True) + return ApiResponse.success( + data=serializer.data, message=constants.SUCCESS, status=status.HTTP_200_OK + ) + + class EventMasterSearchAPIView(APIView): authentication_classes = [JWTAuthentication] permission_classes = [IsAuthenticated] @@ -688,9 +710,12 @@ class EventFilterByLocationAPIView(APIView): ) max_distance_km = 10 # Set your desired maximum distance - current_and_future_events_query = Q(active=True, deleted=False, draft=False, created_by__is_active=True,) & ( - Q(end_date__gte=today) - ) + current_and_future_events_query = Q( + active=True, + deleted=False, + draft=False, + created_by__is_active=True, + ) & (Q(end_date__gte=today)) # Get the queryset based on the filter conditions events_queryset = Event.objects.filter(current_and_future_events_query) @@ -961,3 +986,60 @@ class EventShareView(APIView): data="Event shared successfully.", status=status.HTTP_200_OK, ) + +class EventListView(generics.ListAPIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + queryset = Event.objects.filter(active=True, draft=False, deleted=False) + serializer_class = EventListSerializer + filter_backends = [DjangoFilterBackend] + filterset_class = EventFilter + + def apply_popularity_latest(self, queryset, ordering): + # Split the ordering fields and remove any leading '-' characters + ordering_fields = [field.lstrip("-") for field in ordering.split(",")] + + # Check if 'nearest' is in the ordering fields and remove it as nearest work only for lat and longitude + if "nearest" in ordering_fields: + ordering.replace("nearest", "") + ordering_fields.remove("nearest") + + # Annotate the queryset with popularity if 'popularity' is in the ordering fields + if "popularity" in ordering_fields: + queryset = queryset.annotate(popularity=Count("interaction_event")) + + # Replace 'latest' with '-created_on' in the ordering fields + ordering = ",".join( + "-created_on" if field == "latest" else f"-{field}" + for field in ordering_fields + ) + # Apply the ordering to the queryset + return queryset.order_by(*ordering.split(",")) + + def get_queryset(self): + queryset = super().get_queryset() + + # Sort by nearest location if latitude and longitude are provided + latitude = self.request.query_params.get("latitude") + longitude = self.request.query_params.get("longitude") + + if latitude and longitude: + print("latitude fucntion called") + gmaps_service = GoogleMapsservice() + queryset = gmaps_service.get_nearest_events(queryset, float(latitude), float(longitude)) + + # Apply popularity annotation and ordering if requested + ordering = self.request.query_params.get("ordering") + if ordering: + queryset = self.apply_popularity_latest(queryset, ordering) + return queryset + + def get(self, request, *args, **kwargs): + queryset = self.filter_queryset(self.get_queryset()) + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + data = self.get_paginated_response(serializer.data) + return ApiResponse.success(message=constants.SUCCESS, data=data) + serializer = self.get_serializer(queryset, many=True) + return ApiResponse.success(message=constants.SUCCESS, data=serializer.data) From 31e9d5a03c8999995349b87ddb85266ef296cc2c Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Wed, 10 Jul 2024 12:53:48 +0530 Subject: [PATCH 062/187] fix(module_2_filter):improve location filter and ordering nearest event --- manage_events/api/views.py | 4 ++-- requirements.txt | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/manage_events/api/views.py b/manage_events/api/views.py index d15be1a..2acdeb1 100644 --- a/manage_events/api/views.py +++ b/manage_events/api/views.py @@ -1022,14 +1022,14 @@ class EventListView(generics.ListAPIView): # Sort by nearest location if latitude and longitude are provided latitude = self.request.query_params.get("latitude") longitude = self.request.query_params.get("longitude") + ordering = self.request.query_params.get("ordering", "") - if latitude and longitude: + if latitude and longitude and "nearest" in ordering: print("latitude fucntion called") gmaps_service = GoogleMapsservice() queryset = gmaps_service.get_nearest_events(queryset, float(latitude), float(longitude)) # Apply popularity annotation and ordering if requested - ordering = self.request.query_params.get("ordering") if ordering: queryset = self.apply_popularity_latest(queryset, ordering) return queryset diff --git a/requirements.txt b/requirements.txt index 37cd484..3d196bc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,6 +22,7 @@ django-cors-headers==4.3.1 django-debug-toolbar==4.3.0 django-environ==0.11.2 django-extensions==3.2.3 +django-filter==24.2 django-phonenumber-field==7.3.0 django-quill-editor==0.1.40 django-taggit==5.0.1 @@ -69,7 +70,7 @@ sqlparse==0.4.4 stripe==8.2.0 tqdm==4.66.2 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 From 38fad86990de1cdbad3fd88d817e0abfbc6dd116 Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Thu, 11 Jul 2024 12:47:39 +0530 Subject: [PATCH 063/187] fix(module_2_filter):removed end date and changed price type --- manage_events/api/filters.py | 8 ++++---- manage_events/models.py | 9 +++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/manage_events/api/filters.py b/manage_events/api/filters.py index aeb164c..08e7543 100644 --- a/manage_events/api/filters.py +++ b/manage_events/api/filters.py @@ -11,9 +11,9 @@ class EventFilter(filters.FilterSet): 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") - end_date = filters.DateFilter(field_name="end_date", lookup_expr="lte") - price_from = filters.DateFilter(field_name="entry_fee", lookup_expr="gte") - price_to = filters.DateFilter(field_name="entry_fee", lookup_expr="lte") + # end_date = filters.DateFilter(field_name="end_date", lookup_expr="lte") + price_from = filters.NumberFilter(field_name="entry_fee", lookup_expr="gte") + price_to = filters.NumberFilter(field_name="entry_fee", lookup_expr="lte") age_group = filters.CharFilter(field_name="age_group", lookup_expr="icontains") class Meta: @@ -23,7 +23,7 @@ class EventFilter(filters.FilterSet): 'location', 'category', 'start_date', - 'end_date', + # 'end_date', 'price_from', 'price_to', 'age_group', diff --git a/manage_events/models.py b/manage_events/models.py index 54619dc..590f1cf 100644 --- a/manage_events/models.py +++ b/manage_events/models.py @@ -34,6 +34,15 @@ class Venue(BaseModel): def __str__(self): return self.title +class AgeGroups(BaseModel): + name = models.CharField(max_length=10, unique=True) + + class Meta: + db_table = "age_group" + + def __str__(self): + return self.name + class EventStatus(models.TextChoices): UPCOMING = "upcoming", "Upcoming" From 23bae8cf3da59f80830f39ade72a8537338b7bf2 Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Fri, 12 Jul 2024 13:13:53 +0530 Subject: [PATCH 064/187] fix(module_2_filter):changed base query of event filter --- manage_events/api/serializers.py | 2 +- manage_events/api/views.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/manage_events/api/serializers.py b/manage_events/api/serializers.py index b198381..7ea484a 100644 --- a/manage_events/api/serializers.py +++ b/manage_events/api/serializers.py @@ -41,7 +41,7 @@ class VenueSerializer(serializers.ModelSerializer): class VenueShortSerializer(serializers.ModelSerializer): class Meta: model = Venue - fields= ["id","title"] + fields = ["id", "title"] class EventCategorySerializer(serializers.ModelSerializer): diff --git a/manage_events/api/views.py b/manage_events/api/views.py index 2acdeb1..923e868 100644 --- a/manage_events/api/views.py +++ b/manage_events/api/views.py @@ -990,7 +990,7 @@ class EventShareView(APIView): class EventListView(generics.ListAPIView): authentication_classes = [JWTAuthentication] permission_classes = [IsAuthenticated] - queryset = Event.objects.filter(active=True, draft=False, deleted=False) + queryset = Event.objects.filter(active=True, draft=False, deleted=False, end_date__gte=timezone.now().date()) serializer_class = EventListSerializer filter_backends = [DjangoFilterBackend] filterset_class = EventFilter @@ -1042,4 +1042,4 @@ class EventListView(generics.ListAPIView): data = self.get_paginated_response(serializer.data) return ApiResponse.success(message=constants.SUCCESS, data=data) serializer = self.get_serializer(queryset, many=True) - return ApiResponse.success(message=constants.SUCCESS, data=serializer.data) + return ApiResponse.success(message=constants.SUCCESS, data=serializer.data) \ No newline at end of file From 91dba0308c580a406ab1ebf132ff9250dfeda68a Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Fri, 12 Jul 2024 16:00:22 +0530 Subject: [PATCH 065/187] fix(module_2_filter):fixed order by latest --- manage_events/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manage_events/api/views.py b/manage_events/api/views.py index 923e868..84543d7 100644 --- a/manage_events/api/views.py +++ b/manage_events/api/views.py @@ -1010,7 +1010,7 @@ class EventListView(generics.ListAPIView): # Replace 'latest' with '-created_on' in the ordering fields ordering = ",".join( - "-created_on" if field == "latest" else f"-{field}" + "-start_date" if field == "latest" else f"-{field}" for field in ordering_fields ) # Apply the ordering to the queryset From 83efd286873135bb4c09cc3021fcd4bf92e09c88 Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Fri, 12 Jul 2024 16:00:55 +0530 Subject: [PATCH 066/187] fix(module_2_filter):fixed order by latest --- manage_events/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manage_events/api/views.py b/manage_events/api/views.py index 84543d7..4d4be00 100644 --- a/manage_events/api/views.py +++ b/manage_events/api/views.py @@ -1010,7 +1010,7 @@ class EventListView(generics.ListAPIView): # Replace 'latest' with '-created_on' in the ordering fields ordering = ",".join( - "-start_date" if field == "latest" else f"-{field}" + "start_date" if field == "latest" else f"-{field}" for field in ordering_fields ) # Apply the ordering to the queryset From d670a1859994620b3ced91ef7217f1ef4213bb4c Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Tue, 16 Jul 2024 19:03:46 +0530 Subject: [PATCH 067/187] feat(module_2_filter): fix nearest ordering issue --- manage_events/api/views.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/manage_events/api/views.py b/manage_events/api/views.py index 4d4be00..50e5468 100644 --- a/manage_events/api/views.py +++ b/manage_events/api/views.py @@ -1013,6 +1013,12 @@ class EventListView(generics.ListAPIView): "start_date" if field == "latest" else f"-{field}" for field in ordering_fields ) + + # If ordering is empty, set a default ordering + if not ordering: + ordering = "-start_date" + + print(f"++++++++++++++++++++++++++++++++ordering data in populatirn flow {ordering}") # Apply the ordering to the queryset return queryset.order_by(*ordering.split(",")) @@ -1029,12 +1035,15 @@ class EventListView(generics.ListAPIView): gmaps_service = GoogleMapsservice() queryset = gmaps_service.get_nearest_events(queryset, float(latitude), float(longitude)) + print(f"=======orderring data is {ordering}") + # Apply popularity annotation and ordering if requested if ordering: queryset = self.apply_popularity_latest(queryset, ordering) return queryset def get(self, request, *args, **kwargs): + print(f"getquery set data is {self.get_queryset()}") queryset = self.filter_queryset(self.get_queryset()) page = self.paginate_queryset(queryset) if page is not None: From 29011dff064f367960f7bade4907ce5e84c589ea Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Wed, 17 Jul 2024 00:37:06 +0530 Subject: [PATCH 068/187] feat(socialmedia):added twitter service --- goodtimes/services.py | 39 +++++++++++++++++++++++++++++++++++++- goodtimes/settings/base.py | 6 ++++++ requirements.txt | 3 ++- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/goodtimes/services.py b/goodtimes/services.py index e2faeb2..ad2fcf7 100644 --- a/goodtimes/services.py +++ b/goodtimes/services.py @@ -1,5 +1,6 @@ import random import googlemaps +import tweepy from django.conf import settings from django.core.files.uploadedfile import UploadedFile from django.core.mail import EmailMessage @@ -745,7 +746,7 @@ class GoogleMapsservice: def get_place_id_from_coordinates(self, latitude, longitude): """ Get the place ID of the given coordinates. - + :param latitude: Latitude of the location :param longitude: Longitude of the location :return: Place ID @@ -817,3 +818,39 @@ class GoogleMapsservice: # queryset = queryset.order_by(preserved_order) return queryset + + +class TwitterAPI: + def __init__(self): + self.api_key = settings.TWITTER_API_KEY + self.api_secret_key = settings.TWITTER_API_SECRET_KEY + self.access_token = settings.TWITTER_ACCESS_TOKEN + self.access_token_secret = settings.TWITTER_ACCESS_TOKEN_SECRET + self.auth = self._setup_auth() + self.api = self._setup_api() + + def _setup_auth(self): + auth = tweepy.OAuthHandler(self.api_key, self.api_secret_key) + auth.set_access_token(self.access_token, self.access_token_secret) + return auth + + def _setup_api(self): + api = tweepy.API(self.auth) + return api + + def post_image_with_caption(self, image_path, caption): + media = self.api.media_upload(image_path) + tweet = self.api.update_status(status=caption, media_ids=[media.media_id]) + return tweet + + +class TwitterPoster: + def __init__(self, twitter_api): + self.twitter_api = twitter_api + + def post_tweet(self, image_path, caption): + try: + tweet = self.twitter_api.post_image_with_caption(image_path, caption) + return {'success': True, 'message': 'Tweet posted successfully!'} + except tweepy.TweepError as e: + return {'success': False, 'message': f'Error posting tweet: {e}'} \ No newline at end of file diff --git a/goodtimes/settings/base.py b/goodtimes/settings/base.py index 5476155..ee53e38 100644 --- a/goodtimes/settings/base.py +++ b/goodtimes/settings/base.py @@ -339,3 +339,9 @@ PLACES_MAPS_API_KEY = env.str("GOOGLE_MAPS_API_KEY") PLACES_MAP_WIDGET_HEIGHT = 480 PLACES_MAP_OPTIONS = '{"center": { "lat": 38.971584, "lng": -95.235072 }, "zoom": 10}' PLACES_MARKER_OPTIONS = '{"draggable": true}' + +# twitter keys +TWITTER_API_KEY = env.str("TWITTER_API_KEY") +TWITTER_API_SECRET_KEY = env.str("TWITTER_API_SECRET_KEY") +TWITTER_ACCESS_TOKEN = env.str("TWITTER_ACCESS_TOKEN") +TWITTER_ACCESS_TOKEN_SECRET = env.str("TWITTER_ACCESS_TOKEN_SECRET") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 3d196bc..f3ae9d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -69,8 +69,9 @@ sniffio==1.3.1 sqlparse==0.4.4 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 From 3da21b0c0b736fc0e9374ed6f0a5bc3942ac573e Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Wed, 17 Jul 2024 12:09:13 +0530 Subject: [PATCH 069/187] feat(age_group): dynamically change age group in filter and event --- manage_events/migrations/0015_agegroups.py | 32 ++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 manage_events/migrations/0015_agegroups.py diff --git a/manage_events/migrations/0015_agegroups.py b/manage_events/migrations/0015_agegroups.py new file mode 100644 index 0000000..32fcd12 --- /dev/null +++ b/manage_events/migrations/0015_agegroups.py @@ -0,0 +1,32 @@ +# Generated by Django 5.0.2 on 2024-07-17 06:36 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('manage_events', '0014_event_principal'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='AgeGroups', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('active', models.BooleanField(default=True)), + ('deleted', models.BooleanField(default=False)), + ('created_on', models.DateTimeField(auto_now_add=True)), + ('modified_on', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=10, unique=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)), + ('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'age_group', + }, + ), + ] From ee25cf5a8b22c92c7ee10a358250b501667e93fc Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Wed, 17 Jul 2024 19:52:02 +0530 Subject: [PATCH 070/187] feat(module_2_filter):change age group dynamically --- manage_events/admin.py | 2 ++ manage_events/api/serializers.py | 5 ++++ manage_events/api/urls.py | 5 ++++ manage_events/api/views.py | 14 +++++++++++ manage_events/forms.py | 23 ++++++++++++------- .../management/commands/populate_age_group.py | 20 ++++++++++++++++ .../migrations/0016_alter_event_age_group.py | 19 +++++++++++++++ .../migrations/0017_alter_event_age_group.py | 18 +++++++++++++++ 8 files changed, 98 insertions(+), 8 deletions(-) create mode 100644 manage_events/management/commands/populate_age_group.py create mode 100644 manage_events/migrations/0016_alter_event_age_group.py create mode 100644 manage_events/migrations/0017_alter_event_age_group.py diff --git a/manage_events/admin.py b/manage_events/admin.py index abaf588..39aae97 100644 --- a/manage_events/admin.py +++ b/manage_events/admin.py @@ -8,6 +8,7 @@ from .models import ( EventMaster, Event, EventPrincipalInteraction, + AgeGroups ) @@ -129,3 +130,4 @@ admin.site.register(Event, EventAdmin) admin.site.register(EventCategory, EventCategoryAdmin) admin.site.register(Venue, VenueAdmin) admin.site.register(EventMaster, EventMasterAdmin) +admin.site.register(AgeGroups) diff --git a/manage_events/api/serializers.py b/manage_events/api/serializers.py index 7ea484a..4529224 100644 --- a/manage_events/api/serializers.py +++ b/manage_events/api/serializers.py @@ -6,6 +6,7 @@ from taggit.models import Tag from manage_events.utils import get_location_info from accounts.api.serializers import ProfileSerializer from manage_events.models import ( + AgeGroups, EventMaster, Event, EventCategory, @@ -49,6 +50,10 @@ class EventCategorySerializer(serializers.ModelSerializer): model = EventCategory fields = ["id", "title", "image", "description", "video_url"] +class AgeGroupsSerializer(serializers.ModelSerializer): + class Meta: + model = AgeGroups + fields = ['id', 'name'] class EventListSerializer(serializers.ModelSerializer): category = EventCategorySerializer(read_only=True) diff --git a/manage_events/api/urls.py b/manage_events/api/urls.py index 1f657d3..ad715ce 100644 --- a/manage_events/api/urls.py +++ b/manage_events/api/urls.py @@ -129,6 +129,11 @@ urlpatterns = [ name="capture_event_share", ), + path( + "age-groups/", views.AgeGroupListView.as_view(), + name="age_group_list" + ), + path( "events/", views.EventListView.as_view(), diff --git a/manage_events/api/views.py b/manage_events/api/views.py index 50e5468..860c65f 100644 --- a/manage_events/api/views.py +++ b/manage_events/api/views.py @@ -18,6 +18,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework_simplejwt.authentication import JWTAuthentication from manage_cms.api.serializers import TagSerializer from manage_events.api.serializers import ( + AgeGroupsSerializer, EventDateRangeSerializer, EventMasterSearchSerializer, EventMasterSerializer, @@ -32,6 +33,7 @@ from manage_events.api.serializers import ( EventListSerializer, ) from manage_events.models import ( + AgeGroups, EventInteractionType, EventMaster, Event, @@ -987,6 +989,18 @@ class EventShareView(APIView): status=status.HTTP_200_OK, ) +class AgeGroupListView(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + serializer_class = AgeGroupsSerializer + model = AgeGroups + + def get(self, request): + queryset = self.model.objects.filter(active=True) + serializer = self.serializer_class(queryset, many=True) + return ApiResponse.success(message=constants.SUCCESS, data=serializer.data) + + class EventListView(generics.ListAPIView): authentication_classes = [JWTAuthentication] permission_classes = [IsAuthenticated] diff --git a/manage_events/forms.py b/manage_events/forms.py index dc5777d..a406e5a 100644 --- a/manage_events/forms.py +++ b/manage_events/forms.py @@ -1,6 +1,6 @@ from django import forms from accounts.models import IAmPrincipal, IAmPrincipalExtendedData -from manage_events.models import EventImage, EventMaster, Event, EventCategory, Venue +from manage_events.models import AgeGroups, EventImage, EventMaster, Event, EventCategory, Venue from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ @@ -17,12 +17,6 @@ class EventCategoryForm(forms.ModelForm): class EventForm(forms.ModelForm): - AGE_GROUP_CHOICES = [ - ('18 & Under', '18 & Under'), - ('18-30', '18-30'), - ('30+', '30+'), - ('Family Event', 'Family Event'), - ] principal = forms.ModelChoiceField( queryset=IAmPrincipal.objects.select_related("extended_data").filter( extended_data__is_onboarded=True, @@ -38,7 +32,11 @@ class EventForm(forms.ModelForm): ) image = forms.ImageField(label="Thumbnail") event_images = forms.ImageField(label="Event Images") - age_group = forms.ChoiceField(choices=AGE_GROUP_CHOICES, label="Age Group", required=True) + age_group = forms.ModelChoiceField( + queryset=AgeGroups.objects.filter(active=True), + label="Age Group", + required=True + ) class Meta: model = Event @@ -96,6 +94,15 @@ class EventForm(forms.ModelForm): if event_images.exists(): self.fields['event_images'].initial = [image.image.url for image in event_images] + # Set the initial value for age_group if instance is provided + print(f"age group is {self.instance.age_group}") + if self.instance and self.instance.pk: + try: + self.fields['age_group'].initial = AgeGroups.objects.get(name=instance.age_group).id + print(f"field initials {self.fields['age_group'].initial}") + except AgeGroups.DoesNotExist: + pass + if principal_id: self.fields['venue'].queryset = Venue.objects.filter(principal_id=principal_id, active=True) else: diff --git a/manage_events/management/commands/populate_age_group.py b/manage_events/management/commands/populate_age_group.py new file mode 100644 index 0000000..b55cceb --- /dev/null +++ b/manage_events/management/commands/populate_age_group.py @@ -0,0 +1,20 @@ +from django.core.management.base import BaseCommand +from ...models import AgeGroups, Event +import random + +class Command(BaseCommand): + help = 'Populate the AgeGroup model with predefined age groups' + + def handle(self, *args, **kwargs): + age_groups = ["18-21", "21-30", "30-40", "40-50", "50+"] + for age in age_groups: + age_group, created = AgeGroups.objects.get_or_create(name=age) + if created: + self.stdout.write(self.style.SUCCESS(f'Age group "{age}" created.')) + else: + self.stdout.write(self.style.WARNING(f'Age group "{age}" already exists.')) + + # Update all Event objects with a random age group + for event in Event.objects.all(): + event.age_group = random.choice(age_groups) + event.save() diff --git a/manage_events/migrations/0016_alter_event_age_group.py b/manage_events/migrations/0016_alter_event_age_group.py new file mode 100644 index 0000000..e853f15 --- /dev/null +++ b/manage_events/migrations/0016_alter_event_age_group.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.2 on 2024-07-17 10:51 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('manage_events', '0015_agegroups'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='age_group', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='manage_events.agegroups'), + ), + ] diff --git a/manage_events/migrations/0017_alter_event_age_group.py b/manage_events/migrations/0017_alter_event_age_group.py new file mode 100644 index 0000000..8f62c33 --- /dev/null +++ b/manage_events/migrations/0017_alter_event_age_group.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.2 on 2024-07-17 10:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('manage_events', '0016_alter_event_age_group'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='age_group', + field=models.CharField(blank=True, max_length=100, null=True), + ), + ] From e96bfe9068afab8c8646a259da09e0fef75f6af1 Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Thu, 18 Jul 2024 13:52:48 +0530 Subject: [PATCH 071/187] feat(module_2_filter):changed age_group in admin side --- manage_events/forms.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/manage_events/forms.py b/manage_events/forms.py index a406e5a..3f40650 100644 --- a/manage_events/forms.py +++ b/manage_events/forms.py @@ -32,8 +32,8 @@ class EventForm(forms.ModelForm): ) image = forms.ImageField(label="Thumbnail") event_images = forms.ImageField(label="Event Images") - age_group = forms.ModelChoiceField( - queryset=AgeGroups.objects.filter(active=True), + age_group = forms.ChoiceField( + choices=[], label="Age Group", required=True ) @@ -96,12 +96,11 @@ class EventForm(forms.ModelForm): # Set the initial value for age_group if instance is provided print(f"age group is {self.instance.age_group}") - if self.instance and self.instance.pk: - try: - self.fields['age_group'].initial = AgeGroups.objects.get(name=instance.age_group).id - print(f"field initials {self.fields['age_group'].initial}") - except AgeGroups.DoesNotExist: - pass + age_groups = [(age_group.name, age_group.name) for age_group in AgeGroups.objects.filter(active=True)] + self.fields['age_group'].choices = age_groups + + if self.instance: + self.fields['age_group'].initial = self.instance.age_group if principal_id: self.fields['venue'].queryset = Venue.objects.filter(principal_id=principal_id, active=True) From b226e1f58425ddc2e88f5731e18acac055bb5bd1 Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Thu, 18 Jul 2024 16:02:15 +0530 Subject: [PATCH 072/187] feat(module_2_filter):fixed migration file issue --- .../migrations/0016_alter_event_age_group.py | 19 ------------------- .../migrations/0017_alter_event_age_group.py | 18 ------------------ 2 files changed, 37 deletions(-) delete mode 100644 manage_events/migrations/0016_alter_event_age_group.py delete mode 100644 manage_events/migrations/0017_alter_event_age_group.py diff --git a/manage_events/migrations/0016_alter_event_age_group.py b/manage_events/migrations/0016_alter_event_age_group.py deleted file mode 100644 index e853f15..0000000 --- a/manage_events/migrations/0016_alter_event_age_group.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.0.2 on 2024-07-17 10:51 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('manage_events', '0015_agegroups'), - ] - - operations = [ - migrations.AlterField( - model_name='event', - name='age_group', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='manage_events.agegroups'), - ), - ] diff --git a/manage_events/migrations/0017_alter_event_age_group.py b/manage_events/migrations/0017_alter_event_age_group.py deleted file mode 100644 index 8f62c33..0000000 --- a/manage_events/migrations/0017_alter_event_age_group.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.0.2 on 2024-07-17 10:51 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('manage_events', '0016_alter_event_age_group'), - ] - - operations = [ - migrations.AlterField( - model_name='event', - name='age_group', - field=models.CharField(blank=True, max_length=100, null=True), - ), - ] From 473a961ab6a4e24cbb726f98b9adf163db23dcd4 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 19 Jul 2024 21:18:18 +0530 Subject: [PATCH 073/187] manage coupons --- goodtimes/settings/base.py | 1 + goodtimes/settings/development.py | 25 +++++++++---------- goodtimes/urls.py | 9 ++++--- goodtimes/webhook.py | 4 +-- manage_coupons/__init__.py | 0 manage_coupons/admin.py | 3 +++ manage_coupons/api/serializers.py | 0 manage_coupons/api/urls.py | 0 manage_coupons/api/views.py | 0 manage_coupons/apps.py | 6 +++++ manage_coupons/forms.py | 0 manage_coupons/migrations/__init__.py | 0 manage_coupons/models.py | 33 +++++++++++++++++++++++++ manage_coupons/tests.py | 3 +++ manage_coupons/urls.py | 0 manage_coupons/views.py | 3 +++ manage_subscriptions/views.py | 32 +++++++++++++----------- static/src/assets/css/payment/style.css | 25 +++++++++++++++++++ templates/stripe_html/index.html | 23 ++++++----------- templates/stripe_html/subscribe.html | 6 +++-- 20 files changed, 120 insertions(+), 53 deletions(-) create mode 100644 manage_coupons/__init__.py create mode 100644 manage_coupons/admin.py create mode 100644 manage_coupons/api/serializers.py create mode 100644 manage_coupons/api/urls.py create mode 100644 manage_coupons/api/views.py create mode 100644 manage_coupons/apps.py create mode 100644 manage_coupons/forms.py create mode 100644 manage_coupons/migrations/__init__.py create mode 100644 manage_coupons/models.py create mode 100644 manage_coupons/tests.py create mode 100644 manage_coupons/urls.py create mode 100644 manage_coupons/views.py diff --git a/goodtimes/settings/base.py b/goodtimes/settings/base.py index 5476155..695359e 100644 --- a/goodtimes/settings/base.py +++ b/goodtimes/settings/base.py @@ -64,6 +64,7 @@ LOCAL_APPS = [ "manage_referrals", "manage_cms", "manage_communications", # for contact us, and feedback + "manage_coupons", "manage_notifications.apps.ManageNotificationsConfig", "chat", ] diff --git a/goodtimes/settings/development.py b/goodtimes/settings/development.py index f33f818..cd26290 100644 --- a/goodtimes/settings/development.py +++ b/goodtimes/settings/development.py @@ -26,17 +26,17 @@ CORS_ORIGIN_ALLOW_ALL = True CORS_ORIGIN_WHITELIST = ("http://localhost:3000",) -if DEBUG: - MIDDLEWARE += [ - "debug_toolbar.middleware.DebugToolbarMiddleware", - ] - INSTALLED_APPS += [ - "debug_toolbar", - "django_extensions", - ] - INTERNAL_IPS = [ - "127.0.0.1", - ] +# if DEBUG: +# MIDDLEWARE += [ +# "debug_toolbar.middleware.DebugToolbarMiddleware", +# ] +# INSTALLED_APPS += [ +# "debug_toolbar", +# "django_extensions", +# ] +# INTERNAL_IPS = [ +# "127.0.0.1", +# ] BASE_DOMAIN = "http://192.168.29.219:8000" @@ -44,14 +44,11 @@ BASE_DOMAIN = "http://192.168.29.219:8000" # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ - MEDIA_URL = "/media/" MEDIA_ROOT = os.path.join(BASE_DIR, "media") -# STATIC_ROOT = os.path.join(BASE_DIR, "static") STATIC_URL = "/static/" - STATICFILES_DIRS = [BASE_DIR.joinpath("static")] STRIPE_CHECKOUT_URL = "http://localhost:8000/subscriptions/stripe-subscription/" diff --git a/goodtimes/urls.py b/goodtimes/urls.py index 5414dc6..031d221 100644 --- a/goodtimes/urls.py +++ b/goodtimes/urls.py @@ -60,8 +60,9 @@ urlpatterns = [ # path('api/', include("accounts.api.urls")), ] -if settings.DEBUG: - import debug_toolbar +# if settings.DEBUG: +# import debug_toolbar - urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) - urlpatterns += [path("__debug__/", include(debug_toolbar.urls))] +# 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))] diff --git a/goodtimes/webhook.py b/goodtimes/webhook.py index 7824ddf..19e9633 100644 --- a/goodtimes/webhook.py +++ b/goodtimes/webhook.py @@ -4,14 +4,13 @@ from django.shortcuts import get_object_or_404 from datetime import timedelta from django.utils import timezone from onesignal_sdk.client import Client as OneSignalClient -from accounts.models import IAmPrincipal, IAmPrincipalOtp, IAmPrincipalType +from accounts.models import IAmPrincipal from manage_notifications.models import ( IAmPrincipalNotificationSettings, InAppNotification, NotificationCategoryChoices, ) from manage_referrals.models import ( - GoodTimeCoins, ReferralRecord, ReferralRecordReward, ReferralTracking, @@ -21,7 +20,6 @@ from manage_subscriptions.models import PrincipalSubscription, Subscription from manage_wallets.models import ( TransactionStatus, TransactionType, - Wallet, Transaction, ) import logging diff --git a/manage_coupons/__init__.py b/manage_coupons/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/manage_coupons/admin.py b/manage_coupons/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/manage_coupons/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/manage_coupons/api/serializers.py b/manage_coupons/api/serializers.py new file mode 100644 index 0000000..e69de29 diff --git a/manage_coupons/api/urls.py b/manage_coupons/api/urls.py new file mode 100644 index 0000000..e69de29 diff --git a/manage_coupons/api/views.py b/manage_coupons/api/views.py new file mode 100644 index 0000000..e69de29 diff --git a/manage_coupons/apps.py b/manage_coupons/apps.py new file mode 100644 index 0000000..a1391cc --- /dev/null +++ b/manage_coupons/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ManageCouponsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "manage_coupons" diff --git a/manage_coupons/forms.py b/manage_coupons/forms.py new file mode 100644 index 0000000..e69de29 diff --git a/manage_coupons/migrations/__init__.py b/manage_coupons/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/manage_coupons/models.py b/manage_coupons/models.py new file mode 100644 index 0000000..46c267f --- /dev/null +++ b/manage_coupons/models.py @@ -0,0 +1,33 @@ +from django.db import models +from django.utils import timezone +from accounts.models import BaseModel, IAmPrincipalType + +# Create your models here. + + +class Coupon(BaseModel): + title = models.CharField(max_length=255) + coupon_code = models.CharField(max_length=50, unique=True) + description = models.TextField(null=True, blank=True) + image = models.ImageField(upload_to="coupon_img", null=True, blank=True) + discount_amount = models.DecimalField( + max_digits=10, decimal_places=2, null=True, blank=True + ) + discount_percentage = models.DecimalField( + max_digits=5, decimal_places=2, null=True, blank=True + ) + principal_types = models.ManyToManyField( + IAmPrincipalType, related_name="principal_type_coupons", blank=True + ) + valid_from = models.DateTimeField() + valid_to = models.DateTimeField() + + class Meta: + db_table = "coupon" + + def __str__(self): + return self.coupon_code + + def is_valid(self): + now = timezone.now() + return self.active and not self.deleted and self.valid_from <= now <= self.valid_to diff --git a/manage_coupons/tests.py b/manage_coupons/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/manage_coupons/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/manage_coupons/urls.py b/manage_coupons/urls.py new file mode 100644 index 0000000..e69de29 diff --git a/manage_coupons/views.py b/manage_coupons/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/manage_coupons/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 959cd39..7a91d56 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -415,15 +415,23 @@ def stripe_config(request): return JsonResponse(stripe_config, safe=False) -def has_active_principal_subscription(principal_id): - return PrincipalSubscription.objects.filter( - principal__id=principal_id, - active=True, - deleted=False, - cancelled=False, - is_paid=True, - end_date__gte=timezone.now().date(), - ).exists() +@csrf_exempt +@require_POST +def validate_coupon(request): + data = json.loads(request.body) + coupon_code = data.get("couponCode", None) + + # Validate coupon code + if coupon_code: + try: + coupon = stripe.Coupon.retrieve(coupon_code) + if coupon["valid"] is False: + return JsonResponse({"error": "Invalid or expired coupon code."}, status=400) + return JsonResponse({"success": "Coupon code is valid."}) + except stripe.error.InvalidRequestError: + return JsonResponse({"error": "Invalid coupon code."}, status=400) + else: + return JsonResponse({"error": "Coupon code not provided."}, status=400) @csrf_exempt @@ -436,12 +444,6 @@ def create_checkout_session(request): subscription_id = data.get("subscriptionId", None) principal_id = request.user.id - # if has_active_principal_subscription(principal_id): - # print("Active principal subscription already exists.") - # return JsonResponse( - # {"error": "Active principal subscription already exists"}, status=400 - # ) - try: subscription = Subscription.objects.get(id=subscription_id) except Subscription.DoesNotExist: diff --git a/static/src/assets/css/payment/style.css b/static/src/assets/css/payment/style.css index 43ef5f8..b58d7c4 100644 --- a/static/src/assets/css/payment/style.css +++ b/static/src/assets/css/payment/style.css @@ -818,4 +818,29 @@ div#accordionExample { .footer .store-app { margin-bottom: 16px; } +} + +.coupon-code-input, +.common-btn { + display: inline-block; + margin-right: 10px; +} + +.common-btn { + /* remove default button margins */ + margin: 0; +} +.feat-card input.form-control.coupon-code-input { + border-radius: 5px; + width: 100%; + background: transparent; + color: #fff; + margin-bottom: 20px; + margin-right: 0; + caret-color: #fff; + text-align: center; +} + +.feat-card input.form-control.coupon-code-input::placeholder { + color: #a5a4a4; } \ No newline at end of file diff --git a/templates/stripe_html/index.html b/templates/stripe_html/index.html index 5016178..1d48216 100644 --- a/templates/stripe_html/index.html +++ b/templates/stripe_html/index.html @@ -1,17 +1,17 @@ -{% load static %} - + + Good times + {% load static %} - Good times @@ -128,6 +128,7 @@
    + @@ -136,7 +137,7 @@
    -
    + {% empty %}

    No subscriptions available.

    {% endfor %} @@ -529,9 +530,11 @@ document.querySelectorAll(".subscribe-btn").forEach(button => { button.addEventListener("click", () => { const subscriptionId = button.getAttribute("data-subscription-id"); - + const couponCode = button.previousElementSibling.value; console.log("subscriptionId: ", subscriptionId); + console.log("couponCode: ", couponCode); button.disabled = true; + button.previousElementSibling.value = ""; // Create checkout session for the selected subscription fetch(stripeFinalUrl, { method: "POST", @@ -547,20 +550,10 @@ }) .catch((error) => { console.error("Error:", error); - // const errorMessageElement = document.getElementById('already-active-subscription'); - // if (errorMessageElement) { - // errorMessageElement.innerText = "Error: " + error.message; // Display the error in the specific div - // errorMessageElement.style.color = 'red'; // Set text color to red - // errorMessageElement.style.fontWeight = 'bold'; // Set text to bold - // } button.disabled = false; }); }); }); - - - - }); diff --git a/templates/stripe_html/subscribe.html b/templates/stripe_html/subscribe.html index 1b38896..7442179 100644 --- a/templates/stripe_html/subscribe.html +++ b/templates/stripe_html/subscribe.html @@ -1,9 +1,11 @@ +{% load static %} + Goodtimes @@ -22,7 +24,7 @@ console.log("Sanity check!"); // Get Stripe publishable key - fetch("https://goodtimes.betadelivery.com/subscriptions/stripe-subscription/") + fetch("http://localhost:8000/subscriptions/stripe-subscription/") .then((result) => { return result.json(); }) .then((data) => { // Initialize Stripe.js @@ -32,7 +34,7 @@ // Event handler document.querySelector("#submitBtn").addEventListener("click", () => { // Get Checkout Session ID - fetch("https://goodtimes.betadelivery.com/subscriptions/create-checkout-session/") + fetch("http://localhost:8000/subscriptions/create-checkout-session/") .then((result) => { return result.json(); }) .then((data) => { console.log(data); From 6a8dc781d2650a6787e1be7401983bc0260dbc1a Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 22 Jul 2024 21:08:56 +0530 Subject: [PATCH 074/187] manage coupons list, add, edit, delete views --- accounts/context_processors.py | 1 + accounts/fixture_script.py | 15 +- .../fixtures/resource_action_fixture.json | 17 ++ accounts/resource_action.py | 1 + goodtimes/settings/development.py | 1 + goodtimes/settings/production.py | 1 + goodtimes/settings/staging.py | 1 + goodtimes/urls.py | 3 + goodtimes/webhook.py | 20 +- manage_coupons/forms.py | 47 ++++ manage_coupons/migrations/0001_initial.py | 81 ++++++ manage_coupons/models.py | 16 +- manage_coupons/urls.py | 23 ++ manage_coupons/views.py | 113 ++++++++- manage_subscriptions/forms.py | 11 +- .../0009_principalsubscription_coupon_code.py | 18 ++ manage_subscriptions/models.py | 1 + manage_subscriptions/urls.py | 5 + manage_subscriptions/views.py | 136 +++++----- templates/elements/sidebar.html | 11 + .../coupon_add.html} | 70 +----- templates/manage_coupons/coupon_list.html | 163 ++++++++++++ templates/manage_stock/stock_index_add.html | 135 ---------- templates/manage_stock/stock_list.html | 237 ------------------ templates/manage_stock/stock_price_list.html | 175 ------------- templates/manage_stock/team_stock.html | 156 ------------ .../subscription_add.html | 66 ----- templates/stripe_html/index.html | 74 ++++-- 28 files changed, 672 insertions(+), 926 deletions(-) create mode 100644 manage_coupons/migrations/0001_initial.py create mode 100644 manage_subscriptions/migrations/0009_principalsubscription_coupon_code.py rename templates/{manage_stock/stock_add.html => manage_coupons/coupon_add.html} (68%) create mode 100644 templates/manage_coupons/coupon_list.html delete mode 100644 templates/manage_stock/stock_index_add.html delete mode 100644 templates/manage_stock/stock_list.html delete mode 100644 templates/manage_stock/stock_price_list.html delete mode 100644 templates/manage_stock/team_stock.html diff --git a/accounts/context_processors.py b/accounts/context_processors.py index 98689de..0eecf0f 100644 --- a/accounts/context_processors.py +++ b/accounts/context_processors.py @@ -44,6 +44,7 @@ def compute_resource_action_constants(request): 'RESOURCE_MANAGE_NOTIFICATIONS': resource_action.RESOURCE_MANAGE_NOTIFICATIONS, 'RESOURCE_MANAGE_REFERRALS': resource_action.RESOURCE_MANAGE_REFERRALS, 'RESOURCE_MANAGE_FEEDBACK': resource_action.RESOURCE_MANAGE_FEEDBACK, + 'RESOURCE_MANAGE_COUPONS': resource_action.RESOURCE_MANAGE_COUPONS, 'RESOURCE_IAM_PRINCIPAL': resource_action.RESOURCE_IAM_PRINCIPAL, 'RESOURCE_IAM_PRINCIPAL_GROUP': resource_action.RESOURCE_IAM_PRINCIPAL_GROUP, 'RESOURCE_IAM_GROUP': resource_action.RESOURCE_IAM_GROUP, diff --git a/accounts/fixture_script.py b/accounts/fixture_script.py index 663ee5b..921f26d 100644 --- a/accounts/fixture_script.py +++ b/accounts/fixture_script.py @@ -27,7 +27,8 @@ from accounts.resource_action import ( RESOURCE_MANAGE_REFERRALS, RESOURCE_MANAGE_FEEDBACK, RESOURCE_MANAGE_WITHDRAWALS, - RESOURCE_MANAGE_BANK_ACCOUNTS + RESOURCE_MANAGE_BANK_ACCOUNTS, + RESOURCE_MANAGE_COUPONS ) # this variable store the data of model principaltype, action, resource fixture_data = [ @@ -334,4 +335,16 @@ fixture_data = [ "action": [1, 2, 3, 4], }, }, + { + "model": "accounts.iamappresource", + "pk": 18, + "fields": { + "name": RESOURCE_MANAGE_COUPONS, + "label": RESOURCE_MANAGE_COUPONS, + "slug": RESOURCE_MANAGE_COUPONS, + "created_on": "2023-09-28T16:17:42.815", + "modified_on": "2023-09-28T16:17:42.815", + "action": [1, 2, 3, 4], + }, + }, ] diff --git a/accounts/fixtures/resource_action_fixture.json b/accounts/fixtures/resource_action_fixture.json index df2e808..e48d27f 100644 --- a/accounts/fixtures/resource_action_fixture.json +++ b/accounts/fixtures/resource_action_fixture.json @@ -386,5 +386,22 @@ 4 ] } + }, + { + "model": "accounts.iamappresource", + "pk": 18, + "fields": { + "name": "manage_coupons", + "label": "manage_coupons", + "slug": "manage_coupons", + "created_on": "2023-09-28T16:17:42.815", + "modified_on": "2023-09-28T16:17:42.815", + "action": [ + 1, + 2, + 3, + 4 + ] + } } ] \ No newline at end of file diff --git a/accounts/resource_action.py b/accounts/resource_action.py index 3e10751..7bad7a7 100644 --- a/accounts/resource_action.py +++ b/accounts/resource_action.py @@ -28,6 +28,7 @@ RESOURCE_MANAGE_REFERRALS = "manage_referrals" RESOURCE_MANAGE_NOTIFICATIONS = "manage_notifications" RESOURCE_MANAGE_WITHDRAWALS = "manage_withdrawals" RESOURCE_MANAGE_BANK_ACCOUNTS = "manage_bank_accounts" +RESOURCE_MANAGE_COUPONS = "manage_coupons" # These constants are used solely for managing the active and inactive state of pages diff --git a/goodtimes/settings/development.py b/goodtimes/settings/development.py index cd26290..0fc2e13 100644 --- a/goodtimes/settings/development.py +++ b/goodtimes/settings/development.py @@ -53,5 +53,6 @@ STATICFILES_DIRS = [BASE_DIR.joinpath("static")] STRIPE_CHECKOUT_URL = "http://localhost:8000/subscriptions/stripe-subscription/" STRIPE_FINAL_URL = "http://localhost:8000/subscriptions/create-checkout-session/" +COUPON_VALIDITY_CHECK_URL = "http://localhost:8000/subscriptions/coupon-validity-check/" LOGO_PATH = "static" diff --git a/goodtimes/settings/production.py b/goodtimes/settings/production.py index c3ed2bf..edd145c 100644 --- a/goodtimes/settings/production.py +++ b/goodtimes/settings/production.py @@ -82,5 +82,6 @@ STRIPE_CHECKOUT_URL = ( STRIPE_FINAL_URL = ( "https://admin.goodtimesltd.co.uk/subscriptions/create-checkout-session/" ) +COUPON_VALIDITY_CHECK_URL = "https://admin.goodtimesltd.co.uk/subscriptions/coupon-validity-check/" LOGO_PATH = "/var/www/goodtimes_prod/goodtimes/static" diff --git a/goodtimes/settings/staging.py b/goodtimes/settings/staging.py index 315c495..ade2303 100644 --- a/goodtimes/settings/staging.py +++ b/goodtimes/settings/staging.py @@ -82,5 +82,6 @@ STRIPE_CHECKOUT_URL = ( STRIPE_FINAL_URL = ( "https://staging.goodtimesltd.co.uk/subscriptions/create-checkout-session/" ) +COUPON_VALIDITY_CHECK_URL = "https://staging.goodtimesltd.co.uk/subscriptions/coupon-validity-check/" LOGO_PATH = "/var/www/goodtimes/static" diff --git a/goodtimes/urls.py b/goodtimes/urls.py index 031d221..c31e7f0 100644 --- a/goodtimes/urls.py +++ b/goodtimes/urls.py @@ -53,6 +53,9 @@ urlpatterns = [ path("subscriptions/", include("manage_subscriptions.urls")), path("api/subscriptions/", include("manage_subscriptions.api.urls")), + path("coupons/", include("manage_coupons.urls")), + # path("api/coupons/", include("manage_coupons.api.urls")), + path("notifications/", include("manage_notifications.urls")), path("api/notifications/", include("manage_notifications.api.urls")), diff --git a/goodtimes/webhook.py b/goodtimes/webhook.py index 19e9633..5f1481a 100644 --- a/goodtimes/webhook.py +++ b/goodtimes/webhook.py @@ -5,6 +5,7 @@ from datetime import timedelta from django.utils import timezone from onesignal_sdk.client import Client as OneSignalClient from accounts.models import IAmPrincipal +from manage_coupons.models import Coupon from manage_notifications.models import ( IAmPrincipalNotificationSettings, InAppNotification, @@ -132,6 +133,16 @@ class WebhookService: def get_order_id(self): return self.charge_data["metadata"]["order_id"] + def get_coupon(self): + coupon_code = self.charge_data["metadata"].get("couponCode") + if coupon_code: + try: + return Coupon.objects.get(coupon_code=coupon_code) + except Coupon.DoesNotExist: + logger.error(f"Invalid coupon code: {coupon_code}") + raise ValueError(f"Invalid coupon code: {coupon_code}") + return None + class ReferralRewardService: def __init__(self, principal, principal_subscription, subscription): @@ -225,7 +236,7 @@ class SubscriptionService: def __init__(self): self.principal_subscription = None - def create_principal_subscription(self, principal, subscription, order_id): + def create_principal_subscription(self, principal, subscription, order_id, coupon=None): subscription_days = subscription.plan.days today = timezone.now().date() last_date = today + timedelta(days=subscription_days) @@ -237,7 +248,11 @@ class SubscriptionService: start_date=today, end_date=last_date, grace_period_end_date=last_date + timedelta(days=15), + coupon_code=coupon.coupon_code if coupon else None, ) + if coupon: + coupon.no_of_redeems += 1 + coupon.save() self.principal_subscription = principal_subscription return principal_subscription @@ -260,6 +275,7 @@ class PaymentProcessingService: self.transaction = self.webhook_service.get_transaction() self.subscription = self.webhook_service.get_subscription() self.order_id = self.webhook_service.get_order_id() + self.coupon = self.webhook_service.get_coupon() self.subscription_service = SubscriptionService() self.principal_subscription = None @@ -274,7 +290,7 @@ class PaymentProcessingService: # Create or update the principal subscription self.principal_subscription = ( self.subscription_service.create_principal_subscription( - self.principal, self.subscription, self.order_id + self.principal, self.subscription, self.order_id, self.coupon ) ) print("First Part Done....!!!!!") diff --git a/manage_coupons/forms.py b/manage_coupons/forms.py index e69de29..c89a25e 100644 --- a/manage_coupons/forms.py +++ b/manage_coupons/forms.py @@ -0,0 +1,47 @@ +from django import forms +from django.core.exceptions import ValidationError +from manage_coupons.models import Coupon + + +class CouponForm(forms.ModelForm): + class Meta: + model = Coupon + fields = [ + "title", + "coupon_code", + "description", + "image", + "discount_amount", + "discount_percentage", + "valid_from", + "valid_to", + "max_redeems", + ] + widgets = { + "valid_from": forms.DateTimeInput(attrs={"type": "datetime-local"}), + "valid_to": forms.DateTimeInput(attrs={"type": "datetime-local"}), + } + + def clean(self): + cleaned_data = super().clean() + discount_amount = cleaned_data.get("discount_amount") + discount_percentage = cleaned_data.get("discount_percentage") + valid_from = cleaned_data.get("valid_from") + valid_to = cleaned_data.get("valid_to") + + if discount_amount and discount_percentage: + raise ValidationError( + "You can only set either a discount amount or a discount percentage, not both." + ) + + if not discount_amount and not discount_percentage: + raise ValidationError( + "You must set either a discount amount or a discount percentage." + ) + + if valid_from and valid_to and valid_from >= valid_to: + raise ValidationError( + "The valid_from date must be earlier than the valid_to date." + ) + + return cleaned_data diff --git a/manage_coupons/migrations/0001_initial.py b/manage_coupons/migrations/0001_initial.py new file mode 100644 index 0000000..728c884 --- /dev/null +++ b/manage_coupons/migrations/0001_initial.py @@ -0,0 +1,81 @@ +# Generated by Django 5.0.2 on 2024-07-22 12:20 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Coupon", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("active", models.BooleanField(default=True)), + ("deleted", models.BooleanField(default=False)), + ("created_on", models.DateTimeField(auto_now_add=True)), + ("modified_on", models.DateTimeField(auto_now=True)), + ("title", models.CharField(max_length=255)), + ("coupon_code", models.CharField(max_length=50, unique=True)), + ("no_of_redeems", models.IntegerField(default=0)), + ("description", models.TextField(blank=True, null=True)), + ( + "image", + models.ImageField(blank=True, null=True, upload_to="coupon_img"), + ), + ( + "discount_amount", + models.DecimalField( + blank=True, decimal_places=2, max_digits=10, null=True + ), + ), + ( + "discount_percentage", + models.DecimalField( + blank=True, decimal_places=2, max_digits=5, null=True + ), + ), + ("valid_from", models.DateTimeField()), + ("valid_to", models.DateTimeField()), + ("max_redeems", models.IntegerField(default=0)), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_created", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "modified_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_modified", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "db_table": "coupon", + }, + ), + ] diff --git a/manage_coupons/models.py b/manage_coupons/models.py index 46c267f..f38dad6 100644 --- a/manage_coupons/models.py +++ b/manage_coupons/models.py @@ -2,12 +2,11 @@ from django.db import models from django.utils import timezone from accounts.models import BaseModel, IAmPrincipalType -# Create your models here. - class Coupon(BaseModel): title = models.CharField(max_length=255) coupon_code = models.CharField(max_length=50, unique=True) + no_of_redeems = models.IntegerField(default=0) description = models.TextField(null=True, blank=True) image = models.ImageField(upload_to="coupon_img", null=True, blank=True) discount_amount = models.DecimalField( @@ -16,11 +15,9 @@ class Coupon(BaseModel): discount_percentage = models.DecimalField( max_digits=5, decimal_places=2, null=True, blank=True ) - principal_types = models.ManyToManyField( - IAmPrincipalType, related_name="principal_type_coupons", blank=True - ) valid_from = models.DateTimeField() valid_to = models.DateTimeField() + max_redeems = models.IntegerField(default=0) class Meta: db_table = "coupon" @@ -28,6 +25,13 @@ class Coupon(BaseModel): def __str__(self): return self.coupon_code + # If max_redeems is 0, it means that we are allowing unlimited redeems + def is_valid(self): now = timezone.now() - return self.active and not self.deleted and self.valid_from <= now <= self.valid_to + return ( + self.active + and not self.deleted + and self.valid_from <= now <= self.valid_to + and (self.max_redeems == 0 or self.no_of_redeems < self.max_redeems) + ) diff --git a/manage_coupons/urls.py b/manage_coupons/urls.py index e69de29..9c3001c 100644 --- a/manage_coupons/urls.py +++ b/manage_coupons/urls.py @@ -0,0 +1,23 @@ +from django.urls import path +from . import views + +app_name = "manage_coupons" + +urlpatterns = [ + path("coupon/list/", views.CouponView.as_view(), name="coupon_list"), + path( + "coupon/add/", + views.CouponCreateOrUpdateView.as_view(), + name="coupon_add", + ), + path( + "coupon/edit//", + views.CouponCreateOrUpdateView.as_view(), + name="coupon_edit", + ), + path( + "coupon/delete//", + views.CouponDeleteView.as_view(), + name="coupon_delete", + ), +] diff --git a/manage_coupons/views.py b/manage_coupons/views.py index 91ea44a..2bdb0e4 100644 --- a/manage_coupons/views.py +++ b/manage_coupons/views.py @@ -1,3 +1,114 @@ -from django.shortcuts import render +from django.shortcuts import get_object_or_404, render, redirect +from django.views import generic +from django.contrib.auth.mixins import LoginRequiredMixin +from accounts import resource_action +from django.urls import reverse_lazy +from django.contrib import messages +from goodtimes import constants +from manage_coupons.forms import CouponForm +from manage_coupons.models import Coupon # Create your views here. + + +class CouponView(LoginRequiredMixin, generic.ListView): + page_name = resource_action.RESOURCE_MANAGE_COUPONS + resource = resource_action.RESOURCE_MANAGE_COUPONS + action = resource_action.ACTION_READ + model = Coupon + template_name = "manage_coupons/coupon_list.html" + context_object_name = "coupon_obj" + + def get_queryset(self): + queryset = super().get_queryset().filter(deleted=False, active=True) + return queryset.order_by("-created_on") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["page_name"] = self.page_name + return context + + +class CouponCreateOrUpdateView(LoginRequiredMixin, generic.View): + # Set the page_name and resource + page_name = resource_action.RESOURCE_MANAGE_COUPONS + resource = resource_action.RESOURCE_MANAGE_COUPONS + + # Initialize the action as ACTION_CREATE (can change based on logic) + action = resource_action.ACTION_CREATE + + template_name = "manage_coupons/coupon_add.html" + model = Coupon + form_class = CouponForm + success_url = reverse_lazy("manage_coupons:coupon_list") + error_message = "An error occurred while saving the data." + + # Determine the success message dynamically based on whether it's an update or create + def get_success_message(self): + self.success_message = ( + constants.RECORD_CREATED if not self.object else constants.RECORD_UPDATED + ) + return self.success_message + + # Get the object (if exists) based on URL parameter 'pk' + def get_object(self): + pk = self.kwargs.get("pk") + return get_object_or_404(self.model, pk=pk) if pk else None + + # Add page_name and operation to the context + def get_context_data(self, **kwargs): + context = { + "page_name": self.page_name, + "operation": "Add" if not self.object else "Edit", + } + context.update(kwargs) # Include any additional context data passed to the view + return context + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + + # If an object is found, change action to ACTION_UPDATE + if self.object is not None: + self.action = resource_action.ACTION_UPDATE + + form = self.form_class(instance=self.object) + context = self.get_context_data(form=form) + return render(request, self.template_name, context=context) + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + + # If an object is found, change action to ACTION_UPDATE + if self.object is not None: + self.action = resource_action.ACTION_UPDATE + + form = self.form_class(request.POST, request.FILES, instance=self.object) + if not form.is_valid(): + print(form.errors) + context = self.get_context_data(form=form) + return render(request, self.template_name, context=context) + form.save() + messages.success(self.request, self.get_success_message()) + return redirect(self.success_url) + + +class CouponDeleteView(LoginRequiredMixin, generic.View): + page_name = resource_action.RESOURCE_MANAGE_COUPONS + resource = resource_action.RESOURCE_MANAGE_COUPONS + action = resource_action.ACTION_DELETE + model = Coupon + success_url = reverse_lazy("manage_coupons:coupon_list") + success_message = constants.RECORD_DELETED + error_message = constants.RECORD_NOT_FOUND + + def get(self, request, pk): + try: + type_obj = self.model.objects.get(id=pk) + type_obj.deleted = True + type_obj.active = False + type_obj.save() + messages.success(request, self.success_message) + except self.model.DoesNotExist: + messages.success(request, self.error_message) + + return redirect(self.success_url) diff --git a/manage_subscriptions/forms.py b/manage_subscriptions/forms.py index 79ed3ac..65701fa 100644 --- a/manage_subscriptions/forms.py +++ b/manage_subscriptions/forms.py @@ -1,4 +1,5 @@ from django import forms +from accounts.models import IAmPrincipalType from manage_subscriptions.models import PrincipalSubscription, Subscription, Plan @@ -31,7 +32,15 @@ class SubscriptionForm(forms.ModelForm): "active", "deleted", "is_free", - ] # Include all fields you want from the model + ] + + def __init__(self, *args, **kwargs): + super(SubscriptionForm, self).__init__(*args, **kwargs) + event_user = IAmPrincipalType.objects.get(name="event_user") + event_manager = IAmPrincipalType.objects.get(name="event_manager") + self.fields["principal_types"].queryset = IAmPrincipalType.objects.filter( + id__in=[event_user.id, event_manager.id] + ) class PrincipalSubscriptionForm(forms.ModelForm): diff --git a/manage_subscriptions/migrations/0009_principalsubscription_coupon_code.py b/manage_subscriptions/migrations/0009_principalsubscription_coupon_code.py new file mode 100644 index 0000000..b693abb --- /dev/null +++ b/manage_subscriptions/migrations/0009_principalsubscription_coupon_code.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.2 on 2024-07-22 12:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("manage_subscriptions", "0008_subscription_is_free"), + ] + + operations = [ + migrations.AddField( + model_name="principalsubscription", + name="coupon_code", + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/manage_subscriptions/models.py b/manage_subscriptions/models.py index 326f7df..4e8e1da 100644 --- a/manage_subscriptions/models.py +++ b/manage_subscriptions/models.py @@ -71,6 +71,7 @@ class PrincipalSubscription(BaseModel): payment_intent_client_secret = models.CharField( max_length=255, null=True, blank=True ) + coupon_code = models.CharField(max_length=255, null=True, blank=True) class Meta: db_table = "principal_subscription" diff --git a/manage_subscriptions/urls.py b/manage_subscriptions/urls.py index aa767f8..20210bf 100644 --- a/manage_subscriptions/urls.py +++ b/manage_subscriptions/urls.py @@ -70,6 +70,11 @@ urlpatterns = [ views.create_checkout_session, name="create_checkout_session", ), + path( + "coupon-validity-check/", + views.validate_coupon, + name="validate_coupon", + ), path("stripe/", views.SubscriptionPageView.as_view(), name="stripe"), path("success/", views.SuccessView.as_view(), name="success"), path("cancel/", views.CancelView.as_view(), name="cancel"), diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 7a91d56..17e5f8d 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -1,18 +1,16 @@ -from datetime import timedelta +from decimal import Decimal import json -from django.http import HttpResponseBadRequest, JsonResponse, HttpResponseRedirect +from django.http import HttpResponseBadRequest, JsonResponse from django.shortcuts import get_object_or_404, redirect, render import stripe from accounts import resource_action from accounts.models import IAmPrincipal -from goodtimes.utils import ApiResponse -from django.contrib.auth import authenticate, login +from django.contrib.auth import login import jwt -from rest_framework_simplejwt.tokens import AccessToken from django.utils import timezone from django.contrib.auth import get_user_model +from manage_coupons.models import Coupon from manage_subscriptions.forms import ( - PlanForm, SubscriptionForm, PrincipalSubscriptionForm, ) @@ -24,7 +22,6 @@ from manage_wallets.models import ( ) from .models import Plan, Subscription, PrincipalSubscription from django.views import generic -from rest_framework import status from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse_lazy from django.contrib import messages @@ -32,12 +29,10 @@ from goodtimes import constants from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from django.conf import settings -from rest_framework.permissions import IsAuthenticated from django.views.generic.base import TemplateView - +from django.db.models import Q # Create your views here. -from django.db.models import Q class SubscriptionCreateOrUpdateView(LoginRequiredMixin, generic.View): # Set the page_name and resource @@ -101,7 +96,10 @@ class SubscriptionCreateOrUpdateView(LoginRequiredMixin, generic.View): # This code ensures that only one free plan can be created by checking for existing free plans before saving a new one. if form.cleaned_data.get("is_free"): if self.model.objects.filter(Q(is_free=True) & Q(active=True)).exists(): - messages.error(self.request, "A free plan is already available. Please deactivate the existing one before creating a new one.") + messages.error( + self.request, + "A free plan is already available. Please deactivate the existing one before creating a new one.", + ) context = self.get_context_data(form=form) return render(request, self.template_name, context=context) @@ -119,7 +117,12 @@ class SubscriptionView(LoginRequiredMixin, generic.ListView): context_object_name = "subscription_obj" def get_queryset(self): - queryset = super().get_queryset().filter(deleted=False, active=True).prefetch_related("principal_types") + queryset = ( + super() + .get_queryset() + .filter(deleted=False, active=True) + .prefetch_related("principal_types") + ) return queryset.order_by("-created_on") def get_context_data(self, **kwargs): @@ -395,15 +398,19 @@ class SubscriptionPageView(TemplateView): if request.user.is_authenticated: print("request.user: ", request.user) subscriptions = Subscription.objects.filter( - principal_types=request.user.principal_type, active=True, deleted=False, is_free=False + principal_types=request.user.principal_type, + active=True, + deleted=False, + is_free=False, ) if subscriptions.exists(): context["subscriptions"] = subscriptions context["stripeCheckoutUrl"] = settings.STRIPE_CHECKOUT_URL context["stripeFinalUrl"] = settings.STRIPE_FINAL_URL + context["couponValidityCheckUrl"] = settings.COUPON_VALIDITY_CHECK_URL else: - # Handle the case where no subscriptions are found for the principal type. + # Handling the case where no subscriptions are found for the principal type. context["error"] = "No subscriptions found for your user type." return context @@ -420,18 +427,37 @@ def stripe_config(request): def validate_coupon(request): data = json.loads(request.body) coupon_code = data.get("couponCode", None) + subscription_id = data.get("subscriptionId", None) - # Validate coupon code + try: + subscription = Subscription.objects.get(id=subscription_id) + except Subscription.DoesNotExist: + return JsonResponse({"error": "Subscription not found."}, status=404) + + # Validating Coupon if coupon_code: try: - coupon = stripe.Coupon.retrieve(coupon_code) - if coupon["valid"] is False: - return JsonResponse({"error": "Invalid or expired coupon code."}, status=400) - return JsonResponse({"success": "Coupon code is valid."}) - except stripe.error.InvalidRequestError: - return JsonResponse({"error": "Invalid coupon code."}, status=400) - else: - return JsonResponse({"error": "Coupon code not provided."}, status=400) + coupon = Coupon.objects.get(coupon_code=coupon_code) + if not coupon.is_valid(): + return JsonResponse({"error": "Coupon is not valid."}, status=400) + if coupon.discount_amount and coupon.discount_amount > subscription.amount: + return JsonResponse( + {"error": "Coupon discount amount exceeds subscription amount."}, + status=400, + ) + if ( + coupon.discount_percentage + and (coupon.discount_percentage / Decimal("100")) * subscription.amount + > subscription.amount + ): + return JsonResponse( + { + "error": "Coupon discount percentage exceeds subscription amount." + }, + status=400, + ) + except Coupon.DoesNotExist: + return JsonResponse({"error": "Coupon not found."}, status=404) @csrf_exempt @@ -442,6 +468,7 @@ def create_checkout_session(request): data = json.loads(request.body) print("data: ", data) subscription_id = data.get("subscriptionId", None) + coupon_code = data.get("couponCode", None) principal_id = request.user.id try: @@ -453,7 +480,28 @@ def create_checkout_session(request): "order_" + str(timezone.localtime().timestamp()) + str(request.user.email) ) print("order_id: ", order_id) - + # Calculating the final amount after applying the coupon discount + final_amount = subscription.amount + coupon = None + if coupon_code: + try: + coupon = Coupon.objects.get(coupon_code=coupon_code) + if coupon.is_valid(): + if coupon.discount_amount: + final_amount -= coupon.discount_amount + elif coupon.discount_percentage: + final_amount -= final_amount * ( + coupon.discount_percentage / Decimal("100") + ) + final_amount = max( + 0, final_amount + ) # Ensuring the amount is not negative + else: + return JsonResponse( + {"error": "Invalid or expired coupon code."}, status=400 + ) + except Coupon.DoesNotExist: + return JsonResponse({"error": "Coupon not found."}, status=404) # Create a Transaction object with status INITIATE transaction = Transaction.objects.create( principal=request.user, @@ -461,7 +509,7 @@ def create_checkout_session(request): transaction_type=TransactionType.PAYMENT, # or PAYMENT, as applicable payment_method=PaymentMethod.CARD, # Assuming CARD for this example transaction_status=TransactionStatus.INITIATE, - amount=subscription.amount, # Fetching amount from the Subscription object + amount=final_amount, # Fetching amount from the Subscription object order_id=order_id, comment="Principal Subscription Initiated", ) @@ -492,7 +540,7 @@ def create_checkout_session(request): "name": subscription.title, # Adjust with your subscription/product name }, "unit_amount": int( - subscription.amount * 100 + final_amount * 100 ), # Unit amount in cents/pence "tax_behavior": "inclusive", # or 'exclusive', based on your tax settings }, @@ -509,6 +557,7 @@ def create_checkout_session(request): "order_id": str(order_id), "subscription_id": str(subscription.id), "transaction_id": str(transaction.id), + "couponCode": str(coupon.coupon_code) if coupon else None, # "principal_subscription_id": str(principal_subscription.id), }, ) @@ -523,38 +572,3 @@ class SuccessView(TemplateView): class CancelView(TemplateView): template_name = "stripe_html/cancel.html" - - -# class IndexView(TemplateView): -# template_name = "stripe_html/index.html" - -# def get(self, request, *args, **kwargs): -# # Example of extracting the token from a query parameter or cookie -# token = request.GET.get("token") -# # token = request.GET.get("token") or request.COOKIES.get("jwt") -# print("token: ", token) -# if token: -# try: -# # Decode and validate token -# payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) -# print("payload: ", payload) -# try: -# UserModel = get_user_model() -# user = UserModel.objects.get(id=payload["user_id"]) -# # Manually specify the authentication backend -# user.backend = "django.contrib.auth.backends.ModelBackend" -# # Log the user in -# login(request, user) -# print("Logged in user: ", user) - -# except IAmPrincipal.DoesNotExist: -# # Handle expired token -# return HttpResponseBadRequest("No Principal Found") - -# except jwt.ExpiredSignatureError: -# # Handle expired token -# return HttpResponseBadRequest("Expired Signature Error") -# except jwt.InvalidTokenError: -# return HttpResponseBadRequest("Invalid Token Error") - -# return super().get(request, *args, **kwargs) diff --git a/templates/elements/sidebar.html b/templates/elements/sidebar.html index bd845d0..a493e8a 100644 --- a/templates/elements/sidebar.html +++ b/templates/elements/sidebar.html @@ -154,6 +154,17 @@
    {% endif %} + {% if user|has_resource_permission:resource_context.RESOURCE_MANAGE_COUPONS %} + + {% endif %} {% if user|has_resource_permission:resource_context.RESOURCE_PRINCIPAL_SUBSCRIPTIONS %}
    -
    - - -{% endblock content %} - -{% block javascript %} - - - {% include "cdn_through_html/filepond_cdn_js.html" %} - {% include "cdn_through_html/quill_cdn_js.html" %} - {% include "cdn_through_html/tagify_cdn_js.html" %} - - - -{% endblock %} \ No newline at end of file diff --git a/templates/manage_stock/stock_list.html b/templates/manage_stock/stock_list.html deleted file mode 100644 index 8eaf53f..0000000 --- a/templates/manage_stock/stock_list.html +++ /dev/null @@ -1,237 +0,0 @@ -{% extends 'layout/base_template.html' %} -{% load static %} -{% block stylesheet %} - - {% include "cdn_through_html/datatable_cdn_css.html" %} - {% include "cdn_through_html/tabs_cdn_css.html" %} - -{% endblock %} - -{% block content %} - -
    -
    -
    -
    -

    Manage Stock

    -
    -
    - {% comment %} {% endcomment %} - Add Stock Index - Add Stock -
    -
    - -
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - - - - - - - - - - - - {% for data_obj in stock_obj %} - - - - - - - - - - - {% endfor %} - - -
    Record Id CategoryTitleActiveAction
    {{data_obj.id}}{{data_obj.index_type.title}}{{data_obj.title}} - {{data_obj.active}} - - -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - - -{% endblock content %} - -{% block javascript %} - - {% include "cdn_through_html/datatable_cdn_js.html" %} - - -{% endblock %} \ No newline at end of file diff --git a/templates/manage_stock/stock_price_list.html b/templates/manage_stock/stock_price_list.html deleted file mode 100644 index 41fdc58..0000000 --- a/templates/manage_stock/stock_price_list.html +++ /dev/null @@ -1,175 +0,0 @@ -{% extends 'layout/base_template.html' %} -{% load static %} -{% block stylesheet %} - - {% include "cdn_through_html/datatable_cdn_css.html" %} - {% include "cdn_through_html/tabs_cdn_css.html" %} - -{% endblock %} - -{% block content %} - -
    -
    -
    -
    -

    Manage Stock

    -
    -
    - {% comment %} {% endcomment %} - Add Stock Index - Add Stock -
    -
    - -
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - - - - - - - - - - - {% for data_obj in stock_price_obj %} - - - - - - - - - - {% endfor %} - - -
    Record Id CategoryTitleAction
    {{data_obj.id}}{{data_obj.index_type.title}}{{data_obj.title}}
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - - -{% endblock content %} - -{% block javascript %} - - {% include "cdn_through_html/datatable_cdn_js.html" %} - - -{% endblock %} \ No newline at end of file diff --git a/templates/manage_stock/team_stock.html b/templates/manage_stock/team_stock.html deleted file mode 100644 index a112274..0000000 --- a/templates/manage_stock/team_stock.html +++ /dev/null @@ -1,156 +0,0 @@ -{% extends 'layout/base_template.html' %} -{% load static %} -{% block stylesheet %} - - {% include "cdn_through_html/datatable_cdn_css.html" %} - -{% endblock %} - -{% block content %} - -
    -
    -
    -
    -

    {{data_objs.0.team.title}}

    -
    -
    - - {% comment %} Example form {% endcomment %} - {% comment %} Add Entry Fee {% endcomment %} -
    -
    - - -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    - -{% endblock content %} - -{% block javascript %} - - {% include "cdn_through_html/datatable_cdn_js.html" %} - - -{% endblock %} \ No newline at end of file diff --git a/templates/manage_subscriptions/subscription_add.html b/templates/manage_subscriptions/subscription_add.html index 8da8df6..d2cdba4 100644 --- a/templates/manage_subscriptions/subscription_add.html +++ b/templates/manage_subscriptions/subscription_add.html @@ -67,69 +67,3 @@ {% endblock content %} - -{% block javascript %} - - - {% include "cdn_through_html/filepond_cdn_js.html" %} - {% include "cdn_through_html/quill_cdn_js.html" %} - {% include "cdn_through_html/tagify_cdn_js.html" %} - - - -{% endblock %} \ No newline at end of file diff --git a/templates/stripe_html/index.html b/templates/stripe_html/index.html index 1d48216..0a30333 100644 --- a/templates/stripe_html/index.html +++ b/templates/stripe_html/index.html @@ -130,8 +130,9 @@
    - + + +
    @@ -518,38 +519,77 @@ console.log("Sanity check!"); var stripeCheckoutUrl = "{{ stripeCheckoutUrl }}"; var stripeFinalUrl = "{{ stripeFinalUrl }}"; + var couponValidityCheckUrl = "{{ couponValidityCheckUrl }}"; console.log("stripeCheckoutUrl: ", stripeCheckoutUrl); console.log("stripeFinalUrl: ", stripeFinalUrl); - // Get Stripe publishable key + console.log("couponValidityCheckUrl: ", couponValidityCheckUrl); + // Geting Stripe publishable key fetch(stripeCheckoutUrl) - .then((result) => { return result.json(); }) + .then((result) => { + return result.json(); + }) .then((data) => { - // Initialize Stripe.js + // Initializing Stripe.js -- getting stripe public key to generate stripe object for creating checkout session const stripe = Stripe(data.publicKey); console.log("loaded stripe public key"); document.querySelectorAll(".subscribe-btn").forEach(button => { button.addEventListener("click", () => { const subscriptionId = button.getAttribute("data-subscription-id"); const couponCode = button.previousElementSibling.value; + const errorMessageContainer = button.nextElementSibling; console.log("subscriptionId: ", subscriptionId); console.log("couponCode: ", couponCode); button.disabled = true; button.previousElementSibling.value = ""; - // Create checkout session for the selected subscription - fetch(stripeFinalUrl, { - method: "POST", - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ subscriptionId: subscriptionId }), - }) - .then((result) => { return result.json(); }) - .then((data) => { - // Redirect to Stripe Checkout - return stripe.redirectToCheckout({ sessionId: data.sessionId }) + + // Handling any coupon validation errors here before creating stripe final checkout session + fetch(couponValidityCheckUrl, { + method: "POST", + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + subscriptionId: subscriptionId, + couponCode: couponCode + }), + }) + .then(response => { + if (!response.ok) { + return response.json().then(data => { + throw new Error(data.error); + }); + } + return response.json(); + }) + .then(data => { + // Creating checkout session for the selected subscription + fetch(stripeFinalUrl, { + method: "POST", + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + subscriptionId: subscriptionId + }), + }) + .then((result) => { + return result.json(); + }) + .then((data) => { + // Redirects to Stripe Checkout + return stripe.redirectToCheckout({ + sessionId: data.sessionId + }) + }) + .catch((error) => { + console.error("Error:", error); + button.disabled = false; + }); }) .catch((error) => { console.error("Error:", error); + errorMessageContainer.style.display = 'block'; + errorMessageContainer.innerText = error.message; button.disabled = false; }); }); From 33030ca7281e3319704b21a64f51d6805adf48b1 Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Thu, 25 Jul 2024 00:48:24 +0530 Subject: [PATCH 075/187] feat(socialmedia):post event to social media --- goodtimes/services.py | 262 ++++++++++++++++-- goodtimes/settings/base.py | 11 +- .../management/commands/test_facebook_api.py | 31 +++ .../management/commands/test_instagram_api.py | 34 +++ .../management/commands/test_twitter_api.py | 28 ++ manage_events/urls.py | 2 + manage_events/views.py | 50 ++++ static/img/facebook.png | Bin 0 -> 3210 bytes static/img/forward_all_icon.png | Bin 0 -> 8660 bytes static/img/instagram.png | Bin 0 -> 38538 bytes static/img/x_twitter.png | Bin 0 -> 16807 bytes templates/manage_events/event_details.html | 150 +++++++++- 12 files changed, 544 insertions(+), 24 deletions(-) create mode 100644 manage_events/management/commands/test_facebook_api.py create mode 100644 manage_events/management/commands/test_instagram_api.py create mode 100644 manage_events/management/commands/test_twitter_api.py create mode 100644 static/img/facebook.png create mode 100644 static/img/forward_all_icon.png create mode 100644 static/img/instagram.png create mode 100644 static/img/x_twitter.png diff --git a/goodtimes/services.py b/goodtimes/services.py index ad2fcf7..221b622 100644 --- a/goodtimes/services.py +++ b/goodtimes/services.py @@ -1,4 +1,5 @@ import random +import requests import googlemaps import tweepy from django.conf import settings @@ -826,21 +827,31 @@ class TwitterAPI: self.api_secret_key = settings.TWITTER_API_SECRET_KEY self.access_token = settings.TWITTER_ACCESS_TOKEN self.access_token_secret = settings.TWITTER_ACCESS_TOKEN_SECRET - self.auth = self._setup_auth() - self.api = self._setup_api() - - def _setup_auth(self): - auth = tweepy.OAuthHandler(self.api_key, self.api_secret_key) - auth.set_access_token(self.access_token, self.access_token_secret) - return auth + self.client, self.api = self._setup_api() def _setup_api(self): - api = tweepy.API(self.auth) - return api + client = tweepy.Client( + consumer_key=self.api_key, + consumer_secret=self.api_secret_key, + access_token=self.access_token, + access_token_secret=self.access_token_secret, + ) + auth = tweepy.OAuth1UserHandler( + self.api_key, + self.api_secret_key, + self.access_token, + self.access_token_secret, + ) + api = tweepy.API(auth, wait_on_rate_limit=True) + return client, api - def post_image_with_caption(self, image_path, caption): - media = self.api.media_upload(image_path) - tweet = self.api.update_status(status=caption, media_ids=[media.media_id]) + def post_text_tweet(self, caption): + tweet = self.client.create_tweet(text=caption) + return tweet + + def post_image_with_caption(self, image_url, caption): + media = self.api.media_upload(image_url) + tweet = self.client.create_tweet(text=caption, media_ids=[media.media_id]) return tweet @@ -848,9 +859,228 @@ class TwitterPoster: def __init__(self, twitter_api): self.twitter_api = twitter_api - def post_tweet(self, image_path, caption): + def post_text_tweet(self, caption): try: - tweet = self.twitter_api.post_image_with_caption(image_path, caption) + tweet = self.twitter_api.post_text_tweet(caption) return {'success': True, 'message': 'Tweet posted successfully!'} - except tweepy.TweepError as e: - return {'success': False, 'message': f'Error posting tweet: {e}'} \ No newline at end of file + except tweepy.TweepyException as e: + return {'success': False, 'message': f'Error posting tweet: {e}'} + + def post_image_with_caption(self, image_url, caption): + try: + tweet = self.twitter_api.post_image_with_caption(image_url, caption) + return {'success': True, 'message': 'Tweet posted successfully!'} + except tweepy.TweepyException as e: + return {'success': False, 'message': f'Error posting tweet: {e}'} + +class FacebookAPI: + def __init__(self): + 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 + + def post_photo(self, image_url, caption): + if not self.page_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" + params = { + "message": caption, + "url": image_url, + "access_token": self.page_access_token, + } + response = requests.post(url, params=params) + response.raise_for_status() + result = response.json() + if "id" not in result: + print(f"Error posting photo: {result}") + return False + print(f"Data posted successfully. Post Id: {result['id']}") + return True + except requests.exceptions.RequestException as e: + print(f"Error posting photo: {e}") + return False + +class FacebookPoster: + def __init__(self, facebook_api): + 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'} + return {'success': True, 'message': 'Photo posted successfully'} + + + +# import requests + +# app_id = "YOUR_APP_ID" +# app_secret = "YOUR_APP_SECRET" +# page_id = "YOUR_PAGE_ID" # You need to specify the page ID +# # Step 1: Get an App Access Token +# response = requests.get(f"https://graph.facebook.com/oauth/access_token?client_id={app_id}&client_secret={app_secret}&grant_type=client_credentials") +# app_access_token = response.json()["access_token"] + +# # Step 2: Get a Page Access Token +# response = requests.get(f"https://graph.facebook.com/{page_id}?fields=access_token&access_token={app_access_token}") +# page_access_token = response.json()["access_token"] + +# # Use the Page Access Token to query the Page node +# response = requests.get(f"https://graph.facebook.com/{page_id}?access_token={page_access_token}") +# page_data = response.json() +# print(page_data) +class InstagramAPI: + def __init__(self): + self.app_id = settings.FACEBOOK_APP_ID + self.app_secret = settings.FACEBOOK_APP_SECRET + self.page_id = settings.INSTAGRAM_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" + params = { + "grant_type": "client_credentials", + "client_id": self.app_id, + "client_secret": self.app_secret + } + response = requests.get(url, params=params) + 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" + 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() + 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): + try: + url = f"https://graph.facebook.com/{self.page_id}" + params = { + "fields": "access_token", + "access_token": long_lived_token + } + response = requests.get(url, params=params) + response.raise_for_status() + print(f"Page access token: {response.json()}") + return response.json()["access_token"] + except requests.exceptions.RequestException as e: + print(f"Error getting page access token: {e}") + return None + + 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.page_access_token = self._get_page_access_token(long_lived_token) + self.page_access_token = settings.FACEBOOK_ACCESS_TOKEN + return True + + def post_image_with_caption(self, image_path, caption): + image_path="https://admin.goodtimesltd.co.uk/static/img/goodtimes.png" + if not self.page_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}/media" + params = { + "caption": caption, + "image_url": image_path, + "access_token": self.page_access_token + } + response = requests.post(url, data=params) + response.raise_for_status() + result = response.json() + print(f"Post image with caption result: {result['id']}") + + url = f"https://graph.facebook.com/v20.0/{self.page_id}/media_publish" + params = { + "creation_id": result["id"], + "access_token": self.page_access_token + } + response = requests.post(url, params=params) + response.raise_for_status() + result = response.json() + return True + except requests.exceptions.RequestException as e: + print(f"Error posting photo on instagram: {e}") + return False + + +class InstagramPoster: + def __init__(self, instagram_api): + self.instagram_api = instagram_api + + def post_image_with_caption(self, image_path, caption): + if not self.instagram_api.authenticate(): + print("Instagram API authentication failed.") + return {'success': False, 'message': 'Error posting photo. Authenticate failed'} + result = self.instagram_api.post_image_with_caption(image_path, caption) + if not result: + return {'success': False, 'message': 'Error posting photo.'} + return {'success': True, 'message': 'Photo posted successfully'} \ No newline at end of file diff --git a/goodtimes/settings/base.py b/goodtimes/settings/base.py index ee53e38..b6abe36 100644 --- a/goodtimes/settings/base.py +++ b/goodtimes/settings/base.py @@ -344,4 +344,13 @@ PLACES_MARKER_OPTIONS = '{"draggable": true}' TWITTER_API_KEY = env.str("TWITTER_API_KEY") TWITTER_API_SECRET_KEY = env.str("TWITTER_API_SECRET_KEY") TWITTER_ACCESS_TOKEN = env.str("TWITTER_ACCESS_TOKEN") -TWITTER_ACCESS_TOKEN_SECRET = env.str("TWITTER_ACCESS_TOKEN_SECRET") \ No newline at end of file +TWITTER_ACCESS_TOKEN_SECRET = env.str("TWITTER_ACCESS_TOKEN_SECRET") + +# facebook keys +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_ACCESS_TOKEN = env.str("FACEBOOK_ACCESS_TOKEN") + +# Instagram Key +INSTAGRAM_PAGE_ID = env.str('INSTAGRAM_PAGE_ID') diff --git a/manage_events/management/commands/test_facebook_api.py b/manage_events/management/commands/test_facebook_api.py new file mode 100644 index 0000000..efbc3c5 --- /dev/null +++ b/manage_events/management/commands/test_facebook_api.py @@ -0,0 +1,31 @@ +import os +from django.conf import settings +from django.core.management.base import BaseCommand +from goodtimes.services import FacebookAPI, FacebookPoster +from ...models import Event + +class Command(BaseCommand): + help = 'Test facebook posting functionality' + + def handle(self, *args, **kwargs): + event = Event.objects.get(id=20) + if not event: + self.stdout.write(self.style.ERROR("No event found.")) + + if not event.image: + self.stdout.write(self.style.ERROR("No image found.")) + + base_domain = settings.BASE_DOMAIN + image_path = f"{base_domain}{event.image.url}" + print(f"complete path of image {image_path}") + caption = f"{event.title}\nDuration: {event.start_date} to {event.end_date}\nAddress: {event.venue.address}" + + facebook_api = FacebookAPI() + facebook_poster = FacebookPoster(facebook_api) + + response = facebook_poster.post_photo(image_path, caption) + + if response['success']: + self.stdout.write(self.style.SUCCESS(response['message'])) + else: + self.stdout.write(self.style.ERROR(response['message'])) diff --git a/manage_events/management/commands/test_instagram_api.py b/manage_events/management/commands/test_instagram_api.py new file mode 100644 index 0000000..5717284 --- /dev/null +++ b/manage_events/management/commands/test_instagram_api.py @@ -0,0 +1,34 @@ +import os +from django.conf import settings +from django.core.management.base import BaseCommand +from goodtimes.services import InstagramAPI, InstagramPoster +from ...models import Event +import urllib.request + +class Command(BaseCommand): + help = 'Test Instagram posting functionality' + + def handle(self, *args, **kwargs): + event = Event.objects.get(id=20) + if not event: + self.stdout.write(self.style.ERROR("No event found.")) + + if not event.image: + self.stdout.write(self.style.ERROR("No image found.")) + + # base_domain = settings.BASE_DOMAIN + # image_path = f"{base_domain}{event.image.url}" + image_path = event.image.url + print(f"complete path of image {image_path}") + + caption = f"{event.title}\nDuration: {event.start_date} to {event.end_date}\nAddress: {event.venue.address}" + + instagram_api = InstagramAPI() + instagram_poster = InstagramPoster(instagram_api) + + response = instagram_poster.post_image_with_caption(image_path, caption) + + if response['success']: + self.stdout.write(self.style.SUCCESS(response['message'])) + else: + self.stdout.write(self.style.ERROR(response['message'])) diff --git a/manage_events/management/commands/test_twitter_api.py b/manage_events/management/commands/test_twitter_api.py new file mode 100644 index 0000000..2c79553 --- /dev/null +++ b/manage_events/management/commands/test_twitter_api.py @@ -0,0 +1,28 @@ +import os +from django.core.management.base import BaseCommand +from goodtimes.services import TwitterAPI, TwitterPoster +from ...models import Event + +class Command(BaseCommand): + help = 'Test Twitter posting functionality' + + def handle(self, *args, **kwargs): + event = Event.objects.get(id=19) + if not event: + self.stdout.write(self.style.ERROR("No event found.")) + + if not event.image: + self.stdout.write(self.style.ERROR("No image found.")) + + image_path = event.image.path + caption = f"{event.title}\nDuration: {event.start_date} to {event.end_date}\nAddress: {event.venue.address}" + + twitter_api = TwitterAPI() + twitter_poster = TwitterPoster(twitter_api) + + response = twitter_poster.post_image_with_caption(image_path, caption) + + if response['success']: + self.stdout.write(self.style.SUCCESS(response['message'])) + else: + self.stdout.write(self.style.ERROR(response['message'])) diff --git a/manage_events/urls.py b/manage_events/urls.py index bcc3182..1f52b7b 100644 --- a/manage_events/urls.py +++ b/manage_events/urls.py @@ -99,4 +99,6 @@ urlpatterns = [ views.GenerateEventReportView.as_view(), name="generate_event_report", ), + + path("post-to-social-media///", views.SocialMediaPostView.as_view(), name="social_media_post") ] diff --git a/manage_events/views.py b/manage_events/views.py index 45184b1..2381ede 100644 --- a/manage_events/views.py +++ b/manage_events/views.py @@ -1,5 +1,6 @@ from django.shortcuts import get_object_or_404, redirect, render from accounts import resource_action +from goodtimes.services import FacebookAPI, FacebookPoster, InstagramAPI, InstagramPoster, TwitterAPI, TwitterPoster from goodtimes.utils import JsonResponseUtil from manage_events.api.serializers import VenueSerializer, VenueShortSerializer from manage_events.forms import ( @@ -553,3 +554,52 @@ class GenerateEventReportView(generic.View): response["Content-Disposition"] = f'attachment; filename="{filename}"' return response + +class SocialMediaPostView(generic.View): + def get(self, request, *args, **kwargs): + platform = kwargs.get("platform") + event_id = kwargs.get("id") + print(platform, event_id) + errors = [] + + try: + event = Event.objects.get(id=event_id) + except Event.DoesNotExist: + errors.append("Event does not exist") + return JsonResponseUtil.error(message=errors, errors=errors) + + if not event.active: + errors.append("Event is not active") + return JsonResponseUtil.error(message=errors, errors=errors) + + caption = f"{event.title}\nDuration: {event.start_date} to {event.end_date}\nAddress: {event.venue.address}" + print(f"image url and caption is {caption}") + if platform in ['instagram', 'facebook', 'twitter', 'all']: + + if platform in ['twitter', 'all']: + image_url = event.image.path + twitter_api = TwitterAPI() + twitter_poster = TwitterPoster(twitter_api) + result = twitter_poster.post_image_with_caption(image_url, caption) + if not result['success']: + errors.append(result['message']) + + image_url = request.build_absolute_uri(event.image.url) # fb and insta require complete path with domain + if platform in ['facebook', 'all']: + facebook_api = FacebookAPI() + facebook_poster = FacebookPoster(facebook_api) + result = facebook_poster.post_photo(image_url, caption) + if not result["success"]: + errors.append(result["message"]) + + if platform in ['instagram', 'all']: + instagram_api = InstagramAPI() + instagram_poster = InstagramPoster(instagram_api) + result = instagram_poster.post_image_with_caption(image_url, caption) + if not result["success"]: + errors.append(result["message"]) + + if not errors: + return JsonResponseUtil.success(message='Post Successful') + + return JsonResponseUtil.error(message=errors, errors=errors) diff --git a/static/img/facebook.png b/static/img/facebook.png new file mode 100644 index 0000000000000000000000000000000000000000..8400a87c25206fd2acabf17131bced63dc4b30e4 GIT binary patch literal 3210 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 literal 0 HcmV?d00001 diff --git a/static/img/forward_all_icon.png b/static/img/forward_all_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6b6f4d16be602a95fdfb2b47f9cef224abf5ec60 GIT binary patch literal 8660 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 literal 0 HcmV?d00001 diff --git a/static/img/instagram.png b/static/img/instagram.png new file mode 100644 index 0000000000000000000000000000000000000000..b61a96570af161afc8681ad9e85250982dfd434e GIT binary patch literal 38538 zcmW(+WkAzk7yhka^ypMZOSgo?Mu(K7fTSWFf^=+j2`GYuzyw4>%0DF%qXdIiLApa) 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 z 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 IM6N<$g3V}M0ssI2 literal 0 HcmV?d00001 diff --git a/static/img/x_twitter.png b/static/img/x_twitter.png new file mode 100644 index 0000000000000000000000000000000000000000..62887fd359d5462e1b6d33537bf18aad921f1d94 GIT binary patch literal 16807 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 Z8igCOqRhF^1LX!$R#dy5d(Gte{{u(UEV%#x literal 0 HcmV?d00001 diff --git a/templates/manage_events/event_details.html b/templates/manage_events/event_details.html index e6fff65..1fa11b3 100644 --- a/templates/manage_events/event_details.html +++ b/templates/manage_events/event_details.html @@ -2,12 +2,48 @@ {% load static %} {% block stylesheet %} + {% include "cdn_through_html/filepond_cdn_css.html" %} {% include "cdn_through_html/quill_cdn_css.html" %} {% include "cdn_through_html/tagify_cdn_css.html" %} +{% include "cdn_through_html/sweetalert2_cdn_css.html" %} {{form.media}} + + {% endblock %} {% block content %} @@ -25,15 +61,41 @@ {% endif %}
    -
    -
    -

    {{ event.brand.title }}

    -

    {{ event.title }}

    -

    Description: {{ event.description }}

    -

    Created By: {{ event.created_by }}

    +
    + +
    @@ -145,4 +207,78 @@
    -{% endblock content %} \ No newline at end of file +{% endblock content %} + +{% block javascript %} + + {% include "cdn_through_html/sweetalert2_cdn_js.html" %} + +{%endblock javascript%} \ No newline at end of file From a68f114f990ced88c218f11b59fe6535f228e215 Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Thu, 25 Jul 2024 16:45:20 +0530 Subject: [PATCH 076/187] refactor(social media):change message of success and failure in social media response --- goodtimes/services.py | 23 ++++---- goodtimes/utils.py | 2 - .../management/commands/test_facebook_api.py | 3 +- .../management/commands/test_instagram_api.py | 5 +- manage_events/views.py | 53 ++++++++++++++----- templates/manage_events/event_details.html | 29 ++++++++-- 6 files changed, 77 insertions(+), 38 deletions(-) diff --git a/goodtimes/services.py b/goodtimes/services.py index 221b622..c28b41c 100644 --- a/goodtimes/services.py +++ b/goodtimes/services.py @@ -884,7 +884,7 @@ class FacebookAPI: 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() + # response.raise_for_status() print(f"short lived token {response.json()}") return response.json()['access_token'] except requests.exceptions.RequestException as e: @@ -895,7 +895,7 @@ class FacebookAPI: 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() + # response.raise_for_status() print(f"long lived access token : {response.json()}") return response.json()['access_token'] except requests.exceptions.RequestException as e: @@ -932,14 +932,14 @@ class FacebookAPI: "access_token": self.page_access_token, } response = requests.post(url, params=params) - response.raise_for_status() + # response.raise_for_status() result = response.json() if "id" not in result: print(f"Error posting photo: {result}") return False print(f"Data posted successfully. Post Id: {result['id']}") return True - except requests.exceptions.RequestException as e: + except Exception as e: print(f"Error posting photo: {e}") return False @@ -953,7 +953,7 @@ class FacebookPoster: 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'} + return {'success': False, 'message': 'Error posting photo in Facebook'} return {'success': True, 'message': 'Photo posted successfully'} @@ -1026,7 +1026,7 @@ class InstagramAPI: response.raise_for_status() print(f"Page access token: {response.json()}") return response.json()["access_token"] - except requests.exceptions.RequestException as e: + except Exception as e: print(f"Error getting page access token: {e}") return None @@ -1042,7 +1042,6 @@ class InstagramAPI: return True def post_image_with_caption(self, image_path, caption): - image_path="https://admin.goodtimesltd.co.uk/static/img/goodtimes.png" if not self.page_access_token: print("Page access token not obtained. Call Authenticate() first.") return False @@ -1054,9 +1053,9 @@ class InstagramAPI: "access_token": self.page_access_token } response = requests.post(url, data=params) - response.raise_for_status() + # response.raise_for_status() result = response.json() - print(f"Post image with caption result: {result['id']}") + print(f"Post image with caption result: {result}") url = f"https://graph.facebook.com/v20.0/{self.page_id}/media_publish" params = { @@ -1064,10 +1063,10 @@ class InstagramAPI: "access_token": self.page_access_token } response = requests.post(url, params=params) - response.raise_for_status() + # response.raise_for_status() result = response.json() return True - except requests.exceptions.RequestException as e: + except Exception as e: print(f"Error posting photo on instagram: {e}") return False @@ -1082,5 +1081,5 @@ class InstagramPoster: return {'success': False, 'message': 'Error posting photo. Authenticate failed'} result = self.instagram_api.post_image_with_caption(image_path, caption) if not result: - return {'success': False, 'message': 'Error posting photo.'} + return {'success': False, 'message': 'Error posting photo in Instagram.'} return {'success': True, 'message': 'Photo posted successfully'} \ No newline at end of file diff --git a/goodtimes/utils.py b/goodtimes/utils.py index 9fd4742..d85f258 100644 --- a/goodtimes/utils.py +++ b/goodtimes/utils.py @@ -59,8 +59,6 @@ class JsonResponseUtil: response_data["errors"] = errors return JsonResponse(response_data, status=status) - - class RandomGenerator: @staticmethod def number(start, end): diff --git a/manage_events/management/commands/test_facebook_api.py b/manage_events/management/commands/test_facebook_api.py index efbc3c5..5f0a2e3 100644 --- a/manage_events/management/commands/test_facebook_api.py +++ b/manage_events/management/commands/test_facebook_api.py @@ -15,8 +15,7 @@ class Command(BaseCommand): if not event.image: self.stdout.write(self.style.ERROR("No image found.")) - base_domain = settings.BASE_DOMAIN - image_path = f"{base_domain}{event.image.url}" + image_path = f"{settings.BASE_DOMAIN}{event.image.url}" print(f"complete path of image {image_path}") caption = f"{event.title}\nDuration: {event.start_date} to {event.end_date}\nAddress: {event.venue.address}" diff --git a/manage_events/management/commands/test_instagram_api.py b/manage_events/management/commands/test_instagram_api.py index 5717284..a5c689e 100644 --- a/manage_events/management/commands/test_instagram_api.py +++ b/manage_events/management/commands/test_instagram_api.py @@ -16,9 +16,8 @@ class Command(BaseCommand): if not event.image: self.stdout.write(self.style.ERROR("No image found.")) - # base_domain = settings.BASE_DOMAIN - # image_path = f"{base_domain}{event.image.url}" - image_path = event.image.url + image_path = f"{settings.BASE_DOMAIN}{event.image.url}" + # image_path = event.image.url print(f"complete path of image {image_path}") caption = f"{event.title}\nDuration: {event.start_date} to {event.end_date}\nAddress: {event.venue.address}" diff --git a/manage_events/views.py b/manage_events/views.py index 2381ede..939e708 100644 --- a/manage_events/views.py +++ b/manage_events/views.py @@ -533,7 +533,7 @@ class CustomerVenueFilterView(LoginRequiredMixin, generic.View): User = get_user_model() from .report import generate_event_report, generate_event_report_pdf_three -from django.http import HttpResponse +from django.http import HttpResponse, JsonResponse class GenerateEventReportView(generic.View): @@ -561,45 +561,70 @@ class SocialMediaPostView(generic.View): event_id = kwargs.get("id") print(platform, event_id) errors = [] + success_messages = [] try: event = Event.objects.get(id=event_id) except Event.DoesNotExist: errors.append("Event does not exist") - return JsonResponseUtil.error(message=errors, errors=errors) + return JsonResponse({ + 'message': "Error in posting to social media", + 'errors': errors, + 'success_messages': success_messages + }, status=400) if not event.active: errors.append("Event is not active") - return JsonResponseUtil.error(message=errors, errors=errors) + return JsonResponse({ + 'message': "Error in posting to social media", + 'errors': errors, + 'success_messages': success_messages + }, status=400) caption = f"{event.title}\nDuration: {event.start_date} to {event.end_date}\nAddress: {event.venue.address}" - print(f"image url and caption is {caption}") - if platform in ['instagram', 'facebook', 'twitter', 'all']: + if platform in ['instagram', 'facebook', 'twitter', 'all']: if platform in ['twitter', 'all']: image_url = event.image.path twitter_api = TwitterAPI() twitter_poster = TwitterPoster(twitter_api) result = twitter_poster.post_image_with_caption(image_url, caption) - if not result['success']: - errors.append(result['message']) + if result['success']: + success_messages.append("Posted to Twitter successfully") + else: + errors.append("Fail to post on Twitter") - image_url = request.build_absolute_uri(event.image.url) # fb and insta require complete path with domain + 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 not result["success"]: - errors.append(result["message"]) + if result["success"]: + success_messages.append("Posted to Facebook successfully") + else: + errors.append("Fail to post on Facebook") if platform in ['instagram', 'all']: instagram_api = InstagramAPI() instagram_poster = InstagramPoster(instagram_api) result = instagram_poster.post_image_with_caption(image_url, caption) - if not result["success"]: - errors.append(result["message"]) + if result["success"]: + success_messages.append("Posted to Instagram successfully") + else: + errors.append("Fail to post on Instagram") if not errors: - return JsonResponseUtil.success(message='Post Successful') + return JsonResponse({'message': 'Post Successful', 'errors': errors, 'success_messages': success_messages}) - return JsonResponseUtil.error(message=errors, errors=errors) + if errors and success_messages: + return JsonResponse({ + 'message': 'Some posts succeeded while others failed', + 'errors': errors, + 'success_messages': success_messages + }, status=200) + + return JsonResponse({ + 'message': 'Error in posting to social media', + 'errors': errors, + 'success_messages': success_messages + }, status=400) \ No newline at end of file diff --git a/templates/manage_events/event_details.html b/templates/manage_events/event_details.html index 1fa11b3..7d5ed7a 100644 --- a/templates/manage_events/event_details.html +++ b/templates/manage_events/event_details.html @@ -252,26 +252,43 @@ console.log(response); Swal.close(); - if (response.status === 200) { + if (response.success_messages && response.errors && response.errors.length === 0) { + let successMessages = response.success_messages.join("
    "); Swal.fire({ icon: "success", title: "Success", - text: response.message + html: response.message + "
    " + successMessages }); - } else if (response.status === 403) { + } else if (response.errors && response.success_messages) { + let errorMessages = response.errors.join("
    "); + let successMessages = response.success_messages.join("
    "); + Swal.fire({ + icon: "warning", + title: "Partial Success", + html: response.message + "
    Successes:
    " + successMessages + "
    Failures:
    " + errorMessages + }); + } else if (response.errors) { + let errorMessages = response.errors.join("
    "); + Swal.fire({ + icon: "error", + title: "Failures", + html: response.message + "
    " + errorMessages + }); + } else { Swal.fire({ icon: "error", title: "Error", - text: response.message + text: "Unknown error occurred" }); } }, error: function(xhr, status, error) { Swal.close(); + var errorMessage = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : "Something went wrong. Please try again later."; Swal.fire({ icon: "error", title: "Error", - text: "Something went wrong. Please try again later." + text: errorMessage }); } }); @@ -280,5 +297,7 @@ }); }); + + {%endblock javascript%} \ No newline at end of file From 8d212430ec3f1d07ebd89f1726aeb83ae7f4a431 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 26 Jul 2024 16:11:02 +0530 Subject: [PATCH 077/187] sync Wdipl with Staging --- goodtimes/settings/wdipl.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/goodtimes/settings/wdipl.py b/goodtimes/settings/wdipl.py index fdc2f39..bb170a5 100644 --- a/goodtimes/settings/wdipl.py +++ b/goodtimes/settings/wdipl.py @@ -60,7 +60,7 @@ ALLOWED_HOSTS = ["127.0.0.1", "goodtimes.betadelivery.com", "154.41.254.33"] # }, # } -# BASE_DOMAIN = "https://goodtimes.betadelivery.com" +BASE_DOMAIN = "https://goodtimes.betadelivery.com" # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ @@ -81,3 +81,8 @@ STRIPE_CHECKOUT_URL = ( STRIPE_FINAL_URL = ( "https://goodtimes.betadelivery.com/subscriptions/create-checkout-session/" ) +COUPON_VALIDITY_CHECK_URL = ( + "https://goodtimes.betadelivery.com/subscriptions/coupon-validity-check/" +) + +LOGO_PATH = "/var/www/goodtimes/static" From 4afbfb33f7d6cec15eb5894b92b62cc5f0544e0d Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 26 Jul 2024 16:27:41 +0530 Subject: [PATCH 078/187] sync Wdipl with Staging 2 --- manage_subscriptions/views.py | 50 +++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 17e5f8d..fcb7c9b 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -31,6 +31,7 @@ from django.views.decorators.http import require_POST from django.conf import settings from django.views.generic.base import TemplateView from django.db.models import Q + # Create your views here. @@ -434,30 +435,33 @@ def validate_coupon(request): except Subscription.DoesNotExist: return JsonResponse({"error": "Subscription not found."}, status=404) + # If no coupon code is provided, assume no discount and proceed + if not coupon_code: + return JsonResponse({"message": "No coupon code provided."}, status=200) + # Validating Coupon - if coupon_code: - try: - coupon = Coupon.objects.get(coupon_code=coupon_code) - if not coupon.is_valid(): - return JsonResponse({"error": "Coupon is not valid."}, status=400) - if coupon.discount_amount and coupon.discount_amount > subscription.amount: - return JsonResponse( - {"error": "Coupon discount amount exceeds subscription amount."}, - status=400, - ) - if ( - coupon.discount_percentage - and (coupon.discount_percentage / Decimal("100")) * subscription.amount - > subscription.amount - ): - return JsonResponse( - { - "error": "Coupon discount percentage exceeds subscription amount." - }, - status=400, - ) - except Coupon.DoesNotExist: - return JsonResponse({"error": "Coupon not found."}, status=404) + try: + coupon = Coupon.objects.get(coupon_code=coupon_code) + if not coupon.is_valid(): + return JsonResponse({"error": "Coupon is not valid."}, status=400) + if coupon.discount_amount and coupon.discount_amount > subscription.amount: + return JsonResponse( + {"error": "Coupon discount amount exceeds subscription amount."}, + status=400, + ) + if ( + coupon.discount_percentage + and (coupon.discount_percentage / Decimal("100")) * subscription.amount + > subscription.amount + ): + return JsonResponse( + {"error": "Coupon discount percentage exceeds subscription amount."}, + status=400, + ) + except Coupon.DoesNotExist: + return JsonResponse({"error": "Coupon not found."}, status=404) + + return JsonResponse({"message": "Coupon is valid."}, status=200) @csrf_exempt From 9397b26708d79556a436c4273fb5021869df9307 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 26 Jul 2024 17:54:07 +0530 Subject: [PATCH 079/187] debugging coupon on stripe --- manage_subscriptions/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index fcb7c9b..1c21774 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -486,6 +486,7 @@ def create_checkout_session(request): print("order_id: ", order_id) # Calculating the final amount after applying the coupon discount final_amount = subscription.amount + print("final_amount before applying coupon: ", final_amount) coupon = None if coupon_code: try: @@ -500,6 +501,7 @@ def create_checkout_session(request): final_amount = max( 0, final_amount ) # Ensuring the amount is not negative + print("final_amount after applying coupon: ", final_amount) else: return JsonResponse( {"error": "Invalid or expired coupon code."}, status=400 From 2db723f12dccdc9ec4c57bf7a7e29a2eaf2ab6bd Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 26 Jul 2024 17:58:28 +0530 Subject: [PATCH 080/187] debugging coupon on stripe 2 --- manage_subscriptions/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 1c21774..0d6e59f 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -473,6 +473,7 @@ def create_checkout_session(request): print("data: ", data) subscription_id = data.get("subscriptionId", None) coupon_code = data.get("couponCode", None) + print("Initialize coupon_code: ", coupon_code) principal_id = request.user.id try: @@ -508,6 +509,7 @@ def create_checkout_session(request): ) except Coupon.DoesNotExist: return JsonResponse({"error": "Coupon not found."}, status=404) + print("after coupon try block: ", coupon) # Create a Transaction object with status INITIATE transaction = Transaction.objects.create( principal=request.user, From 53a572c19e2e31974e738826c836a7b04d9ef878 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Fri, 26 Jul 2024 18:04:17 +0530 Subject: [PATCH 081/187] debugging coupon on stripe 3 --- templates/stripe_html/index.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/stripe_html/index.html b/templates/stripe_html/index.html index 0a30333..641afcb 100644 --- a/templates/stripe_html/index.html +++ b/templates/stripe_html/index.html @@ -569,7 +569,8 @@ 'Content-Type': 'application/json', }, body: JSON.stringify({ - subscriptionId: subscriptionId + subscriptionId: subscriptionId, + couponCode: couponCode }), }) .then((result) => { From cdf9d06e8c49f52a8f509ae8dc381e3697eccc95 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 29 Jul 2024 11:41:49 +0530 Subject: [PATCH 082/187] implementing stripe subscription --- manage_subscriptions/views.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 0d6e59f..4f897fc 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -542,21 +542,23 @@ def create_checkout_session(request): # customer=customer.id, # Optional: Link the session to the Stripe customer created above line_items=[ { - "price_data": { - "currency": "gbp", - "product_data": { - "name": subscription.title, # Adjust with your subscription/product name - }, - "unit_amount": int( - final_amount * 100 - ), # Unit amount in cents/pence - "tax_behavior": "inclusive", # or 'exclusive', based on your tax settings - }, + "price": "price_1PgkAUCesU6kunsI0AwDONty", + # "price_data": { + # "currency": "gbp", + # "product_data": { + # "name": subscription.title, # Adjust with your subscription/product name + # }, + # "unit_amount": int( + # final_amount * 100 + # ), # Unit amount in cents/pence + # "tax_behavior": "inclusive", # or 'exclusive', based on your tax settings + # }, "quantity": 1, } ], # allow_promotion_codes=True, - mode="payment", + # mode="payment", + mode="subscription", # discounts=[{"coupon": "VLMAsicx"}], success_url=request.build_absolute_uri("/subscriptions/success/"), cancel_url=request.build_absolute_uri("/subscriptions/cancel/"), From d739d9bd91737e450e73bda8b69c7996aab7c2ac Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 29 Jul 2024 11:51:03 +0530 Subject: [PATCH 083/187] debugging on stripe subscription --- manage_subscriptions/views.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 4f897fc..9ff9dd4 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -542,23 +542,23 @@ def create_checkout_session(request): # customer=customer.id, # Optional: Link the session to the Stripe customer created above line_items=[ { - "price": "price_1PgkAUCesU6kunsI0AwDONty", - # "price_data": { - # "currency": "gbp", - # "product_data": { - # "name": subscription.title, # Adjust with your subscription/product name - # }, - # "unit_amount": int( - # final_amount * 100 - # ), # Unit amount in cents/pence - # "tax_behavior": "inclusive", # or 'exclusive', based on your tax settings - # }, + # "price": "price_1PgkAUCesU6kunsI0AwDONty", + "price_data": { + "currency": "gbp", + "product_data": { + "name": subscription.title, # Adjust with your subscription/product name + }, + "unit_amount": int( + final_amount * 100 + ), # Unit amount in cents/pence + "tax_behavior": "inclusive", # or 'exclusive', based on your tax settings + }, "quantity": 1, } ], # allow_promotion_codes=True, - # mode="payment", - mode="subscription", + mode="payment", + # mode="subscription", # discounts=[{"coupon": "VLMAsicx"}], success_url=request.build_absolute_uri("/subscriptions/success/"), cancel_url=request.build_absolute_uri("/subscriptions/cancel/"), From 036b8ae93544c2ec6850ddbae651046f833e0013 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 29 Jul 2024 12:08:56 +0530 Subject: [PATCH 084/187] debugging on stripe subscription 2 --- manage_subscriptions/views.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 9ff9dd4..16d7399 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -542,23 +542,13 @@ def create_checkout_session(request): # customer=customer.id, # Optional: Link the session to the Stripe customer created above line_items=[ { - # "price": "price_1PgkAUCesU6kunsI0AwDONty", - "price_data": { - "currency": "gbp", - "product_data": { - "name": subscription.title, # Adjust with your subscription/product name - }, - "unit_amount": int( - final_amount * 100 - ), # Unit amount in cents/pence - "tax_behavior": "inclusive", # or 'exclusive', based on your tax settings - }, + "price": "price_1PgkAUCesU6kunsI0AwDONty", "quantity": 1, } ], # allow_promotion_codes=True, - mode="payment", - # mode="subscription", + # mode="payment", + mode="subscription", # discounts=[{"coupon": "VLMAsicx"}], success_url=request.build_absolute_uri("/subscriptions/success/"), cancel_url=request.build_absolute_uri("/subscriptions/cancel/"), From 77be3218b72bc7f4a90f8f39de822f5c9b0f0689 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 29 Jul 2024 12:14:43 +0530 Subject: [PATCH 085/187] debugging on stripe subscription 3 --- templates/stripe_html/index.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/stripe_html/index.html b/templates/stripe_html/index.html index 641afcb..019463b 100644 --- a/templates/stripe_html/index.html +++ b/templates/stripe_html/index.html @@ -577,6 +577,8 @@ return result.json(); }) .then((data) => { + console.log("data: ", data); + console.log("data.sessionId: ", data.sessionId); // Redirects to Stripe Checkout return stripe.redirectToCheckout({ sessionId: data.sessionId From 7992e456a4e19912477cbbfa27fec89d4a3227b9 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 29 Jul 2024 12:56:22 +0530 Subject: [PATCH 086/187] debugging on stripe subscription 4 --- goodtimes/settings/wdipl.py | 3 +++ manage_subscriptions/views.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/goodtimes/settings/wdipl.py b/goodtimes/settings/wdipl.py index bb170a5..5b5acbf 100644 --- a/goodtimes/settings/wdipl.py +++ b/goodtimes/settings/wdipl.py @@ -86,3 +86,6 @@ COUPON_VALIDITY_CHECK_URL = ( ) LOGO_PATH = "/var/www/goodtimes/static" + +STRIPE_TEST_MODE_SECRET_KEY = env.str("STRIPE_TEST_MODE_SECRET_KEY") +STRIPE_TEST_MODE_PUBLISH_KEY = env.str("STRIPE_TEST_MODE_PUBLISH_KEY") diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 16d7399..2d390a0 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -419,7 +419,7 @@ class SubscriptionPageView(TemplateView): @csrf_exempt def stripe_config(request): if request.method == "GET": - stripe_config = {"publicKey": settings.STRIPE_PUBLISH_KEY} + stripe_config = {"publicKey": settings.STRIPE_TEST_MODE_PUBLISH_KEY} return JsonResponse(stripe_config, safe=False) @@ -468,7 +468,7 @@ def validate_coupon(request): @require_POST def create_checkout_session(request): success_url = reverse_lazy("manage_subscriptions:stripe") - stripe.api_key = settings.STRIPE_SECRET_KEY + stripe.api_key = settings.STRIPE_TEST_MODE_SECRET_KEY data = json.loads(request.body) print("data: ", data) subscription_id = data.get("subscriptionId", None) From 27a124b0b2487100ce1b922d71eef1c4c5fc01fb Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Mon, 29 Jul 2024 15:39:43 +0530 Subject: [PATCH 087/187] feat(filter): fixed nearest, latest and preference event --- goodtimes/services.py | 21 ++++--- manage_events/api/serializers.py | 12 ++++ manage_events/api/views.py | 96 ++++++++++++++++++-------------- 3 files changed, 80 insertions(+), 49 deletions(-) diff --git a/goodtimes/services.py b/goodtimes/services.py index c28b41c..6488f7e 100644 --- a/goodtimes/services.py +++ b/goodtimes/services.py @@ -806,17 +806,24 @@ class GoogleMapsservice: if element["status"] == "OK" and element["distance"]["value"] <= radius_km * 1000 # Convert km to meters } + print(f"distance is {distances} and distances key is {distances.keys()}") + # Filter the queryset to include only events within the specified radius queryset = queryset.filter(id__in=distances.keys()) - # # Sort the event IDs by their distances in ascending order - # event_ids_by_distance = sorted(distances, key=distances.get) + print(f"query set after distance filter {queryset}") - # # Create a Case/When expression to preserve the order of events by distance - # preserved_order = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(event_ids_by_distance)]) - # print(f"preserved_order is {preserved_order}") - # # Order the queryset based on the preserved order - # queryset = queryset.order_by(preserved_order) + # Sort the event IDs by their distances in ascending order + event_ids_by_distance = sorted(distances, key=distances.get) + print(f"sort event by it distance {event_ids_by_distance}") + + # Create a Case/When expression to preserve the order of events by distance + preserved_order = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(event_ids_by_distance)]) + print(f"preserved_order is {preserved_order}") + # Order the queryset based on the preserved order + queryset = queryset.order_by(preserved_order) + for data in queryset: + print(f"queryset after preserverd order {data.id}") return queryset diff --git a/manage_events/api/serializers.py b/manage_events/api/serializers.py index 4529224..30982b0 100644 --- a/manage_events/api/serializers.py +++ b/manage_events/api/serializers.py @@ -50,6 +50,18 @@ class EventCategorySerializer(serializers.ModelSerializer): model = EventCategory fields = ["id", "title", "image", "description", "video_url"] + def get_image_url(self, obj, field_name, request): + image_field = getattr(obj, field_name) + if image_field: + return request.build_absolute_uri(image_field.url) + return "" + + def to_representation(self, instance): + data = super().to_representation(instance) + request = self.context.get("request") + data["image"] = self.get_image_url(instance, "image", request) + return data + class AgeGroupsSerializer(serializers.ModelSerializer): class Meta: model = AgeGroups diff --git a/manage_events/api/views.py b/manage_events/api/views.py index 860c65f..73f71a7 100644 --- a/manage_events/api/views.py +++ b/manage_events/api/views.py @@ -586,7 +586,7 @@ class EventPreferencesView(APIView): def get(self, request, *args, **kwargs): """Get all event categories for the authenticated user.""" obj = self.model.objects.filter(active=True, deleted=False) - serializer = self.serializer_class(obj, many=True) + serializer = self.serializer_class(obj, many=True, context={"request": request}) return ApiResponse.success( data=serializer.data, message=constants.SUCCESS, status=status.HTTP_200_OK ) @@ -1004,65 +1004,77 @@ class AgeGroupListView(APIView): class EventListView(generics.ListAPIView): authentication_classes = [JWTAuthentication] permission_classes = [IsAuthenticated] - queryset = Event.objects.filter(active=True, draft=False, deleted=False, end_date__gte=timezone.now().date()) serializer_class = EventListSerializer filter_backends = [DjangoFilterBackend] filterset_class = EventFilter - def apply_popularity_latest(self, queryset, ordering): - # Split the ordering fields and remove any leading '-' characters - ordering_fields = [field.lstrip("-") for field in ordering.split(",")] - - # Check if 'nearest' is in the ordering fields and remove it as nearest work only for lat and longitude - if "nearest" in ordering_fields: - ordering.replace("nearest", "") - ordering_fields.remove("nearest") - - # Annotate the queryset with popularity if 'popularity' is in the ordering fields - if "popularity" in ordering_fields: - queryset = queryset.annotate(popularity=Count("interaction_event")) - - # Replace 'latest' with '-created_on' in the ordering fields - ordering = ",".join( - "start_date" if field == "latest" else f"-{field}" - for field in ordering_fields - ) - - # If ordering is empty, set a default ordering - if not ordering: - ordering = "-start_date" - - print(f"++++++++++++++++++++++++++++++++ordering data in populatirn flow {ordering}") - # Apply the ordering to the queryset - return queryset.order_by(*ordering.split(",")) - def get_queryset(self): - queryset = super().get_queryset() + # Base queryset filtering active, non-draft, non-deleted events with end date in the future + print(f"queryparams is {self.request.query_params}") - # Sort by nearest location if latitude and longitude are provided + if self.request.query_params: + queryset = Event.objects.filter( + active=True, + draft=False, + deleted=False, + end_date__gte=timezone.now().date() + ) + else: + preferences = PrincipalPreference.objects.get(principal=self.request.user) + preferred_categories_ids = preferences.preferred_categories.values_list( + "id", flat=True + ) + queryset = Event.objects.filter( + active=True, + draft=False, + deleted=False, + end_date__gte=timezone.now().date(), + category__in=preferred_categories_ids + ) + + # Get query parameters latitude = self.request.query_params.get("latitude") longitude = self.request.query_params.get("longitude") - ordering = self.request.query_params.get("ordering", "") + ordering = self.request.query_params.get("sort", "") + # Handle nearest location sorting if latitude and longitude and "nearest" in ordering: - print("latitude fucntion called") - gmaps_service = GoogleMapsservice() - queryset = gmaps_service.get_nearest_events(queryset, float(latitude), float(longitude)) + queryset = self.apply_nearest_filter(queryset, latitude, longitude) - print(f"=======orderring data is {ordering}") + # Handle popularity and latest sorting + # if ordering: + queryset = self.apply_popularity_latest(queryset, ordering) + + return queryset + + def apply_nearest_filter(self, queryset, latitude, longitude): + gmaps_service = GoogleMapsservice() + return gmaps_service.get_nearest_events(queryset, float(latitude), float(longitude)) + + def apply_popularity_latest(self, queryset, ordering): + # Split ordering fields and process each field + ordering_fields = [field.lstrip("-") for field in ordering.split(",")] + + # Remove 'nearest' from ordering as it's handled separately + if "nearest" in ordering_fields: + ordering_fields.remove("nearest") + ordering = ordering.replace("nearest", "") + + # Annotate with popularity and order it if requested + if "popularity" in ordering_fields: + queryset = queryset.annotate(popularity=Count("interaction_event")).order_by("-popularity") + + # order latest record and by default sorting + queryset = queryset.order_by("start_date") - # Apply popularity annotation and ordering if requested - if ordering: - queryset = self.apply_popularity_latest(queryset, ordering) return queryset def get(self, request, *args, **kwargs): - print(f"getquery set data is {self.get_queryset()}") queryset = self.filter_queryset(self.get_queryset()) page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) - data = self.get_paginated_response(serializer.data) - return ApiResponse.success(message=constants.SUCCESS, data=data) + return self.get_paginated_response(serializer.data) + serializer = self.get_serializer(queryset, many=True) return ApiResponse.success(message=constants.SUCCESS, data=serializer.data) \ No newline at end of file From ce676aa81dd3a0caace19eeba693ca299f913022 Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Tue, 30 Jul 2024 12:41:41 +0530 Subject: [PATCH 088/187] feat(filter):refactor sorting logic --- manage_events/api/filters.py | 8 ++++++- manage_events/api/views.py | 44 ++++++++++++++++++------------------ 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/manage_events/api/filters.py b/manage_events/api/filters.py index 08e7543..cb88f8c 100644 --- a/manage_events/api/filters.py +++ b/manage_events/api/filters.py @@ -31,4 +31,10 @@ class EventFilter(filters.FilterSet): def filter_category(self, queryset, name, value): category = value.split(',') - return queryset.filter(category__title__in=category) \ No newline at end of file + return queryset.filter(category__title__in=category) + + # def filter_queryset(self, queryset): + # queryset = super().filter_queryset(queryset) + # if 'price_from' in self.data or 'price_to' in self.data: + # queryset = queryset.order_by('entry_fee') + # return queryset \ No newline at end of file diff --git a/manage_events/api/views.py b/manage_events/api/views.py index 73f71a7..5bfd641 100644 --- a/manage_events/api/views.py +++ b/manage_events/api/views.py @@ -1010,31 +1010,28 @@ class EventListView(generics.ListAPIView): def get_queryset(self): # Base queryset filtering active, non-draft, non-deleted events with end date in the future - print(f"queryparams is {self.request.query_params}") + queryset = Event.objects.filter( + active=True, + draft=False, + deleted=False, + end_date__gte=timezone.now().date() + ) - if self.request.query_params: - queryset = Event.objects.filter( - active=True, - draft=False, - deleted=False, - end_date__gte=timezone.now().date() - ) - else: + if not self.request.query_params: preferences = PrincipalPreference.objects.get(principal=self.request.user) preferred_categories_ids = preferences.preferred_categories.values_list( "id", flat=True ) - queryset = Event.objects.filter( - active=True, - draft=False, - deleted=False, - end_date__gte=timezone.now().date(), - category__in=preferred_categories_ids - ) + queryset = queryset.filter(category__in=preferred_categories_ids).order_by("start_date") + + return queryset + + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) # Get query parameters - latitude = self.request.query_params.get("latitude") - longitude = self.request.query_params.get("longitude") + latitude = self.request.query_params.get("latitude", "") + longitude = self.request.query_params.get("longitude", "") ordering = self.request.query_params.get("sort", "") # Handle nearest location sorting @@ -1042,8 +1039,7 @@ class EventListView(generics.ListAPIView): queryset = self.apply_nearest_filter(queryset, latitude, longitude) # Handle popularity and latest sorting - # if ordering: - queryset = self.apply_popularity_latest(queryset, ordering) + queryset = self.apply_sorting(queryset, ordering) return queryset @@ -1051,7 +1047,7 @@ class EventListView(generics.ListAPIView): gmaps_service = GoogleMapsservice() return gmaps_service.get_nearest_events(queryset, float(latitude), float(longitude)) - def apply_popularity_latest(self, queryset, ordering): + def apply_sorting(self, queryset, ordering): # Split ordering fields and process each field ordering_fields = [field.lstrip("-") for field in ordering.split(",")] @@ -1065,7 +1061,11 @@ class EventListView(generics.ListAPIView): queryset = queryset.annotate(popularity=Count("interaction_event")).order_by("-popularity") # order latest record and by default sorting - queryset = queryset.order_by("start_date") + if "latest" in ordering_fields: + queryset = queryset.order_by("start_date") + + if "price" in ordering_fields: + queryset = queryset.order_by('entry_fee') return queryset From e3189344ead1ad8a1e3054fe3b47f961c068f6c9 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Wed, 31 Jul 2024 13:12:17 +0530 Subject: [PATCH 089/187] auto recurring testing --- goodtimes/settings/base.py | 7 +- goodtimes/webhook.py | 62 ++++- manage_coupons/forms.py | 1 - .../migrations/0002_coupon_coupon_id.py | 18 ++ manage_coupons/models.py | 1 + manage_coupons/urls.py | 10 +- manage_coupons/utils.py | 47 ++++ manage_coupons/views.py | 31 ++- manage_subscriptions/api/views.py | 25 +- manage_subscriptions/forms.py | 19 +- ...principalsubscription_comments_and_more.py | 97 ++++++++ manage_subscriptions/models.py | 33 ++- manage_subscriptions/urls.py | 16 +- manage_subscriptions/views.py | 224 ++++++++++++++++-- templates/manage_coupons/coupon_list.html | 4 +- .../manage_subscriptions/product_add.html | 49 ++++ .../manage_subscriptions/product_list.html | 124 ++++++++++ .../subscription_list.html | 5 +- 18 files changed, 704 insertions(+), 69 deletions(-) create mode 100644 manage_coupons/migrations/0002_coupon_coupon_id.py create mode 100644 manage_coupons/utils.py create mode 100644 manage_subscriptions/migrations/0010_principalsubscription_comments_and_more.py create mode 100644 templates/manage_subscriptions/product_add.html create mode 100644 templates/manage_subscriptions/product_list.html diff --git a/goodtimes/settings/base.py b/goodtimes/settings/base.py index 695359e..4009cbc 100644 --- a/goodtimes/settings/base.py +++ b/goodtimes/settings/base.py @@ -303,8 +303,11 @@ SIMPLE_JWT = { "JTI_CLAIM": "jti", } -STRIPE_SECRET_KEY = env.str("STRIPE_SECRET_KEY") -STRIPE_PUBLISH_KEY = env.str("STRIPE_PUBLISH_KEY") +STRIPE_SECRET_KEY = "sk_test_51OexsKCesU6kunsIsbSKSZc1BF4gjklniaue8lmpkGKqDzenQtMkR8tKAryxErJXqp0jPiu1Gg7papa4tqZfKL9G00qUM4toB2" +STRIPE_PUBLISH_KEY = "pk_test_51OexsKCesU6kunsINDvKUhbelxeUmDAVZGSOisZ6XXHCp3pKtl4vs0pR42w0OcjZhngmECsXQNbAKNPOhiFMTJ8o00sRZQG0lh" + +# STRIPE_SECRET_KEY = env.str("STRIPE_SECRET_KEY") +# STRIPE_PUBLISH_KEY = env.str("STRIPE_PUBLISH_KEY") ONE_SIGNAL_APP_ID = env.str("ONE_SIGNAL_APP_ID") diff --git a/goodtimes/webhook.py b/goodtimes/webhook.py index 5f1481a..c825b91 100644 --- a/goodtimes/webhook.py +++ b/goodtimes/webhook.py @@ -1,3 +1,4 @@ +import datetime from django.conf import settings from django.db import transaction from django.shortcuts import get_object_or_404 @@ -130,6 +131,13 @@ class WebhookService: logger.error(f"Invalid subscription ID: {subscription_id}") raise ValueError(f"Invalid subscription ID: {subscription_id}") + def get_stripe_subscription(self): + stripe_subscription_id = self.charge_data["metadata"]["subscription"] + if stripe_subscription_id: + return stripe_subscription_id + else: + return None + def get_order_id(self): return self.charge_data["metadata"]["order_id"] @@ -236,23 +244,44 @@ class SubscriptionService: def __init__(self): self.principal_subscription = None - def create_principal_subscription(self, principal, subscription, order_id, coupon=None): + def create_principal_subscription( + self, + principal, + subscription, + stripe_subscription, + order_id, + current_period_start, + current_period_end, + coupon=None, + ): subscription_days = subscription.plan.days today = timezone.now().date() - last_date = today + timedelta(days=subscription_days) + start_date = ( + datetime.datetime.fromtimestamp(current_period_start).date() + if current_period_start + else today + ) + end_date = ( + datetime.datetime.fromtimestamp(current_period_end).date() + if current_period_end + else (today + timedelta(days=subscription_days)) + ) + principal_subscription = PrincipalSubscription.objects.create( principal=principal, subscription=subscription, + stripe_subscription_id=stripe_subscription if stripe_subscription else "", is_paid=True, + is_stripe_subscription=True if stripe_subscription else False, order_id=order_id, - start_date=today, - end_date=last_date, - grace_period_end_date=last_date + timedelta(days=15), + start_date=start_date, + end_date=end_date, + grace_period_end_date=end_date + timedelta(days=15), coupon_code=coupon.coupon_code if coupon else None, ) - if coupon: - coupon.no_of_redeems += 1 - coupon.save() + # if coupon: + # coupon.no_of_redeems += 1 + # coupon.save() self.principal_subscription = principal_subscription return principal_subscription @@ -267,13 +296,16 @@ class SubscriptionService: class PaymentProcessingService: - def __init__(self, webhook_data): + def __init__(self, webhook_data, current_period_start, current_period_end): self.webhook_service = WebhookService(webhook_data) self.notification_service = NotificationService() + self.current_period_start = current_period_start + self.current_period_end = current_period_end # Retrieve objects self.principal = self.webhook_service.get_principal() self.transaction = self.webhook_service.get_transaction() self.subscription = self.webhook_service.get_subscription() + self.stripe_subscription = self.webhook_service.get_stripe_subscription() self.order_id = self.webhook_service.get_order_id() self.coupon = self.webhook_service.get_coupon() self.subscription_service = SubscriptionService() @@ -290,7 +322,13 @@ class PaymentProcessingService: # Create or update the principal subscription self.principal_subscription = ( self.subscription_service.create_principal_subscription( - self.principal, self.subscription, self.order_id, self.coupon + self.principal, + self.subscription, + self.stripe_subscription, + self.order_id, + self.coupon, + self.current_period_start, + self.current_period_end, ) ) print("First Part Done....!!!!!") @@ -303,9 +341,9 @@ class PaymentProcessingService: referral_service = ReferralRewardService( self.principal, self.principal_subscription, self.subscription ) - print("Above Third Part...!!!!!!!!!!!") + print("Third Part Done...!!!!!!!!!!!") referral_service.credit_referral_reward_if_applicable() - print("Third Part Done....!!!!!") + print("Fourth Part Done....!!!!!") self.notification_service.payment_success_notification( self.principal, self.subscription, diff --git a/manage_coupons/forms.py b/manage_coupons/forms.py index c89a25e..e40b3eb 100644 --- a/manage_coupons/forms.py +++ b/manage_coupons/forms.py @@ -8,7 +8,6 @@ class CouponForm(forms.ModelForm): model = Coupon fields = [ "title", - "coupon_code", "description", "image", "discount_amount", diff --git a/manage_coupons/migrations/0002_coupon_coupon_id.py b/manage_coupons/migrations/0002_coupon_coupon_id.py new file mode 100644 index 0000000..999883c --- /dev/null +++ b/manage_coupons/migrations/0002_coupon_coupon_id.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.2 on 2024-07-31 07:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("manage_coupons", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="coupon", + name="coupon_id", + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/manage_coupons/models.py b/manage_coupons/models.py index f38dad6..25ce934 100644 --- a/manage_coupons/models.py +++ b/manage_coupons/models.py @@ -6,6 +6,7 @@ from accounts.models import BaseModel, IAmPrincipalType class Coupon(BaseModel): title = models.CharField(max_length=255) coupon_code = models.CharField(max_length=50, unique=True) + coupon_id = models.CharField(max_length=255, blank=True, null=True) no_of_redeems = models.IntegerField(default=0) description = models.TextField(null=True, blank=True) image = models.ImageField(upload_to="coupon_img", null=True, blank=True) diff --git a/manage_coupons/urls.py b/manage_coupons/urls.py index 9c3001c..2d53400 100644 --- a/manage_coupons/urls.py +++ b/manage_coupons/urls.py @@ -10,11 +10,11 @@ urlpatterns = [ views.CouponCreateOrUpdateView.as_view(), name="coupon_add", ), - path( - "coupon/edit//", - views.CouponCreateOrUpdateView.as_view(), - name="coupon_edit", - ), + # path( + # "coupon/edit//", + # views.CouponCreateOrUpdateView.as_view(), + # name="coupon_edit", + # ), path( "coupon/delete//", views.CouponDeleteView.as_view(), diff --git a/manage_coupons/utils.py b/manage_coupons/utils.py new file mode 100644 index 0000000..57578a3 --- /dev/null +++ b/manage_coupons/utils.py @@ -0,0 +1,47 @@ +import stripe +from decimal import Decimal + + +def handle_stripe_coupon(coupon_instance, stripe_secret_key): + """ + Handles the creation or updating of a Stripe coupon. + Returns True if successful, otherwise returns False. + """ + try: + stripe.api_key = stripe_secret_key + + # Prepare coupon data without setting the ID + coupon_data = { + "name": coupon_instance.title, + "metadata": { + "local_id": coupon_instance.id, + }, + "redeem_by": int(coupon_instance.valid_to.timestamp()), + "max_redemptions": ( + coupon_instance.max_redeems if coupon_instance.max_redeems > 0 else None + ), + "duration": "once", + } + + if coupon_instance.discount_amount: + coupon_data["amount_off"] = int( + coupon_instance.discount_amount * Decimal(100) + ) # Amount in cents/fils + coupon_data["currency"] = "gbp" + elif coupon_instance.discount_percentage: + coupon_data["percent_off"] = float(coupon_instance.discount_percentage) + + # Creating a new Stripe coupon + stripe_coupon = stripe.Coupon.create(**coupon_data) + # Using the Stripe-generated ID for coupon_code and coupon_id + coupon_instance.coupon_code = stripe_coupon.id + coupon_instance.coupon_id = stripe_coupon.id + + # Saving the coupon instance after successful Stripe operation + coupon_instance.save() + return True, "Coupon successfully created or updated." + + except Exception as e: + error_message = f"Error creating/updating Stripe coupon: {e}" + print(error_message) + return False, error_message diff --git a/manage_coupons/views.py b/manage_coupons/views.py index 2bdb0e4..8886aee 100644 --- a/manage_coupons/views.py +++ b/manage_coupons/views.py @@ -1,12 +1,15 @@ +from django.conf import settings from django.shortcuts import get_object_or_404, render, redirect from django.views import generic from django.contrib.auth.mixins import LoginRequiredMixin +import stripe from accounts import resource_action from django.urls import reverse_lazy from django.contrib import messages from goodtimes import constants from manage_coupons.forms import CouponForm from manage_coupons.models import Coupon +from manage_coupons.utils import handle_stripe_coupon # Create your views here. @@ -87,9 +90,18 @@ class CouponCreateOrUpdateView(LoginRequiredMixin, generic.View): print(form.errors) context = self.get_context_data(form=form) return render(request, self.template_name, context=context) - form.save() - messages.success(self.request, self.get_success_message()) - return redirect(self.success_url) + + success, message = handle_stripe_coupon( + form.instance, settings.STRIPE_SECRET_KEY + ) + if success: + messages.success(self.request, message) + return redirect(self.success_url) + else: + messages.error(self.request, message) + return render( + request, self.template_name, context=self.get_context_data(form=form) + ) class CouponDeleteView(LoginRequiredMixin, generic.View): @@ -104,11 +116,22 @@ class CouponDeleteView(LoginRequiredMixin, generic.View): def get(self, request, pk): try: type_obj = self.model.objects.get(id=pk) + + if type_obj.coupon_id: + stripe.api_key = settings.STRIPE_SECRET_KEY + try: + stripe.Coupon.delete(type_obj.coupon_id) + except stripe.error.StripeError as e: + # Handle Stripe errors + error_message = f"Stripe error: {e.user_message or e}" + messages.error(request, error_message) + return redirect(self.success_url) + type_obj.deleted = True type_obj.active = False type_obj.save() messages.success(request, self.success_message) except self.model.DoesNotExist: - messages.success(request, self.error_message) + messages.warning(request, self.error_message) return redirect(self.success_url) diff --git a/manage_subscriptions/api/views.py b/manage_subscriptions/api/views.py index c583aa6..5935ffe 100644 --- a/manage_subscriptions/api/views.py +++ b/manage_subscriptions/api/views.py @@ -179,6 +179,9 @@ class StripeWebhookTest(APIView): event_id = event["id"] event_type = event["type"] principal_id = event["data"]["object"]["metadata"]["principal"] + stripe_subscription_id = stripe.Subscription.retrieve( + event["data"]["object"]["metadata"]["subscription"] + ) webhook_event, created = WebhookEvent.objects.get_or_create( event_id=event_id, @@ -188,21 +191,25 @@ class StripeWebhookTest(APIView): }, ) + if stripe_subscription_id: + stripe_subscription = stripe.Subscription.retrieve(stripe_subscription_id) + current_period_start = stripe_subscription["current_period_start"] + current_period_end = stripe_subscription["current_period_end"] + else: + current_period_start = None + current_period_end = None + if not created and webhook_event.status == "processed": return ApiResponse.success( status=status.HTTP_208_ALREADY_REPORTED, message="Event already processed", ) - # Check if there is an active principal subscription - # if self._has_active_principal_subscription(principal_id): - # return ApiResponse.success( - # status=status.HTTP_208_ALREADY_REPORTED, - # message="Active principal subscription already exists", - # ) - - # payment_service = services.PaymentProcessingService(webhook_data=event) - payment_service = PaymentProcessingService(webhook_data=event) + payment_service = PaymentProcessingService( + webhook_data=event, + current_period_start=current_period_start, + current_period_end=current_period_end, + ) payment_service.process_event() webhook_event = WebhookEvent.objects.get(event_id=event_id) webhook_event.status = "processed" diff --git a/manage_subscriptions/forms.py b/manage_subscriptions/forms.py index 65701fa..303a58c 100644 --- a/manage_subscriptions/forms.py +++ b/manage_subscriptions/forms.py @@ -1,6 +1,11 @@ from django import forms from accounts.models import IAmPrincipalType -from manage_subscriptions.models import PrincipalSubscription, Subscription, Plan +from manage_subscriptions.models import ( + PrincipalSubscription, + StripeProduct, + Subscription, + Plan, +) class PlanForm(forms.ModelForm): @@ -53,3 +58,15 @@ class PrincipalSubscriptionForm(forms.ModelForm): "grace_period_end_date": forms.DateInput(attrs={"type": "date"}), "cancelled_date_time": forms.DateTimeInput(attrs={"type": "datetime"}), } + + +class StripeProductForm(forms.ModelForm): + class Meta: + model = StripeProduct + fields = [ + "title", + "description", + ] + widgets = { + "description": forms.Textarea(attrs={"rows": 3}), + } diff --git a/manage_subscriptions/migrations/0010_principalsubscription_comments_and_more.py b/manage_subscriptions/migrations/0010_principalsubscription_comments_and_more.py new file mode 100644 index 0000000..20544a5 --- /dev/null +++ b/manage_subscriptions/migrations/0010_principalsubscription_comments_and_more.py @@ -0,0 +1,97 @@ +# Generated by Django 5.0.2 on 2024-07-31 07:34 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("manage_subscriptions", "0009_principalsubscription_coupon_code"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name="principalsubscription", + name="comments", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name="principalsubscription", + name="is_stripe_subscription", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="principalsubscription", + name="stripe_subscription_id", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name="subscription", + name="price_id", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.CreateModel( + name="StripeProduct", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("active", models.BooleanField(default=True)), + ("deleted", models.BooleanField(default=False)), + ("created_on", models.DateTimeField(auto_now_add=True)), + ("modified_on", models.DateTimeField(auto_now=True)), + ("title", models.CharField(max_length=255)), + ("product_id", models.CharField(blank=True, max_length=255, null=True)), + ("description", models.TextField(blank=True, null=True)), + ("metadata", models.JSONField(blank=True, null=True)), + ("image_url", models.URLField(blank=True, null=True)), + ( + "default_price_id", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_created", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "modified_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_modified", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "db_table": "stripe_product", + }, + ), + migrations.AddField( + model_name="subscription", + name="stripe_product", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="subscription_product", + to="manage_subscriptions.stripeproduct", + ), + ), + ] diff --git a/manage_subscriptions/models.py b/manage_subscriptions/models.py index 4e8e1da..0aea65d 100644 --- a/manage_subscriptions/models.py +++ b/manage_subscriptions/models.py @@ -17,8 +17,31 @@ class Plan(BaseModel): return self.title +class StripeProduct(BaseModel): + title = models.CharField(max_length=255) + product_id = models.CharField(max_length=255, blank=True, null=True) + description = models.TextField(blank=True, null=True) + metadata = models.JSONField(blank=True, null=True) + image_url = models.URLField(blank=True, null=True) + default_price_id = models.CharField(max_length=255, blank=True, null=True) + + class Meta: + db_table = "stripe_product" + + def __str__(self): + return self.title + + class Subscription(BaseModel): title = models.CharField(max_length=255) + price_id = models.CharField(max_length=255, blank=True, null=True) + stripe_product = models.ForeignKey( + StripeProduct, + related_name="subscription_product", + on_delete=models.CASCADE, + null=True, + blank=True, + ) short_description = models.CharField(max_length=255, null=True, blank=True) long_description = models.TextField(null=True, blank=True) image = models.ImageField(upload_to="subscription_img", null=True, blank=True) @@ -31,7 +54,10 @@ class Subscription(BaseModel): IAmPrincipalType, related_name="principal_type_subscriptions", blank=True ) referral_percentage = models.DecimalField(max_digits=5, decimal_places=2) - is_free = models.BooleanField(default=False, help_text="Indicates whether this subscription is free and only accessible by administrators, not visible to regular users.") + is_free = models.BooleanField( + default=False, + help_text="Indicates whether this subscription is free and only accessible by administrators, not visible to regular users.", + ) class Meta: db_table = "subscription" @@ -67,6 +93,9 @@ class PrincipalSubscription(BaseModel): cancelled_date_time = models.DateTimeField(null=True, blank=True) grace_period_end_date = models.DateField(null=True, blank=True) stripe_customer_id = models.CharField(max_length=255, null=True, blank=True) + stripe_subscription_id = models.CharField(max_length=255, null=True, blank=True) + comments = models.CharField(max_length=255, null=True, blank=True) + is_stripe_subscription = models.BooleanField(default=False) payment_intent_id = models.CharField(max_length=255, null=True, blank=True) payment_intent_client_secret = models.CharField( max_length=255, null=True, blank=True @@ -78,7 +107,7 @@ class PrincipalSubscription(BaseModel): def __str__(self): return f"{self.subscription} - {self.principal.first_name}" - + def generate_order_id(email): return f"order_{str(timezone.localtime().timestamp())}{str(email)}" diff --git a/manage_subscriptions/urls.py b/manage_subscriptions/urls.py index 20210bf..5f2bfe2 100644 --- a/manage_subscriptions/urls.py +++ b/manage_subscriptions/urls.py @@ -12,16 +12,16 @@ urlpatterns = [ views.SubscriptionCreateOrUpdateView.as_view(), name="subscription_add", ), - path( - "subscription/edit//", - views.SubscriptionCreateOrUpdateView.as_view(), - name="subscription_edit", - ), # path( - # "subscription/delete/", - # views.SubscriptionDeleteView.as_view(), - # name="subscription_delete", + # "subscription/edit//", + # views.SubscriptionCreateOrUpdateView.as_view(), + # name="subscription_edit", # ), + path( + "subscription/delete/", + views.SubscriptionDeleteView.as_view(), + name="subscription_delete", + ), # PLANS path("plan/list/", views.PlanView.as_view(), name="plan_list"), # path( diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 2d390a0..318cad3 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -11,6 +11,7 @@ from django.utils import timezone from django.contrib.auth import get_user_model from manage_coupons.models import Coupon from manage_subscriptions.forms import ( + StripeProductForm, SubscriptionForm, PrincipalSubscriptionForm, ) @@ -20,7 +21,7 @@ from manage_wallets.models import ( TransactionStatus, TransactionType, ) -from .models import Plan, Subscription, PrincipalSubscription +from .models import Plan, StripeProduct, Subscription, PrincipalSubscription from django.views import generic from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse_lazy @@ -104,10 +105,52 @@ class SubscriptionCreateOrUpdateView(LoginRequiredMixin, generic.View): context = self.get_context_data(form=form) return render(request, self.template_name, context=context) + # Processing Stripe price creation and handling free subscription + success, message = self.handle_stripe_price(form) + if not success: + messages.error(self.request, message) + context = self.get_context_data(form=form) + return render(request, self.template_name, context=context) + form.save() messages.success(self.request, self.get_success_message()) return redirect(self.success_url) + def handle_stripe_price(self, form): + try: + stripe.api_key = settings.STRIPE_SECRET_KEY + + # creating Stripe price only if the subscription is not free + if not form.cleaned_data.get("is_free"): + # Getting Stripe Product ID + stripe_product = form.instance.stripe_product + plan = form.instance.plan + + # Map the Plan interval to Stripe's recurring interval + # It will only work if the plan title is 'month', 'year' 'week' or 'day' + stripe_interval = plan.title + # Create the Stripe price + stripe_price = stripe.Price.create( + unit_amount=int( + form.cleaned_data["amount"] * 100 + ), # Amount in cents + currency="gbp", # Adjust the currency as needed + recurring={ + "interval": stripe_interval + }, # Use the interval from Plan + product=stripe_product.product_id, + ) + # Assign the Stripe price ID to the subscription + form.instance.price_id = stripe_price.id + else: + form.instance.price_id = None # No price ID for free subscriptions + + return True, "" # Success + except stripe.error.StripeError as e: + return False, f"Stripe error: {str(e)}" + except Exception as e: + return False, f"An error occurred: {str(e)}" + class SubscriptionView(LoginRequiredMixin, generic.ListView): page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS @@ -132,26 +175,165 @@ class SubscriptionView(LoginRequiredMixin, generic.ListView): return context -# class SubscriptionDeleteView(LoginRequiredMixin, generic.View): -# page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS -# resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS -# action = resource_action.ACTION_DELETE -# model = Subscription -# success_url = reverse_lazy("manage_subscriptions:subscription_list") -# success_message = constants.RECORD_DELETED -# error_message = constants.RECORD_NOT_FOUND +class SubscriptionDeleteView(LoginRequiredMixin, generic.View): + page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS + resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS + action = resource_action.ACTION_DELETE + model = Subscription + success_url = reverse_lazy("manage_subscriptions:subscription_list") + success_message = constants.RECORD_DELETED + error_message = constants.RECORD_NOT_FOUND -# def get(self, request, pk): -# try: -# type_obj = self.model.objects.get(id=pk) -# type_obj.deleted = True -# type_obj.active = False -# type_obj.save() -# messages.success(request, self.success_message) -# except self.model.DoesNotExist: -# messages.success(request, self.error_message) + def get(self, request, pk): + try: + # Retrieve the subscription object + subscription = self.model.objects.get(id=pk) -# return redirect(self.success_url) + # Checking if there is a Stripe Price ID associated with the subscription + stripe_price_id = subscription.price_id + if stripe_price_id: + stripe.api_key = settings.STRIPE_SECRET_KEY + + try: + # Updating the Stripe price to mark it as inactive + stripe.Price.modify(stripe_price_id, active=False) + except stripe.error.StripeError as e: + # Handle Stripe errors + messages.error(request, f"Stripe error: {str(e)}") + return redirect(self.success_url) + + # Updating the subscription model record + subscription.deleted = True + subscription.active = False + subscription.save() + + messages.success(request, self.success_message) + + except self.model.DoesNotExist: + messages.error(request, self.error_message) + + return redirect(self.success_url) + + +class StripeProductCreateOrUpdateView(LoginRequiredMixin, generic.View): + # Set the page_name and resource + page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS + resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS + + # Initialize the action as ACTION_CREATE (can change based on logic) + action = resource_action.ACTION_CREATE # Default action + + template_name = "manage_subscriptions/product_add.html" + model = StripeProduct + form_class = StripeProductForm + success_url = reverse_lazy("manage_subscriptions:product_list") + error_message = "An error occurred while saving the data." + + # Determine the success message dynamically based on whether it's an update or create + def get_success_message(self): + self.success_message = ( + constants.RECORD_CREATED if not self.object else constants.RECORD_UPDATED + ) + return self.success_message + + # Get the object (if exists) based on URL parameter 'pk' + def get_object(self): + pk = self.kwargs.get("pk") + return get_object_or_404(self.model, pk=pk) if pk else None + + # Add page_name and operation to the context + def get_context_data(self, **kwargs): + context = { + "page_name": self.page_name, + "operation": "Add" if not self.object else "Edit", + } + context.update(kwargs) # Include any additional context data passed to the view + return context + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + + # If an object is found, change action to ACTION_UPDATE + if self.object is not None: + self.action = resource_action.ACTION_UPDATE + + form = self.form_class(instance=self.object) + context = self.get_context_data(form=form) + return render(request, self.template_name, context=context) + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + + # If an object is found, change action to ACTION_UPDATE + if self.object is not None: + self.action = resource_action.ACTION_UPDATE + + form = self.form_class(request.POST, instance=self.object) + if not form.is_valid(): + print(form.errors) + context = self.get_context_data(form=form) + return render(request, self.template_name, context=context) + form.save() + messages.success(self.request, self.get_success_message()) + return redirect(self.success_url) + + +class StripeProductView(LoginRequiredMixin, generic.ListView): + page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS + resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS + action = resource_action.ACTION_READ + model = Subscription + template_name = "manage_subscriptions/product_list.html" + context_object_name = "product_obj" + + def get_queryset(self): + queryset = super().get_queryset().filter(deleted=False, active=True) + return queryset.order_by("-created_on") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["page_name"] = self.page_name + return context + + +class StripeProductDeleteView(LoginRequiredMixin, generic.View): + page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS + resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS + action = resource_action.ACTION_DELETE + model = Subscription + success_url = reverse_lazy("manage_subscriptions:product_list") + success_message = constants.RECORD_DELETED + error_message = constants.RECORD_NOT_FOUND + + def get(self, request, pk): + try: + # Retrieve the subscription object + product = self.model.objects.get(id=pk) + + # Checking if there is a Stripe Product ID associated with the subscription + stripe_product_id = product.product_id + if stripe_product_id: + stripe.api_key = settings.STRIPE_SECRET_KEY + + try: + # Updating the Stripe price to mark it as inactive + stripe.Product.modify(stripe_product_id, active=False) + except stripe.error.StripeError as e: + # Handle Stripe errors + messages.error(request, f"Stripe error: {str(e)}") + return redirect(self.success_url) + + # Updating the subscription model record + product.deleted = True + product.active = False + product.save() + + messages.success(request, self.success_message) + + except self.model.DoesNotExist: + messages.error(request, self.error_message) + + return redirect(self.success_url) # class PlanCreateOrUpdateView(LoginRequiredMixin, generic.View): @@ -419,7 +601,7 @@ class SubscriptionPageView(TemplateView): @csrf_exempt def stripe_config(request): if request.method == "GET": - stripe_config = {"publicKey": settings.STRIPE_TEST_MODE_PUBLISH_KEY} + stripe_config = {"publicKey": settings.STRIPE_PUBLISH_KEY} return JsonResponse(stripe_config, safe=False) @@ -468,7 +650,7 @@ def validate_coupon(request): @require_POST def create_checkout_session(request): success_url = reverse_lazy("manage_subscriptions:stripe") - stripe.api_key = settings.STRIPE_TEST_MODE_SECRET_KEY + stripe.api_key = settings.STRIPE_SECRET_KEY data = json.loads(request.body) print("data: ", data) subscription_id = data.get("subscriptionId", None) diff --git a/templates/manage_coupons/coupon_list.html b/templates/manage_coupons/coupon_list.html index f00bceb..032b54b 100644 --- a/templates/manage_coupons/coupon_list.html +++ b/templates/manage_coupons/coupon_list.html @@ -88,7 +88,7 @@
    -
    +

    {{ event.brand.title }}

    @@ -72,6 +72,7 @@
    + {% if publish %}
    @@ -92,6 +93,7 @@
    + {% endif %}
    From 33a11af52015faca3cd6ddca0a73c2abcaaa9ab2 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Thu, 8 Aug 2024 13:58:45 +0530 Subject: [PATCH 133/187] updated webhook module --- accounts/api/serializers.py | 8 +- .../webhook/payment_processing_service.py | 148 +++++++++++------- goodtimes/webhook/subscription_service.py | 22 +-- manage_subscriptions/api/views.py | 44 ++++++ manage_subscriptions/views.py | 1 - 5 files changed, 141 insertions(+), 82 deletions(-) diff --git a/accounts/api/serializers.py b/accounts/api/serializers.py index e0cfb27..e932668 100644 --- a/accounts/api/serializers.py +++ b/accounts/api/serializers.py @@ -136,8 +136,10 @@ class PasswordResetSerializer(BasePasswordSerializer, serializers.ModelSerialize model = IAmPrincipal fields = ["password", "confirm_password"] + from phonenumbers import parse, phonenumberutil, NumberParseException + class ProfileSerializer(serializers.ModelSerializer): profile_photo = serializers.ImageField(required=False) email = serializers.CharField(read_only=True) @@ -227,6 +229,7 @@ class ProfileSerializer(serializers.ModelSerializer): "has_active_subscription": False, "in_grace_period": False, "grace_period_end_date": None, + "subscription_id": None, } today = timezone.now().date() @@ -245,6 +248,9 @@ class ProfileSerializer(serializers.ModelSerializer): ) # Order by descending grace_period_end_date and take the first if latest_subscription: + subscription_status["subscription_id"] = ( + latest_subscription.stripe_subscription_id + ) # Check if we're within the grace period if today <= latest_subscription.grace_period_end_date: subscription_status["has_active_subscription"] = ( @@ -369,4 +375,4 @@ class AppVersionSerializer(serializers.ModelSerializer): class IAmPrincipalExtendedDataSerializer(serializers.ModelSerializer): class Meta: model = IAmPrincipalExtendedData - fields = "__all__" \ No newline at end of file + fields = "__all__" diff --git a/goodtimes/webhook/payment_processing_service.py b/goodtimes/webhook/payment_processing_service.py index fb9af51..5bdc525 100644 --- a/goodtimes/webhook/payment_processing_service.py +++ b/goodtimes/webhook/payment_processing_service.py @@ -27,35 +27,40 @@ class PaymentProcessingService: self.stripe_subscription = stripe_subscription self.current_period_start = current_period_start self.current_period_end = current_period_end - self.principal_subscription = None @property def charge_data(self): + """Return charge data from the webhook service.""" return self.webhook_service.charge_data @property def principal(self): + """Return the principal from the webhook service.""" return self.webhook_service.get_principal() @property def subscription(self): + """Return the subscription from the webhook service.""" return self.webhook_service.get_subscription() @property def order_id(self): + """Return the order ID from the webhook service.""" return self.webhook_service.get_order_id() @property def coupon(self): + """Return the coupon from the webhook service.""" return self.webhook_service.get_coupon() @property def amount(self): + """Return the final amount from the webhook service.""" return self.webhook_service.get_final_amount() def create_transaction(self): """Create a transaction based on webhook data.""" - transaction = Transaction.objects.create( + return Transaction.objects.create( principal=self.principal, principal_subscription=None, transaction_type=TransactionType.PAYMENT, @@ -65,69 +70,92 @@ class PaymentProcessingService: order_id=self.order_id, comment="Principal Subscription Initiated", ) - return transaction def process_event(self): - event_type = self.webhook_service.event_type - if event_type == "checkout.session.completed": - self.handle_success() - elif event_type == "invoice.payment_succeeded": - if self.charge_data.get("billing_reason") != "subscription_create": - self.handle_success() - else: - self.handle_failure() - - def handle_success(self): + """Process the webhook event.""" with transaction.atomic(): - # Create or update the principal subscription - transactio = self.create_transaction() + event_type = self.webhook_service.event_type + txn = self.create_transaction() try: - self.principal_subscription = ( - self.subscription_service.create_principal_subscription( - principal=self.principal, - subscription=self.subscription, - stripe_subscription=self.stripe_subscription, - order_id=self.order_id, - current_period_start=self.current_period_start, - current_period_end=self.current_period_end, - coupon=self.coupon, - ) - ) - print("First Part Done....!!!!!") - - # Update transaction status to success - self.subscription_service.update_transaction_success( - transactio, self.principal_subscription - ) - print("Second Part Done....!!!!!") - - # Handle referral rewards - referral_service = ReferralRewardService( - self.principal, self.principal_subscription, self.subscription - ) - print("Third Part Done...!!!!!!!!!!!") - referral_service.credit_referral_reward_if_applicable() - print("Fourth Part Done....!!!!!") - - # Send payment success notification - self.notification_service.payment_success_notification( - self.principal, - self.subscription, - self.principal_subscription, - transactio.amount, - ) + if event_type == "checkout.session.completed": + self.handle_success(txn) + elif event_type == "invoice.payment_succeeded": + if self.charge_data.get("billing_reason") != "subscription_create": + self.handle_success(txn) + else: + self.handle_failure(txn) except Exception as e: - transactio.transaction_status = TransactionStatus.FAIL - transactio.error_message = str(e) logger.error(f"Transaction Error: {str(e)}") - transactio.save() raise e - else: - transactio.transaction_status = TransactionStatus.SUCCESS - transactio.save() - def handle_failure(self, transactio): - self.subscription_service.update_transaction_failure(transactio) - # self.notification_service.payment_failed_notification( - # self.principal, self.subscription, self.transaction.amount - # ) + def handle_success(self, transaction): + """Handle a successful payment.""" + try: + self.create_principal_subscription() + self.process_referral_rewards() + self.send_success_notification(transaction) + self.update_transaction_status( + transaction, + TransactionStatus.SUCCESS, + self.subscription_service.principal_subscription, + ) + except Exception as e: + self.handle_failure(transaction, error_message=str(e)) + logger.error(f"Transaction Error: {str(e)}") + raise e + + def create_principal_subscription(self): + """Create or update the principal subscription.""" + self.subscription_service.principal_subscription = ( + self.subscription_service.create_principal_subscription( + principal=self.principal, + subscription=self.subscription, + stripe_subscription=self.stripe_subscription, + order_id=self.order_id, + current_period_start=self.current_period_start, + current_period_end=self.current_period_end, + coupon=self.coupon, + ) + ) + print("Principal Subscription Created") + + def process_referral_rewards(self): + """Handle referral rewards.""" + referral_service = ReferralRewardService( + self.principal, + self.subscription_service.principal_subscription, + self.subscription, + ) + referral_service.credit_referral_reward_if_applicable() + print("Referral Rewards Processed") + + def send_success_notification(self, transaction): + """Send a payment success notification.""" + self.notification_service.payment_success_notification( + self.principal, + self.subscription, + self.subscription_service.principal_subscription, + transaction.amount, + ) + print("Payment Success Notification Sent") + + def handle_failure(self, transaction, error_message=None): + """Handle a failed payment.""" + self.update_transaction_status( + transaction, TransactionStatus.FAIL, error_message=error_message + ) + self.notification_service.payment_failed_notification( + self.principal, self.subscription, transaction + ) + print("Payment Failure Notification Sent") + + def update_transaction_status( + self, transaction, status, principal_subscription=None, error_message=None + ): + """Update the transaction status and associate with a subscription if provided.""" + transaction.transaction_status = status + if principal_subscription: + transaction.principal_subscription = principal_subscription + if error_message: + transaction.error_message = error_message + transaction.save() diff --git a/goodtimes/webhook/subscription_service.py b/goodtimes/webhook/subscription_service.py index 265b27b..0940563 100644 --- a/goodtimes/webhook/subscription_service.py +++ b/goodtimes/webhook/subscription_service.py @@ -2,7 +2,6 @@ from datetime import timedelta from django.utils import timezone import datetime from manage_subscriptions.models import PrincipalSubscription -from manage_wallets.models import TransactionStatus class SubscriptionService: @@ -11,10 +10,12 @@ class SubscriptionService: @property def principal_subscription(self): + """Get the current principal subscription.""" return self._principal_subscription @principal_subscription.setter def principal_subscription(self, value): + """Set the current principal subscription.""" self._principal_subscription = value def create_principal_subscription( @@ -51,16 +52,6 @@ class SubscriptionService: self.principal_subscription = principal_subscription return principal_subscription - def update_transaction_success(self, principal_transaction, principal_subscription): - """Update transaction status to success and associate with a subscription.""" - self._update_transaction_status( - principal_transaction, TransactionStatus.SUCCESS, principal_subscription - ) - - def update_transaction_failure(self, principal_transaction): - """Update transaction status to failure.""" - self._update_transaction_status(principal_transaction, TransactionStatus.FAIL) - def _calculate_dates( self, current_period_start, current_period_end, subscription_days ): @@ -83,12 +74,3 @@ class SubscriptionService: coupon.no_of_redeems += 1 coupon.save() print("Coupon Saved Successfully!!!") - - def _update_transaction_status( - self, transaction, status, principal_subscription=None - ): - """Update the transaction status and associate with a subscription if provided.""" - transaction.transaction_status = status - if principal_subscription: - transaction.principal_subscription = principal_subscription - transaction.save() diff --git a/manage_subscriptions/api/views.py b/manage_subscriptions/api/views.py index 63f4321..44acebb 100644 --- a/manage_subscriptions/api/views.py +++ b/manage_subscriptions/api/views.py @@ -293,3 +293,47 @@ class LastActiveSubscriptionView(APIView): message="No Active Subscription Found", errors="No Active Subscription Found", ) + + +class CancelSubscription(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + def post(self, request): + data = json.loads(request.body) + subscription_id = data.get("subscription_id") + + try: + subscription = PrincipalSubscription.objects.get( + id=subscription_id, principal=request.user + ) + except PrincipalSubscription.DoesNotExist: + return ApiResponse( + message=constants.FAILURE, + errors="Subscription not found.", + status=status.HTTP_404_NOT_FOUND, + ) + + with transaction.atomic(): + if subscription.is_stripe_subscription: + # Cancel Stripe subscription + try: + stripe.Subscription.modify(subscription.stripe_subscription_id, cancel_at_period_end=True) + except stripe.error.InvalidRequestError as e: + return ApiResponse( + message=constants.FAILURE, + errors=f"Stripe error: {str(e)}", + status=status.HTTP_400_BAD_REQUEST, + ) + + # Updating subscription status in the local database + subscription.status = SubscriptionStatus.INACTIVE + subscription.cancelled = True + subscription.cancelled_date_time = timezone.now() + subscription.save() + + return ApiResponse( + message=constants.SUCCESS, + data="Subscription cancelled successfully.", + status=status.HTTP_200_OK, + ) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index b22b7b8..23cb784 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -727,7 +727,6 @@ def create_checkout_session(request): order_id = f"order_{timezone.localtime().timestamp()}_{request.user.email}" # Default transaction amount based on subscription amount - final_amount = subscription.amount print("Before Session Data") session_data = { "payment_method_types": ["card"], From e8b90b8fdb6475d848a9b3c069b6e2945fd1d910 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Thu, 8 Aug 2024 14:07:59 +0530 Subject: [PATCH 134/187] updated webhook module --- goodtimes/webhook/payment_processing_service.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/goodtimes/webhook/payment_processing_service.py b/goodtimes/webhook/payment_processing_service.py index 5bdc525..cfd83b9 100644 --- a/goodtimes/webhook/payment_processing_service.py +++ b/goodtimes/webhook/payment_processing_service.py @@ -80,9 +80,11 @@ class PaymentProcessingService: if event_type == "checkout.session.completed": self.handle_success(txn) elif event_type == "invoice.payment_succeeded": + print("self.charge_data.billing_reason", self.charge_data.get("billing_reason")) if self.charge_data.get("billing_reason") != "subscription_create": self.handle_success(txn) else: + print("Should not be here...") self.handle_failure(txn) except Exception as e: logger.error(f"Transaction Error: {str(e)}") From 70c5c815fa265565056906650c3baf95ce696faf Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Thu, 8 Aug 2024 14:59:01 +0530 Subject: [PATCH 135/187] updated webhook module --- goodtimes/webhook/payment_processing_service.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/goodtimes/webhook/payment_processing_service.py b/goodtimes/webhook/payment_processing_service.py index cfd83b9..2359d9a 100644 --- a/goodtimes/webhook/payment_processing_service.py +++ b/goodtimes/webhook/payment_processing_service.py @@ -83,8 +83,7 @@ class PaymentProcessingService: print("self.charge_data.billing_reason", self.charge_data.get("billing_reason")) if self.charge_data.get("billing_reason") != "subscription_create": self.handle_success(txn) - else: - print("Should not be here...") + elif event_type == "checkout.session.expired" or event_type == "invoice.payment_failed": self.handle_failure(txn) except Exception as e: logger.error(f"Transaction Error: {str(e)}") From 32a834726ef65ff8c586344365bca780b4152864 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Thu, 8 Aug 2024 17:21:10 +0530 Subject: [PATCH 136/187] updated webhook module --- .../webhook/payment_processing_service.py | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/goodtimes/webhook/payment_processing_service.py b/goodtimes/webhook/payment_processing_service.py index 2359d9a..ce0284b 100644 --- a/goodtimes/webhook/payment_processing_service.py +++ b/goodtimes/webhook/payment_processing_service.py @@ -75,15 +75,22 @@ class PaymentProcessingService: """Process the webhook event.""" with transaction.atomic(): event_type = self.webhook_service.event_type - txn = self.create_transaction() try: - if event_type == "checkout.session.completed": - self.handle_success(txn) - elif event_type == "invoice.payment_succeeded": - print("self.charge_data.billing_reason", self.charge_data.get("billing_reason")) + if event_type == "invoice.payment_succeeded": if self.charge_data.get("billing_reason") != "subscription_create": + txn = self.create_transaction() self.handle_success(txn) - elif event_type == "checkout.session.expired" or event_type == "invoice.payment_failed": + elif event_type == "checkout.session.completed": + txn = self.create_transaction() + self.handle_success(txn) + elif event_type in ["checkout.session.expired", "invoice.payment_failed"]: + order_id = self.order_id + try: + txn = Transaction.objects.get(order_id=order_id) + txn.transaction_status = TransactionStatus.FAIL + return + except Transaction.DoesNotExist: + txn = self.create_transaction() self.handle_failure(txn) except Exception as e: logger.error(f"Transaction Error: {str(e)}") @@ -146,7 +153,7 @@ class PaymentProcessingService: transaction, TransactionStatus.FAIL, error_message=error_message ) self.notification_service.payment_failed_notification( - self.principal, self.subscription, transaction + self.principal, self.subscription, transaction.amount ) print("Payment Failure Notification Sent") From e0ed41f096b749a2232392ff2d416f091ad2d3bd Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Thu, 8 Aug 2024 17:31:05 +0530 Subject: [PATCH 137/187] updated webhook module --- .../subscription_list.html | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/templates/manage_subscriptions/subscription_list.html b/templates/manage_subscriptions/subscription_list.html index 6333106..c52486d 100644 --- a/templates/manage_subscriptions/subscription_list.html +++ b/templates/manage_subscriptions/subscription_list.html @@ -102,16 +102,17 @@
    • - - - + width="24" height="24" viewBox="0 0 24 24" fill="none" + stroke="currentColor" stroke-width="2" + stroke-linecap="round" stroke-linejoin="round" + class="feather feather-trash p-1 br-8 mb-1"> + + + +
    • From 4c2d693ebfed18f6caa2dd44a7586bb72ee0f33f Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Thu, 8 Aug 2024 18:07:23 +0530 Subject: [PATCH 138/187] updated webhook module --- goodtimes/webhook/payment_processing_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/goodtimes/webhook/payment_processing_service.py b/goodtimes/webhook/payment_processing_service.py index ce0284b..30906c8 100644 --- a/goodtimes/webhook/payment_processing_service.py +++ b/goodtimes/webhook/payment_processing_service.py @@ -88,6 +88,7 @@ class PaymentProcessingService: try: txn = Transaction.objects.get(order_id=order_id) txn.transaction_status = TransactionStatus.FAIL + txn.save() return except Transaction.DoesNotExist: txn = self.create_transaction() From 234e9e900ac9315e8ed3e82b31b164c9dfb15877 Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Sun, 11 Aug 2024 21:10:52 +0530 Subject: [PATCH 139/187] refactor(Profile):changed profile serializer --- accounts/api/serializers.py | 77 ++----------------------------------- 1 file changed, 3 insertions(+), 74 deletions(-) diff --git a/accounts/api/serializers.py b/accounts/api/serializers.py index fd39cc8..fe67d96 100644 --- a/accounts/api/serializers.py +++ b/accounts/api/serializers.py @@ -141,15 +141,9 @@ from phonenumbers import parse, phonenumberutil, NumberParseException class ProfileSerializer(serializers.ModelSerializer): profile_photo = serializers.ImageField(required=False) - email = serializers.CharField(read_only=True) - invite_count = serializers.SerializerMethodField(read_only=True) principal_type_name = serializers.SerializerMethodField(read_only=True) - has_active_subscription = serializers.SerializerMethodField(read_only=True) - has_preferences = serializers.SerializerMethodField(read_only=True) - register_complete = serializers.BooleanField(read_only=True) + email = serializers.CharField(read_only=True) is_active = serializers.BooleanField(read_only=True) - going_events_count = serializers.SerializerMethodField(read_only=True) - interested_events_count = serializers.SerializerMethodField(read_only=True) phone_no = serializers.CharField(required=True) class Meta: @@ -163,18 +157,12 @@ class ProfileSerializer(serializers.ModelSerializer): "last_name", "phone_no", "email", - "invite_count", - "register_complete", - "has_active_subscription", - "has_preferences", "linkedin_profile", "youtube_profile", "facebook_profile", "instagram_profile", "website", "is_active", - "going_events_count", - "interested_events_count", ] # def validate_phone_no(self, value): @@ -196,71 +184,14 @@ class ProfileSerializer(serializers.ModelSerializer): instance.last_name = validated_data.get("last_name", instance.last_name) return super().update(instance, validated_data) - def get_going_events_count(self, obj): - return EventPrincipalInteraction.objects.filter( - principal=obj, status="going" - ).count() - - def get_interested_events_count(self, obj): - return EventPrincipalInteraction.objects.filter( - principal=obj, status="interested" - ).count() - - def get_invite_count(self, obj): - if obj: - return ReferralRecord.get_invite_count(obj) - return 0 - - def get_principal_type_name(self, obj): - return obj.principal_type.name if obj.principal_type else None - - def get_has_preferences(self, obj): - return PrincipalPreference.objects.filter(principal=obj).exists() - def get_image_url(self, obj, field_name, request): image_field = getattr(obj, field_name) if image_field: return request.build_absolute_uri(image_field.url) return "" - def get_has_active_subscription(self, obj): - subscription_status = { - "has_active_subscription": False, - "in_grace_period": False, - "grace_period_end_date": None, - } - today = timezone.now().date() - - # Attempt to find the active subscription with the furthest grace_period_end_date - latest_subscription = ( - PrincipalSubscription.objects.filter( - principal=obj, - is_paid=True, - cancelled=False, - deleted=False, - active=True, - status=SubscriptionStatus.ACTIVE, - ) - .order_by("-grace_period_end_date") - .first() - ) # Order by descending grace_period_end_date and take the first - - if latest_subscription: - # Check if we're within the grace period - if today <= latest_subscription.grace_period_end_date: - subscription_status["has_active_subscription"] = ( - today <= latest_subscription.end_date - ) - subscription_status["in_grace_period"] = ( - latest_subscription.end_date - < today - <= latest_subscription.grace_period_end_date - ) - subscription_status["grace_period_end_date"] = ( - latest_subscription.grace_period_end_date - ) - - return subscription_status + def get_principal_type_name(self, obj): + return obj.principal_type.name if obj.principal_type else None def to_representation(self, instance): data = super().to_representation(instance) @@ -274,7 +205,6 @@ class ProfileExtendedDataSerializer(serializers.ModelSerializer): has_active_subscription = serializers.SerializerMethodField(read_only=True) preference = serializers.SerializerMethodField(read_only=True) register_complete = serializers.BooleanField(read_only=True) - is_active = serializers.BooleanField(read_only=True) going_events_count = serializers.SerializerMethodField(read_only=True) interested_events_count = serializers.SerializerMethodField(read_only=True) feature_limit = serializers.SerializerMethodField(read_only=True) @@ -287,7 +217,6 @@ class ProfileExtendedDataSerializer(serializers.ModelSerializer): "register_complete", "has_active_subscription", "preference", - "is_active", "going_events_count", "interested_events_count", "feature_limit" From 1a2013e10b1e3b04e2bd63f74195c7300adb37bc Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 12 Aug 2024 12:54:29 +0530 Subject: [PATCH 140/187] my subscriptions page --- goodtimes/webhook/subscription_service.py | 1 + manage_coupons/forms.py | 28 +-- manage_coupons/models.py | 41 +++++ manage_subscriptions/forms.py | 2 - manage_subscriptions/models.py | 16 ++ manage_subscriptions/urls.py | 11 +- manage_subscriptions/views.py | 161 +++++++++++++----- .../manage_subscriptions/product_list.html | 25 +-- .../subscription_details.html | 77 +++++++++ .../subscription_list.html | 8 +- .../stripe_html/active_subscription.html | 70 ++++++++ 11 files changed, 337 insertions(+), 103 deletions(-) create mode 100644 templates/manage_subscriptions/subscription_details.html create mode 100644 templates/stripe_html/active_subscription.html diff --git a/goodtimes/webhook/subscription_service.py b/goodtimes/webhook/subscription_service.py index 0940563..68770f1 100644 --- a/goodtimes/webhook/subscription_service.py +++ b/goodtimes/webhook/subscription_service.py @@ -38,6 +38,7 @@ class SubscriptionService: subscription=subscription, stripe_subscription_id=stripe_subscription or "Non Recurring", is_paid=True, + auto_renew=bool(stripe_subscription), is_stripe_subscription=bool(stripe_subscription), order_id=order_id, start_date=start_date, diff --git a/manage_coupons/forms.py b/manage_coupons/forms.py index e40b3eb..22874e5 100644 --- a/manage_coupons/forms.py +++ b/manage_coupons/forms.py @@ -9,7 +9,7 @@ class CouponForm(forms.ModelForm): fields = [ "title", "description", - "image", + # "image", "discount_amount", "discount_percentage", "valid_from", @@ -19,28 +19,6 @@ class CouponForm(forms.ModelForm): widgets = { "valid_from": forms.DateTimeInput(attrs={"type": "datetime-local"}), "valid_to": forms.DateTimeInput(attrs={"type": "datetime-local"}), + # "discount_amount": forms.NumberInput(attrs={'step': '0.01'}), + # "discount_percentage": forms.NumberInput(attrs={'step': '0.01'}), } - - def clean(self): - cleaned_data = super().clean() - discount_amount = cleaned_data.get("discount_amount") - discount_percentage = cleaned_data.get("discount_percentage") - valid_from = cleaned_data.get("valid_from") - valid_to = cleaned_data.get("valid_to") - - if discount_amount and discount_percentage: - raise ValidationError( - "You can only set either a discount amount or a discount percentage, not both." - ) - - if not discount_amount and not discount_percentage: - raise ValidationError( - "You must set either a discount amount or a discount percentage." - ) - - if valid_from and valid_to and valid_from >= valid_to: - raise ValidationError( - "The valid_from date must be earlier than the valid_to date." - ) - - return cleaned_data diff --git a/manage_coupons/models.py b/manage_coupons/models.py index 25ce934..743ce43 100644 --- a/manage_coupons/models.py +++ b/manage_coupons/models.py @@ -1,6 +1,7 @@ from django.db import models from django.utils import timezone from accounts.models import BaseModel, IAmPrincipalType +from django.core.exceptions import ValidationError class Coupon(BaseModel): @@ -23,6 +24,46 @@ class Coupon(BaseModel): class Meta: db_table = "coupon" + def clean(self): + """ + Validate the Coupon instance. Ensure that the `max_redeems` is greater than 0, + that either `discount_amount` or `discount_percentage` is set, and that + `valid_from` is earlier than `valid_to`. + """ + if self.max_redeems < 1: + raise ValidationError({"max_redeems": "Redeems must be more than 1."}) + + # Ensure discount_amount is non-negative + if self.discount_amount is not None and self.discount_amount < 1: + raise ValidationError( + {"discount_amount": "Discount amount must be more than 1."} + ) + + # Ensure discount_percentage is non-negative + if self.discount_percentage is not None and self.discount_percentage < 1: + raise ValidationError( + {"discount_percentage": "Discount percentage must be more than 1."} + ) + + if self.discount_amount and self.discount_percentage: + raise ValidationError( + "You can only set either a discount amount or a discount percentage, not both." + ) + + if not self.discount_amount and not self.discount_percentage: + raise ValidationError( + "You must set either a discount amount or a discount percentage." + ) + + if self.valid_from and self.valid_to and self.valid_from >= self.valid_to: + raise ValidationError( + "The valid_from date must be earlier than the valid_to date." + ) + + def save(self, *args, **kwargs): + self.clean() # Call clean before saving to ensure validation + super().save(*args, **kwargs) + def __str__(self): return self.coupon_code diff --git a/manage_subscriptions/forms.py b/manage_subscriptions/forms.py index ffbe30e..12294f5 100644 --- a/manage_subscriptions/forms.py +++ b/manage_subscriptions/forms.py @@ -31,8 +31,6 @@ class SubscriptionForm(forms.ModelForm): "high_amount", "amount", "short_description", - # "long_description", - # "image", "principal_types", "referral_percentage", "active", diff --git a/manage_subscriptions/models.py b/manage_subscriptions/models.py index 0aea65d..0c3ec19 100644 --- a/manage_subscriptions/models.py +++ b/manage_subscriptions/models.py @@ -1,5 +1,6 @@ from datetime import timedelta, timezone from django.db import models +from django.core.exceptions import ValidationError from accounts.models import BaseModel, IAmPrincipal, IAmPrincipalType from django.utils.translation import gettext_lazy as _ @@ -65,6 +66,21 @@ class Subscription(BaseModel): def __str__(self): return self.title + def clean(self): + # Ensure amount is greater than 1 + if self.amount <= 1: + raise ValidationError({"amount": "Amount must be greater than 1."}) + + # Ensure high_amount is greater than amount + if self.high_amount <= self.amount: + raise ValidationError( + {"high_amount": "High amount must be greater than amount."} + ) + + def save(self, *args, **kwargs): + self.clean() # Call clean before saving to ensure validation + super().save(*args, **kwargs) + class SubscriptionStatus(models.TextChoices): ACTIVE = "active", _("Active") diff --git a/manage_subscriptions/urls.py b/manage_subscriptions/urls.py index fee84c5..840de54 100644 --- a/manage_subscriptions/urls.py +++ b/manage_subscriptions/urls.py @@ -12,6 +12,7 @@ urlpatterns = [ views.SubscriptionCreateOrUpdateView.as_view(), name="subscription_add", ), + path("subscription//", views.SubscriptionDetailView.as_view(), name="subscription_detail"), # path( # "subscription/edit//", # views.SubscriptionCreateOrUpdateView.as_view(), @@ -31,11 +32,11 @@ urlpatterns = [ views.StripeProductCreateOrUpdateView.as_view(), name="stripe_product_add", ), - path( - "product/delete/", - views.StripeProductDeleteView.as_view(), - name="stripe_product_delete", - ), + # path( + # "product/delete/", + # views.StripeProductDeleteView.as_view(), + # name="stripe_product_delete", + # ), # PLANS path("plan/list/", views.PlanView.as_view(), name="plan_list"), # path( diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 23cb784..cfb8348 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -21,7 +21,13 @@ from manage_wallets.models import ( TransactionStatus, TransactionType, ) -from .models import Plan, StripeProduct, Subscription, PrincipalSubscription +from .models import ( + Plan, + StripeProduct, + Subscription, + PrincipalSubscription, + SubscriptionStatus, +) from django.views import generic from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse_lazy @@ -179,6 +185,20 @@ class SubscriptionView(LoginRequiredMixin, generic.ListView): return context +class SubscriptionDetailView(generic.DetailView): + page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS + resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS + action = resource_action.ACTION_READ + model = Subscription + template_name = "manage_subscriptions/subscription_details.html" + context_object_name = "subscription" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["page_name"] = self.page_name + return context + + class SubscriptionDeleteView(LoginRequiredMixin, generic.View): page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS @@ -325,44 +345,61 @@ class StripeProductView(LoginRequiredMixin, generic.ListView): return context -class StripeProductDeleteView(LoginRequiredMixin, generic.View): - page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS - resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS - action = resource_action.ACTION_DELETE - model = StripeProduct - success_url = reverse_lazy("manage_subscriptions:stripe_product_list") - success_message = constants.RECORD_DELETED - error_message = constants.RECORD_NOT_FOUND +""" we are not using product delete functionality because there may be multiple stripe's prices + attached to one product and in case of any error it will mismatch the stripe's price with + our database Subscription objects""" +# class StripeProductDeleteView(LoginRequiredMixin, generic.View): +# page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS +# resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS +# action = resource_action.ACTION_DELETE +# model = StripeProduct +# success_url = reverse_lazy("manage_subscriptions:stripe_product_list") +# success_message = constants.RECORD_DELETED +# error_message = constants.RECORD_NOT_FOUND - def get(self, request, pk): - try: - # Retrieve the subscription object - product = self.model.objects.get(id=pk) +# def get(self, request, pk): +# try: +# # Retrieve the subscription object +# product = self.model.objects.get(id=pk) - # Checking if there is a Stripe Product ID associated with the subscription - stripe_product_id = product.product_id - if stripe_product_id: - stripe.api_key = settings.STRIPE_SECRET_KEY +# # Fetching the related subscriptions (prices) +# related_subscriptions = Subscription.objects.filter(stripe_product=product) - try: - # Updating the Stripe price to mark it as inactive - stripe.Product.modify(stripe_product_id, active=False) - except stripe.error.StripeError as e: - # Handle Stripe errors - messages.error(request, f"Stripe error: {str(e)}") - return redirect(self.success_url) +# # Checking if there is a Stripe Product ID associated with the subscription +# stripe_product_id = product.product_id +# if stripe_product_id: +# stripe.api_key = settings.STRIPE_SECRET_KEY - # Updating the subscription model record - product.deleted = True - product.active = False - product.save() +# # Deactivating related prices on Stripe first +# for subscription in related_subscriptions: +# price_id = subscription.price_id +# if price_id: +# try: +# stripe.Price.modify(price_id, active=False) +# except stripe.error.StripeError as e: +# # Handle Stripe errors +# messages.error(request, f"Stripe error: {str(e)}") +# return redirect(self.success_url) - messages.success(request, self.success_message) +# try: +# # Updating the Stripe price to mark it as inactive +# stripe.Product.modify(stripe_product_id, active=False) +# except stripe.error.StripeError as e: +# # Handle Stripe errors +# messages.error(request, f"Stripe error: {str(e)}") +# return redirect(self.success_url) - except self.model.DoesNotExist: - messages.error(request, self.error_message) +# # Updating the subscription model record +# product.deleted = True +# product.active = False +# product.save() - return redirect(self.success_url) +# messages.success(request, self.success_message) + +# except self.model.DoesNotExist: +# messages.error(request, self.error_message) + +# return redirect(self.success_url) # class PlanCreateOrUpdateView(LoginRequiredMixin, generic.View): @@ -572,6 +609,32 @@ class PrincipalSubscriptionDeleteView(LoginRequiredMixin, generic.View): class SubscriptionPageView(TemplateView): template_name = "stripe_html/index.html" + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + request = self.request + if request.user.is_authenticated: + print("request.user: ", request.user) + subscriptions = Subscription.objects.filter( + principal_types=request.user.principal_type, + active=True, + deleted=False, + is_free=False, + ) + + if subscriptions.exists(): + context["subscriptions"] = subscriptions + context["stripeCheckoutUrl"] = settings.STRIPE_CHECKOUT_URL + context["stripeFinalUrl"] = settings.STRIPE_FINAL_URL + context["couponValidityCheckUrl"] = settings.COUPON_VALIDITY_CHECK_URL + else: + # Handling the case where no subscriptions are found for the principal type. + context["error"] = "No subscriptions found for your user type." + return context + + +class ActiveSubscriptionView(TemplateView): + template_name = "stripe_html/active_subscription.html" + def get(self, request, *args, **kwargs): # Example of extracting the token from a query parameter or cookie token = request.GET.get("token") or request.session.get("jwt") @@ -608,22 +671,25 @@ class SubscriptionPageView(TemplateView): context = super().get_context_data(**kwargs) request = self.request if request.user.is_authenticated: - print("request.user: ", request.user) - subscriptions = Subscription.objects.filter( - principal_types=request.user.principal_type, - active=True, - deleted=False, - is_free=False, + active_subscription = ( + PrincipalSubscription.objects.filter( + principal=request.user, + is_paid=True, + cancelled=False, + deleted=False, + active=True, + status=SubscriptionStatus.ACTIVE, + ) + .order_by("-grace_period_end_date") + .first() ) - if subscriptions.exists(): - context["subscriptions"] = subscriptions - context["stripeCheckoutUrl"] = settings.STRIPE_CHECKOUT_URL - context["stripeFinalUrl"] = settings.STRIPE_FINAL_URL - context["couponValidityCheckUrl"] = settings.COUPON_VALIDITY_CHECK_URL + if active_subscription: + context["active_subscription"] = active_subscription else: - # Handling the case where no subscriptions are found for the principal type. - context["error"] = "No subscriptions found for your user type." + # If no active subscription is found, redirect to the SubscriptionPageView + return redirect("manage_subscriptions:stripe") + return context @@ -691,7 +757,10 @@ def validate_coupon(request): return JsonResponse( {"error": "Coupon max redeems reached."}, status=400 ) - return JsonResponse({"data": {"coupon": stripe_coupon, "finalAmount": final_amount}}, status=200) + return JsonResponse( + {"data": {"coupon": stripe_coupon, "finalAmount": final_amount}}, + status=200, + ) except stripe.error.InvalidRequestError: return JsonResponse( {"error": f"Invalid coupon code: {coupon_code}"}, status=400 diff --git a/templates/manage_subscriptions/product_list.html b/templates/manage_subscriptions/product_list.html index 6cc9c14..3551f4b 100644 --- a/templates/manage_subscriptions/product_list.html +++ b/templates/manage_subscriptions/product_list.html @@ -49,9 +49,6 @@ style="width: 69.2656px;"> Stripe Product ID Active - Action @@ -63,29 +60,9 @@ {{data_obj.active}} - - - + {% endfor %} -
    diff --git a/templates/manage_subscriptions/subscription_details.html b/templates/manage_subscriptions/subscription_details.html new file mode 100644 index 0000000..44833a7 --- /dev/null +++ b/templates/manage_subscriptions/subscription_details.html @@ -0,0 +1,77 @@ +{% extends 'layout/base_template.html' %} +{% load static %} + +{% block content %} +
    +
    + +
    +
    +
    + {% if subscription.image %} + {{ subscription.title }} + {% endif %} +

    {{ subscription.title }}

    +
    +
    +
    + + +
    +
    +
    +
    Plan & Pricing
    +
    +
    +

    Plan: {{ subscription.plan.title }}

    +

    Price ID: {{ subscription.price_id|default:"Not a Stripe Subscription" }}

    +

    Stripe Product: {{ subscription.stripe_product|default:"None" }}

    +

    Amount: ${{ subscription.amount }}

    +

    High Amount: ${{ subscription.high_amount }}

    +

    Referral Percentage: {{ subscription.referral_percentage }}%

    +

    Is Free: + {% if subscription.is_free %} + Yes + {% else %} + No + {% endif %} +

    +
    +
    +
    + + +
    +
    +
    +
    Descriptions
    +
    +
    +

    Short Description: {{ subscription.short_description|default:"Not Provided" }}

    +

    Long Description:

    +

    {{ subscription.long_description|default:"Not Provided" }}

    +
    +
    +
    + + +
    +
    +
    +
    Principal Types
    +
    +
    +
      + {% for principal_type in subscription.principal_types.all %} +
    • {{ principal_type.name }}
    • + {% endfor %} +
    +
    +
    +
    + +
    +
    + +{% endblock content %} + \ No newline at end of file diff --git a/templates/manage_subscriptions/subscription_list.html b/templates/manage_subscriptions/subscription_list.html index c52486d..41afe54 100644 --- a/templates/manage_subscriptions/subscription_list.html +++ b/templates/manage_subscriptions/subscription_list.html @@ -115,7 +115,13 @@ - +
  • + + + visibility + + +
  • diff --git a/templates/stripe_html/active_subscription.html b/templates/stripe_html/active_subscription.html new file mode 100644 index 0000000..06f440a --- /dev/null +++ b/templates/stripe_html/active_subscription.html @@ -0,0 +1,70 @@ + + + + + + Active Subscription + + + + +
    +
    +
    +

    Your Active Subscription

    +
    +
    + +
    +
    +
    Subscription:
    +

    {{ active_subscription.subscription.name }}

    +
    +
    +
    Principal:
    +

    {{ active_subscription.principal.first_name }} {{ active_subscription.principal.last_name }}

    +
    +
    + +
    +
    +
    Status:
    +

    {{ active_subscription.get_status_display }}

    +
    +
    +
    Start Date:
    +

    {{ active_subscription.start_date }}

    +
    +
    + +
    +
    +
    End Date:
    +

    {{ active_subscription.end_date }}

    +
    +
    +
    Auto Renew:
    +

    {{ active_subscription.auto_renew|yesno:"Yes,No" }}

    +
    +
    + + +
    +
    +
    Coupon Code:
    +

    {{ active_subscription.coupon_code }}

    +
    +
    + + + +
    +
    +
    + + + + + From 15a7308cb8930a68f7eefc902672784643a5d831 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 12 Aug 2024 12:57:22 +0530 Subject: [PATCH 141/187] my subscriptions page 2 --- manage_subscriptions/urls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manage_subscriptions/urls.py b/manage_subscriptions/urls.py index 840de54..5b1f6fb 100644 --- a/manage_subscriptions/urls.py +++ b/manage_subscriptions/urls.py @@ -91,6 +91,7 @@ urlpatterns = [ name="validate_coupon", ), path("stripe/", views.SubscriptionPageView.as_view(), name="stripe"), + path("active/", views.ActiveSubscriptionView.as_view(), name="active"), path("success/", views.SuccessView.as_view(), name="success"), path("cancel/", views.CancelView.as_view(), name="cancel"), # path("join-now/", views.IndexView.as_view(), name="index"), From ee27d86010313ed499f6d97c88fa2040ed4ab125 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 12 Aug 2024 13:47:12 +0530 Subject: [PATCH 142/187] my subscriptions page 3 --- manage_subscriptions/urls.py | 1 + manage_subscriptions/views.py | 38 ++++- .../stripe_html/active_subscription.html | 149 ++++++++++++------ 3 files changed, 140 insertions(+), 48 deletions(-) diff --git a/manage_subscriptions/urls.py b/manage_subscriptions/urls.py index 5b1f6fb..0085713 100644 --- a/manage_subscriptions/urls.py +++ b/manage_subscriptions/urls.py @@ -92,6 +92,7 @@ urlpatterns = [ ), path("stripe/", views.SubscriptionPageView.as_view(), name="stripe"), path("active/", views.ActiveSubscriptionView.as_view(), name="active"), + path("active/", views.CancelSubscriptionView.as_view(), name="cancel_subscription"), path("success/", views.SuccessView.as_view(), name="success"), path("cancel/", views.CancelView.as_view(), name="cancel"), # path("join-now/", views.IndexView.as_view(), name="index"), diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index cfb8348..c7ba6a1 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -38,6 +38,7 @@ from django.views.decorators.http import require_POST from django.conf import settings from django.views.generic.base import TemplateView from django.db.models import Q +from django.db import transaction # Create your views here. @@ -670,6 +671,7 @@ class ActiveSubscriptionView(TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) request = self.request + today = timezone.now().date() if request.user.is_authenticated: active_subscription = ( PrincipalSubscription.objects.filter( @@ -684,7 +686,7 @@ class ActiveSubscriptionView(TemplateView): .first() ) - if active_subscription: + if active_subscription and active_subscription.end_date > today: context["active_subscription"] = active_subscription else: # If no active subscription is found, redirect to the SubscriptionPageView @@ -693,6 +695,40 @@ class ActiveSubscriptionView(TemplateView): return context +class CancelSubscriptionView(LoginRequiredMixin, generic.View): + def post(self, request, *args, **kwargs): + subscription_id = request.POST.get("subscription_id") + + try: + subscription = PrincipalSubscription.objects.get( + id=subscription_id, principal=request.user + ) + except PrincipalSubscription.DoesNotExist: + messages.error(request, "Subscription not found.") + return redirect("manage_subscriptions:cancel") + + try: + with transaction.atomic(): + if subscription.is_stripe_subscription: + # Cancel Stripe subscription + stripe.Subscription.modify( + subscription.stripe_subscription_id, cancel_at_period_end=True + ) + + # Updating subscription status in the local database + subscription.status = SubscriptionStatus.INACTIVE + subscription.cancelled = True + subscription.active = False + subscription.cancelled_date_time = timezone.now() + subscription.save() + + messages.success(request, "Subscription cancelled successfully.") + return redirect("manage_subscriptions:success") + except stripe.error.InvalidRequestError as e: + messages.error(request, f"Stripe error: {str(e)}") + return redirect("manage_subscriptions:cancel") + + @csrf_exempt def stripe_config(request): if request.method == "GET": diff --git a/templates/stripe_html/active_subscription.html b/templates/stripe_html/active_subscription.html index 06f440a..f489d9f 100644 --- a/templates/stripe_html/active_subscription.html +++ b/templates/stripe_html/active_subscription.html @@ -1,70 +1,125 @@ + Active Subscription - + + -
    + +
    +

    Your Active Subscription

    +
    + +
    -
    -

    Your Active Subscription

    +
    +

    {{ active_subscription.subscription.name }}

    - -
    -
    -
    Subscription:
    -

    {{ active_subscription.subscription.name }}

    -
    -
    -
    Principal:
    -

    {{ active_subscription.principal.first_name }} {{ active_subscription.principal.last_name }}

    -
    -
    +

    Status: {{ active_subscription.status }}

    +

    Start Date: {{ active_subscription.start_date }}

    +

    End Date: {{ active_subscription.end_date }}

    +

    Auto Renew: {{ active_subscription.auto_renew }}

    -
    -
    -
    Status:
    -

    {{ active_subscription.get_status_display }}

    -
    -
    -
    Start Date:
    -

    {{ active_subscription.start_date }}

    -
    + {% if active_subscription.cancelled %} +
    +

    Cancellation Details

    +

    Cancelled: Yes

    +

    Cancellation Date: {{ active_subscription.cancelled_date_time }}

    +

    Grace Period Ends: {{ active_subscription.grace_period_end_date }}

    + {% endif %} -
    -
    -
    End Date:
    -

    {{ active_subscription.end_date }}

    + {% if active_subscription.auto_renew and not active_subscription.cancelled %} +
    +

    Cancel Subscription

    +
    + {% csrf_token %} + + +
    -
    -
    Auto Renew:
    -

    {{ active_subscription.auto_renew|yesno:"Yes,No" }}

    -
    -
    + {% endif %} - -
    -
    -
    Coupon Code:
    -

    {{ active_subscription.coupon_code }}

    -
    -
    - - -
    - - + From 6e93793665ebbdeea3e92589672756799244bae2 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 12 Aug 2024 14:49:48 +0530 Subject: [PATCH 143/187] my subscriptions page 4 --- manage_subscriptions/views.py | 45 ++++++++++++++--------------------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index c7ba6a1..3ae8910 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -1,6 +1,6 @@ from decimal import Decimal import json -from django.http import HttpResponseBadRequest, JsonResponse +from django.http import HttpResponseBadRequest, HttpResponseRedirect, JsonResponse from django.shortcuts import get_object_or_404, redirect, render import stripe from accounts import resource_action @@ -30,7 +30,7 @@ from .models import ( ) from django.views import generic from django.contrib.auth.mixins import LoginRequiredMixin -from django.urls import reverse_lazy +from django.urls import reverse, reverse_lazy from django.contrib import messages from goodtimes import constants from django.views.decorators.csrf import csrf_exempt @@ -637,7 +637,6 @@ class ActiveSubscriptionView(TemplateView): template_name = "stripe_html/active_subscription.html" def get(self, request, *args, **kwargs): - # Example of extracting the token from a query parameter or cookie token = request.GET.get("token") or request.session.get("jwt") print("token: ", token) if token: @@ -647,29 +646,21 @@ class ActiveSubscriptionView(TemplateView): # Decode and validate token payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) print("payload: ", payload) - try: - UserModel = get_user_model() - user = UserModel.objects.get(id=payload["user_id"]) - # Manually specify the authentication backend - user.backend = "django.contrib.auth.backends.ModelBackend" - # Log the user in - login(request, user) - print("Logged in user: ", user) - - except IAmPrincipal.DoesNotExist: - # Handle expired token - return HttpResponseBadRequest("No Principal Found") - - except jwt.ExpiredSignatureError: - # Handle expired token - return HttpResponseBadRequest("Expired Signature Error") - except jwt.InvalidTokenError: - return HttpResponseBadRequest("Invalid Token Error") - + user = get_user_model().objects.get(id=payload["user_id"]) + # Manually specify the authentication backend + user.backend = "django.contrib.auth.backends.ModelBackend" + # Log the user in + login(request, user) + print("Logged in user: ", user) + except ( + IAmPrincipal.DoesNotExist, + jwt.ExpiredSignatureError, + jwt.InvalidTokenError, + ): + return HttpResponseBadRequest("Invalid token or user not found") return super().get(request, *args, **kwargs) def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) request = self.request today = timezone.now().date() if request.user.is_authenticated: @@ -682,17 +673,17 @@ class ActiveSubscriptionView(TemplateView): active=True, status=SubscriptionStatus.ACTIVE, ) + .select_related("principal") # Optimize query .order_by("-grace_period_end_date") .first() ) if active_subscription and active_subscription.end_date > today: + context = super().get_context_data(**kwargs) context["active_subscription"] = active_subscription + return context else: - # If no active subscription is found, redirect to the SubscriptionPageView - return redirect("manage_subscriptions:stripe") - - return context + return HttpResponseRedirect(reverse("manage_subscriptions:stripe")) class CancelSubscriptionView(LoginRequiredMixin, generic.View): From 08adadffbf81d168c42f8de0f9f0d19800a50be3 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 12 Aug 2024 14:58:44 +0530 Subject: [PATCH 144/187] my subscriptions page 5 --- manage_subscriptions/views.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 3ae8910..d5c6efa 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -658,10 +658,6 @@ class ActiveSubscriptionView(TemplateView): jwt.InvalidTokenError, ): return HttpResponseBadRequest("Invalid token or user not found") - return super().get(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - request = self.request today = timezone.now().date() if request.user.is_authenticated: active_subscription = ( @@ -678,12 +674,30 @@ class ActiveSubscriptionView(TemplateView): .first() ) - if active_subscription and active_subscription.end_date > today: - context = super().get_context_data(**kwargs) - context["active_subscription"] = active_subscription - return context - else: + if not active_subscription or active_subscription.end_date < today: return HttpResponseRedirect(reverse("manage_subscriptions:stripe")) + return super().get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + request = self.request + today = timezone.now().date() + if request.user.is_authenticated: + active_subscription = ( + PrincipalSubscription.objects.filter( + principal=request.user, + is_paid=True, + cancelled=False, + deleted=False, + active=True, + status=SubscriptionStatus.ACTIVE, + ) + .select_related("principal") # Optimize query + .order_by("-grace_period_end_date") + .first() + ) + context["active_subscription"] = active_subscription + return context class CancelSubscriptionView(LoginRequiredMixin, generic.View): From 5cee9f21ade025d63e60b745fa196cec7c7f5d2a Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 12 Aug 2024 15:08:55 +0530 Subject: [PATCH 145/187] my subscriptions page 6 --- .../stripe_html/active_subscription.html | 69 ++++++++++++++----- 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/templates/stripe_html/active_subscription.html b/templates/stripe_html/active_subscription.html index f489d9f..5896ccf 100644 --- a/templates/stripe_html/active_subscription.html +++ b/templates/stripe_html/active_subscription.html @@ -76,6 +76,31 @@ text-align: center; } } + + .bg-dark { + background-color: #000 !important; + } + .text-light { + color: #f8f9fa !important; + } + .text-gold { + color: #d4af37 !important; + } + .border-gold { + border: 2px solid #d4af37 !important; + } + .border-bottom-gold { + border-bottom: 2px solid #d4af37 !important; + } + .btn-outline-gold { + color: #d4af37; + border-color: #d4af37; + } + .btn-outline-gold:hover { + background-color: #d4af37; + color: #000; + } + @@ -86,39 +111,47 @@
    -
    -
    -

    {{ active_subscription.subscription.name }}

    +
    +
    +

    {{ active_subscription.subscription.title }}

    -

    Status: {{ active_subscription.status }}

    +
    Principal:
    +

    {{ active_subscription.principal.first_name }} {{ active_subscription.principal.last_name }}

    +

    Status: {{ active_subscription.get_status_display }}

    Start Date: {{ active_subscription.start_date }}

    End Date: {{ active_subscription.end_date }}

    -

    Auto Renew: {{ active_subscription.auto_renew }}

    - +

    Auto Renew: {{ active_subscription.auto_renew|yesno:"Yes,No" }}

    + + {% if active_subscription.coupon_code %} +

    Coupon Code: {{ active_subscription.coupon_code }}

    + {% endif %} + {% if active_subscription.cancelled %} -
    -

    Cancellation Details

    +
    +

    Cancellation Details

    Cancelled: Yes

    Cancellation Date: {{ active_subscription.cancelled_date_time }}

    Grace Period Ends: {{ active_subscription.grace_period_end_date }}

    {% endif %} - + {% if active_subscription.auto_renew and not active_subscription.cancelled %} -
    -

    Cancel Subscription

    -
    - {% csrf_token %} - - -
    -
    +
    +

    Cancel Subscription

    +
    + {% csrf_token %} + + +
    +
    {% endif %} -
    + + + From e47a96c742ec3e8b53f7eb974669ce5d371fdd4c Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 12 Aug 2024 15:31:41 +0530 Subject: [PATCH 146/187] my subscriptions page 7 --- manage_subscriptions/urls.py | 7 +- manage_subscriptions/views.py | 16 ++- .../principal_subscription_details.html | 99 +++++++++++++++++++ .../principal_subscriptions_list.html | 20 ++-- .../subscription_details.html | 2 +- .../stripe_html/active_subscription.html | 2 +- 6 files changed, 129 insertions(+), 17 deletions(-) create mode 100644 templates/manage_subscriptions/principal_subscription_details.html diff --git a/manage_subscriptions/urls.py b/manage_subscriptions/urls.py index 0085713..63ff4d3 100644 --- a/manage_subscriptions/urls.py +++ b/manage_subscriptions/urls.py @@ -70,6 +70,11 @@ urlpatterns = [ views.PrincipalSubscriptionCreateOrUpdateView.as_view(), name="principal_subscription_edit", ), + path( + "principal_subscription/", + views.PrincipalSubscriptionDetailView.as_view(), + name="principal_subscription_detail", + ), path( "principal_subscription/delete/", views.PrincipalSubscriptionDeleteView.as_view(), @@ -92,7 +97,7 @@ urlpatterns = [ ), path("stripe/", views.SubscriptionPageView.as_view(), name="stripe"), path("active/", views.ActiveSubscriptionView.as_view(), name="active"), - path("active/", views.CancelSubscriptionView.as_view(), name="cancel_subscription"), + path("cancel-subscription/", views.CancelSubscriptionView.as_view(), name="cancel_subscription"), path("success/", views.SuccessView.as_view(), name="success"), path("cancel/", views.CancelView.as_view(), name="cancel"), # path("join-now/", views.IndexView.as_view(), name="index"), diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index d5c6efa..c26a1e4 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -186,7 +186,7 @@ class SubscriptionView(LoginRequiredMixin, generic.ListView): return context -class SubscriptionDetailView(generic.DetailView): +class SubscriptionDetailView(LoginRequiredMixin, generic.DetailView): page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS action = resource_action.ACTION_READ @@ -585,6 +585,20 @@ class PrincipalSubscriptionView(LoginRequiredMixin, generic.ListView): return context +class PrincipalSubscriptionDetailView(LoginRequiredMixin, generic.DetailView): + page_name = resource_action.RESOURCE_PRINCIPAL_SUBSCRIPTIONS + resource = resource_action.RESOURCE_PRINCIPAL_SUBSCRIPTIONS + action = resource_action.ACTION_READ + model = PrincipalSubscription + template_name = "manage_subscriptions/principal_subscription_details.html" + context_object_name = "principal_subscription_obj" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["page_name"] = self.page_name + return context + + class PrincipalSubscriptionDeleteView(LoginRequiredMixin, generic.View): page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS diff --git a/templates/manage_subscriptions/principal_subscription_details.html b/templates/manage_subscriptions/principal_subscription_details.html new file mode 100644 index 0000000..cff8244 --- /dev/null +++ b/templates/manage_subscriptions/principal_subscription_details.html @@ -0,0 +1,99 @@ +{% extends 'layout/base_template.html' %} +{% load static %} + +{% block content %} +
    +
    + +
    +
    +
    + {% if principal_subscription_obj.subscription.image %} + {{ principal_subscription_obj.subscription.title }} + {% endif %} +

    {{ principal_subscription_obj.subscription.title }}

    +
    +
    +
    + + +
    +
    +
    +
    Subscription Info
    +
    +
    +

    Principal: {{ principal_subscription_obj.principal.first_name }} {{ principal_subscription_obj.principal.last_name }}

    +

    Status: {{ principal_subscription_obj.get_status_display }}

    +

    Start Date: {{ principal_subscription_obj.start_date }}

    +

    End Date: {{ principal_subscription_obj.end_date }}

    +

    Auto Renew: {{ principal_subscription_obj.auto_renew|yesno:"Yes,No" }}

    +

    Cancelled: {% if principal_subscription_obj.cancelled %}Yes{% else %}No{% endif %}

    + + {% if principal_subscription_obj.coupon_code %} +

    Coupon Code: {{ principal_subscription_obj.coupon_code }}

    + {% endif %} +
    +
    +
    + + +
    +
    +
    +
    Cancellation and Payment Info
    +
    +
    +

    Order ID: {{ principal_subscription_obj.order_id|default:"Not Provided" }}

    +

    Grace Period Ends: {{ principal_subscription_obj.grace_period_end_date|default:"Not Provided" }}

    +

    Stripe Subscription ID: {{ principal_subscription_obj.stripe_subscription_id|default:"Not Provided" }}

    +

    Comments: {{ principal_subscription_obj.comments|default:"Not Provided" }}

    +
    +
    +
    + + + {% if principal_subscription_obj.auto_renew and not principal_subscription_obj.cancelled %} +
    +
    +
    +
    Cancel Subscription
    +
    + {% csrf_token %} + + +
    +
    +
    +
    + {% endif %} + +
    +
    +{% endblock content %} + + diff --git a/templates/manage_subscriptions/principal_subscriptions_list.html b/templates/manage_subscriptions/principal_subscriptions_list.html index 97583ef..7bbc327 100644 --- a/templates/manage_subscriptions/principal_subscriptions_list.html +++ b/templates/manage_subscriptions/principal_subscriptions_list.html @@ -110,19 +110,13 @@ d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"> - +
  • + + + visibility + + +
  • diff --git a/templates/manage_subscriptions/subscription_details.html b/templates/manage_subscriptions/subscription_details.html index 44833a7..0e69252 100644 --- a/templates/manage_subscriptions/subscription_details.html +++ b/templates/manage_subscriptions/subscription_details.html @@ -8,7 +8,7 @@
    - {% if subscription.image %} + {% if principal_subscription_obj.image %} {{ subscription.title }} {% endif %}

    {{ subscription.title }}

    diff --git a/templates/stripe_html/active_subscription.html b/templates/stripe_html/active_subscription.html index 5896ccf..a78f350 100644 --- a/templates/stripe_html/active_subscription.html +++ b/templates/stripe_html/active_subscription.html @@ -116,7 +116,7 @@

    {{ active_subscription.subscription.title }}

    -
    Principal:
    +
    Full Name:

    {{ active_subscription.principal.first_name }} {{ active_subscription.principal.last_name }}

    Status: {{ active_subscription.get_status_display }}

    Start Date: {{ active_subscription.start_date }}

    From efbdc47aab3f52c370e4efbb03961e9f0952ea41 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 12 Aug 2024 15:33:01 +0530 Subject: [PATCH 147/187] my subscriptions page 8 --- manage_subscriptions/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index c26a1e4..d528e3a 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -720,7 +720,7 @@ class CancelSubscriptionView(LoginRequiredMixin, generic.View): try: subscription = PrincipalSubscription.objects.get( - id=subscription_id, principal=request.user + stripe_subscription_id=subscription_id, principal=request.user ) except PrincipalSubscription.DoesNotExist: messages.error(request, "Subscription not found.") From 438f0005ee1915e25cc575f05ea5350b0a3b3997 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 12 Aug 2024 15:39:47 +0530 Subject: [PATCH 148/187] my subscriptions page 9 --- manage_subscriptions/urls.py | 2 ++ manage_subscriptions/views.py | 12 ++++++++++-- .../stripe_html/subscription_cancel_fails.html | 13 +++++++++++++ .../stripe_html/subscription_cancel_success.html | 13 +++++++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 templates/stripe_html/subscription_cancel_fails.html create mode 100644 templates/stripe_html/subscription_cancel_success.html diff --git a/manage_subscriptions/urls.py b/manage_subscriptions/urls.py index 63ff4d3..e26620d 100644 --- a/manage_subscriptions/urls.py +++ b/manage_subscriptions/urls.py @@ -100,5 +100,7 @@ urlpatterns = [ path("cancel-subscription/", views.CancelSubscriptionView.as_view(), name="cancel_subscription"), path("success/", views.SuccessView.as_view(), name="success"), path("cancel/", views.CancelView.as_view(), name="cancel"), + path("subscription-cancel-success/", views.SubscriptionCancelSuccessView.as_view(), name="subscription_cancel_success"), + path("subscription-cancel-fails/", views.SubscriptionCancelFailsView.as_view(), name="subscription_cancel_fails"), # path("join-now/", views.IndexView.as_view(), name="index"), ] diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index d528e3a..9a8eea7 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -742,10 +742,10 @@ class CancelSubscriptionView(LoginRequiredMixin, generic.View): subscription.save() messages.success(request, "Subscription cancelled successfully.") - return redirect("manage_subscriptions:success") + return redirect("manage_subscriptions:subscription_cancel_success") except stripe.error.InvalidRequestError as e: messages.error(request, f"Stripe error: {str(e)}") - return redirect("manage_subscriptions:cancel") + return redirect("manage_subscriptions:subscription_cancel_fails") @csrf_exempt @@ -922,3 +922,11 @@ class SuccessView(TemplateView): class CancelView(TemplateView): template_name = "stripe_html/cancel.html" + + +class SubscriptionCancelSuccessView(TemplateView): + template_name = "stripe_html/subscription_cancel_success.html" + + +class SubscriptionCancelFailsView(TemplateView): + template_name = "stripe_html/subscription_cancel_fails.html" diff --git a/templates/stripe_html/subscription_cancel_fails.html b/templates/stripe_html/subscription_cancel_fails.html new file mode 100644 index 0000000..fbf0c60 --- /dev/null +++ b/templates/stripe_html/subscription_cancel_fails.html @@ -0,0 +1,13 @@ + + + + + + Goodtimes + + +

    + Subscription Cancellation Failed +

    + + \ No newline at end of file diff --git a/templates/stripe_html/subscription_cancel_success.html b/templates/stripe_html/subscription_cancel_success.html new file mode 100644 index 0000000..8ed5b83 --- /dev/null +++ b/templates/stripe_html/subscription_cancel_success.html @@ -0,0 +1,13 @@ + + + + + + Goodtimes + + +

    + Subscription Successfully Cancelled +

    + + \ No newline at end of file From cfff16393fa3df6b2d3b673fb8349409ffa51ea5 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 12 Aug 2024 19:01:02 +0530 Subject: [PATCH 149/187] my subscriptions page 10 --- .../webhook/payment_processing_service.py | 65 +++++++++++-------- goodtimes/webhook/webhook_service.py | 4 -- manage_subscriptions/views.py | 3 - 3 files changed, 38 insertions(+), 34 deletions(-) diff --git a/goodtimes/webhook/payment_processing_service.py b/goodtimes/webhook/payment_processing_service.py index 30906c8..4d2f7b1 100644 --- a/goodtimes/webhook/payment_processing_service.py +++ b/goodtimes/webhook/payment_processing_service.py @@ -22,6 +22,7 @@ class PaymentProcessingService: current_period_end, ): self.webhook_service = WebhookService(webhook_data) + self._order_id = None self.notification_service = NotificationService() self.subscription_service = SubscriptionService() self.stripe_subscription = stripe_subscription @@ -45,8 +46,13 @@ class PaymentProcessingService: @property def order_id(self): - """Return the order ID from the webhook service.""" - return self.webhook_service.get_order_id() + """Return the order ID from the created transaction.""" + return self._order_id + + @order_id.setter + def order_id(self, value): + """Set the order ID.""" + self._order_id = value @property def coupon(self): @@ -60,47 +66,52 @@ class PaymentProcessingService: def create_transaction(self): """Create a transaction based on webhook data.""" - return Transaction.objects.create( + transaction = Transaction.objects.create( principal=self.principal, principal_subscription=None, transaction_type=TransactionType.PAYMENT, payment_method=PaymentMethod.CARD, transaction_status=TransactionStatus.INITIATE, amount=self.amount, - order_id=self.order_id, + # order_id=self.order_id, comment="Principal Subscription Initiated", ) + # Save the transaction to auto-generate the order_id + transaction.save() + + # Step 1: Update the order_id in PaymentProcessingService + self.order_id = transaction.order_id + + return transaction def process_event(self): """Process the webhook event.""" - with transaction.atomic(): - event_type = self.webhook_service.event_type - try: - if event_type == "invoice.payment_succeeded": - if self.charge_data.get("billing_reason") != "subscription_create": - txn = self.create_transaction() - self.handle_success(txn) - elif event_type == "checkout.session.completed": - txn = self.create_transaction() + try: + with transaction.atomic(): + event_type = self.webhook_service.event_type + if event_type == "invoice.payment_succeeded" and self.charge_data.get("billing_reason") == "subscription_create": + logger.info(f"Skipping event {event_type} with billing reason 'subscription_create'") + return + + txn = self.create_transaction() + + if event_type in ["checkout.session.completed", "invoice.payment_succeeded"]: self.handle_success(txn) elif event_type in ["checkout.session.expired", "invoice.payment_failed"]: - order_id = self.order_id - try: - txn = Transaction.objects.get(order_id=order_id) - txn.transaction_status = TransactionStatus.FAIL - txn.save() - return - except Transaction.DoesNotExist: - txn = self.create_transaction() self.handle_failure(txn) - except Exception as e: - logger.error(f"Transaction Error: {str(e)}") - raise e + else: + logger.warning(f"Unknown event type {event_type}. Skipping.") + return + + except Exception as e: + logger.error(f"Unexpected error: {str(e)}") + self.handle_failure(txn) + raise def handle_success(self, transaction): """Handle a successful payment.""" try: - self.create_principal_subscription() + self.create_principal_subscription(transaction) self.process_referral_rewards() self.send_success_notification(transaction) self.update_transaction_status( @@ -113,14 +124,14 @@ class PaymentProcessingService: logger.error(f"Transaction Error: {str(e)}") raise e - def create_principal_subscription(self): + def create_principal_subscription(self, transaction): """Create or update the principal subscription.""" self.subscription_service.principal_subscription = ( self.subscription_service.create_principal_subscription( principal=self.principal, subscription=self.subscription, stripe_subscription=self.stripe_subscription, - order_id=self.order_id, + order_id=transaction.order_id, current_period_start=self.current_period_start, current_period_end=self.current_period_end, coupon=self.coupon, diff --git a/goodtimes/webhook/webhook_service.py b/goodtimes/webhook/webhook_service.py index cbbadce..ba8cd4f 100644 --- a/goodtimes/webhook/webhook_service.py +++ b/goodtimes/webhook/webhook_service.py @@ -61,10 +61,6 @@ class WebhookService: """Retrieve subscription from metadata.""" return self._get_object_from_metadata(Subscription, "subscription_id") - def get_order_id(self): - """Retrieve order ID from metadata.""" - return self._metadata.get("order_id") - def get_coupon(self): """Retrieve coupon from metadata.""" coupon_code = self._metadata.get("couponCode") diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 9a8eea7..57a32eb 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -848,8 +848,6 @@ def create_checkout_session(request): except Subscription.DoesNotExist: return JsonResponse({"error": "Subscription not found."}, status=404) - order_id = f"order_{timezone.localtime().timestamp()}_{request.user.email}" - # Default transaction amount based on subscription amount print("Before Session Data") session_data = { @@ -859,7 +857,6 @@ def create_checkout_session(request): "metadata": { "transaction_amount": str(transaction_amount), "principal": str(request.user.id), - "order_id": order_id, "subscription_id": str(subscription.id), "product_id": str( subscription.stripe_product.product_id From 8f59f81796654d07426f8f52329e9df3587316b4 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 12 Aug 2024 20:47:36 +0530 Subject: [PATCH 150/187] my subscriptions page 11 --- goodtimes/webhook/payment_processing_service.py | 1 - manage_subscriptions/forms.py | 13 +++++++++++++ manage_subscriptions/models.py | 6 ++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/goodtimes/webhook/payment_processing_service.py b/goodtimes/webhook/payment_processing_service.py index 4d2f7b1..5b075fd 100644 --- a/goodtimes/webhook/payment_processing_service.py +++ b/goodtimes/webhook/payment_processing_service.py @@ -105,7 +105,6 @@ class PaymentProcessingService: except Exception as e: logger.error(f"Unexpected error: {str(e)}") - self.handle_failure(txn) raise def handle_success(self, transaction): diff --git a/manage_subscriptions/forms.py b/manage_subscriptions/forms.py index 12294f5..d34f9cf 100644 --- a/manage_subscriptions/forms.py +++ b/manage_subscriptions/forms.py @@ -46,6 +46,19 @@ class SubscriptionForm(forms.ModelForm): id__in=[event_user.id, event_manager.id] ) + def clean(self): + cleaned_data = super().clean() + + stripe_product = cleaned_data.get("stripe_product") + + if not stripe_product: + self.add_error( + "stripe_product", + "Please select a Stripe product to create a subscription.", + ) + + return cleaned_data + class PrincipalSubscriptionForm(forms.ModelForm): class Meta: diff --git a/manage_subscriptions/models.py b/manage_subscriptions/models.py index 0c3ec19..21f3adf 100644 --- a/manage_subscriptions/models.py +++ b/manage_subscriptions/models.py @@ -77,6 +77,12 @@ class Subscription(BaseModel): {"high_amount": "High amount must be greater than amount."} ) + # Ensure stripe_product is compulsory present + # if not self.stripe_product: + # raise ValidationError( + # {"stripe_product": "Please select stripe product to create subscription."} + # ) + def save(self, *args, **kwargs): self.clean() # Call clean before saving to ensure validation super().save(*args, **kwargs) From f22777a72fd834135d430075d2ec80e9129812b3 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Tue, 13 Aug 2024 12:40:44 +0530 Subject: [PATCH 151/187] my subscriptions page 12 --- manage_subscriptions/views.py | 2 +- templates/stripe_html/active_subscription.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 57a32eb..1ce53de 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -720,7 +720,7 @@ class CancelSubscriptionView(LoginRequiredMixin, generic.View): try: subscription = PrincipalSubscription.objects.get( - stripe_subscription_id=subscription_id, principal=request.user + id=subscription_id, principal=request.user ) except PrincipalSubscription.DoesNotExist: messages.error(request, "Subscription not found.") diff --git a/templates/stripe_html/active_subscription.html b/templates/stripe_html/active_subscription.html index a78f350..c6bef02 100644 --- a/templates/stripe_html/active_subscription.html +++ b/templates/stripe_html/active_subscription.html @@ -141,7 +141,7 @@

    Cancel Subscription

    {% csrf_token %} - +
    From 7278ef6d925b199d17407c537df4454fd4381eea Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Tue, 13 Aug 2024 12:47:23 +0530 Subject: [PATCH 152/187] my subscriptions page 13 --- manage_subscriptions/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 1ce53de..b6105d4 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -678,10 +678,10 @@ class ActiveSubscriptionView(TemplateView): PrincipalSubscription.objects.filter( principal=request.user, is_paid=True, - cancelled=False, + # cancelled=False, deleted=False, - active=True, - status=SubscriptionStatus.ACTIVE, + # active=True, + # status=SubscriptionStatus.ACTIVE, ) .select_related("principal") # Optimize query .order_by("-grace_period_end_date") From 9c2b484fa0b5a88a9d1affafc62cd04fa444090b Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Tue, 13 Aug 2024 13:08:25 +0530 Subject: [PATCH 153/187] my subscriptions page 14 --- manage_subscriptions/views.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index b6105d4..7c636a8 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -674,21 +674,13 @@ class ActiveSubscriptionView(TemplateView): return HttpResponseBadRequest("Invalid token or user not found") today = timezone.now().date() if request.user.is_authenticated: - active_subscription = ( - PrincipalSubscription.objects.filter( - principal=request.user, - is_paid=True, - # cancelled=False, - deleted=False, - # active=True, - # status=SubscriptionStatus.ACTIVE, - ) - .select_related("principal") # Optimize query - .order_by("-grace_period_end_date") - .first() - ) + latest_subscription = PrincipalSubscription.objects.filter( + principal=request.user, + is_paid=True, + end_date__lte=today, + ).order_by('-end_date').last() - if not active_subscription or active_subscription.end_date < today: + if latest_subscription: return HttpResponseRedirect(reverse("manage_subscriptions:stripe")) return super().get(request, *args, **kwargs) From e57901de33bcce411fcf9a19a43e714a8bf9c32e Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Tue, 13 Aug 2024 13:10:33 +0530 Subject: [PATCH 154/187] my subscriptions page 15 --- manage_subscriptions/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 7c636a8..80a0af4 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -677,7 +677,7 @@ class ActiveSubscriptionView(TemplateView): latest_subscription = PrincipalSubscription.objects.filter( principal=request.user, is_paid=True, - end_date__lte=today, + end_date__gte=today, ).order_by('-end_date').last() if latest_subscription: From a0dabe85ac6eb1c9838ecdf38bacc9c394d75dfc Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Tue, 13 Aug 2024 13:13:16 +0530 Subject: [PATCH 155/187] my subscriptions page 16 --- manage_subscriptions/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 80a0af4..752cea0 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -677,10 +677,11 @@ class ActiveSubscriptionView(TemplateView): latest_subscription = PrincipalSubscription.objects.filter( principal=request.user, is_paid=True, + deleted=False, end_date__gte=today, ).order_by('-end_date').last() - if latest_subscription: + if not latest_subscription: return HttpResponseRedirect(reverse("manage_subscriptions:stripe")) return super().get(request, *args, **kwargs) From e80b01a549ad50140d05413d5d90e43901d4f7ab Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Tue, 13 Aug 2024 13:15:42 +0530 Subject: [PATCH 156/187] my subscriptions page 17 --- manage_subscriptions/views.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 752cea0..55fac46 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -690,20 +690,13 @@ class ActiveSubscriptionView(TemplateView): request = self.request today = timezone.now().date() if request.user.is_authenticated: - active_subscription = ( - PrincipalSubscription.objects.filter( - principal=request.user, - is_paid=True, - cancelled=False, - deleted=False, - active=True, - status=SubscriptionStatus.ACTIVE, - ) - .select_related("principal") # Optimize query - .order_by("-grace_period_end_date") - .first() - ) - context["active_subscription"] = active_subscription + latest_subscription = PrincipalSubscription.objects.filter( + principal=request.user, + is_paid=True, + deleted=False, + end_date__gte=today, + ).order_by('-end_date').last() + context["active_subscription"] = latest_subscription return context From 8f6d57703185e4e64e99e18ce8dd06fd92d01b3d Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Tue, 13 Aug 2024 14:01:18 +0530 Subject: [PATCH 157/187] my subscriptions page 18 --- manage_subscriptions/views.py | 3 ++- templates/stripe_html/index.html | 12 +++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 55fac46..6bcecde 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -827,6 +827,7 @@ def create_checkout_session(request): subscription_id = data.get("subscriptionId") coupon_code = data.get("couponCode") transaction_amount = data.get("discountAmount") + is_recurring = data.get("isRecurring") principal_id = request.user.id try: @@ -867,7 +868,7 @@ def create_checkout_session(request): # Creating the Stripe Checkout Session try: - if subscription.price_id: + if is_recurring and subscription.price_id: session_data["line_items"] = [ { "price": subscription.price_id, diff --git a/templates/stripe_html/index.html b/templates/stripe_html/index.html index 06b75db..1a35145 100644 --- a/templates/stripe_html/index.html +++ b/templates/stripe_html/index.html @@ -129,6 +129,13 @@
    + +
    + + +
    @@ -536,11 +543,13 @@ button.addEventListener("click", () => { const subscriptionId = button.getAttribute("data-subscription-id"); const priceId = button.getAttribute("data-price-id"); + const recurringCheckbox = button.closest('.feat-card').querySelector(".recurring-checkbox"); const couponCode = button.previousElementSibling.value; const errorMessageContainer = button.nextElementSibling; console.log("subscriptionId: ", subscriptionId); console.log("couponCode: ", couponCode); console.log("priceId: ", priceId); + console.log("recurring: ", recurringCheckbox.checked); // Checking if the checkbox is checked button.disabled = true; button.previousElementSibling.value = ""; @@ -552,7 +561,8 @@ }, body: JSON.stringify({ subscriptionId: subscriptionId, - couponCode: couponCode + couponCode: couponCode, + isRecurring: recurringCheckbox.checked }), }) .then(response => { From ab684a3e072fc4fe4a9b4fdf4c6c5c311104216e Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Tue, 13 Aug 2024 14:05:00 +0530 Subject: [PATCH 158/187] my subscriptions page 19 --- templates/stripe_html/index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/stripe_html/index.html b/templates/stripe_html/index.html index 1a35145..2864e18 100644 --- a/templates/stripe_html/index.html +++ b/templates/stripe_html/index.html @@ -130,9 +130,9 @@
    -
    - -
    diff --git a/templates/stripe_html/webview_404.html b/templates/stripe_html/webview_404.html index 803c753..e846f4a 100644 --- a/templates/stripe_html/webview_404.html +++ b/templates/stripe_html/webview_404.html @@ -116,7 +116,7 @@

    404

    -
    An error occurred. Please try again later.
    +
    Something went wrong. Please try again later.
    From 70d807220e91a35603bd87bbf132e61c50eb29fe Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Fri, 23 Aug 2024 17:25:08 +0530 Subject: [PATCH 170/187] fix: age-group filter, url redirect --- accounts/views.py | 13 ++++++++++++- manage_events/api/filters.py | 7 ++++++- manage_subscriptions/models.py | 2 +- manage_subscriptions/views.py | 4 ++-- manage_wallets/api/views.py | 2 +- .../principal_subscription_add.html | 2 +- 6 files changed, 23 insertions(+), 7 deletions(-) diff --git a/accounts/views.py b/accounts/views.py index 490ef43..24f052f 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -797,8 +797,18 @@ class CustomerUpdateView(LoginRequiredMixin, generic.View): return render(request, self.template_name, context=context) class CustomerDetailView(LoginRequiredMixin, generic.DetailView): + page_name = resource_action.RESOURCE_MANAGE_CUSTOMER + resource = resource_action.RESOURCE_MANAGE_CUSTOMER + action = resource_action.ACTION_READ template_name = 'accounts/customer/customer_detail.html' + def get_context_data(self, **kwargs): + context = { + "page_name": self.page_name, + } + context.update(kwargs) # Include any additional context data passed to the view + return context + def get(self, request, *args, **kwargs): principal_obj = IAmPrincipal.objects.get(pk=kwargs.get("pk")) try: @@ -806,7 +816,8 @@ class CustomerDetailView(LoginRequiredMixin, generic.DetailView): except Exception as e: principal_preference = None principal_subscription = PrincipalSubscription.objects.filter(principal=principal_obj).order_by("-start_date").first() - return render(request, self.template_name, locals()) + context = self.get_context_data(principal_obj=principal_obj,principal_preference=principal_preference,principal_subscription=principal_subscription) + return render(request, self.template_name, context=context) class CustomerListView(LoginRequiredMixin, generic.ListView): page_name = resource_action.RESOURCE_MANAGE_CUSTOMER diff --git a/manage_events/api/filters.py b/manage_events/api/filters.py index cb88f8c..e4adacb 100644 --- a/manage_events/api/filters.py +++ b/manage_events/api/filters.py @@ -14,7 +14,7 @@ class EventFilter(filters.FilterSet): # end_date = filters.DateFilter(field_name="end_date", lookup_expr="lte") price_from = filters.NumberFilter(field_name="entry_fee", lookup_expr="gte") price_to = filters.NumberFilter(field_name="entry_fee", lookup_expr="lte") - age_group = filters.CharFilter(field_name="age_group", lookup_expr="icontains") + age_group = filters.CharFilter(method="filter_age_group") class Meta: model = Event @@ -32,6 +32,11 @@ class EventFilter(filters.FilterSet): def filter_category(self, queryset, name, value): category = value.split(',') return queryset.filter(category__title__in=category) + + def filter_age_group(self, queryset, name, value): + age_group = value.split(',') + return queryset.filter(age_group__in=age_group) + # def filter_queryset(self, queryset): # queryset = super().filter_queryset(queryset) diff --git a/manage_subscriptions/models.py b/manage_subscriptions/models.py index b14e238..9c929fa 100644 --- a/manage_subscriptions/models.py +++ b/manage_subscriptions/models.py @@ -72,7 +72,7 @@ class Subscription(BaseModel): # Create new product and price price = StripeService.create_price( product_data={ - "name": self.txitle, + "name": self.title, "description": self.short_description, }, unit_amount=int(self.amount * 100), diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index ab5f782..5c582dc 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -178,8 +178,8 @@ class SubscriptionDeleteView(LoginRequiredMixin, generic.View): class PrincipalSubscriptionCreateOrUpdateView(LoginRequiredMixin, generic.View): # Set the page_name and resource - page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS - resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS + page_name = resource_action.RESOURCE_PRINCIPAL_SUBSCRIPTIONS + resource = resource_action.RESOURCE_PRINCIPAL_SUBSCRIPTIONS # Initialize the action as ACTION_CREATE (can change based on logic) action = resource_action.ACTION_CREATE # Default action diff --git a/manage_wallets/api/views.py b/manage_wallets/api/views.py index f477a22..931bbcb 100644 --- a/manage_wallets/api/views.py +++ b/manage_wallets/api/views.py @@ -146,7 +146,7 @@ class TransactionView(APIView): models.TransactionStatus.SUCCESS, models.TransactionStatus.FAIL, ], - ) + ).order_by("-created_on") serializer = serializers.TransactionSerializer(queryset, many=True) response = { diff --git a/templates/manage_subscriptions/principal_subscription_add.html b/templates/manage_subscriptions/principal_subscription_add.html index 8da8df6..d524ae8 100644 --- a/templates/manage_subscriptions/principal_subscription_add.html +++ b/templates/manage_subscriptions/principal_subscription_add.html @@ -16,7 +16,7 @@
    -

    {{operation}} {{page_name}}

    +

    {{operation}} Customer Subscription

    -

    Plan: {{ subscription.plan.title }}

    -

    Price ID: {{ subscription.price_id|default:"Not a Stripe Subscription" }}

    -

    Stripe Product: {{ subscription.stripe_product|default:"None" }}

    +

    Customer type : + {% for principal_type in subscription.principal_types.all %} + {{ principal_type.name }} + {% endfor %} +

    +

    Plan: {{subscription.interval_count}} {{ subscription.interval }}

    +

    Stripe Price ID: {{ subscription.price_id|default:"Not a Stripe Subscription" }}

    +

    Stripe Product ID: {{ subscription.product_id|default:"None" }}

    Amount: ${{ subscription.amount }}

    High Amount: ${{ subscription.high_amount }}

    Referral Percentage: {{ subscription.referral_percentage }}%

    @@ -49,26 +54,11 @@

    Short Description: {{ subscription.short_description|default:"Not Provided" }}

    Long Description:

    -

    {{ subscription.long_description|default:"Not Provided" }}

    +

    {{ subscription.long_description.html|default:"Not Provided"|safe }}

    - -
    -
    -
    -
    Principal Types
    -
    -
    -
      - {% for principal_type in subscription.principal_types.all %} -
    • {{ principal_type.name }}
    • - {% endfor %} -
    -
    -
    -
    diff --git a/templates/manage_subscriptions/subscription_list.html b/templates/manage_subscriptions/subscription_list.html index ee40a60..c2a59d7 100644 --- a/templates/manage_subscriptions/subscription_list.html +++ b/templates/manage_subscriptions/subscription_list.html @@ -84,6 +84,26 @@ diff --git a/templates/stripe_html/index.html b/templates/stripe_html/index.html index 61bc410..0adc577 100644 --- a/templates/stripe_html/index.html +++ b/templates/stripe_html/index.html @@ -98,49 +98,45 @@ {% for subscription in subscriptions %}
    -
    - -
    {{ subscription.title }} -
    - {% if subscription.image %} - {{ subscription.title }} - {% endif %} -
    -
    - {% if subscription.short_description %} -

    {{ subscription.short_description }}

    - {% endif %} - {% if subscription.long_description %} -

    {{ subscription.long_description|truncatewords:20 }}

    - {% endif %} -
    Subscription Amount
    - {% if subscription.high_amount and subscription.high_amount > subscription.amount %} -

    £ {{ subscription.high_amount }} £ {{ subscription.amount }} +

    {{subscription.title}}

    + {% if subscription.high_amount and subscription.high_amount > subscription.amount %} +

    + £{{subscription.high_amount}} + £{{subscription.amount}} + {% if subscription.interval_count == 1 %} + / {{ subscription.interval| capfirst }} + {% else %} + / {{ subscription.interval_count }} {{ subscription.interval | capfirst }}s + {% endif %}

    - {% else %} -

    £ {{ subscription.amount }}

    - {% endif %} -

    Subscription Cycle: {{subscription.interval_count}} {{ subscription.interval | capfirst }}

    - -
    -
    - {% comment %} {% endcomment %} - -
    - - -
    - - - - -
    + {% else %} +

    + £{{subscription.amount}} + {% if interval_count == 1 %} + / {{ subscription.interval }} + {% else %} + / {{ subscription.interval_count }} {{ subscription.interval | capfirst }}s + {% endif %} +

    + {% endif %} + {% if subscription.short_description %} +

    {{ subscription.short_description }}

    + {% endif %} + {% if subscription.long_description %} +

    {{ subscription.long_description.html|safe }}

    + {% endif %} +
    + + +
    + +
    {% endfor %} - +
    @@ -551,8 +547,6 @@ return result.json(); }) .then((data) => { - console.log("data: ", data); - console.log("data.sessionId: ", data.sessionId); // Redirects to Stripe Checkout return stripe.redirectToCheckout({ sessionId: data.sessionId From bef5a52b55885008e7ddc7f01ca94a7d5f678c50 Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Sun, 25 Aug 2024 22:57:32 +0530 Subject: [PATCH 172/187] fix: create subscription and update logic --- manage_subscriptions/forms.py | 29 +++++++------------ ...014_alter_subscription_long_description.py | 2 +- manage_subscriptions/models.py | 14 +++++---- manage_subscriptions/views.py | 12 +++++++- 4 files changed, 31 insertions(+), 26 deletions(-) diff --git a/manage_subscriptions/forms.py b/manage_subscriptions/forms.py index d4afaee..05dc2fc 100644 --- a/manage_subscriptions/forms.py +++ b/manage_subscriptions/forms.py @@ -21,7 +21,6 @@ class SubscriptionForm(forms.ModelForm): "active", "is_free", ] - exclude = [] def __init__(self, *args, **kwargs): super(SubscriptionForm, self).__init__(*args, **kwargs) @@ -31,23 +30,17 @@ class SubscriptionForm(forms.ModelForm): id__in=[event_user.id, event_manager.id] ) - if self.instance: - # If there is an instance (i.e. we're editing an existing subscription) - - # Use a dictionary comprehension to create a new dictionary of fields - # that excludes the readonly fields - self.fields = { - field_name: field - for field_name, field in self.fields.items() - if field_name not in [ - "interval", - "interval_count", - "amount", - "high_amount", - "principal_types", - ] - } - +class SubscriptionUpdateForm(forms.ModelForm): + class Meta: + model = Subscription + fields = [ + "title", + "short_description", + "long_description", + "referral_percentage", + "active", + "is_free", + ] class PrincipalSubscriptionForm(forms.ModelForm): class Meta: diff --git a/manage_subscriptions/migrations/0014_alter_subscription_long_description.py b/manage_subscriptions/migrations/0014_alter_subscription_long_description.py index 51a85f9..d117a17 100644 --- a/manage_subscriptions/migrations/0014_alter_subscription_long_description.py +++ b/manage_subscriptions/migrations/0014_alter_subscription_long_description.py @@ -14,6 +14,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='subscription', name='long_description', - field=django_quill.fields.QuillField(), + field=django_quill.fields.QuillField(default=''), ), ] diff --git a/manage_subscriptions/models.py b/manage_subscriptions/models.py index 70bba3b..a15766d 100644 --- a/manage_subscriptions/models.py +++ b/manage_subscriptions/models.py @@ -58,8 +58,9 @@ class Subscription(BaseModel): def save(self, *args, **kwargs): from goodtimes.services import StripeService - if not self.delete: - self.clean() + if self.pk and self.deleted: + return super().save(*args, **kwargs) + self.clean() if self.is_free: # If is_free is True, set amounts to 0 and remove Stripe price and product IDs @@ -68,7 +69,7 @@ class Subscription(BaseModel): self.price_id = None self.product_id = None else: - if self.id and self.price_id: # Update existing subscription + if self.pk and self.price_id: # Update existing subscription # Retrieve existing price and product from Stripe price = StripeService.retrieve_price(self.price_id) if not price["success"]: @@ -77,7 +78,7 @@ class Subscription(BaseModel): # Update price active status if it differs from local active status if self.active != price["data"].active: StripeService.update_price(price_id=self.price_id, active=self.active) - + # Retrieve existing product from Stripe product = StripeService.retrive_product(self.product_id) if not product["success"]: @@ -90,7 +91,8 @@ class Subscription(BaseModel): name=self.title, description=self.short_description ) - else: # Create new subscription + else: + print("new pricde create is clled =========================================================") # Create new product and price price = StripeService.create_price( product_data={ @@ -175,7 +177,7 @@ class PrincipalSubscription(BaseModel): # If the active flag is False, set the status to inactive if not self.active: self.status = SubscriptionStatus.INACTIVE - super.save(*args, **kwargs) + super().save(*args, **kwargs) def generate_order_id(email): diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 1c5e00b..75335bf 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -14,6 +14,7 @@ from manage_coupons.models import Coupon from manage_subscriptions.forms import ( SubscriptionForm, PrincipalSubscriptionForm, + SubscriptionUpdateForm, ) from manage_wallets.models import ( PaymentMethod, @@ -83,6 +84,11 @@ class SubscriptionCreateOrUpdateView(LoginRequiredMixin, generic.View): if self.object is not None: self.action = resource_action.ACTION_UPDATE + if self.object: + self.form_class = SubscriptionUpdateForm + else: + self.form_class = self.form_class + form = self.form_class(instance=self.object) context = self.get_context_data(form=form) return render(request, self.template_name, context=context) @@ -94,7 +100,11 @@ class SubscriptionCreateOrUpdateView(LoginRequiredMixin, generic.View): if self.object is not None: self.action = resource_action.ACTION_UPDATE - form = self.form_class(request.POST, instance=self.object) + if self.object: + form = SubscriptionUpdateForm(request.POST, instance=self.object) + else: + form = self.form_class(request.POST) + if not form.is_valid(): print(form.errors) context = self.get_context_data(form=form) From 89639c7a38b539f8151e7b914d2f7048e533866e Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Tue, 3 Sep 2024 12:07:56 +0530 Subject: [PATCH 173/187] feat(social media): added api for post to social media --- accounts/urls.py | 2 +- accounts/views.py | 2 +- manage_events/api/urls.py | 2 + manage_events/api/views.py | 80 ++++++++++++++++++- templates/includes/dynamic_template_form.html | 2 +- 5 files changed, 83 insertions(+), 5 deletions(-) diff --git a/accounts/urls.py b/accounts/urls.py index f8f0d1b..99c99c9 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -24,7 +24,7 @@ urlpatterns = [ path('principal/add/', views.PrincipalCreateOrUpdateView.as_view(), name="principal_add"), path('principal/edit/', views.PrincipalCreateOrUpdateView.as_view(), name="principal_edit"), # path('principal/delete/', views.PrincipalDeleteView.as_view(), name="principal_delete"), - path('principal/resource/permission/edit//', views.PrincipalResourcePermissionEditView.as_view(), + path('principal/resource/permission/edit//', views.PrincipalResourcePermissionEditView.as_view(), name="principal_resource_permission_edit"), path('principal/group/link/', views.PrincipalGroupLinkListView.as_view(), name="principal_group_link_list"), diff --git a/accounts/views.py b/accounts/views.py index 24f052f..d282201 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1127,7 +1127,7 @@ class CustomerImportView(LoginRequiredMixin, generic.View): register_complete=True, principal_type=principal_type, business_name=business_name, - phone_no=str(phone_no), + phone_no=phone_no, address_line1=address, city=region, country=country, diff --git a/manage_events/api/urls.py b/manage_events/api/urls.py index 24e86b7..1937875 100644 --- a/manage_events/api/urls.py +++ b/manage_events/api/urls.py @@ -141,4 +141,6 @@ urlpatterns = [ views.EventListView.as_view(), name="event_filter", ), + + path("post-to-social-media//", views.SocialMediaPostView.as_view(), name="social_media_post") ] diff --git a/manage_events/api/views.py b/manage_events/api/views.py index b6a6a9a..7ce903f 100644 --- a/manage_events/api/views.py +++ b/manage_events/api/views.py @@ -13,7 +13,7 @@ from django.db.models import Q, Count from taggit.models import Tag from django.utils.dateparse import parse_date from goodtimes import services -from goodtimes.services import GoogleMapsservice +from goodtimes.services import FacebookAPI, FacebookPoster, GoogleMapsservice, InstagramAPI, InstagramPoster, TwitterAPI, TwitterPoster from goodtimes.utils import ApiResponse, CapacityError from rest_framework.permissions import IsAuthenticated from rest_framework_simplejwt.authentication import JWTAuthentication @@ -1124,4 +1124,80 @@ class EventListView(generics.ListAPIView): return self.get_paginated_response(serializer.data) serializer = self.get_serializer(queryset, many=True) - return ApiResponse.success(message=constants.SUCCESS, data=serializer.data) \ No newline at end of file + return ApiResponse.success(message=constants.SUCCESS, data=serializer.data) + + +from rest_framework.response import Response +class SocialMediaPostView(APIView): + def get(self, request, *args, **kwargs): + platform = request.query_params.get("platform", "") + event_id = kwargs.get("id") + print(platform, event_id) + errors = [] + success_messages = [] + + try: + event = Event.objects.get(id=event_id) + except Event.DoesNotExist: + errors.append("Event does not exist") + return Response({ + 'message': "Error in posting to social media", + 'errors': errors, + 'success_messages': success_messages + }, status=400) + + if not event.active: + errors.append("Event is not active") + return Response({ + 'message': "Error in posting to social media", + 'errors': errors, + 'success_messages': success_messages + }, status=400) + + caption = f"{event.title}\nDuration: {event.start_date} to {event.end_date}\nAddress: {event.venue.address}" + + if platform in ['instagram', 'facebook', 'twitter', 'all']: + if platform in ['twitter', 'all']: + image_url = event.image.path + twitter_api = TwitterAPI() + twitter_poster = TwitterPoster(twitter_api) + result = twitter_poster.post_image_with_caption(image_url, caption) + if result['success']: + success_messages.append("Posted to Twitter successfully") + else: + errors.append("Fail to post on Twitter") + + 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: + errors.append("Fail to post on Facebook") + + if platform in ['instagram', 'all']: + instagram_api = InstagramAPI() + instagram_poster = InstagramPoster(instagram_api) + result = instagram_poster.post_image_with_caption(image_url, caption) + if result["success"]: + success_messages.append("Posted to Instagram successfully") + else: + errors.append("Fail to post on Instagram") + + if not errors: + return Response({'message': 'Post Successful', 'errors': errors, 'success_messages': success_messages}) + + if errors and success_messages: + return Response({ + 'message': 'Some posts succeeded while others failed', + 'errors': errors, + 'success_messages': success_messages + }, status=200) + + return Response({ + 'message': 'Error in posting to social media', + 'errors': errors, + 'success_messages': success_messages + }, status=400) \ No newline at end of file diff --git a/templates/includes/dynamic_template_form.html b/templates/includes/dynamic_template_form.html index 060eaee..b8be13b 100644 --- a/templates/includes/dynamic_template_form.html +++ b/templates/includes/dynamic_template_form.html @@ -60,7 +60,7 @@
    {% endif %} {% if field.errors %} -
    +
    {% for error in field.errors %}

    {{ error }}

    {% endfor %} From b6b4295d7ec21a6960d6658422a348637325fdbc Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Tue, 24 Sep 2024 18:56:42 +0530 Subject: [PATCH 174/187] fixed: webhook, edit customer --- accounts/forms.py | 67 ++++++- accounts/views.py | 187 +++++++++++++----- goodtimes/services.py | 48 ++--- goodtimes/webhook/webhook_service.py | 2 +- manage_subscriptions/api/views.py | 110 +++++------ manage_subscriptions/views.py | 1 + .../accounts/customer/customer_list.html | 2 - .../customer/customer_manager_edit.html | 162 +++++++++++++++ ...tml => customer_onboard_manager_edit.html} | 0 .../accounts/customer/customer_user_edit.html | 153 ++++++++++++++ 10 files changed, 588 insertions(+), 144 deletions(-) create mode 100644 templates/accounts/customer/customer_manager_edit.html rename templates/accounts/customer/{customer_edit.html => customer_onboard_manager_edit.html} (100%) create mode 100644 templates/accounts/customer/customer_user_edit.html diff --git a/accounts/forms.py b/accounts/forms.py index a6eab95..ccb045e 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -401,7 +401,7 @@ class CreateCustomerForm(forms.Form): super().__init__(*args, **kwargs) self.fields['preferences'].queryset = EventCategory.objects.all() -class UpdateCustomerForm(forms.Form): +class UpdateOnboardedEventManagerForm(forms.Form): first_name = forms.CharField(max_length=255, required=True, label='First Name') last_name = forms.CharField(max_length=255, required=True, label='Last Name') business_name = forms.CharField(max_length=200, required=True, label="Business Name") @@ -442,5 +442,70 @@ class UpdateCustomerForm(forms.Form): super().__init__(*args, **kwargs) self.fields['preferences'].queryset = EventCategory.objects.all() + +class UpdateEventManagerForm(forms.ModelForm): + 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") + email = forms.EmailField(required=True, label='Email', widget=forms.TextInput(attrs={'readonly': 'readonly'})) + phone_no = PhoneNumberField( + widget=forms.TextInput(), + label="Phone No" + ) + address_line1 = forms.CharField(widget=forms.Textarea(attrs={'rows': 4, 'cols': 40})) + city = forms.CharField(max_length=200, required=False, label="Region") + country = forms.CharField(max_length=200, required=False, label="Country") + website = forms.URLField(max_length=255, required=False, label="Website") + linkedin_profile = forms.URLField(max_length=200, required=False, label="LinkedIn") + facebook_profile = forms.URLField(max_length=200, required=False, label="Facebook") + instagram_profile = forms.URLField(max_length=200, required=False, label="Instagram") + twitter_profile = forms.URLField(max_length=200, required=False, label="Twitter") + is_active = forms.BooleanField(required=False, label='Active', help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",) + + class Meta: + model = models.IAmPrincipal + fields = [ + "first_name", + "last_name", + "business_name", + "email", + "phone_no", + 'address_line1', + 'city', + 'country', + "website", + "linkedin_profile", + "facebook_profile", + "instagram_profile", + "twitter_profile", + "is_active", + ] + +class UpdateEventUserForm(forms.ModelForm): + first_name = forms.CharField(max_length=255, required=True, label='First Name') + last_name = forms.CharField(max_length=255, required=True, label='Last Name') + email = forms.EmailField(required=True, label='Email', widget=forms.TextInput(attrs={'readonly': 'readonly'})) + phone_no = PhoneNumberField( + widget=forms.TextInput(), + label="Phone No" + ) + address_line1 = forms.CharField(widget=forms.Textarea(attrs={'rows': 4, 'cols': 40})) + city = forms.CharField(max_length=200, required=False, label="Region") + country = forms.CharField(max_length=200, required=False, label="Country") + is_active = forms.BooleanField(required=False, label='Active', help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",) + + class Meta: + model = models.IAmPrincipal + fields = [ + "first_name", + "last_name", + "email", + "phone_no", + 'address_line1', + 'city', + 'country', + "is_active", + ] + class UploadExcelForm(forms.Form): file = forms.FileField() diff --git a/accounts/views.py b/accounts/views.py index 24f052f..6aa85b4 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -21,6 +21,7 @@ from django.urls import reverse_lazy from django.views import generic from django.db import models, transaction, IntegrityError from django.utils import timezone +import phonenumbers from accounts import permission from goodtimes import constants from goodtimes.services import EmailService @@ -28,6 +29,8 @@ from goodtimes.utils import JsonResponseUtil from manage_events.models import EventCategory, PrincipalPreference from manage_referrals.models import ReferralCode from manage_subscriptions.models import PrincipalSubscription, Subscription +import datetime +from datetime import datetime from . import resource_action from .forms import ( @@ -39,7 +42,9 @@ from .forms import ( IAmPrincipalRoleAppResourceActionLinkForm, IAmPrincipalGroupLinkForm, ProfileEditForm, - UpdateCustomerForm, + UpdateEventManagerForm, + UpdateEventUserForm, + UpdateOnboardedEventManagerForm, UploadExcelForm, ) from .models import ( @@ -183,10 +188,6 @@ class PrincipalListView(LoginRequiredMixin, generic.ListView): context["page_name"] = self.page_name return context - -import datetime - - class PrincipalCreateOrUpdateView(LoginRequiredMixin, generic.View): page_name = resource_action.RESOURCE_IAM_PRINCIPAL model = IAmPrincipal @@ -680,29 +681,13 @@ class CustomerUpdateView(LoginRequiredMixin, generic.View): page_name = resource_action.RESOURCE_MANAGE_CUSTOMER resource = resource_action.RESOURCE_MANAGE_CUSTOMER model = IAmPrincipal - form_class = UpdateCustomerForm - template_name = "accounts/customer/customer_edit.html" + template_name = None success_url = reverse_lazy("accounts:customer_list") success_message = "Updated Successfully" error_message = "An error occurred while saving the data." - def get_context_data(self, **kwargs): - context = { - "page_name": self.page_name, - "operation": "Edit", - } - context.update(kwargs) # Include any additional context data passed to the view - return context - - def get(self, request, *args, **kwargs): - principal_id = kwargs.get("pk") - try: - principal_obj = IAmPrincipal.objects.get(pk=principal_id) - except Exception as e: - messages.error(request, f"No Record of id {principal_id} is found") - return redirect(self.success_url) - - print(f"principal address is {principal_obj.address_line1}") + def OnboardedEventManagerFormData(self, principal_obj): + form_class = UpdateOnboardedEventManagerForm initial_data = { "first_name": principal_obj.first_name, @@ -735,7 +720,40 @@ class CustomerUpdateView(LoginRequiredMixin, generic.View): initial_data["free_start_date"] = None initial_data["free_end_date"] = None - form = self.form_class(initial=initial_data) + return form_class(initial=initial_data) + + def get_context_data(self, **kwargs): + context = { + "page_name": self.page_name, + "operation": "Edit", + } + context.update(kwargs) # Include any additional context data passed to the view + return context + + def get(self, request, *args, **kwargs): + principal_id = kwargs.get("pk") + try: + principal_obj = IAmPrincipal.objects.get(pk=principal_id) + except Exception as e: + messages.error(request, f"No Record of id {principal_id} is found") + return redirect(self.success_url) + + print(f"principal address is {principal_obj.address_line1}") + + is_onboarded = False + if hasattr(principal_obj, 'extended_data') and principal_obj.extended_data: + is_onboarded = principal_obj.extended_data.is_onboarded + + if is_onboarded and principal_obj.principal_type.name == resource_action.PRINCIPAL_TYPE_EVENT_MANAGER: + form = self.OnboardedEventManagerFormData(principal_obj) + self.template_name = "accounts/customer/customer_onboard_manager_edit.html" + elif not is_onboarded and principal_obj.principal_type.name == resource_action.PRINCIPAL_TYPE_EVENT_MANAGER: + form = UpdateEventManagerForm(instance=principal_obj) + self.template_name = "accounts/customer/customer_manager_edit.html" + elif principal_obj.principal_type.name == resource_action.PRINCIPAL_TYPE_EVENT_USER: + form = UpdateEventUserForm(instance=principal_obj) + self.template_name = "accounts/customer/customer_user_edit.html" + context = self.get_context_data(form=form, principal_obj=principal_obj) print("context dta is ", context) return render(request, self.template_name, context=context) @@ -747,10 +765,31 @@ 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) + + is_onboarded = False + if hasattr(principal_obj, 'extended_data') and principal_obj.extended_data: + is_onboarded = principal_obj.extended_data.is_onboarded + + # Dynamically choose the form based on the principal object's data + if is_onboarded and principal_obj.principal_type.name == resource_action.PRINCIPAL_TYPE_EVENT_MANAGER: + form_class = UpdateOnboardedEventManagerForm + self.template_name = "accounts/customer/customer_onboard_manager_edit.html" + elif not is_onboarded and principal_obj.principal_type.name == resource_action.PRINCIPAL_TYPE_EVENT_MANAGER: + form_class = UpdateEventManagerForm + self.template_name = "accounts/customer/customer_manager_edit.html" + elif principal_obj.principal_type.name == resource_action.PRINCIPAL_TYPE_EVENT_USER: + form_class = UpdateEventUserForm + self.template_name = "accounts/customer/customer_user_edit.html" + else: + messages.error(request, "Invalid principal type") + return redirect(self.success_url) + + form = form_class(request.POST, instance=principal_obj) + 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 @@ -768,26 +807,27 @@ class CustomerUpdateView(LoginRequiredMixin, generic.View): principal_obj.twitter_profile = form.cleaned_data.get("twitter_profile") principal_obj.save() - # update principal preferences record - principal_preference, _ = PrincipalPreference.objects.get_or_create(principal=principal_obj) - principal_preference.preferred_categories.set(form.cleaned_data.get("preferences")) + if is_onboarded: # only update preference and subscription if it is added by admin + # update principal preferences record + principal_preference, _ = PrincipalPreference.objects.get_or_create(principal=principal_obj) + principal_preference.preferred_categories.set(form.cleaned_data.get("preferences")) - # update principal subscription record - principal_subscription = PrincipalSubscription.objects.filter(principal=principal_obj).order_by('-end_date').first() - if principal_subscription: - principal_subscription.start_date = form.cleaned_data.get("free_start_date") - principal_subscription.end_date = form.cleaned_data.get("free_end_date") - principal_subscription.grace_period_end_date = form.cleaned_data.get("free_end_date") + datetime.timedelta(days=15) - principal_subscription.save() - else: - PrincipalSubscription.objects.create( - principal=principal_obj, - start_date=form.cleaned_data.get("free_start_date"), - end_date=form.cleaned_data.get("free_end_date"), - grace_period_end_date=PrincipalSubscription.generate_grace_period_end_date(form.cleaned_data.get("free_end_date")), - is_paid=True, - subscription=Subscription.objects.filter().first() # Assuming you want to link a default subscription - ) + # update principal subscription record + principal_subscription = PrincipalSubscription.objects.filter(principal=principal_obj).order_by('-end_date').first() + if principal_subscription: + principal_subscription.start_date = form.cleaned_data.get("free_start_date") + principal_subscription.end_date = form.cleaned_data.get("free_end_date") + principal_subscription.grace_period_end_date = form.cleaned_data.get("free_end_date") + datetime.timedelta(days=15) + principal_subscription.save() + else: + PrincipalSubscription.objects.create( + principal=principal_obj, + start_date=form.cleaned_data.get("free_start_date"), + end_date=form.cleaned_data.get("free_end_date"), + grace_period_end_date=PrincipalSubscription.generate_grace_period_end_date(form.cleaned_data.get("free_end_date")), + is_paid=True, + subscription=Subscription.objects.filter().first() # Assuming you want to link a default subscription + ) messages.success(self.request, self.success_message) return redirect(self.success_url) @@ -941,10 +981,10 @@ def export_excel_template(request): 'Last Name', 'Business Name', 'Email', - 'Phone No', + 'Phone No (+919999999999)', 'Preferences (should be separated by comma)', - 'Free period start date (YYYY-MM-DD)', - 'Free period end date (YYYY-MM-DD)', + 'Free period start date (DD-MM-YYYY)', + 'Free period end date (DD-MM-YYYY)', 'Address', 'Region', 'Country', @@ -976,10 +1016,10 @@ def export_excel_template(request): ['Last Name', 'The last name of the customer. This is a required field.'], ['Business Name', 'The official name of the customer\'s business or organization.'], ['Email', 'The email address of the customer. This must be a unique email not already used in the system. This is a required Field'], - ['Phone No', 'The business phone number. It should include the country code if applicable.'], + ['Phone No', 'The business phone number. It should include the country code if applicable (+919999999999).'], ['Category', 'A comma-separated list of event categories the customer is interested in. These should match existing categories in the system. This is a required Field'], - ['Free period start date', 'The start date of the customer\'s free trial period, formatted as YYYY-MM-DD.'], - ['Free period end date', 'The end date of the customer\'s free trial period, formatted as YYYY-MM-DD. This date must be later than the start date.'], + ['Free period start date', 'The start date of the customer\'s free trial period, formatted as DD-MM-YYYY.'], + ['Free period end date', 'The end date of the customer\'s free trial period, formatted as DD-MM-YYYY. This date must be later than the start date.'], ['Address', 'The complete business address, including street, city, and postal code.'], ['Region', 'The geographic region where the business operates.'], ['Country', 'The country where the business is based.'], @@ -1029,7 +1069,7 @@ class CustomerTransferView(LoginRequiredMixin, generic.View): "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() @@ -1054,6 +1094,36 @@ class CustomerImportView(LoginRequiredMixin, generic.View): context.update(kwargs) # Include any additional context data passed to the view return context + def validate_date(self, date_str, row_num, error_log, field_name): + """function to validate the date format DD-MM-YYYY""" + # Check if the input is already a datetime object + if isinstance(date_str, datetime): + return date_str + + # If it's a string, attempt to validate it + if isinstance(date_str, str): + try: + return datetime.strptime(date_str, '%d-%m-%Y') + except ValueError: + error_log.append(f"Row {row_num}: {field_name} '{date_str}' is not in the format DD-MM-YYYY.") + return None + + # If it's neither a string nor a datetime object, log an error + error_log.append(f"Row {row_num}: {field_name} '{date_str}' is of invalid type.") + return None + + def validate_phone(self, phone_str, row_num, error_log): + """Helper function to validate phone number""" + try: + phone_number = phonenumbers.parse(phone_str, None) + if not phonenumbers.is_valid_number(phone_number): + error_log.append(f"Row {row_num}: Phone number '{phone_str}' is not valid.") + return None + return phone_number + except Exception as e: + error_log.append(f"Row {row_num}: Phone number '{phone_str}' could not be parsed.") + return None + def get(self, request, *args, **kwargs): form = self.form_class() context = self.get_context_data(form=form) @@ -1104,11 +1174,24 @@ class CustomerImportView(LoginRequiredMixin, generic.View): error_log.append(f"Row {idx}: Email {email} already exists.") continue + # Validate start_date and end_date formats + start_date = self.validate_date(start_date, idx, error_log, 'Start Date') + end_date = self.validate_date(end_date, idx, error_log, 'End Date') + + if not start_date or not end_date: + continue # Skip if dates are invalid + # validate date rnage if end_date < start_date: error_log.append(f"Row {idx}: End date {end_date} must greater then start date {start_date}.") continue + # Validate phone number + if phone_no: + phone_number = self.validate_phone(str(phone_no), idx, error_log) + if not phone_number: + continue # Skip if phone number is invalid + # validate preferences preference_list = [pref.strip() for pref in preferences.split(',')] event_categories = EventCategory.objects.filter(title__in=preference_list) diff --git a/goodtimes/services.py b/goodtimes/services.py index fbf3ef8..92848f2 100644 --- a/goodtimes/services.py +++ b/goodtimes/services.py @@ -568,7 +568,7 @@ class GoogleMapsservice: def search_addresses_containing(self, keyword): """ Search for a list of addresses containing the given keyword. - + :param keyword: Keyword to search for in addresses :return: List of matching addresses containing the keyword """ @@ -589,49 +589,51 @@ class GoogleMapsservice: Returns: QuerySet: The filtered and sorted queryset of events. """ - - # Set the origin to the provided latitude and longitude origins = [(latitude, longitude)] - # Create a list of destination coordinates for all events with valid venues - destinations = [ - (event.venue.latitude, event.venue.longitude) + # Create a list of destination coordinates and map them to the events + destinations_and_events = [ + ((event.venue.latitude, event.venue.longitude), event) for event in queryset if event.venue.latitude and event.venue.longitude ] - # If there is no destination coordinates - if not destinations: + if not destinations_and_events: return queryset - # Get the distance matrix from the Google Maps API - matrix = self.get_distance_matrix(origins, destinations) + # Batch size for Google Distance Matrix API (max 25 elements) + batch_size = 25 + distances = {} - # Create a dictionary of event IDs and their corresponding distances - distances = { - event.id: element["distance"]["value"] - for event, element in zip(queryset, matrix["rows"][0]["elements"]) - if element["status"] == "OK" and element["distance"]["value"] <= radius_km * 1000 # Convert km to meters - } + # Loop through batches of destinations + for i in range(0, len(destinations_and_events), batch_size): + batch = destinations_and_events[i:i + batch_size] + print(f"batch list count is {len(batch)}") + destinations = [coords for coords, _ in batch] + events_in_batch = [event for _, event in batch] - print(f"distance is {distances} and distances key is {distances.keys()}") + # Call the Google Maps API for the current batch + matrix = self.get_distance_matrix(origins, destinations) + + # Extract distances and associate them with events + for event, element in zip(events_in_batch, matrix["rows"][0]["elements"]): + if element["status"] == "OK" and element["distance"]["value"] <= radius_km * 1000: # Convert km to meters + distances[event.id] = element["distance"]["value"] + + if not distances: + return queryset.none() # Filter the queryset to include only events within the specified radius queryset = queryset.filter(id__in=distances.keys()) - print(f"query set after distance filter {queryset}") - # Sort the event IDs by their distances in ascending order event_ids_by_distance = sorted(distances, key=distances.get) - print(f"sort event by it distance {event_ids_by_distance}") # Create a Case/When expression to preserve the order of events by distance preserved_order = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(event_ids_by_distance)]) - print(f"preserved_order is {preserved_order}") + # Order the queryset based on the preserved order queryset = queryset.order_by(preserved_order) - for data in queryset: - print(f"queryset after preserverd order {data.id}") return queryset diff --git a/goodtimes/webhook/webhook_service.py b/goodtimes/webhook/webhook_service.py index ba8cd4f..c2b9f5a 100644 --- a/goodtimes/webhook/webhook_service.py +++ b/goodtimes/webhook/webhook_service.py @@ -22,7 +22,7 @@ class WebhookService: def _fetch_metadata(self): """Fetch metadata based on the event type.""" - if self._event_type == "checkout.session.completed": + if self._event_type in ["checkout.session.expired", "checkout.session.completed"]: return self._charge_data.get("metadata", {}) elif self._event_type == "invoice.payment_succeeded": subscription_id = self._charge_data.get("subscription") diff --git a/manage_subscriptions/api/views.py b/manage_subscriptions/api/views.py index cb32eab..a912406 100644 --- a/manage_subscriptions/api/views.py +++ b/manage_subscriptions/api/views.py @@ -121,48 +121,6 @@ class CreatePrincipalSubscriptionApi(APIView): return ApiResponse.error(**fail_response) -# class CreatePrincipalSubscriptionApi(APIView): -# authentication_classes = [JWTAuthentication] -# permission_classes = [IsAuthenticated] - -# def post(self, request): -# serializer = PrincipalSubscriptionSerializer(data=request.data) - -# if serializer.is_valid(): -# subscription_id = serializer.validated_data.get("subscription").id -# try: -# subscription = Subscription.objects.get(id=subscription_id) -# except Subscription.DoesNotExist: -# return ApiResponse.error( -# status=status.HTTP_404_NOT_FOUND, message="Subscription not found." -# ) - -# start_date = timezone.localtime().date() -# end_date = start_date + timedelta(days=subscription.plan.days) -# grace_period_end_date = end_date + timedelta(days=15) - -# # You can directly pass the additional fields as save method arguments -# instance = serializer.save( -# start_date=start_date, -# end_date=end_date, -# grace_period_end_date=grace_period_end_date, -# created_by=request.user, # Assuming your model has this field and you want to track who created the subscription -# ) - -# success_response = { -# "status": status.HTTP_201_CREATED, # Use 201 for successful resource creation -# "message": "Success", -# "data": serializer.data, -# } -# return ApiResponse.success(**success_response) - -# else: -# fail_response = { -# "status": status.HTTP_400_BAD_REQUEST, -# "message": "Validation Failed", -# "errors": serializer.errors, -# } -# return ApiResponse.error(**fail_response) @method_decorator(csrf_exempt, name="dispatch") @@ -177,28 +135,29 @@ class StripeWebhookTest(APIView): sig_header = request.META["HTTP_STRIPE_SIGNATURE"] endpoint_secret = "whsec_ccf1f87295603cdd1733995ee2d3c0d6f74c7ceaf28916ea45114a54b7ce1d0f" # Make sure to retrieve this from your settings event = None + webhook_event = None + try: + # Construct Stripe event event = stripe.Event.construct_from(json.loads(payload), stripe.api_key) event_id = event["id"] event_type = event["type"] stripe_subscription_id = event["data"]["object"].get("subscription") + # Retrieve subscription details if available stripe_subscription = ( stripe.Subscription.retrieve(stripe_subscription_id) if stripe_subscription_id else None ) - current_period_start = ( - stripe_subscription["current_period_start"] - if stripe_subscription - else None - ) - current_period_end = ( - stripe_subscription["current_period_end"] - if stripe_subscription - else None - ) + current_period_start = stripe_subscription["current_period_start"] if stripe_subscription else None + current_period_end = stripe_subscription["current_period_end"] if stripe_subscription else None + + # Log received event details + logger.info(f"Received event {event_type} with ID {event_id}") + + # Get or create WebhookEvent in DB webhook_event, created = WebhookEvent.objects.get_or_create( event_id=event_id, defaults={ @@ -208,59 +167,80 @@ class StripeWebhookTest(APIView): ) if not created and webhook_event.status == "processed": + logger.info(f"Event {event_id} already processed.") return ApiResponse.success( status=status.HTTP_208_ALREADY_REPORTED, - message="Event already processed", + message="Event already processed.", ) + # Process the event payment_service = PaymentProcessingService( webhook_data=event, stripe_subscription=stripe_subscription_id, current_period_start=current_period_start, current_period_end=current_period_end, ) - payment_service.process_event() + + # Mark event as successfully processed webhook_event.status = "processed" webhook_event.processed_at = timezone.now() webhook_event.save() + logger.info(f"Event {event_id} processed successfully.") return ApiResponse.success( - status=status.HTTP_200_OK, message="Event processed successfully" + status=status.HTTP_200_OK, message="Event processed successfully." ) except stripe.error.SignatureVerificationError as e: - logger.error(f"Invalid Stripe signature: {str(e)}") + logger.error(f"Invalid Stripe signature for event: {str(e)}") return ApiResponse.error( status=status.HTTP_400_BAD_REQUEST, - message="Invalid signature", + message="Invalid signature.", errors=str(e), ) + except ValueError as e: - logger.error(f"Invalid payload: {str(e)}") + logger.error(f"Invalid payload for event: {str(e)}") return ApiResponse.error( status=status.HTTP_400_BAD_REQUEST, - message="Invalid payload", + message="Invalid payload.", errors=str(e), ) - except Transaction.DoesNotExist as e: - logger.error(f"Transaction does not exist: {str(e)}") + + except stripe.error.InvalidRequestError as e: + logger.error(f"Invalid request for event: {str(e)}") return ApiResponse.error( - status=status.HTTP_404_NOT_FOUND, - message="Transaction not found", + status=status.HTTP_400_BAD_REQUEST, + message="Invalid request to Stripe.", errors=str(e), ) + + except stripe.error.StripeError as e: + logger.error(f"General Stripe error: {str(e)}") + return ApiResponse.error( + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="Stripe error occurred.", + errors=str(e), + ) + except Exception as e: - logger.error(f"Error processing webhook event: {str(e)}") + logger.error(f"Unexpected error processing event {event_id}: {str(e)}") if "webhook_event" in locals(): webhook_event.status = "failed" webhook_event.error_message = str(e) webhook_event.save() + return ApiResponse.error( status=status.HTTP_500_INTERNAL_SERVER_ERROR, - message="Error processing event", + message="Unexpected error processing event.", errors=str(e), ) + finally: + print(f"finally is runn") + webhook_event.status = "processed" + webhook_event.processed_at = timezone.now() + webhook_event.save() class LastActiveSubscriptionView(APIView): diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 75335bf..e2af70e 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -535,6 +535,7 @@ def create_checkout_session(request): "metadata": { "transaction_amount": str(subscription.amount), "principal": str(principal_id), + "principal_email": str(request.user.email), "subscription_id": str(subscription.id), "product_id": subscription.product_id, "couponCode": coupon_code if coupon_code else None, diff --git a/templates/accounts/customer/customer_list.html b/templates/accounts/customer/customer_list.html index 0f04305..caf4463 100644 --- a/templates/accounts/customer/customer_list.html +++ b/templates/accounts/customer/customer_list.html @@ -129,7 +129,6 @@ -->
      - {% if data_obj.extended_data.is_onboarded%}
    • - {%endif%}
    • diff --git a/templates/accounts/customer/customer_manager_edit.html b/templates/accounts/customer/customer_manager_edit.html new file mode 100644 index 0000000..ccf62ef --- /dev/null +++ b/templates/accounts/customer/customer_manager_edit.html @@ -0,0 +1,162 @@ +{% extends 'layout/base_template.html' %} +{% load static %} +{% block stylesheet %} + + + {% include "cdn_through_html/flatpicker_cdn_css.html" %} +{% endblock %} + +{% block content %} + +
      +
      + +
      +
      +
      +
      + +
      + {% csrf_token %} + {% include 'includes/dynamic_template_form.html' with form=form %} +
      +
      +
      +
      + +
      +
      +
      +
      +
      +
      + + +{% endblock content %} + +{% block javascript %} + + {% include "cdn_through_html/flatpicker_cdn_js.html" %} + {% include "cdn_through_html/jquery_validate_cdn_js.html" %} + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/accounts/customer/customer_edit.html b/templates/accounts/customer/customer_onboard_manager_edit.html similarity index 100% rename from templates/accounts/customer/customer_edit.html rename to templates/accounts/customer/customer_onboard_manager_edit.html diff --git a/templates/accounts/customer/customer_user_edit.html b/templates/accounts/customer/customer_user_edit.html new file mode 100644 index 0000000..44ac214 --- /dev/null +++ b/templates/accounts/customer/customer_user_edit.html @@ -0,0 +1,153 @@ +{% extends 'layout/base_template.html' %} +{% load static %} +{% block stylesheet %} + + + {% include "cdn_through_html/flatpicker_cdn_css.html" %} +{% endblock %} + +{% block content %} + +
      +
      + +
      +
      +
      +
      + +
      + {% csrf_token %} + {% include 'includes/dynamic_template_form.html' with form=form %} +
      +
      +
      +
      + +
      +
      +
      +
      +
      +
      + + +{% endblock content %} + +{% block javascript %} + + {% include "cdn_through_html/flatpicker_cdn_js.html" %} + {% include "cdn_through_html/jquery_validate_cdn_js.html" %} + + + + +{% endblock %} \ No newline at end of file From 449d8615c849cd6bc3f2f99fe5ab06fb0a8f8d73 Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Thu, 26 Sep 2024 13:18:34 +0530 Subject: [PATCH 175/187] refactor: manage customer edit --- accounts/forms.py | 66 +----- accounts/views.py | 126 ++++------- .../accounts/customer/customer_edit.html | 206 ++++++++++++++++++ 3 files changed, 250 insertions(+), 148 deletions(-) create mode 100644 templates/accounts/customer/customer_edit.html diff --git a/accounts/forms.py b/accounts/forms.py index ccb045e..001fed5 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -401,7 +401,7 @@ class CreateCustomerForm(forms.Form): super().__init__(*args, **kwargs) self.fields['preferences'].queryset = EventCategory.objects.all() -class UpdateOnboardedEventManagerForm(forms.Form): +class UpdateCustomerForm(forms.Form): first_name = forms.CharField(max_length=255, required=True, label='First Name') last_name = forms.CharField(max_length=255, required=True, label='Last Name') business_name = forms.CharField(max_length=200, required=True, label="Business Name") @@ -443,69 +443,5 @@ class UpdateOnboardedEventManagerForm(forms.Form): self.fields['preferences'].queryset = EventCategory.objects.all() -class UpdateEventManagerForm(forms.ModelForm): - 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") - email = forms.EmailField(required=True, label='Email', widget=forms.TextInput(attrs={'readonly': 'readonly'})) - phone_no = PhoneNumberField( - widget=forms.TextInput(), - label="Phone No" - ) - address_line1 = forms.CharField(widget=forms.Textarea(attrs={'rows': 4, 'cols': 40})) - city = forms.CharField(max_length=200, required=False, label="Region") - country = forms.CharField(max_length=200, required=False, label="Country") - website = forms.URLField(max_length=255, required=False, label="Website") - linkedin_profile = forms.URLField(max_length=200, required=False, label="LinkedIn") - facebook_profile = forms.URLField(max_length=200, required=False, label="Facebook") - instagram_profile = forms.URLField(max_length=200, required=False, label="Instagram") - twitter_profile = forms.URLField(max_length=200, required=False, label="Twitter") - is_active = forms.BooleanField(required=False, label='Active', help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",) - - class Meta: - model = models.IAmPrincipal - fields = [ - "first_name", - "last_name", - "business_name", - "email", - "phone_no", - 'address_line1', - 'city', - 'country', - "website", - "linkedin_profile", - "facebook_profile", - "instagram_profile", - "twitter_profile", - "is_active", - ] - -class UpdateEventUserForm(forms.ModelForm): - first_name = forms.CharField(max_length=255, required=True, label='First Name') - last_name = forms.CharField(max_length=255, required=True, label='Last Name') - email = forms.EmailField(required=True, label='Email', widget=forms.TextInput(attrs={'readonly': 'readonly'})) - phone_no = PhoneNumberField( - widget=forms.TextInput(), - label="Phone No" - ) - address_line1 = forms.CharField(widget=forms.Textarea(attrs={'rows': 4, 'cols': 40})) - city = forms.CharField(max_length=200, required=False, label="Region") - country = forms.CharField(max_length=200, required=False, label="Country") - is_active = forms.BooleanField(required=False, label='Active', help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",) - - class Meta: - model = models.IAmPrincipal - fields = [ - "first_name", - "last_name", - "email", - "phone_no", - 'address_line1', - 'city', - 'country', - "is_active", - ] - class UploadExcelForm(forms.Form): file = forms.FileField() diff --git a/accounts/views.py b/accounts/views.py index 8602b44..11258d3 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -30,7 +30,7 @@ from manage_events.models import EventCategory, PrincipalPreference from manage_referrals.models import ReferralCode from manage_subscriptions.models import PrincipalSubscription, Subscription import datetime -from datetime import datetime +from datetime import datetime, timedelta from . import resource_action from .forms import ( @@ -42,9 +42,7 @@ from .forms import ( IAmPrincipalRoleAppResourceActionLinkForm, IAmPrincipalGroupLinkForm, ProfileEditForm, - UpdateEventManagerForm, - UpdateEventUserForm, - UpdateOnboardedEventManagerForm, + UpdateCustomerForm, UploadExcelForm, ) from .models import ( @@ -681,13 +679,29 @@ class CustomerUpdateView(LoginRequiredMixin, generic.View): page_name = resource_action.RESOURCE_MANAGE_CUSTOMER resource = resource_action.RESOURCE_MANAGE_CUSTOMER model = IAmPrincipal - template_name = None + form_class = UpdateCustomerForm + template_name = "accounts/customer/customer_edit.html" success_url = reverse_lazy("accounts:customer_list") success_message = "Updated Successfully" error_message = "An error occurred while saving the data." - def OnboardedEventManagerFormData(self, principal_obj): - form_class = UpdateOnboardedEventManagerForm + def get_context_data(self, **kwargs): + context = { + "page_name": self.page_name, + "operation": "Edit", + } + context.update(kwargs) # Include any additional context data passed to the view + return context + + def get(self, request, *args, **kwargs): + principal_id = kwargs.get("pk") + try: + principal_obj = IAmPrincipal.objects.get(pk=principal_id) + except Exception as e: + messages.error(request, f"No Record of id {principal_id} is found") + return redirect(self.success_url) + + print(f"principal address is {principal_obj.address_line1}") initial_data = { "first_name": principal_obj.first_name, @@ -720,40 +734,7 @@ class CustomerUpdateView(LoginRequiredMixin, generic.View): initial_data["free_start_date"] = None initial_data["free_end_date"] = None - return form_class(initial=initial_data) - - def get_context_data(self, **kwargs): - context = { - "page_name": self.page_name, - "operation": "Edit", - } - context.update(kwargs) # Include any additional context data passed to the view - return context - - def get(self, request, *args, **kwargs): - principal_id = kwargs.get("pk") - try: - principal_obj = IAmPrincipal.objects.get(pk=principal_id) - except Exception as e: - messages.error(request, f"No Record of id {principal_id} is found") - return redirect(self.success_url) - - print(f"principal address is {principal_obj.address_line1}") - - is_onboarded = False - if hasattr(principal_obj, 'extended_data') and principal_obj.extended_data: - is_onboarded = principal_obj.extended_data.is_onboarded - - if is_onboarded and principal_obj.principal_type.name == resource_action.PRINCIPAL_TYPE_EVENT_MANAGER: - form = self.OnboardedEventManagerFormData(principal_obj) - self.template_name = "accounts/customer/customer_onboard_manager_edit.html" - elif not is_onboarded and principal_obj.principal_type.name == resource_action.PRINCIPAL_TYPE_EVENT_MANAGER: - form = UpdateEventManagerForm(instance=principal_obj) - self.template_name = "accounts/customer/customer_manager_edit.html" - elif principal_obj.principal_type.name == resource_action.PRINCIPAL_TYPE_EVENT_USER: - form = UpdateEventUserForm(instance=principal_obj) - self.template_name = "accounts/customer/customer_user_edit.html" - + form = self.form_class(initial=initial_data) context = self.get_context_data(form=form, principal_obj=principal_obj) print("context dta is ", context) return render(request, self.template_name, context=context) @@ -765,31 +746,10 @@ 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) - - is_onboarded = False - if hasattr(principal_obj, 'extended_data') and principal_obj.extended_data: - is_onboarded = principal_obj.extended_data.is_onboarded - - # Dynamically choose the form based on the principal object's data - if is_onboarded and principal_obj.principal_type.name == resource_action.PRINCIPAL_TYPE_EVENT_MANAGER: - form_class = UpdateOnboardedEventManagerForm - self.template_name = "accounts/customer/customer_onboard_manager_edit.html" - elif not is_onboarded and principal_obj.principal_type.name == resource_action.PRINCIPAL_TYPE_EVENT_MANAGER: - form_class = UpdateEventManagerForm - self.template_name = "accounts/customer/customer_manager_edit.html" - elif principal_obj.principal_type.name == resource_action.PRINCIPAL_TYPE_EVENT_USER: - form_class = UpdateEventUserForm - self.template_name = "accounts/customer/customer_user_edit.html" - else: - messages.error(request, "Invalid principal type") - return redirect(self.success_url) - - form = form_class(request.POST, instance=principal_obj) - + form = self.form_class(request.POST) if not form.is_valid(): context = self.get_context_data(form=form) return render(request, self.template_name, context=context) - try: with transaction.atomic(): # update principal data @@ -805,29 +765,29 @@ class CustomerUpdateView(LoginRequiredMixin, generic.View): principal_obj.facebook_profile = form.cleaned_data.get("facebook_profile") principal_obj.instagram_profile = form.cleaned_data.get("instagram_profile") principal_obj.twitter_profile = form.cleaned_data.get("twitter_profile") + principal_obj.is_active = form.cleaned_data.get("active") principal_obj.save() - if is_onboarded: # only update preference and subscription if it is added by admin - # update principal preferences record - principal_preference, _ = PrincipalPreference.objects.get_or_create(principal=principal_obj) - principal_preference.preferred_categories.set(form.cleaned_data.get("preferences")) + # update principal preferences record + principal_preference, _ = PrincipalPreference.objects.get_or_create(principal=principal_obj) + principal_preference.preferred_categories.set(form.cleaned_data.get("preferences")) - # update principal subscription record - principal_subscription = PrincipalSubscription.objects.filter(principal=principal_obj).order_by('-end_date').first() - if principal_subscription: - principal_subscription.start_date = form.cleaned_data.get("free_start_date") - principal_subscription.end_date = form.cleaned_data.get("free_end_date") - principal_subscription.grace_period_end_date = form.cleaned_data.get("free_end_date") + datetime.timedelta(days=15) - principal_subscription.save() - else: - PrincipalSubscription.objects.create( - principal=principal_obj, - start_date=form.cleaned_data.get("free_start_date"), - end_date=form.cleaned_data.get("free_end_date"), - grace_period_end_date=PrincipalSubscription.generate_grace_period_end_date(form.cleaned_data.get("free_end_date")), - is_paid=True, - subscription=Subscription.objects.filter().first() # Assuming you want to link a default subscription - ) + # update principal subscription record + principal_subscription = PrincipalSubscription.objects.filter(principal=principal_obj).order_by('-end_date').first() + if principal_subscription: + principal_subscription.start_date = form.cleaned_data.get("free_start_date") + principal_subscription.end_date = form.cleaned_data.get("free_end_date") + principal_subscription.grace_period_end_date = form.cleaned_data.get("free_end_date") + timedelta(days=15) + principal_subscription.save() + else: + PrincipalSubscription.objects.create( + principal=principal_obj, + start_date=form.cleaned_data.get("free_start_date"), + end_date=form.cleaned_data.get("free_end_date"), + grace_period_end_date=PrincipalSubscription.generate_grace_period_end_date(form.cleaned_data.get("free_end_date")), + is_paid=True, + subscription=Subscription.objects.filter().first() # Assuming you want to link a default subscription + ) messages.success(self.request, self.success_message) return redirect(self.success_url) diff --git a/templates/accounts/customer/customer_edit.html b/templates/accounts/customer/customer_edit.html new file mode 100644 index 0000000..7a45364 --- /dev/null +++ b/templates/accounts/customer/customer_edit.html @@ -0,0 +1,206 @@ +{% extends 'layout/base_template.html' %} +{% load static %} +{% block stylesheet %} + + + {% include "cdn_through_html/flatpicker_cdn_css.html" %} +{% endblock %} + +{% block content %} + +
      +
      +
      + + + {% if principal_obj.extended_data and not principal_obj.extended_data.is_transferred and principal_obj.extended_data.is_onboarded and principal_obj.principal_type.name == 'event_manager' %} + + {% endif %} + +
      +
      +
      +
      +
      + +
      + {% csrf_token %} + {% include 'includes/dynamic_template_form.html' with form=form %} +
      +
      +
      +
      + +
      +
      +
      +
      +
      +
      + + +{% endblock content %} + +{% block javascript %} + + {% include "cdn_through_html/flatpicker_cdn_js.html" %} + {% include "cdn_through_html/jquery_validate_cdn_js.html" %} + + + + +{% endblock %} \ No newline at end of file From 540442fa287df8c270f372ea6ae04110d6d1b842 Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Wed, 30 Oct 2024 12:33:53 +0530 Subject: [PATCH 176/187] fix: stipe key issue --- goodtimes/settings/base.py | 9 +++++---- manage_subscriptions/api/views.py | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/goodtimes/settings/base.py b/goodtimes/settings/base.py index c4a02c3..d0dde01 100644 --- a/goodtimes/settings/base.py +++ b/goodtimes/settings/base.py @@ -303,11 +303,12 @@ SIMPLE_JWT = { "JTI_CLAIM": "jti", } -STRIPE_SECRET_KEY = "sk_test_51OexsKCesU6kunsIsbSKSZc1BF4gjklniaue8lmpkGKqDzenQtMkR8tKAryxErJXqp0jPiu1Gg7papa4tqZfKL9G00qUM4toB2" -STRIPE_PUBLISH_KEY = "pk_test_51OexsKCesU6kunsINDvKUhbelxeUmDAVZGSOisZ6XXHCp3pKtl4vs0pR42w0OcjZhngmECsXQNbAKNPOhiFMTJ8o00sRZQG0lh" -# STRIPE_SECRET_KEY = env.str("STRIPE_SECRET_KEY") -# STRIPE_PUBLISH_KEY = env.str("STRIPE_PUBLISH_KEY") +STRIPE_SECRET_KEY = env.str("STRIPE_SECRET_KEY") +STRIPE_PUBLISH_KEY = env.str("STRIPE_PUBLISH_KEY") +# https://dashboard.stripe.com/webhooks/create?endpoint_location=local +# This is your Stripe CLI webhook secret for testing your endpoint locally. +ENDPOINT_SECRET = "whsec_ccf1f87295603cdd1733995ee2d3c0d6f74c7ceaf28916ea45114a54b7ce1d0f" ONE_SIGNAL_APP_ID = env.str("ONE_SIGNAL_APP_ID") diff --git a/manage_subscriptions/api/views.py b/manage_subscriptions/api/views.py index a912406..56deb6e 100644 --- a/manage_subscriptions/api/views.py +++ b/manage_subscriptions/api/views.py @@ -133,7 +133,8 @@ class StripeWebhookTest(APIView): stripe.api_key = settings.STRIPE_SECRET_KEY payload = request.body sig_header = request.META["HTTP_STRIPE_SIGNATURE"] - endpoint_secret = "whsec_ccf1f87295603cdd1733995ee2d3c0d6f74c7ceaf28916ea45114a54b7ce1d0f" # Make sure to retrieve this from your settings + # This is your Stripe CLI webhook secret for testing your endpoint locally. + endpoint_secret = settings.ENDPOINT_SECRET event = None webhook_event = None From 6f07fe4877d3357cf041a4f2448cfa66a9198f15 Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Wed, 30 Oct 2024 12:36:05 +0530 Subject: [PATCH 177/187] Updated : key guest for multiple --- manage_events/api/serializers.py | 32 ++++++++++++++++++++++++++++++++ manage_events/models.py | 13 +++++++++++++ 2 files changed, 45 insertions(+) diff --git a/manage_events/api/serializers.py b/manage_events/api/serializers.py index 042da84..7714253 100644 --- a/manage_events/api/serializers.py +++ b/manage_events/api/serializers.py @@ -107,6 +107,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) @@ -178,6 +184,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): @@ -212,9 +224,16 @@ class CreateEventSerializer(serializers.ModelSerializer): "coupon_description", ] + 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 +243,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 +264,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 +274,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: diff --git a/manage_events/models.py b/manage_events/models.py index 03638ab..276db10 100644 --- a/manage_events/models.py +++ b/manage_events/models.py @@ -129,6 +129,19 @@ class Event(BaseModel): 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 From eef12c857990f2d8fae55a49d8b192c7ca3a57b4 Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Wed, 6 Nov 2024 14:39:50 +0530 Subject: [PATCH 178/187] chore(email): adjust email configuration to reduce spam --- accounts/api/views.py | 4 ++-- accounts/views.py | 2 +- env.example | 1 + goodtimes/settings/base.py | 1 + manage_communications/views.py | 2 +- manage_events/api/views.py | 2 +- manage_events/management/commands/manager_report.py | 2 +- .../accounts/customer/account_transfer_email_template.html | 2 +- 8 files changed, 9 insertions(+), 7 deletions(-) diff --git a/accounts/api/views.py b/accounts/api/views.py index 26cf063..f48fa32 100644 --- a/accounts/api/views.py +++ b/accounts/api/views.py @@ -123,7 +123,7 @@ class RegistrationEmailView(APIView): email_service = EmailService( subject="Good Times - OTP", to=[email], - from_email=settings.EMAIL_HOST_USER, + from_email=settings.DEFAULT_FROM_EMAIL, ) email_service.load_template( "otp/otp.html", context={"OTP": otp, "action": "Register"} @@ -322,7 +322,7 @@ class OtpRequestView(APIView): email_service = EmailService( subject="Good Times - OTP", to=[email], - from_email=settings.EMAIL_HOST_USER, + from_email=settings.DEFAULT_FROM_EMAIL, ) email_service.load_template( "otp/otp.html", context={"OTP": otp, "action": "Login"} diff --git a/accounts/views.py b/accounts/views.py index 11258d3..1f36689 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1017,7 +1017,7 @@ class CustomerTransferView(LoginRequiredMixin, generic.View): email_service = EmailService( subject="Your Exclusive Account Access Details with Good Times!", to=principal_obj.email, - from_email=settings.EMAIL_HOST_USER, + from_email=settings.DEFAULT_FROM_EMAIL, ) # Send the email diff --git a/env.example b/env.example index 7c8ebed..dc80c69 100644 --- a/env.example +++ b/env.example @@ -23,6 +23,7 @@ EMAIL_PORT= EMAIL_HOST_USER= EMAIL_HOST_PASSWORD= EMAIL_USE_TLS= +DEFAULT_FROM_EMAIL= GOOGLE_MAPS_API_KEY= diff --git a/goodtimes/settings/base.py b/goodtimes/settings/base.py index d0dde01..5d57fd3 100644 --- a/goodtimes/settings/base.py +++ b/goodtimes/settings/base.py @@ -239,6 +239,7 @@ EMAIL_HOST_USER = env.str("EMAIL_HOST_USER") EMAIL_HOST_PASSWORD = env.str("EMAIL_HOST_PASSWORD") EMAIL_PORT = env.str("EMAIL_PORT") EMAIL_USE_TLS = True +DEFAULT_FROM_EMAIL = env.str("DEFAULT_FROM_EMAIL") # LOGGING diff --git a/manage_communications/views.py b/manage_communications/views.py index 6cdaeba..a1a2108 100644 --- a/manage_communications/views.py +++ b/manage_communications/views.py @@ -56,7 +56,7 @@ class ContactUsReplyView(LoginRequiredMixin, generic.View): to=[ email, ], - from_email=settings.EMAIL_HOST_USER, + from_email=settings.DEFAULT_FROM_EMAIL, ) print("email_service: ", email_service) email_service.load_template( diff --git a/manage_events/api/views.py b/manage_events/api/views.py index 7ce903f..6c135c1 100644 --- a/manage_events/api/views.py +++ b/manage_events/api/views.py @@ -165,7 +165,7 @@ class CreateVenueApi(APIView): # email_service = EmailService( # subject="Good Times - Report", # to=[user.email], -# from_email=settings.EMAIL_HOST_USER, +# from_email=settings.DEFAULT_FROM_EMAIL, # ) # email_service.attach(filename, buffer.getvalue(), "application/pdf") diff --git a/manage_events/management/commands/manager_report.py b/manage_events/management/commands/manager_report.py index 8da35e6..5e8778e 100644 --- a/manage_events/management/commands/manager_report.py +++ b/manage_events/management/commands/manager_report.py @@ -27,7 +27,7 @@ class Command(BaseCommand): subject="Monthly Event Report", body="Please find the attached report for the last month.", to=[email], - from_email=settings.EMAIL_HOST_USER, + from_email=settings.DEFAULT_FROM_EMAIL, ) email_message.attach(filename, pdf_data, "application/pdf") email_message.send() diff --git a/templates/accounts/customer/account_transfer_email_template.html b/templates/accounts/customer/account_transfer_email_template.html index 3c9423a..42fea12 100644 --- a/templates/accounts/customer/account_transfer_email_template.html +++ b/templates/accounts/customer/account_transfer_email_template.html @@ -26,6 +26,6 @@

      We sincerely hope your experience with Good Times has been delightful thus far and look forward to continuing to exceed your expectations!

      Warmest regards,

      Good Times
      - {{ settings.EMAIL_HOST_USER }}

      + {{ settings.DEFAULT_FROM_EMAIL }}

      \ No newline at end of file From 76ac9413c4c00e5d96f0baa7f23b24a1a503490d Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Thu, 28 Nov 2024 15:56:20 +0530 Subject: [PATCH 179/187] fix: report isssue and custom command to generate individual marchant report --- .../commands/get_specific_manager_report.py | 60 +++++++++++++++++++ .../management/commands/manager_report.py | 2 +- manage_events/report.py | 8 +-- 3 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 manage_events/management/commands/get_specific_manager_report.py diff --git a/manage_events/management/commands/get_specific_manager_report.py b/manage_events/management/commands/get_specific_manager_report.py new file mode 100644 index 0000000..97a8adb --- /dev/null +++ b/manage_events/management/commands/get_specific_manager_report.py @@ -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)}")) diff --git a/manage_events/management/commands/manager_report.py b/manage_events/management/commands/manager_report.py index 5e8778e..c2117bc 100644 --- a/manage_events/management/commands/manager_report.py +++ b/manage_events/management/commands/manager_report.py @@ -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) diff --git a/manage_events/report.py b/manage_events/report.py index 704ce90..70c0d75 100644 --- a/manage_events/report.py +++ b/manage_events/report.py @@ -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() From 3f263b2af5106211ea5d80acaac92168463506bb Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Thu, 28 Nov 2024 16:05:32 +0530 Subject: [PATCH 180/187] fixed: filter title with tag and calendar issue --- manage_events/api/filters.py | 9 +++- manage_events/api/urls.py | 4 +- manage_events/api/views.py | 101 ++++++++++++++--------------------- 3 files changed, 51 insertions(+), 63 deletions(-) diff --git a/manage_events/api/filters.py b/manage_events/api/filters.py index e26a938..856ccbe 100644 --- a/manage_events/api/filters.py +++ b/manage_events/api/filters.py @@ -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) diff --git a/manage_events/api/urls.py b/manage_events/api/urls.py index 1937875..07fc2b4 100644 --- a/manage_events/api/urls.py +++ b/manage_events/api/urls.py @@ -11,7 +11,7 @@ urlpatterns = [ name="add_event", ), path("edit-event//", views.EventEditAPIView.as_view(), name="event-edit"), - path("get-events/", views.EventsAPIView.as_view(), name="events"), + path( "event//", 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/", diff --git a/manage_events/api/views.py b/manage_events/api/views.py index 6c135c1..3a682ed 100644 --- a/manage_events/api/views.py +++ b/manage_events/api/views.py @@ -199,67 +199,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 +979,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] From 8ccec7012c4637dbb60be794aec84c1bf57d02c4 Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Mon, 2 Dec 2024 11:51:51 +0530 Subject: [PATCH 181/187] refactor: custom command to update social key --- goodtimes/asgi.py | 4 +- goodtimes/services.py | 49 +-------- goodtimes/settings/base.py | 5 +- goodtimes/settings/staging.py | 2 +- .../commands/update_facebook_tokens.py | 103 ++++++++++++++++++ manage_events/views.py | 2 +- templates/manage_events/event_add.html | 1 + 7 files changed, 117 insertions(+), 49 deletions(-) create mode 100644 manage_events/management/commands/update_facebook_tokens.py diff --git a/goodtimes/asgi.py b/goodtimes/asgi.py index eccfdf2..0c06e2b 100644 --- a/goodtimes/asgi.py +++ b/goodtimes/asgi.py @@ -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( { diff --git a/goodtimes/services.py b/goodtimes/services.py index 92848f2..940b66f 100644 --- a/goodtimes/services.py +++ b/goodtimes/services.py @@ -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() diff --git a/goodtimes/settings/base.py b/goodtimes/settings/base.py index 5d57fd3..fc658bb 100644 --- a/goodtimes/settings/base.py +++ b/goodtimes/settings/base.py @@ -83,7 +83,7 @@ THIRD_PARTY_APPS = [ "allauth.socialaccount.providers.apple", "allauth.socialaccount.providers.google", "django_filters", - # "django_crontab", + "django_crontab", # "django_celery_results", # "django_celery_beat", ] @@ -336,7 +336,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 +356,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 diff --git a/goodtimes/settings/staging.py b/goodtimes/settings/staging.py index 71641cd..cb67fbc 100644 --- a/goodtimes/settings/staging.py +++ b/goodtimes/settings/staging.py @@ -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( diff --git a/manage_events/management/commands/update_facebook_tokens.py b/manage_events/management/commands/update_facebook_tokens.py new file mode 100644 index 0000000..901df24 --- /dev/null +++ b/manage_events/management/commands/update_facebook_tokens.py @@ -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 diff --git a/manage_events/views.py b/manage_events/views.py index be68863..b3f50ce 100644 --- a/manage_events/views.py +++ b/manage_events/views.py @@ -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']: diff --git a/templates/manage_events/event_add.html b/templates/manage_events/event_add.html index 950a41e..2aeec9e 100644 --- a/templates/manage_events/event_add.html +++ b/templates/manage_events/event_add.html @@ -195,6 +195,7 @@ // Initialize Tagify initializeTagify('#id_tags'); + initializeTagify('#id_key_guest'); // Handle principal change handlePrincipalChange(); From 3425d828916b7995212d6d13b62ac39108daa0e5 Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Mon, 2 Dec 2024 12:05:45 +0530 Subject: [PATCH 182/187] updated : requirement.txt --- requirements.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1eaff72..7f10b36 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 From 24949ade3ead4625e2cf167f3c2f2852a50a65ce Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Fri, 20 Dec 2024 19:43:44 +0530 Subject: [PATCH 183/187] feat(customer): customer cred and postcode for address --- ...iamprincipalextendeddata_encrypted_pass.py | 18 ++ accounts/models.py | 16 +- accounts/urls.py | 1 + accounts/views.py | 45 +++- goodtimes/services.py | 19 +- goodtimes/settings/base.py | 2 + manage_events/api/views.py | 1 + manage_events/forms.py | 2 + .../migrations/0017_venue_postcode.py | 18 ++ manage_events/models.py | 3 +- manage_events/views.py | 17 +- ...015_alter_subscription_long_description.py | 19 ++ requirements.txt | 2 +- .../accounts/customer/customer_list.html | 227 ++++++++++++------ templates/manage_venues/venue_list.html | 3 + 15 files changed, 301 insertions(+), 92 deletions(-) create mode 100644 accounts/migrations/0016_iamprincipalextendeddata_encrypted_pass.py create mode 100644 manage_events/migrations/0017_venue_postcode.py create mode 100644 manage_subscriptions/migrations/0015_alter_subscription_long_description.py diff --git a/accounts/migrations/0016_iamprincipalextendeddata_encrypted_pass.py b/accounts/migrations/0016_iamprincipalextendeddata_encrypted_pass.py new file mode 100644 index 0000000..9c4bda1 --- /dev/null +++ b/accounts/migrations/0016_iamprincipalextendeddata_encrypted_pass.py @@ -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), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index a7c6968..538369b 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -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, diff --git a/accounts/urls.py b/accounts/urls.py index 99c99c9..369f3be 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -42,6 +42,7 @@ urlpatterns = [ path('principal/role/delete//', views.AppRoleDeleteView.as_view(), name="role_delete"), path('customer/', views.CustomerListView.as_view(), name="customer_list"), + path('customer/get-decrypted-password//', views.GetDecryptedPasswordView.as_view(), name='get_decrypted_password'), path('customer/add/', views.CustomerCreateView.as_view(), name="customer_add"), path('customer/edit//', views.CustomerUpdateView.as_view(), name="customer_edit"), path('customer/detail//', views.CustomerDetailView.as_view(), name="customer_detail"), diff --git a/accounts/views.py b/accounts/views.py index 1f36689..3463ec2 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -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 @@ -621,6 +621,12 @@ 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( email=form.cleaned_data.get('email'), @@ -628,7 +634,7 @@ class CustomerCreateView(LoginRequiredMixin, generic.View): 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 +657,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 +672,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: @@ -862,6 +868,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,7 +1049,7 @@ class CustomerTransferView(LoginRequiredMixin, generic.View): # Send the email try: - temp_password = "goodtimes#2024" + temp_password = "GoodTimes@2025" principal_obj.password = make_password(temp_password) principal_obj.save() email_service.load_template( @@ -1159,12 +1186,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 +1240,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( diff --git a/goodtimes/services.py b/goodtimes/services.py index 940b66f..792e22c 100644 --- a/goodtimes/services.py +++ b/goodtimes/services.py @@ -726,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'} @@ -1130,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}'} \ No newline at end of file + + 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() + diff --git a/goodtimes/settings/base.py b/goodtimes/settings/base.py index fc658bb..8e96bc9 100644 --- a/goodtimes/settings/base.py +++ b/goodtimes/settings/base.py @@ -77,6 +77,7 @@ THIRD_PARTY_APPS = [ "taggit", "django_quill", "corsheaders", + 'django_extensions', "allauth", "allauth.account", "allauth.socialaccount", @@ -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 diff --git a/manage_events/api/views.py b/manage_events/api/views.py index 3a682ed..99228c2 100644 --- a/manage_events/api/views.py +++ b/manage_events/api/views.py @@ -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) diff --git a/manage_events/forms.py b/manage_events/forms.py index 3f40650..c17ecfd 100644 --- a/manage_events/forms.py +++ b/manage_events/forms.py @@ -148,6 +148,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 +162,7 @@ class VenueForm(forms.ModelForm): "principal", "title", "address", + "postcode", "image", "latitude", "longitude", diff --git a/manage_events/migrations/0017_venue_postcode.py b/manage_events/migrations/0017_venue_postcode.py new file mode 100644 index 0000000..8eb3bef --- /dev/null +++ b/manage_events/migrations/0017_venue_postcode.py @@ -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), + ), + ] diff --git a/manage_events/models.py b/manage_events/models.py index 276db10..e0d19f8 100644 --- a/manage_events/models.py +++ b/manage_events/models.py @@ -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" diff --git a/manage_events/views.py b/manage_events/views.py index b3f50ce..d1899a4 100644 --- a/manage_events/views.py +++ b/manage_events/views.py @@ -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']: diff --git a/manage_subscriptions/migrations/0015_alter_subscription_long_description.py b/manage_subscriptions/migrations/0015_alter_subscription_long_description.py new file mode 100644 index 0000000..417f2d0 --- /dev/null +++ b/manage_subscriptions/migrations/0015_alter_subscription_long_description.py @@ -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(), + ), + ] diff --git a/requirements.txt b/requirements.txt index 7f10b36..0724efc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -75,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 diff --git a/templates/accounts/customer/customer_list.html b/templates/accounts/customer/customer_list.html index caf4463..31b0ffb 100644 --- a/templates/accounts/customer/customer_list.html +++ b/templates/accounts/customer/customer_list.html @@ -1,8 +1,10 @@ {% extends 'layout/base_template.html' %} {% load static %} {% block stylesheet %} - - {% include "cdn_through_html/datatable_cdn_css.html" %} + +{% 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 @@ Download Excel Template - + Import @@ -27,12 +29,12 @@ Export - + Add Customer
    - - + +
    @@ -46,39 +48,39 @@ Record Id - Image - First Name - Last Name - Email - - Principal Type + Image + First Name + Last Name + Email + # + Principal Type - Email Verified - Referral Count - Onboarded by Admin - Transferred to Customer - Created On - Modified On - Active + Email Verified + Referral Count + Onboarded by Admin + Transferred to Customer + + Created On + Modified On + Active - Action + Action @@ -92,33 +94,43 @@ {{ data_obj.first_name }} {{ data_obj.last_name }} {{ data_obj.email }} - + + {% if data_obj.extended_data %} + + {% endif %} + {{ data_obj.principal_type.name }} {{ data_obj.email_verified }} {{ data_obj.referral_count }} - + {% if data_obj.extended_data %} - {{ data_obj.extended_data.is_onboarded }} + {{ data_obj.extended_data.is_onboarded }} {% else %} - False + False {% endif %} - + {% if data_obj.extended_data %} - {{ data_obj.extended_data.is_transferred }} + {{ data_obj.extended_data.is_transferred }} {% else %} - False + False {% endif %} {{ data_obj.created_on }} {{ data_obj.modified_on }} - + {{ data_obj.is_active }} @@ -129,11 +141,12 @@ --> @@ -159,31 +183,88 @@
    + + + + {% endblock content %} {% block javascript %} - - {% include "cdn_through_html/datatable_cdn_js.html" %} - - + {% endblock %} \ No newline at end of file diff --git a/templates/manage_venues/venue_list.html b/templates/manage_venues/venue_list.html index 31e8a2b..232d291 100644 --- a/templates/manage_venues/venue_list.html +++ b/templates/manage_venues/venue_list.html @@ -45,6 +45,8 @@ aria-sort="ascending" style="width: 69.2656px;"> Title Address + Postcode Latitude {{data_obj.title}} {{data_obj.address}} + {{data_obj.postcode}} {{data_obj.latitude}} {{data_obj.longitude}} From b559c62926ac5f1397e0a1c94fdc67e3c72817ff Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Tue, 24 Dec 2024 17:00:59 +0530 Subject: [PATCH 184/187] updated(event): added link field in event --- manage_events/api/serializers.py | 2 ++ manage_events/migrations/0018_event_link.py | 18 ++++++++++++++++++ manage_events/models.py | 1 + 3 files changed, 21 insertions(+) create mode 100644 manage_events/migrations/0018_event_link.py diff --git a/manage_events/api/serializers.py b/manage_events/api/serializers.py index 7714253..d16492b 100644 --- a/manage_events/api/serializers.py +++ b/manage_events/api/serializers.py @@ -144,6 +144,7 @@ class EventDetailSerializer(serializers.ModelSerializer): "key_guest", "coupon_code", "coupon_description", + "link", "age_group", "images", "is_favorited", @@ -222,6 +223,7 @@ class CreateEventSerializer(serializers.ModelSerializer): "tags", "coupon_code", "coupon_description", + "link" ] def validate_key_guest(self, value): diff --git a/manage_events/migrations/0018_event_link.py b/manage_events/migrations/0018_event_link.py new file mode 100644 index 0000000..2721ecb --- /dev/null +++ b/manage_events/migrations/0018_event_link.py @@ -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), + ), + ] diff --git a/manage_events/models.py b/manage_events/models.py index e0d19f8..9be9dd4 100644 --- a/manage_events/models.py +++ b/manage_events/models.py @@ -125,6 +125,7 @@ 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 From bec8f9d6082e855ae44eb1ab6c17f982070c6b6d Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Wed, 8 Jan 2025 12:37:07 +0530 Subject: [PATCH 185/187] Update(Customer): added profile photo field --- accounts/forms.py | 2 + accounts/views.py | 24 +- goodtimes/urls.py | 10 +- manage_events/api/serializers.py | 1 + manage_events/forms.py | 1 + templates/accounts/customer/customer_add.html | 389 +++++++++++------- .../accounts/customer/customer_edit.html | 83 +++- .../accounts/customer/customer_list.html | 12 +- templates/manage_events/event_add.html | 23 +- 9 files changed, 370 insertions(+), 175 deletions(-) diff --git a/accounts/forms.py b/accounts/forms.py index 001fed5..9f07e76 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -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") diff --git a/accounts/views.py b/accounts/views.py index 3463ec2..c4052f2 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -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) @@ -629,6 +629,7 @@ class CustomerCreateView(LoginRequiredMixin, generic.View): # 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'), @@ -710,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, @@ -752,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") @@ -1049,17 +1053,23 @@ class CustomerTransferView(LoginRequiredMixin, generic.View): # Send the email try: - temp_password = "GoodTimes@2025" + 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)}") diff --git a/goodtimes/urls.py b/goodtimes/urls.py index c31e7f0..f1cc9b8 100644 --- a/goodtimes/urls.py +++ b/goodtimes/urls.py @@ -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))] diff --git a/manage_events/api/serializers.py b/manage_events/api/serializers.py index d16492b..f6c15ea 100644 --- a/manage_events/api/serializers.py +++ b/manage_events/api/serializers.py @@ -99,6 +99,7 @@ class EventListSerializer(serializers.ModelSerializer): "entry_fee", "key_guest", "age_group", + "link", # "images", # "is_favorited", # "reviews", diff --git a/manage_events/forms.py b/manage_events/forms.py index c17ecfd..070f1cd 100644 --- a/manage_events/forms.py +++ b/manage_events/forms.py @@ -46,6 +46,7 @@ class EventForm(forms.ModelForm): "title", # "event_master", "description", + "link", "image", "event_images", # "status", diff --git a/templates/accounts/customer/customer_add.html b/templates/accounts/customer/customer_add.html index 4c30dc8..d147cf1 100644 --- a/templates/accounts/customer/customer_add.html +++ b/templates/accounts/customer/customer_add.html @@ -1,9 +1,30 @@ {% extends 'layout/base_template.html' %} {% load static %} {% block stylesheet %} - - - {% include "cdn_through_html/flatpicker_cdn_css.html" %} + + +{% include "cdn_through_html/flatpicker_cdn_css.html" %} +{% include "cdn_through_html/filepond_cdn_css.html" %} + + + {% endblock %} {% block content %} @@ -12,10 +33,12 @@
    @@ -24,14 +47,15 @@
    -
    + {% csrf_token %} {% include 'includes/dynamic_template_form.html' with form=form %}
    -
    +
    - +
    @@ -43,155 +67,216 @@ {% endblock content %} {% block javascript %} - - {% include "cdn_through_html/flatpicker_cdn_js.html" %} - {% include "cdn_through_html/jquery_validate_cdn_js.html" %} - - +{% 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" %} + + - + // 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(); + }); + }) + {% endblock %} \ No newline at end of file diff --git a/templates/accounts/customer/customer_edit.html b/templates/accounts/customer/customer_edit.html index 7a45364..a764f5e 100644 --- a/templates/accounts/customer/customer_edit.html +++ b/templates/accounts/customer/customer_edit.html @@ -4,6 +4,26 @@ {% include "cdn_through_html/flatpicker_cdn_css.html" %} + {% include "cdn_through_html/filepond_cdn_css.html" %} + + {% endblock %} {% block content %} @@ -33,7 +53,7 @@
    -
    + {% csrf_token %} {% include 'includes/dynamic_template_form.html' with form=form %}
    @@ -53,12 +73,73 @@ {% block javascript %} + {% 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" %}