clone from upstream lithium project

This commit is contained in:
wsvincent 2018-02-15 12:53:04 -05:00 committed by badbl0cks
parent 6f5167c24f
commit f946e4933a
56 changed files with 754 additions and 503 deletions

15
.gitignore vendored Normal file
View file

@ -0,0 +1,15 @@
# OSX #
.DS_Store
# Byte-compiled / optimized / DLL files #
__pycache__/
*.py[cod]
*$py.class
# Django #
*.log
db.sqlite3
media
# Virtual environment
.venv

5
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,5 @@
# Contributing
Thank you for help improving DjangoX. All kinds of contributions are welcome. Please note that this starter project is *intentionally* basic: I don't plan to add environment variables, Docker, or other production-appropriate features as I feel they will overwhelm beginners. But I'm open to suggestions!
Please submit an Issue or even better a PR and I'll review :)

27
Dockerfile Normal file
View file

@ -0,0 +1,27 @@
# Pull base image
FROM python:3.12.2-slim-bookworm
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# Create and set work directory called `app`
RUN mkdir -p /code
WORKDIR /code
# Install dependencies
COPY requirements.txt /tmp/requirements.txt
RUN set -ex && \
pip install --upgrade pip && \
pip install -r /tmp/requirements.txt && \
rm -rf /root/.cache/
# Copy local project
COPY . /code/
# Expose port 8000
EXPOSE 8000
# Use gunicorn on port 8000
CMD ["gunicorn", "--bind", ":8000", "--workers", "2", "django_project.wsgi"]

View file

@ -1,6 +1,6 @@
djangox: Copyright (c) 2018 William Vincent
djangox: Copyright (c) 2020 William Vincent
django-allauth: Copyright (c) 2010 Raymond Penners and contributors
cookie-cutter-django: Copyright (c) 2013-2018 Daniel Greenfeld
cookie-cutter-django: Copyright (c) 2013-2020 Daniel Greenfeld
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation

16
Pipfile
View file

@ -1,16 +0,0 @@
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[packages]
django = "*"
django-allauth = "*"
django-crispy-forms = "*"
[dev-packages]

128
Pipfile.lock generated
View file

@ -1,128 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "6d92f3e54c0f137198dc7ff38d1703a2c25f77a1870919b2cd22333a8fe35d51"
},
"host-environment-markers": {
"implementation_name": "cpython",
"implementation_version": "3.6.4",
"os_name": "posix",
"platform_machine": "x86_64",
"platform_python_implementation": "CPython",
"platform_release": "17.4.0",
"platform_system": "Darwin",
"platform_version": "Darwin Kernel Version 17.4.0: Sun Dec 17 09:19:54 PST 2017; root:xnu-4570.41.2~1/RELEASE_X86_64",
"python_full_version": "3.6.4",
"python_version": "3.6",
"sys_platform": "darwin"
},
"pipfile-spec": 6,
"requires": {},
"sources": [
{
"name": "pypi",
"url": "https://pypi.python.org/simple",
"verify_ssl": true
}
]
},
"default": {
"certifi": {
"hashes": [
"sha256:14131608ad2fd56836d33a71ee60fa1c82bc9d2c8d98b7bdbc631fe1b3cd1296",
"sha256:edbc3f203427eef571f79a7692bb160a2b0f7ccaa31953e99bd17e307cf63f7d"
],
"version": "==2018.1.18"
},
"chardet": {
"hashes": [
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691",
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"
],
"version": "==3.0.4"
},
"defusedxml": {
"hashes": [
"sha256:702a91ade2968a82beb0db1e0766a6a273f33d4616a6ce8cde475d8e09853b20",
"sha256:24d7f2f94f7f3cb6061acb215685e5125fbcdc40a857eff9de22518820b0a4f4"
],
"version": "==0.5.0"
},
"django": {
"hashes": [
"sha256:7c8ff92285406fb349e765e9ade685eec7271d6f5c3f918e495a74768b765c99",
"sha256:dc3b61d054f1bced64628c62025d480f655303aea9f408e5996c339a543b45f0"
],
"version": "==2.0.2"
},
"django-allauth": {
"hashes": [
"sha256:7b31526cccd1c46f9f09acf0703068e8a9669337d29eb065f7e8143c2d897339"
],
"version": "==0.35.0"
},
"django-crispy-forms": {
"hashes": [
"sha256:d37fe72eb550b41ba651c06293fb861d5a9e6e3ada23304718858cd6250d258d",
"sha256:b29a9a671194e3a482891f319f69da81e30ae81c075f6e37adb14a83ba2c409b"
],
"version": "==1.7.0"
},
"idna": {
"hashes": [
"sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4",
"sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f"
],
"version": "==2.6"
},
"oauthlib": {
"hashes": [
"sha256:ce57b501e906ff4f614e71c36a3ab9eacbb96d35c24d1970d2539bbc3ec70ce1"
],
"version": "==2.0.6"
},
"python3-openid": {
"hashes": [
"sha256:0086da6b6ef3161cfe50fb1ee5cceaf2cda1700019fda03c2c5c440ca6abe4fa",
"sha256:628d365d687e12da12d02c6691170f4451db28d6d68d050007e4a40065868502"
],
"version": "==3.1.0"
},
"pytz": {
"hashes": [
"sha256:ed6509d9af298b7995d69a440e2822288f2eca1681b8cce37673dbb10091e5fe",
"sha256:f93ddcdd6342f94cea379c73cddb5724e0d6d0a1c91c9bdef364dc0368ba4fda",
"sha256:61242a9abc626379574a166dc0e96a66cd7c3b27fc10868003fa210be4bff1c9",
"sha256:ba18e6a243b3625513d85239b3e49055a2f0318466e0b8a92b8fb8ca7ccdf55f",
"sha256:07edfc3d4d2705a20a6e99d97f0c4b61c800b8232dc1c04d87e8554f130148dd",
"sha256:3a47ff71597f821cd84a162e71593004286e5be07a340fd462f0d33a760782b5",
"sha256:5bd55c744e6feaa4d599a6cbd8228b4f8f9ba96de2c38d56f08e534b3c9edf0d",
"sha256:887ab5e5b32e4d0c86efddd3d055c1f363cbaa583beb8da5e22d2fa2f64d51ef",
"sha256:410bcd1d6409026fbaa65d9ed33bf6dd8b1e94a499e32168acfc7b332e4095c0"
],
"version": "==2018.3"
},
"requests": {
"hashes": [
"sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b",
"sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e"
],
"version": "==2.18.4"
},
"requests-oauthlib": {
"hashes": [
"sha256:50a8ae2ce8273e384895972b56193c7409601a66d4975774c60c2aed869639ca",
"sha256:883ac416757eada6d3d07054ec7092ac21c7f35cb1d2cf82faf205637081f468"
],
"version": "==0.8.0"
},
"urllib3": {
"hashes": [
"sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b",
"sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"
],
"version": "==1.22"
}
},
"develop": {}
}

137
README.md
View file

@ -1,67 +1,122 @@
# DjangoX
# Lithium: A Django-Powered Boilerplate
Lithium is a batteries-included Django starter project with everything you need to start coding, including user authentication, static files, default styling, debugging, DRY forms, custom error pages, and more.
**DjangoX** - A framework for launching new Django projects quickly.
> This project was formerly known as _DjangoX_ but was renamed to _Lithium_ in November 2024.
Comes with a custom user model, social authentication, and email/password for sign up and log in.
https://github.com/wsvincent/djangox/assets/766418/a73ea730-a7b4-4e53-bf51-aa68f6816d6a
![Falconx](static/images/falconx.png)
## 👋 Free Newsletter
[Sign up for updates](https://buttondown.com/lithiumsaas) to the free and upcoming premium SaaS version!
## Features
## 🚀 Features
* Django 2.0 and Python 3.6
* [Pipenv](https://github.com/pypa/pipenv) for virtualenvs
* User registration via [django-allauth](https://github.com/pennersr/django-allauth)
* Add social auth via Google, Facebook, etc
* [Bootstrap v4](https://getbootstrap.com/)
* Custom user model with email and no username
- Django 5.1 & Python 3.12
- Installation via [Pip](https://pypi.org/project/pip/) or [Docker](https://www.docker.com/)
- User authentication--log in, sign up, password reset--via [django-allauth](https://github.com/pennersr/django-allauth)
- Static files configured with [Whitenoise](http://whitenoise.evans.io/en/stable/index.html)
- Styling with [Bootstrap v5](https://getbootstrap.com/)
- Debugging with [django-debug-toolbar](https://github.com/jazzband/django-debug-toolbar)
- DRY forms with [django-crispy-forms](https://github.com/django-crispy-forms/django-crispy-forms)
- Custom 404, 500, and 403 error pages
## First-time setup
## Table of Contents
* **[Installation](#installation)**
* [Pip](#pip)
* [Docker](#docker)
* [Next Steps](#next-steps)
* [Contributing](#contributing)
* [Support](#support)
* [License](#license)
1. Make sure Python 3.6x and Pipenv are already installed. [See here for help](https://djangoforbeginners.com/initial-setup/).
2. Install packages with `pipenv install`
3. Activate a virtual environment with `pipenv shell`
4. Set up the initial migration for our custom user models in `users`
## 📖 Installation
Lithium can be installed via Pip or Docker. To start, clone the repo to your local computer and change into the proper directory.
$ python manage.py makemigrations users
```
$ git clone https://github.com/wsvincent/lithium.git
$ cd lithium
```
5. Build the database schema:
### Pip
You can use [pip](https://pypi.org/project/pip/) to create a fresh virtual environment on either Windows or macOS.
$ python manage.py migrate
```
# On Windows
$ python -m venv .venv
$ Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
$ .venv\Scripts\Activate.ps1
(.venv) $
6. Create a superuser:
# On macOS
$ python -m venv .venv
$ source .venv/bin/activate
(.venv) $
```
$ python manage.py createsuperuser
Then install all packages hosted in `requirements.txt` and run `migrate` to configure the initial database. The command `createsuperuser` will create a new superuser account for accessing the admin. Execute the `runserver` commandt o start up the local server.
7. Confirm everything is working:
```
(.venv) $ pip install -r requirements.txt
(.venv) $ python manage.py migrate
(.venv) $ python manage.py createsuperuser
(.venv) $ python manage.py runserver
# Load the site at http://127.0.0.1:8000 or http://127.0.0.1:8000/admin for the admin
```
$ python manage.py runserver
### Docker
Load the site at [http://127.0.0.1:8000](http://127.0.0.1:8000).
To use Docker with PostgreSQL as the database update the `DATABASES` section of `django_project/settings.py` to reflect the following:
Click on links for "Sign up" or "Log in."
```python
# django_project/settings.py
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "postgres",
"USER": "postgres",
"PASSWORD": "postgres",
"HOST": "db", # set in docker-compose.yml
"PORT": 5432, # default postgres port
}
}
```
8. This is optional but I also recommend logging into admin and changing the default site:
The `INTERNAL_IPS` configuration in `django_project/settings.py` must be also be updated:
Go to [http://127.0.0.1:8000/admin]([http://127.0.0.1:8000/admin]). You may need to logout and then login with your superuser account.
```python
# config/settings.py
# django-debug-toolbar
import socket
hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
INTERNAL_IPS = [ip[:-1] + "1" for ip in ips]
```
Navigate to [http://127.0.0.1:8000/admin/sites/site/](http://127.0.0.1:8000/admin/sites/site/) and change the default "example.com" to "127.0.0.1" and the name to "<YOUR_PROJECT_NAME>" for local development.
And then proceed to build the Docker image, run the container, and execute the standard commands within Docker.
## Recommendations
```
$ docker compose up -d --build
$ docker compose exec web python manage.py migrate
$ docker compose exec web python manage.py createsuperuser
# Load the site at http://127.0.0.1:8000 or http://127.0.0.1:8000/admin for the admin
```
* Use [PostgreSQL locally via Docker](https://wsvincent.com/django-docker-postgresql/)
* Use [django-environ](https://github.com/joke2k/django-environ) for environment variables
* Update [EMAIL_BACKEND](https://docs.djangoproject.com/en/2.0/topics/email/#module-django.core.mail) to [configure an SMTP backend](https://djangoforbeginners.com/password-change-reset/)
* Add [django-debug-toolbar](https://github.com/jazzband/django-debug-toolbar) and [django-extensions](https://github.com/django-extensions/django-extensions)
## Next Steps
* Make the [admin more secure](https://opensource.com/article/18/1/10-tips-making-django-admin-more-secure)
- Add environment variables. There are multiple packages but I personally prefer [environs](https://pypi.org/project/environs/).
- Add [gunicorn](https://pypi.org/project/gunicorn/) as the production web server.
- Update the [EMAIL_BACKEND](https://docs.djangoproject.com/en/4.0/topics/email/#module-django.core.mail) and connect with a mail provider.
- Make the [admin more secure](https://opensource.com/article/18/1/10-tips-making-django-admin-more-secure).
- `django-allauth` supports [social authentication](https://django-allauth.readthedocs.io/en/latest/providers.html) if you need that.
## Adding Social Authentication
I cover all of these steps in tutorials and premium courses over at [LearnDjango.com](https://learndjango.com).
* [Configuring Google](https://wsvincent.com/django-allauth-tutorial-custom-user-model/#google-credentials)
* [Configuring Facebook](http://www.sarahhagstrom.com/2013/09/the-missing-django-allauth-tutorial/#Create_and_configure_a_Facebook_app)
* [Configuring Github](https://wsvincent.com/django-allauth-tutorial/)
* `django-allauth` supports [many, many other providers in the official docs](https://django-allauth.readthedocs.io/en/latest/providers.html)
## 🤝 Contributing
## Acknowledgments
Contributions, issues and feature requests are welcome! See [CONTRIBUTING.md](https://github.com/wsvincent/djangox/blob/master/CONTRIBUTING.md).
This project is heavily inspired by [cookiecutter-django](https://github.com/pydanny/cookiecutter-django). It's my own preferred template for starting new projects built out of a personal desire to actually understand all the config magic in `cookiecutter-django`.
## ⭐️ Support
Give a ⭐️ if this project helped you!
## License
[The MIT License](LICENSE)

View file

@ -7,9 +7,24 @@ from .models import CustomUser
class CustomUserAdmin(UserAdmin):
model = CustomUser
add_form = CustomUserCreationForm
form = CustomUserChangeForm
model = CustomUser
list_display = [
"email",
"username",
]
# Explicitly define add_fieldsets to prevent unexpected fields
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": ("username", "email", "password1", "password2"),
},
),
)
admin.site.register(CustomUser, CustomUserAdmin)

6
accounts/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'accounts'

View file

@ -2,16 +2,14 @@ from django import forms
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser
class CustomUserCreationForm(UserCreationForm):
class Meta(UserCreationForm.Meta):
model = CustomUser
fields = ('username', 'email')
fields = ('email', 'username',)
class CustomUserChangeForm(UserChangeForm):
class Meta:
model = CustomUser
fields = UserChangeForm.Meta.fields
fields = ('email', 'username',)

View file

@ -0,0 +1,117 @@
# Generated by Django 5.0.2 on 2024-02-19 12:10
import django.contrib.auth.models
import django.contrib.auth.validators
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='CustomUser',
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'
),
),
(
'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()),
],
),
]

8
accounts/models.py Normal file
View file

@ -0,0 +1,8 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
class CustomUser(AbstractUser):
pass
def __str__(self):
return self.email

3
accounts/views.py Normal file
View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View file

7
django_project/asgi.py Normal file
View file

@ -0,0 +1,7 @@
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_project.settings")
application = get_asgi_application()

202
django_project/settings.py Normal file
View file

@ -0,0 +1,202 @@
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-0peo@#x9jur3!h$ryje!$879xww8y1y66jx!%*#ymhg&jkozs2"
# https://docs.djangoproject.com/en/dev/ref/settings/#debug
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1"]
# Application definition
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"whitenoise.runserver_nostatic",
"django.contrib.staticfiles",
"django.contrib.sites",
# Third-party
"allauth",
"allauth.account",
"crispy_forms",
"crispy_bootstrap5",
"debug_toolbar",
# Local
"accounts",
"pages",
]
# https://docs.djangoproject.com/en/dev/ref/settings/#middleware
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware", # WhiteNoise
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"debug_toolbar.middleware.DebugToolbarMiddleware", # Django Debug Toolbar
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"allauth.account.middleware.AccountMiddleware", # django-allauth
]
# https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf
ROOT_URLCONF = "django_project.urls"
# https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application
WSGI_APPLICATION = "django_project.wsgi.application"
# https://docs.djangoproject.com/en/dev/ref/settings/#templates
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",
],
},
},
]
# https://docs.djangoproject.com/en/dev/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# For Docker/PostgreSQL usage uncomment this and comment the DATABASES config above
# DATABASES = {
# "default": {
# "ENGINE": "django.db.backends.postgresql",
# "NAME": "postgres",
# "USER": "postgres",
# "PASSWORD": "postgres",
# "HOST": "db", # set in docker-compose.yml
# "PORT": 5432, # default postgres port
# }
# }
# Password validation
# https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators
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",
},
]
# Internationalization
# https://docs.djangoproject.com/en/dev/topics/i18n/
# https://docs.djangoproject.com/en/dev/ref/settings/#language-code
LANGUAGE_CODE = "en-us"
# https://docs.djangoproject.com/en/dev/ref/settings/#time-zone
TIME_ZONE = "UTC"
# https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-USE_I18N
USE_I18N = True
# https://docs.djangoproject.com/en/dev/ref/settings/#use-tz
USE_TZ = True
# https://docs.djangoproject.com/en/dev/ref/settings/#locale-paths
LOCALE_PATHS = [BASE_DIR / 'locale']
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
# https://docs.djangoproject.com/en/dev/ref/settings/#static-root
STATIC_ROOT = BASE_DIR / "staticfiles"
# https://docs.djangoproject.com/en/dev/ref/settings/#static-url
STATIC_URL = "/static/"
# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
STATICFILES_DIRS = [BASE_DIR / "static"]
# https://whitenoise.readthedocs.io/en/latest/django.html
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
# Default primary key field type
# https://docs.djangoproject.com/en/stable/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# django-crispy-forms
# https://django-crispy-forms.readthedocs.io/en/latest/install.html#template-packs
CRISPY_ALLOWED_TEMPLATE_PACKS = 'bootstrap5'
CRISPY_TEMPLATE_PACK = "bootstrap5"
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email
DEFAULT_FROM_EMAIL = "root@localhost"
# django-debug-toolbar
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html
# https://docs.djangoproject.com/en/dev/ref/settings/#internal-ips
INTERNAL_IPS = ["127.0.0.1"]
# https://docs.djangoproject.com/en/dev/topics/auth/customizing/#substituting-a-custom-user-model
AUTH_USER_MODEL = "accounts.CustomUser"
# django-allauth config
# https://docs.djangoproject.com/en/dev/ref/settings/#site-id
SITE_ID = 1
# https://docs.djangoproject.com/en/dev/ref/settings/#login-redirect-url
LOGIN_REDIRECT_URL = "home"
# https://django-allauth.readthedocs.io/en/latest/views.html#logout-account-logout
ACCOUNT_LOGOUT_REDIRECT_URL = "home"
# https://django-allauth.readthedocs.io/en/latest/installation.html?highlight=backends
AUTHENTICATION_BACKENDS = (
"django.contrib.auth.backends.ModelBackend",
"allauth.account.auth_backends.AuthenticationBackend",
)
# https://django-allauth.readthedocs.io/en/latest/configuration.html
ACCOUNT_SESSION_REMEMBER = True
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = False
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_AUTHENTICATION_METHOD = "email"
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_UNIQUE_EMAIL = True

16
django_project/urls.py Normal file
View file

@ -0,0 +1,16 @@
from django.conf import settings
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("accounts/", include("allauth.urls")),
path("", include("pages.urls")),
]
if settings.DEBUG:
import debug_toolbar
urlpatterns = [
path("__debug__/", include(debug_toolbar.urls)),
] + urlpatterns

View file

@ -2,6 +2,6 @@ import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangox.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_project.settings")
application = get_wsgi_application()

View file

@ -1,145 +0,0 @@
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '4(j*7q=-dm@4&d8hb)-ivy#b(&_3ew19ujzo#h_hq-39!6-5d+'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'allauth',
'allauth.account',
'allauth.socialaccount',
# 'allauth.socialaccount.providers.google',
'crispy_forms',
'users',
'pages',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'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 = 'djangox.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(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 = 'djangox.wsgi.application'
# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
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',
},
]
# Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.0/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
# Bootstrap Crispy-Forms config
CRISPY_TEMPLATE_PACK = 'bootstrap4'
# Authentication settings
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
AUTHENTICATION_BACKENDS = (
"django.contrib.auth.backends.ModelBackend",
"allauth.account.auth_backends.AuthenticationBackend",
)
AUTH_USER_MODEL = 'users.CustomUser'
SITE_ID = 1
LOGIN_REDIRECT_URL = 'home'
ACCOUNT_LOGOUT_REDIRECT_URL = 'home'
ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_MIN_LENGTH = 3
ACCOUNT_EMAIL_VERIFICATION = 'optional' # set this to 'mandatory'?
ACCOUNT_UNIQUE_EMAIL = True
# ACCOUNT_USER_MODEL_EMAIL_FIELD = 'email'
ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = False
ACCOUNT_SESSION_REMEMBER = True

View file

@ -1,13 +0,0 @@
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('', include('pages.urls')),
# Django Admin
path('admin/', admin.site.urls),
# User management
path('users/', include('users.urls')),
path('accounts/', include('allauth.urls')), # new
]

19
docker-compose.yml Normal file
View file

@ -0,0 +1,19 @@
services:
web:
build: .
command: python /code/manage.py runserver 0.0.0.0:8000
volumes:
- .:/code
ports:
- 8000:8000
depends_on:
- db
db:
image: postgres:16
volumes:
- postgres_data:/var/lib/postgresql/data/
environment:
- "POSTGRES_HOST_AUTH_METHOD=trust"
volumes:
postgres_data:

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View file

@ -1,9 +1,12 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangox.settings")
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_project.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
@ -13,3 +16,7 @@ if __name__ == "__main__":
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()

View file

@ -1,6 +0,0 @@
- tut on custom/auth...
- add about page...
- add base.html with nav, update other templates
- add 404, 403, 500 pages
- customize password reset...

View file

@ -2,4 +2,4 @@ from django.apps import AppConfig
class PagesConfig(AppConfig):
name = 'pages'
name = "pages"

View file

@ -1,8 +1,8 @@
from django.urls import path
from . import views
from .views import HomePageView, AboutPageView
urlpatterns = [
path('', views.HomePageView.as_view(), name='home'),
path('about/', views.AboutPageView.as_view(), name='about'),
path("", HomePageView.as_view(), name="home"),
path("about/", AboutPageView.as_view(), name="about"),
]

View file

@ -2,8 +2,8 @@ from django.views.generic import TemplateView
class HomePageView(TemplateView):
template_name = 'pages/home.html'
template_name = "pages/home.html"
class AboutPageView(TemplateView):
template_name = 'pages/about.html'
template_name = "pages/about.html"

26
requirements.txt Normal file
View file

@ -0,0 +1,26 @@
asgiref==3.8.1
certifi==2022.12.7
cffi==1.15.1
charset-normalizer==3.0.1
crispy-bootstrap5==2024.10
cryptography==39.0.1
defusedxml==0.7.1
Django==5.1.2
django-allauth==65.0.2
django-crispy-forms==2.3
django-debug-toolbar==4.4.6
gunicorn==23.0.0
idna==3.4
oauthlib==3.2.2
packaging==23.1
psycopg==3.2.3
psycopg-binary==3.2.3
pycparser==2.21
PyJWT==2.6.0
python3-openid==3.2.0
requests==2.28.2
requests-oauthlib==1.3.1
sqlparse==0.4.3
typing_extensions==4.9.0
urllib3==1.26.14
whitenoise==6.7.0

33
static/css/base.css Normal file
View file

@ -0,0 +1,33 @@
/* Sticky footer styles
-------------------------------------------------- */
html {
position: relative;
min-height: 100%;
font-size: 14px;
}
@media (min-width: 768px) {
html {
font-size: 16px;
}
}
body {
margin-bottom: 60px; /* Margin bottom by footer height */
}
.container {
max-width: 960px;
}
.pricing-header {
max-width: 700px;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
height: 60px; /* Set the fixed height of the footer here */
line-height: 60px; /* Vertically center the text there */
background-color: #f5f5f5;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

BIN
static/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

0
static/js/base.js Normal file
View file

View file

@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends '_base.html' %}
{% block title %}Forbidden (403){% endblock title %}

View file

@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends '_base.html' %}
{% block title %}404 Page not found{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends '_base.html' %}
{% block title %}500 Server Error{% endblock %}

95
templates/_base.html Normal file
View file

@ -0,0 +1,95 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<title>{% block title %}DjangoX{% endblock title %}</title>
<meta name="description" content="A framework for launching new Django projects quickly.">
<meta name="author" content="">
<link rel="shortcut icon" type="image/x-icon" href="{% static 'images/favicon.ico' %}">
{% block css %}
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="{% static 'css/base.css' %}">
{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="{% url 'home' %}">DjangoX</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="{% url 'home' %}">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'about' %}">About</a>
</li>
</ul>
{% if user.is_authenticated %}
<div class="mr-auto">
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
aria-expanded="false">
Settings
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="#">{{ user.email }}</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li><a class="dropdown-item" href="{% url 'account_change_password' %}">Change password</a></li>
<li><a class="dropdown-item" href="{% url 'account_logout' %}">Sign out</a></li>
</ul>
</li>
</ul>
</div>
{% else %}
<div class="mr-auto">
<form class="form d-flex">
<a href="{% url 'account_login' %}" class="btn btn-outline-secondary">Log in</a>
<a href="{% url 'account_signup' %}" class="btn btn-primary ms-2">Sign up</a>
</form>
</div>
{% endif %}
</div>
</div>
</nav>
<div class="container">
{% block content %}
<p>Default content...</p>
{% endblock content %}
</div>
<footer class="footer">
<div class="container">
<span class="text-muted">Footer...</span>
</div>
</footer>
{% block javascript %}
<!-- Bootstrap JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
<!-- Project JS -->
<script src="{% static 'js/base.js' %}"></script>
{% endblock javascript %}
</body>
</html>

View file

@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends '_base.html' %}
{% load crispy_forms_tags %}
{% block title %}Log in{% endblock %}
@ -10,5 +10,4 @@
{{ form|crispy }}
<button class="btn btn-success" type="submit">Log in</button>
</form>
<a href="{% url 'account_reset_password' %}">Forgot Password?</a>
{% endblock content %}

View file

@ -0,0 +1,18 @@
{% extends '_base.html' %}
{% load crispy_forms_tags %}
{% block title %}Log out{% endblock %}
{% block content %}
<h1>Sign Out</h1>
<p>Are you sure you want to sign out?</p>
<form method="post" action="{% url 'account_logout' %}">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-danger" type="submit">Sign Out</button>
</form>
{% endblock content %}

View file

@ -1,11 +1,11 @@
{% extends 'base.html' %}
{% extends '_base.html' %}
{% load crispy_forms_tags %}
{% block title %}Change Password{% endblock %}
{% block content %}
<h2>Change Password</h2>
<form method="post" action="{% url 'account_change_password' %}" class="password_change">
<form method="post" action="{% url 'account_change_password' %}">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-success" type="submit">Change Password</button>

View file

@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends '_base.html' %}
{% load crispy_forms_tags %}
{% block title %}Password Reset{% endblock %}
@ -7,9 +7,7 @@
<h2>Forgot your password? </h2>
<form method="POST" action="{% url 'account_reset_password' %}" class="password_reset">
{% csrf_token %}
{{ form|crispy }}
{{ form | crispy }}
<button class="btn btn-primary" type="submit">Reset Password</button>
</form>
{% endblock content %}

View file

@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends '_base.html' %}
{% load crispy_forms_tags %}
{% block title %}Password Reset Done{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends '_base.html' %}
{% load crispy_forms_tags %}
{% block title %}Change Password{% endblock title %}

View file

@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends '_base.html' %}
{% load crispy_forms_tags %}
{% block title %}Change Password Done{% endblock title %}

View file

@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends '_base.html' %}
{% load crispy_forms_tags %}
{% block title %}Set Password{% endblock title %}
@ -6,9 +6,10 @@
{% block content %}
<form method="POST" action="" class="password_set">
{% csrf_token %}
{% bootstrap_form password_set_form %}
{{ form | crispy }}
<div class="form-actions">
<button class="btn btn-primary" type="submit" name="action" value="{% trans "Set Password" %}"/>Change Password</button>
<button class="btn btn-primary" type="submit" name="action" value="Set Password">Change
Password</button>
</div>
</form>
{% endblock content %}

View file

@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends '_base.html' %}
{% load crispy_forms_tags %}
{% block title %}Sign up{% endblock %}

View file

@ -1,76 +0,0 @@
{% load static %}
{% load socialaccount %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<title>{% block title %}DjangoX{% endblock title %}</title>
<meta name="description" content="A framework for launching new Django projects quickly.">
<meta name="author" content="">
<link rel="shortcut icon" type="image/x-icon" href="{% static 'images/favicon.ico' %}">
{% block css %}
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<link rel="stylesheet" href="{% static 'css/base.css' %}">
{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4">
<a class="navbar-brand" href="{% url 'home' %}">DjangoX</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
{% if user.is_authenticated %}
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link dropdown-toggle" href="#" id="userMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ user.username }}
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="userMenu">
<a class="dropdown-item" href="{% url 'account_change_password' %}">Change password</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="{% url 'account_logout' %}">Log out</a>
</div>
</li>
</ul>
{% else %}
<form class="form-inline ml-auto">
<a href="{% url 'account_login' %}" class="btn btn-outline-secondary">Log in</a>
<a href="{% url 'account_signup' %}" class="btn btn-primary ml-2">Sign up</a>
</form>
{% endif %}
</div>
</nav>
<div class="container">
{% block content %}
<p>Default content...</p>
{% endblock content %}
</div>
{% block javascript %}
<!-- Bootstrap JavaScript -->
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
<!-- 3rd party JavaScript -->
<!-- Project JS -->
{% endblock javascript %}
</body>
</html>

View file

@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends '_base.html' %}
{% block title %}About page{% endblock %}

View file

@ -1,10 +1,11 @@
{% extends 'base.html' %}
{% extends '_base.html' %}
{% load static %}
{% block title %}Home page{% endblock title %}
{% block content %}
<h1>Home page</h1>
<img src="{% static 'images/falconx.png' %}" class="img-fluid" alt="FalconX launch"/>
<div class="pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
<img src="{% static 'images/logo.png' %}" class="img-fluid" alt="DjangoX logo"/>
<p class="lead">A Django starter project with batteries.</p>
</div>
{% endblock content %}

View file

@ -1,6 +0,0 @@
<h2>Login</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Login</button>
</form>

View file

@ -1,5 +0,0 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
name = 'users'

View file

@ -1,9 +0,0 @@
from django.contrib.auth.models import AbstractUser, UserManager
class CustomUserManager(UserManager):
pass
class CustomUser(AbstractUser):
objects = CustomUserManager()

View file

@ -1,6 +0,0 @@
from django.urls import path
from . import views
urlpatterns = [
path('signup/', views.SignUp.as_view(), name='signup'),
]

View file

@ -1,10 +0,0 @@
from django.urls import reverse_lazy
from django.views import generic
from .forms import CustomUserCreationForm
class SignUp(generic.CreateView):
form_class = CustomUserCreationForm
success_url = reverse_lazy('login')
template_name = 'account/signup.html'