From ca83c38cd20397fd13706e682a0767b1502b4b08 Mon Sep 17 00:00:00 2001 From: sarahxsanders Date: Wed, 21 Jan 2026 17:25:29 -0500 Subject: [PATCH 1/3] chore: add django app --- apps/django/django3-saas/.env.example | 21 ++ apps/django/django3-saas/.gitignore | 29 ++ apps/django/django3-saas/accounts/__init__.py | 0 apps/django/django3-saas/accounts/admin.py | 11 + apps/django/django3-saas/accounts/forms.py | 22 ++ .../accounts/migrations/0001_initial.py | 43 +++ ...ail_verified_at_user_stripe_customer_id.py | 23 ++ .../accounts/migrations/__init__.py | 0 apps/django/django3-saas/accounts/models.py | 31 ++ apps/django/django3-saas/accounts/urls.py | 17 + apps/django/django3-saas/accounts/views.py | 71 ++++ apps/django/django3-saas/billing/__init__.py | 0 apps/django/django3-saas/billing/admin.py | 117 +++++++ .../billing/management/__init__.py | 0 .../billing/management/commands/__init__.py | 0 .../billing/management/commands/seed_plans.py | 75 ++++ .../billing/migrations/0001_initial.py | 56 +++ .../billing/migrations/__init__.py | 0 apps/django/django3-saas/billing/models.py | 71 ++++ apps/django/django3-saas/billing/urls.py | 15 + apps/django/django3-saas/billing/views.py | 327 ++++++++++++++++++ apps/django/django3-saas/config/__init__.py | 0 apps/django/django3-saas/config/settings.py | 113 ++++++ apps/django/django3-saas/config/urls.py | 13 + apps/django/django3-saas/config/views.py | 9 + apps/django/django3-saas/config/wsgi.py | 5 + .../django/django3-saas/dashboard/__init__.py | 0 apps/django/django3-saas/dashboard/admin.py | 18 + apps/django/django3-saas/dashboard/forms.py | 11 + .../dashboard/migrations/0001_initial.py | 43 +++ .../dashboard/migrations/__init__.py | 0 apps/django/django3-saas/dashboard/models.py | 38 ++ apps/django/django3-saas/dashboard/urls.py | 12 + apps/django/django3-saas/dashboard/views.py | 111 ++++++ apps/django/django3-saas/manage.py | 18 + .../django/django3-saas/marketing/__init__.py | 0 apps/django/django3-saas/marketing/urls.py | 9 + apps/django/django3-saas/marketing/views.py | 11 + apps/django/django3-saas/requirements.txt | 6 + apps/django/django3-saas/static/.gitkeep | 0 .../templates/accounts/login.html | 18 + .../templates/accounts/password_reset.html | 16 + .../accounts/password_reset_complete.html | 9 + .../accounts/password_reset_confirm.html | 18 + .../accounts/password_reset_done.html | 11 + .../accounts/password_reset_email.html | 10 + .../accounts/password_reset_subject.txt | 1 + .../templates/accounts/register.html | 15 + .../templates/accounts/settings.html | 23 ++ apps/django/django3-saas/templates/base.html | 87 +++++ .../templates/billing/cancel.html | 23 ++ .../templates/billing/change_plan.html | 23 ++ .../templates/billing/manage.html | 46 +++ .../templates/billing/pricing.html | 53 +++ .../templates/billing/subscribe.html | 30 ++ .../templates/dashboard/create_project.html | 16 + .../templates/dashboard/delete_project.html | 18 + .../templates/dashboard/edit_project.html | 16 + .../templates/dashboard/index.html | 114 ++++++ .../templates/dashboard/projects.html | 38 ++ .../django3-saas/templates/errors/404.html | 12 + .../django3-saas/templates/errors/500.html | 12 + .../templates/marketing/features.html | 44 +++ .../templates/marketing/home.html | 38 ++ 64 files changed, 2037 insertions(+) create mode 100644 apps/django/django3-saas/.env.example create mode 100644 apps/django/django3-saas/.gitignore create mode 100644 apps/django/django3-saas/accounts/__init__.py create mode 100644 apps/django/django3-saas/accounts/admin.py create mode 100644 apps/django/django3-saas/accounts/forms.py create mode 100644 apps/django/django3-saas/accounts/migrations/0001_initial.py create mode 100644 apps/django/django3-saas/accounts/migrations/0002_user_email_verified_at_user_stripe_customer_id.py create mode 100644 apps/django/django3-saas/accounts/migrations/__init__.py create mode 100644 apps/django/django3-saas/accounts/models.py create mode 100644 apps/django/django3-saas/accounts/urls.py create mode 100644 apps/django/django3-saas/accounts/views.py create mode 100644 apps/django/django3-saas/billing/__init__.py create mode 100644 apps/django/django3-saas/billing/admin.py create mode 100644 apps/django/django3-saas/billing/management/__init__.py create mode 100644 apps/django/django3-saas/billing/management/commands/__init__.py create mode 100644 apps/django/django3-saas/billing/management/commands/seed_plans.py create mode 100644 apps/django/django3-saas/billing/migrations/0001_initial.py create mode 100644 apps/django/django3-saas/billing/migrations/__init__.py create mode 100644 apps/django/django3-saas/billing/models.py create mode 100644 apps/django/django3-saas/billing/urls.py create mode 100644 apps/django/django3-saas/billing/views.py create mode 100644 apps/django/django3-saas/config/__init__.py create mode 100644 apps/django/django3-saas/config/settings.py create mode 100644 apps/django/django3-saas/config/urls.py create mode 100644 apps/django/django3-saas/config/views.py create mode 100644 apps/django/django3-saas/config/wsgi.py create mode 100644 apps/django/django3-saas/dashboard/__init__.py create mode 100644 apps/django/django3-saas/dashboard/admin.py create mode 100644 apps/django/django3-saas/dashboard/forms.py create mode 100644 apps/django/django3-saas/dashboard/migrations/0001_initial.py create mode 100644 apps/django/django3-saas/dashboard/migrations/__init__.py create mode 100644 apps/django/django3-saas/dashboard/models.py create mode 100644 apps/django/django3-saas/dashboard/urls.py create mode 100644 apps/django/django3-saas/dashboard/views.py create mode 100644 apps/django/django3-saas/manage.py create mode 100644 apps/django/django3-saas/marketing/__init__.py create mode 100644 apps/django/django3-saas/marketing/urls.py create mode 100644 apps/django/django3-saas/marketing/views.py create mode 100644 apps/django/django3-saas/requirements.txt create mode 100644 apps/django/django3-saas/static/.gitkeep create mode 100644 apps/django/django3-saas/templates/accounts/login.html create mode 100644 apps/django/django3-saas/templates/accounts/password_reset.html create mode 100644 apps/django/django3-saas/templates/accounts/password_reset_complete.html create mode 100644 apps/django/django3-saas/templates/accounts/password_reset_confirm.html create mode 100644 apps/django/django3-saas/templates/accounts/password_reset_done.html create mode 100644 apps/django/django3-saas/templates/accounts/password_reset_email.html create mode 100644 apps/django/django3-saas/templates/accounts/password_reset_subject.txt create mode 100644 apps/django/django3-saas/templates/accounts/register.html create mode 100644 apps/django/django3-saas/templates/accounts/settings.html create mode 100644 apps/django/django3-saas/templates/base.html create mode 100644 apps/django/django3-saas/templates/billing/cancel.html create mode 100644 apps/django/django3-saas/templates/billing/change_plan.html create mode 100644 apps/django/django3-saas/templates/billing/manage.html create mode 100644 apps/django/django3-saas/templates/billing/pricing.html create mode 100644 apps/django/django3-saas/templates/billing/subscribe.html create mode 100644 apps/django/django3-saas/templates/dashboard/create_project.html create mode 100644 apps/django/django3-saas/templates/dashboard/delete_project.html create mode 100644 apps/django/django3-saas/templates/dashboard/edit_project.html create mode 100644 apps/django/django3-saas/templates/dashboard/index.html create mode 100644 apps/django/django3-saas/templates/dashboard/projects.html create mode 100644 apps/django/django3-saas/templates/errors/404.html create mode 100644 apps/django/django3-saas/templates/errors/500.html create mode 100644 apps/django/django3-saas/templates/marketing/features.html create mode 100644 apps/django/django3-saas/templates/marketing/home.html diff --git a/apps/django/django3-saas/.env.example b/apps/django/django3-saas/.env.example new file mode 100644 index 00000000..b8e3ce5c --- /dev/null +++ b/apps/django/django3-saas/.env.example @@ -0,0 +1,21 @@ +# Django settings +SECRET_KEY=your-secret-key-here +DEBUG=True +ALLOWED_HOSTS=localhost,127.0.0.1 + +# Database (defaults to SQLite if not set) +DATABASE_URL= + +# Email settings (defaults to console backend for development) +EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend +EMAIL_HOST= +EMAIL_PORT=587 +EMAIL_HOST_USER= +EMAIL_HOST_PASSWORD= +EMAIL_USE_TLS=True +DEFAULT_FROM_EMAIL=noreply@example.com + +# Stripe settings (optional - for payment processing) +STRIPE_PUBLIC_KEY= +STRIPE_SECRET_KEY= +STRIPE_WEBHOOK_SECRET= diff --git a/apps/django/django3-saas/.gitignore b/apps/django/django3-saas/.gitignore new file mode 100644 index 00000000..be9c7d9b --- /dev/null +++ b/apps/django/django3-saas/.gitignore @@ -0,0 +1,29 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +.venv/ +venv/ +ENV/ + +# Django +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal +*.pot +*.pyc + +# Static files +staticfiles/ + +# Environment +.env + +# IDE +.idea/ +.vscode/ +*.swp +*.swo diff --git a/apps/django/django3-saas/accounts/__init__.py b/apps/django/django3-saas/accounts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/django/django3-saas/accounts/admin.py b/apps/django/django3-saas/accounts/admin.py new file mode 100644 index 00000000..6b043c78 --- /dev/null +++ b/apps/django/django3-saas/accounts/admin.py @@ -0,0 +1,11 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from .models import User + + +@admin.register(User) +class CustomUserAdmin(UserAdmin): + list_display = ['username', 'email', 'is_staff', 'date_joined'] + fieldsets = UserAdmin.fieldsets + ( + ('Profile', {'fields': ('bio',)}), + ) diff --git a/apps/django/django3-saas/accounts/forms.py b/apps/django/django3-saas/accounts/forms.py new file mode 100644 index 00000000..3edb3684 --- /dev/null +++ b/apps/django/django3-saas/accounts/forms.py @@ -0,0 +1,22 @@ +from django import forms +from django.contrib.auth.forms import UserCreationForm, AuthenticationForm +from .models import User + + +class RegisterForm(UserCreationForm): + email = forms.EmailField(required=True) + company_name = forms.CharField(max_length=200, required=False) + + class Meta: + model = User + fields = ['username', 'email', 'company_name', 'password1', 'password2'] + + +class LoginForm(AuthenticationForm): + username = forms.CharField(widget=forms.TextInput(attrs={'autofocus': True})) + + +class ProfileForm(forms.ModelForm): + class Meta: + model = User + fields = ['first_name', 'last_name', 'email', 'company_name'] diff --git a/apps/django/django3-saas/accounts/migrations/0001_initial.py b/apps/django/django3-saas/accounts/migrations/0001_initial.py new file mode 100644 index 00000000..c4e96d1f --- /dev/null +++ b/apps/django/django3-saas/accounts/migrations/0001_initial.py @@ -0,0 +1,43 @@ +from django.db import migrations, models +import django.contrib.auth.models +import django.contrib.auth.validators +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('company_name', models.CharField(blank=True, max_length=200)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/apps/django/django3-saas/accounts/migrations/0002_user_email_verified_at_user_stripe_customer_id.py b/apps/django/django3-saas/accounts/migrations/0002_user_email_verified_at_user_stripe_customer_id.py new file mode 100644 index 00000000..cc6746b5 --- /dev/null +++ b/apps/django/django3-saas/accounts/migrations/0002_user_email_verified_at_user_stripe_customer_id.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.27 on 2026-01-21 22:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='email_verified_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='user', + name='stripe_customer_id', + field=models.CharField(blank=True, max_length=100), + ), + ] diff --git a/apps/django/django3-saas/accounts/migrations/__init__.py b/apps/django/django3-saas/accounts/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/django/django3-saas/accounts/models.py b/apps/django/django3-saas/accounts/models.py new file mode 100644 index 00000000..747a354d --- /dev/null +++ b/apps/django/django3-saas/accounts/models.py @@ -0,0 +1,31 @@ +from hashlib import md5 +from django.contrib.auth.models import AbstractUser +from django.db import models + + +class User(AbstractUser): + company_name = models.CharField(max_length=200, blank=True) + email_verified_at = models.DateTimeField(null=True, blank=True) + + # Stripe customer ID for billing + stripe_customer_id = models.CharField(max_length=100, blank=True) + + def __str__(self): + return self.username + + def avatar_url(self, size=128): + digest = md5(self.email.lower().encode('utf-8')).hexdigest() + return f'https://www.gravatar.com/avatar/{digest}?d=identicon&s={size}' + + def get_active_subscription(self): + return self.subscriptions.filter(status='active').first() + + def is_subscribed(self): + return self.subscriptions.filter(status='active').exists() + + def get_plan(self): + sub = self.get_active_subscription() + return sub.plan if sub else None + + def is_email_verified(self): + return self.email_verified_at is not None diff --git a/apps/django/django3-saas/accounts/urls.py b/apps/django/django3-saas/accounts/urls.py new file mode 100644 index 00000000..52720159 --- /dev/null +++ b/apps/django/django3-saas/accounts/urls.py @@ -0,0 +1,17 @@ +from django.urls import path +from . import views + +app_name = 'accounts' + +urlpatterns = [ + path('login/', views.CustomLoginView.as_view(), name='login'), + path('logout/', views.CustomLogoutView.as_view(), name='logout'), + path('register/', views.register, name='register'), + path('settings/', views.settings, name='settings'), + + # Password reset + path('password-reset/', views.CustomPasswordResetView.as_view(), name='password_reset'), + path('password-reset/done/', views.CustomPasswordResetDoneView.as_view(), name='password_reset_done'), + path('password-reset///', views.CustomPasswordResetConfirmView.as_view(), name='password_reset_confirm'), + path('password-reset/complete/', views.CustomPasswordResetCompleteView.as_view(), name='password_reset_complete'), +] diff --git a/apps/django/django3-saas/accounts/views.py b/apps/django/django3-saas/accounts/views.py new file mode 100644 index 00000000..03b8ea06 --- /dev/null +++ b/apps/django/django3-saas/accounts/views.py @@ -0,0 +1,71 @@ +from django.shortcuts import render, redirect +from django.contrib.auth import login +from django.contrib.auth.decorators import login_required +from django.contrib.auth.views import ( + LoginView, LogoutView, + PasswordResetView, PasswordResetDoneView, + PasswordResetConfirmView, PasswordResetCompleteView +) +from django.contrib import messages +from django.urls import reverse_lazy +from .forms import RegisterForm, LoginForm, ProfileForm + + +class CustomLoginView(LoginView): + form_class = LoginForm + template_name = 'accounts/login.html' + + +class CustomLogoutView(LogoutView): + next_page = reverse_lazy('accounts:login') + + +class CustomPasswordResetView(PasswordResetView): + template_name = 'accounts/password_reset.html' + email_template_name = 'accounts/password_reset_email.html' + subject_template_name = 'accounts/password_reset_subject.txt' + success_url = reverse_lazy('accounts:password_reset_done') + + +class CustomPasswordResetDoneView(PasswordResetDoneView): + template_name = 'accounts/password_reset_done.html' + + +class CustomPasswordResetConfirmView(PasswordResetConfirmView): + template_name = 'accounts/password_reset_confirm.html' + success_url = reverse_lazy('accounts:password_reset_complete') + + +class CustomPasswordResetCompleteView(PasswordResetCompleteView): + template_name = 'accounts/password_reset_complete.html' + + +def register(request): + if request.user.is_authenticated: + return redirect('dashboard:index') + + if request.method == 'POST': + form = RegisterForm(request.POST) + if form.is_valid(): + user = form.save() + login(request, user) + messages.success(request, 'Registration successful. Welcome!') + return redirect('dashboard:index') + else: + form = RegisterForm() + + return render(request, 'accounts/register.html', {'form': form}) + + +@login_required +def settings(request): + if request.method == 'POST': + form = ProfileForm(request.POST, instance=request.user) + if form.is_valid(): + form.save() + messages.success(request, 'Settings updated.') + return redirect('accounts:settings') + else: + form = ProfileForm(instance=request.user) + + return render(request, 'accounts/settings.html', {'form': form}) diff --git a/apps/django/django3-saas/billing/__init__.py b/apps/django/django3-saas/billing/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/django/django3-saas/billing/admin.py b/apps/django/django3-saas/billing/admin.py new file mode 100644 index 00000000..489b3f00 --- /dev/null +++ b/apps/django/django3-saas/billing/admin.py @@ -0,0 +1,117 @@ +from django.contrib import admin +from django.utils.html import format_html +from django.db.models import Count, Sum +from .models import Plan, Subscription + + +@admin.register(Plan) +class PlanAdmin(admin.ModelAdmin): + list_display = ['name', 'price_display', 'interval', 'subscriber_count', 'is_active', 'is_featured', 'sort_order'] + list_filter = ['is_active', 'is_featured', 'interval'] + prepopulated_fields = {'slug': ('name',)} + ordering = ['sort_order', 'price'] + search_fields = ['name', 'slug'] + readonly_fields = ['created_at', 'updated_at'] + + fieldsets = ( + (None, { + 'fields': ('name', 'slug', 'description') + }), + ('Pricing', { + 'fields': ('price', 'currency', 'interval') + }), + ('Features', { + 'fields': ('features',), + 'description': 'Enter features as a JSON array: ["Feature 1", "Feature 2"]' + }), + ('Display', { + 'fields': ('is_active', 'is_featured', 'sort_order') + }), + ('Stripe', { + 'fields': ('stripe_price_id',), + 'classes': ('collapse',) + }), + ('Timestamps', { + 'fields': ('created_at', 'updated_at'), + 'classes': ('collapse',) + }), + ) + + def price_display(self, obj): + return obj.get_price_display() + price_display.short_description = 'Price' + + def subscriber_count(self, obj): + count = obj.subscriptions.filter(status='active').count() + return format_html('{}', count) + subscriber_count.short_description = 'Active Subscribers' + + def get_queryset(self, request): + return super().get_queryset(request).annotate( + active_subs=Count('subscriptions', filter=models.Q(subscriptions__status='active')) + ) + + +@admin.register(Subscription) +class SubscriptionAdmin(admin.ModelAdmin): + list_display = ['user', 'plan', 'status_badge', 'current_period_start', 'current_period_end', 'is_demo'] + list_filter = ['status', 'plan', 'created_at'] + search_fields = ['user__username', 'user__email', 'stripe_subscription_id'] + raw_id_fields = ['user'] + readonly_fields = ['created_at', 'updated_at'] + date_hierarchy = 'created_at' + + fieldsets = ( + (None, { + 'fields': ('user', 'plan', 'status') + }), + ('Billing Period', { + 'fields': ('current_period_start', 'current_period_end', 'canceled_at') + }), + ('Stripe', { + 'fields': ('stripe_subscription_id', 'stripe_customer_id'), + 'classes': ('collapse',) + }), + ('Timestamps', { + 'fields': ('created_at', 'updated_at'), + 'classes': ('collapse',) + }), + ) + + def status_badge(self, obj): + colors = { + 'active': '#28a745', + 'canceled': '#dc3545', + 'past_due': '#ffc107', + 'trialing': '#17a2b8', + 'paused': '#6c757d', + } + color = colors.get(obj.status, '#6c757d') + return format_html( + '{}', + color, obj.get_status_display() + ) + status_badge.short_description = 'Status' + + def is_demo(self, obj): + if obj.stripe_subscription_id and obj.stripe_subscription_id.startswith('sub_demo_'): + return format_html('Demo') + return format_html('Live') + is_demo.short_description = 'Mode' + + actions = ['mark_as_canceled', 'mark_as_active'] + + def mark_as_canceled(self, request, queryset): + from django.utils import timezone + updated = queryset.update(status='canceled', canceled_at=timezone.now()) + self.message_user(request, f'{updated} subscription(s) marked as canceled.') + mark_as_canceled.short_description = 'Mark selected as canceled' + + def mark_as_active(self, request, queryset): + updated = queryset.update(status='active', canceled_at=None) + self.message_user(request, f'{updated} subscription(s) marked as active.') + mark_as_active.short_description = 'Mark selected as active' + + +# Import models for use in admin queryset +from django.db import models diff --git a/apps/django/django3-saas/billing/management/__init__.py b/apps/django/django3-saas/billing/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/django/django3-saas/billing/management/commands/__init__.py b/apps/django/django3-saas/billing/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/django/django3-saas/billing/management/commands/seed_plans.py b/apps/django/django3-saas/billing/management/commands/seed_plans.py new file mode 100644 index 00000000..425ff16b --- /dev/null +++ b/apps/django/django3-saas/billing/management/commands/seed_plans.py @@ -0,0 +1,75 @@ +from django.core.management.base import BaseCommand +from billing.models import Plan + + +class Command(BaseCommand): + help = 'Seed the database with demo pricing plans' + + def handle(self, *args, **options): + plans_data = [ + { + 'name': 'Starter', + 'slug': 'starter', + 'description': 'Perfect for individuals and small projects.', + 'price': 0, + 'interval': 'month', + 'features': [ + '1 project', + '1,000 API requests/month', + 'Community support', + 'Basic analytics', + ], + 'is_active': True, + 'is_featured': False, + 'sort_order': 1, + 'stripe_price_id': '', + }, + { + 'name': 'Growth', + 'slug': 'growth', + 'description': 'For growing teams that need more power.', + 'price': 29, + 'interval': 'month', + 'features': [ + '10 projects', + '50,000 API requests/month', + 'Email support', + 'Advanced analytics', + 'Team collaboration', + ], + 'is_active': True, + 'is_featured': True, + 'sort_order': 2, + 'stripe_price_id': 'price_growth_monthly', + }, + { + 'name': 'Scale', + 'slug': 'scale', + 'description': 'For businesses that need unlimited scale.', + 'price': 99, + 'interval': 'month', + 'features': [ + 'Unlimited projects', + 'Unlimited API requests', + 'Priority support', + 'Custom analytics', + 'Team collaboration', + 'SSO integration', + 'Dedicated account manager', + ], + 'is_active': True, + 'is_featured': False, + 'sort_order': 3, + 'stripe_price_id': 'price_scale_monthly', + }, + ] + + for plan_data in plans_data: + plan, created = Plan.objects.update_or_create( + slug=plan_data['slug'], + defaults=plan_data + ) + status = 'Created' if created else 'Updated' + self.stdout.write(f'{status}: {plan.name}') + + self.stdout.write(self.style.SUCCESS('Successfully seeded plans')) diff --git a/apps/django/django3-saas/billing/migrations/0001_initial.py b/apps/django/django3-saas/billing/migrations/0001_initial.py new file mode 100644 index 00000000..a7b81374 --- /dev/null +++ b/apps/django/django3-saas/billing/migrations/0001_initial.py @@ -0,0 +1,56 @@ +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Plan', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('slug', models.SlugField(unique=True)), + ('description', models.TextField(blank=True)), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('currency', models.CharField(default='USD', max_length=3)), + ('interval', models.CharField(choices=[('month', 'Monthly'), ('year', 'Yearly')], default='month', max_length=20)), + ('features', models.JSONField(default=list)), + ('is_active', models.BooleanField(default=True)), + ('is_featured', models.BooleanField(default=False)), + ('sort_order', models.IntegerField(default=0)), + ('stripe_price_id', models.CharField(blank=True, max_length=100)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'ordering': ['sort_order', 'price'], + }, + ), + migrations.CreateModel( + name='Subscription', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('active', 'Active'), ('canceled', 'Canceled'), ('past_due', 'Past Due'), ('trialing', 'Trialing'), ('paused', 'Paused')], default='active', max_length=20)), + ('current_period_start', models.DateTimeField()), + ('current_period_end', models.DateTimeField()), + ('canceled_at', models.DateTimeField(blank=True, null=True)), + ('stripe_subscription_id', models.CharField(blank=True, max_length=100)), + ('stripe_customer_id', models.CharField(blank=True, max_length=100)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('plan', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='subscriptions', to='billing.plan')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subscriptions', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/apps/django/django3-saas/billing/migrations/__init__.py b/apps/django/django3-saas/billing/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/django/django3-saas/billing/models.py b/apps/django/django3-saas/billing/models.py new file mode 100644 index 00000000..282e2207 --- /dev/null +++ b/apps/django/django3-saas/billing/models.py @@ -0,0 +1,71 @@ +from django.db import models +from django.conf import settings + + +class Plan(models.Model): + name = models.CharField(max_length=100) + slug = models.SlugField(unique=True) + description = models.TextField(blank=True) + price = models.DecimalField(max_digits=10, decimal_places=2) + currency = models.CharField(max_length=3, default='USD') + interval = models.CharField( + max_length=20, + choices=[('month', 'Monthly'), ('year', 'Yearly')], + default='month' + ) + features = models.JSONField(default=list) + is_active = models.BooleanField(default=True) + is_featured = models.BooleanField(default=False) + sort_order = models.IntegerField(default=0) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # Stripe integration (optional) + stripe_price_id = models.CharField(max_length=100, blank=True) + + class Meta: + ordering = ['sort_order', 'price'] + + def __str__(self): + return self.name + + def get_price_display(self): + if self.price == 0: + return 'Free' + return f'${self.price}/{self.interval}' + + +class Subscription(models.Model): + STATUS_CHOICES = [ + ('active', 'Active'), + ('canceled', 'Canceled'), + ('past_due', 'Past Due'), + ('trialing', 'Trialing'), + ('paused', 'Paused'), + ] + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='subscriptions' + ) + plan = models.ForeignKey(Plan, on_delete=models.PROTECT, related_name='subscriptions') + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active') + current_period_start = models.DateTimeField() + current_period_end = models.DateTimeField() + canceled_at = models.DateTimeField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # Stripe integration (optional) + stripe_subscription_id = models.CharField(max_length=100, blank=True) + stripe_customer_id = models.CharField(max_length=100, blank=True) + + class Meta: + ordering = ['-created_at'] + + def __str__(self): + return f'{self.user.username} - {self.plan.name}' + + def is_active(self): + return self.status == 'active' diff --git a/apps/django/django3-saas/billing/urls.py b/apps/django/django3-saas/billing/urls.py new file mode 100644 index 00000000..6aa1342d --- /dev/null +++ b/apps/django/django3-saas/billing/urls.py @@ -0,0 +1,15 @@ +from django.urls import path +from . import views + +app_name = 'billing' + +urlpatterns = [ + path('pricing/', views.pricing, name='pricing'), + path('subscribe//', views.subscribe, name='subscribe'), + path('success/', views.success, name='success'), + path('manage/', views.manage, name='manage'), + path('change-plan//', views.change_plan, name='change_plan'), + path('cancel/', views.cancel, name='cancel'), + path('portal/', views.billing_portal, name='billing_portal'), + path('webhook/', views.webhook, name='webhook'), +] diff --git a/apps/django/django3-saas/billing/views.py b/apps/django/django3-saas/billing/views.py new file mode 100644 index 00000000..075923e2 --- /dev/null +++ b/apps/django/django3-saas/billing/views.py @@ -0,0 +1,327 @@ +import uuid +from django.shortcuts import render, redirect, get_object_or_404 +from django.contrib.auth.decorators import login_required +from django.contrib import messages +from django.conf import settings +from django.http import HttpResponse +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_POST +from django.utils import timezone +from datetime import timedelta +from .models import Plan, Subscription + +# Check if Stripe is configured +STRIPE_CONFIGURED = bool(getattr(settings, 'STRIPE_SECRET_KEY', '')) + +if STRIPE_CONFIGURED: + import stripe + stripe.api_key = settings.STRIPE_SECRET_KEY + + +def pricing(request): + """Display pricing plans.""" + plans = Plan.objects.filter(is_active=True) + return render(request, 'billing/pricing.html', {'plans': plans}) + + +@login_required +def subscribe(request, plan_slug): + """Subscribe to a plan - redirects to Stripe Checkout or creates demo subscription.""" + plan = get_object_or_404(Plan, slug=plan_slug, is_active=True) + + # Check if user already has an active subscription + existing = request.user.subscriptions.filter(status='active').first() + if existing: + messages.warning(request, 'You already have an active subscription.') + return redirect('billing:manage') + + if request.method == 'POST': + if STRIPE_CONFIGURED and plan.stripe_price_id: + # Create Stripe Checkout Session + try: + checkout_session = stripe.checkout.Session.create( + customer_email=request.user.email, + payment_method_types=['card'], + line_items=[{ + 'price': plan.stripe_price_id, + 'quantity': 1, + }], + mode='subscription', + success_url=request.build_absolute_uri('/billing/success/') + '?session_id={CHECKOUT_SESSION_ID}', + cancel_url=request.build_absolute_uri('/billing/pricing/'), + metadata={ + 'user_id': request.user.id, + 'plan_id': plan.id, + }, + allow_promotion_codes=True, + ) + return redirect(checkout_session.url) + except Exception as e: + messages.error(request, f'Payment error: {str(e)}') + return redirect('billing:pricing') + else: + # Demo mode - create subscription directly + now = timezone.now() + Subscription.objects.create( + user=request.user, + plan=plan, + status='active', + current_period_start=now, + current_period_end=now + timedelta(days=30 if plan.interval == 'month' else 365), + stripe_subscription_id=f'sub_demo_{uuid.uuid4().hex[:12]}', + ) + messages.success(request, f'Successfully subscribed to {plan.name}! (Demo mode)') + return redirect('dashboard:index') + + return render(request, 'billing/subscribe.html', { + 'plan': plan, + 'stripe_configured': STRIPE_CONFIGURED, + }) + + +@login_required +def success(request): + """Handle successful Stripe checkout.""" + session_id = request.GET.get('session_id') + + if STRIPE_CONFIGURED and session_id: + try: + session = stripe.checkout.Session.retrieve(session_id) + messages.success(request, 'Subscription successful! Welcome aboard.') + except Exception: + messages.warning(request, 'Could not verify payment. Please check your subscription status.') + + return redirect('dashboard:index') + + +@login_required +def manage(request): + """Manage subscription - view current plan, upgrade/downgrade options.""" + subscription = request.user.get_active_subscription() + plans = Plan.objects.filter(is_active=True) + return render(request, 'billing/manage.html', { + 'subscription': subscription, + 'plans': plans, + 'stripe_configured': STRIPE_CONFIGURED, + }) + + +@login_required +def change_plan(request, plan_slug): + """Change subscription plan.""" + plan = get_object_or_404(Plan, slug=plan_slug, is_active=True) + subscription = request.user.get_active_subscription() + + if not subscription: + return redirect('billing:subscribe', plan_slug=plan_slug) + + if request.method == 'POST': + if STRIPE_CONFIGURED and subscription.stripe_subscription_id and not subscription.stripe_subscription_id.startswith('sub_demo_'): + # Update Stripe subscription + try: + stripe_sub = stripe.Subscription.retrieve(subscription.stripe_subscription_id) + stripe.Subscription.modify( + subscription.stripe_subscription_id, + items=[{ + 'id': stripe_sub['items']['data'][0].id, + 'price': plan.stripe_price_id, + }], + proration_behavior='create_prorations', + ) + subscription.plan = plan + subscription.save() + messages.success(request, f'Plan changed to {plan.name}.') + except Exception as e: + messages.error(request, f'Error changing plan: {str(e)}') + else: + # Demo mode + subscription.plan = plan + subscription.save() + messages.success(request, f'Plan changed to {plan.name}. (Demo mode)') + + return redirect('billing:manage') + + return render(request, 'billing/change_plan.html', { + 'plan': plan, + 'subscription': subscription, + }) + + +@login_required +def cancel(request): + """Cancel subscription.""" + subscription = request.user.get_active_subscription() + + if not subscription: + messages.warning(request, 'No active subscription to cancel.') + return redirect('billing:manage') + + if request.method == 'POST': + if STRIPE_CONFIGURED and subscription.stripe_subscription_id and not subscription.stripe_subscription_id.startswith('sub_demo_'): + # Cancel at period end in Stripe + try: + stripe.Subscription.modify( + subscription.stripe_subscription_id, + cancel_at_period_end=True, + ) + except Exception as e: + messages.error(request, f'Error canceling: {str(e)}') + return redirect('billing:manage') + + subscription.status = 'canceled' + subscription.canceled_at = timezone.now() + subscription.save() + messages.success(request, 'Subscription canceled. You will have access until the end of your billing period.') + return redirect('billing:manage') + + return render(request, 'billing/cancel.html', {'subscription': subscription}) + + +@login_required +def billing_portal(request): + """Redirect to Stripe Billing Portal for self-service management.""" + if not STRIPE_CONFIGURED: + messages.info(request, 'Billing portal is not available in demo mode.') + return redirect('billing:manage') + + subscription = request.user.get_active_subscription() + if not subscription or not subscription.stripe_customer_id: + messages.warning(request, 'No billing information found.') + return redirect('billing:manage') + + try: + portal_session = stripe.billing_portal.Session.create( + customer=subscription.stripe_customer_id, + return_url=request.build_absolute_uri('/billing/manage/'), + ) + return redirect(portal_session.url) + except Exception as e: + messages.error(request, f'Error accessing billing portal: {str(e)}') + return redirect('billing:manage') + + +@csrf_exempt +@require_POST +def webhook(request): + """Handle Stripe webhooks.""" + payload = request.body + sig_header = request.META.get('HTTP_STRIPE_SIGNATURE') + webhook_secret = getattr(settings, 'STRIPE_WEBHOOK_SECRET', '') + + if not STRIPE_CONFIGURED or not webhook_secret: + return HttpResponse(status=400) + + try: + event = stripe.Webhook.construct_event( + payload, sig_header, webhook_secret + ) + except ValueError: + return HttpResponse(status=400) + except Exception: + return HttpResponse(status=400) + + # Handle the event + if event['type'] == 'checkout.session.completed': + session = event['data']['object'] + _handle_checkout_completed(session) + elif event['type'] == 'customer.subscription.updated': + subscription_data = event['data']['object'] + _handle_subscription_updated(subscription_data) + elif event['type'] == 'customer.subscription.deleted': + subscription_data = event['data']['object'] + _handle_subscription_deleted(subscription_data) + elif event['type'] == 'invoice.payment_failed': + invoice = event['data']['object'] + _handle_payment_failed(invoice) + + return HttpResponse(status=200) + + +def _handle_checkout_completed(session): + """Create subscription after successful checkout.""" + from accounts.models import User + + user_id = session.get('metadata', {}).get('user_id') + plan_id = session.get('metadata', {}).get('plan_id') + + if not user_id or not plan_id: + return + + try: + user = User.objects.get(id=user_id) + plan = Plan.objects.get(id=plan_id) + except (User.DoesNotExist, Plan.DoesNotExist): + return + + # Get subscription details from Stripe + stripe_sub = stripe.Subscription.retrieve(session['subscription']) + + Subscription.objects.create( + user=user, + plan=plan, + status='active', + current_period_start=timezone.datetime.fromtimestamp( + stripe_sub['current_period_start'], tz=timezone.utc + ), + current_period_end=timezone.datetime.fromtimestamp( + stripe_sub['current_period_end'], tz=timezone.utc + ), + stripe_subscription_id=stripe_sub['id'], + stripe_customer_id=stripe_sub['customer'], + ) + + +def _handle_subscription_updated(subscription_data): + """Update subscription status.""" + try: + subscription = Subscription.objects.get( + stripe_subscription_id=subscription_data['id'] + ) + except Subscription.DoesNotExist: + return + + status_map = { + 'active': 'active', + 'past_due': 'past_due', + 'canceled': 'canceled', + 'trialing': 'trialing', + 'paused': 'paused', + } + + subscription.status = status_map.get(subscription_data['status'], 'active') + subscription.current_period_start = timezone.datetime.fromtimestamp( + subscription_data['current_period_start'], tz=timezone.utc + ) + subscription.current_period_end = timezone.datetime.fromtimestamp( + subscription_data['current_period_end'], tz=timezone.utc + ) + subscription.save() + + +def _handle_subscription_deleted(subscription_data): + """Handle subscription cancellation.""" + try: + subscription = Subscription.objects.get( + stripe_subscription_id=subscription_data['id'] + ) + subscription.status = 'canceled' + subscription.canceled_at = timezone.now() + subscription.save() + except Subscription.DoesNotExist: + pass + + +def _handle_payment_failed(invoice): + """Handle failed payment.""" + subscription_id = invoice.get('subscription') + if not subscription_id: + return + + try: + subscription = Subscription.objects.get( + stripe_subscription_id=subscription_id + ) + subscription.status = 'past_due' + subscription.save() + except Subscription.DoesNotExist: + pass diff --git a/apps/django/django3-saas/config/__init__.py b/apps/django/django3-saas/config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/django/django3-saas/config/settings.py b/apps/django/django3-saas/config/settings.py new file mode 100644 index 00000000..3c02dea1 --- /dev/null +++ b/apps/django/django3-saas/config/settings.py @@ -0,0 +1,113 @@ +import os +from pathlib import Path +from dotenv import load_dotenv + +load_dotenv() + +BASE_DIR = Path(__file__).resolve().parent.parent + +SECRET_KEY = os.environ.get('SECRET_KEY', 'django-insecure-dev-key-change-in-production') + +DEBUG = os.environ.get('DEBUG', 'True').lower() in ('true', '1', 'yes') + +ALLOWED_HOSTS = [h.strip() for h in os.environ.get('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',')] + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'accounts', + 'billing', + 'dashboard', + 'marketing', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'config.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'config.wsgi.application' + +DATABASE_URL = os.environ.get('DATABASE_URL', '') +if DATABASE_URL: + import dj_database_url + DATABASES = { + 'default': dj_database_url.parse(DATABASE_URL) + } +else: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } + } + +AUTH_PASSWORD_VALIDATORS = [ + {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, + {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'}, + {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, + {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, +] + +AUTH_USER_MODEL = 'accounts.User' + +LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'UTC' +USE_I18N = True +USE_TZ = True + +STATIC_URL = '/static/' +STATIC_ROOT = BASE_DIR / 'staticfiles' +STATICFILES_DIRS = [BASE_DIR / 'static'] +STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +LOGIN_URL = 'accounts:login' +LOGIN_REDIRECT_URL = 'dashboard:index' +LOGOUT_REDIRECT_URL = 'marketing:home' + +# Email settings (console backend for development) +EMAIL_BACKEND = os.environ.get( + 'EMAIL_BACKEND', + 'django.core.mail.backends.console.EmailBackend' +) +EMAIL_HOST = os.environ.get('EMAIL_HOST', 'localhost') +EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 25)) +EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER', '') +EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD', '') +EMAIL_USE_TLS = os.environ.get('EMAIL_USE_TLS', 'False').lower() in ('true', '1', 'yes') +DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'noreply@example.com') + +# Stripe settings (optional - for billing integration) +STRIPE_PUBLIC_KEY = os.environ.get('STRIPE_PUBLIC_KEY', '') +STRIPE_SECRET_KEY = os.environ.get('STRIPE_SECRET_KEY', '') +STRIPE_WEBHOOK_SECRET = os.environ.get('STRIPE_WEBHOOK_SECRET', '') diff --git a/apps/django/django3-saas/config/urls.py b/apps/django/django3-saas/config/urls.py new file mode 100644 index 00000000..a3d56f6d --- /dev/null +++ b/apps/django/django3-saas/config/urls.py @@ -0,0 +1,13 @@ +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path('admin/', admin.site.urls), + path('accounts/', include('accounts.urls')), + path('billing/', include('billing.urls')), + path('dashboard/', include('dashboard.urls')), + path('', include('marketing.urls')), +] + +handler404 = 'config.views.handler404' +handler500 = 'config.views.handler500' diff --git a/apps/django/django3-saas/config/views.py b/apps/django/django3-saas/config/views.py new file mode 100644 index 00000000..43a79d15 --- /dev/null +++ b/apps/django/django3-saas/config/views.py @@ -0,0 +1,9 @@ +from django.shortcuts import render + + +def handler404(request, exception): + return render(request, 'errors/404.html', status=404) + + +def handler500(request): + return render(request, 'errors/500.html', status=500) diff --git a/apps/django/django3-saas/config/wsgi.py b/apps/django/django3-saas/config/wsgi.py new file mode 100644 index 00000000..885c6e5b --- /dev/null +++ b/apps/django/django3-saas/config/wsgi.py @@ -0,0 +1,5 @@ +import os +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +application = get_wsgi_application() diff --git a/apps/django/django3-saas/dashboard/__init__.py b/apps/django/django3-saas/dashboard/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/django/django3-saas/dashboard/admin.py b/apps/django/django3-saas/dashboard/admin.py new file mode 100644 index 00000000..abd0a00c --- /dev/null +++ b/apps/django/django3-saas/dashboard/admin.py @@ -0,0 +1,18 @@ +from django.contrib import admin +from .models import Project, ActivityLog + + +@admin.register(Project) +class ProjectAdmin(admin.ModelAdmin): + list_display = ['name', 'owner', 'is_active', 'created_at'] + list_filter = ['is_active', 'created_at'] + search_fields = ['name', 'description', 'owner__username'] + raw_id_fields = ['owner'] + + +@admin.register(ActivityLog) +class ActivityLogAdmin(admin.ModelAdmin): + list_display = ['user', 'action', 'created_at'] + list_filter = ['action', 'created_at'] + search_fields = ['user__username', 'action', 'description'] + raw_id_fields = ['user'] diff --git a/apps/django/django3-saas/dashboard/forms.py b/apps/django/django3-saas/dashboard/forms.py new file mode 100644 index 00000000..547886cb --- /dev/null +++ b/apps/django/django3-saas/dashboard/forms.py @@ -0,0 +1,11 @@ +from django import forms +from .models import Project + + +class ProjectForm(forms.ModelForm): + class Meta: + model = Project + fields = ['name', 'description', 'is_active'] + widgets = { + 'description': forms.Textarea(attrs={'rows': 4}), + } diff --git a/apps/django/django3-saas/dashboard/migrations/0001_initial.py b/apps/django/django3-saas/dashboard/migrations/0001_initial.py new file mode 100644 index 00000000..606d09e6 --- /dev/null +++ b/apps/django/django3-saas/dashboard/migrations/0001_initial.py @@ -0,0 +1,43 @@ +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Project', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('description', models.TextField(blank=True)), + ('is_active', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='projects', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='ActivityLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('action', models.CharField(max_length=100)), + ('description', models.TextField(blank=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activities', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/apps/django/django3-saas/dashboard/migrations/__init__.py b/apps/django/django3-saas/dashboard/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/django/django3-saas/dashboard/models.py b/apps/django/django3-saas/dashboard/models.py new file mode 100644 index 00000000..ce9aeed4 --- /dev/null +++ b/apps/django/django3-saas/dashboard/models.py @@ -0,0 +1,38 @@ +from django.db import models +from django.conf import settings + + +class Project(models.Model): + owner = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='projects' + ) + name = models.CharField(max_length=200) + description = models.TextField(blank=True) + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ['-created_at'] + + def __str__(self): + return self.name + + +class ActivityLog(models.Model): + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='activities' + ) + action = models.CharField(max_length=100) + description = models.TextField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['-created_at'] + + def __str__(self): + return f'{self.user.username} - {self.action}' diff --git a/apps/django/django3-saas/dashboard/urls.py b/apps/django/django3-saas/dashboard/urls.py new file mode 100644 index 00000000..fd79a18f --- /dev/null +++ b/apps/django/django3-saas/dashboard/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from . import views + +app_name = 'dashboard' + +urlpatterns = [ + path('', views.index, name='index'), + path('projects/', views.projects, name='projects'), + path('projects/create/', views.create_project, name='create_project'), + path('projects//edit/', views.edit_project, name='edit_project'), + path('projects//delete/', views.delete_project, name='delete_project'), +] diff --git a/apps/django/django3-saas/dashboard/views.py b/apps/django/django3-saas/dashboard/views.py new file mode 100644 index 00000000..be99138b --- /dev/null +++ b/apps/django/django3-saas/dashboard/views.py @@ -0,0 +1,111 @@ +from django.shortcuts import render, redirect, get_object_or_404 +from django.contrib.auth.decorators import login_required +from django.contrib import messages +from django.utils import timezone +from datetime import timedelta +from .models import Project, ActivityLog +from .forms import ProjectForm + + +@login_required +def index(request): + projects = request.user.projects.all()[:5] + activities = request.user.activities.all()[:10] + subscription = request.user.get_active_subscription() + + # Calculate usage metrics for the user + now = timezone.now() + thirty_days_ago = now - timedelta(days=30) + + metrics = { + 'project_count': request.user.projects.count(), + 'active_projects': request.user.projects.filter(is_active=True).count(), + 'activities_this_month': request.user.activities.filter( + created_at__gte=thirty_days_ago + ).count(), + } + + # Add subscription info + if subscription: + days_remaining = (subscription.current_period_end - now).days + metrics['days_remaining'] = max(0, days_remaining) + metrics['plan_name'] = subscription.plan.name + + return render(request, 'dashboard/index.html', { + 'projects': projects, + 'activities': activities, + 'subscription': subscription, + 'metrics': metrics, + }) + + +@login_required +def projects(request): + projects = request.user.projects.all() + return render(request, 'dashboard/projects.html', {'projects': projects}) + + +@login_required +def create_project(request): + if request.method == 'POST': + form = ProjectForm(request.POST) + if form.is_valid(): + project = form.save(commit=False) + project.owner = request.user + project.save() + + ActivityLog.objects.create( + user=request.user, + action='project_created', + description=f'Created project: {project.name}' + ) + + messages.success(request, 'Project created.') + return redirect('dashboard:projects') + else: + form = ProjectForm() + + return render(request, 'dashboard/create_project.html', {'form': form}) + + +@login_required +def edit_project(request, pk): + project = get_object_or_404(Project, pk=pk, owner=request.user) + + if request.method == 'POST': + form = ProjectForm(request.POST, instance=project) + if form.is_valid(): + form.save() + + ActivityLog.objects.create( + user=request.user, + action='project_updated', + description=f'Updated project: {project.name}' + ) + + messages.success(request, 'Project updated.') + return redirect('dashboard:projects') + else: + form = ProjectForm(instance=project) + + return render(request, 'dashboard/edit_project.html', {'form': form, 'project': project}) + + +@login_required +def delete_project(request, pk): + project = get_object_or_404(Project, pk=pk, owner=request.user) + + if request.method == 'POST': + name = project.name + project.delete() + + ActivityLog.objects.create( + user=request.user, + action='project_deleted', + description=f'Deleted project: {name}' + ) + + messages.success(request, 'Project deleted.') + return redirect('dashboard:projects') + + return render(request, 'dashboard/delete_project.html', {'project': project}) diff --git a/apps/django/django3-saas/manage.py b/apps/django/django3-saas/manage.py new file mode 100644 index 00000000..fcc53d76 --- /dev/null +++ b/apps/django/django3-saas/manage.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +import os +import sys + +def main(): + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + +if __name__ == '__main__': + main() diff --git a/apps/django/django3-saas/marketing/__init__.py b/apps/django/django3-saas/marketing/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/django/django3-saas/marketing/urls.py b/apps/django/django3-saas/marketing/urls.py new file mode 100644 index 00000000..5425d47b --- /dev/null +++ b/apps/django/django3-saas/marketing/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from . import views + +app_name = 'marketing' + +urlpatterns = [ + path('', views.home, name='home'), + path('features/', views.features, name='features'), +] diff --git a/apps/django/django3-saas/marketing/views.py b/apps/django/django3-saas/marketing/views.py new file mode 100644 index 00000000..eebb4b29 --- /dev/null +++ b/apps/django/django3-saas/marketing/views.py @@ -0,0 +1,11 @@ +from django.shortcuts import render + + +def home(request): + """Landing page for the SaaS application.""" + return render(request, 'marketing/home.html') + + +def features(request): + """Features page showcasing product capabilities.""" + return render(request, 'marketing/features.html') diff --git a/apps/django/django3-saas/requirements.txt b/apps/django/django3-saas/requirements.txt new file mode 100644 index 00000000..e10e2f8d --- /dev/null +++ b/apps/django/django3-saas/requirements.txt @@ -0,0 +1,6 @@ +Django>=3.2,<5.0 +python-dotenv>=1.0.0 +gunicorn>=21.0.0 +whitenoise>=6.6.0 +dj-database-url>=2.0.0 +stripe>=7.0.0 diff --git a/apps/django/django3-saas/static/.gitkeep b/apps/django/django3-saas/static/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/apps/django/django3-saas/templates/accounts/login.html b/apps/django/django3-saas/templates/accounts/login.html new file mode 100644 index 00000000..9c6880b7 --- /dev/null +++ b/apps/django/django3-saas/templates/accounts/login.html @@ -0,0 +1,18 @@ +{% extends 'base.html' %} + +{% block title %}Login{% endblock %} + +{% block content %} +

Login

+ +
+ {% csrf_token %} + {{ form.as_p }} + +
+ +

+ Forgot your password? +

+

Don't have an account? Register

+{% endblock %} diff --git a/apps/django/django3-saas/templates/accounts/password_reset.html b/apps/django/django3-saas/templates/accounts/password_reset.html new file mode 100644 index 00000000..4170b3d9 --- /dev/null +++ b/apps/django/django3-saas/templates/accounts/password_reset.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} + +{% block title %}Reset Password{% endblock %} + +{% block content %} +

Reset Password

+

Enter your email address and we'll send you a link to reset your password.

+ +
+ {% csrf_token %} + {{ form.as_p }} + +
+ +

Back to Login

+{% endblock %} diff --git a/apps/django/django3-saas/templates/accounts/password_reset_complete.html b/apps/django/django3-saas/templates/accounts/password_reset_complete.html new file mode 100644 index 00000000..aa5a5857 --- /dev/null +++ b/apps/django/django3-saas/templates/accounts/password_reset_complete.html @@ -0,0 +1,9 @@ +{% extends 'base.html' %} + +{% block title %}Password Reset Complete{% endblock %} + +{% block content %} +

Password Reset Complete

+

Your password has been reset successfully.

+

Login Now

+{% endblock %} diff --git a/apps/django/django3-saas/templates/accounts/password_reset_confirm.html b/apps/django/django3-saas/templates/accounts/password_reset_confirm.html new file mode 100644 index 00000000..fb0d2a4c --- /dev/null +++ b/apps/django/django3-saas/templates/accounts/password_reset_confirm.html @@ -0,0 +1,18 @@ +{% extends 'base.html' %} + +{% block title %}Set New Password{% endblock %} + +{% block content %} +

Set New Password

+ +{% if validlink %} +
+ {% csrf_token %} + {{ form.as_p }} + +
+{% else %} +

This password reset link is invalid or has expired.

+

Request a new link

+{% endif %} +{% endblock %} diff --git a/apps/django/django3-saas/templates/accounts/password_reset_done.html b/apps/django/django3-saas/templates/accounts/password_reset_done.html new file mode 100644 index 00000000..832749f1 --- /dev/null +++ b/apps/django/django3-saas/templates/accounts/password_reset_done.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} + +{% block title %}Password Reset Sent{% endblock %} + +{% block content %} +

Check Your Email

+

We've sent you an email with instructions to reset your password.

+

If you don't receive an email, make sure you entered the address you registered with.

+ +

Back to Login

+{% endblock %} diff --git a/apps/django/django3-saas/templates/accounts/password_reset_email.html b/apps/django/django3-saas/templates/accounts/password_reset_email.html new file mode 100644 index 00000000..ced6109a --- /dev/null +++ b/apps/django/django3-saas/templates/accounts/password_reset_email.html @@ -0,0 +1,10 @@ +You're receiving this email because you requested a password reset for your account. + +Please click the link below to reset your password: + +{{ protocol }}://{{ domain }}{% url 'accounts:password_reset_confirm' uidb64=uid token=token %} + +If you didn't request this, you can ignore this email. + +Thanks, +The Django SaaS Team diff --git a/apps/django/django3-saas/templates/accounts/password_reset_subject.txt b/apps/django/django3-saas/templates/accounts/password_reset_subject.txt new file mode 100644 index 00000000..7a3a8129 --- /dev/null +++ b/apps/django/django3-saas/templates/accounts/password_reset_subject.txt @@ -0,0 +1 @@ +Password Reset - Django SaaS \ No newline at end of file diff --git a/apps/django/django3-saas/templates/accounts/register.html b/apps/django/django3-saas/templates/accounts/register.html new file mode 100644 index 00000000..e6b09da5 --- /dev/null +++ b/apps/django/django3-saas/templates/accounts/register.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block title %}Register{% endblock %} + +{% block content %} +

Register

+ +
+ {% csrf_token %} + {{ form.as_p }} + +
+ +

Already have an account? Login

+{% endblock %} diff --git a/apps/django/django3-saas/templates/accounts/settings.html b/apps/django/django3-saas/templates/accounts/settings.html new file mode 100644 index 00000000..14edba6c --- /dev/null +++ b/apps/django/django3-saas/templates/accounts/settings.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} + +{% block title %}Settings{% endblock %} + +{% block content %} +

Settings

+ +
+

Profile Information

+
+ {% csrf_token %} + {{ form.as_p }} + +
+
+ +
+

Security

+

+ Change Password +

+
+{% endblock %} diff --git a/apps/django/django3-saas/templates/base.html b/apps/django/django3-saas/templates/base.html new file mode 100644 index 00000000..6da2962b --- /dev/null +++ b/apps/django/django3-saas/templates/base.html @@ -0,0 +1,87 @@ + + + + + + {% block title %}Django SaaS{% endblock %} + + + {% block extra_css %}{% endblock %} + + +
+ +
+ +
+ {% if messages %} + {% for message in messages %} +

{{ message }}

+ {% endfor %} + {% endif %} + + {% block content %}{% endblock %} +
+ +
+

Django SaaS App

+
+ + {% block extra_js %}{% endblock %} + + diff --git a/apps/django/django3-saas/templates/billing/cancel.html b/apps/django/django3-saas/templates/billing/cancel.html new file mode 100644 index 00000000..9e11d38a --- /dev/null +++ b/apps/django/django3-saas/templates/billing/cancel.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} + +{% block title %}Cancel Subscription{% endblock %} + +{% block content %} +

Cancel Subscription

+ +
+

Are you sure?

+

+ You're about to cancel your {{ subscription.plan.name }} subscription. +

+

+ You will continue to have access until {{ subscription.current_period_end|date:"M d, Y" }}. +

+ +
+ {% csrf_token %} + + Keep Subscription +
+
+{% endblock %} diff --git a/apps/django/django3-saas/templates/billing/change_plan.html b/apps/django/django3-saas/templates/billing/change_plan.html new file mode 100644 index 00000000..5df476a5 --- /dev/null +++ b/apps/django/django3-saas/templates/billing/change_plan.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} + +{% block title %}Change Plan{% endblock %} + +{% block content %} +

Change Plan

+ +
+

+ Current Plan: {{ subscription.plan.name }} ({{ subscription.plan.get_price_display }})
+ New Plan: {{ plan.name }} ({{ plan.get_price_display }}) +

+ +
+ {% csrf_token %} +

+ Your plan will be changed immediately. +

+ + Cancel +
+
+{% endblock %} diff --git a/apps/django/django3-saas/templates/billing/manage.html b/apps/django/django3-saas/templates/billing/manage.html new file mode 100644 index 00000000..d94a5998 --- /dev/null +++ b/apps/django/django3-saas/templates/billing/manage.html @@ -0,0 +1,46 @@ +{% extends 'base.html' %} + +{% block title %}Manage Subscription{% endblock %} + +{% block content %} +

Billing

+ +{% if subscription %} +
+

Current Subscription

+

+ Plan: {{ subscription.plan.name }}
+ Status: + + {{ subscription.get_status_display }} +
+ Price: {{ subscription.plan.get_price_display }}
+ Current Period: {{ subscription.current_period_start|date:"M d, Y" }} - {{ subscription.current_period_end|date:"M d, Y" }} +

+ + +
+ +

Change Plan

+
+ {% for plan in plans %} + {% if plan != subscription.plan %} +
+

{{ plan.name }}

+

{{ plan.get_price_display }}

+ Switch +
+ {% endif %} + {% endfor %} +
+ +{% else %} +
+

No Active Subscription

+

Choose a plan to get started.

+ View Plans +
+{% endif %} +{% endblock %} diff --git a/apps/django/django3-saas/templates/billing/pricing.html b/apps/django/django3-saas/templates/billing/pricing.html new file mode 100644 index 00000000..d8045564 --- /dev/null +++ b/apps/django/django3-saas/templates/billing/pricing.html @@ -0,0 +1,53 @@ +{% extends 'base.html' %} + +{% block title %}Pricing - Django SaaS{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +

Simple, Transparent Pricing

+

Choose the plan that fits your needs.

+ +
+ {% for plan in plans %} + + {% empty %} +

No plans available.

+ {% endfor %} +
+{% endblock %} diff --git a/apps/django/django3-saas/templates/billing/subscribe.html b/apps/django/django3-saas/templates/billing/subscribe.html new file mode 100644 index 00000000..2b40bb72 --- /dev/null +++ b/apps/django/django3-saas/templates/billing/subscribe.html @@ -0,0 +1,30 @@ +{% extends 'base.html' %} + +{% block title %}Subscribe to {{ plan.name }}{% endblock %} + +{% block content %} +

Subscribe to {{ plan.name }}

+ +
+

{{ plan.name }} - {{ plan.get_price_display }}

+ {% if plan.description %} +

{{ plan.description }}

+ {% endif %} + +

Features

+
    + {% for feature in plan.features %} +
  • {{ feature }}
  • + {% endfor %} +
+ +
+ {% csrf_token %} +

+ By subscribing, you agree to our terms of service. +

+ + Cancel +
+
+{% endblock %} diff --git a/apps/django/django3-saas/templates/dashboard/create_project.html b/apps/django/django3-saas/templates/dashboard/create_project.html new file mode 100644 index 00000000..38d627f6 --- /dev/null +++ b/apps/django/django3-saas/templates/dashboard/create_project.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} + +{% block title %}Create Project{% endblock %} + +{% block content %} +

Create Project

+ +
+
+ {% csrf_token %} + {{ form.as_p }} + + Cancel +
+
+{% endblock %} diff --git a/apps/django/django3-saas/templates/dashboard/delete_project.html b/apps/django/django3-saas/templates/dashboard/delete_project.html new file mode 100644 index 00000000..06a4c12b --- /dev/null +++ b/apps/django/django3-saas/templates/dashboard/delete_project.html @@ -0,0 +1,18 @@ +{% extends 'base.html' %} + +{% block title %}Delete Project{% endblock %} + +{% block content %} +

Delete Project

+ +
+

Are you sure you want to delete {{ project.name }}?

+

This action cannot be undone.

+ +
+ {% csrf_token %} + + Cancel +
+
+{% endblock %} diff --git a/apps/django/django3-saas/templates/dashboard/edit_project.html b/apps/django/django3-saas/templates/dashboard/edit_project.html new file mode 100644 index 00000000..5ac1bd36 --- /dev/null +++ b/apps/django/django3-saas/templates/dashboard/edit_project.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} + +{% block title %}Edit Project{% endblock %} + +{% block content %} +

Edit Project

+ +
+
+ {% csrf_token %} + {{ form.as_p }} + + Cancel +
+
+{% endblock %} diff --git a/apps/django/django3-saas/templates/dashboard/index.html b/apps/django/django3-saas/templates/dashboard/index.html new file mode 100644 index 00000000..3162eef2 --- /dev/null +++ b/apps/django/django3-saas/templates/dashboard/index.html @@ -0,0 +1,114 @@ +{% extends 'base.html' %} + +{% block title %}Dashboard{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +

Dashboard

+ +
+
+

{{ metrics.project_count }}

+

Total Projects

+
+
+

{{ metrics.active_projects }}

+

Active Projects

+
+
+

{% if subscription %}{{ subscription.plan.name }}{% else %}Free{% endif %}

+

Current Plan

+
+ {% if subscription %} +
+

{{ metrics.days_remaining }}

+

Days Remaining

+
+ {% endif %} +
+

{{ metrics.activities_this_month }}

+

Actions This Month

+
+
+ +{% if not subscription %} +
+
+
+

Upgrade Your Plan

+

Get more projects and features with a paid plan.

+
+ View Plans +
+
+{% endif %} + +
+
+
+

Recent Projects

+ View All +
+ + {% if projects %} + {% for project in projects %} +
+
+

{{ project.name }}

+ + {% if project.is_active %}Active{% else %}Inactive{% endif %} + +
+ {% if project.description %} +

{{ project.description|truncatewords:20 }}

+ {% endif %} + Created {{ project.created_at|date:"M d, Y" }} +
+ {% endfor %} + {% else %} +
+

No projects yet.

+ Create your first project +
+ {% endif %} +
+ +
+

Recent Activity

+ {% if activities %} + {% for activity in activities %} +
+

{{ activity.description }}

+ {{ activity.created_at|date:"M d, Y H:i" }} +
+ {% endfor %} + {% else %} +

No recent activity.

+ {% endif %} + + {% if subscription %} +

Subscription

+
+

Plan: {{ subscription.plan.name }}

+

Status: + + {{ subscription.get_status_display }} + +

+

Renews: {{ subscription.current_period_end|date:"M d, Y" }}

+ Manage +
+ {% endif %} +
+
+{% endblock %} diff --git a/apps/django/django3-saas/templates/dashboard/projects.html b/apps/django/django3-saas/templates/dashboard/projects.html new file mode 100644 index 00000000..e9752a48 --- /dev/null +++ b/apps/django/django3-saas/templates/dashboard/projects.html @@ -0,0 +1,38 @@ +{% extends 'base.html' %} + +{% block title %}Projects{% endblock %} + +{% block content %} +
+

Projects

+ New Project +
+ +{% if projects %} + {% for project in projects %} +
+
+
+

{{ project.name }}

+ {% if project.description %} +

{{ project.description|truncatewords:30 }}

+ {% endif %} + Created {{ project.created_at|date:"M d, Y" }} +
+
+ + {% if project.is_active %}Active{% else %}Inactive{% endif %} + + Edit + Delete +
+
+
+ {% endfor %} +{% else %} +
+

No projects yet.

+ Create your first project +
+{% endif %} +{% endblock %} diff --git a/apps/django/django3-saas/templates/errors/404.html b/apps/django/django3-saas/templates/errors/404.html new file mode 100644 index 00000000..04a38f32 --- /dev/null +++ b/apps/django/django3-saas/templates/errors/404.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block title %}Page Not Found - Django SaaS{% endblock %} + +{% block content %} +
+

404

+

Page Not Found

+

The page you're looking for doesn't exist or has been moved.

+ Go Home +
+{% endblock %} diff --git a/apps/django/django3-saas/templates/errors/500.html b/apps/django/django3-saas/templates/errors/500.html new file mode 100644 index 00000000..9e5dd1db --- /dev/null +++ b/apps/django/django3-saas/templates/errors/500.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block title %}Server Error - Django SaaS{% endblock %} + +{% block content %} +
+

500

+

Server Error

+

Something went wrong on our end. Please try again later.

+ Go Home +
+{% endblock %} diff --git a/apps/django/django3-saas/templates/marketing/features.html b/apps/django/django3-saas/templates/marketing/features.html new file mode 100644 index 00000000..148aeb69 --- /dev/null +++ b/apps/django/django3-saas/templates/marketing/features.html @@ -0,0 +1,44 @@ +{% extends 'base.html' %} + +{% block title %}Features - Django SaaS{% endblock %} + +{% block content %} +

Features

+

Everything you need to manage and scale your business.

+ +
+
+

Project Management

+

Create, organize, and manage all your projects from a central dashboard.

+
+ +
+

Activity Tracking

+

Keep track of all changes with detailed activity logs and history.

+
+ +
+

Subscription Billing

+

Flexible subscription plans with easy upgrade and downgrade options.

+
+ +
+

User Management

+

Manage your account settings, profile, and security preferences.

+
+ +
+

Admin Dashboard

+

Full admin panel to manage users, plans, and subscriptions.

+
+ +
+

Secure & Reliable

+

Built with security best practices. Your data is safe with us.

+
+
+ + +{% endblock %} diff --git a/apps/django/django3-saas/templates/marketing/home.html b/apps/django/django3-saas/templates/marketing/home.html new file mode 100644 index 00000000..e5ad8aad --- /dev/null +++ b/apps/django/django3-saas/templates/marketing/home.html @@ -0,0 +1,38 @@ +{% extends 'base.html' %} + +{% block title %}Django SaaS - Home{% endblock %} + +{% block content %} +
+

Build Better Products Faster

+

+ A modern SaaS platform to manage your projects, track progress, and scale your business. +

+
+ {% if user.is_authenticated %} + Go to Dashboard + {% else %} + Start Free Trial + Login + {% endif %} +
+
+ +
+

Why Choose Us?

+
+
+

Project Management

+

Organize and track all your projects in one place with our intuitive dashboard.

+
+
+

Flexible Plans

+

Choose from multiple pricing tiers that grow with your business needs.

+
+
+

Activity Tracking

+

Monitor your team's progress with detailed activity logs and metrics.

+
+
+
+{% endblock %} From 61be440caa7f59f30358650e16c75f204d7ec601 Mon Sep 17 00:00:00 2001 From: sarahxsanders Date: Wed, 21 Jan 2026 17:33:45 -0500 Subject: [PATCH 2/3] remove posthog hah --- apps/django/django3-saas/.env.example | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/django/django3-saas/.env.example b/apps/django/django3-saas/.env.example index b8e3ce5c..85b45dc1 100644 --- a/apps/django/django3-saas/.env.example +++ b/apps/django/django3-saas/.env.example @@ -19,3 +19,7 @@ DEFAULT_FROM_EMAIL=noreply@example.com STRIPE_PUBLIC_KEY= STRIPE_SECRET_KEY= STRIPE_WEBHOOK_SECRET= + +# PostHog Analytics (optional - for analytics tracking) +POSTHOG_API_KEY= +POSTHOG_HOST=https://us.i.posthog.com From a9650061f51f0908ea19f3a23a8a8066c9ba7769 Mon Sep 17 00:00:00 2001 From: sarahxsanders Date: Thu, 22 Jan 2026 13:49:55 -0500 Subject: [PATCH 3/3] add README --- apps/django/django3-saas/.env.example | 4 - apps/django/django3-saas/README.md | 131 ++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 apps/django/django3-saas/README.md diff --git a/apps/django/django3-saas/.env.example b/apps/django/django3-saas/.env.example index 85b45dc1..b8e3ce5c 100644 --- a/apps/django/django3-saas/.env.example +++ b/apps/django/django3-saas/.env.example @@ -19,7 +19,3 @@ DEFAULT_FROM_EMAIL=noreply@example.com STRIPE_PUBLIC_KEY= STRIPE_SECRET_KEY= STRIPE_WEBHOOK_SECRET= - -# PostHog Analytics (optional - for analytics tracking) -POSTHOG_API_KEY= -POSTHOG_HOST=https://us.i.posthog.com diff --git a/apps/django/django3-saas/README.md b/apps/django/django3-saas/README.md new file mode 100644 index 00000000..6e117bfc --- /dev/null +++ b/apps/django/django3-saas/README.md @@ -0,0 +1,131 @@ +# Django SaaS example app + +A Django 3.0+ SaaS application for testing PostHog wizard integration. This app provides subscription billing, user authentication, and project management features. + +## Running the app + +### Prerequisites + +- Python 3.10+ +- SQLite (included with Python, used by default) +- Stripe account (optional, app runs in demo mode without it) + +### Installation + +1. Create and activate a virtual environment: + +```bash +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate +``` + +1. Install dependencies: + +```bash +pip install -r requirements.txt +``` + +1. Set up environment variables (create a `.env` file): + +```bash +SECRET_KEY=your-secret-key +# DATABASE_URL=postgresql://... # Optional, defaults to SQLite +STRIPE_PUBLIC_KEY=pk_test_... # Optional, enables Stripe +STRIPE_SECRET_KEY=sk_test_... # Optional, enables Stripe +STRIPE_WEBHOOK_SECRET=whsec_... # Optional, for webhooks +``` + +> **Note:** By default, the app uses SQLite (`db.sqlite3`) and runs in demo mode without Stripe. No additional setup is required for local development. + +1. Initialize the database: + +```bash +python manage.py migrate +python manage.py seed_plans # Optional: seed pricing plans +``` + +1. Run the development server: + +```bash +python manage.py runserver +``` + +The app will be available at `http://127.0.0.1:8000`. + +--- + +## Application structure + +``` +├── manage.py # Django management script +├── requirements.txt # Python dependencies +├── accounts/ # User authentication & profiles +│ ├── models.py # Custom User model +│ ├── views.py # Login, register, password reset +│ └── forms.py # Auth forms +├── billing/ # Subscription & payment handling +│ ├── models.py # Plan, Subscription models +│ ├── views.py # Stripe checkout, webhooks, billing portal +│ ├── admin.py # Django admin customization +│ └── management/ # seed_plans command +├── config/ # Django settings +│ ├── settings.py +│ ├── urls.py +│ └── wsgi.py +├── dashboard/ # Main app functionality +│ ├── models.py # Project, ActivityLog models +│ ├── views.py # Dashboard, project CRUD +│ └── forms.py +├── marketing/ # Public pages +│ └── views.py # Home, features pages +├── static/ # CSS, JS, images +└── templates/ # HTML templates +``` + +## Features + +### Authentication + +- User registration with email +- Login/logout with session management +- Password reset via email +- User profiles and settings + +### Billing & subscriptions + +- Pricing page with plan tiers +- Stripe Checkout integration +- Subscription management (upgrade/downgrade/cancel) +- Stripe webhook handling +- Demo mode when Stripe is not configured + +### Dashboard + +- Project CRUD (create, read, update, delete) +- Activity logging +- Usage metrics display +- Subscription status + +### Admin panel + +- Django admin at `/admin/` +- Plan management with subscriber counts +- Subscription management with status badges + +## Database models + +| Model | Description | +|-------|-------------| +| `User` | Custom user model with authentication and Stripe customer ID | +| `Plan` | Subscription plans with pricing and Stripe price IDs | +| `Subscription` | User subscriptions with status tracking | +| `Project` | User projects with activity logging | +| `ActivityLog` | Audit trail for user actions | + +## Key dependencies + +- **Django** - Web framework +- **Stripe** - Payment processing +- **Whitenoise** - Static file serving +- **dj-database-url** - Database configuration from URL +- **python-dotenv** - Environment variable management