Merge branch 'development' of https://github.com/WDI-Ideas/goodtimes into feature/module_9_coupons
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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)
|
||||
@@ -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):
|
||||
|
||||
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'),
|
||||
),
|
||||
]
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
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',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -35,10 +35,18 @@
|
||||
<div class="col-md-3">Last Name</div>
|
||||
<div class="col-md-9">{{principal_obj.last_name}}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">Business Name</div>
|
||||
<div class="col-md-9">{{principal_obj.business_name}}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">Email Address</div>
|
||||
<div class="col-md-9">{{principal_obj.email}}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">Phone No</div>
|
||||
<div class="col-md-9">{{principal_obj.phone_no}}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">Preferences</div>
|
||||
<div class="col-md-9">
|
||||
@@ -61,6 +69,38 @@
|
||||
<div class="col-md-3">End Date</div>
|
||||
<div class="col-md-9">{% if principal_subscription %}{{ principal_subscription.end_date }}{% else %}No subscription found{% endif %}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">Address</div>
|
||||
<div class="col-md-9">{{principal_obj.address_line1}}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">Region</div>
|
||||
<div class="col-md-9">{{principal_obj.city}}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">Country</div>
|
||||
<div class="col-md-9">{{principal_obj.country}}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">Website</div>
|
||||
<div class="col-md-9">{{principal_obj.website}}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">Facebook</div>
|
||||
<div class="col-md-9">{{principal_obj.facebook_profile}}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">LinkedIn</div>
|
||||
<div class="col-md-9">{{principal_obj.linkedin_profile}}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">Instagram</div>
|
||||
<div class="col-md-9">{{principal_obj.instagram_profile}}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">Twitter</div>
|
||||
<div class="col-md-9">{{principal_obj.twitter_profile}}</div>
|
||||
</div>
|
||||
{% 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' %}
|
||||
<div class="col text-end">
|
||||
<a class="btn btn-dark mb-2 me-4" href="{% url 'accounts:customer_transfer' principal_obj.id %}">
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
</td> -->
|
||||
<td class="text-center">
|
||||
<ul class="table-controls">
|
||||
{% if data_obj.extended_data.is_onboarded%}
|
||||
<li><a href="{% url 'accounts:customer_edit' data_obj.id%}" class="bs-tooltip"
|
||||
data-bs-toggle="tooltip" data-bs-placement="top" title=""
|
||||
data-original-title="Edit" data-bs-original-title="Edit"
|
||||
@@ -141,6 +142,7 @@
|
||||
d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z">
|
||||
</path>
|
||||
</svg></a></li>
|
||||
{%endif%}
|
||||
<li><a href="{% url 'accounts:customer_detail' data_obj.id%}" class="bs-tooltip" data-bs-toggle="tooltip" data-bs-placement="top" title="" data-original-title="View" data-bs-original-title="View" aria-label="View">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-eye"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
|
||||
</a></li>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="col-md-{{ publish|yesno:'8,12' }}">
|
||||
<div class="card mb-3" style="border-radius: 20px; box-shadow: 0 4px 8px rgba(0,0,0,.1);">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">{{ event.brand.title }}</h2>
|
||||
@@ -72,6 +72,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if publish %}
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-3" style="border-radius: 20px; box-shadow: 0 4px 8px rgba(0,0,0,.1);">
|
||||
<div class="card-body">
|
||||
@@ -92,6 +93,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user