Self-hostable website analytics on Django: a straightforward collector API, dashboards, a world map, and PDF reports.
analyticsdjangodockerhandcodedpythonself-hostedsqliteviteweb-analytics
1import os
2from pathlib import Path
3
4from django.contrib.messages import constants as messages
5
6
7# Build paths inside the project like this: BASE_DIR / 'subdir'.
8BASE_DIR = Path(__file__).resolve().parent.parent.parent
9
10
11# Application definition
12
13INSTALLED_APPS = [
14 'django.contrib.admin',
15 'django.contrib.auth',
16 'django.contrib.contenttypes',
17 'django.contrib.sessions',
18 'django.contrib.messages',
19 'django.contrib.staticfiles',
20
21 'accounts',
22 'properties',
23 'collector',
24 'pages',
25]
26
27MIDDLEWARE = [
28 'django.middleware.security.SecurityMiddleware',
29
30 'whitenoise.middleware.WhiteNoiseMiddleware',
31
32 'django.contrib.sessions.middleware.SessionMiddleware',
33 'django.middleware.common.CommonMiddleware',
34 'django.middleware.csrf.CsrfViewMiddleware',
35 'django.contrib.auth.middleware.AuthenticationMiddleware',
36 'django.contrib.messages.middleware.MessageMiddleware',
37 'django.middleware.clickjacking.XFrameOptionsMiddleware',
38]
39
40ROOT_URLCONF = 'analytics.urls'
41
42TEMPLATES = [
43 {
44 'BACKEND': 'django.template.backends.django.DjangoTemplates',
45 'DIRS': [BASE_DIR / 'analytics/templates'],
46 'APP_DIRS': True,
47 'OPTIONS': {
48 'context_processors': [
49 'django.template.context_processors.debug',
50 'django.template.context_processors.request',
51 'django.contrib.auth.context_processors.auth',
52 'django.contrib.messages.context_processors.messages',
53
54 'analytics.context_processors.collector',
55 'analytics.context_processors.canonical',
56 'analytics.context_processors.base_url',
57 ],
58 },
59 },
60]
61
62WSGI_APPLICATION = 'analytics.wsgi.application'
63
64
65# Messages
66# https://docs.djangoproject.com/en/3.0/ref/settings/#messages
67
68MESSAGE_TAGS = {
69 messages.DEBUG: "alert-info",
70 messages.INFO: "alert-info",
71 messages.SUCCESS: "alert-success",
72 messages.WARNING: "alert-warning",
73 messages.ERROR: "alert-danger",
74}
75
76
77# Internationalization
78# https://docs.djangoproject.com/en/4.0/topics/i18n/
79
80LANGUAGE_CODE = 'en-us'
81TIME_ZONE = 'UTC'
82USE_I18N = True
83USE_TZ = True
84USE_THOUSAND_SEPARATOR = True
85
86
87# Static files (CSS, JavaScript, Images)
88# https://docs.djangoproject.com/en/4.0/howto/static-files/
89
90STATIC_URL = 'static/'
91STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
92STATICFILES_DIRS = (
93 BASE_DIR / "analytics/static",
94 BASE_DIR / "analytics/static_maps",
95)
96STATIC_ROOT = BASE_DIR / "static"
97
98
99# Media files (Images, Videos)
100# https://docs.djangoproject.com/en/4.0/ref/settings/#media-root
101
102MEDIA_URL = 'media/'
103MEDIA_ROOT = BASE_DIR / "media"
104
105
106# Database
107# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
108
109DATABASES = {
110 'default': {
111 'ENGINE': 'django.db.backends.sqlite3',
112 'NAME': BASE_DIR / 'db.sqlite3',
113 'OPTIONS': {
114 'timeout': 30,
115 'transaction_mode': 'IMMEDIATE',
116 # mmap_size is intentionally omitted: gunicorn workers each open
117 # their own connection, and SQLite's mmap is documented as unsafe
118 # for multi-process writers — it amplified an unrelated WAL
119 # inconsistency into full database corruption in the status repo
120 # on 2026-04-19.
121 'init_command': (
122 'PRAGMA journal_mode=WAL;'
123 'PRAGMA synchronous=NORMAL;'
124 'PRAGMA foreign_keys=ON;'
125 'PRAGMA temp_store=MEMORY;'
126 'PRAGMA journal_size_limit=67108864;'
127 'PRAGMA cache_size=-20000;'
128 ),
129 },
130 }
131}
132
133
134# Default primary key field type
135# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
136
137DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
138
139
140# Auth
141# https://docs.djangoproject.com/en/3.0/ref/settings/#auth
142
143AUTH_USER_MODEL = "accounts.User"
144LOGIN_REDIRECT_URL = "properties"
145
146
147# Email
148# https://docs.djangoproject.com/en/4.0/topics/email/#console-backend
149
150EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'