refactor(subscription): removed unnecessary code
This commit is contained in:
@@ -51,7 +51,7 @@ STATIC_URL = "/static/"
|
||||
|
||||
STATICFILES_DIRS = [BASE_DIR.joinpath("static")]
|
||||
|
||||
STRIPE_CHECKOUT_URL = "https://c2f5-122-179-140-110.ngrok-free.app/subscriptions/create-checkout-session/"
|
||||
COUPON_VALIDITY_CHECK_URL = "https://c2f5-122-179-140-110.ngrok-free.app/subscriptions/coupon-validity-check/"
|
||||
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"
|
||||
|
||||
@@ -30,10 +30,10 @@ class SubscriptionService:
|
||||
):
|
||||
"""Create a principal subscription and return it."""
|
||||
start_date, end_date = self._calculate_dates(
|
||||
current_period_start, current_period_end, subscription.calulate_days()
|
||||
current_period_start, current_period_end, subscription.calculate_days()
|
||||
)
|
||||
|
||||
PrincipalSubscription.objects.filter(principal=principal, status=SubscriptionStatus.ACTIVE).update(status=SubscriptionStatus.EXPIRED)
|
||||
PrincipalSubscription.objects.filter(principal=principal, status=SubscriptionStatus.ACTIVE).update(status=SubscriptionStatus.EXPIRED, active=False)
|
||||
|
||||
principal_subscription = PrincipalSubscription.objects.create(
|
||||
principal=principal,
|
||||
|
||||
@@ -110,7 +110,6 @@ 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,
|
||||
|
||||
@@ -1,32 +1,19 @@
|
||||
from django.contrib import admin
|
||||
from .models import (
|
||||
Plan,
|
||||
PrincipalSubscription,
|
||||
StripeProduct,
|
||||
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
|
||||
@@ -48,7 +35,7 @@ class PrincipalSubscriptionAdmin(admin.ModelAdmin):
|
||||
"is_paid",
|
||||
"auto_renew",
|
||||
"status",
|
||||
"cancelled",
|
||||
# "cancelled",
|
||||
) # Enable filtering by these fields
|
||||
search_fields = (
|
||||
"subscription__title",
|
||||
@@ -64,27 +51,6 @@ class PrincipalSubscriptionAdmin(admin.ModelAdmin):
|
||||
|
||||
admin.site.register(PrincipalSubscription, PrincipalSubscriptionAdmin)
|
||||
|
||||
|
||||
class StripeProductAdmin(admin.ModelAdmin):
|
||||
list_display = ("id", "title", "product_id", "default_price_id")
|
||||
search_fields = ("title", "product_id", "description")
|
||||
list_filter = ("default_price_id",)
|
||||
readonly_fields = ("product_id", "default_price_id")
|
||||
fields = (
|
||||
"title",
|
||||
"description",
|
||||
"metadata",
|
||||
"image_url",
|
||||
"product_id",
|
||||
"default_price_id",
|
||||
"active",
|
||||
"deleted",
|
||||
)
|
||||
|
||||
|
||||
admin.site.register(StripeProduct, StripeProductAdmin)
|
||||
|
||||
|
||||
@admin.register(WebhookEvent)
|
||||
class WebhookEventAdmin(admin.ModelAdmin):
|
||||
list_display = ("event_id", "received_at", "event_type", "processed_at", "status")
|
||||
|
||||
@@ -315,7 +315,7 @@ class CancelSubscription(APIView):
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
if subscription.is_stripe_subscription:
|
||||
if subscription.stripe_subscription_id:
|
||||
# Cancel Stripe subscription
|
||||
try:
|
||||
stripe.Subscription.modify(subscription.stripe_subscription_id, cancel_at_period_end=True)
|
||||
@@ -328,7 +328,7 @@ class CancelSubscription(APIView):
|
||||
|
||||
# Updating subscription status in the local database
|
||||
subscription.status = SubscriptionStatus.INACTIVE
|
||||
subscription.cancelled = True
|
||||
# subscription.cancelled = True
|
||||
subscription.cancelled_date_time = timezone.now()
|
||||
subscription.save()
|
||||
|
||||
|
||||
@@ -2,25 +2,9 @@ from django import forms
|
||||
from accounts.models import IAmPrincipalType
|
||||
from manage_subscriptions.models import (
|
||||
PrincipalSubscription,
|
||||
StripeProduct,
|
||||
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
|
||||
|
||||
|
||||
class SubscriptionForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Subscription
|
||||
@@ -66,13 +50,3 @@ class PrincipalSubscriptionForm(forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
class StripeProductForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = StripeProduct
|
||||
fields = [
|
||||
"title",
|
||||
"description",
|
||||
]
|
||||
widgets = {
|
||||
"description": forms.Textarea(attrs={"rows": 3}),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Generated by Django 5.0.2 on 2024-08-21 18:10
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('manage_subscriptions', '0012_subscription_interval_subscription_interval_count'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='subscription',
|
||||
name='plan',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='stripeproduct',
|
||||
name='created_by',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='stripeproduct',
|
||||
name='modified_by',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='subscription',
|
||||
name='stripe_product',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='principalsubscription',
|
||||
name='cancelled',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='principalsubscription',
|
||||
name='is_stripe_subscription',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Plan',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='StripeProduct',
|
||||
),
|
||||
]
|
||||
@@ -5,34 +5,6 @@ from django.core.exceptions import ValidationError
|
||||
from accounts.models import BaseModel, IAmPrincipal, IAmPrincipalType
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# Create your models here.
|
||||
|
||||
|
||||
class Plan(BaseModel):
|
||||
title = models.CharField(max_length=255)
|
||||
days = models.PositiveIntegerField()
|
||||
|
||||
class Meta:
|
||||
db_table = "plan"
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class StripeProduct(BaseModel):
|
||||
title = models.CharField(max_length=255)
|
||||
product_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
metadata = models.JSONField(blank=True, null=True)
|
||||
image_url = models.URLField(blank=True, null=True)
|
||||
default_price_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "stripe_product"
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class Subscription(BaseModel):
|
||||
MONTH = "month"
|
||||
@@ -49,19 +21,9 @@ class Subscription(BaseModel):
|
||||
title = models.CharField(max_length=255)
|
||||
price_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
product_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
stripe_product = models.ForeignKey(
|
||||
StripeProduct,
|
||||
related_name="subscription_product",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
short_description = models.CharField(max_length=255, null=True, blank=True)
|
||||
long_description = models.TextField(null=True, blank=True)
|
||||
image = models.ImageField(upload_to="subscription_img", null=True, blank=True)
|
||||
plan = models.ForeignKey(
|
||||
Plan, related_name="subscription_plan", on_delete=models.CASCADE
|
||||
)
|
||||
interval = models.CharField(max_length=10, choices=INTERVAL_TYPES)
|
||||
interval_count = models.IntegerField(default=1)
|
||||
high_amount = models.DecimalField(max_digits=14, decimal_places=2, default=0.00)
|
||||
@@ -110,13 +72,13 @@ class Subscription(BaseModel):
|
||||
# Create new product and price
|
||||
price = StripeService.create_price(
|
||||
product_data={
|
||||
"name": self.title,
|
||||
"name": self.txitle,
|
||||
"description": self.short_description,
|
||||
},
|
||||
unit_amount=int(self.amount * 100),
|
||||
currency="gbp",
|
||||
recurring={
|
||||
"interval": self.plan.title,
|
||||
"interval": self.interval,
|
||||
"interval_count": self.interval_count,
|
||||
},
|
||||
metadata={
|
||||
@@ -133,15 +95,14 @@ class Subscription(BaseModel):
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def calculate_date(self):
|
||||
count = {
|
||||
self.DAY: 1,
|
||||
self.MONTH: 30, # assuming a month is 30 days
|
||||
self.YEAR: 365,
|
||||
self.WEEK: 7
|
||||
}
|
||||
|
||||
return count[self.interval] * self.interval_count
|
||||
def calculate_days(self):
|
||||
count = {
|
||||
self.DAY: 1,
|
||||
self.MONTH: 30, # assuming a month is 30 days
|
||||
self.YEAR: 365,
|
||||
self.WEEK: 7
|
||||
}
|
||||
return count[self.interval] * self.interval_count
|
||||
|
||||
|
||||
|
||||
@@ -169,13 +130,11 @@ class PrincipalSubscription(BaseModel):
|
||||
start_date = models.DateField()
|
||||
end_date = models.DateField()
|
||||
order_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
cancelled = models.BooleanField(default=False)
|
||||
cancelled_date_time = models.DateTimeField(null=True, blank=True)
|
||||
grace_period_end_date = models.DateField(null=True, blank=True)
|
||||
stripe_customer_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
stripe_subscription_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
comments = models.CharField(max_length=255, null=True, blank=True)
|
||||
is_stripe_subscription = models.BooleanField(default=False)
|
||||
payment_intent_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
payment_intent_client_secret = models.CharField(
|
||||
max_length=255, null=True, blank=True
|
||||
@@ -203,7 +162,6 @@ class PrincipalSubscription(BaseModel):
|
||||
return cls.objects.filter(
|
||||
principal=principal,
|
||||
is_paid=True,
|
||||
# cancelled=False,
|
||||
active=True,
|
||||
status=SubscriptionStatus.ACTIVE,
|
||||
grace_period_end_date__gt=timezone.now().date(),
|
||||
@@ -214,18 +172,16 @@ class PrincipalSubscription(BaseModel):
|
||||
return cls.objects.filter(
|
||||
principal=principal,
|
||||
is_paid=True,
|
||||
# cancelled=False,
|
||||
active=True,
|
||||
status=SubscriptionStatus.ACTIVE,
|
||||
end_date__gt=timezone.now().date(),
|
||||
)
|
||||
).order_by('-end_date').last()
|
||||
|
||||
@classmethod
|
||||
def get_principal_subscription(cls, principal):
|
||||
return cls.objects.filter(
|
||||
principal=principal,
|
||||
is_paid=True,
|
||||
# cancelled=False,
|
||||
active=True,
|
||||
status=SubscriptionStatus.ACTIVE,
|
||||
).order_by("-grace_period_end_date").first()
|
||||
|
||||
@@ -18,17 +18,6 @@ urlpatterns = [
|
||||
views.SubscriptionDeleteView.as_view(),
|
||||
name="subscription_delete",
|
||||
),
|
||||
# Stripe Products
|
||||
path(
|
||||
"product/list/", views.StripeProductView.as_view(), name="stripe_product_list"
|
||||
),
|
||||
path(
|
||||
"product/add/",
|
||||
views.StripeProductCreateOrUpdateView.as_view(),
|
||||
name="stripe_product_add",
|
||||
),
|
||||
# PLANS
|
||||
path("plan/list/", views.PlanView.as_view(), name="plan_list"),
|
||||
|
||||
# Principal Subscription
|
||||
path(
|
||||
@@ -36,11 +25,6 @@ urlpatterns = [
|
||||
views.PrincipalSubscriptionView.as_view(),
|
||||
name="principal_subscriptions_list",
|
||||
),
|
||||
# path(
|
||||
# "principal_subscription/add/",
|
||||
# views.PrincipalSubscriptionCreateOrUpdateView.as_view(),
|
||||
# name="principal_subscription_add",
|
||||
# ),
|
||||
path(
|
||||
"principal_subscription/edit/<int:pk>/",
|
||||
views.PrincipalSubscriptionCreateOrUpdateView.as_view(),
|
||||
@@ -56,11 +40,6 @@ urlpatterns = [
|
||||
views.PrincipalSubscriptionDeleteView.as_view(),
|
||||
name="principal_subscription_delete",
|
||||
),
|
||||
path(
|
||||
"stripe-subscription/",
|
||||
views.stripe_config,
|
||||
name="stripe_subscription",
|
||||
),
|
||||
path(
|
||||
"create-checkout-session/",
|
||||
views.create_checkout_session,
|
||||
|
||||
@@ -8,26 +8,6 @@ API_KEY = settings.GOOGLE_MAPS_API_KEY
|
||||
gmaps = googlemaps.Client(key=API_KEY)
|
||||
|
||||
|
||||
def get_active_subscription_id_for_principal(principal):
|
||||
# Filter subscriptions for the principal that are active and not cancelled
|
||||
active_subscriptions = PrincipalSubscription.objects.filter(
|
||||
principal=principal,
|
||||
status=SubscriptionStatus.ACTIVE,
|
||||
is_paid=True,
|
||||
cancelled=False,
|
||||
deleted=False,
|
||||
active=True,
|
||||
end_date__gte=now().date(), # Ensure the subscription hasn't expired
|
||||
).order_by(
|
||||
"-end_date"
|
||||
) # Order by end_date to get the most recent active subscription
|
||||
|
||||
if active_subscriptions.exists():
|
||||
# Return the ID of the most recent active subscription
|
||||
return active_subscriptions.first().id
|
||||
return None
|
||||
|
||||
|
||||
def get_location_info(latitude, longitude):
|
||||
reverse_geocode_result = gmaps.reverse_geocode((latitude, longitude))
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ from django.contrib.auth import get_user_model
|
||||
from goodtimes.services import StripeService
|
||||
from manage_coupons.models import Coupon
|
||||
from manage_subscriptions.forms import (
|
||||
StripeProductForm,
|
||||
SubscriptionForm,
|
||||
PrincipalSubscriptionForm,
|
||||
)
|
||||
@@ -23,8 +22,6 @@ from manage_wallets.models import (
|
||||
TransactionType,
|
||||
)
|
||||
from .models import (
|
||||
Plan,
|
||||
StripeProduct,
|
||||
Subscription,
|
||||
PrincipalSubscription,
|
||||
SubscriptionStatus,
|
||||
@@ -179,129 +176,6 @@ class SubscriptionDeleteView(LoginRequiredMixin, generic.View):
|
||||
return redirect(self.success_url)
|
||||
|
||||
|
||||
class StripeProductCreateOrUpdateView(LoginRequiredMixin, generic.View):
|
||||
# Set the page_name and resource
|
||||
page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
|
||||
resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
|
||||
|
||||
# Initialize the action as ACTION_CREATE (can change based on logic)
|
||||
action = resource_action.ACTION_CREATE # Default action
|
||||
|
||||
template_name = "manage_subscriptions/product_add.html"
|
||||
model = StripeProduct
|
||||
form_class = StripeProductForm
|
||||
success_url = reverse_lazy("manage_subscriptions:stripe_product_list")
|
||||
error_message = "An error occurred while saving the data."
|
||||
|
||||
# Determine the success message dynamically based on whether it's an update or create
|
||||
def get_success_message(self):
|
||||
self.success_message = (
|
||||
constants.RECORD_CREATED if not self.object else constants.RECORD_UPDATED
|
||||
)
|
||||
return self.success_message
|
||||
|
||||
# Get the object (if exists) based on URL parameter 'pk'
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get("pk")
|
||||
return get_object_or_404(self.model, pk=pk) if pk else None
|
||||
|
||||
# Add page_name and operation to the context
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
"page_name": self.page_name,
|
||||
"operation": "Add" if not self.object else "Edit",
|
||||
}
|
||||
context.update(kwargs) # Include any additional context data passed to the view
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
|
||||
# If an object is found, change action to ACTION_UPDATE
|
||||
if self.object is not None:
|
||||
self.action = resource_action.ACTION_UPDATE
|
||||
|
||||
form = self.form_class(instance=self.object)
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
|
||||
# If an object is found, change action to ACTION_UPDATE
|
||||
if self.object is not None:
|
||||
self.action = resource_action.ACTION_UPDATE
|
||||
|
||||
form = self.form_class(request.POST, instance=self.object)
|
||||
if not form.is_valid():
|
||||
print(form.errors)
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
success, message = self.handle_stripe_product(form)
|
||||
if not success:
|
||||
messages.error(self.request, message)
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
form.save()
|
||||
messages.success(self.request, self.get_success_message())
|
||||
return redirect(self.success_url)
|
||||
|
||||
def handle_stripe_product(self, form):
|
||||
try:
|
||||
stripe.api_key = settings.STRIPE_SECRET_KEY
|
||||
|
||||
stripe_product = stripe.Product.create(
|
||||
name=form.cleaned_data.get("title"),
|
||||
description=form.cleaned_data.get("description"),
|
||||
)
|
||||
|
||||
# Save Stripe Product ID to the form instance
|
||||
form.instance.product_id = stripe_product.id
|
||||
|
||||
return True, "" # Success
|
||||
except stripe.error.StripeError as e:
|
||||
return False, f"Stripe error: {str(e)}"
|
||||
except Exception as e:
|
||||
return False, f"An error occurred: {str(e)}"
|
||||
|
||||
|
||||
class StripeProductView(LoginRequiredMixin, generic.ListView):
|
||||
page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
|
||||
resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
|
||||
action = resource_action.ACTION_READ
|
||||
model = StripeProduct
|
||||
template_name = "manage_subscriptions/product_list.html"
|
||||
context_object_name = "product_obj"
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset().filter(deleted=False, active=True)
|
||||
return queryset.order_by("-created_on")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_name"] = self.page_name
|
||||
return context
|
||||
|
||||
|
||||
class PlanView(LoginRequiredMixin, generic.ListView):
|
||||
page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
|
||||
resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
|
||||
action = resource_action.ACTION_READ
|
||||
model = Plan
|
||||
template_name = "manage_subscriptions/plan_list.html"
|
||||
context_object_name = "plan_obj"
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(deleted=False)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_name"] = self.page_name
|
||||
return context
|
||||
|
||||
|
||||
class PrincipalSubscriptionCreateOrUpdateView(LoginRequiredMixin, generic.View):
|
||||
# Set the page_name and resource
|
||||
page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
|
||||
@@ -474,11 +348,9 @@ class ActiveSubscriptionView(generic.View):
|
||||
|
||||
today = timezone.now().date()
|
||||
if request.user.is_authenticated:
|
||||
latest_subscription = PrincipalSubscription.objects.filter(
|
||||
principal=request.user,
|
||||
is_paid=True,
|
||||
end_date__gte=today,
|
||||
).order_by('-end_date').last()
|
||||
latest_subscription = PrincipalSubscription.get_active_princial_subscription(request.user)
|
||||
|
||||
print(f"latest subscription reodr is {latest_subscription}")
|
||||
|
||||
if not latest_subscription:
|
||||
return HttpResponseRedirect(reverse("manage_subscriptions:stripe"))
|
||||
@@ -549,13 +421,6 @@ class CancelAutoSubscriptionView(LoginRequiredMixin, generic.View):
|
||||
# return redirect("manage_subscriptions:subscription_cancel_fails")
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def stripe_config(request):
|
||||
if request.method == "GET":
|
||||
stripe_config = {"publicKey": settings.STRIPE_PUBLISH_KEY}
|
||||
return JsonResponse(stripe_config, safe=False)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_POST
|
||||
def validate_coupon(request):
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if user|has_resource_permission:resource_context.RESOURCE_MANAGE_COUPONS %}
|
||||
{% comment %} {% if user|has_resource_permission:resource_context.RESOURCE_MANAGE_COUPONS %}
|
||||
<li class="menu {% if page_name == resource_context.RESOURCE_MANAGE_COUPONS %}active{% endif %}">
|
||||
<a href="{% url 'manage_coupons:coupon_list'%}" aria-expanded="false"
|
||||
class="dropdown-toggle">
|
||||
@@ -164,7 +164,7 @@
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %} {% endcomment %}
|
||||
{% if user|has_resource_permission:resource_context.RESOURCE_PRINCIPAL_SUBSCRIPTIONS %}
|
||||
<li class="menu {% if page_name == resource_context.RESOURCE_PRINCIPAL_SUBSCRIPTIONS %}active{% endif %}">
|
||||
<a href="{% url 'manage_subscriptions:principal_subscriptions_list'%}" aria-expanded="false"
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
{% extends 'layout/base_template.html' %}
|
||||
{% load static %}
|
||||
{% block stylesheet %}
|
||||
<!-- include required css cdn link through html here -->
|
||||
|
||||
{% include "cdn_through_html/filepond_cdn_css.html" %}
|
||||
{% include "cdn_through_html/quill_cdn_css.html" %}
|
||||
{% include "cdn_through_html/tagify_cdn_css.html" %}
|
||||
{{form.media}}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row layout-top-spacing">
|
||||
<div class="col-lg-12">
|
||||
<div class="row mb-2">
|
||||
<div class="col">
|
||||
<h3>{{operation}} {{page_name}}</h3>
|
||||
</div>
|
||||
<div class="col text-end">
|
||||
<button class="btn btn-dark mb-2 me-4" onclick="history.back()">
|
||||
<i class="fa fa-arrow-left"></i>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row layout-spacing">
|
||||
<div class="col-lg-12">
|
||||
<div class="statbox widget box box-shadow">
|
||||
<div class="widget-content widget-content-area">
|
||||
|
||||
<form method="POST" novalidate>
|
||||
{% csrf_token %}
|
||||
{% include 'includes/dynamic_template_form.html' with form=form %}
|
||||
<div class="mt-4 mb-0">
|
||||
<div class="d-grid"><button class="btn btn-primary btn-block" type="submit">Submit</button></div>
|
||||
</div>
|
||||
{% comment %} <div class="mb-3">
|
||||
<label for="title" class="form-label">Title</label>
|
||||
<input type="text" class="form-control" id="title" aria-describedby="title">
|
||||
<div id="emailHelp" class="form-text" style="color: grey;">We'll never share your email with anyone else.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Description</label>
|
||||
<div id="description"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="product-images">Image</label>
|
||||
<div class="multiple-file-upload">
|
||||
<div class="filepond--root filepond file-upload-multiple filepond--hopper" id="images" data-style-button-remove-item-position="left" data-style-button-process-item-position="right" data-style-load-indicator-position="right" data-style-progress-indicator-position="right" data-style-button-remove-item-align="false" style="height: 57px;"><input class="filepond--browser" type="file" id="filepond--browser-feeq8o6dj" name="filepond" aria-controls="filepond--assistant-feeq8o6dj" aria-labelledby="filepond--drop-label-feeq8o6dj" multiple=""><a class="filepond--credits" aria-hidden="true" href="https://pqina.nl/" target="_blank" rel="noopener noreferrer" style="transform: translateY(49px);">Powered by PQINA</a><div class="filepond--drop-label" style="transform: translate3d(0px, 0px, 0px); opacity: 1;"><label for="filepond--browser-feeq8o6dj" id="filepond--drop-label-feeq8o6dj" aria-hidden="true">Drag & Drop your files or <span class="filepond--label-action" tabindex="0">Browse</span></label></div><div class="filepond--list-scroller" style="transform: translate3d(0px, 41px, 0px);"><ul class="filepond--list" role="list"></ul></div><div class="filepond--panel filepond--panel-root" data-scalable="true"><div class="filepond--panel-top filepond--panel-root"></div><div class="filepond--panel-center filepond--panel-root" style="transform: translate3d(0px, 8px, 0px) scale3d(1, 0.41, 1);"></div><div class="filepond--panel-bottom filepond--panel-root" style="transform: translate3d(0px, 49px, 0px);"></div></div><span class="filepond--assistant" id="filepond--assistant-feeq8o6dj" role="status" aria-live="polite" aria-relevant="additions"></span><div class="filepond--drip"></div><fieldset class="filepond--data"></fieldset></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="tags">Tags</label>
|
||||
<input id="tags" class="tags" value="">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button> {% endcomment %}
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block javascript %}
|
||||
<!-- include required css cdn link through html here -->
|
||||
|
||||
{% include "cdn_through_html/filepond_cdn_js.html" %}
|
||||
{% include "cdn_through_html/quill_cdn_js.html" %}
|
||||
{% include "cdn_through_html/tagify_cdn_js.html" %}
|
||||
|
||||
|
||||
<script>
|
||||
/**
|
||||
* ===================================
|
||||
* Blog Description Editor
|
||||
* ===================================
|
||||
*/
|
||||
var quill = new Quill('#description', {
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ header: [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
['image', 'code-block']
|
||||
]
|
||||
},
|
||||
placeholder: 'Write description...',
|
||||
theme: 'snow' // or 'bubble'
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* ====================
|
||||
* File Pond
|
||||
* ====================
|
||||
*/
|
||||
|
||||
// We want to preview images, so we register
|
||||
// the Image Preview plugin, We also register
|
||||
// exif orientation (to correct mobile image
|
||||
// orientation) and size validation, to prevent
|
||||
// large files from being added
|
||||
FilePond.registerPlugin(
|
||||
FilePondPluginImagePreview,
|
||||
FilePondPluginImageExifOrientation,
|
||||
FilePondPluginFileValidateSize,
|
||||
// FilePondPluginImageEdit
|
||||
);
|
||||
|
||||
// Select the file input and use
|
||||
// create() to turn it into a pond
|
||||
var ecommerce = FilePond.create(document.querySelector('.file-upload-multiple'));
|
||||
|
||||
|
||||
/**
|
||||
* =====================
|
||||
* Blog Tags
|
||||
* =====================
|
||||
*/
|
||||
// The DOM element you wish to replace with Tagify
|
||||
var input = document.querySelector('#id_tags');
|
||||
|
||||
// initialize Tagify on the above input node reference
|
||||
new Tagify(input,{
|
||||
originalInputValueFormat: valuesArr => valuesArr.map(item => item.value).join(', ')
|
||||
})
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,107 +0,0 @@
|
||||
{% extends 'layout/base_template.html' %}
|
||||
{% load static %}
|
||||
{% block stylesheet %}
|
||||
<!-- include required css cdn link through html here -->
|
||||
{% include "cdn_through_html/datatable_cdn_css.html" %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row layout-top-spacing">
|
||||
<div class="col-lg-12">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<h3>Manage Plans</h3>
|
||||
</div>
|
||||
<div class="col-sm-6 text-md-end">
|
||||
<!--
|
||||
<button class="btn btn-dark mb-2 me-md-4" onclick="history.back()">
|
||||
<i class="fa fa-arrow-left"></i>
|
||||
Back
|
||||
</button>
|
||||
-->
|
||||
|
||||
<a class="btn btn-primary mb-2" href="{% url 'manage_subscriptions:subscription_list' %}">Subscriptions</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row layout-spacing">
|
||||
<div class="col-lg-12">
|
||||
<div class="statbox widget box box-shadow">
|
||||
<div class="widget-content widget-content-area">
|
||||
<div id="style-3_wrapper" class="dataTables_wrapper container-fluid dt-bootstrap4 no-footer">
|
||||
<div class="table-responsive">
|
||||
<table id="style-3" class="table style-3 dt-table-hover dataTable no-footer" role="grid"
|
||||
aria-describedby="style-3_info">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th class="checkbox-column sorting_asc" tabindex="0"
|
||||
aria-controls="style-3" aria-sort="ascending"
|
||||
style="width: 69.2656px;"> Record Id </th>
|
||||
<th class="checkbox-column sorting_asc" tabindex="0"
|
||||
aria-controls="style-3" aria-sort="ascending"
|
||||
style="width: 69.2656px;"> Title </th>
|
||||
<th class="checkbox-column sorting_asc" tabindex="0"
|
||||
aria-controls="style-3" aria-sort="ascending"
|
||||
style="width: 69.2656px;"> Days </th>
|
||||
<th class="sorting" tabindex="7" aria-controls="style-3"
|
||||
style="width: 79.7969px;">Active</th>
|
||||
<!-- <th class="dt-no-sorting sorting" tabindex="8"
|
||||
aria-controls="style-3"
|
||||
style="width: 100.625px;">Action</th> -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for data_obj in plan_obj %}
|
||||
<tr role="row">
|
||||
<td class="checkbox-column text-center sorting_1"> {{data_obj.id}}</td>
|
||||
<td>{{data_obj.title}}</td>
|
||||
<td>{{data_obj.days}}</td>
|
||||
<td class="text-center">
|
||||
<span class="shadow-none badge {% if data_obj.active %}badge-primary{% else %}badge-danger{% endif %}">{{data_obj.active}}</span>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block javascript %}
|
||||
<!-- include required js cdn link through html here -->
|
||||
{% include "cdn_through_html/datatable_cdn_js.html" %}
|
||||
|
||||
<script>
|
||||
c3 = $('#style-3').DataTable({
|
||||
"dom": "<'dt--top-section'<'row'<'col-12 col-sm-6 d-flex justify-content-sm-start justify-content-center'l><'col-12 col-sm-6 d-flex justify-content-sm-end justify-content-center mt-sm-0 mt-3'f>>>" +
|
||||
"<'table-responsive'tr>" +
|
||||
"<'dt--bottom-section d-sm-flex justify-content-sm-between text-center'<'dt--pages-count mb-sm-0 mb-3'i><'dt--pagination'p>>",
|
||||
"oLanguage": {
|
||||
"oPaginate": { "sPrevious": '<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-arrow-left"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>', "sNext": '<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-arrow-right"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>' },
|
||||
"sInfo": "Showing page _PAGE_ of _PAGES_",
|
||||
"sSearch": '<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-search"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>',
|
||||
"sSearchPlaceholder": "Search...",
|
||||
"sLengthMenu": "Results : _MENU_",
|
||||
},
|
||||
"order": [[ 0, "desc" ]],
|
||||
"stripeClasses": [],
|
||||
"lengthMenu": [5, 10, 20, 50],
|
||||
"pageLength": 10
|
||||
});
|
||||
|
||||
multiCheck(c3);
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -53,16 +53,17 @@
|
||||
</div>
|
||||
|
||||
<!-- Cancellation Button -->
|
||||
{% if principal_subscription_obj.auto_renew and not principal_subscription_obj.cancelled %}
|
||||
{% if principal_subscription_obj.auto_renew and not principal_subscription_obj.cancelled_date_time %}
|
||||
<div class="col-md-12 mb-4">
|
||||
<div class="card shadow-sm bg-dark text-light border-gold">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="text-gold">Cancel Subscription</h5>
|
||||
<form method="POST" action="{% url 'manage_subscriptions:cancel_subscription' %}">
|
||||
<a class="btn btn-primary" href="{% url 'manage_subscriptions:cancel_subscription' subscription_id=principal_subscription_obj.id %}">Cancel Subscription</a>
|
||||
{% comment %} <form method="POST" action="{% url 'manage_subscriptions:cancel_subscription' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="subscription_id" value="{{ principal_subscription_obj.id }}">
|
||||
<button type="submit" class="btn btn-outline-gold">Cancel Subscription</button>
|
||||
</form>
|
||||
</form> {% endcomment %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
{% extends 'layout/base_template.html' %}
|
||||
{% load static %}
|
||||
{% block stylesheet %}
|
||||
<!-- include required css cdn link through html here -->
|
||||
|
||||
{% include "cdn_through_html/filepond_cdn_css.html" %}
|
||||
{% include "cdn_through_html/quill_cdn_css.html" %}
|
||||
{% include "cdn_through_html/tagify_cdn_css.html" %}
|
||||
{{form.media}}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row layout-top-spacing">
|
||||
<div class="col-lg-12">
|
||||
<div class="row mb-2">
|
||||
<div class="col">
|
||||
<h3>Add Product</h3>
|
||||
</div>
|
||||
<div class="col text-end">
|
||||
<button class="btn btn-dark mb-2 me-4" onclick="history.back()">
|
||||
<i class="fa fa-arrow-left"></i>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row layout-spacing">
|
||||
<div class="col-lg-12">
|
||||
<div class="statbox widget box box-shadow">
|
||||
<div class="widget-content widget-content-area">
|
||||
|
||||
<form method="POST" novalidate>
|
||||
{% csrf_token %}
|
||||
{% include 'includes/dynamic_template_form.html' with form=form %}
|
||||
<div class="mt-4 mb-0">
|
||||
<div class="d-grid"><button class="btn btn-primary btn-block" type="submit">Submit</button></div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock content %}
|
||||
@@ -1,104 +0,0 @@
|
||||
{% extends 'layout/base_template.html' %}
|
||||
{% load static %}
|
||||
{% block stylesheet %}
|
||||
<!-- include required css cdn link through html here -->
|
||||
{% include "cdn_through_html/datatable_cdn_css.html" %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row layout-top-spacing">
|
||||
<div class="col-lg-12">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<h3>Manage Products</h3>
|
||||
</div>
|
||||
<div class="col-sm-6 text-md-end">
|
||||
|
||||
<button class="btn btn-dark mb-2 me-md-4" onclick="history.back()">
|
||||
<i class="fa fa-arrow-left"></i>
|
||||
Back
|
||||
</button>
|
||||
|
||||
<!-- <a class="btn btn-primary mb-2" href="{% url 'manage_subscriptions:stripe_product_add' %}">Add Products</a> -->
|
||||
<!-- <a class="btn btn-primary mb-2" href="{% url 'manage_subscriptions:plan_list' %}">Plans</a>
|
||||
<a class="btn btn-primary mb-2" href="{% url 'manage_subscriptions:principal_subscriptions_list' %}">Principal Subscription</a> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row layout-spacing">
|
||||
<div class="col-lg-12">
|
||||
<div class="statbox widget box box-shadow">
|
||||
<div class="widget-content widget-content-area">
|
||||
<div id="style-3_wrapper" class="dataTables_wrapper container-fluid dt-bootstrap4 no-footer">
|
||||
<div class="table-responsive">
|
||||
<table id="style-3" class="table style-3 dt-table-hover dataTable no-footer" role="grid"
|
||||
aria-describedby="style-3_info">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th class="checkbox-column sorting_asc" tabindex="0"
|
||||
aria-controls="style-3" aria-sort="ascending"
|
||||
style="width: 69.2656px;"> Record Id </th>
|
||||
<th class="checkbox-column sorting_asc" tabindex="0"
|
||||
aria-controls="style-3" aria-sort="ascending"
|
||||
style="width: 69.2656px;"> Title </th>
|
||||
<th class="checkbox-column sorting_asc" tabindex="0"
|
||||
aria-controls="style-3" aria-sort="ascending"
|
||||
style="width: 69.2656px;"> Stripe Product ID </th>
|
||||
<th class="sorting" tabindex="7" aria-controls="style-3"
|
||||
style="width: 79.7969px;">Active</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for data_obj in product_obj %}
|
||||
<tr role="row">
|
||||
<td class="checkbox-column text-center sorting_1"> {{data_obj.id}}</td>
|
||||
<td>{{data_obj.title}}</td>
|
||||
<td>{{data_obj.product_id}}</td>
|
||||
<td class="text-center">
|
||||
<span class="shadow-none badge {% if data_obj.active %}badge-primary{% else %}badge-danger{% endif %}">{{data_obj.active}}</span>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block javascript %}
|
||||
<!-- include required js cdn link through html here -->
|
||||
{% include "cdn_through_html/datatable_cdn_js.html" %}
|
||||
|
||||
<script>
|
||||
c3 = $('#style-3').DataTable({
|
||||
"dom": "<'dt--top-section'<'row'<'col-12 col-sm-6 d-flex justify-content-sm-start justify-content-center'l><'col-12 col-sm-6 d-flex justify-content-sm-end justify-content-center mt-sm-0 mt-3'f>>>" +
|
||||
"<'table-responsive'tr>" +
|
||||
"<'dt--bottom-section d-sm-flex justify-content-sm-between text-center'<'dt--pages-count mb-sm-0 mb-3'i><'dt--pagination'p>>",
|
||||
"oLanguage": {
|
||||
"oPaginate": { "sPrevious": '<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-arrow-left"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>', "sNext": '<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-arrow-right"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>' },
|
||||
"sInfo": "Showing page _PAGE_ of _PAGES_",
|
||||
"sSearch": '<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-search"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>',
|
||||
"sSearchPlaceholder": "Search...",
|
||||
"sLengthMenu": "Results : _MENU_",
|
||||
},
|
||||
"order": [[ 0, "desc" ]],
|
||||
"stripeClasses": [],
|
||||
"lengthMenu": [5, 10, 20, 50],
|
||||
"pageLength": 10
|
||||
});
|
||||
|
||||
multiCheck(c3);
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -15,17 +15,7 @@
|
||||
<h3>Manage Subscriptions</h3>
|
||||
</div>
|
||||
<div class="col-sm-6 text-md-end">
|
||||
<!--
|
||||
<button class="btn btn-dark mb-2 me-md-4" onclick="history.back()">
|
||||
<i class="fa fa-arrow-left"></i>
|
||||
Back
|
||||
</button>
|
||||
-->
|
||||
<a class="btn btn-primary mb-2" href="{% url 'manage_subscriptions:stripe_product_list' %}">Products</a>
|
||||
<a class="btn btn-primary mb-2" href="{% url 'manage_subscriptions:subscription_add' %}">Add Subscriptions</a>
|
||||
<a class="btn btn-primary mb-2" href="{% url 'manage_subscriptions:stripe_product_add' %}">Add Stripe Product</a>
|
||||
<!-- <a class="btn btn-primary mb-2" href="{% url 'manage_subscriptions:plan_list' %}">Plans</a>
|
||||
<a class="btn btn-primary mb-2" href="{% url 'manage_subscriptions:principal_subscriptions_list' %}">Principal Subscription</a> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
<h2 class="card-title text-gold">404</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="text-gold">An error occurred. Please try again later.</h5>
|
||||
<h5 class="text-gold">Something went wrong. Please try again later.</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user