diff --git a/accounts/admin.py b/accounts/admin.py
index 77300ee..15b1c65 100644
--- a/accounts/admin.py
+++ b/accounts/admin.py
@@ -40,7 +40,7 @@ from .models import IAmPrincipalLocation
class IAmPrincipalLocationAdmin(admin.ModelAdmin):
- list_display = ("id", "principal", "latitude", "longitude")
+ list_display = ("id", "principal", "latitude", "longitude", "created_on", "modified_on")
search_fields = (
"principal__first_name",
"principal__last_name",
diff --git a/accounts/api/serializers.py b/accounts/api/serializers.py
index fcfff7d..5ee2ebe 100644
--- a/accounts/api/serializers.py
+++ b/accounts/api/serializers.py
@@ -6,10 +6,12 @@ from rest_framework import serializers
from accounts.models import (
AppVersion,
IAmPrincipal,
+ IAmPrincipalExtendedData,
IAmPrincipalType,
# IAmPrincipalKYCDetails,
)
-from manage_events.models import EventPrincipalInteraction, PrincipalPreference
+
+from manage_events.models import EventInteractionType, EventPrincipalInteraction, FreeUsageFeatureLimit, PrincipalPreference
from manage_referrals.models import (
ReferralCode,
ReferralRecord,
@@ -136,17 +138,15 @@ class PasswordResetSerializer(BasePasswordSerializer, serializers.ModelSerialize
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)
- 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:
model = IAmPrincipal
@@ -157,21 +157,29 @@ class ProfileSerializer(serializers.ModelSerializer):
"player_id",
"first_name",
"last_name",
+ 'business_name',
+ "phone_no",
"email",
- "invite_count",
- "register_complete",
- "has_active_subscription",
- "has_preferences",
"linkedin_profile",
"youtube_profile",
"facebook_profile",
"instagram_profile",
+ "twitter_profile",
"website",
"is_active",
- "going_events_count",
- "interested_events_count",
]
+ # def validate_phone_no(self, value):
+ # try:
+ # # Parse the phone number
+ # phone_number = parse(value)
+ # # Check for validity
+ # if not phonenumberutil.is_valid_number(phone_number):
+ # raise serializers.ValidationError('Please enter a valid phone number.')
+ # return value
+ # except NumberParseException:
+ # raise serializers.ValidationError('The phone number format is invalid.')
+
def update(self, instance, validated_data):
instance.profile_photo = validated_data.get(
"profile_photo", instance.profile_photo
@@ -180,14 +188,53 @@ class ProfileSerializer(serializers.ModelSerializer):
instance.last_name = validated_data.get("last_name", instance.last_name)
return super().update(instance, validated_data)
+ 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_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)
+ request = self.context.get("request")
+ data["profile_photo"] = self.get_image_url(instance, "profile_photo", request)
+ return data
+
+class ProfileExtendedDataSerializer(serializers.ModelSerializer):
+ invite_count = serializers.SerializerMethodField(read_only=True)
+ principal_type_name = serializers.SerializerMethodField(read_only=True)
+ has_active_subscription = serializers.SerializerMethodField(read_only=True)
+ preference = serializers.SerializerMethodField(read_only=True)
+ principal_preference_count = serializers.SerializerMethodField(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)
+
+ class Meta:
+ model = IAmPrincipal
+ fields = [
+ "principal_type_name",
+ "invite_count",
+ "register_complete",
+ "has_active_subscription",
+ "preference",
+ "principal_preference_count",
+ "going_events_count",
+ "interested_events_count",
+ "feature_limit"
+ ]
+
def get_going_events_count(self, obj):
return EventPrincipalInteraction.objects.filter(
- principal=obj, status="going"
+ principal=obj, status=EventInteractionType.GOING
).count()
def get_interested_events_count(self, obj):
return EventPrincipalInteraction.objects.filter(
- principal=obj, status="interested"
+ principal=obj, status=EventInteractionType.INTERESTED
).count()
def get_invite_count(self, obj):
@@ -198,38 +245,34 @@ class ProfileSerializer(serializers.ModelSerializer):
def get_principal_type_name(self, obj):
return obj.principal_type.name if obj.principal_type else None
- def get_has_preferences(self, obj):
+ def get_preference(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_principal_preference_count(self, obj):
+ principal_preference = PrincipalPreference.objects.filter(principal=obj).first()
+ if principal_preference:
+ categories = principal_preference.preferred_categories.all()
+ return categories.count()
+ return 0
def get_has_active_subscription(self, obj):
subscription_status = {
"has_active_subscription": False,
"in_grace_period": False,
"grace_period_end_date": None,
+ "subscription_id": 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
+ latest_subscription = PrincipalSubscription.get_principal_subscription(obj)
+
+ print(f"subscrition record {latest_subscription}")
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"] = (
@@ -245,13 +288,11 @@ class ProfileSerializer(serializers.ModelSerializer):
)
return subscription_status
-
- def to_representation(self, instance):
- data = super().to_representation(instance)
- request = self.context.get("request")
- data["profile_photo"] = self.get_image_url(instance, "profile_photo", request)
- return data
-
+
+ def get_feature_limit(self, obj):
+ from manage_events.api.serializers import FreeUsageFeatureLimitSerializer
+ obj = FreeUsageFeatureLimit.objects.first()
+ return FreeUsageFeatureLimitSerializer().to_representation(obj)
# class PrincipalKYCDetailsSerializer(serializers.ModelSerializer):
# aadhar_front_image = serializers.ImageField(required=False)
@@ -348,4 +389,10 @@ class AppVersionSerializer(serializers.ModelSerializer):
"version",
"force_upgrade",
"recommend_upgrade",
- ]
\ No newline at end of file
+ ]
+
+
+class IAmPrincipalExtendedDataSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = IAmPrincipalExtendedData
+ fields = "__all__"
diff --git a/accounts/api/urls.py b/accounts/api/urls.py
index 1428632..783612e 100644
--- a/accounts/api/urls.py
+++ b/accounts/api/urls.py
@@ -17,7 +17,8 @@ urlpatterns = [
path('request-otp/', views.OtpRequestView.as_view(), name='send_otp'),
path('verify-otp/', views.OTPVerificationView.as_view(), name='send_otp'),
- # path('profile/view//', views.ProfileView.as_view(), name='profile_veiw'),
+ path('profile/extended-data/', views.ProfileExtendedView.as_view(), name='profile_veiw'),
+
path('profile/view/', views.ProfileView.as_view(), name='profile_veiw'),
path('profile/edit/', views.ProfileView.as_view(), name='profile_edit'),
path('profile/password-reset/', views.ProfilePasswordResetView.as_view(), name='password_reset'),
@@ -38,5 +39,6 @@ urlpatterns = [
name="delete_account",
),
path('version-check/', views.VersionCheck.as_view(), name='version_check'),
+ path('transfer-check/', views.AccountTransferCheckView.as_view(), name='transfer_check'),
]
diff --git a/accounts/api/views.py b/accounts/api/views.py
index 0c7b59e..f48fa32 100644
--- a/accounts/api/views.py
+++ b/accounts/api/views.py
@@ -17,6 +17,7 @@ from django.views.decorators.http import require_http_methods
from accounts.models import (
AppVersion,
IAmPrincipal,
+ IAmPrincipalExtendedData,
IAmPrincipalOtp,
IAmPrincipalSource,
IAmPrincipalType,
@@ -41,7 +42,9 @@ from .serializers import (
# RegistrationPasswordSerializer,
# PhoneSerializer,
EmailSerializer,
+ IAmPrincipalExtendedDataSerializer,
PlayerIDSerializer,
+ ProfileExtendedDataSerializer,
RegistrationPasswordSerializer,
RegistrationSerializer,
ReferralCodeSerializer,
@@ -120,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"}
@@ -319,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"}
@@ -536,6 +539,21 @@ class ProfileView(APIView):
)
return ApiResponse.success(message=constants.SUCCESS, data=serializer.data)
+class ProfileExtendedView(APIView):
+ authentication_classes = [JWTAuthentication]
+ permission_classes = [IsAuthenticated]
+ model = IAmPrincipal
+ serializer = ProfileExtendedDataSerializer
+
+ def get(self, request, *args, **kwargs):
+ serializer = self.serializer(instance=request.user)
+ success_response = {
+ "status": status.HTTP_200_OK,
+ "message": constants.SUCCESS,
+ "data": serializer.data,
+ }
+ return ApiResponse.success(**success_response)
+
class ProfilePasswordResetView(APIView):
authentication_classes = [JWTAuthentication]
@@ -937,7 +955,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 +963,6 @@ class SoftDeletePrincipalAPIView(APIView):
)
principal.is_active = False
- principal.deleted = True
principal.save()
return ApiResponse.success(
status=status.HTTP_200_OK,
@@ -960,13 +977,75 @@ 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)
+
+
+class AccountTransferCheckView(APIView):
+ authentication_classes = [JWTAuthentication]
+ permission_classes = [IsAuthenticated]
+ model = IAmPrincipalExtendedData
+ serializer_class = IAmPrincipalExtendedDataSerializer
+
+ def get(self, request, *args, **kwargs):
+ print("request.user is ", request.user)
+ try:
+ obj = IAmPrincipalExtendedData.objects.get(principal=request.user)
+ except 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,
+ "message": constants.RECORD_NOT_FOUND,
+ "errors": str(e),
+ }
+ return ApiResponse.error(**error_response)
+
+ serializer = self.serializer_class(obj)
+ print("serializer data", serializer)
+ return ApiResponse.success(message=constants.SUCCESS, data=serializer.data)
+
+ def post(self, request, *args, **kwargs):
+ try:
+ obj = IAmPrincipalExtendedData.objects.get(principal=request.user)
+ except 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,
+ "message": constants.RECORD_NOT_FOUND,
+ "errors": str(e),
+ }
+ return ApiResponse.error(**error_response)
+
+ obj.pwd_changed_post_transfer = True
+ obj.save()
+ serializer = self.serializer_class(obj)
+ return ApiResponse.success(message=constants.SUCCESS, data=serializer.data)
\ No newline at end of file
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/forms.py b/accounts/forms.py
index d4d90ee..9f07e76 100644
--- a/accounts/forms.py
+++ b/accounts/forms.py
@@ -6,6 +6,7 @@ from django.core import validators
from django.utils.translation import gettext_lazy as _
from goodtimes import constants
+from manage_events.models import EventCategory
from . import models
# from .backend import EmailBackend
@@ -357,4 +358,92 @@ class IAmPrincipalResourceLinkForm(IAmPrincipalForm):
principal.principal_resource.set(principal_resource_data)
# Save the instance to the database
if commit:
- principal.save()
\ No newline at end of file
+ principal.save()
+
+
+class CreateCustomerForm(forms.Form):
+ 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")
+ email = forms.EmailField(required=True, label='Email')
+ phone_no = PhoneNumberField(
+ widget=forms.TextInput(),
+ label="Phone No"
+ )
+ preferences = forms.ModelMultipleChoiceField(
+ queryset=EventCategory.objects.all(),
+ widget=forms.widgets.SelectMultiple(
+ attrs={"class": "form_select js-example-basic-multiple"}
+ ),
+ required=True,
+ label='Preferences'
+ )
+ free_start_date = forms.DateField(
+ required=True,
+ label=_('Free period start date'),
+ help_text=_('Enter the start date of the free period')
+ )
+ free_end_date = forms.DateField(
+ required=True,
+ label=_('Free period end date'),
+ help_text=_('Enter the end date of the free period')
+ )
+ 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")
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ 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")
+ email = forms.EmailField(required=True, label='Email', widget=forms.TextInput(attrs={'readonly': 'readonly'}))
+ phone_no = PhoneNumberField(
+ widget=forms.TextInput(),
+ label="Phone No"
+ )
+ preferences = forms.ModelMultipleChoiceField(
+ queryset=EventCategory.objects.all(),
+ widget=forms.widgets.SelectMultiple(
+ attrs={"class": "form_select js-example-basic-multiple"}
+ ),
+ required=True,
+ label='Preferences'
+ )
+ free_start_date = forms.DateField(
+ required=True,
+ label=_('Free period start date'),
+ help_text=_('Enter the start date of the free period')
+ )
+ free_end_date = forms.DateField(
+ required=True,
+ label=_('Free period end date'),
+ help_text=_('Enter the end date of the free period')
+ )
+ 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")
+ active = forms.BooleanField(required=False, label='Active', help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",)
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields['preferences'].queryset = EventCategory.objects.all()
+
+
+class UploadExcelForm(forms.Form):
+ file = forms.FileField()
diff --git a/accounts/migrations/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/accounts/migrations/0011_alter_iamprincipallocation_principal.py b/accounts/migrations/0011_alter_iamprincipallocation_principal.py
new file mode 100644
index 0000000..885aedf
--- /dev/null
+++ b/accounts/migrations/0011_alter_iamprincipallocation_principal.py
@@ -0,0 +1,24 @@
+# Generated by Django 5.0.2 on 2024-06-20 08:17
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("accounts", "0010_alter_appversion_app_type"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="iamprincipallocation",
+ name="principal",
+ field=models.OneToOneField(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="principal_location",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ]
diff --git a/accounts/migrations/0012_iamprincipalextendeddata.py b/accounts/migrations/0012_iamprincipalextendeddata.py
new file mode 100644
index 0000000..220b1e2
--- /dev/null
+++ b/accounts/migrations/0012_iamprincipalextendeddata.py
@@ -0,0 +1,28 @@
+# Generated by Django 5.0.2 on 2024-06-25 07:28
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0011_alter_iamprincipallocation_principal'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='IAmPrincipalExtendedData',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('is_onboarded', models.BooleanField(default=False, help_text='Indicates whether the user was onboarded by an admin.')),
+ ('is_transferred', models.BooleanField(default=False, help_text='Indicates whether the account has been transferred to the user.')),
+ ('transferred_on', models.DateTimeField(blank=True, help_text='The date and time when the account was transferred to the user.', null=True)),
+ ('principal', models.OneToOneField(help_text='The principal user to which this extended data is related.', on_delete=django.db.models.deletion.CASCADE, related_name='extended_data', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'db_table': 'iam_principal_extended_data',
+ },
+ ),
+ ]
diff --git a/accounts/migrations/0013_iamprincipalextendeddata_pwd_changed_post_transfer.py b/accounts/migrations/0013_iamprincipalextendeddata_pwd_changed_post_transfer.py
new file mode 100644
index 0000000..6815936
--- /dev/null
+++ b/accounts/migrations/0013_iamprincipalextendeddata_pwd_changed_post_transfer.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.2 on 2024-06-27 08:11
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0012_iamprincipalextendeddata'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='iamprincipalextendeddata',
+ name='pwd_changed_post_transfer',
+ field=models.BooleanField(default=False, help_text='Indicates if the user changed their password after the account was transferred.'),
+ ),
+ ]
diff --git a/accounts/migrations/0014_iamprincipal_business_name.py b/accounts/migrations/0014_iamprincipal_business_name.py
new file mode 100644
index 0000000..36e5e8d
--- /dev/null
+++ b/accounts/migrations/0014_iamprincipal_business_name.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.2 on 2024-08-11 16:51
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0013_iamprincipalextendeddata_pwd_changed_post_transfer'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='iamprincipal',
+ name='business_name',
+ field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Business Name'),
+ ),
+ ]
diff --git a/accounts/migrations/0015_iamprincipal_twitter_profile.py b/accounts/migrations/0015_iamprincipal_twitter_profile.py
new file mode 100644
index 0000000..06cbd13
--- /dev/null
+++ b/accounts/migrations/0015_iamprincipal_twitter_profile.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.2 on 2024-08-12 08:07
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0014_iamprincipal_business_name'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='iamprincipal',
+ name='twitter_profile',
+ field=models.URLField(blank=True, max_length=255, null=True, verbose_name='Principal Twitter'),
+ ),
+ ]
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 c3f5508..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,
@@ -320,6 +321,8 @@ class IAmPrincipal(AbstractUser):
blank=True,
null=True,
)
+ business_name = models.CharField(verbose_name="Business Name", max_length=200, blank=True, null=True)
+ twitter_profile = models.URLField(verbose_name="Principal Twitter", max_length=255, null=True, blank=True)
USERNAME_FIELD = "email"
REQUIRED_FIELDS = []
@@ -331,8 +334,54 @@ 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",
+ on_delete=models.CASCADE,
+ help_text="The principal user to which this extended data is related."
+ )
+ is_onboarded = models.BooleanField(
+ default=False,
+ help_text="Indicates whether the user was onboarded by an admin."
+ )
+ is_transferred = models.BooleanField(
+ default=False,
+ help_text="Indicates whether the account has been transferred to the user."
+ )
+ transferred_on = models.DateTimeField(
+ null=True,
+ blank=True,
+ help_text="The date and time when the account was transferred to the user."
+ )
+ pwd_changed_post_transfer = models.BooleanField(default=False, help_text="Indicates if the user changed their password after the account was transferred.")
+ encrypted_pass = models.TextField(blank=True, null=True)
+ class Meta:
+ db_table = "iam_principal_extended_data"
+
+ def __str__(self):
+ return f"Extended Data for {self.principal}"
+
+ def save(self, *args, **kwargs):
+ if self.is_transferred and self.transferred_on is None:
+ self.transferred_on = datetime.datetime.now()
+ super().save(*args, **kwargs)
+
+ @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,
@@ -419,7 +468,7 @@ class IAmPrincipalBiometric(BaseModel):
class IAmPrincipalLocation(BaseModel):
- principal = models.ForeignKey(
+ principal = models.OneToOneField(
IAmPrincipal, related_name="principal_location", on_delete=models.CASCADE
)
latitude = models.DecimalField(max_digits=18, decimal_places=15)
@@ -432,76 +481,6 @@ class IAmPrincipalLocation(BaseModel):
return f"{self.principal.first_name}:{self.latitude}, {self.longitude}"
-# Excluded in migrations
-# class IAmPrincipalKYCDetails(models.Model):
-# # the below is the table structure from Hritik Dhanawde for KYC
-# kid =
-# status =
-# customer_identifier =
-# reference_id =
-# customer_name =
-# reference_id =
-# customer_name =
-# workflow_name =
-# template_id =
-# kyc_created_at =
-# access_token =
-
-# # Regex pattern for Aadhar number with exactly 12 digits
-# AADHAR_REGEX = r"^\d{12}$"
-# # Regex pattern for PAN number in the format AAAAB1234C
-# PAN_REGEX = r"^[A-Z]{5}[0-9]{4}[A-Z]$"
-# # Regex pattern for IFSC code (11 alphanumeric characters)
-# IFSC_REGEX = r"^[A-Za-z]{4}\d{7}$"
-
-# principal = models.OneToOneField(
-# IAmPrincipal, on_delete=models.CASCADE
-# ) # Assuming IAmPrincipal is the user model.
-# aadhar_front_image = models.ImageField(upload_to="kyc/", blank=True, null=True)
-# aadhar_back_image = models.ImageField(upload_to="kyc/", blank=True, null=True)
-# aadhar_number = models.CharField(
-# max_length=12,
-# blank=True,
-# null=True,
-# validators=[
-# RegexValidator(AADHAR_REGEX, message="Aadhar number must be 12 digits.")
-# ],
-# )
-# pan_image = models.ImageField(upload_to="kyc/", blank=True, null=True)
-# pan_number = models.CharField(
-# max_length=10,
-# blank=True,
-# null=True,
-# validators=[
-# RegexValidator(
-# PAN_REGEX, message="PAN number must be in the format AAAAB1234C."
-# )
-# ],
-# )
-# is_aadhar_verified = models.BooleanField(default=False)
-# is_pan_verified = models.BooleanField(default=False)
-# account_no = models.CharField(max_length=20, blank=True, null=True)
-# bank_name = models.CharField(max_length=100, blank=True, null=True)
-# branch_name = models.CharField(max_length=100, blank=True, null=True)
-# ifsc_code = models.CharField(
-# max_length=11,
-# blank=True,
-# null=True,
-# validators=[
-# RegexValidator(
-# IFSC_REGEX, message="IFSC code must be 11 alphanumeric characters."
-# )
-# ],
-# )
-
-# class Meta:
-# db_table = "iam_principal_kyc_details"
-
-
-# def __str__(self):
-# return f"KYC Information for {self.principal.email}"
-
-
class AppType(models.TextChoices):
ANDROID = "android", "android"
IOS = "ios", "ios"
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/accounts/urls.py b/accounts/urls.py
index d9dc83a..369f3be 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"),
@@ -42,7 +42,15 @@ 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"),
+ path('customer/transfer//', views.CustomerTransferView.as_view(), name="customer_transfer"),
+ path('customer/check-email/', views.CustomerCheckEmail.as_view(), name="customer_check_email"),
+ path('customer/download-excel-template/', views.export_excel_template, name='download_excel_template'),
+ path('customer/import-customer-data/', views.CustomerImportView.as_view(), name='import_customer_data'),
+ path('customer/export-customer-data/', views.CustomerExportView.as_view(), name='export_customer_data'),
# ignore this to path this for example setup
path('principal/example/', TemplateView.as_view(template_name="accounts/iam_module/example_form.html"), name="example_add"),
diff --git a/accounts/views.py b/accounts/views.py
index 6f5c89d..c4052f2 100644
--- a/accounts/views.py
+++ b/accounts/views.py
@@ -1,10 +1,12 @@
import logging
+from django.conf import settings
from django.db.models import Count, Q
from django.contrib import messages
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.views import LogoutView
from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.mixins import LoginRequiredMixin
+from django.contrib.auth.hashers import make_password
from django.contrib.auth.views import (
LoginView,
PasswordResetCompleteView,
@@ -13,16 +15,26 @@ from django.contrib.auth.views import (
PasswordResetView,
)
from django.core.exceptions import ValidationError
+from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse_lazy
from django.views import generic
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, Encryptor
+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, timedelta
from . import resource_action
from .forms import (
+ CreateCustomerForm,
CustomAuthenticationForm,
IAmPrincipalForm,
IAmPrincipalGroupRoleLinkForm,
@@ -30,9 +42,12 @@ from .forms import (
IAmPrincipalRoleAppResourceActionLinkForm,
IAmPrincipalGroupLinkForm,
ProfileEditForm,
+ UpdateCustomerForm,
+ UploadExcelForm,
)
from .models import (
IAmPrincipal,
+ IAmPrincipalExtendedData,
IAmPrincipalType,
IAmAppResourceActionLink,
IAmPrincipalGroup,
@@ -171,10 +186,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
@@ -557,6 +568,266 @@ class AppRoleDeleteView(LoginRequiredMixin, generic.View):
"""Customer"""
+class CustomerCheckEmail(generic.View):
+ model = IAmPrincipal
+
+ def post(self, request, *args, **kwargs):
+ email = request.POST.get('email')
+ print("check email is cllaed ", email)
+ if self.model.objects.filter(email=email).exists():
+ print("exist called")
+ return JsonResponse({'message': 'This email address is already in use.'}, status=400)
+ else:
+ print("email is valid")
+ return JsonResponse({'message': 'Email is available.'}, status=200)
+
+
+class CustomerCreateView(LoginRequiredMixin, generic.View):
+ page_name = resource_action.RESOURCE_MANAGE_CUSTOMER
+ resource = resource_action.RESOURCE_MANAGE_CUSTOMER
+ model = IAmPrincipal
+ form_class = CreateCustomerForm
+ template_name = "accounts/customer/customer_add.html"
+ success_url = reverse_lazy("accounts:customer_list")
+ success_message = "Saved Successfully"
+ error_message = "An error occurred while saving the data."
+
+ def get_context_data(self, **kwargs):
+ context = {
+ "page_name": self.page_name,
+ "operation": "Add",
+ }
+ context.update(kwargs) # Include any additional context data passed to the view
+ return context
+
+ def get(self, request, *args, **kwargs):
+ form = self.form_class()
+ context = self.get_context_data(form=form)
+ return render(request, self.template_name, context=context)
+
+ def post(self, request, *args, **kwargs):
+ print(request.POST)
+ # return redirect(self.success_url)
+ form = self.form_class(request.POST, request.FILES)
+ context = self.get_context_data(form=form)
+ if not form.is_valid():
+ return render(request, self.template_name, context=context)
+
+ free_subscription = Subscription.objects.filter(is_free=True, active=True).first()
+
+ if not free_subscription:
+ messages.error(self.request, "Create a free subscription record for admin in manage subscription")
+ return render(request, self.template_name, context=context)
+
+ try:
+ with transaction.atomic():
+ random_password = IAmPrincipal.generate_random_password()
+
+ # Encrypt the password
+ encryptor = Encryptor()
+ encrypted_password = encryptor.encrypt(random_password)
+
+ # save principal data
+ principal_obj = IAmPrincipal.objects.create(
+ profile_photo = form.cleaned_data.get("profile_photo"),
+ email=form.cleaned_data.get('email'),
+ first_name=form.cleaned_data.get('first_name'),
+ last_name=form.cleaned_data.get('last_name'),
+ business_name=form.cleaned_data.get('business_name'),
+ phone_no=form.cleaned_data.get('phone_no'),
+ password=make_password(random_password),
+ username=form.cleaned_data.get("email"),
+ email_verified=True,
+ register_complete=True,
+ principal_type=IAmPrincipalType.objects.get(name=resource_action.PRINCIPAL_TYPE_EVENT_MANAGER),
+ address_line1=form.cleaned_data.get("address_line1"),
+ city=form.cleaned_data.get("city"),
+ country=form.cleaned_data.get("country"),
+ website=form.cleaned_data.get("website"),
+ linkedin_profile=form.cleaned_data.get("linkedin_profile"),
+ facebook_profile=form.cleaned_data.get("facebook_profile"),
+ instagram_profile=form.cleaned_data.get("instagram_profile"),
+ twitter_profile=form.cleaned_data.get("twitter_profile"),
+ )
+
+ # generate referralcode of manager
+ ReferralCode.create_referral_code_for_user_manager(
+ principal=principal_obj, principal_type=principal_obj.principal_type
+ )
+
+ IAmPrincipalExtendedData.objects.create(
+ principal=principal_obj,
+ is_onboarded=True,
+ encrypted_pass=encrypted_password, # Save encrypted password
+ )
+
+ # save principal preferences record
+ principal_preference = PrincipalPreference.objects.create(principal=principal_obj)
+ principal_preference.preferred_categories.set(form.cleaned_data.get("preferences"))
+
+ principal_subscription = PrincipalSubscription.objects.create(
+ start_date=form.cleaned_data.get("free_start_date"),
+ end_date=form.cleaned_data.get("free_end_date"),
+ principal=principal_obj,
+ grace_period_end_date=PrincipalSubscription.generate_grace_period_end_date(form.cleaned_data.get("free_end_date")),
+ is_paid=True,
+ subscription=free_subscription
+ )
+ messages.success(self.request, constants.REGISTRATION_SUCCESS)
+ return redirect(self.success_url)
+ except Exception as e:
+ print("errror is ", e)
+ messages.error(self.request, str(e))
+ context = self.get_context_data(form=form)
+ return render(request, self.template_name, context=context)
+
+
+class CustomerUpdateView(LoginRequiredMixin, generic.View):
+ page_name = resource_action.RESOURCE_MANAGE_CUSTOMER
+ resource = resource_action.RESOURCE_MANAGE_CUSTOMER
+ model = IAmPrincipal
+ form_class = UpdateCustomerForm
+ template_name = "accounts/customer/customer_edit.html"
+ success_url = reverse_lazy("accounts:customer_list")
+ success_message = "Updated Successfully"
+ error_message = "An error occurred while saving the data."
+
+ def get_context_data(self, **kwargs):
+ context = {
+ "page_name": self.page_name,
+ "operation": "Edit",
+ }
+ context.update(kwargs) # Include any additional context data passed to the view
+ return context
+
+ def get(self, request, *args, **kwargs):
+ principal_id = kwargs.get("pk")
+ 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 = {
+ "profile_photo": principal_obj.profile_photo,
+ "first_name": principal_obj.first_name,
+ "last_name": principal_obj.last_name,
+ "email": principal_obj.email,
+ "business_name": principal_obj.business_name,
+ "phone_no": principal_obj.phone_no,
+ "address_line1": principal_obj.address_line1,
+ "city": principal_obj.city,
+ "country": principal_obj.country,
+ "website": principal_obj.website,
+ "linkedin_profile": principal_obj.linkedin_profile,
+ "facebook_profile": principal_obj.facebook_profile,
+ "instagram_profile": principal_obj.instagram_profile,
+ "twitter_profile": principal_obj.twitter_profile,
+ "active": principal_obj.is_active
+ }
+
+ try:
+ principal_preference = PrincipalPreference.objects.get(principal=principal_obj)
+ initial_data["preferences"] = list(principal_preference.preferred_categories.all().values_list("id", flat=True))
+ except PrincipalPreference.DoesNotExist:
+ initial_data["preferences"] = []
+
+ try:
+ subscription = PrincipalSubscription.objects.filter(principal=principal_obj).latest("created_on")
+ initial_data["free_start_date"] = subscription.start_date
+ initial_data["free_end_date"] = subscription.end_date
+ except PrincipalSubscription.DoesNotExist:
+ initial_data["free_start_date"] = None
+ initial_data["free_end_date"] = None
+
+ form = self.form_class(initial=initial_data)
+ context = self.get_context_data(form=form, principal_obj=principal_obj)
+ print("context dta is ", context)
+ return render(request, self.template_name, context=context)
+
+ def post(self, request, *args, **kwargs):
+ 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 customer id {principal_id} is found")
+ return redirect(self.success_url)
+ 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")
+ principal_obj.phone_no = form.cleaned_data.get("phone_no")
+ principal_obj.address_line1 = form.cleaned_data.get("address_line1")
+ principal_obj.city = form.cleaned_data.get("city")
+ principal_obj.country = form.cleaned_data.get("country")
+ principal_obj.website = form.cleaned_data.get("website")
+ principal_obj.linkedin_profile = form.cleaned_data.get("linkedin_profile")
+ 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()
+
+ # 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") + timedelta(days=15)
+ principal_subscription.save()
+ else:
+ PrincipalSubscription.objects.create(
+ principal=principal_obj,
+ start_date=form.cleaned_data.get("free_start_date"),
+ end_date=form.cleaned_data.get("free_end_date"),
+ grace_period_end_date=PrincipalSubscription.generate_grace_period_end_date(form.cleaned_data.get("free_end_date")),
+ is_paid=True,
+ subscription=Subscription.objects.filter().first() # Assuming you want to link a default subscription
+ )
+
+ messages.success(self.request, self.success_message)
+ return redirect(self.success_url)
+ except Exception as e:
+ messages.error(self.request, str(e))
+ context = self.get_context_data(form=form)
+ return render(request, self.template_name, context=context)
+
+class CustomerDetailView(LoginRequiredMixin, generic.DetailView):
+ 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:
+ principal_preference = PrincipalPreference.objects.get(principal_id=principal_obj.id)
+ except Exception as e:
+ principal_preference = None
+ principal_subscription = PrincipalSubscription.objects.filter(principal=principal_obj).order_by("-start_date").first()
+ 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
@@ -570,7 +841,7 @@ class CustomerListView(LoginRequiredMixin, generic.ListView):
queryset = (
super()
.get_queryset()
- .select_related("principal_type", "principal_source")
+ .select_related("principal_type", "principal_source", "extended_data")
.filter(
models.Q(
principal_type__name=resource_action.PRINCIPAL_TYPE_EVENT_MANAGER
@@ -601,12 +872,459 @@ 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
+from openpyxl.worksheet.datavalidation import DataValidation
+from openpyxl.styles import Font
+from django.http import HttpResponse
+
+# def export_excel_template(request):
+# # Define the columns and create an empty DataFrame
+# columns = ['First Name', 'Last Name', 'Email', 'Preferences', 'Free period start date', 'Free period end date']
+# df = pd.DataFrame(columns=columns)
+
+# # Create a workbook and select the active worksheet
+# wb = Workbook()
+# ws = wb.active
+# ws.title = 'Customer Registration'
+
+# # # Write the column headers
+# # for col_num, column_title in enumerate(df.columns, 1):
+# # cell = ws.cell(row=1, column=col_num, value=column_title)
+# # cell.font = Font(bold=True)
+
+# # # Create a hidden sheet for preferences
+# # ws_prefs = wb.create_sheet(title="Preferences")
+# # ws_prefs.sheet_state = 'hidden'
+
+# # # Fetch preferences options from the EventCategory model
+# # preferences_options = EventCategory.objects.values_list('title', flat=True)
+
+# # # Write preferences to the hidden sheet
+# # for row_num, preference in enumerate(preferences_options, 1):
+# # ws_prefs.cell(row=row_num, column=1, value=preference)
+
+# # # Define the range for preferences in the hidden sheet
+# # preferences_range = f"Preferences!$A$1:$A${len(preferences_options)}"
+
+# # # Add Data Validation for preferences (drop-down list)
+# # dv_preferences = DataValidation(
+# # type="list",
+# # formula1=preferences_range,
+# # allow_blank=True,
+# # showDropDown=True
+# # )
+# # ws.add_data_validation(dv_preferences)
+# # dv_preferences.add(f'D2:D1048576') # Apply to the whole column
+
+# # # Add Data Validation for date comparison
+# # dv_start_date = DataValidation(
+# # type="date",
+# # operator="greaterThan",
+# # formula1='"1900-01-01"',
+# # showErrorMessage=True
+# # )
+# # dv_end_date = DataValidation(
+# # type="custom",
+# # formula1="=AND(ISNUMBER(F2), F2>E2)",
+# # showErrorMessage=True,
+# # errorTitle="Invalid Date",
+# # error="End date must be greater than start date."
+# # )
+# # ws.add_data_validation(dv_start_date)
+# # ws.add_data_validation(dv_end_date)
+# # dv_start_date.add(f'E2:E1048576')
+# # dv_end_date.add(f'F2:F1048576')
+
+# # Save the workbook to a bytes buffer
+# response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
+# response['Content-Disposition'] = 'attachment; filename=customer_registration_template.xlsx'
+# wb.save(response)
+# return response
+
+# from openpyxl.styles import Font
+def export_excel_template(request):
+ # Define the columns for the Customer Registration sheet
+ columns = [
+ 'First Name',
+ 'Last Name',
+ 'Business Name',
+ 'Email',
+ 'Phone No (+919999999999)',
+ 'Preferences (should be separated by comma)',
+ 'Free period start date (DD-MM-YYYY)',
+ 'Free period end date (DD-MM-YYYY)',
+ 'Address',
+ 'Region',
+ 'Country',
+ 'Website',
+ 'LinkedIn',
+ 'Facebook',
+ 'Instagram',
+ 'Twitter',
+ ]
+ df = pd.DataFrame(columns=columns)
+
+ # Create a workbook and add the Customer Registration worksheet
+ wb = Workbook()
+ ws_customer = wb.active
+ ws_customer.title = 'Manager Onboarding'
+
+ # Write the column headers for the Customer Registration sheet
+ for col_num, column_title in enumerate(df.columns, 1):
+ cell = ws_customer.cell(row=1, column=col_num, value=column_title)
+ cell.font = Font(bold=True)
+
+ # Create the Readme worksheet
+ ws_readme = wb.create_sheet(title='Readme')
+
+ # Add information about each field to the Readme sheet
+ readme_data = [
+ ['Field Name', 'Description'],
+ ['First Name', 'The first name of the customer. This is a required field.'],
+ ['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 (+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 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.'],
+ ['Website', 'The URL of the business\'s official website. Ensure it includes "http://" or "https://".'],
+ ['LinkedIn', 'The LinkedIn profile URL of the business. Ensure it includes "http://" or "https://".'],
+ ['Facebook', 'The Facebook page URL of the business. Ensure it includes "http://" or "https://".'],
+ ['Instagram', 'The Instagram profile URL of the business. Ensure it includes "http://" or "https://".'],
+ ['Twitter', 'The Twitter handle or profile URL of the business. Ensure it includes "http://" or "https://".'],
+ ]
+
+ # Write the Readme data to the Readme worksheet
+ for row_num, row_data in enumerate(readme_data, 1):
+ for col_num, cell_value in enumerate(row_data, 1):
+ cell = ws_readme.cell(row=row_num, column=col_num, value=cell_value)
+ if row_num == 1: # Make the header bold
+ cell.font = Font(bold=True)
+
+ # Save the workbook to a bytes buffer
+ response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
+ response['Content-Disposition'] = 'attachment; filename=customer_registration_template.xlsx'
+ wb.save(response)
+
+ return response
+
+class CustomerTransferView(LoginRequiredMixin, generic.View):
+ model = IAmPrincipal
+
+ def get(self, request, *args, **kwargs):
+ try:
+ principal_obj = self.model.objects.get(pk=kwargs.get("pk"))
+ except self.model.DoesNotExist:
+ messages.error(request, "Something went wrong")
+ return redirect(reverse_lazy("accounts:customer_detail"))
+
+ email_service = EmailService(
+ subject="Your Exclusive Account Access Details with Good Times!",
+ to=principal_obj.email,
+ from_email=settings.DEFAULT_FROM_EMAIL,
+ )
+
+ # Send the email
+ try:
+ 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()
+ messages.success(request, "Account Transfer mail send successfully")
+ except Exception as e:
+ messages.error(request, f"{str(e)}")
+
+ return redirect(reverse_lazy("accounts:customer_detail", kwargs={"pk": kwargs.get("pk")}))
+
+
+class CustomerImportView(LoginRequiredMixin, generic.View):
+ page_name = resource_action.RESOURCE_MANAGE_CUSTOMER
+ resource = resource_action.RESOURCE_MANAGE_CUSTOMER
+ action = resource_action.ACTION_READ
+ template_name = "accounts/customer/customer_bulk_template.html"
+ form_class = UploadExcelForm
+
+ def get_context_data(self, **kwargs):
+ context = {
+ "page_name": self.page_name,
+ }
+ context.update(kwargs) # Include any additional context data passed to the view
+ return context
+
+ def 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)
+ return render(request, self.template_name, context=context)
+
+ def post(self, request, *args, **kwargs):
+ form = self.form_class(request.POST, request.FILES)
+ context = self.get_context_data(form=form)
+ if not form.is_valid():
+ print(form.errors)
+ return render(request, self.template_name, context=context)
+
+ excel_file = request.FILES['file']
+
+ wb = load_workbook(filename=excel_file)
+
+ # Check if the specific sheet exists
+ if 'Manager Onboarding' not in wb.sheetnames:
+ form.add_error('file', 'The required sheet "Manager Onboarding" is not present in the uploaded file.')
+ return render(request, self.template_name, context=context)
+
+ # Load the "Manager Onboarding" worksheet
+ ws = wb['Manager Onboarding']
+
+ error_log = []
+
+ principals = []
+ preferences_list = []
+ subscriptions = []
+ principal_type = IAmPrincipalType.objects.get(name=resource_action.PRINCIPAL_TYPE_EVENT_MANAGER)
+ free_subscription = Subscription.objects.filter(is_free=True, active=True).first()
+
+ if not free_subscription:
+ messages.error(self.request, "Create a free subscription record for admin in manage subscription")
+ return render(request, self.template_name, context=context)
+
+ for idx, row in enumerate(ws.iter_rows(min_row=2, values_only=True), start=2):
+ first_name, last_name, business_name, email, phone_no, preferences, start_date, end_date, address, region, country, website, linkedin, facebook, instagram, twitter = row
+ print(f"{first_name}, {last_name, email, preferences, start_date, end_date}")
+
+ # validate all data
+ if not first_name or not last_name or not email or not preferences or not start_date or not end_date:
+ error_log.append(f"Row {idx}: Missing data.")
+ continue
+
+ # validate email existence
+ if IAmPrincipal.objects.filter(email=email).exists():
+ error_log.append(f"Row {idx}: Email {email} already exists.")
+ continue
+
+ # Validate 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)
+ if len(event_categories) != len(preference_list):
+ 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(random_password),
+ username=email.strip(),
+ email_verified=True,
+ register_complete=True,
+ principal_type=principal_type,
+ business_name=business_name,
+ phone_no=phone_no,
+ address_line1=address,
+ city=region,
+ country=country,
+ website=website,
+ linkedin_profile=linkedin,
+ facebook_profile=facebook,
+ instagram_profile=instagram,
+ twitter_profile=twitter
+ )
+ principals.append(principal)
+
+ # Collect preferences to be set later
+ preferences_list.append((principal, event_categories, start_date, end_date))
+
+ if error_log:
+ context = self.get_context_data(form=form, error_log=error_log)
+ messages.error(request, "No record 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
+ 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])
+
+ # Create subscriptions and preferences
+ for principal, event_categories, start_date, end_date in preferences_list:
+ principal = principals.get(email=principal.email)
+
+ # Generate referral code for the manager
+ ReferralCode.create_referral_code_for_user_manager(principal=principal, principal_type=principal_type)
+
+ # Create IAmPrincipalExtendedData record
+ IAmPrincipalExtendedData.objects.create(principal=principal, is_onboarded=True,encrypted_pass=encrypted_password,)
+
+ # Create PrincipalSubscription record
+ subscription = PrincipalSubscription(
+ principal=principal,
+ start_date=start_date,
+ end_date=end_date,
+ grace_period_end_date=PrincipalSubscription.generate_grace_period_end_date(end_date),
+ is_paid=True,
+ subscription=free_subscription
+ )
+ subscriptions.append(subscription)
+
+ # Create PrincipalPreferences record
+ preference = PrincipalPreference(principal=principal)
+ preference.save()
+ preference.preferred_categories.set(event_categories)
+
+ # Bulk create subscriptions
+ PrincipalSubscription.objects.bulk_create(subscriptions)
+
+ messages.success(request, "Data imported successfully")
+ return render(request, self.template_name, context=context)
+
+
+class CustomerExportView(LoginRequiredMixin, generic.View):
+ model = IAmPrincipal
+
+ def get(self, request, *args, **kwargs):
+ princiapls = IAmPrincipal.objects.select_related("extended_data").filter(principal_type__name=resource_action.PRINCIPAL_TYPE_EVENT_MANAGER)
+
+ # prepare data for excel file
+ data = []
+ for principal in princiapls:
+ data.append([
+ principal.email,
+ principal.first_name,
+ principal.last_name,
+ str(principal.phone_no) 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'
+ ])
+
+ # Define the columns for the Excel file
+ columns = ["Email", "First Name", "Last Name", "Phone Number", "Email Verified", "Active"]
+
+ # Create a workbook and select the active worksheet
+ wb = Workbook()
+ ws = wb.active
+ ws.title = "Event Managers List"
+
+ # Write the column headers
+ for col_num, column_title in enumerate(columns, 1):
+ cell = ws.cell(row=1, column=col_num, value=column_title)
+ cell.font = Font(bold=True)
+
+ # write the data rows
+
+ for row_num, row_data in enumerate(data, 2):
+ for col_num, cell_value in enumerate(row_data, 1):
+ ws.cell(row=row_num, column=col_num, value=cell_value)
+
+ response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
+ response['Content-Disposition'] = 'attachment; filename=event_managers.xlsx'
+ wb.save(response)
+
+ return response
class DatatableListView(LoginRequiredMixin, generic.ListView):
pass
-
class PrincipalProfileView(LoginRequiredMixin, generic.ListView):
page_name = resource_action.RESOURCE_MANAGE_DASHBOARD
model = IAmPrincipal
diff --git a/chat/consumers.py b/chat/consumers.py
index 9bf2a3f..a38567a 100644
--- a/chat/consumers.py
+++ b/chat/consumers.py
@@ -1,18 +1,10 @@
-from channels.generic.websocket import AsyncWebsocketConsumer, WebsocketConsumer
+from channels.generic.websocket import AsyncWebsocketConsumer
import json
-import django
-django.setup()
-
-from accounts.models import IAmPrincipal
from channels.exceptions import StopConsumer
-from asgiref.sync import async_to_sync, sync_to_async
from django.utils import timezone
from chat.models import ChatGroup, ChatMessage
from channels.db import database_sync_to_async
-from django.db import close_old_connections
-from rest_framework_simplejwt.tokens import AccessToken
from rest_framework_simplejwt.authentication import JWTAuthentication
-import threading
class ChatConsumer(AsyncWebsocketConsumer):
@@ -26,15 +18,6 @@ class ChatConsumer(AsyncWebsocketConsumer):
print("token_key: ", token_key)
self.user = await self.get_user_async(token_key)
print("self.user: ", self.user)
- # Start the thread to get the user object
- # user_thread = threading.Thread(target=self.get_user_async, args=(token_key,))
- # user_thread.start()
-
- # # Wait for the thread to finish and assign the user object to the scope
- # user_thread.join()
- # self.scope["user"] = self.user
- # print("User: ", self.scope["user"])
- # print("self.user: ", self.user)
# Join room group
await self.channel_layer.group_add(self.room_name, self.channel_name)
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/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/mixins.py b/goodtimes/mixins.py
new file mode 100644
index 0000000..af0d2ce
--- /dev/null
+++ b/goodtimes/mixins.py
@@ -0,0 +1,32 @@
+from django.views import generic
+from goodtimes.utils import JsonResponseUtil
+
+
+class ActionMixin(generic.View):
+ model = None
+
+ def post(self, request, *args, **kwargs):
+
+ if self.model is None:
+ raise NotImplementedError("Subclasses of BaseActionView must define a 'model' attribute.")
+
+ action = request.POST.get('action') # 'archive', 'active', or 'unarchive'
+ ids = request.POST.getlist('ids[]') # List of IDs to perform action on
+ active = request.POST.get('active')
+ print(f"arhive action {action} and id is {ids} and active data is {active}")
+ if action == 'archive':
+ # Update 'deleted' field to True for the selected users
+ self.model.objects.filter(id__in=ids).update(deleted=True, active=False)
+ message = 'Record archived successfully.'
+ elif action == 'active':
+ # Update 'active' field to True for the selected users
+ self.model.objects.filter(id__in=ids).update(active=active.capitalize())
+ message = 'Record updated successfully.'
+ elif action == 'unarchive':
+ # Update 'deleted' field to False for the selected users
+ self.model.objects.filter(id__in=ids).update(deleted=False)
+ message = 'Record unarchived successfully.'
+ else:
+ return JsonResponseUtil.error(message="Invalid Action")
+
+ return JsonResponseUtil.success(message=message)
\ No newline at end of file
diff --git a/goodtimes/services.py b/goodtimes/services.py
index 0468a27..792e22c 100644
--- a/goodtimes/services.py
+++ b/goodtimes/services.py
@@ -1,4 +1,9 @@
import random
+import requests
+import googlemaps
+import stripe
+import stripe.error
+import tweepy
from django.conf import settings
from django.core.files.uploadedfile import UploadedFile
from django.core.mail import EmailMessage
@@ -6,14 +11,11 @@ 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,
- ReferralRecord,
- ReferralRecordReward,
- ReferralTracking,
-)
+
from manage_subscriptions.models import PrincipalSubscription, Subscription
from manage_wallets.models import (
TransactionStatus,
@@ -147,7 +149,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
@@ -201,195 +205,6 @@ class SMSService:
# self.send(phone_numbers, body)
return otp_code
-
-class PaymentProcessingService:
- def __init__(self, webhook_data):
- self.webhook_data = webhook_data
- self.event_type = webhook_data["type"]
- self.charge_data = webhook_data["data"]["object"]
- self.customer_id = self._get_customer_id()
- self.transaction = self._get_transaction_by_id()
- self.principal = self.transaction.principal
- self.principal_subscription = None
-
- def _get_customer_id(self):
- # Access the customer ID from the charge object
- return self.charge_data.get("customer", None)
-
- def _get_transaction_by_id(self):
- logger.debug("self.metadata: ", self.charge_data["metadata"])
- logger.debug("transaction_id: ", self.charge_data["metadata"]["transaction_id"])
- transaction_id = self.charge_data["metadata"]["transaction_id"]
- if transaction_id:
- try:
- logger.debug("_get_transaction_by_id: ", transaction_id)
- return Transaction.objects.get(id=int(transaction_id))
- except Transaction.DoesNotExist:
- logger.error(f"Transaction ID {transaction_id} not found.")
- return None
-
- def _get_subscription(self):
- logger.debug(
- "subscription_id: ", self.charge_data["metadata"]["subscription_id"]
- )
- subscription_id = self.charge_data["metadata"]["subscription_id"]
- if subscription_id:
- try:
- return Subscription.objects.get(id=int(subscription_id))
- except Subscription.DoesNotExist:
- logger.error(f"Subscription ID {subscription_id} not found.")
- return None
-
- def _create_principal_subscription(self):
- order_id = self.charge_data["metadata"]["order_id"]
- try:
- subscription = self._get_subscription()
-
- subscription_days = subscription.plan.days
- today = timezone.now().date()
- last_date = today + timedelta(days=int(subscription_days))
-
- principal_subscription = PrincipalSubscription.objects.create(
- principal=self.principal,
- subscription=subscription,
- is_paid=True,
- order_id=order_id,
- start_date=today,
- end_date=last_date,
- grace_period_end_date=last_date + timedelta(days=15),
- )
- self.principal_subscription = principal_subscription
- return principal_subscription
- except Subscription.DoesNotExist:
- logger.error(
- "SOmething Went Wrong inside _create_principal_subscription()."
- )
-
- return None
-
- def process_event(self):
- if self.event_type == "checkout.session.completed":
- self._handle_success()
- else:
- self._handle_failure()
-
- def _handle_success(self):
- with transaction.atomic():
- self._create_principal_subscription()
- self._update_transaction_success()
- self._credit_referral_reward_if_applicable()
-
- def _credit_referral_reward_if_applicable(self):
- # Step 1: Check for an existing, completed referral record
- referral_record = ReferralRecord.objects.filter(
- active=True,
- deleted=False,
- referred_principal_id=self.principal.id,
- is_completed=True,
- ).first()
-
- if referral_record:
- # Step 2: Check for an active subscription of the referrer
- today = timezone.now().date()
- active_subscription = (
- PrincipalSubscription.objects.filter(
- principal=referral_record.referrer_principal,
- is_paid=True,
- end_date__gte=today,
- cancelled=False,
- deleted=False,
- )
- .order_by("-end_date")
- .first()
- )
- if active_subscription:
- subscription = self._get_subscription()
- if subscription:
- # Calculate the reward value
- percentage = (
- subscription.referral_percentage * subscription.amount / 100
- )
-
- # Create a reward entry
- ReferralRecordReward.objects.create(
- referral_record=referral_record,
- subscription=subscription,
- coins=1, # Assuming this is a default or a calculated value
- value=percentage,
- )
-
- self._credit_good_time_coin(
- referral_record.referrer_principal, percentage
- )
- # Here's where you call _update_reward
- self._update_reward(
- referral_record=referral_record,
- active_subscription=active_subscription,
- create_subscription_method=self.principal_subscription,
- has_active_subscription=True,
- )
- else:
- # If there is no active subscription, still need to update reward without active_subscription
- self._update_reward(
- referral_record=referral_record,
- active_subscription=None,
- create_subscription_method=self.principal_subscription,
- has_active_subscription=False,
- )
-
- def _credit_good_time_coin(self, referrer_principal, percentage):
- # wallet, created = Wallet.objects.get_or_create(principal=referrer_principal)
- # wallet.coins += 1
- # wallet.save()
- Transaction.objects.create(
- principal=referrer_principal,
- transaction_type=TransactionType.CREDIT,
- payment_method="",
- transaction_status=TransactionStatus.SUCCESS,
- amount=percentage,
- coins=1,
- comment="Referral reward",
- # Populate other fields as necessary, such as `order_id`, `product_id`, or `reference_id` if applicable
- )
-
- def _handle_failure(self):
- # Implement any necessary logic to handle a failed payment
- self._update_transaction_failure()
-
- def _update_reward(
- self,
- referral_record,
- active_subscription,
- create_subscription_method,
- has_active_subscription,
- ):
- # Check if the referrer has an active subscription and get its ID if it exists
- referrer_subscription_id = (
- active_subscription.id if active_subscription else None
- )
-
- # Create a new subscription for the referred principal
- referred_subscription_id = self.principal_subscription.id
-
- # Create or update the ReferralTracking record
- ReferralTracking.objects.create(
- referral_record=referral_record,
- referrer_subscription_id=referrer_subscription_id,
- referred_subscription_id=referred_subscription_id,
- is_referrer_subscribed=has_active_subscription,
- )
-
- def _update_transaction_success(self):
- principal_subscription = self.principal_subscription
- self.transaction.transaction_status = TransactionStatus.SUCCESS
- self.transaction.principal_subscription = principal_subscription
- self.transaction.save()
-
- def _update_transaction_failure(self):
- self.transaction.transaction_status = TransactionStatus.FAIL
- self.transaction.save()
-
-
class InteractionCalculator:
def __init__(self, event):
self.event = event
@@ -436,19 +251,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 +276,80 @@ 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()
+ 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
- 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
@@ -735,3 +497,648 @@ 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.
+ """
+ origins = [(latitude, 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 not destinations_and_events:
+ return queryset
+
+ # Batch size for Google Distance Matrix API (max 25 elements)
+ batch_size = 25
+ distances = {}
+
+ # 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]
+
+ # 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())
+
+ # 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)])
+
+ # Order the queryset based on the preserved order
+ 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.client, self.api = self._setup_api()
+
+ def _setup_api(self):
+ 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_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
+
+
+class TwitterPoster:
+ def __init__(self, twitter_api):
+ self.twitter_api = twitter_api
+
+ def post_text_tweet(self, caption):
+ try:
+ tweet = self.twitter_api.post_text_tweet(caption)
+ return {'success': True, 'message': 'Tweet posted successfully!'}
+ 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.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.access_token:
+ print("Page access token not obtained. Call authenticate() first.")
+ return False
+ try:
+ url = f"https://graph.facebook.com/{self.graph_api_version}/{self.page_id}/photos"
+ params = {
+ "message": caption,
+ "url": image_url,
+ "access_token": self.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 Exception 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):
+ result = self.facebook_api.post_photo(image_url, caption)
+ if not result:
+ return {'success': False, 'message': 'Error posting photo in Facebook'}
+ 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 Exception 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):
+ 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}")
+
+ 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 Exception 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 in Instagram.'}
+ return {'success': True, 'message': 'Photo posted successfully'}
+
+
+class StripeService:
+ stripe.api_key = settings.STRIPE_SECRET_KEY
+
+ @staticmethod
+ def create_product(name: str, description: str = None, metadata: dict = None):
+ """
+ Create a Stripe Product.
+
+ :param name: Name of the product, meant to be displayable to the customer.
+ :param description: An optional description of the product.
+ :param metadata: An optional dictionary of key-value pairs to attach to the product.
+ :return: The created Stripe product object.
+
+ See: https://docs.stripe.com/api/products/create?lang=python
+ """
+ try:
+ product = stripe.Product.create(name=name, description=description, metadata=metadata)
+ return {'success': True, 'data': product}
+ except stripe.error.StripeError as e:
+ return {'success': False, 'message': f"Error creating product: {e}"}
+
+ @staticmethod
+ def retrive_product(product_id: str):
+ """
+ Retrieve a Stripe Product by its ID.
+
+ :param product_id: The ID of the product to retrieve.
+ :return: The retrieved Stripe Product object.
+
+ See: https://docs.stripe.com/api/products/update?lang=python
+ """
+ try:
+ product = stripe.Product.retrieve(product_id)
+ return {'success': True, 'data': product}
+ except stripe.error.StripeError as e:
+ return {'success': False, 'message': f"Error retriving product: {e}"}
+
+ @staticmethod
+ def update_product(product_id: str, **kwargs):
+ """
+ Update a Stripe Product by its ID.
+
+ :param product_id: The ID of the product to update.
+ :param kwargs: Optional paramters to update the product, such as:
+ - name : The new name of the product.
+ - description : The new description of the product.
+ - active : A boolean flag indicating if the product is active.
+ - metadata : A dictionary of key-value pairs to attach to the product.
+ :return: The updated Stripe Product object.
+
+ See: https://docs.stripe.com/api/products/update?lang=python
+ """
+ try:
+ product = stripe.Product.modify(product_id, **kwargs)
+ return {'success': True, 'data': product}
+ except stripe.error.StripeError as e:
+ return {'success': False, 'message': f"Error updating product: {e}"}
+
+ @staticmethod
+ def delete_product(product_id: str):
+ """
+ Delete a Stripe Product by its ID.
+
+ :param product_id: ID of the product to delete.
+ :return: The deleted Stripe Product object.
+
+ See: https://docs.stripe.com/api/products/delete?lang=python
+ """
+ try:
+ product = stripe.Product.delete(product_id)
+ return {'success': True, 'data': product}
+ except stripe.error.StripeError as e:
+ return {'success': False, 'message': f"Error deleting product: {e}"}
+
+ @staticmethod
+ def create_price(product_id: str = None, product_data: dict = None, unit_amount: int = None, currency: str = 'gbp', recurring: dict = None, metadata: dict = None):
+ """
+ Create a Stripe Price for a product.
+
+ :param product_id: ID of the product for which the price is being created.
+ :param product_data: A dictionary with product details to create a new product on the fly. Example:
+ - name : The name of the product.
+ - description : The description of the product.
+ :param unit_amount: The amount to be charged.(in cents)
+ :param currency: The currency of the price.
+ :param recurring: A dictionary with recurring pricing details. Example:
+ - interval : The interval at which the price is charged (e.g., 'day', 'week', 'month', 'year'.
+ - interval_count : The number of intervals at which the price is charged.
+ :param metadata: An optional dictionary of key-value pairs to attach to the price.
+ :return: The created Stripe Price object.
+ :raise ValueError: If neither product_id nor product_data is provided.
+
+ See: https://docs.stripe.com/api/prices/create?lang=python
+ """
+ if not product_id and not product_data:
+ raise ValueError("Either product_id or product_data must be provided to create a price.")
+
+ price_data = {
+ 'unit_amount': unit_amount,
+ 'currency': currency,
+ 'recurring': recurring,
+ 'metadata': metadata
+ }
+
+ if product_id:
+ price_data['product'] = product_id
+ elif product_data:
+ price_data['product'] = stripe.Product.create(**product_data).id
+ try:
+ price = stripe.Price.create(**price_data)
+ return {'success': True, 'data': price}
+ except stripe.error.StripeError as e:
+ return {'success': False, 'message': f"Error creating price: {e}"}
+
+ @staticmethod
+ def retrieve_price(price_id: str):
+ """
+ Retrieve a Stripe Price by its ID.
+ :param price_id: ID of the price to retrive
+ :return: The retrieved Stripe Price object
+
+ See: https://docs.stripe.com/api/prices/retrieve?lang=python
+ """
+ try:
+ price = stripe.Price.retrieve(price_id)
+ return {'success': True, 'data': price}
+ except stripe.error.StripeError as e:
+ return {'success': False, 'message': f"Error retrieving price: {e}"}
+
+ @staticmethod
+ def update_price(price_id: str, **kwargs):
+ """
+ Update a Stripe Price by its ID.
+ :param price_id: ID of the price to update
+ :param kwargs: Optional parameters to update the price, such as:
+ - active: A boolean flag indicating if the price is active.
+ - nickname: A nickname for the price, useful for labeling and organizing.
+ - metadata: A set of key-value pairs to attach to the price object.
+ :return: The updated Stripe Price object
+
+ See: https://docs.stripe.com/api/prices/update?lang=python
+ """
+ try:
+ price = stripe.Price.modify(price_id, **kwargs)
+ return {'success': True, 'data': price}
+ except stripe.error.StripeError as e:
+ return {'success': False, 'message': f"Error updating price: {e}"}
+
+ # stipe not provide to delete the price
+
+ @staticmethod
+ def create_coupon(
+ amount_off: int = None,
+ percent_off: float = None,
+ duration: str = "once",
+ name: str = None,
+ currency: str = None,
+ redeem_by: datetime = None,
+ max_redemptions: int = 0,
+ metadata: dict = None
+ ) -> dict:
+ """
+ Creates a Stripe Coupon with either a fixed amount off or a percentage off.
+
+ :param amount_off: The discount amount to be applied (in the smallest currency unit, e.g., cents). This cannot be used in conjunction with `percent_off`.
+ :param percent_off: The discount percentage to be applied to the price. This cannot be used in conjunction with `amount_off`.
+ :param duration: The duration for which the coupon is valid. Valid values are:
+ - "once": The coupon will apply to the next invoice only.
+ :param name: An optional name for the coupon.
+ :param currency: The currency in which the `amount_off` is specified. Required if `amount_off` is used.
+ :param redeem_by: A timestamp at which the coupon will no longer be redeemable.
+ The coupon can still be applied to invoices created after the `redeem_by` date,
+ if the subscription was active prior to the date.
+ :param max_redemptions: The maximum number of times this coupon can be redeemed in total.
+ Defaults to 0, meaning unlimited redemptions.
+ :param metadata: A set of key-value pairs to store additional information about the coupon in Stripe.
+
+ :return: A dictionary containing:
+ - 'success': Boolean indicating the success of the operation.
+ - 'data': The created Stripe Coupon object if successful.
+ - 'message': Error message if the operation failed.
+
+ :raises ValueError: If both `amount_off` and `percent_off` are provided, or if neither is provided.
+ Also raised if `amount_off` is provided without a corresponding `currency`.
+ :raises stripe.error.StripeError: If an error occurs while creating the coupon via the Stripe API.
+
+ See: https://docs.stripe.com/api/coupons/create?lang=python
+ """
+ if amount_off and percent_off:
+ raise ValueError("You can provide either `amount_off` or `percent_off`, but not both.")
+
+ if not amount_off and not percent_off:
+ raise ValueError("You must provide either `amount_off` or `percent_off`.")
+
+ if amount_off and not currency:
+ raise ValueError("Currency must be provided when `amount_off` is specified.")
+
+ coupon_data = {
+ "duration": duration,
+ "name": name,
+ "redeem_by": redeem_by,
+ "max_redemptions": max_redemptions,
+ "metadata": metadata,
+ }
+
+ if amount_off:
+ coupon_data.update({
+ "amount_off": amount_off,
+ "currency": currency,
+ })
+ elif percent_off:
+ coupon_data.update({
+ "percent_off": percent_off,
+ })
+
+ try:
+ coupon = stripe.Coupon.create(**coupon_data)
+ return {'success': True, 'data': coupon}
+ except stripe.error.StripeError as e:
+ return {'success': False, 'message': f"Error creating coupon: {e}"}
+
+ @staticmethod
+ def retrieve_coupon(coupon_id: str):
+ """
+ Retrieve a Stripe Coupon by its ID.
+
+ :param coupon_id: The ID of the coupon to retrieve.
+ :return: The retrieved Stripe Coupon object.
+
+ See: https://docs.stripe.com/api/coupons/retrieve?lang=python
+ """
+ try:
+ coupon = stripe.Coupon.retrieve(coupon_id)
+ return {'success': True, 'data': coupon}
+ except stripe.error.StripeError as e:
+ return {'success': False, 'message': f"Error retrieving coupon: {e}"}
+
+ @staticmethod
+ def delete_coupon(coupon_id: str):
+ """
+ Retrieve a Stripe Coupon by its ID.
+
+ :param coupon_id: The ID of the coupon to retrieve.
+ :return: The retrieved Stripe Coupon object.
+ """
+ try:
+ coupon = stripe.Coupon.delete(coupon_id)
+ return {'success': True, 'data': coupon}
+ except stripe.error.StripeError as e:
+ return {'success': False, 'message': f"Error deleting coupon: {e}"}
+
+ @staticmethod
+ def cancel_auto_renew_subscription(subscription_id: str):
+ """
+ Cancels the auto-renewal of a Stripe subscription.
+
+ :param subscription_id: The ID of the subscription to cancel auto-renewal for.
+ :return: A dictionary with success status and the updated subscription object or an error message.
+ """
+ try:
+ # Update the subscription to cancel at the end of the current period
+ subscription = stripe.Subscription.modify(
+ subscription_id,
+ cancel_at_period_end=True
+ )
+ return {'success': True, 'data': subscription}
+ except stripe.error.StripeError as e:
+
+ return {'success': False, 'message': f'Error cancelling subscription auto-renewal: {e}'}
+
+from cryptography.fernet import Fernet
+class Encryptor:
+ def __init__(self):
+ self.key = "paMSf3Ny8KAMs1tRLcVOQQhRxTnInHLwP7WtVdm8O_4="
+ self.fernet = Fernet(self.key)
+
+ def encrypt(self, plaintext):
+ return self.fernet.encrypt(plaintext.encode()).decode()
+
+ def decrypt(self, encrypted_text):
+ return self.fernet.decrypt(encrypted_text.encode()).decode()
+
diff --git a/goodtimes/settings/base.py b/goodtimes/settings/base.py
index 75e1ce9..8e96bc9 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",
]
@@ -76,12 +77,14 @@ THIRD_PARTY_APPS = [
"taggit",
"django_quill",
"corsheaders",
+ 'django_extensions',
"allauth",
"allauth.account",
"allauth.socialaccount",
"allauth.socialaccount.providers.apple",
"allauth.socialaccount.providers.google",
- # "django_crontab",
+ "django_filters",
+ "django_crontab",
# "django_celery_results",
# "django_celery_beat",
]
@@ -209,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
@@ -237,6 +241,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
@@ -301,8 +306,13 @@ SIMPLE_JWT = {
"JTI_CLAIM": "jti",
}
+
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")
ONE_SIGNAL_API_KEY = env.str("ONE_SIGNAL_API_KEY")
@@ -328,7 +338,7 @@ CHANNEL_LAYERS = {
WEBSOCKET_TIMEOUT = 30
CRONJOBS = [
- # ("0 9 * * 1-5", "manage_games.cron.update_game_status_live"),
+ # ('0 0 * * *', 'myapp.cron.daily_task >> /path/to/logfile.log 2>&1'),
]
GOOGLE_MAPS_API_KEY = env.str("GOOGLE_MAPS_API_KEY")
@@ -337,3 +347,19 @@ 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")
+
+# 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_GRAPH_VERSION_API = env.str("FACEBOOK_GRAPH_VERSION_API")
+FACEBOOK_ACCESS_TOKEN = env.str("FACEBOOK_ACCESS_TOKEN")
+
+# Instagram Key
+INSTAGRAM_PAGE_ID = env.str('INSTAGRAM_PAGE_ID')
diff --git a/goodtimes/settings/development.py b/goodtimes/settings/development.py
index f362626..92b63e2 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,15 +44,14 @@ 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/"
-STRIPE_FINAL_URL = "http://localhost:8000/subscriptions/create-checkout-session/"
+STRIPE_CHECKOUT_URL = "https://deciding-firmly-fly.ngrok-free.app/subscriptions/create-checkout-session/"
+COUPON_VALIDITY_CHECK_URL = "https://deciding-firmly-fly.ngrok-free.app/subscriptions/coupon-validity-check/"
+
+LOGO_PATH = "static"
diff --git a/goodtimes/settings/production.py b/goodtimes/settings/production.py
index b2413da..9efa105 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/"
-)
-STRIPE_FINAL_URL = (
- "https://staging.goodtimesltd.co.uk/subscriptions/create-checkout-session/"
+ "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 7d4d773..cb67fbc 100644
--- a/goodtimes/settings/staging.py
+++ b/goodtimes/settings/staging.py
@@ -6,61 +6,61 @@ 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",".staging.goodtimesltd.co.uk"]
-# 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://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/
@@ -75,9 +75,10 @@ STATIC_URL = "/static/"
STATICFILES_DIRS = [BASE_DIR.joinpath("static")]
+
STRIPE_CHECKOUT_URL = (
- "https://staging.goodtimesltd.co.uk/subscriptions/stripe-subscription/"
-)
-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/settings/wdipl.py b/goodtimes/settings/wdipl.py
index fdc2f39..da93a03 100644
--- a/goodtimes/settings/wdipl.py
+++ b/goodtimes/settings/wdipl.py
@@ -5,7 +5,7 @@ import colorlog
# from logging.handlers import TimedRotatingFileHandler
-DEBUG = False
+DEBUG = True
ALLOWED_HOSTS = ["127.0.0.1", "goodtimes.betadelivery.com", "154.41.254.33"]
@@ -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/
@@ -76,8 +76,13 @@ STATIC_URL = "/static/"
STATICFILES_DIRS = [BASE_DIR.joinpath("static")]
STRIPE_CHECKOUT_URL = (
- "https://goodtimes.betadelivery.com/subscriptions/stripe-subscription/"
-)
-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"
+
+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/goodtimes/urls.py b/goodtimes/urls.py
index 5414dc6..f1cc9b8 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")),
@@ -60,8 +63,9 @@ urlpatterns = [
# path('api/', include("accounts.api.urls")),
]
+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 += [path("__debug__/", include(debug_toolbar.urls))]
diff --git a/goodtimes/utils.py b/goodtimes/utils.py
index d56def8..d85f258 100644
--- a/goodtimes/utils.py
+++ b/goodtimes/utils.py
@@ -1,3 +1,4 @@
+from django.http import JsonResponse
from rest_framework.response import Response
from rest_framework.renderers import JSONRenderer
from rest_framework import status
@@ -40,6 +41,24 @@ class ApiResponse:
# return ApiResponse.error("Validation error", errors, status_code)
+class JsonResponseUtil:
+ """
+ A utility class for creating JSON responses with a standardized format.
+ """
+ @staticmethod
+ def success(message, data=None, status=200):
+ response_data = {"success": True, "status": status, "message": message}
+ if data is not None:
+ response_data["data"] = data
+ return JsonResponse(response_data, status=status)
+
+ @staticmethod
+ def error(message, errors=None, status=403):
+ response_data = {"success": False, "status": status, "message": message}
+ if errors is not None:
+ response_data["errors"] = errors
+ return JsonResponse(response_data, status=status)
+
class RandomGenerator:
@staticmethod
def number(start, end):
diff --git a/goodtimes/webhook.py b/goodtimes/webhook.py
deleted file mode 100644
index 9b54998..0000000
--- a/goodtimes/webhook.py
+++ /dev/null
@@ -1,285 +0,0 @@
-from django.conf import settings
-from django.db import transaction
-import requests
-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_referrals.models import (
- GoodTimeCoins,
- ReferralRecord,
- ReferralRecordReward,
- ReferralTracking,
-)
-from django.core.exceptions import ObjectDoesNotExist
-from manage_subscriptions.models import PrincipalSubscription, Subscription
-from manage_wallets.models import (
- TransactionStatus,
- TransactionType,
- Wallet,
- Transaction,
-)
-import logging
-
-
-logger = logging.getLogger(__name__)
-
-
-class NotificationService:
- def __init__(self):
- self.client = OneSignalClient(
- app_id=settings.ONE_SIGNAL_APP_ID, rest_api_key=settings.ONE_SIGNAL_API_KEY
- )
-
- def send_notification(self, title, message, player_id):
- if player_id is None:
- print("Player ID is None, skipping notification")
- return
- notification_payload = {
- "headings": {"en": title},
- "contents": {"en": message},
- "include_player_ids": [player_id],
- }
- response = self.client.send_notification(notification_payload)
- return response
-
- def save_notification(self, principal, title, message, notification_category):
- InAppNotification.objects.create(
- principal=principal,
- title=title,
- message=message,
- notification_category=notification_category,
- )
-
- def payment_success_notification(
- self, principal, subscription, principal_subscription, amount
- ):
- print("payment_success_notification: ", principal.player_id)
- title = "Payment Successful"
- 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)
-
- 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.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)
-
-
-class WebhookService:
- def __init__(self, webhook_data):
- self.webhook_data = webhook_data
- self.event_type = webhook_data["type"]
- self.charge_data = webhook_data["data"]["object"]
-
- def get_event_type(self):
- return self.event_type
-
- def get_principal(self):
- principal_id = self.charge_data["metadata"]["principal"]
- try:
- return IAmPrincipal.objects.get(id=int(principal_id))
- except (ObjectDoesNotExist, ValueError):
- logger.error(f"Invalid principal ID: {principal_id}")
- raise ValueError(f"Invalid principal ID: {principal_id}")
-
- def get_transaction(self):
- transaction_id = self.charge_data["metadata"]["transaction_id"]
- try:
- return Transaction.objects.get(id=int(transaction_id))
- except (ObjectDoesNotExist, ValueError):
- logger.error(f"Invalid transaction ID: {transaction_id}")
- raise ValueError(f"Invalid transaction ID: {transaction_id}")
-
- def get_subscription(self):
- subscription_id = self.charge_data["metadata"]["subscription_id"]
- try:
- return Subscription.objects.get(id=int(subscription_id))
- except (ObjectDoesNotExist, ValueError):
- logger.error(f"Invalid subscription ID: {subscription_id}")
- raise ValueError(f"Invalid subscription ID: {subscription_id}")
-
- def get_order_id(self):
- return self.charge_data["metadata"]["order_id"]
-
-
-class ReferralRewardService:
- def __init__(self, principal, principal_subscription, subscription):
- self.notification_service = NotificationService()
- self.principal = principal
- self.principal_subscription = principal_subscription
- self.subscription = subscription
-
- def _fetch_referral_record(self):
- return ReferralRecord.objects.filter(
- referred_principal=self.principal,
- is_completed=True,
- active=True, # Assuming 'active' is a field determining if the record is currently relevant
- deleted=False, # Assuming logical deletion is handled by a 'deleted' field
- ).first()
-
- def _check_active_subscription(self, referrer_principal):
- today = timezone.now().date()
- return (
- PrincipalSubscription.objects.filter(
- principal=referrer_principal,
- is_paid=True,
- end_date__gte=today,
- cancelled=False,
- deleted=False,
- )
- .order_by("-end_date")
- .first()
- )
-
- def _credit_reward(self, referral_record, subscription):
- amount = subscription.referral_percentage * subscription.amount / 100
- ReferralRecordReward.objects.create(
- referral_record=referral_record,
- subscription=subscription,
- coins=1, # This value could be dynamically calculated or configured elsewhere
- value=amount,
- )
- self._credit_transaction(referral_record.referrer_principal, amount)
- self.notification_service.referral_received_notification(
- referral_record.referrer_principal, amount, self.principal.email
- )
-
- def _credit_transaction(self, referrer_principal, amount):
- print("_credit_transaction: ", referrer_principal)
- Transaction.objects.create(
- principal=referrer_principal,
- transaction_type=TransactionType.CREDIT,
- payment_method="",
- transaction_status=TransactionStatus.SUCCESS,
- amount=amount,
- coins=1,
- comment="Referral reward",
- # Populate other fields as necessary, such as `order_id`, `product_id`, or `reference_id` if applicable
- )
-
- def _update_reward_status(self, referral_record, active_subscription):
- # Check if the referrer has an active subscription and get its ID if it exists
- referrer_subscription_id = (
- active_subscription.id if active_subscription else None
- )
-
- # Create a new subscription for the referred principal
- referred_subscription_id = self.principal_subscription.id
-
- is_referrer_subscribed = bool(active_subscription)
-
- ReferralTracking.objects.create(
- referral_record=referral_record,
- referrer_subscription_id=referrer_subscription_id,
- referred_subscription_id=referred_subscription_id,
- is_referrer_subscribed=is_referrer_subscribed,
- )
-
- def credit_referral_reward_if_applicable(self):
- referral_record = self._fetch_referral_record()
- if referral_record:
- active_subscription = self._check_active_subscription(
- referral_record.referrer_principal
- )
- if active_subscription:
- print("active_subscription: ", active_subscription)
- if self.subscription:
- print("self.subscription: ", self.subscription)
- self._credit_reward(referral_record, self.subscription)
-
- self._update_reward_status(referral_record, active_subscription)
-
-
-class SubscriptionService:
- def __init__(self):
- self.principal_subscription = None
-
- def create_principal_subscription(self, principal, subscription, order_id):
- subscription_days = subscription.plan.days
- today = timezone.now().date()
- last_date = today + timedelta(days=subscription_days)
- principal_subscription = PrincipalSubscription.objects.create(
- principal=principal,
- subscription=subscription,
- is_paid=True,
- order_id=order_id,
- start_date=today,
- end_date=last_date,
- grace_period_end_date=last_date + timedelta(days=15),
- )
- self.principal_subscription = principal_subscription
- return principal_subscription
-
- def update_transaction_success(self, principal_transaction, principal_subscription):
- principal_transaction.transaction_status = TransactionStatus.SUCCESS
- principal_transaction.principal_subscription = principal_subscription
- principal_transaction.save()
-
- def update_transaction_failure(self, principal_transaction):
- principal_transaction.transaction_status = TransactionStatus.FAIL
- principal_transaction.save()
-
-
-class PaymentProcessingService:
- def __init__(self, webhook_data):
- self.webhook_service = WebhookService(webhook_data)
- self.notification_service = NotificationService()
- # Retrieve objects
- self.principal = self.webhook_service.get_principal()
- self.transaction = self.webhook_service.get_transaction()
- self.subscription = self.webhook_service.get_subscription()
- self.order_id = self.webhook_service.get_order_id()
- self.subscription_service = SubscriptionService()
- self.principal_subscription = None
-
- def process_event(self):
- if self.webhook_service.get_event_type() == "checkout.session.completed":
- self.handle_success()
- else:
- self.handle_failure()
-
- def handle_success(self):
- with transaction.atomic():
- # Create or update the principal subscription
- self.principal_subscription = (
- self.subscription_service.create_principal_subscription(
- self.principal, self.subscription, self.order_id
- )
- )
- print("First Part Done....!!!!!")
- # Update transaction status to success
- self.subscription_service.update_transaction_success(
- self.transaction, self.principal_subscription
- )
- print("Second Part Done....!!!!!")
- # Now handle referral rewards, if applicable
- referral_service = ReferralRewardService(
- self.principal, self.principal_subscription, self.subscription
- )
- print("Above Third Part...!!!!!!!!!!!")
- referral_service.credit_referral_reward_if_applicable()
- print("Third Part Done....!!!!!")
- self.notification_service.payment_success_notification(
- self.principal,
- self.subscription,
- self.principal_subscription,
- self.transaction.amount,
- )
-
- def handle_failure(self):
- self.subscription_service.update_transaction_failure(self.transaction)
- # self.notification_service.payment_failed_notification(
- # self.principal, self.subscription, self.transaction.amount
- # )
diff --git a/goodtimes/webhook/__init__.py b/goodtimes/webhook/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/goodtimes/webhook/notification_service.py b/goodtimes/webhook/notification_service.py
new file mode 100644
index 0000000..0167e9f
--- /dev/null
+++ b/goodtimes/webhook/notification_service.py
@@ -0,0 +1,80 @@
+from onesignal_sdk.client import Client as OneSignalClient
+import logging
+from manage_notifications.models import (
+ IAmPrincipalNotificationSettings,
+ InAppNotification,
+ NotificationCategoryChoices,
+)
+from django.shortcuts import get_object_or_404
+from django.conf import settings
+
+
+logger = logging.getLogger(__name__)
+
+
+class NotificationService:
+ def __init__(self):
+ self.client = OneSignalClient(
+ app_id=settings.ONE_SIGNAL_APP_ID, rest_api_key=settings.ONE_SIGNAL_API_KEY
+ )
+
+ def send_notification(self, title, message, player_id):
+ if player_id is None:
+ print("Player ID is None, skipping notification")
+ return
+ notification_payload = {
+ "headings": {"en": title},
+ "contents": {"en": message},
+ "include_player_ids": [player_id],
+ }
+ response = self.client.send_notification(notification_payload)
+ return response
+
+ def save_notification(self, principal, title, message, notification_category):
+ InAppNotification.objects.create(
+ principal=principal,
+ title=title,
+ message=message,
+ notification_category=notification_category,
+ )
+
+ def payment_success_notification(
+ self, principal, subscription, principal_subscription, amount
+ ):
+ print("payment_success_notification: ", principal.player_id)
+ title = "Payment Successful"
+ 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
+ )
+
+ 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
+ self.send_notification(title, message, principal.player_id)
+
+ 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
+ )
+
+ 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
diff --git a/goodtimes/webhook/payment_processing_service.py b/goodtimes/webhook/payment_processing_service.py
new file mode 100644
index 0000000..5b075fd
--- /dev/null
+++ b/goodtimes/webhook/payment_processing_service.py
@@ -0,0 +1,180 @@
+from venv import logger
+from django.db import transaction
+
+from manage_wallets.models import (
+ PaymentMethod,
+ Transaction,
+ TransactionStatus,
+ TransactionType,
+)
+from .notification_service import NotificationService
+from .referral_reward_service import ReferralRewardService
+from .subscription_service import SubscriptionService
+from .webhook_service import WebhookService
+
+
+class PaymentProcessingService:
+ def __init__(
+ self,
+ webhook_data,
+ stripe_subscription,
+ current_period_start,
+ 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
+ self.current_period_start = current_period_start
+ self.current_period_end = current_period_end
+
+ @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 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):
+ """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(
+ 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,
+ 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."""
+ 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"]:
+ self.handle_failure(txn)
+ else:
+ logger.warning(f"Unknown event type {event_type}. Skipping.")
+ return
+
+ except Exception as e:
+ logger.error(f"Unexpected error: {str(e)}")
+ raise
+
+ def handle_success(self, transaction):
+ """Handle a successful payment."""
+ try:
+ self.create_principal_subscription(transaction)
+ 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, 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=transaction.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.amount
+ )
+ 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/referral_reward_service.py b/goodtimes/webhook/referral_reward_service.py
new file mode 100644
index 0000000..31198c2
--- /dev/null
+++ b/goodtimes/webhook/referral_reward_service.py
@@ -0,0 +1,110 @@
+from .notification_service import NotificationService
+from manage_referrals.models import (
+ ReferralRecord,
+ ReferralRecordReward,
+ ReferralTracking,
+)
+from manage_wallets.models import Transaction, TransactionType, TransactionStatus
+from django.utils import timezone
+from manage_subscriptions.models import PrincipalSubscription
+
+
+class ReferralRewardService:
+ def __init__(self, principal, principal_subscription, subscription):
+ self._notification_service = NotificationService()
+ self._principal = principal
+ self._principal_subscription = principal_subscription
+ self._subscription = subscription
+
+ @property
+ def principal(self):
+ return self._principal
+
+ @property
+ def principal_subscription(self):
+ return self._principal_subscription
+
+ @property
+ def subscription(self):
+ return self._subscription
+
+ @staticmethod
+ def _fetch_referral_record(principal):
+ """Fetch the referral record for the given principal."""
+ return ReferralRecord.objects.filter(
+ referred_principal=principal,
+ is_completed=True,
+ active=True,
+ deleted=False,
+ ).first()
+
+ @staticmethod
+ def _check_active_subscription(referrer_principal):
+ """Check if the referrer principal has an active subscription."""
+ today = timezone.now().date()
+ return (
+ PrincipalSubscription.objects.filter(
+ principal=referrer_principal,
+ is_paid=True,
+ end_date__gte=today,
+ active=True,
+ )
+ .order_by("-end_date")
+ .first()
+ )
+
+ def _credit_reward(self, referral_record, subscription):
+ amount = subscription.referral_percentage * subscription.amount / 100
+ ReferralRecordReward.objects.create(
+ referral_record=referral_record,
+ subscription=subscription,
+ coins=1, # This value could be dynamically calculated or configured elsewhere
+ value=amount,
+ )
+ self._credit_transaction(referral_record.referrer_principal, amount)
+ self._notification_service.referral_received_notification(
+ referral_record.referrer_principal, amount, self.principal.email
+ )
+
+ def _credit_transaction(self, referrer_principal, amount):
+ """Create a transaction record for the referral reward."""
+ print("referrer_principal: ", referrer_principal)
+ Transaction.objects.create(
+ principal=referrer_principal,
+ transaction_type=TransactionType.CREDIT,
+ payment_method="",
+ transaction_status=TransactionStatus.SUCCESS,
+ amount=amount,
+ coins=1,
+ comment="Referral reward",
+ )
+
+ def _update_reward_status(self, referral_record, active_subscription):
+ """Update the status of the referral reward."""
+ referrer_subscription_id = (
+ active_subscription.id if active_subscription else None
+ )
+
+ # Create a new subscription for the referred principal
+ referred_subscription_id = self.principal_subscription.id
+
+ is_referrer_subscribed = bool(active_subscription)
+
+ ReferralTracking.objects.create(
+ referral_record=referral_record,
+ referrer_subscription_id=referrer_subscription_id,
+ referred_subscription_id=referred_subscription_id,
+ is_referrer_subscribed=is_referrer_subscribed,
+ )
+
+ def credit_referral_reward_if_applicable(self):
+ """Credit referral reward if applicable based on the referral record."""
+ referral_record = self._fetch_referral_record(self.principal)
+ if referral_record:
+ active_subscription = self._check_active_subscription(
+ referral_record.referrer_principal
+ )
+ if active_subscription and self.subscription:
+ self._credit_reward(referral_record, self.subscription)
+
+ self._update_reward_status(referral_record, active_subscription)
diff --git a/goodtimes/webhook/subscription_service.py b/goodtimes/webhook/subscription_service.py
new file mode 100644
index 0000000..ef9ba32
--- /dev/null
+++ b/goodtimes/webhook/subscription_service.py
@@ -0,0 +1,78 @@
+from datetime import timedelta
+from django.utils import timezone
+import datetime
+from manage_subscriptions.models import PrincipalSubscription, SubscriptionStatus
+
+
+class SubscriptionService:
+ def __init__(self):
+ self._principal_subscription = None
+
+ @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(
+ self,
+ principal,
+ subscription,
+ stripe_subscription,
+ order_id,
+ current_period_start,
+ current_period_end,
+ coupon=None,
+ ):
+ """Create a principal subscription and return it."""
+ start_date, end_date = self._calculate_dates(
+ current_period_start, current_period_end, subscription.calculate_days()
+ )
+
+ PrincipalSubscription.objects.filter(principal=principal, status=SubscriptionStatus.ACTIVE).update(status=SubscriptionStatus.EXPIRED, active=False)
+
+ principal_subscription = PrincipalSubscription.objects.create(
+ principal=principal,
+ subscription=subscription,
+ stripe_subscription_id=stripe_subscription,
+ is_paid=True,
+ auto_renew=bool(stripe_subscription),
+ order_id=order_id,
+ start_date=start_date,
+ end_date=end_date,
+ grace_period_end_date=PrincipalSubscription.generate_grace_period_end_date(end_date),
+ coupon_code=coupon.coupon_code if coupon else None,
+ )
+
+ if coupon:
+ self._update_coupon(coupon)
+
+ self.principal_subscription = principal_subscription
+ return principal_subscription
+
+ def _calculate_dates(
+ self, current_period_start, current_period_end, subscription_days
+ ):
+ """Calculate subscription start and end dates."""
+ today = timezone.now().date()
+ 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))
+ )
+ return start_date, end_date
+
+ def _update_coupon(self, coupon):
+ """Update coupon usage count."""
+ coupon.no_of_redeems += 1
+ coupon.save()
+ print("Coupon Saved Successfully!!!")
diff --git a/goodtimes/webhook/webhook_service.py b/goodtimes/webhook/webhook_service.py
new file mode 100644
index 0000000..c2b9f5a
--- /dev/null
+++ b/goodtimes/webhook/webhook_service.py
@@ -0,0 +1,90 @@
+from decimal import Decimal
+from django.conf import settings
+import stripe
+from django.core.exceptions import ObjectDoesNotExist
+from accounts.models import IAmPrincipal
+from manage_coupons.models import Coupon
+from manage_subscriptions.models import Subscription
+import logging
+
+
+logger = logging.getLogger(__name__)
+
+stripe.api_key = settings.STRIPE_SECRET_KEY
+
+
+class WebhookService:
+ def __init__(self, webhook_data):
+ self._webhook_data = webhook_data
+ self._event_type = webhook_data["type"]
+ self._charge_data = webhook_data["data"]["object"]
+ self._metadata = self._fetch_metadata()
+
+ def _fetch_metadata(self):
+ """Fetch metadata based on the event type."""
+ 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")
+ if subscription_id:
+ subscription = stripe.Subscription.retrieve(subscription_id)
+ return subscription.get("metadata", {})
+ return {}
+
+ @property
+ def event_type(self):
+ return self._event_type
+
+ @property
+ def charge_data(self):
+ return self._charge_data
+
+ def _get_object_from_metadata(self, model, id_key):
+ """Retrieve object from metadata."""
+ obj_id = self._metadata.get(id_key)
+ if obj_id:
+ try:
+ return model.objects.get(id=int(obj_id))
+ except (ObjectDoesNotExist, ValueError) as e:
+ logger.error(f"Invalid {model.__name__} ID: {obj_id}")
+ raise ValueError(f"Invalid {model.__name__} ID: {obj_id}") from e
+ return None
+
+ def get_event_type(self):
+ return self.event_type
+
+ def get_principal(self):
+ """Retrieve principal from metadata."""
+ return self._get_object_from_metadata(IAmPrincipal, "principal")
+
+ def get_subscription(self):
+ """Retrieve subscription from metadata."""
+ return self._get_object_from_metadata(Subscription, "subscription_id")
+
+ def get_coupon(self):
+ """Retrieve coupon from metadata."""
+ coupon_code = self._metadata.get("couponCode")
+ print("get_coupon:coupon_code: ", coupon_code)
+ 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
+
+ def get_final_amount(self):
+ """Retrieve Amount after coupon discount from either stripe event or metadata."""
+ if self.event_type == "checkout.session.completed":
+ return (
+ Decimal(self._charge_data.get("amount_total", 0)) / 100
+ )
+ elif self.event_type == "invoice.payment_succeeded":
+ return (
+ Decimal(self._charge_data.get("amount_paid", 0)) / 100
+ )
+
+ # Fallback: Try to get the amount from metadata
+ return (
+ Decimal(self._metadata.get("metadata", {}).get("finalAmount", 0)) / 100
+ )
diff --git a/manage_cms/api/serializers.py b/manage_cms/api/serializers.py
index b9385f6..1da6dbd 100644
--- a/manage_cms/api/serializers.py
+++ b/manage_cms/api/serializers.py
@@ -55,6 +55,13 @@ class OrganizationSerializer(serializers.ModelSerializer):
"subscription_agreement",
"license_agreement_user",
"license_agreement_merchant",
+ "contact_us_email",
+ "instagram_handle",
+ "facebook_handle",
+ "linkedin_handle",
+ "website_url",
+ "address",
+ "contact_us_phone",
]
class EducationVideoSerializer(serializers.ModelSerializer):
diff --git a/manage_cms/forms.py b/manage_cms/forms.py
index 1d79433..7b323f4 100644
--- a/manage_cms/forms.py
+++ b/manage_cms/forms.py
@@ -22,26 +22,31 @@ class OrganizationForm(forms.ModelForm):
fields = [
"title",
"contact_us_email",
+ "contact_us_phone",
+ "address",
+ "website_url",
"instagram_handle",
"facebook_handle",
"linkedin_handle",
+ "twitter_handle",
"logo_image",
"favicon_image",
- "website_url",
]
labels = {
"title": "Organization Title",
"contact_us_email": "Contact Email",
+ "contact_us_phone": "Contact Phone No",
+ "Address": "Contact Address",
+ "website_url": "Website URL",
"instagram_handle": "Instagram URL",
"facebook_handle": "Facebook URL",
"linkedin_handle": "LinkedIn URL",
+ "Twitter_handle": "Twitter URL",
"logo_image": "Organization Logo",
"favicon_image": "Favicon",
- "website_url": "Website URL",
}
-
class NewsAndArticleCategoryForm(forms.ModelForm):
class Meta:
model = NewsAndArticlesCategory
diff --git a/manage_cms/migrations/0002_organization_address_organization_contact_us_phone.py b/manage_cms/migrations/0002_organization_address_organization_contact_us_phone.py
new file mode 100644
index 0000000..d30c25f
--- /dev/null
+++ b/manage_cms/migrations/0002_organization_address_organization_contact_us_phone.py
@@ -0,0 +1,25 @@
+# Generated by Django 5.0.2 on 2024-08-16 08:29
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('manage_cms', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='organization',
+ name='address',
+ field=models.TextField(default='this is address'),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='organization',
+ name='contact_us_phone',
+ field=models.CharField(default='+911212121212', max_length=16),
+ preserve_default=False,
+ ),
+ ]
diff --git a/manage_cms/migrations/0003_organization_twitter_handle_and_more.py b/manage_cms/migrations/0003_organization_twitter_handle_and_more.py
new file mode 100644
index 0000000..fbbb32e
--- /dev/null
+++ b/manage_cms/migrations/0003_organization_twitter_handle_and_more.py
@@ -0,0 +1,28 @@
+# Generated by Django 5.0.2 on 2024-08-16 09:29
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('manage_cms', '0002_organization_address_organization_contact_us_phone'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='organization',
+ name='twitter_handle',
+ field=models.URLField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name='organization',
+ name='address',
+ field=models.TextField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name='organization',
+ name='contact_us_phone',
+ field=models.CharField(blank=True, max_length=16, null=True),
+ ),
+ ]
diff --git a/manage_cms/models.py b/manage_cms/models.py
index 861aa8f..65474d3 100644
--- a/manage_cms/models.py
+++ b/manage_cms/models.py
@@ -89,6 +89,7 @@ class Organization(BaseModel):
instagram_handle = models.URLField(blank=True, null=True)
facebook_handle = models.URLField(blank=True, null=True)
linkedin_handle = models.URLField(blank=True, null=True)
+ twitter_handle = models.URLField(blank=True, null=True)
logo_image = models.ImageField(blank=True, null=True, upload_to="organization/logo")
favicon_image = models.ImageField(
blank=True, null=True, upload_to="organization/favicon"
@@ -104,6 +105,8 @@ class Organization(BaseModel):
subscription_agreement = QuillField()
license_agreement_user = QuillField()
license_agreement_merchant = QuillField()
+ address = models.TextField(blank=True, null=True)
+ contact_us_phone = models.CharField(max_length=16, blank=True, null=True)
class Meta:
db_table = "organization"
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
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_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..e195db7
--- /dev/null
+++ b/manage_coupons/admin.py
@@ -0,0 +1,40 @@
+from django.contrib import admin
+from .models import Coupon
+
+
+class CouponAdmin(admin.ModelAdmin):
+ list_display = (
+ "id",
+ "title",
+ "coupon_id",
+ "coupon_code",
+ "discount_amount",
+ "discount_percentage",
+ "valid_from",
+ "valid_to",
+ "max_redeems",
+ "no_of_redeems",
+ "is_active",
+ )
+ search_fields = ("title", "coupon_code")
+ list_filter = ("valid_from", "valid_to", "max_redeems")
+ readonly_fields = ("no_of_redeems",)
+
+ fieldsets = (
+ (
+ None,
+ {"fields": ("title", "coupon_code", "coupon_id", "description", "image")},
+ ),
+ (
+ "Discount Information",
+ {"fields": ("discount_amount", "discount_percentage")},
+ ),
+ ("Validity", {"fields": ("valid_from", "valid_to")}),
+ ("Redemption", {"fields": ("max_redeems", "no_of_redeems")}),
+ )
+
+ def is_active(self, obj):
+ return obj.is_valid()
+
+
+admin.site.register(Coupon, CouponAdmin)
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..22874e5
--- /dev/null
+++ b/manage_coupons/forms.py
@@ -0,0 +1,24 @@
+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",
+ "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"}),
+ # "discount_amount": forms.NumberInput(attrs={'step': '0.01'}),
+ # "discount_percentage": forms.NumberInput(attrs={'step': '0.01'}),
+ }
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/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/migrations/0003_alter_coupon_discount_amount_and_more.py b/manage_coupons/migrations/0003_alter_coupon_discount_amount_and_more.py
new file mode 100644
index 0000000..c18f000
--- /dev/null
+++ b/manage_coupons/migrations/0003_alter_coupon_discount_amount_and_more.py
@@ -0,0 +1,33 @@
+# Generated by Django 5.0.2 on 2024-08-21 10:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('manage_coupons', '0002_coupon_coupon_id'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='coupon',
+ name='discount_amount',
+ field=models.DecimalField(blank=True, decimal_places=2, help_text='Representing the amount to subtract from an invoice total (required if discount_percentage is not passed)', max_digits=10, null=True),
+ ),
+ migrations.AlterField(
+ model_name='coupon',
+ name='discount_percentage',
+ field=models.DecimalField(blank=True, decimal_places=2, help_text='A positive float larger than 0, and smaller or equal to 100, that represents the discount the coupon will apply (required if discount_amount is not passed).', max_digits=5, null=True),
+ ),
+ migrations.AlterField(
+ model_name='coupon',
+ name='max_redeems',
+ field=models.IntegerField(default=1),
+ ),
+ migrations.AlterField(
+ model_name='coupon',
+ name='valid_to',
+ field=models.DateTimeField(help_text='Datetime for the last redeemable date. After this, the coupon is invalid for new customers.'),
+ ),
+ ]
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..08cb2b7
--- /dev/null
+++ b/manage_coupons/models.py
@@ -0,0 +1,104 @@
+from decimal import Decimal
+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):
+ 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)
+ discount_amount = models.DecimalField(
+ max_digits=10, decimal_places=2, null=True, blank=True, help_text="Representing the amount to subtract from an invoice total (required if discount_percentage is not passed)"
+ )
+ discount_percentage = models.DecimalField(
+ max_digits=5, decimal_places=2, null=True, blank=True, help_text="A positive float larger than 0, and smaller or equal to 100, that represents the discount the coupon will apply (required if discount_amount is not passed)."
+ )
+ valid_from = models.DateTimeField()
+ valid_to = models.DateTimeField(help_text="Datetime for the last redeemable date. After this, the coupon is invalid for new customers.")
+ max_redeems = models.IntegerField(default=1)
+
+ class Meta:
+ db_table = "coupon"
+
+ def __str__(self):
+ return self.coupon_code
+
+ 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):
+ from goodtimes.services import StripeService
+ if not self.delete:
+ self.clean() # Call clean before saving to ensure validation
+
+ if not self.pk and not self.coupon_id:
+ amount_off = int(self.discount_amount * Decimal(100)) if self.discount_amount else None
+ percent_off = float(self.discount_percentage) if self.discount_percentage else None
+
+ result = StripeService.create_coupon(
+ amount_off=amount_off,
+ percent_off=percent_off,
+ duration="once",
+ name=self.title,
+ redeem_by=int(self.valid_to.timestamp()),
+ max_redemptions=self.max_redeems,
+ currency='gbp',
+ metadata={"local_id": self.id}
+ )
+
+ if not result["success"]:
+ raise ValueError(f"Failed to create Stripe coupon: {result['message']}")
+
+ self.coupon_code = result['data'].id
+ self.coupon_id = result["data"].id
+
+ super().save(*args, **kwargs)
+
+ # 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
+ # and (self.max_redeems == 0 or self.no_of_redeems < self.max_redeems)
+ # )
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..2d53400
--- /dev/null
+++ 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/utils.py b/manage_coupons/utils.py
new file mode 100644
index 0000000..2cacf99
--- /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."
+
+ except Exception as e:
+ error_message = f"Error creating Stripe coupon: {e}"
+ print(error_message)
+ return False, error_message
diff --git a/manage_coupons/views.py b/manage_coupons/views.py
new file mode 100644
index 0000000..754951e
--- /dev/null
+++ b/manage_coupons/views.py
@@ -0,0 +1,129 @@
+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.
+
+
+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(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)
+
+ 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.warning(request, self.error_message)
+
+ return redirect(self.success_url)
diff --git a/manage_events/admin.py b/manage_events/admin.py
index ac72d3d..fab8c59 100644
--- a/manage_events/admin.py
+++ b/manage_events/admin.py
@@ -1,5 +1,16 @@
from django.contrib import admin
-from .models import EventCategory, Venue, EventMaster, Event, EventPrincipalInteraction
+from .models import (
+ EventCategory,
+ EventShare,
+ EventView,
+ Favorites,
+ FreeUsageFeatureLimit,
+ Venue,
+ EventMaster,
+ Event,
+ EventPrincipalInteraction,
+ AgeGroups
+)
# Register your models here.
@@ -78,7 +89,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,11 +99,37 @@ 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 = ("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")
+
+
+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)
admin.site.register(Event, EventAdmin)
admin.site.register(EventCategory, EventCategoryAdmin)
admin.site.register(Venue, VenueAdmin)
admin.site.register(EventMaster, EventMasterAdmin)
+admin.site.register(AgeGroups)
+admin.site.register(FreeUsageFeatureLimit)
diff --git a/manage_events/api/filters.py b/manage_events/api/filters.py
new file mode 100644
index 0000000..856ccbe
--- /dev/null
+++ b/manage_events/api/filters.py
@@ -0,0 +1,52 @@
+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(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")
+ # 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(method="filter_age_group")
+
+ class Meta:
+ model = Event
+ fields = [
+ 'title',
+ 'location',
+ 'category',
+ 'start_date',
+ # 'end_date',
+ 'price_from',
+ 'price_to',
+ '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)
+
+ 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)
+ # 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/serializers.py b/manage_events/api/serializers.py
index 2a36c82..f6c15ea 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,
@@ -13,11 +14,18 @@ from manage_events.models import (
EventPrincipalInteraction,
EventReview,
Favorites,
+ FreeUsageFeatureLimit,
Venue,
PrincipalPreference,
)
+class FreeUsageFeatureLimitSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = FreeUsageFeatureLimit
+ fields = ['id', 'category_limit']
+
+
class EventImageSerializer(serializers.ModelSerializer):
class Meta:
model = EventImage
@@ -38,18 +46,39 @@ 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:
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
+ fields = ['id', 'name']
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)
+ # venue = VenueSerializer(read_only=True)
+ # draft = serializers.BooleanField(read_only=True)
+ # tags = TagSerializer(many=True, read_only=True)
class Meta:
model = Event
@@ -62,22 +91,29 @@ class EventListSerializer(serializers.ModelSerializer):
"from_time",
"to_time",
"category",
- "venue",
- "venue_capacity",
+ # "venue",
+ # "venue_capacity",
"image",
# "video_url",
- "entry_type",
+ # "entry_type",
"entry_fee",
"key_guest",
"age_group",
+ "link",
# "images",
# "is_favorited",
# "reviews",
- "tags",
+ # "tags",
# "principal_interaction",
- "draft",
+ # "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)
@@ -107,6 +143,9 @@ class EventDetailSerializer(serializers.ModelSerializer):
"entry_type",
"entry_fee",
"key_guest",
+ "coupon_code",
+ "coupon_description",
+ "link",
"age_group",
"images",
"is_favorited",
@@ -147,6 +186,12 @@ class EventDetailSerializer(serializers.ModelSerializer):
"status_display": interaction.get_status_display(),
}
return None
+
+ # def to_representation(self, instance):
+ # """Customize the representation of the instance."""
+ # representation = super().to_representation(instance)
+ # representation['key_guest'] = instance.get_key_guests() # Return as a list
+ # return representation
class CreateEventSerializer(serializers.ModelSerializer):
@@ -177,11 +222,21 @@ class CreateEventSerializer(serializers.ModelSerializer):
"draft",
"venue",
"tags",
+ "coupon_code",
+ "coupon_description",
+ "link"
]
+ def validate_key_guest(self, value):
+ if value and not isinstance(value, str):
+ raise serializers.ValidationError("key_guest must be a string")
+ return value
+
def create(self, validated_data):
tags = validated_data.pop("tags", None)
images_data = validated_data.pop("images", None)
+ key_guest = validated_data.pop("key_guest", None)
+
event = Event.objects.create(**validated_data)
if tags:
@@ -191,11 +246,15 @@ class CreateEventSerializer(serializers.ModelSerializer):
for image_data in images_data:
EventImage.objects.create(event=event, image=image_data)
+ if key_guest:
+ event.set_key_guests(key_guest)
+ event.save()
return event
def update(self, instance, validated_data):
tags = validated_data.pop("tags", None)
images_data = validated_data.pop("images", None)
+ key_guest = validated_data.pop("key_guest", None)
# Update fields if there is any change.
if tags is not None:
@@ -204,11 +263,13 @@ class CreateEventSerializer(serializers.ModelSerializer):
instance.tags.add(*tags)
if images_data is not None:
- # Assuming you want to add new images without deleting the old ones
- # If you want to replace them, you should delete the old images first
+ EventImage.objects.filter(event=instance).delete()
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)
@@ -216,6 +277,12 @@ class CreateEventSerializer(serializers.ModelSerializer):
return instance
+ # def to_representation(self, instance):
+ # """Customize the representation of the instance."""
+ # representation = super().to_representation(instance)
+ # representation['key_guest'] = instance.get_key_guests() # Return as a list
+ # return representation
+
class CreateVenueSerializer(serializers.ModelSerializer):
class Meta:
@@ -236,32 +303,6 @@ class IAmPrincipalLocationSerializer(serializers.ModelSerializer):
model = IAmPrincipalLocation
fields = ["latitude", "longitude"]
- def create(self, validated_data):
- principal = self.context["request"].user
- latitude = validated_data.get("latitude")
- longitude = validated_data.get("longitude")
- location = get_location_info(latitude=latitude, longitude=longitude)
- print("location: ", location)
- city = location.get("city")
- state = location.get("state")
- country = location.get("country")
- country_code = location.get("country_code")
-
- if hasattr(principal, "city"):
- principal.city = city or state # Use state as city if city is not found
- if hasattr(principal, "state"):
- principal.state = state
- if hasattr(principal, "country"):
- principal.country = country
- if hasattr(principal, "address_line1"):
- principal.address_line1 = country_code
-
- # save the principal object after making changes
- principal.save()
- return IAmPrincipalLocation.objects.create(
- principal=principal, **validated_data
- )
-
class PrincipalPreferenceSerializer(serializers.ModelSerializer):
preferred_categories = serializers.PrimaryKeyRelatedField(
diff --git a/manage_events/api/urls.py b/manage_events/api/urls.py
index 4e632f9..07fc2b4 100644
--- a/manage_events/api/urls.py
+++ b/manage_events/api/urls.py
@@ -4,13 +4,14 @@ from . import views
app_name = "manage_events_api"
urlpatterns = [
+ path('free/feature-limit/', views.FreeUsageFeatureLimitView.as_view(), name='feature-limit'),
path(
"add-event/",
views.CreateEventApi.as_view(),
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(),
@@ -56,6 +57,11 @@ urlpatterns = [
views.PrincipalPreferenceDetailView.as_view(),
name="principal-preferences",
),
+ path(
+ "preferences/",
+ views.EventPreferencesView.as_view(),
+ name="preferences",
+ ),
# Principal Location
path(
"add-location/",
@@ -111,4 +117,32 @@ 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",
+ ),
+ # For counting event shares
+ path(
+ "event//share/",
+ views.EventShareView.as_view(),
+ name="capture_event_share",
+ ),
+
+ path(
+ "age-groups/", views.AgeGroupListView.as_view(),
+ name="age_group_list"
+ ),
+
+ path("get-events/calendar/", views.EventsCalenderAPIView.as_view(), name="events-calendar"),
+
+ # event list with filter
+ path(
+ "events/",
+ 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 6c632d5..99228c2 100644
--- a/manage_events/api/views.py
+++ b/manage_events/api/views.py
@@ -1,19 +1,25 @@
+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 import resource_action
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 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
from manage_cms.api.serializers import TagSerializer
from manage_events.api.serializers import (
+ AgeGroupsSerializer,
EventDateRangeSerializer,
EventMasterSearchSerializer,
EventMasterSerializer,
@@ -22,26 +28,47 @@ from manage_events.api.serializers import (
EventCategorySerializer,
EventDetailSerializer,
EventReviewSerializer,
+ FreeUsageFeatureLimitSerializer,
IAmPrincipalLocationSerializer,
PrincipalPreferenceSerializer,
VenueSerializer,
EventListSerializer,
)
from manage_events.models import (
+ AgeGroups,
EventInteractionType,
EventMaster,
Event,
EventCategory,
EventPrincipalInteraction,
EventReview,
+ EventShare,
+ EventView,
Favorites,
+ FreeUsageFeatureLimit,
PrincipalPreference,
Venue,
)
import requests
-from manage_events.utils import haversine_one
+from manage_events.utils import haversine_one, update_principal_location
+from manage_subscriptions.models import PrincipalSubscription
+from .filters import EventFilter
+class FreeUsageFeatureLimitView(APIView):
+ authentication_classes = [JWTAuthentication]
+ permission_classes = [IsAuthenticated]
+ model = FreeUsageFeatureLimit
+ serializer_class = FreeUsageFeatureLimitSerializer
+
+ def get(self, request):
+ obj = self.model.objects.first()
+ serializer = self.serializer_class(obj)
+ return ApiResponse.success(
+ status=status.HTTP_200_OK,
+ message=constants.SUCCESS,
+ data=serializer.data,
+ )
class CreateEventApi(APIView):
authentication_classes = [JWTAuthentication]
@@ -52,7 +79,7 @@ class CreateEventApi(APIView):
serializer = CreateEventSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
- serializer.save(created_by=self.request.user)
+ serializer.save(created_by=self.request.user, principal=self.request.user)
# Add additional logic for handling other relationships (e.g., Venue)
return ApiResponse.success(
@@ -110,10 +137,20 @@ class CreateVenueApi(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
- serializer = VenueSerializer(data=request.data, context={"request": 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)
+ data["longitude"] = round(float(data["longitude"]), 8)
+
+ serializer = VenueSerializer(data=data, context={"request": request})
serializer.is_valid(raise_exception=True)
- serializer.save(created_by=self.request.user, active=True)
+ serializer.save(
+ created_by=self.request.user, principal=self.request.user, active=True
+ )
# Add additional logic for handling other relationships (e.g., Venue)
return ApiResponse.success(
@@ -123,6 +160,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.DEFAULT_FROM_EMAIL,
+# )
+# email_service.attach(filename, buffer.getvalue(), "application/pdf")
+
+# # Send the email
+# email_service.send()
+
+
class VenueDeleteAPIView(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
@@ -149,67 +200,6 @@ class VenueDeleteAPIView(APIView):
)
-class EventsAPIView(APIView):
- authentication_classes = [JWTAuthentication]
- permission_classes = [IsAuthenticated]
-
- def get(self, request, *args, **kwargs):
- filter = request.query_params.get("filter", None)
- query = request.query_params.get("query", None)
- category_id = request.query_params.get("category_id", None)
- params = [
- "expensive",
- "cheap",
- "preference",
- "today",
- "tomorrow",
- "category",
- "key_guest",
- "tags",
- ]
- if filter not in params:
- return ApiResponse.error(
- status=status.HTTP_400_BAD_REQUEST,
- message=constants.FAILURE,
- errors="No filter found",
- )
-
- try:
- if filter == "today":
- events = services.EventFilterService.filter_events_for_today()
- elif filter == "tomorrow":
- events = services.EventFilterService.filter_events_for_tomorrow()
- elif filter == "key_guest":
- events = services.EventFilterService.filter_events_by_search(
- search_query=query
- )
- elif filter == "category" and category_id is not None:
- events = services.EventFilterService.filter_events_by_category(
- int(category_id)
- )
- else:
- events = services.EventFilterService.filter_events(
- filter_type=filter, principal=request.user
- )
- serializer = EventDetailSerializer(
- events, context={"request": request}, many=True
- )
- # serializer = EventListSerializer(
- # events, context={"request": request}, many=True
- # )
- return ApiResponse.success(
- status=status.HTTP_200_OK,
- message=constants.SUCCESS,
- data=serializer.data,
- )
- except Exception as e:
- return ApiResponse.error(
- status=status.HTTP_400_BAD_REQUEST,
- message=constants.FAILURE,
- errors=str(e),
- )
-
-
class MyEventsAPIView(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
@@ -248,6 +238,9 @@ class MyEventsAPIView(APIView):
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,
@@ -445,22 +438,58 @@ class IAmPrincipalLocationAPIView(APIView):
)
def post(self, request, *args, **kwargs):
- serializer = IAmPrincipalLocationSerializer(
- data=request.data, context={"request": request}
- )
- print("serializer: ", serializer)
- if serializer.is_valid():
- serializer.save()
+ data = request.data.copy()
+
+ # Convert latitude and longitude to float and round to 8 decimal places
+ latitude = round(float(data["latitude"]), 15)
+ longitude = round(float(data["longitude"]), 15)
+
+ try:
+ principal = request.user
+ location = IAmPrincipalLocation.objects.get(principal=principal)
+
+ # Update existing location
+ location.latitude = latitude
+ location.longitude = longitude
+ location.save()
+
+ # Update principal fields using the utility function
+ update_principal_location(principal, latitude, longitude)
+
+ serializer = IAmPrincipalLocationSerializer(
+ location, context={"request": request}
+ )
return ApiResponse.success(
status=status.HTTP_200_OK,
message=constants.SUCCESS,
data=serializer.data,
)
- return ApiResponse.error(
- status=status.HTTP_400_BAD_REQUEST,
- message=constants.FAILURE,
- errors=serializer.errors,
- )
+
+ except IAmPrincipalLocation.DoesNotExist:
+ # Create a new location object
+ location = IAmPrincipalLocation.objects.create(
+ principal=principal, latitude=latitude, longitude=longitude
+ )
+
+ # Update principal fields using the utility function
+ update_principal_location(principal, latitude, longitude)
+
+ serializer = IAmPrincipalLocationSerializer(
+ location, context={"request": request}
+ )
+ return ApiResponse.success(
+ status=status.HTTP_201_CREATED,
+ message=constants.SUCCESS,
+ data=serializer.data,
+ )
+
+ except Exception as e:
+ print(f"Error occurred while saving location: {e}")
+ return ApiResponse.error(
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ message=constants.FAILURE,
+ errors=str(e),
+ )
class PrincipalPreferenceView(APIView):
@@ -468,6 +497,27 @@ class PrincipalPreferenceView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request, *args, **kwargs):
+
+ principal = request.user
+ # Check if the principal has a subscription
+ if not PrincipalSubscription.has_principal_subscription(principal):
+ # Get the preferred categories from the request data
+ preferred_categories = request.data.get("preferred_categories", [])
+
+ # Get the category limit for free usage
+ category_limit = FreeUsageFeatureLimit.get_category_limit()
+
+ # Check if the principal is an event user and has exceeded the category limit
+ if principal.principal_type.name == resource_action.PRINCIPAL_TYPE_EVENT_USER and len(preferred_categories) > category_limit:
+ # Create an error message indicating that a paid subscription is required
+ error_message = f"Upgrade to paid subscription to select more than {category_limit} categories."
+
+ return ApiResponse.error(
+ status=status.HTTP_400_BAD_REQUEST,
+ message=error_message,
+ errors=error_message,
+ )
+
serializer = PrincipalPreferenceSerializer(
data=request.data, context={"request": request}
)
@@ -506,6 +556,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, context={"request": request})
+ return ApiResponse.success(
+ data=serializer.data, message=constants.SUCCESS, status=status.HTTP_200_OK
+ )
+
+
class EventMasterSearchAPIView(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
@@ -626,9 +691,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) & (
- 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)
@@ -652,7 +720,7 @@ class EventFilterByLocationAPIView(APIView):
venues_within_range.append(venue.id)
print("venues_within_range: ", venues_within_range)
# venues_data = [venue_to_dict(venue) for venue in venues_within_range]
- events = Event.objects.filter(venue__id__in=venues_within_range)
+ events = events_queryset.filter(venue__id__in=venues_within_range)
# Serialize and return the filtered events
serializer = EventDetailSerializer(
@@ -846,3 +914,270 @@ 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,
+ )
+
+
+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,
+ )
+
+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 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]
+ serializer_class = EventListSerializer
+ filter_backends = [DjangoFilterBackend]
+ filterset_class = EventFilter
+
+ def get_queryset(self):
+ """
+ Returns a queryset of events filtered by the user's preferences and subscription status.
+ """
+ principal = self.request.user
+
+ # Filter the base queryset to only include active, non-draft, non-deleted events with an end date in the future
+ queryset = Event.objects.filter(
+ active=True,
+ draft=False,
+ deleted=False,
+ end_date__gte=timezone.now().date()
+ )
+
+ # If no filter is applied and the user does not have a subscription,
+ # only show events that match the user's preferred categories
+ if not self.request.query_params or not PrincipalSubscription.has_principal_subscription(principal):
+ # Get the user's preferred categories
+ 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(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", "")
+ ordering = self.request.query_params.get("sort", "")
+
+ # Handle nearest location sorting
+ if latitude and longitude and "nearest" in ordering:
+ queryset = self.apply_nearest_filter(queryset, latitude, longitude)
+
+ # Handle popularity and latest sorting
+ queryset = self.apply_sorting(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_sorting(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
+ if "latest" in ordering_fields:
+ queryset = queryset.order_by("start_date")
+
+ if "price" in ordering_fields:
+ queryset = queryset.order_by('entry_fee')
+
+ 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)
+ return self.get_paginated_response(serializer.data)
+
+ serializer = self.get_serializer(queryset, many=True)
+ 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/manage_events/forms.py b/manage_events/forms.py
index 02d2e90..070f1cd 100644
--- a/manage_events/forms.py
+++ b/manage_events/forms.py
@@ -1,5 +1,6 @@
from django import forms
-from manage_events.models import EventMaster, Event, EventCategory, Venue
+from accounts.models import IAmPrincipal, IAmPrincipalExtendedData
+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 _
@@ -16,83 +17,121 @@ class EventCategoryForm(forms.ModelForm):
class EventForm(forms.ModelForm):
+ principal = forms.ModelChoiceField(
+ queryset=IAmPrincipal.objects.select_related("extended_data").filter(
+ extended_data__is_onboarded=True,
+ extended_data__is_transferred=False
+ ),
+ label="Non-transfer user list",
+ required=True
+ )
+ venue = forms.ModelChoiceField(
+ queryset=Venue.objects.none(),
+ label="venue",
+ required=True
+ )
+ image = forms.ImageField(label="Thumbnail")
+ event_images = forms.ImageField(label="Event Images")
+ age_group = forms.ChoiceField(
+ choices=[],
+ label="Age Group",
+ required=True
+ )
+
class Meta:
model = Event
fields = [
+ "principal",
+ "venue",
"title",
# "event_master",
"description",
+ "link",
"image",
- "status",
+ "event_images",
+ # "status",
"start_date",
"end_date",
"from_time",
"to_time",
"category",
- "venue",
"venue_capacity",
- "video_url",
+ # "video_url",
"entry_type",
"entry_fee",
"key_guest",
"age_group",
+ "coupon_code",
+ "coupon_description",
"tags",
"draft",
"active",
"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.TextInput(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]
+
+ # Set the initial value for age_group if instance is provided
+ print(f"age group is {self.instance.age_group}")
+ 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)
+ 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:
@@ -101,23 +140,35 @@ class EventMasterForm(forms.ModelForm):
class VenueForm(forms.ModelForm):
+ principal = forms.ModelChoiceField(
+ queryset=IAmPrincipal.objects.select_related("extended_data").filter(
+ extended_data__is_onboarded=True,
+ extended_data__is_transferred=False
+ ),
+ label="Non-transfer user list",
+ required=True
+ )
+ image = forms.ImageField(required=True)
+ postcode = forms.CharField(required=True, max_length=10)
+ latitude = forms.DecimalField(
+ widget=forms.NumberInput()
+ )
+ longitude = forms.DecimalField(
+ widget=forms.NumberInput()
+ )
+
class Meta:
model = Venue
fields = [
+ "principal",
"title",
- "description",
"address",
+ "postcode",
"image",
- "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/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
new file mode 100644
index 0000000..c2117bc
--- /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, 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)
+
+ 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.DEFAULT_FROM_EMAIL,
+ )
+ email_message.attach(filename, pdf_data, "application/pdf")
+ email_message.send()
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/management/commands/test_facebook_api.py b/manage_events/management/commands/test_facebook_api.py
new file mode 100644
index 0000000..5f0a2e3
--- /dev/null
+++ b/manage_events/management/commands/test_facebook_api.py
@@ -0,0 +1,30 @@
+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."))
+
+ 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}"
+
+ 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..a5c689e
--- /dev/null
+++ b/manage_events/management/commands/test_instagram_api.py
@@ -0,0 +1,33 @@
+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."))
+
+ 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}"
+
+ 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/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/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/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/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/migrations/0012_event_coupon_code_event_coupon_description.py b/manage_events/migrations/0012_event_coupon_code_event_coupon_description.py
new file mode 100644
index 0000000..4fc4935
--- /dev/null
+++ b/manage_events/migrations/0012_event_coupon_code_event_coupon_description.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.0.2 on 2024-06-20 06:54
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("manage_events", "0011_alter_event_entry_type"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="event",
+ name="coupon_code",
+ field=models.CharField(blank=True, max_length=255, null=True),
+ ),
+ migrations.AddField(
+ model_name="event",
+ name="coupon_description",
+ field=models.TextField(blank=True, null=True),
+ ),
+ ]
diff --git a/manage_events/migrations/0013_venue_principal.py b/manage_events/migrations/0013_venue_principal.py
new file mode 100644
index 0000000..6a32b53
--- /dev/null
+++ b/manage_events/migrations/0013_venue_principal.py
@@ -0,0 +1,21 @@
+# Generated by Django 5.0.2 on 2024-06-25 17:04
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('manage_events', '0012_event_coupon_code_event_coupon_description'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='venue',
+ name='principal',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='venues_principal', to=settings.AUTH_USER_MODEL),
+ ),
+ ]
diff --git a/manage_events/migrations/0014_event_principal.py b/manage_events/migrations/0014_event_principal.py
new file mode 100644
index 0000000..15109c4
--- /dev/null
+++ b/manage_events/migrations/0014_event_principal.py
@@ -0,0 +1,21 @@
+# Generated by Django 5.0.2 on 2024-06-25 17:09
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('manage_events', '0013_venue_principal'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='event',
+ name='principal',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='events_principal', to=settings.AUTH_USER_MODEL),
+ ),
+ ]
diff --git a/manage_events/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',
+ },
+ ),
+ ]
diff --git a/manage_events/migrations/0016_freeusagefeaturelimit.py b/manage_events/migrations/0016_freeusagefeaturelimit.py
new file mode 100644
index 0000000..0d87275
--- /dev/null
+++ b/manage_events/migrations/0016_freeusagefeaturelimit.py
@@ -0,0 +1,25 @@
+# Generated by Django 5.0.2 on 2024-08-05 10:27
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('manage_events', '0015_agegroups'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='FreeUsageFeatureLimit',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('category_limit', models.PositiveIntegerField(default=3, help_text='The maximum number of categories that free app users can select.')),
+ ],
+ options={
+ 'verbose_name': 'Free Usage Feature Limit',
+ 'verbose_name_plural': 'Free Usage Feature Limits',
+ 'db_table': 'free_usage_feature_limit',
+ },
+ ),
+ ]
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/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 4f876cb..9be9dd4 100644
--- a/manage_events/models.py
+++ b/manage_events/models.py
@@ -1,12 +1,33 @@
from django.db import models
+from django.core.exceptions import ValidationError
from accounts.models import BaseModel, IAmPrincipal
from django.db import transaction
from taggit.managers import TaggableManager
-# from django.contrib.gis.db import models as gis_models
+class FreeUsageFeatureLimit(models.Model):
+ category_limit = models.PositiveIntegerField(
+ default=3,
+ help_text="The maximum number of categories that free app users can select."
+ )
+
+ class Meta:
+ db_table = "free_usage_feature_limit"
+ verbose_name = "Free Usage Feature Limit"
+ verbose_name_plural = "Free Usage Feature Limits"
+
+ def __str__(self):
+ return f"Free usage limit: {self.category_limit} categories"
+
+ def save(self, *args, **kwargs):
+ if not self.pk and FreeUsageFeatureLimit.objects.exists():
+ raise ValidationError("There can only be one FreeUsageFeatureLimit instance.")
+ return super().save(*args, **kwargs)
+
+ @classmethod
+ def get_category_limit(cls):
+ return cls.objects.values_list('category_limit', flat=True).first()
-# Create your models here.
class EventCategory(BaseModel):
title = models.CharField(max_length=255)
image = models.ImageField(upload_to="event_category", null=True, blank=True)
@@ -18,6 +39,7 @@ class EventCategory(BaseModel):
class Venue(BaseModel):
+ principal = models.ForeignKey(IAmPrincipal, related_name="venues_principal", on_delete=models.CASCADE, null=True)
title = models.CharField(max_length=255)
description = models.TextField(null=True, blank=True)
address = models.TextField(null=True, blank=True)
@@ -29,10 +51,20 @@ 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"
+
+ def __str__(self):
+ return self.name
+
class EventStatus(models.TextChoices):
UPCOMING = "upcoming", "Upcoming"
@@ -55,6 +87,11 @@ class EventMaster(BaseModel):
class Event(BaseModel):
+ ENTRY_TYPE_CHOICES = [
+ ("free", "Free"),
+ ("paid", "Paid"),
+ ]
+ principal = models.ForeignKey(IAmPrincipal, related_name="events_principal", on_delete=models.CASCADE, null=True)
title = models.CharField(max_length=255)
category = models.ForeignKey(EventCategory, on_delete=models.CASCADE)
event_master = models.ForeignKey(
@@ -75,15 +112,37 @@ class Event(BaseModel):
video_url = models.URLField(max_length=200, blank=True, null=True)
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.
+ )
key_guest = models.TextField(blank=True, null=True)
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)
+ coupon_code = models.CharField(max_length=255, blank=True, null=True)
+ coupon_description = models.TextField(blank=True, null=True)
+ link = models.URLField(max_length=255, blank=True, null=True)
+
+ def increment_shares(self):
+ self.social_media_shares_count += 1
+ self.save()
+
+ def set_key_guests(self, guests):
+ """Set the key guests as a comma-seperated string."""
+ if isinstance(guests, list):
+ self.key_guest = ",".join(guests)
+ elif isinstance(guests, str):
+ self.key_guest = guests
+ else:
+ raise ValueError("Guests must be a comma-seperated string")
+
+ def get_key_guests(self):
+ """Return the key guests as a list of strings."""
+ return self.key_guest.split(",") if self.key_guest else []
def __str__(self):
return self.title
@@ -132,7 +191,7 @@ class PrincipalPreference(BaseModel):
)
def __str__(self):
- return str(self.preferred_categories)
+ return str(self.preferred_categories.name)
class Meta:
db_table = "user_preference"
@@ -173,3 +232,26 @@ 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}"
+
+
+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
new file mode 100644
index 0000000..70c0d75
--- /dev/null
+++ b/manage_events/report.py
@@ -0,0 +1,378 @@
+from django.db.models import Count, Q
+from django.utils import timezone
+from datetime import timedelta
+from reportlab.lib.pagesizes import letter
+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
+from reportlab.platypus import (
+ SimpleDocTemplate,
+ Table,
+ TableStyle,
+ Paragraph,
+ Spacer,
+ PageBreak,
+ Image,
+)
+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 accounts.models import IAmPrincipalType
+from manage_events.models import Event, EventInteractionType, EventShare, EventView
+import os
+
+
+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 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()
+ 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, start_date, end_date):
+ # 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
+ )
+
+ report_data = []
+ for event in events:
+ # Collecting individual counts for each event
+ 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.filter(active=True, deleted=False).count()
+ views_count = event.views.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")
+ .annotate(share_count=Count("principal"))
+ )
+ shares_data = {
+ User.objects.get(id=share["principal"]).get_full_name(): share[
+ "share_count"
+ ]
+ 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": 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,
+ }
+ )
+
+ return report_data
+
+
+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()
+ # pdf = canvas.Canvas(buffer, pagesize=letter)
+ pdf = SimpleDocTemplate(buffer, pagesize=letter)
+ width, height = letter
+ elements = []
+
+ styles = getSampleStyleSheet()
+
+ custom_style = ParagraphStyle(
+ name="Custom",
+ parent=styles["Normal"],
+ fontName="Helvetica",
+ fontSize=14,
+ 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(
+ 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_email = Paragraph(f"Contact Email - {user.email}", styles["Title"])
+
+ elements.extend(
+ [
+ title,
+ Spacer(1, 12),
+ report_for_month,
+ Spacer(1, 12),
+ organiser_name,
+ Spacer(1, 12),
+ contact_email,
+ Spacer(1, 72), # Add space before the logo
+ ]
+ )
+
+ # Insert company logo
+ logo_path = settings.LOGO_PATH
+ print("logo_path: ", logo_path)
+ 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
+ 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)}"
+ )
+ 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):
+ data.append(
+ [
+ idx,
+ event["event_name"],
+ event["event_type"],
+ event["event_date"],
+ ]
+ )
+
+ table = Table(data, colWidths=[50, 250, 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["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 Favorites - {favorites_count}", custom_style)
+ )
+ elements.append(Spacer(1, 12))
+ elements.append(
+ 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)
+ 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]
+
+ # 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"],
+ ]
+
+ for i in range(5):
+ location = top_locations[i][0] if i < len(top_locations) 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])
+ 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:
+ 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))
+
+ 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']}"
+
+ 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, 48))
+
+ pie_data = [
+ event["views_count"],
+ event["social_media_shares"],
+ event["favorites_count"],
+ event["interested_count"],
+ event["going_count"],
+ ]
+ pie_labels = ["Views", "Shares", "Favorites", "Interested", "Going"]
+
+ drawing = Drawing(300, 200)
+ pie = Pie()
+ pie.data = pie_data
+ pie.labels = pie_labels
+ pie.width = 150
+ pie.height = 150
+ pie.x = 75
+ pie.y = 25
+ drawing.add(pie)
+ elements.append(drawing)
+ elements.append(PageBreak())
+ pdf.build(elements, onFirstPage=add_page_number, onLaterPages=add_page_number)
+ 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..1f52b7b 100644
--- a/manage_events/urls.py
+++ b/manage_events/urls.py
@@ -89,4 +89,16 @@ 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(),
+ name="generate_event_report",
+ ),
+
+ path("post-to-social-media///", views.SocialMediaPostView.as_view(), name="social_media_post")
]
diff --git a/manage_events/utils.py b/manage_events/utils.py
index 1037987..8600c50 100644
--- a/manage_events/utils.py
+++ b/manage_events/utils.py
@@ -1,4 +1,5 @@
import math
+from accounts.models import IAmPrincipal
from manage_events.models import Event, Venue
from django.utils.timezone import now
import googlemaps
@@ -104,3 +105,23 @@ def get_location_info(latitude, longitude):
}
else:
return {}
+
+
+def update_principal_location(principal, latitude, longitude):
+ location_data = get_location_info(latitude=latitude, longitude=longitude)
+
+ city = location_data.get("city")
+ state = location_data.get("state")
+ country = location_data.get("country")
+ country_code = location_data.get("country_code")
+
+ if hasattr(principal, "city"):
+ principal.city = city or state
+ if hasattr(principal, "state"):
+ principal.state = state
+ if hasattr(principal, "country"):
+ principal.country = country
+ if hasattr(principal, "address_line1"):
+ principal.address_line1 = country_code
+
+ principal.save()
diff --git a/manage_events/views.py b/manage_events/views.py
index 5952aca..d1899a4 100644
--- a/manage_events/views.py
+++ b/manage_events/views.py
@@ -1,5 +1,8 @@
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 (
EventMasterForm,
EventCategoryForm,
@@ -7,12 +10,14 @@ 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
from django.contrib import messages
from goodtimes import constants
+from django.contrib.auth import get_user_model
+from datetime import date
# Create your views here.
@@ -221,7 +226,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
@@ -256,6 +261,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()
@@ -265,23 +273,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)
- form.save()
+ 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)
@@ -332,24 +363,26 @@ class EventDetailView(generic.DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_name"] = self.page_name
- event_id = self.object.id # Get the current event's ID
+ event = self.object # Get the current event's ID
# Separate count for interested and going
interested_count = EventPrincipalInteraction.objects.filter(
- event_id=event_id, status="interested"
+ event=event, status="interested"
).count()
going_count = EventPrincipalInteraction.objects.filter(
- event_id=event_id, status="going"
+ event=event, status="going"
).count()
context["interested_count"] = interested_count
context["going_count"] = going_count
+ today = date.today()
+ context["publish"] = not event.draft and event.active and event.end_date >= today
# Reviews for the event
- context["reviews"] = self.object.reviews.all()
+ context["reviews"] = event.reviews.all()
# Images of the event
- context["images"] = self.object.event_images.all()
+ context["images"] = event.event_images.all()
return context
@@ -413,6 +446,7 @@ class VenueCreateOrUpdateView(LoginRequiredMixin, generic.View):
def get(self, request, *args, **kwargs):
self.object = self.get_object()
+ print(f"self.object is {self.object}")
# If an object is found, change action to ACTION_UPDATE
if self.object is not None:
@@ -424,6 +458,7 @@ class VenueCreateOrUpdateView(LoginRequiredMixin, generic.View):
def post(self, request, *args, **kwargs):
self.object = self.get_object()
+ print(f"form data is {request.POST} and self.object is {self.object}")
# If an object is found, change action to ACTION_UPDATE
if self.object is not None:
@@ -434,7 +469,10 @@ class VenueCreateOrUpdateView(LoginRequiredMixin, generic.View):
print(form.errors)
context = self.get_context_data(form=form)
return render(request, self.template_name, context=context)
- form.save()
+
+ instance = form.save()
+ instance.created_by = form.cleaned_data.get("principal")
+ instance.save()
messages.success(self.request, self.get_success_message())
return redirect(self.success_url)
@@ -477,3 +515,124 @@ class VenueDeleteView(LoginRequiredMixin, generic.View):
messages.success(request, self.error_message)
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, active=True)
+ 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, JsonResponse
+
+
+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)
+
+ # Get the user
+ user = get_object_or_404(User, id=user_id)
+
+ # Generate the PDF
+ 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")
+ 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 = []
+ success_messages = []
+
+ try:
+ event = Event.objects.get(id=event_id)
+ except Event.DoesNotExist:
+ errors.append("Event does not exist")
+ 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 JsonResponse({
+ 'message': "Error in posting to social media",
+ 'errors': errors,
+ 'success_messages': success_messages
+ }, status=400)
+
+ 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']:
+ 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']:
+ 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']:
+ 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 JsonResponse({'message': 'Post Successful', 'errors': errors, 'success_messages': success_messages})
+
+ 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/manage_notifications/management/commands/interested_going.py b/manage_notifications/management/commands/interested_going.py
index ce33b70..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):
@@ -62,8 +70,8 @@ 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,
).select_related("principal", "event")
diff --git a/manage_notifications/management/commands/one_week_alert.py b/manage_notifications/management/commands/one_week_alert.py
index 5740382..4dfbb9d 100644
--- a/manage_notifications/management/commands/one_week_alert.py
+++ b/manage_notifications/management/commands/one_week_alert.py
@@ -75,26 +75,30 @@ class Command(BaseCommand):
message = f"Your subscription is going to expire in {days_before} days."
for principal in eligible_principals:
- try:
- player_id = principal.principal.player_id
- except AttributeError:
- continue
-
notification_title = "Subscription Expiry 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.principal,
title=notification_title,
message=message,
notification_category=NotificationCategoryChoices.SUBSCRIPTION,
)
+
+ if principal.is_enabled:
+ try:
+ player_id = principal.principal.player_id
+ except AttributeError:
+ continue
+
+ notification_payload = {
+ "headings": {"en": notification_title},
+ "contents": {"en": message},
+ "include_player_ids": [player_id],
+ }
+ response = client.send_notification(notification_payload)
+
+ # Save the notification in the database
in_app_notification.save()
+
self.stdout.write(
self.style.SUCCESS(
f"Notifications for {days_before} days sent successfully"
@@ -106,8 +110,7 @@ class Command(BaseCommand):
return IAmPrincipalNotificationSettings.objects.filter(
principal__principal_subscription__end_date=target_date,
principal__principal_subscription__status=SubscriptionStatus.ACTIVE,
- principal__principal_subscription__cancelled=False,
principal__principal_subscription__deleted=False,
notification_category=NotificationCategoryChoices.SUBSCRIPTION,
- is_enabled=True,
+ # is_enabled=True,
).select_related("principal")
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.",
+ )
diff --git a/manage_subscriptions/admin.py b/manage_subscriptions/admin.py
index d0c6816..c3fe4ca 100644
--- a/manage_subscriptions/admin.py
+++ b/manage_subscriptions/admin.py
@@ -1,31 +1,19 @@
from django.contrib import admin
from .models import (
- Plan,
PrincipalSubscription,
Subscription,
WebhookEvent,
) # Update this with the correct import path for your models
-# Plan ModelAdmin
-class PlanAdmin(admin.ModelAdmin):
- list_display = ("id", "title", "days") # Include 'id' field here
- search_fields = ("title",) # Add search functionality by title
-
-
-# Register Plan with the admin site
-admin.site.register(Plan, PlanAdmin)
-
-
# Subscription ModelAdmin
class SubscriptionAdmin(admin.ModelAdmin):
- list_display = ("id", "title", "plan", "amount") # Include 'id' field here
- list_select_related = ("plan",) # Optimizes queries for the plan field
+ list_display = ("id", "title", "interval", "amount") # Include 'id' field here
+ list_select_related = ("interval",) # Optimizes queries for the interval field
search_fields = (
"title",
- "plan__title",
- ) # Add search functionality by title and plan's title
- raw_id_fields = ("plan",) # Use a raw ID widget for the plan ForeignKey field
+ "interval",
+ ) # Add search functionality by title and interval's title
# Register Subscription with the admin site
@@ -47,7 +35,7 @@ class PrincipalSubscriptionAdmin(admin.ModelAdmin):
"is_paid",
"auto_renew",
"status",
- "cancelled",
+ # "cancelled",
) # Enable filtering by these fields
search_fields = (
"subscription__title",
@@ -63,7 +51,6 @@ class PrincipalSubscriptionAdmin(admin.ModelAdmin):
admin.site.register(PrincipalSubscription, PrincipalSubscriptionAdmin)
-
@admin.register(WebhookEvent)
class WebhookEventAdmin(admin.ModelAdmin):
list_display = ("event_id", "received_at", "event_type", "processed_at", "status")
diff --git a/manage_subscriptions/api/urls.py b/manage_subscriptions/api/urls.py
index fb63bae..22e94f0 100644
--- a/manage_subscriptions/api/urls.py
+++ b/manage_subscriptions/api/urls.py
@@ -1,8 +1,5 @@
from django.urls import path
from . import views
-from rest_framework_simplejwt.views import (
- TokenRefreshView,
-)
urlpatterns = [
diff --git a/manage_subscriptions/api/views.py b/manage_subscriptions/api/views.py
index 05cd2e6..56deb6e 100644
--- a/manage_subscriptions/api/views.py
+++ b/manage_subscriptions/api/views.py
@@ -1,5 +1,6 @@
from datetime import timedelta
import datetime
+import json
from django.db import transaction
from django.shortcuts import get_object_or_404
from django.utils import timezone
@@ -7,9 +8,7 @@ from rest_framework import status
from rest_framework.views import APIView
from django.conf import settings
import stripe
-from accounts.models import IAmPrincipal
-import json
-from goodtimes import constants, services
+from goodtimes import constants
from manage_subscriptions.models import (
Subscription,
PrincipalSubscription,
@@ -35,7 +34,11 @@ from .serializers import PrincipalSubscriptionSerializer
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from rest_framework.response import Response
-from goodtimes.webhook import PaymentProcessingService
+from goodtimes.webhook.payment_processing_service import PaymentProcessingService
+import logging
+
+
+logger = logging.getLogger(__name__)
class CreatePrincipalSubscriptionApi(APIView):
@@ -118,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")
@@ -172,83 +133,115 @@ 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
+
try:
+ # Construct Stripe event
event = stripe.Event.construct_from(json.loads(payload), stripe.api_key)
event_id = event["id"]
event_type = event["type"]
- principal_id = event["data"]["object"]["metadata"]["principal"]
+ 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
+
+ # 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={
"event_type": event_type,
- "event_payload": json.loads(payload),
+ "event_payload": event,
},
)
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.",
)
- # 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)
+ # 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()
- webhook_event = WebhookEvent.objects.get(event_id=event_id)
+
+ # Mark event as successfully processed
webhook_event.status = "processed"
- webhook_event.processed_at = timezone.now() # Make sure to import timezone
+ 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 for event: {str(e)}")
+ return ApiResponse.error(
+ status=status.HTTP_400_BAD_REQUEST,
+ message="Invalid signature.",
+ errors=str(e),
)
except ValueError as e:
- # Invalid payload
+ logger.error(f"Invalid payload for event: {str(e)}")
return ApiResponse.error(
status=status.HTTP_400_BAD_REQUEST,
- message="Invalid payload",
- errors=str(e),
- )
- except stripe.error.SignatureVerificationError as e:
- # Invalid signature
- return ApiResponse.error(
- status=status.HTTP_400_BAD_REQUEST,
- message="Invalid signature",
- errors=str(e),
- )
- except Transaction.DoesNotExist:
- # Handle case where the transaction does not exist
- return ApiResponse.error(
- status=status.HTTP_404_NOT_FOUND, message="Transaction not found"
- )
- except Exception as e:
- 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="Invalid payload.",
errors=str(e),
)
- def _has_active_principal_subscription(self, principal_id):
- return PrincipalSubscription.objects.filter(
- principal__id=principal_id,
- active=True,
- deleted=False,
- is_paid=True,
- end_date__gte=timezone.now().date(),
- ).exists()
+ except stripe.error.InvalidRequestError as e:
+ logger.error(f"Invalid request for event: {str(e)}")
+ return ApiResponse.error(
+ 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"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="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):
@@ -281,3 +274,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.stripe_subscription_id:
+ # 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/forms.py b/manage_subscriptions/forms.py
index 3579fc4..05dc2fc 100644
--- a/manage_subscriptions/forms.py
+++ b/manage_subscriptions/forms.py
@@ -1,43 +1,64 @@
from django import forms
-from manage_subscriptions.models import PrincipalSubscription, Subscription, Plan
-
-
-class PlanForm(forms.ModelForm):
- class Meta:
- model = Plan
- fields = ["title", "days"] # Include all fields you want from the model
-
- # You can add custom validation for Plan fields here if needed
- # Example:
- # def clean_title(self):
- # title = self.cleaned_data.get('title')
- # # Add your validation logic here
- # return title
-
+from accounts.models import IAmPrincipalType
+from manage_subscriptions.models import (
+ PrincipalSubscription,
+ Subscription,
+)
class SubscriptionForm(forms.ModelForm):
class Meta:
model = Subscription
fields = [
"title",
- "plan",
+ "short_description",
+ "long_description",
+ "interval",
+ "interval_count",
"high_amount",
"amount",
- "short_description",
- # "long_description",
- # "image",
"principal_types",
"referral_percentage",
- ] # Include all fields you want from the model
+ "active",
+ "is_free",
+ ]
+ 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 SubscriptionUpdateForm(forms.ModelForm):
+ class Meta:
+ model = Subscription
+ fields = [
+ "title",
+ "short_description",
+ "long_description",
+ "referral_percentage",
+ "active",
+ "is_free",
+ ]
class PrincipalSubscriptionForm(forms.ModelForm):
class Meta:
model = PrincipalSubscription
- fields = "__all__" # Includes all fields from the model
+ fields = [
+ "subscription",
+ "principal",
+ "status",
+ "start_date",
+ "end_date",
+ "grace_period_end_date",
+ "comments",
+ "coupon_code"
+ ] # Includes all fields from the model
widgets = {
"start_date": forms.DateInput(attrs={"type": "date"}),
"end_date": forms.DateInput(attrs={"type": "date"}),
"grace_period_end_date": forms.DateInput(attrs={"type": "date"}),
- "cancelled_date_time": forms.DateTimeInput(attrs={"type": "datetime"}),
}
+
+
diff --git a/manage_subscriptions/migrations/0008_subscription_is_free.py b/manage_subscriptions/migrations/0008_subscription_is_free.py
new file mode 100644
index 0000000..d86bd47
--- /dev/null
+++ b/manage_subscriptions/migrations/0008_subscription_is_free.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.2 on 2024-06-25 07:28
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('manage_subscriptions', '0007_alter_subscription_referral_percentage'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='subscription',
+ name='is_free',
+ field=models.BooleanField(default=False, help_text='Indicates whether this subscription is free and only accessible by administrators, not visible to regular users.'),
+ ),
+ ]
diff --git a/manage_subscriptions/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/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/migrations/0011_subscription_product_id.py b/manage_subscriptions/migrations/0011_subscription_product_id.py
new file mode 100644
index 0000000..5a478b1
--- /dev/null
+++ b/manage_subscriptions/migrations/0011_subscription_product_id.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.2 on 2024-08-18 18:55
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('manage_subscriptions', '0010_principalsubscription_comments_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='subscription',
+ name='product_id',
+ field=models.CharField(blank=True, max_length=255, null=True),
+ ),
+ ]
diff --git a/manage_subscriptions/migrations/0012_subscription_interval_subscription_interval_count.py b/manage_subscriptions/migrations/0012_subscription_interval_subscription_interval_count.py
new file mode 100644
index 0000000..2a9979b
--- /dev/null
+++ b/manage_subscriptions/migrations/0012_subscription_interval_subscription_interval_count.py
@@ -0,0 +1,24 @@
+# Generated by Django 5.0.2 on 2024-08-19 09:17
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('manage_subscriptions', '0011_subscription_product_id'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='subscription',
+ name='interval',
+ field=models.CharField(choices=[('month', 'month'), ('day', 'day'), ('week', 'week'), ('year', 'year')], default='month', max_length=10),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='subscription',
+ name='interval_count',
+ field=models.IntegerField(default=1),
+ ),
+ ]
diff --git a/manage_subscriptions/migrations/0013_remove_subscription_plan_and_more.py b/manage_subscriptions/migrations/0013_remove_subscription_plan_and_more.py
new file mode 100644
index 0000000..be3ba14
--- /dev/null
+++ b/manage_subscriptions/migrations/0013_remove_subscription_plan_and_more.py
@@ -0,0 +1,43 @@
+# Generated by Django 5.0.2 on 2024-08-21 18:10
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('manage_subscriptions', '0012_subscription_interval_subscription_interval_count'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='subscription',
+ name='plan',
+ ),
+ migrations.RemoveField(
+ model_name='stripeproduct',
+ name='created_by',
+ ),
+ migrations.RemoveField(
+ model_name='stripeproduct',
+ name='modified_by',
+ ),
+ migrations.RemoveField(
+ model_name='subscription',
+ name='stripe_product',
+ ),
+ migrations.RemoveField(
+ model_name='principalsubscription',
+ name='cancelled',
+ ),
+ migrations.RemoveField(
+ model_name='principalsubscription',
+ name='is_stripe_subscription',
+ ),
+ migrations.DeleteModel(
+ name='Plan',
+ ),
+ migrations.DeleteModel(
+ name='StripeProduct',
+ ),
+ ]
diff --git a/manage_subscriptions/migrations/0014_alter_subscription_long_description.py b/manage_subscriptions/migrations/0014_alter_subscription_long_description.py
new file mode 100644
index 0000000..d117a17
--- /dev/null
+++ b/manage_subscriptions/migrations/0014_alter_subscription_long_description.py
@@ -0,0 +1,19 @@
+# Generated by Django 5.0.2 on 2024-08-25 10:39
+
+import django_quill.fields
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('manage_subscriptions', '0013_remove_subscription_plan_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='subscription',
+ name='long_description',
+ field=django_quill.fields.QuillField(default=''),
+ ),
+ ]
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/manage_subscriptions/models.py b/manage_subscriptions/models.py
index ae99b31..a15766d 100644
--- a/manage_subscriptions/models.py
+++ b/manage_subscriptions/models.py
@@ -1,35 +1,42 @@
+from datetime import timedelta
+from django.utils import 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 _
-
-# Create your models here.
-
-
-class Plan(BaseModel):
- title = models.CharField(max_length=255)
- days = models.PositiveIntegerField()
-
- class Meta:
- db_table = "plan"
-
- def __str__(self):
- return self.title
+from django_quill.fields import QuillField
class Subscription(BaseModel):
+ MONTH = "month"
+ DAY = "day"
+ WEEK = "week"
+ YEAR = "year"
+
+ INTERVAL_TYPES = [
+ (MONTH, "month"),
+ (DAY, "day"),
+ (WEEK, "week"),
+ (YEAR, "year"),
+ ]
title = models.CharField(max_length=255)
+ price_id = models.CharField(max_length=255, blank=True, null=True)
+ product_id = models.CharField(max_length=255, blank=True, null=True)
short_description = models.CharField(max_length=255, null=True, blank=True)
- long_description = models.TextField(null=True, blank=True)
+ long_description = QuillField()
image = models.ImageField(upload_to="subscription_img", null=True, blank=True)
- plan = models.ForeignKey(
- Plan, related_name="subscription_plan", on_delete=models.CASCADE
- )
+ interval = models.CharField(max_length=10, choices=INTERVAL_TYPES)
+ interval_count = models.IntegerField(default=1)
high_amount = models.DecimalField(max_digits=14, decimal_places=2, default=0.00)
amount = models.DecimalField(max_digits=14, decimal_places=2, default=0.00)
principal_types = models.ManyToManyField(
IAmPrincipalType, related_name="principal_type_subscriptions", blank=True
)
referral_percentage = models.DecimalField(max_digits=5, decimal_places=2)
+ is_free = models.BooleanField(
+ default=False,
+ help_text="Indicates whether this subscription is free and only accessible by administrators, not visible to regular users.",
+ )
class Meta:
db_table = "subscription"
@@ -37,6 +44,90 @@ 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):
+ from goodtimes.services import StripeService
+
+ 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
+ self.high_amount = 0.00
+ self.amount = 0.00
+ self.price_id = None
+ self.product_id = None
+ else:
+ 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"]:
+ raise Exception(price['message'])
+
+ # 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"]:
+ raise Exception(product['message'])
+
+ # Update product data if it has changed
+ if product["data"].name != self.title or product["data"].description != self.short_description:
+ StripeService.update_product(
+ product_id=self.product_id,
+ name=self.title,
+ description=self.short_description
+ )
+ else:
+ print("new pricde create is clled =========================================================")
+ # Create new product and price
+ price = StripeService.create_price(
+ product_data={
+ "name": self.title,
+ "description": self.short_description,
+ },
+ unit_amount=int(self.amount * 100),
+ currency="gbp",
+ recurring={
+ "interval": self.interval,
+ "interval_count": self.interval_count,
+ },
+ metadata={
+ "subscription_id": self.id
+ }
+
+ )
+ if not price["success"]:
+ raise Exception(price['message'])
+
+ # Add the IDs to the record
+ self.price_id = price["data"].id
+ self.product_id = price["data"].product
+
+ super().save(*args, **kwargs)
+
+ def calculate_days(self):
+ count = {
+ self.DAY: 1,
+ self.MONTH: 30, # assuming a month is 30 days
+ self.YEAR: 365,
+ self.WEEK: 7
+ }
+ return count[self.interval] * self.interval_count
+
class SubscriptionStatus(models.TextChoices):
ACTIVE = "active", _("Active")
@@ -61,20 +152,78 @@ class PrincipalSubscription(BaseModel):
start_date = models.DateField()
end_date = models.DateField()
order_id = models.CharField(max_length=255, null=True, blank=True)
- cancelled = models.BooleanField(default=False)
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)
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
)
+ coupon_code = models.CharField(max_length=255, null=True, blank=True)
class Meta:
db_table = "principal_subscription"
def __str__(self):
return f"{self.subscription} - {self.principal.first_name}"
+
+ def save(self, *args, **kwargs):
+ # If the subscription status is expired or inactive, set the active flag to False
+ if self.status in [SubscriptionStatus.EXPIRED, SubscriptionStatus.INACTIVE]:
+ self.active = False
+
+ # If the active flag is False, set the status to inactive
+ if not self.active:
+ self.status = SubscriptionStatus.INACTIVE
+ super().save(*args, **kwargs)
+
+
+ def generate_order_id(email):
+ return f"order_{str(timezone.localtime().timestamp())}{str(email)}"
+
+ def generate_grace_period_end_date(date):
+ return date + timedelta(days=15)
+
+ @classmethod
+ def has_principal_subscription(cls, principal):
+ return cls.get_grace_period_princial_subscription(principal).exists()
+
+ @classmethod
+ def get_grace_period_princial_subscription(cls, principal):
+ return cls.objects.filter(
+ principal=principal,
+ is_paid=True,
+ active=True,
+ status=SubscriptionStatus.ACTIVE,
+ grace_period_end_date__gt=timezone.now().date(),
+ )
+
+ @classmethod
+ def get_active_princial_subscription(cls, principal):
+ return cls.objects.filter(
+ principal=principal,
+ is_paid=True,
+ active=True,
+ status=SubscriptionStatus.ACTIVE,
+ end_date__gt=timezone.now().date(),
+ ).order_by('-end_date').last()
+
+ @classmethod
+ def get_principal_subscription(cls, principal):
+ return cls.objects.filter(
+ principal=principal,
+ is_paid=True,
+ active=True,
+ status=SubscriptionStatus.ACTIVE,
+ ).order_by("-grace_period_end_date").first()
+
+ @classmethod
+ def cancel_stipe_auto_renew_subscription(cls, subscription):
+ subscription.auto_renew = False
+ subscription.cancelled_date_time = timezone.now()
+ subscription.save()
class WebhookEvent(BaseModel):
diff --git a/manage_subscriptions/urls.py b/manage_subscriptions/urls.py
index aa767f8..21c0d15 100644
--- a/manage_subscriptions/urls.py
+++ b/manage_subscriptions/urls.py
@@ -17,61 +17,51 @@ urlpatterns = [
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(
- # "plan/add/",
- # views.PlanCreateOrUpdateView.as_view(),
- # name="plan_add",
- # ),
- # path(
- # "plan/edit//",
- # views.PlanCreateOrUpdateView.as_view(),
- # name="plan_edit",
- # ),
- # path(
- # "plan/delete/",
- # views.PlanDeleteView.as_view(),
- # name="plan_delete",
- # ),
+ path("subscription//", views.SubscriptionDetailView.as_view(), name="subscription_detail"),
+ path(
+ "subscription/delete/",
+ views.SubscriptionDeleteView.as_view(),
+ name="subscription_delete",
+ ),
+
# Principal Subscription
path(
"principal_subscription/list/",
views.PrincipalSubscriptionView.as_view(),
name="principal_subscriptions_list",
),
- # path(
- # "principal_subscription/add/",
- # views.PrincipalSubscriptionCreateOrUpdateView.as_view(),
- # name="principal_subscription_add",
- # ),
path(
"principal_subscription/edit//",
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(),
name="principal_subscription_delete",
),
- path(
- "stripe-subscription/",
- views.stripe_config,
- name="stripe_subscription",
- ),
path(
"create-checkout-session/",
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("active/", views.ActiveSubscriptionView.as_view(), name="active"),
+ path("cancel-subscription/", views.CancelAutoSubscriptionView.as_view(), name="cancel_subscription"),
+ path("404/", views.ErrorView.as_view(), name="error"),
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/utils.py b/manage_subscriptions/utils.py
index f69e4b6..a7dbd83 100644
--- a/manage_subscriptions/utils.py
+++ b/manage_subscriptions/utils.py
@@ -8,26 +8,6 @@ API_KEY = settings.GOOGLE_MAPS_API_KEY
gmaps = googlemaps.Client(key=API_KEY)
-def get_active_subscription_id_for_principal(principal):
- # Filter subscriptions for the principal that are active and not cancelled
- active_subscriptions = PrincipalSubscription.objects.filter(
- principal=principal,
- status=SubscriptionStatus.ACTIVE,
- is_paid=True,
- cancelled=False,
- deleted=False,
- active=True,
- end_date__gte=now().date(), # Ensure the subscription hasn't expired
- ).order_by(
- "-end_date"
- ) # Order by end_date to get the most recent active subscription
-
- if active_subscriptions.exists():
- # Return the ID of the most recent active subscription
- return active_subscriptions.first().id
- return None
-
-
def get_location_info(latitude, longitude):
reverse_geocode_result = gmaps.reverse_geocode((latitude, longitude))
diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py
index dbbdcec..e2af70e 100644
--- a/manage_subscriptions/views.py
+++ b/manage_subscriptions/views.py
@@ -1,20 +1,20 @@
-from datetime import timedelta
+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
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 goodtimes.services import StripeService
+from manage_coupons.models import Coupon
from manage_subscriptions.forms import (
- PlanForm,
SubscriptionForm,
PrincipalSubscriptionForm,
+ SubscriptionUpdateForm,
)
from manage_wallets.models import (
PaymentMethod,
@@ -22,18 +22,22 @@ from manage_wallets.models import (
TransactionStatus,
TransactionType,
)
-from .models import Plan, Subscription, PrincipalSubscription
+from .models import (
+ Subscription,
+ PrincipalSubscription,
+ SubscriptionStatus,
+)
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.urls import reverse, reverse_lazy
from django.contrib import messages
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
+from django.db import transaction
# Create your views here.
@@ -80,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)
@@ -91,16 +100,30 @@ 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)
return render(request, self.template_name, context=context)
+
+ # This code ensures that only one free plan can be created by checking for existing free plans before saving a new one.
+ if form.cleaned_data.get("is_free"):
+ if self.model.objects.filter(Q(is_free=True) & Q(active=True)).exists():
+ messages.error(
+ self.request,
+ "A free plan is already available. Please deactivate the existing one before creating a new one.",
+ )
+ context = self.get_context_data(form=form)
+ return render(request, self.template_name, context=context)
+
form.save()
messages.success(self.request, self.get_success_message())
return redirect(self.success_url)
-
class SubscriptionView(LoginRequiredMixin, generic.ListView):
page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
@@ -110,7 +133,12 @@ class SubscriptionView(LoginRequiredMixin, generic.ListView):
context_object_name = "subscription_obj"
def get_queryset(self):
- queryset = super().get_queryset().filter(deleted=False, active=True)
+ queryset = (
+ super()
+ .get_queryset()
+ .filter(deleted=False)
+ .prefetch_related("principal_types")
+ )
return queryset.order_by("-created_on")
def get_context_data(self, **kwargs):
@@ -119,101 +147,13 @@ 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
-
-# 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)
-
-
-# class PlanCreateOrUpdateView(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/plan_add.html"
-# model = Plan
-# form_class = PlanForm
-# success_url = reverse_lazy("manage_subscriptions:plan_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 PlanView(LoginRequiredMixin, generic.ListView):
+class SubscriptionDetailView(LoginRequiredMixin, generic.DetailView):
page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
action = resource_action.ACTION_READ
- model = Plan
- template_name = "manage_subscriptions/plan_list.html"
- context_object_name = "plan_obj"
-
- def get_queryset(self):
- return super().get_queryset().filter(deleted=False)
+ 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)
@@ -221,32 +161,35 @@ class PlanView(LoginRequiredMixin, generic.ListView):
return context
-# class PlanDeleteView(LoginRequiredMixin, generic.View):
-# page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
-# resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
-# action = resource_action.ACTION_DELETE
-# model = Plan
-# success_url = reverse_lazy("manage_subscriptions:plan_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)
+ subscription.deleted = True
+ subscription.active = False
+ subscription.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 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
@@ -323,6 +266,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
@@ -345,64 +302,211 @@ class PrincipalSubscriptionDeleteView(LoginRequiredMixin, generic.View):
return redirect(self.success_url)
-class SubscriptionPageView(TemplateView):
+class SubscriptionPageView(generic.View):
template_name = "stripe_html/index.html"
+ model = Subscription
+ error_url = reverse_lazy("manage_subscriptions:error")
+
+ def get(self, request):
+ if not request.user.is_authenticated:
+ return HttpResponseRedirect(self.error_url)
+
+ print("request user is :", request.user)
+ obj = self.model.objects.filter(
+ principal_types=request.user.principal_type,
+ active=True,
+ is_free=False,
+ )
+
+ if not obj.exists():
+ print(f"No pre-define subscription details found in {self.model} table for user_type {request.user.principal_type}")
+ return HttpResponseRedirect(self.error_url)
+
+ context = {
+ "subscriptions": obj,
+ # "stripeCheckoutUrl": request.build_absolute_uri(reverse("manage_subscriptions:create_checkout_session")),
+ # "couponValidityCheckUrl": request.build_absolute_uri(reverse("manage_subscriptions:validate_coupon")),
+ "stripeCheckoutUrl": settings.STRIPE_CHECKOUT_URL,
+ "couponValidityCheckUrl": settings.COUPON_VALIDITY_CHECK_URL,
+ "stripe_public_key": settings.STRIPE_PUBLISH_KEY
+ }
+ return render(request, self.template_name, context=context)
+
+class ActiveSubscriptionView(generic.View):
+ template_name = "stripe_html/active_subscription.html"
+ model = IAmPrincipal
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)
+ user = self.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)
+ except (
+ IAmPrincipal.DoesNotExist,
+ jwt.ExpiredSignatureError,
+ jwt.InvalidTokenError,
+ ):
+ return HttpResponseRedirect(reverse("manage_subscriptions:error"))
- 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)
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- request = self.request
+ today = timezone.now().date()
if request.user.is_authenticated:
- print("request.user: ", request.user)
- subscriptions = Subscription.objects.filter(
- principal_types=request.user.principal_type
- )
+ latest_subscription = PrincipalSubscription.get_active_princial_subscription(request.user)
- if subscriptions.exists():
- context["subscriptions"] = subscriptions
- context["stripeCheckoutUrl"] = settings.STRIPE_CHECKOUT_URL
- context["stripeFinalUrl"] = settings.STRIPE_FINAL_URL
- else:
- # Handle the case where no subscriptions are found for the principal type.
- context["error"] = "No subscriptions found for your user type."
- return context
+ print(f"latest subscription reodr is {latest_subscription}")
+
+ if not latest_subscription:
+ return HttpResponseRedirect(reverse("manage_subscriptions:stripe"))
+
+ return render(request, self.template_name, context={"subscription": latest_subscription})
+ return HttpResponseRedirect(reverse("manage_subscriptions:error"))
+
+class CancelAutoSubscriptionView(LoginRequiredMixin, generic.View):
+ model = PrincipalSubscription
+ error_url = reverse_lazy("manage_subscriptions:error")
+
+ def get(self, request, *args, **kwargs):
+ subscription_id = self.kwargs.get("subscription_id")
+
+ try:
+ subscription = self.model.objects.get(
+ id=subscription_id, principal=request.user
+ )
+ except self.model.DoesNotExist:
+ messages.error(request, "Subscription not found.")
+ return redirect("manage_subscriptions:error")
+
+ try:
+ if subscription.stripe_subscription_id:
+ data = StripeService.cancel_auto_renew_subscription(subscription.stripe_subscription_id)
+ if not data["success"]:
+ return redirect(self.error_url)
+
+ self.model.cancel_stipe_auto_renew_subscription(subscription)
+
+ except Exception as e:
+ print(f'an error occur {str(e)}')
+ messages.error(request, f"An error occurred while cancelling the subscription {str(e)}")
+ return redirect(self.error_url)
+
+ return redirect(reverse_lazy("manage_subscriptions:active"))
+
+ # 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.auto_renew = False
+ # subscription.cancelled_date_time = timezone.now()
+ # subscription.save()
+
+ # messages.success(request, "Subscription cancelled successfully.")
+ # 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:subscription_cancel_fails")
@csrf_exempt
-def stripe_config(request):
- if request.method == "GET":
- stripe_config = {"publicKey": settings.STRIPE_PUBLISH_KEY}
- return JsonResponse(stripe_config, safe=False)
+@require_POST
+def validate_coupon(request):
+ data = json.loads(request.body)
+ coupon_code = data.get("couponCode", None)
+ subscription_id = data.get("subscriptionId", None)
+ final_amount = None
+
+ try:
+ subscription = Subscription.objects.get(id=subscription_id)
+ 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
+ try:
+ coupon = Coupon.objects.get(coupon_code=coupon_code)
+
+ if not coupon.is_valid():
+ return JsonResponse({"error": "Coupon is not valid."}, status=400)
+
+ # Check discount amount
+ if coupon.discount_amount and coupon.discount_amount > subscription.amount:
+ final_amount = subscription.amount - coupon.discount_amount
+ return JsonResponse(
+ {"error": "Coupon discount amount exceeds subscription amount."},
+ status=400,
+ )
+
+ # Check discount percentage
+ if coupon.discount_percentage:
+ discount = (
+ coupon.discount_percentage / Decimal("100")
+ ) * subscription.amount
+ if discount > subscription.amount:
+ return JsonResponse(
+ {
+ "error": "Coupon discount percentage exceeds subscription amount."
+ },
+ status=400,
+ )
+ final_amount = subscription.amount - discount
+ # Retrieving coupon from Stripe if applicable
+ if coupon.coupon_id:
+ try:
+ stripe_coupon = stripe.Coupon.retrieve(coupon.coupon_id)
+ print("stripe_coupon: ", stripe_coupon)
+ if (
+ stripe_coupon.max_redemptions
+ and stripe_coupon.times_redeemed >= stripe_coupon.max_redemptions
+ ):
+ return JsonResponse(
+ {"error": "Coupon max redeems reached."}, status=400
+ )
+ 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
+ )
+ except stripe.error.StripeError as e:
+ return JsonResponse({"error": f"Stripe error: {str(e)}"}, status=400)
+ else:
+ return JsonResponse(
+ {
+ "error": "Coupon is either invalid, expired, or not associated with Stripe."
+ },
+ status=400,
+ )
+ except Coupon.DoesNotExist:
+ return JsonResponse({"error": "Coupon not found."}, status=404)
@csrf_exempt
@@ -410,97 +514,82 @@ def stripe_config(request):
def create_checkout_session(request):
stripe.api_key = settings.STRIPE_SECRET_KEY
data = json.loads(request.body)
- print("data: ", data)
- subscription_id = data.get("subscriptionId", None)
+ subscription_id = data.get("subscriptionId")
+ coupon_code = data.get("couponCode")
+ transaction_amount = data.get("finalAmount")
+ is_recurring = data.get("isRecurring")
+ principal_id = request.user.id
+
+ print(f"subscription data is {subscription_id}, {coupon_code}, { is_recurring}")
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)
- )
- print("order_id: ", order_id)
-
- # Create a Transaction object with status INITIATE
- transaction = Transaction.objects.create(
- principal=request.user,
- principal_subscription=None, # Since the subscription is not created yet
- 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
- order_id=order_id,
- 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),
- # )
+ # Default transaction amount based on subscription amount
+ session_data = {
+ "payment_method_types": ["card"],
+ "success_url": request.build_absolute_uri("/subscriptions/success/"),
+ "cancel_url": request.build_absolute_uri("/subscriptions/cancel/"),
+ "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,
+ },
+ }
+ print("session_data: ", session_data)
+ # Coupon Handling
+ if coupon_code:
+ try:
+ stripe_coupon = stripe.Coupon.retrieve(coupon_code)
+ session_data["discounts"] = [{"coupon": stripe_coupon.id}]
+ except stripe.error.InvalidRequestError:
+ return JsonResponse(
+ {"error": f"Invalid coupon code: {coupon_code}"}, status=400
+ )
+ except stripe.error.StripeError as e:
+ return JsonResponse({"error": f"Stripe error: {str(e)}"}, status=400)
+ # Creating the Stripe Checkout Session
try:
- # customer = stripe.Customer.create(
- # email=request.user.email,
- # shipping={
- # "name": request.user.first_name,
- # "address": {
- # "line1": request.user.city,
- # "city": request.user.city,
- # "postal_code": "SW1A 2AA",
- # "country": request.user.address_line1, # Adjust accordingly
- # },
- # },
- # )
-
- # Create a checkout session
- checkout_session = stripe.checkout.Session.create(
- payment_method_types=["card"],
- # customer=customer.id, # Optional: Link the session to the Stripe customer created above
- line_items=[
+ if is_recurring and subscription.price_id:
+ session_data["line_items"] = [
+ {
+ "price": subscription.price_id,
+ "quantity": 1,
+ }
+ ]
+ session_data["mode"] = "subscription"
+ session_data["subscription_data"] = {
+ "metadata": session_data["metadata"],
+ }
+ else:
+ session_data["line_items"] = [
{
"price_data": {
"currency": "gbp",
"product_data": {
- "name": subscription.title, # Adjust with your subscription/product name
+ "name": subscription.title,
+ "description": subscription.short_description,
},
- "unit_amount": int(
- subscription.amount * 100
- ), # Unit amount in cents/pence
- "tax_behavior": "inclusive", # or 'exclusive', based on your tax settings
+ "unit_amount": int(subscription.amount * 100),
},
"quantity": 1,
}
- ],
- mode="payment",
- success_url=request.build_absolute_uri("/subscriptions/success/"),
- cancel_url=request.build_absolute_uri("/subscriptions/cancel/"),
- metadata={
- "principal": str(request.user.id),
- "order_id": str(order_id),
- "subscription_id": str(subscription.id),
- "transaction_id": str(transaction.id),
- # "principal_subscription_id": str(principal_subscription.id),
- },
- )
+ ]
+ session_data["mode"] = "payment"
+ checkout_session = stripe.checkout.Session.create(**session_data)
return JsonResponse({"sessionId": checkout_session["id"]})
except Exception as e:
- return JsonResponse({"error": str(e)})
+ return JsonResponse({"error": str(e)}, status=500)
+class ErrorView(TemplateView):
+ template_name = "stripe_html/webview_404.html"
class SuccessView(TemplateView):
template_name = "stripe_html/success.html"
@@ -509,36 +598,9 @@ class CancelView(TemplateView):
template_name = "stripe_html/cancel.html"
-# class IndexView(TemplateView):
-# template_name = "stripe_html/index.html"
+class SubscriptionCancelSuccessView(TemplateView):
+ template_name = "stripe_html/subscription_cancel_success.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)
+class SubscriptionCancelFailsView(TemplateView):
+ template_name = "stripe_html/subscription_cancel_fails.html"
diff --git a/manage_wallets/api/views.py b/manage_wallets/api/views.py
index 85deee9..931bbcb 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,
+ ],
+ ).order_by("-created_on")
serializer = serializers.TransactionSerializer(queryset, many=True)
response = {
diff --git a/requirements.txt b/requirements.txt
index 08ccda6..0724efc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,21 +6,24 @@ autobahn==23.6.2
Automat==22.10.0
certifi==2024.2.2
cffi==1.16.0
-channels==4.0.0
+channels==4.1.0
channels-redis==4.2.0
+chardet==5.2.0
charset-normalizer==3.3.2
colorama==0.4.6
colorlog==6.8.2
constantly==23.10.4
cryptography==42.0.2
-daphne==4.1.0
+daphne==4.1.2
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
+django-filter==24.2
django-phonenumber-field==7.3.0
django-quill-editor==0.1.40
django-taggit==5.0.1
@@ -30,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
@@ -42,7 +46,10 @@ mysqlclient==2.2.4
numpy==1.26.4
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
pyasn1==0.5.1
@@ -52,10 +59,12 @@ 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
redis==5.0.2
+reportlab==4.2.0
requests==2.31.0
requests-oauthlib==1.3.1
service-identity==24.1.0
@@ -64,8 +73,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
diff --git a/static/images/category/business.png b/static/images/category/business.png
new file mode 100644
index 0000000..e4b9bf5
Binary files /dev/null and b/static/images/category/business.png differ
diff --git a/static/images/category/cultural.png b/static/images/category/cultural.png
new file mode 100644
index 0000000..b76f8a8
Binary files /dev/null and b/static/images/category/cultural.png differ
diff --git a/static/images/category/education.png b/static/images/category/education.png
new file mode 100644
index 0000000..615b824
Binary files /dev/null and b/static/images/category/education.png differ
diff --git a/static/images/category/entertainment.png b/static/images/category/entertainment.png
new file mode 100644
index 0000000..52cba2a
Binary files /dev/null and b/static/images/category/entertainment.png differ
diff --git a/static/images/category/health.png b/static/images/category/health.png
new file mode 100644
index 0000000..75b4ec3
Binary files /dev/null and b/static/images/category/health.png differ
diff --git a/static/images/category/liesure.png b/static/images/category/liesure.png
new file mode 100644
index 0000000..6feb43c
Binary files /dev/null and b/static/images/category/liesure.png differ
diff --git a/static/images/category/outdoor.png b/static/images/category/outdoor.png
new file mode 100644
index 0000000..1311fef
Binary files /dev/null and b/static/images/category/outdoor.png differ
diff --git a/static/images/category/recreation.png b/static/images/category/recreation.png
new file mode 100644
index 0000000..7125301
Binary files /dev/null and b/static/images/category/recreation.png differ
diff --git a/static/img/facebook.png b/static/img/facebook.png
new file mode 100644
index 0000000..8400a87
Binary files /dev/null and b/static/img/facebook.png differ
diff --git a/static/img/forward_all_icon.png b/static/img/forward_all_icon.png
new file mode 100644
index 0000000..6b6f4d1
Binary files /dev/null and b/static/img/forward_all_icon.png differ
diff --git a/static/img/instagram.png b/static/img/instagram.png
new file mode 100644
index 0000000..b61a965
Binary files /dev/null and b/static/img/instagram.png differ
diff --git a/static/img/x_twitter.png b/static/img/x_twitter.png
new file mode 100644
index 0000000..62887fd
Binary files /dev/null and b/static/img/x_twitter.png differ
diff --git a/static/src/assets/css/payment/style.css b/static/src/assets/css/payment/style.css
index e20914f..5fb6746 100644
--- a/static/src/assets/css/payment/style.css
+++ b/static/src/assets/css/payment/style.css
@@ -1,808 +1,872 @@
+@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;
+ width: 100%;
+}
+
+
+/* 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);
+}
+
+.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: start;
+ margin-bottom: 15px;
+}
+
+/* New css */
+.feat-card .para {
+ font-size: 18px;
+ }
+ .currency{
+ font-size: 36px;
+ color: #fff;
+ margin-right:5px
+ }
+ .interval{
+ font-size: 14px;
+ color: gray;
+ }
+
+ .actual-price {
+ text-decoration: line-through;
+ color: #ccc;
+}
+ .offer-price {
+ font-size: 36px;
+ /* font-weight: bold; */
+ color: #fff;
+}
+
+
+/* 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;
+ }
+}
+
+.coupon-code-input,
+.common-btn {
+ display: inline-block;
+ margin-right: 10px;
+}
+
+.common-btn {
+ /* remove default button margins */
+ margin: 0;
+ width: 100%;
+}
+.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/static/src/plugins/src/jquery-validate/jquery.validate.min.js b/static/src/plugins/src/jquery-validate/jquery.validate.min.js
new file mode 100644
index 0000000..7f5f510
--- /dev/null
+++ b/static/src/plugins/src/jquery-validate/jquery.validate.min.js
@@ -0,0 +1,4 @@
+/*! jQuery Validation Plugin - v1.19.3 - 1/9/2021
+ * https://jqueryvalidation.org/
+ * Copyright (c) 2021 Jörn Zaefferer; Licensed MIT */
+!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof module&&module.exports?module.exports=a(require("jquery")):a(jQuery)}(function(a){a.extend(a.fn,{validate:function(b){if(!this.length)return void(b&&b.debug&&window.console&&console.warn("Nothing selected, can't validate, returning nothing."));var c=a.data(this[0],"validator");return c?c:(this.attr("novalidate","novalidate"),c=new a.validator(b,this[0]),a.data(this[0],"validator",c),c.settings.onsubmit&&(this.on("click.validate",":submit",function(b){c.submitButton=b.currentTarget,a(this).hasClass("cancel")&&(c.cancelSubmit=!0),void 0!==a(this).attr("formnovalidate")&&(c.cancelSubmit=!0)}),this.on("submit.validate",function(b){function d(){var d,e;return c.submitButton&&(c.settings.submitHandler||c.formSubmitted)&&(d=a("").attr("name",c.submitButton.name).val(a(c.submitButton).val()).appendTo(c.currentForm)),!(c.settings.submitHandler&&!c.settings.debug)||(e=c.settings.submitHandler.call(c,c.currentForm,b),d&&d.remove(),void 0!==e&&e)}return c.settings.debug&&b.preventDefault(),c.cancelSubmit?(c.cancelSubmit=!1,d()):c.form()?c.pendingRequest?(c.formSubmitted=!0,!1):d():(c.focusInvalid(),!1)})),c)},valid:function(){var b,c,d;return a(this[0]).is("form")?b=this.validate().form():(d=[],b=!0,c=a(this[0].form).validate(),this.each(function(){b=c.element(this)&&b,b||(d=d.concat(c.errorList))}),c.errorList=d),b},rules:function(b,c){var d,e,f,g,h,i,j=this[0],k="undefined"!=typeof this.attr("contenteditable")&&"false"!==this.attr("contenteditable");if(null!=j&&(!j.form&&k&&(j.form=this.closest("form")[0],j.name=this.attr("name")),null!=j.form)){if(b)switch(d=a.data(j.form,"validator").settings,e=d.rules,f=a.validator.staticRules(j),b){case"add":a.extend(f,a.validator.normalizeRule(c)),delete f.messages,e[j.name]=f,c.messages&&(d.messages[j.name]=a.extend(d.messages[j.name],c.messages));break;case"remove":return c?(i={},a.each(c.split(/\s/),function(a,b){i[b]=f[b],delete f[b]}),i):(delete e[j.name],f)}return g=a.validator.normalizeRules(a.extend({},a.validator.classRules(j),a.validator.attributeRules(j),a.validator.dataRules(j),a.validator.staticRules(j)),j),g.required&&(h=g.required,delete g.required,g=a.extend({required:h},g)),g.remote&&(h=g.remote,delete g.remote,g=a.extend(g,{remote:h})),g}}});var b=function(a){return a.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")};a.extend(a.expr.pseudos||a.expr[":"],{blank:function(c){return!b(""+a(c).val())},filled:function(c){var d=a(c).val();return null!==d&&!!b(""+d)},unchecked:function(b){return!a(b).prop("checked")}}),a.validator=function(b,c){this.settings=a.extend(!0,{},a.validator.defaults,b),this.currentForm=c,this.init()},a.validator.format=function(b,c){return 1===arguments.length?function(){var c=a.makeArray(arguments);return c.unshift(b),a.validator.format.apply(this,c)}:void 0===c?b:(arguments.length>2&&c.constructor!==Array&&(c=a.makeArray(arguments).slice(1)),c.constructor!==Array&&(c=[c]),a.each(c,function(a,c){b=b.replace(new RegExp("\\{"+a+"\\}","g"),function(){return c})}),b)},a.extend(a.validator,{defaults:{messages:{},groups:{},rules:{},errorClass:"error",pendingClass:"pending",validClass:"valid",errorElement:"label",focusCleanup:!1,focusInvalid:!0,errorContainer:a([]),errorLabelContainer:a([]),onsubmit:!0,ignore:":hidden",ignoreTitle:!1,onfocusin:function(a){this.lastActive=a,this.settings.focusCleanup&&(this.settings.unhighlight&&this.settings.unhighlight.call(this,a,this.settings.errorClass,this.settings.validClass),this.hideThese(this.errorsFor(a)))},onfocusout:function(a){this.checkable(a)||!(a.name in this.submitted)&&this.optional(a)||this.element(a)},onkeyup:function(b,c){var d=[16,17,18,20,35,36,37,38,39,40,45,144,225];9===c.which&&""===this.elementValue(b)||a.inArray(c.keyCode,d)!==-1||(b.name in this.submitted||b.name in this.invalid)&&this.element(b)},onclick:function(a){a.name in this.submitted?this.element(a):a.parentNode.name in this.submitted&&this.element(a.parentNode)},highlight:function(b,c,d){"radio"===b.type?this.findByName(b.name).addClass(c).removeClass(d):a(b).addClass(c).removeClass(d)},unhighlight:function(b,c,d){"radio"===b.type?this.findByName(b.name).removeClass(c).addClass(d):a(b).removeClass(c).addClass(d)}},setDefaults:function(b){a.extend(a.validator.defaults,b)},messages:{required:"This field is required.",remote:"Please fix this field.",email:"Please enter a valid email address.",url:"Please enter a valid URL.",date:"Please enter a valid date.",dateISO:"Please enter a valid date (ISO).",number:"Please enter a valid number.",digits:"Please enter only digits.",equalTo:"Please enter the same value again.",maxlength:a.validator.format("Please enter no more than {0} characters."),minlength:a.validator.format("Please enter at least {0} characters."),rangelength:a.validator.format("Please enter a value between {0} and {1} characters long."),range:a.validator.format("Please enter a value between {0} and {1}."),max:a.validator.format("Please enter a value less than or equal to {0}."),min:a.validator.format("Please enter a value greater than or equal to {0}."),step:a.validator.format("Please enter a multiple of {0}.")},autoCreateRanges:!1,prototype:{init:function(){function b(b){var c="undefined"!=typeof a(this).attr("contenteditable")&&"false"!==a(this).attr("contenteditable");if(!this.form&&c&&(this.form=a(this).closest("form")[0],this.name=a(this).attr("name")),d===this.form){var e=a.data(this.form,"validator"),f="on"+b.type.replace(/^validate/,""),g=e.settings;g[f]&&!a(this).is(g.ignore)&&g[f].call(e,this,b)}}this.labelContainer=a(this.settings.errorLabelContainer),this.errorContext=this.labelContainer.length&&this.labelContainer||a(this.currentForm),this.containers=a(this.settings.errorContainer).add(this.settings.errorLabelContainer),this.submitted={},this.valueCache={},this.pendingRequest=0,this.pending={},this.invalid={},this.reset();var c,d=this.currentForm,e=this.groups={};a.each(this.settings.groups,function(b,c){"string"==typeof c&&(c=c.split(/\s/)),a.each(c,function(a,c){e[c]=b})}),c=this.settings.rules,a.each(c,function(b,d){c[b]=a.validator.normalizeRule(d)}),a(this.currentForm).on("focusin.validate focusout.validate keyup.validate",":text, [type='password'], [type='file'], select, textarea, [type='number'], [type='search'], [type='tel'], [type='url'], [type='email'], [type='datetime'], [type='date'], [type='month'], [type='week'], [type='time'], [type='datetime-local'], [type='range'], [type='color'], [type='radio'], [type='checkbox'], [contenteditable], [type='button']",b).on("click.validate","select, option, [type='radio'], [type='checkbox']",b),this.settings.invalidHandler&&a(this.currentForm).on("invalid-form.validate",this.settings.invalidHandler)},form:function(){return this.checkForm(),a.extend(this.submitted,this.errorMap),this.invalid=a.extend({},this.errorMap),this.valid()||a(this.currentForm).triggerHandler("invalid-form",[this]),this.showErrors(),this.valid()},checkForm:function(){this.prepareForm();for(var a=0,b=this.currentElements=this.elements();b[a];a++)this.check(b[a]);return this.valid()},element:function(b){var c,d,e=this.clean(b),f=this.validationTargetFor(e),g=this,h=!0;return void 0===f?delete this.invalid[e.name]:(this.prepareElement(f),this.currentElements=a(f),d=this.groups[f.name],d&&a.each(this.groups,function(a,b){b===d&&a!==f.name&&(e=g.validationTargetFor(g.clean(g.findByName(a))),e&&e.name in g.invalid&&(g.currentElements.push(e),h=g.check(e)&&h))}),c=this.check(f)!==!1,h=h&&c,c?this.invalid[f.name]=!1:this.invalid[f.name]=!0,this.numberOfInvalids()||(this.toHide=this.toHide.add(this.containers)),this.showErrors(),a(b).attr("aria-invalid",!c)),h},showErrors:function(b){if(b){var c=this;a.extend(this.errorMap,b),this.errorList=a.map(this.errorMap,function(a,b){return{message:a,element:c.findByName(b)[0]}}),this.successList=a.grep(this.successList,function(a){return!(a.name in b)})}this.settings.showErrors?this.settings.showErrors.call(this,this.errorMap,this.errorList):this.defaultShowErrors()},resetForm:function(){a.fn.resetForm&&a(this.currentForm).resetForm(),this.invalid={},this.submitted={},this.prepareForm(),this.hideErrors();var b=this.elements().removeData("previousValue").removeAttr("aria-invalid");this.resetElements(b)},resetElements:function(a){var b;if(this.settings.unhighlight)for(b=0;a[b];b++)this.settings.unhighlight.call(this,a[b],this.settings.errorClass,""),this.findByName(a[b].name).removeClass(this.settings.validClass);else a.removeClass(this.settings.errorClass).removeClass(this.settings.validClass)},numberOfInvalids:function(){return this.objectLength(this.invalid)},objectLength:function(a){var b,c=0;for(b in a)void 0!==a[b]&&null!==a[b]&&a[b]!==!1&&c++;return c},hideErrors:function(){this.hideThese(this.toHide)},hideThese:function(a){a.not(this.containers).text(""),this.addWrapper(a).hide()},valid:function(){return 0===this.size()},size:function(){return this.errorList.length},focusInvalid:function(){if(this.settings.focusInvalid)try{a(this.findLastActive()||this.errorList.length&&this.errorList[0].element||[]).filter(":visible").trigger("focus").trigger("focusin")}catch(b){}},findLastActive:function(){var b=this.lastActive;return b&&1===a.grep(this.errorList,function(a){return a.element.name===b.name}).length&&b},elements:function(){var b=this,c={};return a(this.currentForm).find("input, select, textarea, [contenteditable]").not(":submit, :reset, :image, :disabled").not(this.settings.ignore).filter(function(){var d=this.name||a(this).attr("name"),e="undefined"!=typeof a(this).attr("contenteditable")&&"false"!==a(this).attr("contenteditable");return!d&&b.settings.debug&&window.console&&console.error("%o has no name assigned",this),e&&(this.form=a(this).closest("form")[0],this.name=d),this.form===b.currentForm&&(!(d in c||!b.objectLength(a(this).rules()))&&(c[d]=!0,!0))})},clean:function(b){return a(b)[0]},errors:function(){var b=this.settings.errorClass.split(" ").join(".");return a(this.settings.errorElement+"."+b,this.errorContext)},resetInternals:function(){this.successList=[],this.errorList=[],this.errorMap={},this.toShow=a([]),this.toHide=a([])},reset:function(){this.resetInternals(),this.currentElements=a([])},prepareForm:function(){this.reset(),this.toHide=this.errors().add(this.containers)},prepareElement:function(a){this.reset(),this.toHide=this.errorsFor(a)},elementValue:function(b){var c,d,e=a(b),f=b.type,g="undefined"!=typeof e.attr("contenteditable")&&"false"!==e.attr("contenteditable");return"radio"===f||"checkbox"===f?this.findByName(b.name).filter(":checked").val():"number"===f&&"undefined"!=typeof b.validity?b.validity.badInput?"NaN":e.val():(c=g?e.text():e.val(),"file"===f?"C:\\fakepath\\"===c.substr(0,12)?c.substr(12):(d=c.lastIndexOf("/"),d>=0?c.substr(d+1):(d=c.lastIndexOf("\\"),d>=0?c.substr(d+1):c)):"string"==typeof c?c.replace(/\r/g,""):c)},check:function(b){b=this.validationTargetFor(this.clean(b));var c,d,e,f,g=a(b).rules(),h=a.map(g,function(a,b){return b}).length,i=!1,j=this.elementValue(b);"function"==typeof g.normalizer?f=g.normalizer:"function"==typeof this.settings.normalizer&&(f=this.settings.normalizer),f&&(j=f.call(b,j),delete g.normalizer);for(d in g){e={method:d,parameters:g[d]};try{if(c=a.validator.methods[d].call(this,j,b,e.parameters),"dependency-mismatch"===c&&1===h){i=!0;continue}if(i=!1,"pending"===c)return void(this.toHide=this.toHide.not(this.errorsFor(b)));if(!c)return this.formatAndAdd(b,e),!1}catch(k){throw this.settings.debug&&window.console&&console.log("Exception occurred when checking element "+b.id+", check the '"+e.method+"' method.",k),k instanceof TypeError&&(k.message+=". Exception occurred when checking element "+b.id+", check the '"+e.method+"' method."),k}}if(!i)return this.objectLength(g)&&this.successList.push(b),!0},customDataMessage:function(b,c){return a(b).data("msg"+c.charAt(0).toUpperCase()+c.substring(1).toLowerCase())||a(b).data("msg")},customMessage:function(a,b){var c=this.settings.messages[a];return c&&(c.constructor===String?c:c[b])},findDefined:function(){for(var a=0;aWarning: No message defined for "+b.name+""),e=/\$?\{(\d+)\}/g;return"function"==typeof d?d=d.call(this,c.parameters,b):e.test(d)&&(d=a.validator.format(d.replace(e,"{$1}"),c.parameters)),d},formatAndAdd:function(a,b){var c=this.defaultMessage(a,b);this.errorList.push({message:c,element:a,method:b.method}),this.errorMap[a.name]=c,this.submitted[a.name]=c},addWrapper:function(a){return this.settings.wrapper&&(a=a.add(a.parent(this.settings.wrapper))),a},defaultShowErrors:function(){var a,b,c;for(a=0;this.errorList[a];a++)c=this.errorList[a],this.settings.highlight&&this.settings.highlight.call(this,c.element,this.settings.errorClass,this.settings.validClass),this.showLabel(c.element,c.message);if(this.errorList.length&&(this.toShow=this.toShow.add(this.containers)),this.settings.success)for(a=0;this.successList[a];a++)this.showLabel(this.successList[a]);if(this.settings.unhighlight)for(a=0,b=this.validElements();b[a];a++)this.settings.unhighlight.call(this,b[a],this.settings.errorClass,this.settings.validClass);this.toHide=this.toHide.not(this.toShow),this.hideErrors(),this.addWrapper(this.toShow).show()},validElements:function(){return this.currentElements.not(this.invalidElements())},invalidElements:function(){return a(this.errorList).map(function(){return this.element})},showLabel:function(b,c){var d,e,f,g,h=this.errorsFor(b),i=this.idOrName(b),j=a(b).attr("aria-describedby");h.length?(h.removeClass(this.settings.validClass).addClass(this.settings.errorClass),h.html(c)):(h=a("<"+this.settings.errorElement+">").attr("id",i+"-error").addClass(this.settings.errorClass).html(c||""),d=h,this.settings.wrapper&&(d=h.hide().show().wrap("<"+this.settings.wrapper+"/>").parent()),this.labelContainer.length?this.labelContainer.append(d):this.settings.errorPlacement?this.settings.errorPlacement.call(this,d,a(b)):d.insertAfter(b),h.is("label")?h.attr("for",i):0===h.parents("label[for='"+this.escapeCssMeta(i)+"']").length&&(f=h.attr("id"),j?j.match(new RegExp("\\b"+this.escapeCssMeta(f)+"\\b"))||(j+=" "+f):j=f,a(b).attr("aria-describedby",j),e=this.groups[b.name],e&&(g=this,a.each(g.groups,function(b,c){c===e&&a("[name='"+g.escapeCssMeta(b)+"']",g.currentForm).attr("aria-describedby",h.attr("id"))})))),!c&&this.settings.success&&(h.text(""),"string"==typeof this.settings.success?h.addClass(this.settings.success):this.settings.success(h,b)),this.toShow=this.toShow.add(h)},errorsFor:function(b){var c=this.escapeCssMeta(this.idOrName(b)),d=a(b).attr("aria-describedby"),e="label[for='"+c+"'], label[for='"+c+"'] *";return d&&(e=e+", #"+this.escapeCssMeta(d).replace(/\s+/g,", #")),this.errors().filter(e)},escapeCssMeta:function(a){return a.replace(/([\\!"#$%&'()*+,.\/:;<=>?@\[\]^`{|}~])/g,"\\$1")},idOrName:function(a){return this.groups[a.name]||(this.checkable(a)?a.name:a.id||a.name)},validationTargetFor:function(b){return this.checkable(b)&&(b=this.findByName(b.name)),a(b).not(this.settings.ignore)[0]},checkable:function(a){return/radio|checkbox/i.test(a.type)},findByName:function(b){return a(this.currentForm).find("[name='"+this.escapeCssMeta(b)+"']")},getLength:function(b,c){switch(c.nodeName.toLowerCase()){case"select":return a("option:selected",c).length;case"input":if(this.checkable(c))return this.findByName(c.name).filter(":checked").length}return b.length},depend:function(a,b){return!this.dependTypes[typeof a]||this.dependTypes[typeof a](a,b)},dependTypes:{"boolean":function(a){return a},string:function(b,c){return!!a(b,c.form).length},"function":function(a,b){return a(b)}},optional:function(b){var c=this.elementValue(b);return!a.validator.methods.required.call(this,c,b)&&"dependency-mismatch"},startRequest:function(b){this.pending[b.name]||(this.pendingRequest++,a(b).addClass(this.settings.pendingClass),this.pending[b.name]=!0)},stopRequest:function(b,c){this.pendingRequest--,this.pendingRequest<0&&(this.pendingRequest=0),delete this.pending[b.name],a(b).removeClass(this.settings.pendingClass),c&&0===this.pendingRequest&&this.formSubmitted&&this.form()?(a(this.currentForm).submit(),this.submitButton&&a("input:hidden[name='"+this.submitButton.name+"']",this.currentForm).remove(),this.formSubmitted=!1):!c&&0===this.pendingRequest&&this.formSubmitted&&(a(this.currentForm).triggerHandler("invalid-form",[this]),this.formSubmitted=!1)},previousValue:function(b,c){return c="string"==typeof c&&c||"remote",a.data(b,"previousValue")||a.data(b,"previousValue",{old:null,valid:!0,message:this.defaultMessage(b,{method:c})})},destroy:function(){this.resetForm(),a(this.currentForm).off(".validate").removeData("validator").find(".validate-equalTo-blur").off(".validate-equalTo").removeClass("validate-equalTo-blur").find(".validate-lessThan-blur").off(".validate-lessThan").removeClass("validate-lessThan-blur").find(".validate-lessThanEqual-blur").off(".validate-lessThanEqual").removeClass("validate-lessThanEqual-blur").find(".validate-greaterThanEqual-blur").off(".validate-greaterThanEqual").removeClass("validate-greaterThanEqual-blur").find(".validate-greaterThan-blur").off(".validate-greaterThan").removeClass("validate-greaterThan-blur")}},classRuleSettings:{required:{required:!0},email:{email:!0},url:{url:!0},date:{date:!0},dateISO:{dateISO:!0},number:{number:!0},digits:{digits:!0},creditcard:{creditcard:!0}},addClassRules:function(b,c){b.constructor===String?this.classRuleSettings[b]=c:a.extend(this.classRuleSettings,b)},classRules:function(b){var c={},d=a(b).attr("class");return d&&a.each(d.split(" "),function(){this in a.validator.classRuleSettings&&a.extend(c,a.validator.classRuleSettings[this])}),c},normalizeAttributeRule:function(a,b,c,d){/min|max|step/.test(c)&&(null===b||/number|range|text/.test(b))&&(d=Number(d),isNaN(d)&&(d=void 0)),d||0===d?a[c]=d:b===c&&"range"!==b&&(a[c]=!0)},attributeRules:function(b){var c,d,e={},f=a(b),g=b.getAttribute("type");for(c in a.validator.methods)"required"===c?(d=b.getAttribute(c),""===d&&(d=!0),d=!!d):d=f.attr(c),this.normalizeAttributeRule(e,g,c,d);return e.maxlength&&/-1|2147483647|524288/.test(e.maxlength)&&delete e.maxlength,e},dataRules:function(b){var c,d,e={},f=a(b),g=b.getAttribute("type");for(c in a.validator.methods)d=f.data("rule"+c.charAt(0).toUpperCase()+c.substring(1).toLowerCase()),""===d&&(d=!0),this.normalizeAttributeRule(e,g,c,d);return e},staticRules:function(b){var c={},d=a.data(b.form,"validator");return d.settings.rules&&(c=a.validator.normalizeRule(d.settings.rules[b.name])||{}),c},normalizeRules:function(b,c){return a.each(b,function(d,e){if(e===!1)return void delete b[d];if(e.param||e.depends){var f=!0;switch(typeof e.depends){case"string":f=!!a(e.depends,c.form).length;break;case"function":f=e.depends.call(c,c)}f?b[d]=void 0===e.param||e.param:(a.data(c.form,"validator").resetElements(a(c)),delete b[d])}}),a.each(b,function(a,d){b[a]="function"==typeof d&&"normalizer"!==a?d(c):d}),a.each(["minlength","maxlength"],function(){b[this]&&(b[this]=Number(b[this]))}),a.each(["rangelength","range"],function(){var a;b[this]&&(Array.isArray(b[this])?b[this]=[Number(b[this][0]),Number(b[this][1])]:"string"==typeof b[this]&&(a=b[this].replace(/[\[\]]/g,"").split(/[\s,]+/),b[this]=[Number(a[0]),Number(a[1])]))}),a.validator.autoCreateRanges&&(null!=b.min&&null!=b.max&&(b.range=[b.min,b.max],delete b.min,delete b.max),null!=b.minlength&&null!=b.maxlength&&(b.rangelength=[b.minlength,b.maxlength],delete b.minlength,delete b.maxlength)),b},normalizeRule:function(b){if("string"==typeof b){var c={};a.each(b.split(/\s/),function(){c[this]=!0}),b=c}return b},addMethod:function(b,c,d){a.validator.methods[b]=c,a.validator.messages[b]=void 0!==d?d:a.validator.messages[b],c.length<3&&a.validator.addClassRules(b,a.validator.normalizeRule(b))},methods:{required:function(b,c,d){if(!this.depend(d,c))return"dependency-mismatch";if("select"===c.nodeName.toLowerCase()){var e=a(c).val();return e&&e.length>0}return this.checkable(c)?this.getLength(b,c)>0:void 0!==b&&null!==b&&b.length>0},email:function(a,b){return this.optional(b)||/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(a)},url:function(a,b){return this.optional(b)||/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[\/?#]\S*)?$/i.test(a)},date:function(){var a=!1;return function(b,c){return a||(a=!0,this.settings.debug&&window.console&&console.warn("The `date` method is deprecated and will be removed in version '2.0.0'.\nPlease don't use it, since it relies on the Date constructor, which\nbehaves very differently across browsers and locales. Use `dateISO`\ninstead or one of the locale specific methods in `localizations/`\nand `additional-methods.js`.")),this.optional(c)||!/Invalid|NaN/.test(new Date(b).toString())}}(),dateISO:function(a,b){return this.optional(b)||/^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test(a)},number:function(a,b){return this.optional(b)||/^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(a)},digits:function(a,b){return this.optional(b)||/^\d+$/.test(a)},minlength:function(a,b,c){var d=Array.isArray(a)?a.length:this.getLength(a,b);return this.optional(b)||d>=c},maxlength:function(a,b,c){var d=Array.isArray(a)?a.length:this.getLength(a,b);return this.optional(b)||d<=c},rangelength:function(a,b,c){var d=Array.isArray(a)?a.length:this.getLength(a,b);return this.optional(b)||d>=c[0]&&d<=c[1]},min:function(a,b,c){return this.optional(b)||a>=c},max:function(a,b,c){return this.optional(b)||a<=c},range:function(a,b,c){return this.optional(b)||a>=c[0]&&a<=c[1]},step:function(b,c,d){var e,f=a(c).attr("type"),g="Step attribute on input type "+f+" is not supported.",h=["text","number","range"],i=new RegExp("\\b"+f+"\\b"),j=f&&!i.test(h.join()),k=function(a){var b=(""+a).match(/(?:\.(\d+))?$/);return b&&b[1]?b[1].length:0},l=function(a){return Math.round(a*Math.pow(10,e))},m=!0;if(j)throw new Error(g);return e=k(d),(k(b)>e||l(b)%l(d)!==0)&&(m=!1),this.optional(c)||m},equalTo:function(b,c,d){var e=a(d);return this.settings.onfocusout&&e.not(".validate-equalTo-blur").length&&e.addClass("validate-equalTo-blur").on("blur.validate-equalTo",function(){a(c).valid()}),b===e.val()},remote:function(b,c,d,e){if(this.optional(c))return"dependency-mismatch";e="string"==typeof e&&e||"remote";var f,g,h,i=this.previousValue(c,e);return this.settings.messages[c.name]||(this.settings.messages[c.name]={}),i.originalMessage=i.originalMessage||this.settings.messages[c.name][e],this.settings.messages[c.name][e]=i.message,d="string"==typeof d&&{url:d}||d,h=a.param(a.extend({data:b},d.data)),i.old===h?i.valid:(i.old=h,f=this,this.startRequest(c),g={},g[c.name]=b,a.ajax(a.extend(!0,{mode:"abort",port:"validate"+c.name,dataType:"json",data:g,context:f.currentForm,success:function(a){var d,g,h,j=a===!0||"true"===a;f.settings.messages[c.name][e]=i.originalMessage,j?(h=f.formSubmitted,f.resetInternals(),f.toHide=f.errorsFor(c),f.formSubmitted=h,f.successList.push(c),f.invalid[c.name]=!1,f.showErrors()):(d={},g=a||f.defaultMessage(c,{method:e,parameters:b}),d[c.name]=i.message=g,f.invalid[c.name]=!0,f.showErrors(d)),i.valid=j,f.stopRequest(c,j)}},d)),"pending")}}});var c,d={};return a.ajaxPrefilter?a.ajaxPrefilter(function(a,b,c){var e=a.port;"abort"===a.mode&&(d[e]&&d[e].abort(),d[e]=c)}):(c=a.ajax,a.ajax=function(b){var e=("mode"in b?b:a.ajaxSettings).mode,f=("port"in b?b:a.ajaxSettings).port;return"abort"===e?(d[f]&&d[f].abort(),d[f]=c.apply(this,arguments),d[f]):c.apply(this,arguments)}),a});
\ No newline at end of file
diff --git a/templates/accounts/customer/account_transfer_email_template.html b/templates/accounts/customer/account_transfer_email_template.html
new file mode 100644
index 0000000..42fea12
--- /dev/null
+++ b/templates/accounts/customer/account_transfer_email_template.html
@@ -0,0 +1,31 @@
+
+
+
+ Your Exclusive Account Access Details with Good Times!
+
+
+
+
Dear Valued Customer,
+
Greetings from Good Times! We trust this correspondence finds you in splendid spirits.
+
We are pleased to provide you with your account credentials for seamless access:
+
+
+
Username:
+
{{ principal_obj.email }}
+
+
+
Password:
+
{{ temp_password }}
+
+
+
Please utilize the temporary password to access your account promptly. Upon your initial login, we recommend changing your password to further enhance security measures.
+
We sincerely hope your experience with Good Times has been delightful thus far and look forward to continuing to exceed your expectations!
+
Warmest regards,
+ Good Times
+ {{ settings.DEFAULT_FROM_EMAIL }}
+
+
\ No newline at end of file
diff --git a/templates/accounts/customer/customer_add.html b/templates/accounts/customer/customer_add.html
new file mode 100644
index 0000000..d147cf1
--- /dev/null
+++ b/templates/accounts/customer/customer_add.html
@@ -0,0 +1,282 @@
+{% extends 'layout/base_template.html' %}
+{% load static %}
+{% block stylesheet %}
+
+
+{% include "cdn_through_html/flatpicker_cdn_css.html" %}
+{% include "cdn_through_html/filepond_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' %}
+
+
+ {% 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' %}
+
+
+ {% 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' %}
+
@@ -46,10 +38,19 @@
style="width: 69.2656px;"> Title
Plan Days
+ style="width: 69.2656px;"> Interval
+
Interval Count
Amount
+
Customer Type
+
Free for Admin
Active
{{data_obj.id}}
{{data_obj.title}}
-
{{data_obj.plan.days}}
+
{{data_obj.interval | capfirst}}
+
{{data_obj.interval_count }}
{{data_obj.amount}}
+
+ {% if data_obj.principal_types.all %}
+ {% for data in data_obj.principal_types.all %}
+ {{ data.name }}
+ {% endfor %}
+ {% else %}
+ No user type
+ {% endif %}
+