Skip to content

Commit 8dbe640

Browse files
Merge pull request #254 from PostHog/django-app
chore: add django app
2 parents 9fb5fc2 + a965006 commit 8dbe640

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+2168
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Django settings
2+
SECRET_KEY=your-secret-key-here
3+
DEBUG=True
4+
ALLOWED_HOSTS=localhost,127.0.0.1
5+
6+
# Database (defaults to SQLite if not set)
7+
DATABASE_URL=
8+
9+
# Email settings (defaults to console backend for development)
10+
EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend
11+
EMAIL_HOST=
12+
EMAIL_PORT=587
13+
EMAIL_HOST_USER=
14+
EMAIL_HOST_PASSWORD=
15+
EMAIL_USE_TLS=True
16+
DEFAULT_FROM_EMAIL=noreply@example.com
17+
18+
# Stripe settings (optional - for payment processing)
19+
STRIPE_PUBLIC_KEY=
20+
STRIPE_SECRET_KEY=
21+
STRIPE_WEBHOOK_SECRET=
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
*.so
6+
.Python
7+
.venv/
8+
venv/
9+
ENV/
10+
11+
# Django
12+
*.log
13+
local_settings.py
14+
db.sqlite3
15+
db.sqlite3-journal
16+
*.pot
17+
*.pyc
18+
19+
# Static files
20+
staticfiles/
21+
22+
# Environment
23+
.env
24+
25+
# IDE
26+
.idea/
27+
.vscode/
28+
*.swp
29+
*.swo

apps/django/django3-saas/README.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Django SaaS example app
2+
3+
A Django 3.0+ SaaS application for testing PostHog wizard integration. This app provides subscription billing, user authentication, and project management features.
4+
5+
## Running the app
6+
7+
### Prerequisites
8+
9+
- Python 3.10+
10+
- SQLite (included with Python, used by default)
11+
- Stripe account (optional, app runs in demo mode without it)
12+
13+
### Installation
14+
15+
1. Create and activate a virtual environment:
16+
17+
```bash
18+
python -m venv venv
19+
source venv/bin/activate # On Windows: venv\Scripts\activate
20+
```
21+
22+
1. Install dependencies:
23+
24+
```bash
25+
pip install -r requirements.txt
26+
```
27+
28+
1. Set up environment variables (create a `.env` file):
29+
30+
```bash
31+
SECRET_KEY=your-secret-key
32+
# DATABASE_URL=postgresql://... # Optional, defaults to SQLite
33+
STRIPE_PUBLIC_KEY=pk_test_... # Optional, enables Stripe
34+
STRIPE_SECRET_KEY=sk_test_... # Optional, enables Stripe
35+
STRIPE_WEBHOOK_SECRET=whsec_... # Optional, for webhooks
36+
```
37+
38+
> **Note:** By default, the app uses SQLite (`db.sqlite3`) and runs in demo mode without Stripe. No additional setup is required for local development.
39+
40+
1. Initialize the database:
41+
42+
```bash
43+
python manage.py migrate
44+
python manage.py seed_plans # Optional: seed pricing plans
45+
```
46+
47+
1. Run the development server:
48+
49+
```bash
50+
python manage.py runserver
51+
```
52+
53+
The app will be available at `http://127.0.0.1:8000`.
54+
55+
---
56+
57+
## Application structure
58+
59+
```
60+
├── manage.py # Django management script
61+
├── requirements.txt # Python dependencies
62+
├── accounts/ # User authentication & profiles
63+
│ ├── models.py # Custom User model
64+
│ ├── views.py # Login, register, password reset
65+
│ └── forms.py # Auth forms
66+
├── billing/ # Subscription & payment handling
67+
│ ├── models.py # Plan, Subscription models
68+
│ ├── views.py # Stripe checkout, webhooks, billing portal
69+
│ ├── admin.py # Django admin customization
70+
│ └── management/ # seed_plans command
71+
├── config/ # Django settings
72+
│ ├── settings.py
73+
│ ├── urls.py
74+
│ └── wsgi.py
75+
├── dashboard/ # Main app functionality
76+
│ ├── models.py # Project, ActivityLog models
77+
│ ├── views.py # Dashboard, project CRUD
78+
│ └── forms.py
79+
├── marketing/ # Public pages
80+
│ └── views.py # Home, features pages
81+
├── static/ # CSS, JS, images
82+
└── templates/ # HTML templates
83+
```
84+
85+
## Features
86+
87+
### Authentication
88+
89+
- User registration with email
90+
- Login/logout with session management
91+
- Password reset via email
92+
- User profiles and settings
93+
94+
### Billing & subscriptions
95+
96+
- Pricing page with plan tiers
97+
- Stripe Checkout integration
98+
- Subscription management (upgrade/downgrade/cancel)
99+
- Stripe webhook handling
100+
- Demo mode when Stripe is not configured
101+
102+
### Dashboard
103+
104+
- Project CRUD (create, read, update, delete)
105+
- Activity logging
106+
- Usage metrics display
107+
- Subscription status
108+
109+
### Admin panel
110+
111+
- Django admin at `/admin/`
112+
- Plan management with subscriber counts
113+
- Subscription management with status badges
114+
115+
## Database models
116+
117+
| Model | Description |
118+
|-------|-------------|
119+
| `User` | Custom user model with authentication and Stripe customer ID |
120+
| `Plan` | Subscription plans with pricing and Stripe price IDs |
121+
| `Subscription` | User subscriptions with status tracking |
122+
| `Project` | User projects with activity logging |
123+
| `ActivityLog` | Audit trail for user actions |
124+
125+
## Key dependencies
126+
127+
- **Django** - Web framework
128+
- **Stripe** - Payment processing
129+
- **Whitenoise** - Static file serving
130+
- **dj-database-url** - Database configuration from URL
131+
- **python-dotenv** - Environment variable management

apps/django/django3-saas/accounts/__init__.py

Whitespace-only changes.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from django.contrib import admin
2+
from django.contrib.auth.admin import UserAdmin
3+
from .models import User
4+
5+
6+
@admin.register(User)
7+
class CustomUserAdmin(UserAdmin):
8+
list_display = ['username', 'email', 'is_staff', 'date_joined']
9+
fieldsets = UserAdmin.fieldsets + (
10+
('Profile', {'fields': ('bio',)}),
11+
)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from django import forms
2+
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
3+
from .models import User
4+
5+
6+
class RegisterForm(UserCreationForm):
7+
email = forms.EmailField(required=True)
8+
company_name = forms.CharField(max_length=200, required=False)
9+
10+
class Meta:
11+
model = User
12+
fields = ['username', 'email', 'company_name', 'password1', 'password2']
13+
14+
15+
class LoginForm(AuthenticationForm):
16+
username = forms.CharField(widget=forms.TextInput(attrs={'autofocus': True}))
17+
18+
19+
class ProfileForm(forms.ModelForm):
20+
class Meta:
21+
model = User
22+
fields = ['first_name', 'last_name', 'email', 'company_name']
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from django.db import migrations, models
2+
import django.contrib.auth.models
3+
import django.contrib.auth.validators
4+
import django.utils.timezone
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
initial = True
10+
11+
dependencies = [
12+
('auth', '0012_alter_user_first_name_max_length'),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name='User',
18+
fields=[
19+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20+
('password', models.CharField(max_length=128, verbose_name='password')),
21+
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
22+
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
23+
('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')),
24+
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
25+
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
26+
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
27+
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
28+
('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')),
29+
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
30+
('company_name', models.CharField(blank=True, max_length=200)),
31+
('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')),
32+
('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')),
33+
],
34+
options={
35+
'verbose_name': 'user',
36+
'verbose_name_plural': 'users',
37+
'abstract': False,
38+
},
39+
managers=[
40+
('objects', django.contrib.auth.models.UserManager()),
41+
],
42+
),
43+
]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 4.2.27 on 2026-01-21 22:05
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('accounts', '0001_initial'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='user',
15+
name='email_verified_at',
16+
field=models.DateTimeField(blank=True, null=True),
17+
),
18+
migrations.AddField(
19+
model_name='user',
20+
name='stripe_customer_id',
21+
field=models.CharField(blank=True, max_length=100),
22+
),
23+
]

apps/django/django3-saas/accounts/migrations/__init__.py

Whitespace-only changes.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from hashlib import md5
2+
from django.contrib.auth.models import AbstractUser
3+
from django.db import models
4+
5+
6+
class User(AbstractUser):
7+
company_name = models.CharField(max_length=200, blank=True)
8+
email_verified_at = models.DateTimeField(null=True, blank=True)
9+
10+
# Stripe customer ID for billing
11+
stripe_customer_id = models.CharField(max_length=100, blank=True)
12+
13+
def __str__(self):
14+
return self.username
15+
16+
def avatar_url(self, size=128):
17+
digest = md5(self.email.lower().encode('utf-8')).hexdigest()
18+
return f'https://www.gravatar.com/avatar/{digest}?d=identicon&s={size}'
19+
20+
def get_active_subscription(self):
21+
return self.subscriptions.filter(status='active').first()
22+
23+
def is_subscribed(self):
24+
return self.subscriptions.filter(status='active').exists()
25+
26+
def get_plan(self):
27+
sub = self.get_active_subscription()
28+
return sub.plan if sub else None
29+
30+
def is_email_verified(self):
31+
return self.email_verified_at is not None

0 commit comments

Comments
 (0)