Compare commits
288 Commits
97cf4bc753
...
b3e23de837
| Author | SHA1 | Date | |
|---|---|---|---|
| b3e23de837 | |||
|
|
9d20583a78 | ||
|
|
5693ec3c48 | ||
|
|
bec8f9d608 | ||
|
|
b559c62926 | ||
|
|
24949ade3e | ||
|
|
3425d82891 | ||
|
|
8ccec7012c | ||
|
|
3f263b2af5 | ||
|
|
76ac9413c4 | ||
|
|
76323a1ea1 | ||
|
|
eef12c8579 | ||
|
|
9ba960fcad | ||
|
|
6f07fe4877 | ||
|
|
540442fa28 | ||
|
|
449d8615c8 | ||
|
|
389dbbe390 | ||
|
|
b6b4295d7e | ||
|
|
cb904b3057 | ||
|
|
89639c7a38 | ||
|
|
1caad5ceb5 | ||
|
|
b3db97baf4 | ||
|
|
bef5a52b55 | ||
|
|
aefce76db3 | ||
|
|
5dc82d28c7 | ||
|
|
7ba73dff35 | ||
|
|
70d807220e | ||
|
|
3efc093de3 | ||
|
|
1f580c099d | ||
|
|
5d107ad17a | ||
|
|
4866f0a5d4 | ||
|
|
342b36efc6 | ||
|
|
88fd84bbdb | ||
|
|
b43bde15c2 | ||
|
|
c57ff88ec0 | ||
|
|
1b3fc0e4db | ||
|
|
7710f2be9b | ||
|
|
a8fee238c6 | ||
|
|
5bd5b7a500 | ||
|
|
5b68e5643e | ||
|
|
66bf95ef87 | ||
|
|
797a089a50 | ||
|
|
bc3a1d741b | ||
|
|
52887b0328 | ||
|
|
bd76a3a4cf | ||
|
|
ab684a3e07 | ||
|
|
8f6d577031 | ||
|
|
e80b01a549 | ||
|
|
a0dabe85ac | ||
|
|
e57901de33 | ||
|
|
9c2b484fa0 | ||
|
|
7278ef6d92 | ||
|
|
f22777a72f | ||
|
|
8f59f81796 | ||
|
|
cfff16393f | ||
|
|
438f0005ee | ||
|
|
efbdc47aab | ||
|
|
e47a96c742 | ||
|
|
5cee9f21ad | ||
|
|
08adadffbf | ||
|
|
6e93793665 | ||
|
|
ee27d86010 | ||
|
|
15a7308cb8 | ||
|
|
1a2013e10b | ||
|
|
e99095fb19 | ||
|
|
234e9e900a | ||
|
|
390206548b | ||
|
|
4c2d693ebf | ||
|
|
e0ed41f096 | ||
|
|
32a834726e | ||
|
|
70c5c815fa | ||
|
|
e8b90b8fdb | ||
|
|
33a11af520 | ||
|
|
87f711b3f5 | ||
|
|
9894a94b8c | ||
|
|
593dac0a0c | ||
|
|
b837749fe0 | ||
|
|
8fb1a7eb41 | ||
|
|
a81b5b4f7f | ||
|
|
6e8eb96f4f | ||
|
|
48d18d6ef3 | ||
|
|
93796ceb46 | ||
|
|
6d4ebe533a | ||
|
|
5b29245ab2 | ||
|
|
7aabd0c9fa | ||
|
|
fccd7378f7 | ||
|
|
3ac7e0d522 | ||
|
|
73ca0ea974 | ||
|
|
85767e7b96 | ||
|
|
06bfae09b3 | ||
|
|
f595089b05 | ||
|
|
a6dfdf8cd1 | ||
|
|
dbed6999dc | ||
|
|
33d1c3c94e | ||
|
|
c52b42e67d | ||
|
|
a75b579459 | ||
|
|
0a2738496d | ||
|
|
74425add85 | ||
|
|
442ce193d4 | ||
|
|
338f5c29c5 | ||
|
|
4397ac52b8 | ||
|
|
1711af39bc | ||
|
|
9ef1593f33 | ||
|
|
eb18a5d4dc | ||
|
|
d1ab24dc0d | ||
|
|
f7d904fee1 | ||
|
|
0036e3246e | ||
|
|
1cf66abbff | ||
|
|
731f162531 | ||
|
|
b958765b29 | ||
|
|
deeb584c12 | ||
|
|
41ddf0ebca | ||
|
|
d585a8311b | ||
|
|
0f7ec4418c | ||
|
|
cf7f1a5b51 | ||
|
|
4e1d533deb | ||
|
|
54bdca8630 | ||
|
|
0ba056cb63 | ||
|
|
32afbd952e | ||
|
|
31f9e768ea | ||
|
|
e3b86d64fd | ||
|
|
e3189344ea | ||
|
|
6e0d34312c | ||
|
|
ce676aa81d | ||
|
|
7f7ef04a25 | ||
|
|
27a124b0b2 | ||
|
|
7992e456a4 | ||
|
|
77be3218b7 | ||
|
|
036b8ae935 | ||
|
|
d739d9bd91 | ||
|
|
cdf9d06e8c | ||
|
|
53a572c19e | ||
|
|
2db723f12d | ||
|
|
9397b26708 | ||
|
|
4afbfb33f7 | ||
|
|
8d212430ec | ||
|
|
8bcc4bef7d | ||
|
|
a68f114f99 | ||
|
|
66c5b8674a | ||
|
|
33030ca728 | ||
|
|
6a8dc781d2 | ||
|
|
473a961ab6 | ||
|
|
ab80b06f54 | ||
|
|
46802f202a | ||
|
|
b226e1f584 | ||
|
|
81e35c1562 | ||
|
|
e96bfe9068 | ||
|
|
f17f523e25 | ||
|
|
ee25cf5a8b | ||
|
|
161cba37ab | ||
|
|
f918a2f76f | ||
|
|
3da21b0c0b | ||
|
|
29011dff06 | ||
|
|
7921c3f89d | ||
|
|
d670a18599 | ||
|
|
8faa9eb81f | ||
|
|
83efd28687 | ||
|
|
91dba0308c | ||
|
|
23bae8cf3d | ||
|
|
5d320cccfe | ||
|
|
38fad86990 | ||
|
|
2db24f203c | ||
|
|
31e9d5a03c | ||
|
|
b8ac4ed4e6 | ||
|
|
1ebf7f6838 | ||
|
|
e25f402cea | ||
|
|
3396f5ce01 | ||
|
|
d602988b81 | ||
|
|
d5b69ce6bd | ||
|
|
dea6b60c43 | ||
|
|
fa05aee1b1 | ||
|
|
3ae21b747d | ||
|
|
04e428fcf6 | ||
|
|
f336c1f866 | ||
|
|
92f597d830 | ||
|
|
a5ce725b30 | ||
|
|
5cb9c51bc2 | ||
|
|
35d904603d | ||
|
|
8e7f2d6d69 | ||
|
|
a9cb97b406 | ||
|
|
2398d91c93 | ||
|
|
9abfb25cc2 | ||
|
|
ed92443e36 | ||
|
|
142803a5de | ||
|
|
7a0f82b4a6 | ||
|
|
a459742801 | ||
|
|
ef8f91809e | ||
|
|
1ed0d78fa6 | ||
|
|
912c6e42d9 | ||
|
|
ad649848cd | ||
|
|
79422fbf0c | ||
|
|
8ee574de52 | ||
|
|
dd41d4856a | ||
|
|
c87cf3eea8 | ||
|
|
5b201bf91f | ||
|
|
b6a0b0b537 | ||
|
|
8ab25d6c2c | ||
|
|
15265b1914 | ||
|
|
36912fec48 | ||
|
|
d1f24f0867 | ||
|
|
346ff66eeb | ||
|
|
9a3ff98253 | ||
|
|
c639e66ed8 | ||
|
|
e9079f6c82 | ||
|
|
1454fc2838 | ||
|
|
f746f0c29a | ||
|
|
b0e8df3437 | ||
|
|
e6ca0c5c71 | ||
|
|
59a9a5eb98 | ||
|
|
2126660d9b | ||
|
|
cc6345cc4c | ||
|
|
a303d1056d | ||
|
|
fd73d1fb9c | ||
|
|
9d502d5b60 | ||
|
|
6ae855cfe1 | ||
|
|
25ab9b0341 | ||
|
|
9b37e0ee5b | ||
|
|
c5ac748ca6 | ||
|
|
d43d135123 | ||
|
|
6241c5253d | ||
|
|
ecd9246375 | ||
|
|
ea80afb990 | ||
|
|
b6a4567ff8 | ||
|
|
19363af620 | ||
|
|
d09ff0f973 | ||
|
|
5ddccc0860 | ||
|
|
10171170f4 | ||
|
|
a53d221091 | ||
|
|
6262afea17 | ||
|
|
bb99a3e049 | ||
|
|
051d8788bd | ||
|
|
6e0812df38 | ||
|
|
e7b5b37e58 | ||
|
|
b7309f1e68 | ||
|
|
a9725dd0fc | ||
|
|
6a0cddffde | ||
|
|
a3d31f8dd7 | ||
|
|
09e1702a46 | ||
|
|
97bae9e940 | ||
|
|
2f6187df22 | ||
|
|
df59f0714e | ||
|
|
5b404ecd7b | ||
|
|
679abb78fe | ||
|
|
d74fbba0c5 | ||
|
|
ff74aad3e5 | ||
|
|
411dd517a9 | ||
|
|
572143ca4f | ||
|
|
496e8a3fba | ||
|
|
8cb8ab7e97 | ||
|
|
3cab0dbbc4 | ||
|
|
08ae3d5cef | ||
|
|
685ee8a9fb | ||
|
|
eacb7c5cff | ||
|
|
1afda70bf1 | ||
|
|
0079a2f55f | ||
|
|
ca08d0aeb0 | ||
|
|
946b8f016e | ||
|
|
d4a49fcf56 | ||
|
|
fb34cac60f | ||
|
|
ed5d5253f0 | ||
|
|
17a179d278 | ||
|
|
cb0b0a3e68 | ||
|
|
2b170ee912 | ||
|
|
4040ddfd43 | ||
|
|
d036dac763 | ||
|
|
c13f992e36 | ||
|
|
33c9795920 | ||
|
|
899c2c1685 | ||
|
|
98f7628854 | ||
|
|
a6979aba61 | ||
|
|
e23576658b | ||
|
|
478783054b | ||
|
|
8413b058a7 | ||
|
|
446535855b | ||
|
|
b36f983de3 | ||
|
|
f35ffded81 | ||
|
|
0db4639b6a | ||
|
|
ff433e4a76 | ||
|
|
4067285d39 | ||
|
|
c5e3a8afd1 | ||
|
|
8ba3b6d250 | ||
|
|
5b98584c57 | ||
|
|
a35f5f92d4 | ||
|
|
150249cce3 | ||
|
|
348ed6e910 | ||
|
|
af0426c51c | ||
|
|
507f147e51 | ||
|
|
e5017899e3 |
@@ -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",
|
||||
|
||||
@@ -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"] = (
|
||||
@@ -246,12 +289,10 @@ 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)
|
||||
@@ -349,3 +390,9 @@ class AppVersionSerializer(serializers.ModelSerializer):
|
||||
"force_upgrade",
|
||||
"recommend_upgrade",
|
||||
]
|
||||
|
||||
|
||||
class IAmPrincipalExtendedDataSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = IAmPrincipalExtendedData
|
||||
fields = "__all__"
|
||||
|
||||
@@ -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/<str:principal_type>/', 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'),
|
||||
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
@@ -961,12 +978,74 @@ class VersionCheck(APIView):
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
@@ -358,3 +359,91 @@ class IAmPrincipalResourceLinkForm(IAmPrincipalForm):
|
||||
# Save the instance to the database
|
||||
if commit:
|
||||
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()
|
||||
|
||||
20
accounts/migrations/0010_alter_appversion_app_type.py
Normal file
20
accounts/migrations/0010_alter_appversion_app_type.py
Normal file
@@ -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
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
28
accounts/migrations/0012_iamprincipalextendeddata.py
Normal file
28
accounts/migrations/0012_iamprincipalextendeddata.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.0.2 on 2024-06-25 07:28
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0011_alter_iamprincipallocation_principal'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='IAmPrincipalExtendedData',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('is_onboarded', models.BooleanField(default=False, help_text='Indicates whether the user was onboarded by an admin.')),
|
||||
('is_transferred', models.BooleanField(default=False, help_text='Indicates whether the account has been transferred to the user.')),
|
||||
('transferred_on', models.DateTimeField(blank=True, help_text='The date and time when the account was transferred to the user.', null=True)),
|
||||
('principal', models.OneToOneField(help_text='The principal user to which this extended data is related.', on_delete=django.db.models.deletion.CASCADE, related_name='extended_data', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'iam_principal_extended_data',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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.'),
|
||||
),
|
||||
]
|
||||
18
accounts/migrations/0014_iamprincipal_business_name.py
Normal file
18
accounts/migrations/0014_iamprincipal_business_name.py
Normal file
@@ -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'),
|
||||
),
|
||||
]
|
||||
18
accounts/migrations/0015_iamprincipal_twitter_profile.py
Normal file
18
accounts/migrations/0015_iamprincipal_twitter_profile.py
Normal file
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.2 on 2024-12-20 12:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0015_iamprincipal_twitter_profile'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='iamprincipalextendeddata',
|
||||
name='encrypted_pass',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -13,6 +13,7 @@ from django.utils.text import slugify
|
||||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
|
||||
# from manage_subscriptions.models import Subscription
|
||||
|
||||
from goodtimes.utils import RandomGenerator
|
||||
from .resource_action import (
|
||||
PRINCIPAL_TYPE_EVENT_USER,
|
||||
@@ -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 = []
|
||||
@@ -332,6 +335,52 @@ 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(
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -42,7 +42,15 @@ urlpatterns = [
|
||||
path('principal/role/delete/<int:pk>/', views.AppRoleDeleteView.as_view(), name="role_delete"),
|
||||
|
||||
path('customer/', views.CustomerListView.as_view(), name="customer_list"),
|
||||
|
||||
path('customer/get-decrypted-password/<int:customer_id>/', views.GetDecryptedPasswordView.as_view(), name='get_decrypted_password'),
|
||||
path('customer/add/', views.CustomerCreateView.as_view(), name="customer_add"),
|
||||
path('customer/edit/<int:pk>/', views.CustomerUpdateView.as_view(), name="customer_edit"),
|
||||
path('customer/detail/<int:pk>/', views.CustomerDetailView.as_view(), name="customer_detail"),
|
||||
path('customer/transfer/<int:pk>/', views.CustomerTransferView.as_view(), name="customer_transfer"),
|
||||
path('customer/check-email/', views.CustomerCheckEmail.as_view(), name="customer_check_email"),
|
||||
path('customer/download-excel-template/', views.export_excel_template, name='download_excel_template'),
|
||||
path('customer/import-customer-data/', views.CustomerImportView.as_view(), name='import_customer_data'),
|
||||
path('customer/export-customer-data/', views.CustomerExportView.as_view(), name='export_customer_data'),
|
||||
|
||||
# ignore this to path this for example setup
|
||||
path('principal/example/', TemplateView.as_view(template_name="accounts/iam_module/example_form.html"), name="example_add"),
|
||||
|
||||
@@ -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
|
||||
@@ -603,10 +874,457 @@ class CustomerListView(LoginRequiredMixin, generic.ListView):
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -23,6 +23,7 @@ EMAIL_PORT=
|
||||
EMAIL_HOST_USER=
|
||||
EMAIL_HOST_PASSWORD=
|
||||
EMAIL_USE_TLS=
|
||||
DEFAULT_FROM_EMAIL=
|
||||
|
||||
GOOGLE_MAPS_API_KEY=
|
||||
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
32
goodtimes/mixins.py
Normal file
32
goodtimes/mixins.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from django.views import generic
|
||||
from goodtimes.utils import JsonResponseUtil
|
||||
|
||||
|
||||
class ActionMixin(generic.View):
|
||||
model = None
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
if self.model is None:
|
||||
raise NotImplementedError("Subclasses of BaseActionView must define a 'model' attribute.")
|
||||
|
||||
action = request.POST.get('action') # 'archive', 'active', or 'unarchive'
|
||||
ids = request.POST.getlist('ids[]') # List of IDs to perform action on
|
||||
active = request.POST.get('active')
|
||||
print(f"arhive action {action} and id is {ids} and active data is {active}")
|
||||
if action == 'archive':
|
||||
# Update 'deleted' field to True for the selected users
|
||||
self.model.objects.filter(id__in=ids).update(deleted=True, active=False)
|
||||
message = 'Record archived successfully.'
|
||||
elif action == 'active':
|
||||
# Update 'active' field to True for the selected users
|
||||
self.model.objects.filter(id__in=ids).update(active=active.capitalize())
|
||||
message = 'Record updated successfully.'
|
||||
elif action == 'unarchive':
|
||||
# Update 'deleted' field to False for the selected users
|
||||
self.model.objects.filter(id__in=ids).update(deleted=False)
|
||||
message = 'Record unarchived successfully.'
|
||||
else:
|
||||
return JsonResponseUtil.error(message="Invalid Action")
|
||||
|
||||
return JsonResponseUtil.success(message=message)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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')
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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))]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
# )
|
||||
0
goodtimes/webhook/__init__.py
Normal file
0
goodtimes/webhook/__init__.py
Normal file
80
goodtimes/webhook/notification_service.py
Normal file
80
goodtimes/webhook/notification_service.py
Normal file
@@ -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
|
||||
180
goodtimes/webhook/payment_processing_service.py
Normal file
180
goodtimes/webhook/payment_processing_service.py
Normal file
@@ -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()
|
||||
110
goodtimes/webhook/referral_reward_service.py
Normal file
110
goodtimes/webhook/referral_reward_service.py
Normal file
@@ -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)
|
||||
78
goodtimes/webhook/subscription_service.py
Normal file
78
goodtimes/webhook/subscription_service.py
Normal file
@@ -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!!!")
|
||||
90
goodtimes/webhook/webhook_service.py
Normal file
90
goodtimes/webhook/webhook_service.py
Normal file
@@ -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
|
||||
)
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
@@ -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(
|
||||
|
||||
0
manage_coupons/__init__.py
Normal file
0
manage_coupons/__init__.py
Normal file
40
manage_coupons/admin.py
Normal file
40
manage_coupons/admin.py
Normal file
@@ -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)
|
||||
0
manage_coupons/api/serializers.py
Normal file
0
manage_coupons/api/serializers.py
Normal file
0
manage_coupons/api/urls.py
Normal file
0
manage_coupons/api/urls.py
Normal file
0
manage_coupons/api/views.py
Normal file
0
manage_coupons/api/views.py
Normal file
6
manage_coupons/apps.py
Normal file
6
manage_coupons/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ManageCouponsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "manage_coupons"
|
||||
24
manage_coupons/forms.py
Normal file
24
manage_coupons/forms.py
Normal file
@@ -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'}),
|
||||
}
|
||||
81
manage_coupons/migrations/0001_initial.py
Normal file
81
manage_coupons/migrations/0001_initial.py
Normal file
@@ -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",
|
||||
},
|
||||
),
|
||||
]
|
||||
18
manage_coupons/migrations/0002_coupon_coupon_id.py
Normal file
18
manage_coupons/migrations/0002_coupon_coupon_id.py
Normal file
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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.'),
|
||||
),
|
||||
]
|
||||
0
manage_coupons/migrations/__init__.py
Normal file
0
manage_coupons/migrations/__init__.py
Normal file
104
manage_coupons/models.py
Normal file
104
manage_coupons/models.py
Normal file
@@ -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)
|
||||
# )
|
||||
3
manage_coupons/tests.py
Normal file
3
manage_coupons/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
23
manage_coupons/urls.py
Normal file
23
manage_coupons/urls.py
Normal file
@@ -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/<int:pk>/",
|
||||
# views.CouponCreateOrUpdateView.as_view(),
|
||||
# name="coupon_edit",
|
||||
# ),
|
||||
path(
|
||||
"coupon/delete/<int:pk>/",
|
||||
views.CouponDeleteView.as_view(),
|
||||
name="coupon_delete",
|
||||
),
|
||||
]
|
||||
47
manage_coupons/utils.py
Normal file
47
manage_coupons/utils.py
Normal file
@@ -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
|
||||
129
manage_coupons/views.py
Normal file
129
manage_coupons/views.py
Normal file
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
52
manage_events/api/filters.py
Normal file
52
manage_events/api/filters.py
Normal file
@@ -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
|
||||
@@ -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",
|
||||
@@ -148,6 +187,12 @@ class EventDetailSerializer(serializers.ModelSerializer):
|
||||
}
|
||||
return None
|
||||
|
||||
# def to_representation(self, instance):
|
||||
# """Customize the representation of the instance."""
|
||||
# representation = super().to_representation(instance)
|
||||
# representation['key_guest'] = instance.get_key_guests() # Return as a list
|
||||
# return representation
|
||||
|
||||
|
||||
class CreateEventSerializer(serializers.ModelSerializer):
|
||||
tags = TagListSerializerField(required=False)
|
||||
@@ -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(
|
||||
|
||||
@@ -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/<int:pk>/", views.EventEditAPIView.as_view(), name="event-edit"),
|
||||
path("get-events/", views.EventsAPIView.as_view(), name="events"),
|
||||
|
||||
path(
|
||||
"event/<int:pk>/",
|
||||
views.EventDetailAPIView.as_view(),
|
||||
@@ -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/<int:pk>/view/",
|
||||
views.CaptureEventViewAPIView.as_view(),
|
||||
name="capture_event_view",
|
||||
),
|
||||
# For counting event shares
|
||||
path(
|
||||
"event/<int:pk>/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/<int:id>/", views.SocialMediaPostView.as_view(), name="social_media_post")
|
||||
]
|
||||
|
||||
@@ -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,21 +438,57 @@ class IAmPrincipalLocationAPIView(APIView):
|
||||
)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
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(
|
||||
data=request.data, context={"request": request}
|
||||
location, context={"request": request}
|
||||
)
|
||||
print("serializer: ", serializer)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return ApiResponse.success(
|
||||
status=status.HTTP_200_OK,
|
||||
message=constants.SUCCESS,
|
||||
data=serializer.data,
|
||||
)
|
||||
|
||||
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_400_BAD_REQUEST,
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
message=constants.FAILURE,
|
||||
errors=serializer.errors,
|
||||
errors=str(e),
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
# 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}),
|
||||
}
|
||||
|
||||
@@ -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)}"))
|
||||
33
manage_events/management/commands/manager_report.py
Normal file
33
manage_events/management/commands/manager_report.py
Normal file
@@ -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()
|
||||
20
manage_events/management/commands/populate_age_group.py
Normal file
20
manage_events/management/commands/populate_age_group.py
Normal file
@@ -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()
|
||||
30
manage_events/management/commands/test_facebook_api.py
Normal file
30
manage_events/management/commands/test_facebook_api.py
Normal file
@@ -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']))
|
||||
33
manage_events/management/commands/test_instagram_api.py
Normal file
33
manage_events/management/commands/test_instagram_api.py
Normal file
@@ -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']))
|
||||
28
manage_events/management/commands/test_twitter_api.py
Normal file
28
manage_events/management/commands/test_twitter_api.py
Normal file
@@ -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']))
|
||||
103
manage_events/management/commands/update_facebook_tokens.py
Normal file
103
manage_events/management/commands/update_facebook_tokens.py
Normal file
@@ -0,0 +1,103 @@
|
||||
|
||||
import os
|
||||
from django.conf import settings
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
# Load .env variables
|
||||
load_dotenv()
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Update Facebook long-lived access tokens and page access token'
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.app_id = settings.FACEBOOK_APP_ID
|
||||
self.app_secret = settings.FACEBOOK_APP_SECRET
|
||||
self.page_id = settings.FACEBOOK_PAGE_ID
|
||||
self.graph_api_version = settings.FACEBOOK_GRAPH_VERSION_API
|
||||
self.page_access_token = settings.FACEBOOK_ACCESS_TOKEN
|
||||
self.long_lived_token = settings.FACEBOOK_ACCESS_TOKEN
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
"""Handle the token refresh and update .env file."""
|
||||
if self.refresh_access_tokens():
|
||||
self.stdout.write(self.style.SUCCESS("Successfully refreshed Facebook tokens."))
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR("Failed to refresh Facebook tokens."))
|
||||
|
||||
def _exchange_short_to_long_lived_token(self, short_lived_token):
|
||||
"""Exchange short-lived token for long-lived token."""
|
||||
try:
|
||||
url = f"https://graph.facebook.com/{self.graph_api_version}/oauth/access_token"
|
||||
params = {
|
||||
"grant_type": "fb_exchange_token",
|
||||
"client_id": self.app_id,
|
||||
"client_secret": self.app_secret,
|
||||
"fb_exchange_token": short_lived_token,
|
||||
}
|
||||
response = requests.get(url, params=params)
|
||||
response.raise_for_status()
|
||||
long_lived_token = response.json().get("access_token")
|
||||
self.stdout.write(self.style.SUCCESS("Successfully exchanged for long-lived user access token."))
|
||||
return long_lived_token
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.stdout.write(self.style.ERROR(f"Error exchanging short-lived token: {e}"))
|
||||
return None
|
||||
|
||||
def _get_page_access_token(self, user_token):
|
||||
"""Retrieve Page Access Token."""
|
||||
try:
|
||||
url = f"https://graph.facebook.com/{self.graph_api_version}/{self.page_id}"
|
||||
params = {
|
||||
"fields": "access_token",
|
||||
"access_token": user_token,
|
||||
}
|
||||
response = requests.get(url, params=params)
|
||||
response.raise_for_status()
|
||||
page_access_token = response.json().get("access_token")
|
||||
self.stdout.write(self.style.SUCCESS("Successfully obtained page access token."))
|
||||
return page_access_token
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.stdout.write(self.style.ERROR(f"Error retrieving page access token: {e}"))
|
||||
return None
|
||||
|
||||
def _update_env_variable(self, key, value):
|
||||
"""Update a variable in the .env file."""
|
||||
with open('.env', 'r') as file:
|
||||
lines = file.readlines()
|
||||
|
||||
with open('.env', 'w') as file:
|
||||
updated = False
|
||||
for line in lines:
|
||||
if line.startswith(key):
|
||||
file.write(f"{key}={value}\n")
|
||||
updated = True
|
||||
else:
|
||||
file.write(line)
|
||||
if not updated:
|
||||
file.write(f"{key}={value}\n")
|
||||
|
||||
def refresh_access_tokens(self):
|
||||
"""Refresh long-lived user access token and page access token."""
|
||||
if not self.long_lived_token:
|
||||
self.stdout.write(self.style.ERROR("No valid long-lived user access token found."))
|
||||
return False
|
||||
|
||||
# Refresh long-lived user token (optional, based on expiry)
|
||||
refreshed_user_token = self._exchange_short_to_long_lived_token(self.long_lived_token)
|
||||
if refreshed_user_token:
|
||||
self.long_lived_token = refreshed_user_token
|
||||
|
||||
# Refresh page access token
|
||||
# page_access_token = self._get_page_access_token(self.long_lived_token)
|
||||
# if page_access_token:
|
||||
# self.page_access_token = page_access_token
|
||||
# # Update tokens in .env file
|
||||
self._update_env_variable("FACEBOOK_ACCESS_TOKEN", self.long_lived_token)
|
||||
# self._update_env_variable("FACEBOOK_PAGE_ACCESS_TOKEN", self.page_access_token)
|
||||
return True
|
||||
|
||||
self.stdout.write(self.style.ERROR("Failed to refresh page access token."))
|
||||
return False
|
||||
75
manage_events/migrations/0009_eventview.py
Normal file
75
manage_events/migrations/0009_eventview.py
Normal file
@@ -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,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
},
|
||||
),
|
||||
]
|
||||
20
manage_events/migrations/0011_alter_event_entry_type.py
Normal file
20
manage_events/migrations/0011_alter_event_entry_type.py
Normal file
@@ -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
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
21
manage_events/migrations/0013_venue_principal.py
Normal file
21
manage_events/migrations/0013_venue_principal.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.0.2 on 2024-06-25 17:04
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('manage_events', '0012_event_coupon_code_event_coupon_description'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='venue',
|
||||
name='principal',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='venues_principal', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
21
manage_events/migrations/0014_event_principal.py
Normal file
21
manage_events/migrations/0014_event_principal.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.0.2 on 2024-06-25 17:09
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('manage_events', '0013_venue_principal'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='principal',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='events_principal', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
32
manage_events/migrations/0015_agegroups.py
Normal file
32
manage_events/migrations/0015_agegroups.py
Normal file
@@ -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',
|
||||
},
|
||||
),
|
||||
]
|
||||
25
manage_events/migrations/0016_freeusagefeaturelimit.py
Normal file
25
manage_events/migrations/0016_freeusagefeaturelimit.py
Normal file
@@ -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',
|
||||
},
|
||||
),
|
||||
]
|
||||
18
manage_events/migrations/0017_venue_postcode.py
Normal file
18
manage_events/migrations/0017_venue_postcode.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.2 on 2024-12-20 09:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('manage_events', '0016_freeusagefeaturelimit'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='venue',
|
||||
name='postcode',
|
||||
field=models.CharField(blank=True, max_length=20, null=True),
|
||||
),
|
||||
]
|
||||
18
manage_events/migrations/0018_event_link.py
Normal file
18
manage_events/migrations/0018_event_link.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.2 on 2024-12-24 11:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('manage_events', '0017_venue_postcode'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='link',
|
||||
field=models.URLField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
378
manage_events/report.py
Normal file
378
manage_events/report.py
Normal file
@@ -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
|
||||
@@ -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/<int:user_id>/",
|
||||
views.GenerateEventReportView.as_view(),
|
||||
name="generate_event_report",
|
||||
),
|
||||
|
||||
path("post-to-social-media/<int:id>/<str:platform>/", views.SocialMediaPostView.as_view(), name="social_media_post")
|
||||
]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -257,6 +262,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)
|
||||
@@ -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")
|
||||
|
||||
@@ -75,12 +75,20 @@ class Command(BaseCommand):
|
||||
message = f"Your subscription is going to expire in {days_before} days."
|
||||
|
||||
for principal in eligible_principals:
|
||||
notification_title = "Subscription Expiry Reminder"
|
||||
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_title = "Subscription Expiry Reminder"
|
||||
notification_payload = {
|
||||
"headings": {"en": notification_title},
|
||||
"contents": {"en": message},
|
||||
@@ -88,13 +96,9 @@ class Command(BaseCommand):
|
||||
}
|
||||
response = client.send_notification(notification_payload)
|
||||
|
||||
in_app_notification = InAppNotification(
|
||||
principal=principal.principal,
|
||||
title=notification_title,
|
||||
message=message,
|
||||
notification_category=NotificationCategoryChoices.SUBSCRIPTION,
|
||||
)
|
||||
# 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")
|
||||
|
||||
@@ -50,4 +50,5 @@ class ReferralRecordRewardSerializer(serializers.ModelSerializer):
|
||||
"coins",
|
||||
"unique_token",
|
||||
"value",
|
||||
"created_on",
|
||||
]
|
||||
|
||||
@@ -24,4 +24,9 @@ urlpatterns = [
|
||||
views.RedeemRewardView.as_view(),
|
||||
name="redeem_reward",
|
||||
),
|
||||
path(
|
||||
"redeem-selected-rewards/",
|
||||
views.RedeemSelectedRewardsView.as_view(),
|
||||
name="redeem_selected_rewards",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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.",
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
from rest_framework_simplejwt.views import (
|
||||
TokenRefreshView,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
@@ -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",
|
||||
# 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 = services.PaymentProcessingService(webhook_data=event)
|
||||
payment_service = PaymentProcessingService(webhook_data=event)
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -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"}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
18
manage_subscriptions/migrations/0008_subscription_is_free.py
Normal file
18
manage_subscriptions/migrations/0008_subscription_is_free.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.2 on 2024-06-25 07:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('manage_subscriptions', '0007_alter_subscription_referral_percentage'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='subscription',
|
||||
name='is_free',
|
||||
field=models.BooleanField(default=False, help_text='Indicates whether this subscription is free and only accessible by administrators, not visible to regular users.'),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user