Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions apps/django/django3-saas/.env.example
Original file line number Diff line number Diff line change
@@ -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=
29 changes: 29 additions & 0 deletions apps/django/django3-saas/.gitignore
Original file line number Diff line number Diff line change
@@ -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
131 changes: 131 additions & 0 deletions apps/django/django3-saas/README.md
Original file line number Diff line number Diff line change
@@ -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
Empty file.
11 changes: 11 additions & 0 deletions apps/django/django3-saas/accounts/admin.py
Original file line number Diff line number Diff line change
@@ -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',)}),
)
22 changes: 22 additions & 0 deletions apps/django/django3-saas/accounts/forms.py
Original file line number Diff line number Diff line change
@@ -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']
43 changes: 43 additions & 0 deletions apps/django/django3-saas/accounts/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -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()),
],
),
]
Original file line number Diff line number Diff line change
@@ -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),
),
]
Empty file.
31 changes: 31 additions & 0 deletions apps/django/django3-saas/accounts/models.py
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions apps/django/django3-saas/accounts/urls.py
Original file line number Diff line number Diff line change
@@ -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/<uidb64>/<token>/', views.CustomPasswordResetConfirmView.as_view(), name='password_reset_confirm'),
path('password-reset/complete/', views.CustomPasswordResetCompleteView.as_view(), name='password_reset_complete'),
]
Loading