aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--config/settings/base.py5
-rw-r--r--config/urls.py4
-rw-r--r--pyblackbird_cc/payments/__init__.py0
-rw-r--r--pyblackbird_cc/payments/admin.py33
-rw-r--r--pyblackbird_cc/payments/apps.py6
-rw-r--r--pyblackbird_cc/payments/migrations/0001_initial.py103
-rw-r--r--pyblackbird_cc/payments/migrations/0002_subscriptionplan_and_more.py36
-rw-r--r--pyblackbird_cc/payments/migrations/0003_product_price.py53
-rw-r--r--pyblackbird_cc/payments/migrations/0004_rename_stripe_product_id_price_stripe_price_id.py18
-rw-r--r--pyblackbird_cc/payments/migrations/__init__.py0
-rw-r--r--pyblackbird_cc/payments/models.py61
-rw-r--r--pyblackbird_cc/payments/tests.py3
-rw-r--r--pyblackbird_cc/payments/urls.py17
-rw-r--r--pyblackbird_cc/payments/views.py85
-rw-r--r--pyblackbird_cc/resources/migrations/0019_alter_pdfpagesnapshot_options_and_more.py26
-rw-r--r--pyblackbird_cc/resources/models.py7
-rw-r--r--pyblackbird_cc/resources/views.py46
-rw-r--r--pyblackbird_cc/templates/base.html1
-rw-r--r--pyblackbird_cc/templates/payments/cancel.html10
-rw-r--r--pyblackbird_cc/templates/payments/cart_detail.html41
-rw-r--r--pyblackbird_cc/templates/payments/landingpage.html29
-rw-r--r--pyblackbird_cc/templates/payments/success.html11
-rw-r--r--pyproject.toml4
-rw-r--r--requirements/local.txt2
24 files changed, 578 insertions, 23 deletions
diff --git a/config/settings/base.py b/config/settings/base.py
index 0d4ef5f..0324974 100644
--- a/config/settings/base.py
+++ b/config/settings/base.py
@@ -80,9 +80,14 @@ THIRD_PARTY_APPS = [
"django_celery_beat",
]
+STRIPE_SECRET_KEY = env("STRIPE_SECRET_KEY")
+STRIPE_PUBLIC_KEY = env("STRIPE_PUBLIC_KEY")
+STRIPE_LIVE_MODE = False
+
LOCAL_APPS = [
"pyblackbird_cc.users",
"pyblackbird_cc.resources",
+ "pyblackbird_cc.payments",
# Your stuff: custom apps go here
]
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
diff --git a/config/urls.py b/config/urls.py
index 1dc7fcb..5a6eeeb 100644
--- a/config/urls.py
+++ b/config/urls.py
@@ -8,7 +8,7 @@ from django.urls import path
from django.views import defaults as default_views
from django.views.generic import TemplateView
-#import debug_toolbar
+# import debug_toolbar
admin.site.site_header = "Blackbird Admin Panel"
@@ -27,11 +27,11 @@ urlpatterns = [
path("accounts/", include("allauth.urls")),
# Your stuff: custom urls includes go here
path("resources/", include("pyblackbird_cc.resources.urls", namespace="resources")),
+ path("payments/", include("pyblackbird_cc.payments.urls", namespace="payments")),
# Media files
*static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT),
]
-
if settings.DEBUG:
# This allows the error pages to be debugged during development, just visit
# these url in browser to see how these error pages look like.
diff --git a/pyblackbird_cc/payments/__init__.py b/pyblackbird_cc/payments/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pyblackbird_cc/payments/__init__.py
diff --git a/pyblackbird_cc/payments/admin.py b/pyblackbird_cc/payments/admin.py
new file mode 100644
index 0000000..526a1f0
--- /dev/null
+++ b/pyblackbird_cc/payments/admin.py
@@ -0,0 +1,33 @@
+from django.contrib import admin
+
+from .models import ShoppingCart, CartItem, Subscription, SubscriptionPlan, Product, Price
+
+
+class PriceInlineAdmin(admin.TabularInline):
+ model = Price
+ extra = 0
+
+class ProductAdmin(admin.ModelAdmin):
+ inlines = [PriceInlineAdmin]
+
+class SubscriptionPlanAdmin(admin.ModelAdmin):
+ list_display = ('name', 'price', 'description', 'allowed_downloads')
+
+
+class ShoppingCartAdmin(admin.ModelAdmin):
+ list_display = ('user', 'created_at', 'updated_at')
+
+
+class CartItemAdmin(admin.ModelAdmin):
+ list_display = ('cart', 'resource', 'quantity', 'added_at')
+
+
+class SubscriptionAdmin(admin.ModelAdmin):
+ list_display = ('user', 'is_active', 'start_date', 'end_date')
+
+
+admin.site.register(Product, ProductAdmin)
+admin.site.register(SubscriptionPlan)
+admin.site.register(ShoppingCart)
+admin.site.register(CartItem)
+admin.site.register(Subscription)
diff --git a/pyblackbird_cc/payments/apps.py b/pyblackbird_cc/payments/apps.py
new file mode 100644
index 0000000..99c845f
--- /dev/null
+++ b/pyblackbird_cc/payments/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class PaymentsConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "pyblackbird_cc.payments"
diff --git a/pyblackbird_cc/payments/migrations/0001_initial.py b/pyblackbird_cc/payments/migrations/0001_initial.py
new file mode 100644
index 0000000..33b7602
--- /dev/null
+++ b/pyblackbird_cc/payments/migrations/0001_initial.py
@@ -0,0 +1,103 @@
+# Generated by Django 5.0.4 on 2024-09-03 19:21
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ("resources", "0019_alter_pdfpagesnapshot_options_and_more"),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="ShoppingCart",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("created_at", models.DateTimeField(auto_now_add=True)),
+ ("updated_at", models.DateTimeField(auto_now=True)),
+ (
+ "user",
+ models.OneToOneField(
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ],
+ ),
+ migrations.CreateModel(
+ name="Subscription",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("is_active", models.BooleanField(default=False)),
+ ("start_date", models.DateTimeField(blank=True, null=True)),
+ ("end_date", models.DateTimeField(blank=True, null=True)),
+ (
+ "stripe_subscription_id",
+ models.CharField(blank=True, max_length=255, null=True),
+ ),
+ (
+ "user",
+ models.OneToOneField(
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ],
+ ),
+ migrations.CreateModel(
+ name="CartItem",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("quantity", models.PositiveIntegerField(default=1)),
+ ("added_at", models.DateTimeField(auto_now_add=True)),
+ (
+ "resource",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="resources.resource",
+ ),
+ ),
+ (
+ "cart",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="items",
+ to="payments.shoppingcart",
+ ),
+ ),
+ ],
+ options={
+ "unique_together": {("cart", "resource")},
+ },
+ ),
+ ]
diff --git a/pyblackbird_cc/payments/migrations/0002_subscriptionplan_and_more.py b/pyblackbird_cc/payments/migrations/0002_subscriptionplan_and_more.py
new file mode 100644
index 0000000..cab49b5
--- /dev/null
+++ b/pyblackbird_cc/payments/migrations/0002_subscriptionplan_and_more.py
@@ -0,0 +1,36 @@
+# Generated by Django 5.0.4 on 2024-09-03 19:32
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("payments", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="SubscriptionPlan",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("name", models.CharField(max_length=255)),
+ ("price", models.DecimalField(decimal_places=2, max_digits=6)),
+ ("description", models.TextField()),
+ ("allowed_downloads", models.PositiveIntegerField()),
+ ("stripe_plan_id", models.CharField(max_length=255)),
+ ],
+ ),
+ migrations.RemoveField(
+ model_name="subscription",
+ name="stripe_subscription_id",
+ ),
+ ]
diff --git a/pyblackbird_cc/payments/migrations/0003_product_price.py b/pyblackbird_cc/payments/migrations/0003_product_price.py
new file mode 100644
index 0000000..b12d5dc
--- /dev/null
+++ b/pyblackbird_cc/payments/migrations/0003_product_price.py
@@ -0,0 +1,53 @@
+# Generated by Django 5.0.4 on 2024-09-04 19:01
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("payments", "0002_subscriptionplan_and_more"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="Product",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("name", models.CharField(max_length=255)),
+ ("stripe_product_id", models.CharField(max_length=100)),
+ ],
+ ),
+ migrations.CreateModel(
+ name="Price",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("price", models.IntegerField(default=0)),
+ ("stripe_product_id", models.CharField(max_length=100)),
+ (
+ "product",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="payments.product",
+ ),
+ ),
+ ],
+ ),
+ ]
diff --git a/pyblackbird_cc/payments/migrations/0004_rename_stripe_product_id_price_stripe_price_id.py b/pyblackbird_cc/payments/migrations/0004_rename_stripe_product_id_price_stripe_price_id.py
new file mode 100644
index 0000000..e5a339f
--- /dev/null
+++ b/pyblackbird_cc/payments/migrations/0004_rename_stripe_product_id_price_stripe_price_id.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.4 on 2024-09-04 19:17
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("payments", "0003_product_price"),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name="price",
+ old_name="stripe_product_id",
+ new_name="stripe_price_id",
+ ),
+ ]
diff --git a/pyblackbird_cc/payments/migrations/__init__.py b/pyblackbird_cc/payments/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pyblackbird_cc/payments/migrations/__init__.py
diff --git a/pyblackbird_cc/payments/models.py b/pyblackbird_cc/payments/models.py
new file mode 100644
index 0000000..2b66c14
--- /dev/null
+++ b/pyblackbird_cc/payments/models.py
@@ -0,0 +1,61 @@
+from django.conf import settings
+from django.db import models
+
+from pyblackbird_cc.resources.models import Resource
+
+
+class Product(models.Model):
+ name = models.CharField(max_length=255)
+ stripe_product_id = models.CharField(max_length=100)
+
+
+class Price(models.Model):
+ product = models.ForeignKey(Product, on_delete=models.CASCADE)
+ price = models.IntegerField(default=0)
+ stripe_price_id = models.CharField(max_length=100)
+
+ def get_display_price(self):
+ return "{0:.2f}".format(self.price / 100)
+
+
+class ShoppingCart(models.Model):
+ user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ def __str__(self):
+ return f"Shopping Cart for {self.user.username}"
+
+
+class CartItem(models.Model):
+ cart = models.ForeignKey(ShoppingCart, on_delete=models.CASCADE, related_name="items")
+ resource = models.ForeignKey(Resource, on_delete=models.CASCADE)
+ quantity = models.PositiveIntegerField(default=1)
+ added_at = models.DateTimeField(auto_now_add=True)
+
+ class Meta:
+ unique_together = ("cart", "resource")
+
+ def __str__(self):
+ return f"{self.quantity} of {self.resource.name} in {self.cart.user.username}'s cart"
+
+ def get_total_price(self):
+ return self.quantity * self.resource.price
+
+
+class SubscriptionPlan(models.Model):
+ name = models.CharField(max_length=255)
+ price = models.DecimalField(max_digits=6, decimal_places=2)
+ description = models.TextField()
+ allowed_downloads = models.PositiveIntegerField()
+ stripe_plan_id = models.CharField(max_length=255)
+
+
+class Subscription(models.Model):
+ user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
+ is_active = models.BooleanField(default=False)
+ start_date = models.DateTimeField(null=True, blank=True)
+ end_date = models.DateTimeField(null=True, blank=True)
+
+ def __str__(self):
+ return f"Subscription for {self.user.username}"
diff --git a/pyblackbird_cc/payments/tests.py b/pyblackbird_cc/payments/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/pyblackbird_cc/payments/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/pyblackbird_cc/payments/urls.py b/pyblackbird_cc/payments/urls.py
new file mode 100644
index 0000000..dcfe3dc
--- /dev/null
+++ b/pyblackbird_cc/payments/urls.py
@@ -0,0 +1,17 @@
+from django.urls import path
+
+from . import views
+from .views import CancelView, SuccessView
+
+app_name = "payments"
+
+urlpatterns = [
+ # path("checkout/", views.checkout, name="checkout"),
+ # path("cart/", views.cart_detail, name="cart_detail"),
+ path("success/", SuccessView.as_view(), name="success"),
+ path("cancel/", CancelView.as_view(), name="cancel"),
+ path("create-checkout-session/<int:pk>/", views.CreateCheckoutSessionView.as_view(),
+ name="create-checkout-session"),
+ path("landing/", views.ProductLandingPageView.as_view(), name="landing"),
+ # path("webhook/", views.webhook, name="webhook"),
+]
diff --git a/pyblackbird_cc/payments/views.py b/pyblackbird_cc/payments/views.py
new file mode 100644
index 0000000..2e53af8
--- /dev/null
+++ b/pyblackbird_cc/payments/views.py
@@ -0,0 +1,85 @@
+import stripe
+from django.conf import settings
+from django.contrib.auth.decorators import login_required
+from django.shortcuts import get_object_or_404
+from django.shortcuts import render, redirect
+from django.views import View
+from django.views.generic import TemplateView
+
+from pyblackbird_cc.resources.models import Resource
+from .models import ShoppingCart, CartItem, Price, Product
+
+stripe.api_key = settings.STRIPE_SECRET_KEY
+
+
+class CreateCheckoutSessionView(View):
+ def post(self, request, *args, **kwargs):
+ price = Price.objects.get(id=self.kwargs["pk"])
+ domain = "http://localhost:8000"
+ checkout_session = stripe.checkout.Session.create(
+ payment_method_types=["card"],
+ line_items=[
+ {
+ "price": price.stripe_price_id,
+ "quantity": 1,
+ },
+ ],
+ mode="payment",
+ success_url=domain + "payments/success/",
+ cancel_url=domain + "payments/cancel/",
+ )
+ return redirect(checkout_session.url, code=303)
+
+
+class SuccessView(TemplateView):
+ template_name = "payments/success.html"
+
+
+class CancelView(TemplateView):
+ template_name = "payments/cancel.html"
+
+
+class ProductLandingPageView(TemplateView):
+ template_name = "payments/landingpage.html"
+
+ def get_context_data(self, **kwargs):
+ product = Product.objects.get(name="Worksheet 1")
+ prices = Price.objects.filter(product=product)
+ context = super(ProductLandingPageView, self).get_context_data(**kwargs)
+ context.update({"product": product, "prices": prices})
+ return context
+
+
+@login_required
+def add_to_cart(request, resource_id):
+ resource = get_object_or_404(Resource, id=resource_id)
+ cart, created = ShoppingCart.objects.get_or_create(user=request.user)
+ cart_item, created = CartItem.objects.get_or_create(cart=cart, resource=resource)
+ cart_item.quantity += 1
+ cart_item.save()
+ return redirect("cart_detail")
+
+
+@login_required
+def cart_detail(request):
+ cart, created = ShoppingCart.objects.get_or_create(user=request.user)
+ return render(request, "payments/cart_detail.html", {"cart": cart})
+
+
+@login_required
+def checkout(request):
+ cart = ShoppingCart.objects.get(user=request.user)
+ total = sum(item.get_total_price() for item in cart.items.all())
+
+ if request.method == "POST":
+ # Create Stripe PaymentIntent
+ intent = stripe.PaymentIntent.create(
+ amount=int(total * 100), # Stripe amount is in cents
+ currency="usd",
+ automatic_payment_methods={"enabled": True},
+ )
+
+ # Redirect to Stripe checkout or handle payment confirmation
+ return render(request, "cart/checkout_success.html")
+
+ return render(request, "cart/checkout.html", {"cart": cart, "total": total})
diff --git a/pyblackbird_cc/resources/migrations/0019_alter_pdfpagesnapshot_options_and_more.py b/pyblackbird_cc/resources/migrations/0019_alter_pdfpagesnapshot_options_and_more.py
new file mode 100644
index 0000000..511d747
--- /dev/null
+++ b/pyblackbird_cc/resources/migrations/0019_alter_pdfpagesnapshot_options_and_more.py
@@ -0,0 +1,26 @@
+# Generated by Django 5.0.4 on 2024-09-03 19:21
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("resources", "0018_alter_resource_subcategories"),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name="pdfpagesnapshot",
+ options={"verbose_name_plural": "PDF Page Snapshots"},
+ ),
+ migrations.AlterModelOptions(
+ name="pdfresource",
+ options={"verbose_name_plural": "PDF Resources"},
+ ),
+ migrations.AddField(
+ model_name="resource",
+ name="price",
+ field=models.DecimalField(decimal_places=2, default=0.0, max_digits=6),
+ ),
+ ]
diff --git a/pyblackbird_cc/resources/models.py b/pyblackbird_cc/resources/models.py
index f938cb4..16186fa 100644
--- a/pyblackbird_cc/resources/models.py
+++ b/pyblackbird_cc/resources/models.py
@@ -34,6 +34,13 @@ DESC_HELP_TEXT = """
# Create your models here.
class Resource(models.Model):
name = models.CharField(max_length=255, null=False)
+ price = models.DecimalField(
+ max_digits=6,
+ decimal_places=2,
+ default=0.00,
+ null=False,
+ blank=False,
+ )
thumbnail_filenames = models.JSONField(
null=False,
verbose_name="Thumbnail filenames",
diff --git a/pyblackbird_cc/resources/views.py b/pyblackbird_cc/resources/views.py
index 1f68768..f802350 100644
--- a/pyblackbird_cc/resources/views.py
+++ b/pyblackbird_cc/resources/views.py
@@ -15,12 +15,19 @@ from django.shortcuts import redirect
from django.shortcuts import render
from . import services
-from .forms import ResourceCreateForm, ResourceUpdateThumbnailsForm, ResourceUpdatePDFsForm
+from .forms import ResourceCreateForm
from .forms import ResourceUpdateMetadataForm
-from .models import PDFPageSnapshot, ResourceSubcategory, ResourceCategory
+from .forms import ResourceUpdatePDFsForm
+from .forms import ResourceUpdateThumbnailsForm
+from .models import PDFPageSnapshot
from .models import PDFResource
from .models import Resource
-from .s3 import get_presigned_obj_url, upload_files_to_s3, upload_to_s3, upload_snapshotted_pages_to_s3
+from .models import ResourceCategory
+from .models import ResourceSubcategory
+from .s3 import get_presigned_obj_url
+from .s3 import upload_files_to_s3
+from .s3 import upload_snapshotted_pages_to_s3
+from .s3 import upload_to_s3
logger = logging.getLogger(__name__)
@@ -87,10 +94,7 @@ def _extract_metadata_from_resource(resource_obj) -> ResourceInfo | None:
for f in resource_obj.thumbnail_filenames
]
try:
- if resource_obj.subcategories:
- arc_name = resource_obj.subcategories.name
- else:
- arc_name = None
+ arc_name = resource_obj.subcategories.name if resource_obj.subcategories else None
return ResourceInfo(
id=resource_obj.id,
name=resource_obj.name,
@@ -119,7 +123,7 @@ def _extract_metadata_from_resource(resource_obj) -> ResourceInfo | None:
def index(request):
resource_objs = Resource.objects.all()
categories = ResourceCategory.objects.all()
- category = request.GET.get('category', 'all')
+ category = request.GET.get("category", "all")
resource_list = [_extract_metadata_from_resource(r) for r in resource_objs]
@@ -127,11 +131,11 @@ def index(request):
featured_resources = [r for r in resource_list if r.feature_slot]
featured_resources = sorted(featured_resources, key=lambda resource: resource.feature_slot)
- if category != 'all':
+ if category != "all":
resource_list = [r for r in resource_list if r.main_resource_category_name == category]
paginator = Paginator(resource_list, 20)
- page_number = request.GET.get('page')
+ page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
context = {
@@ -157,7 +161,7 @@ def create_metadata(pdf_files) -> Generator[tuple[services.PDFMetadata, str], No
"""
Generates PDF metadata and snapshot images for a list of PDF files.
- This function takes a list of PDF file objects, creates temporary files for each one,
+ This function takes a list of PDF file objects, creates temporary files for each one,
and then uses the `services.get_pdf_metadata_from_path` and `services.export_pages_as_images` functions to extract metadata and snapshot images for each PDF.
The function yields a tuple containing the PDF metadata and a list of snapshot image paths for each PDF file.
@@ -166,7 +170,7 @@ def create_metadata(pdf_files) -> Generator[tuple[services.PDFMetadata, str], No
pdf_files (list[django.core.files.uploadedfile.InMemoryUploadedFile]): A list of PDF file objects.
Yields:
- tuple[services.PDFMetadata, list[str]]: A tuple containing the PDF metadata and a list of snapshot image paths for each PDF file.
+ tuple[servies.PDFMetadata, list[str]]: A tuple containing the PDF metadata and a list of snapshot image paths for each PDF file.
"""
with tempfile.TemporaryDirectory() as temp_dir:
for pdf_file in pdf_files:
@@ -250,9 +254,13 @@ def create_resource(request):
return redirect("resources:resource_detail", resource_id=resource.id)
except IntegrityError:
slot = form.cleaned_data["feature_slot"]
- messages.add_message(request, messages.ERROR, f"Feature slot {slot} is already "
- "in use. Quit this form and remove from existing "
- "resource.")
+ messages.add_message(
+ request,
+ messages.ERROR,
+ f"Feature slot {slot} is already "
+ "in use. Quit this form and remove from existing "
+ "resource.",
+ )
except Exception:
logger.exception("Error creating resource")
form.add_error(None, "An error occurred while creating the resource.")
@@ -293,9 +301,7 @@ def resource_detail(request, resource_id):
"resource_type": resource_obj.resource_type.name,
"main_resource_category": resource_obj.main_resource_category.name,
"additional_resource_category": (
- resource_obj.subcategories.name
- if resource_obj.subcategories
- else None
+ resource_obj.subcategories.name if resource_obj.subcategories else None
),
"age_range": resource_obj.age_range,
"curriculum": resource_obj.curriculum,
@@ -378,8 +384,8 @@ def add_resource_pdfs(request, pk):
"""
Adds PDF files to a resource in the system.
- This view handles the process of adding PDF files to an existing resource.
- It allows the user to upload one or more PDF files, which are then processed and associated with the resource.
+ This view handles the process of adding PDF files to an existing resource.
+ It allows the user to upload one or more PDF files, which are then processed and associated with the resource.
The view creates PDFResource and PDFPageSnapshot objects to represent the uploaded PDFs and their page snapshots, and uploads the files to S3 storage.
Args:
diff --git a/pyblackbird_cc/templates/base.html b/pyblackbird_cc/templates/base.html
index 9e74a50..ce80ca3 100644
--- a/pyblackbird_cc/templates/base.html
+++ b/pyblackbird_cc/templates/base.html
@@ -47,6 +47,7 @@
<script defer src="{% static 'js/project.js' %}"></script>
<script src="{% static "bootstrap/js/bootstrap.js" %}"></script>
<script src="{% static "js/htmx.min.js" %}"></script>
+ <script src="https://js.stripe.com/v3/"></script>
{% endblock javascript %}
</head>
<body>
diff --git a/pyblackbird_cc/templates/payments/cancel.html b/pyblackbird_cc/templates/payments/cancel.html
new file mode 100644
index 0000000..efe0fd3
--- /dev/null
+++ b/pyblackbird_cc/templates/payments/cancel.html
@@ -0,0 +1,10 @@
+{% extends "base.html" %}
+
+{% block content %}
+ <div class="container my-5">
+ <div class="row">
+ <h2>Transaction Cancelled</h2>
+ <p>Your transaction was cancelled.</p>
+ </div>
+ </div>
+{% endblock content %} \ No newline at end of file
diff --git a/pyblackbird_cc/templates/payments/cart_detail.html b/pyblackbird_cc/templates/payments/cart_detail.html
new file mode 100644
index 0000000..ba8c43e
--- /dev/null
+++ b/pyblackbird_cc/templates/payments/cart_detail.html
@@ -0,0 +1,41 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block content %}
+<div class="container my-5">
+ <h1 class="mb-4">Shopping Cart</h1>
+ {% if cart_items %}
+ <table class="table">
+ <thead>
+ <tr>
+ <th>Product</th>
+ <th>Price</th>
+ <th>Quantity</th>
+ <th>Total</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for item in cart_items %}
+ <tr>
+ <td>{{ item.product.name }}</td>
+ <td>${{ item.product.price }}</td>
+ <td>{{ item.quantity }}</td>
+ <td>${{ item.total }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ <tfoot>
+ <tr>
+ <th colspan="3" class="text-end">Total:</th>
+ <th>${{ cart_total }}</th>
+ </tr>
+ </tfoot>
+ </table>
+ <div class="d-flex justify-content-end">
+ <a href="{% url 'checkout' %}" class="btn btn-primary">Checkout</a>
+ </div>
+ {% else %}
+ <p>Your cart is empty.</p>
+ {% endif %}
+</div>
+{% endblock %}
diff --git a/pyblackbird_cc/templates/payments/landingpage.html b/pyblackbird_cc/templates/payments/landingpage.html
new file mode 100644
index 0000000..9897301
--- /dev/null
+++ b/pyblackbird_cc/templates/payments/landingpage.html
@@ -0,0 +1,29 @@
+{% extends "base.html" %}
+{% block content %}
+ <div class="container my-5">
+ <div class="row">
+ <h2>Welcome to PyBlackbird - Buy this</h2>
+
+ <section>
+ <div class="product">
+ <div class="description">
+ <h3>{{ product.name }}</h3>
+ <hr/>
+ {% for price in prices %}
+
+ <div>
+ <h5>${{ price.get_display_price }}</h5>
+ <form action="{% url 'payments:create-checkout-session' price.id %}" method="POST">
+ {% csrf_token %}
+ <button type="submit" class="btn btn-primary">Checkout</button>
+ </form>
+ </div>
+
+ {% endfor %}
+ </div>
+ </div>
+ </section>
+
+
+ </div>
+{% endblock content %} \ No newline at end of file
diff --git a/pyblackbird_cc/templates/payments/success.html b/pyblackbird_cc/templates/payments/success.html
new file mode 100644
index 0000000..c1fb089
--- /dev/null
+++ b/pyblackbird_cc/templates/payments/success.html
@@ -0,0 +1,11 @@
+{% extends "base.html" %}
+
+{% load static %}
+
+{% block content %}
+ <div class="container my-5">
+ <div class="row">
+ <h2>Thanks for your order!</h2>
+ <p>You have successfully transacted.</p>
+ </div>
+{% endblock content %} \ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index cb27ff3..23ac971 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -85,7 +85,7 @@ exclude = [
"staticfiles/*"
]
# Same as Django: https://github.com/cookiecutter/cookiecutter-django/issues/4792.
-line-length = 140
+line-length = 100
indent-width = 4
target-version = "py312"
@@ -149,6 +149,8 @@ select = [
ignore = [
"TRY003",
"RUF001",
+ "COM812",
+ "ISC001",
"EM101",
"S101", # Use of assert detected https://docs.astral.sh/ruff/rules/assert/
"RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
diff --git a/requirements/local.txt b/requirements/local.txt
index e670c96..0ce1688 100644
--- a/requirements/local.txt
+++ b/requirements/local.txt
@@ -34,3 +34,5 @@ factory-boy==3.3.0 # https://github.com/FactoryBoy/factory_boy
django-debug-toolbar==4.3.0 # https://github.com/jazzband/django-debug-toolbar
django-coverage-plugin==3.1.0 # https://github.com/nedbat/django_coverage_plugin
pytest-django==4.8.0 # https://github.com/pytest-dev/pytest-django
+
+botocore~=1.34.93 \ No newline at end of file