diff --git a/accounts/api/serializers.py b/accounts/api/serializers.py index e932668..de7f3a4 100644 --- a/accounts/api/serializers.py +++ b/accounts/api/serializers.py @@ -10,7 +10,8 @@ from accounts.models import ( 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, @@ -142,15 +143,9 @@ from phonenumbers import parse, phonenumberutil, NumberParseException class ProfileSerializer(serializers.ModelSerializer): profile_photo = serializers.ImageField(required=False) - email = serializers.CharField(read_only=True) - invite_count = serializers.SerializerMethodField(read_only=True) principal_type_name = serializers.SerializerMethodField(read_only=True) - has_active_subscription = serializers.SerializerMethodField(read_only=True) - has_preferences = serializers.SerializerMethodField(read_only=True) - register_complete = serializers.BooleanField(read_only=True) + email = serializers.CharField(read_only=True) is_active = serializers.BooleanField(read_only=True) - going_events_count = serializers.SerializerMethodField(read_only=True) - interested_events_count = serializers.SerializerMethodField(read_only=True) phone_no = serializers.CharField(required=True) class Meta: @@ -164,18 +159,12 @@ class ProfileSerializer(serializers.ModelSerializer): "last_name", "phone_no", "email", - "invite_count", - "register_complete", - "has_active_subscription", - "has_preferences", "linkedin_profile", "youtube_profile", "facebook_profile", "instagram_profile", "website", "is_active", - "going_events_count", - "interested_events_count", ] # def validate_phone_no(self, value): @@ -197,14 +186,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): @@ -215,14 +243,15 @@ 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 = { @@ -234,18 +263,9 @@ class ProfileSerializer(serializers.ModelSerializer): 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"] = ( @@ -267,12 +287,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) diff --git a/accounts/api/urls.py b/accounts/api/urls.py index 6a51de5..783612e 100644 --- a/accounts/api/urls.py +++ b/accounts/api/urls.py @@ -17,7 +17,8 @@ urlpatterns = [ path('request-otp/', views.OtpRequestView.as_view(), name='send_otp'), path('verify-otp/', views.OTPVerificationView.as_view(), name='send_otp'), - # path('profile/view//', views.ProfileView.as_view(), name='profile_veiw'), + path('profile/extended-data/', views.ProfileExtendedView.as_view(), name='profile_veiw'), + path('profile/view/', views.ProfileView.as_view(), name='profile_veiw'), path('profile/edit/', views.ProfileView.as_view(), name='profile_edit'), path('profile/password-reset/', views.ProfilePasswordResetView.as_view(), name='password_reset'), diff --git a/accounts/api/views.py b/accounts/api/views.py index f967265..26cf063 100644 --- a/accounts/api/views.py +++ b/accounts/api/views.py @@ -44,6 +44,7 @@ from .serializers import ( EmailSerializer, IAmPrincipalExtendedDataSerializer, PlayerIDSerializer, + ProfileExtendedDataSerializer, RegistrationPasswordSerializer, RegistrationSerializer, ReferralCodeSerializer, @@ -538,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] @@ -1003,7 +1019,7 @@ class AccountTransferCheckView(APIView): "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) @@ -1032,6 +1048,4 @@ class AccountTransferCheckView(APIView): obj.pwd_changed_post_transfer = True obj.save() serializer = self.serializer_class(obj) - return ApiResponse.success(message=constants.SUCCESS, data=serializer.data) - - + return ApiResponse.success(message=constants.SUCCESS, data=serializer.data) \ No newline at end of file diff --git a/accounts/forms.py b/accounts/forms.py index a70b5f7..a6eab95 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -364,7 +364,12 @@ class IAmPrincipalResourceLinkForm(IAmPrincipalForm): class CreateCustomerForm(forms.Form): first_name = forms.CharField(max_length=255, required=True, label='First Name') last_name = forms.CharField(max_length=255, required=True, label='Last Name') + business_name = forms.CharField(max_length=200, required=True, label="Business Name") 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( @@ -383,6 +388,14 @@ class CreateCustomerForm(forms.Form): 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) @@ -391,7 +404,12 @@ class CreateCustomerForm(forms.Form): class UpdateCustomerForm(forms.Form): first_name = forms.CharField(max_length=255, required=True, label='First Name') last_name = forms.CharField(max_length=255, required=True, label='Last Name') + business_name = forms.CharField(max_length=200, required=True, label="Business Name") 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( @@ -410,6 +428,14 @@ class UpdateCustomerForm(forms.Form): 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): diff --git a/accounts/migrations/0014_iamprincipal_business_name.py b/accounts/migrations/0014_iamprincipal_business_name.py new file mode 100644 index 0000000..36e5e8d --- /dev/null +++ b/accounts/migrations/0014_iamprincipal_business_name.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.2 on 2024-08-11 16:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0013_iamprincipalextendeddata_pwd_changed_post_transfer'), + ] + + operations = [ + migrations.AddField( + model_name='iamprincipal', + name='business_name', + field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Business Name'), + ), + ] diff --git a/accounts/migrations/0015_iamprincipal_twitter_profile.py b/accounts/migrations/0015_iamprincipal_twitter_profile.py new file mode 100644 index 0000000..06cbd13 --- /dev/null +++ b/accounts/migrations/0015_iamprincipal_twitter_profile.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.2 on 2024-08-12 08:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0014_iamprincipal_business_name'), + ] + + operations = [ + migrations.AddField( + model_name='iamprincipal', + name='twitter_profile', + field=models.URLField(blank=True, max_length=255, null=True, verbose_name='Principal Twitter'), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index d7c1dd4..a7c6968 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -320,6 +320,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 = [] diff --git a/accounts/views.py b/accounts/views.py index 703c847..490ef43 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -627,11 +627,21 @@ class CustomerCreateView(LoginRequiredMixin, generic.View): email=form.cleaned_data.get('email'), first_name=form.cleaned_data.get('first_name'), last_name=form.cleaned_data.get('last_name'), + business_name=form.cleaned_data.get('business_name'), + phone_no=form.cleaned_data.get('phone_no'), password=make_password("goodtimes#2024"), 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 @@ -648,7 +658,7 @@ class CustomerCreateView(LoginRequiredMixin, generic.View): principal_preference = PrincipalPreference.objects.create(principal=principal_obj) principal_preference.preferred_categories.set(form.cleaned_data.get("preferences")) - principal_subscription= PrincipalSubscription.objects.create( + 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, @@ -660,6 +670,7 @@ class CustomerCreateView(LoginRequiredMixin, generic.View): 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) @@ -685,12 +696,28 @@ class CustomerUpdateView(LoginRequiredMixin, generic.View): def get(self, request, *args, **kwargs): principal_id = kwargs.get("pk") - principal_obj = IAmPrincipal.objects.get(pk=principal_id) + try: + principal_obj = IAmPrincipal.objects.get(pk=principal_id) + except Exception as e: + messages.error(request, f"No Record of id {principal_id} is found") + return redirect(self.success_url) + + print(f"principal address is {principal_obj.address_line1}") initial_data = { "first_name": principal_obj.first_name, "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 } @@ -714,8 +741,12 @@ class CustomerUpdateView(LoginRequiredMixin, generic.View): return render(request, self.template_name, context=context) def post(self, request, *args, **kwargs): - customer_id = kwargs.get("pk") - principal_obj = IAmPrincipal.objects.get(pk=customer_id) + 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) if not form.is_valid(): context = self.get_context_data(form=form) @@ -725,6 +756,16 @@ class CustomerUpdateView(LoginRequiredMixin, generic.View): # update principal data 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.save() # update principal preferences record @@ -760,7 +801,10 @@ class CustomerDetailView(LoginRequiredMixin, generic.DetailView): def get(self, request, *args, **kwargs): principal_obj = IAmPrincipal.objects.get(pk=kwargs.get("pk")) - principal_preference = PrincipalPreference.objects.get(principal_id=principal_obj.id) + 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() return render(request, self.template_name, locals()) @@ -878,26 +922,75 @@ from django.http import HttpResponse # wb.save(response) # return response - +# from openpyxl.styles import Font def export_excel_template(request): - # Define the columns and create an empty DataFrame - columns = ['First Name', 'Last Name', 'Email', 'Preferences(should be seperated by comma)', 'Free period start date(YYYY-MM-DD)', 'Free period end date(YYYY-MM-DD)'] + # Define the columns for the Customer Registration sheet + columns = [ + 'First Name', + 'Last Name', + 'Business Name', + 'Email', + 'Phone No', + 'Preferences (should be separated by comma)', + 'Free period start date (YYYY-MM-DD)', + 'Free period end date (YYYY-MM-DD)', + 'Address', + 'Region', + 'Country', + 'Website', + 'LinkedIn', + 'Facebook', + 'Instagram', + 'Twitter', + ] df = pd.DataFrame(columns=columns) - # Create a workbook and select the active worksheet + # Create a workbook and add the Customer Registration worksheet wb = Workbook() - ws = wb.active - ws.title = 'Customer Registration' + ws_customer = wb.active + ws_customer.title = 'Manager Onboarding' - # Write the column headers + # Write the column headers for the Customer Registration sheet for col_num, column_title in enumerate(df.columns, 1): - cell = ws.cell(row=1, column=col_num, value=column_title) + 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.'], + ['Category', 'A comma-separated list of event categories the customer is interested in. These should match existing categories in the system. This is a required Field'], + ['Free period start date', 'The start date of the customer\'s free trial period, formatted as YYYY-MM-DD.'], + ['Free period end date', 'The end date of the customer\'s free trial period, formatted as YYYY-MM-DD. This date must be later than the start date.'], + ['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): @@ -918,7 +1011,7 @@ class CustomerTransferView(LoginRequiredMixin, generic.View): # Send the email try: - temp_password="goodtimes#2024" + temp_password = "goodtimes#2024" principal_obj.password = make_password(temp_password) principal_obj.save() email_service.load_template( @@ -965,12 +1058,19 @@ class CustomerImportView(LoginRequiredMixin, generic.View): excel_file = request.FILES['file'] wb = load_workbook(filename=excel_file) - ws = wb.active + + # 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_l = [] + 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() @@ -980,7 +1080,7 @@ class CustomerImportView(LoginRequiredMixin, generic.View): 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, email, preferences, start_date, end_date = row + 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 @@ -1014,16 +1114,26 @@ class CustomerImportView(LoginRequiredMixin, generic.View): username=email.strip(), email_verified=True, register_complete=True, - principal_type=principal_type + principal_type=principal_type, + business_name=business_name, + phone_no=str(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_l.append((principal, event_categories, start_date, end_date)) + 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 recore is created check error log and fix the error in the file ") + 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 @@ -1036,7 +1146,7 @@ class CustomerImportView(LoginRequiredMixin, generic.View): 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_l: + for principal, event_categories, start_date, end_date in preferences_list: principal = principals.get(email=principal.email) # Generate referral code for the manager diff --git a/manage_events/admin.py b/manage_events/admin.py index 39aae97..fab8c59 100644 --- a/manage_events/admin.py +++ b/manage_events/admin.py @@ -4,6 +4,7 @@ from .models import ( EventShare, EventView, Favorites, + FreeUsageFeatureLimit, Venue, EventMaster, Event, @@ -131,3 +132,4 @@ admin.site.register(EventCategory, EventCategoryAdmin) admin.site.register(Venue, VenueAdmin) admin.site.register(EventMaster, EventMasterAdmin) admin.site.register(AgeGroups) +admin.site.register(FreeUsageFeatureLimit) diff --git a/manage_events/api/serializers.py b/manage_events/api/serializers.py index 30982b0..042da84 100644 --- a/manage_events/api/serializers.py +++ b/manage_events/api/serializers.py @@ -14,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 diff --git a/manage_events/api/urls.py b/manage_events/api/urls.py index ad715ce..24e86b7 100644 --- a/manage_events/api/urls.py +++ b/manage_events/api/urls.py @@ -4,6 +4,7 @@ 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(), @@ -134,6 +135,7 @@ urlpatterns = [ name="age_group_list" ), + # event list with filter path( "events/", views.EventListView.as_view(), diff --git a/manage_events/api/views.py b/manage_events/api/views.py index 5bfd641..b6a6a9a 100644 --- a/manage_events/api/views.py +++ b/manage_events/api/views.py @@ -6,6 +6,7 @@ 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, Count @@ -27,6 +28,7 @@ from manage_events.api.serializers import ( EventCategorySerializer, EventDetailSerializer, EventReviewSerializer, + FreeUsageFeatureLimitSerializer, IAmPrincipalLocationSerializer, PrincipalPreferenceSerializer, VenueSerializer, @@ -43,14 +45,30 @@ from manage_events.models import ( EventShare, EventView, Favorites, + FreeUsageFeatureLimit, PrincipalPreference, Venue, ) import requests 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] @@ -539,6 +557,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} ) @@ -1009,7 +1048,12 @@ class EventListView(generics.ListAPIView): filterset_class = EventFilter def get_queryset(self): - # Base queryset filtering active, non-draft, non-deleted events with end date in the future + """ + 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, @@ -1017,11 +1061,14 @@ class EventListView(generics.ListAPIView): end_date__gte=timezone.now().date() ) - if not self.request.query_params: - preferences = PrincipalPreference.objects.get(principal=self.request.user) - preferred_categories_ids = preferences.preferred_categories.values_list( - "id", flat=True - ) + # 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 diff --git a/manage_events/migrations/0016_freeusagefeaturelimit.py b/manage_events/migrations/0016_freeusagefeaturelimit.py new file mode 100644 index 0000000..0d87275 --- /dev/null +++ b/manage_events/migrations/0016_freeusagefeaturelimit.py @@ -0,0 +1,25 @@ +# Generated by Django 5.0.2 on 2024-08-05 10:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('manage_events', '0015_agegroups'), + ] + + operations = [ + migrations.CreateModel( + name='FreeUsageFeatureLimit', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('category_limit', models.PositiveIntegerField(default=3, help_text='The maximum number of categories that free app users can select.')), + ], + options={ + 'verbose_name': 'Free Usage Feature Limit', + 'verbose_name_plural': 'Free Usage Feature Limits', + 'db_table': 'free_usage_feature_limit', + }, + ), + ] diff --git a/manage_events/models.py b/manage_events/models.py index 590f1cf..03638ab 100644 --- a/manage_events/models.py +++ b/manage_events/models.py @@ -1,12 +1,33 @@ from django.db import models +from django.core.exceptions import ValidationError from accounts.models import BaseModel, IAmPrincipal from django.db import transaction from taggit.managers import TaggableManager -# from django.contrib.gis.db import models as gis_models +class FreeUsageFeatureLimit(models.Model): + category_limit = models.PositiveIntegerField( + default=3, + help_text="The maximum number of categories that free app users can select." + ) + + class Meta: + db_table = "free_usage_feature_limit" + verbose_name = "Free Usage Feature Limit" + verbose_name_plural = "Free Usage Feature Limits" + + def __str__(self): + return f"Free usage limit: {self.category_limit} categories" + + def save(self, *args, **kwargs): + if not self.pk and FreeUsageFeatureLimit.objects.exists(): + raise ValidationError("There can only be one FreeUsageFeatureLimit instance.") + return super().save(*args, **kwargs) + + @classmethod + def get_category_limit(cls): + return cls.objects.values_list('category_limit', flat=True).first() -# Create your models here. class EventCategory(BaseModel): title = models.CharField(max_length=255) image = models.ImageField(upload_to="event_category", null=True, blank=True) diff --git a/manage_events/views.py b/manage_events/views.py index 939e708..be68863 100644 --- a/manage_events/views.py +++ b/manage_events/views.py @@ -17,6 +17,7 @@ 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. @@ -362,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 diff --git a/manage_subscriptions/models.py b/manage_subscriptions/models.py index 21f3adf..5e3686c 100644 --- a/manage_subscriptions/models.py +++ b/manage_subscriptions/models.py @@ -1,4 +1,5 @@ -from datetime import timedelta, timezone +from datetime import timedelta +from django.utils import timezone from django.db import models from django.core.exceptions import ValidationError from accounts.models import BaseModel, IAmPrincipal, IAmPrincipalType @@ -136,6 +137,34 @@ class PrincipalSubscription(BaseModel): def generate_grace_period_end_date(date): return date + timedelta(days=15) + @classmethod + def has_principal_subscription(cls, principal): + return cls.get_active_princial_subscription(principal).exists() + + @classmethod + def get_active_princial_subscription(cls, principal): + return cls.objects.filter( + principal=principal, + is_paid=True, + cancelled=False, + deleted=False, + active=True, + status=SubscriptionStatus.ACTIVE, + grace_period_end_date__gt=timezone.now().date(), + ) + + # need to improve this + @classmethod + def get_principal_subscription(cls, principal): + return cls.objects.filter( + principal=principal, + is_paid=True, + cancelled=False, + deleted=False, + active=True, + status=SubscriptionStatus.ACTIVE, + ).order_by("-grace_period_end_date").first() + class WebhookEvent(BaseModel): event_id = models.CharField(max_length=255, unique=True, db_index=True) diff --git a/templates/accounts/customer/customer_detail.html b/templates/accounts/customer/customer_detail.html index 1cec652..3fc96ca 100644 --- a/templates/accounts/customer/customer_detail.html +++ b/templates/accounts/customer/customer_detail.html @@ -35,10 +35,18 @@
Last Name
{{principal_obj.last_name}}
+
+
Business Name
+
{{principal_obj.business_name}}
+
Email Address
{{principal_obj.email}}
+
+
Phone No
+
{{principal_obj.phone_no}}
+
Preferences
@@ -61,6 +69,38 @@
End Date
{% if principal_subscription %}{{ principal_subscription.end_date }}{% else %}No subscription found{% endif %}
+
+
Address
+
{{principal_obj.address_line1}}
+
+
+
Region
+
{{principal_obj.city}}
+
+
+
Country
+
{{principal_obj.country}}
+
+
+
Website
+
{{principal_obj.website}}
+
+
+
Facebook
+
{{principal_obj.facebook_profile}}
+
+
+
LinkedIn
+
{{principal_obj.linkedin_profile}}
+
+
+
Instagram
+
{{principal_obj.instagram_profile}}
+
+
+
Twitter
+
{{principal_obj.twitter_profile}}
+
{% if principal_obj.extended_data and not principal_obj.extended_data.is_transferred and principal_obj.extended_data.is_onboarded and principal_obj.principal_type.name == 'event_manager' %}
diff --git a/templates/accounts/customer/customer_list.html b/templates/accounts/customer/customer_list.html index caf4463..0f04305 100644 --- a/templates/accounts/customer/customer_list.html +++ b/templates/accounts/customer/customer_list.html @@ -129,6 +129,7 @@ -->
    + {% if data_obj.extended_data.is_onboarded%}
  • + {%endif%}
  • diff --git a/templates/manage_events/event_details.html b/templates/manage_events/event_details.html index 7d5ed7a..80f1be8 100644 --- a/templates/manage_events/event_details.html +++ b/templates/manage_events/event_details.html @@ -62,7 +62,7 @@
-
+

{{ event.brand.title }}

@@ -72,6 +72,7 @@
+ {% if publish %}
@@ -92,6 +93,7 @@
+ {% endif %}