I've used this repo for a talk to show how to use Django.
from django.contrib import admin

from . import models


@admin.register(models.Contact)
class ContactAdmin(admin.ModelAdmin):
    list_display = ["name", "email", "phone_number", "message", "created_at", "updated_at", "resolved"]
    list_filter = ["resolved"]
    search_fields = ["name", "email", "phone_number", "message", "created_at", "updated_at"]
    readonly_fields = ["created_at", "updated_at"]
    actions = ["mark_as_resolved", "mark_as_unresolved"]

    @admin.display(description="Mark selected as resolved")
    def mark_as_resolved(self, request, queryset):
        queryset.update(resolved=True)

    @admin.display(description="Mark selected as unresolved")
    def mark_as_unresolved(self, request, queryset):
        queryset.update(resolved=False)


@admin.register(models.CSAT)
class CSATAdmin(admin.ModelAdmin):
    list_display = ["rating", "comment", "created_at"]
    list_filter = ["rating"]
    search_fields = ["rating", "comment", "created_at"]
    readonly_fields = ["created_at"]

contact/admin.py
danialkeimasi/quera-bootcamp-django
from django import forms
from django.core.validators import MinLengthValidator

from .models import CSAT, Contact


class ContactForm(forms.ModelForm):
    class Meta:
        model = Contact
        fields = ["name", "email", "phone_number", "message"]
        widgets = {
            "message": forms.Textarea(attrs={"rows": 30, "cols": 80, "placeholder": "Your message here."}),
        }
        labels = {
            "name": "Your Beautiful Name",
        }
        help_texts = {
            "name": ("Before you ask, yes, I think you are beautiful."),
        }
        error_messages = {
            "name": {
                "min_length": "Your name is too short. It must be at least 3 characters long.",
            },
        }


class CSATForm(forms.Form):
    rating = forms.IntegerField(label="How would you rate your experience?", min_value=1, max_value=5)
    comment = forms.CharField(
        label="Do you have any comments?",
        widget=forms.Textarea(attrs={"rows": 30, "cols": 80, "placeholder": "Your message here."}),
        required=False,
        validators=[
            MinLengthValidator(10, message="Comment must be at least 10 characters long."),
        ],
    )

    def clean_rating(self):
        rating = self.cleaned_data["rating"]
        if rating == 1:
            raise forms.ValidationError("Nah man, you can't rate 1 star.")
        return rating

    def clean(self):
        cleaned_data = super().clean()
        rating = cleaned_data.get("rating")
        comment = cleaned_data.get("comment")
        if rating and comment:
            if rating < 3 and len(comment) < 20:
                raise forms.ValidationError("Please provide more details when rating less than 3 stars.")
        return cleaned_data

    def save(self):
        rating = self.cleaned_data["rating"]
        comment = self.cleaned_data["comment"]
        CSAT.objects.create(rating=rating, comment=comment)

contact/forms.py
danialkeimasi/quera-bootcamp-django
# Generated by Django 4.2.5 on 2023-09-07 21:48

import django.core.validators
from django.db import migrations, models

import contact.models


class Migration(migrations.Migration):
    initial = True

    dependencies = []

    operations = [
        migrations.CreateModel(
            name="Contact",
            fields=[
                ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
                (
                    "name",
                    models.CharField(
                        max_length=100,
                        validators=[
                            django.core.validators.RegexValidator("^[a-zA-Z]*$", "Only characters are allowed."),
                            django.core.validators.MinLengthValidator(3),
                            contact.models.validate_name,
                        ],
                    ),
                ),
                ("email", models.EmailField(blank=True, max_length=254)),
                (
                    "phone_number",
                    models.CharField(
                        blank=True,
                        max_length=11,
                        validators=[
                            django.core.validators.RegexValidator(
                                "^09[0-9]+$", "Phone number should be 11 digits and start with 09."
                            )
                        ],
                    ),
                ),
                ("message", models.TextField(validators=[django.core.validators.MinLengthValidator(10)])),
                ("created_at", models.DateTimeField(auto_now_add=True)),
                ("updated_at", models.DateTimeField(auto_now=True)),
                ("resolved", models.BooleanField(default=False)),
            ],
        ),
        migrations.AddConstraint(
            model_name="contact",
            constraint=models.CheckConstraint(
                check=models.Q(
                    models.Q(("email", ""), _negated=True),
                    models.Q(("phone_number", ""), _negated=True),
                    _connector="OR",
                ),
                name="email_or_phone_number",
                violation_error_message="Either email or phone number should be provided.",
            ),
        ),
    ]

contact/migrations/0001_initial.py
danialkeimasi/quera-bootcamp-django
# Generated by Django 4.2.5 on 2023-09-07 22:26

from django.db import migrations, models


class Migration(migrations.Migration):
    dependencies = [
        ("contact", "0001_initial"),
    ]

    operations = [
        migrations.CreateModel(
            name="CSAT",
            fields=[
                ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
                ("rating", models.IntegerField()),
                ("comment", models.TextField(blank=True)),
                ("created_at", models.DateTimeField(auto_now_add=True)),
            ],
        ),
    ]

contact/migrations/0002_csat.py
danialkeimasi/quera-bootcamp-django
# Generated by Django 4.2.5 on 2023-09-08 09:55

import django.core.validators
from django.db import migrations, models

# def update_resolved(app, _):
#     Contact = app.get_model("contact", "Contact")
#     Contact.objects.filter(resolved=True).update(state="resolved")


class Migration(migrations.Migration):
    dependencies = [
        ("contact", "0002_csat"),
    ]

    operations = [
        # This was an example of how to add a new field to a model and populate it with data:
        # migrations.AddField(
        #     model_name="contact",
        #     name="state",
        #     field=models.CharField(
        #         choices=[("resolved", "Resolved"), ("pending", "Pending")], default="pending", max_length=20
        #     ),
        # ),
        # migrations.RunPython(code=update_resolved, reverse_code=migrations.RunPython.noop),
        migrations.AlterField(
            model_name="contact",
            name="phone_number",
            field=models.CharField(
                blank=True,
                max_length=11,
                validators=[
                    django.core.validators.RegexValidator(
                        "^09[0-9]+$", "Phone number should be 11 digits and start with 09."
                    ),
                    django.core.validators.MinLengthValidator(11),
                ],
            ),
        ),
    ]

contact/migrations/0003_contact_state_alter_contact_phone_number.py
danialkeimasi/quera-bootcamp-django
from django.core.exceptions import ValidationError
from django.core.validators import MinLengthValidator, RegexValidator
from django.db import models
from django.db.models import Q


def validate_name(value: str):
    # https://docs.djangoproject.com/en/4.2/ref/validators/#writing-validators

    if value.lower() in ["danial keimasi", "daishogun"]:
        raise ValidationError(f"{value}, go back to work.")


class Contact(models.Model):
    class Meta:
        constraints = [
            models.CheckConstraint(
                check=~Q(email="") | ~Q(phone_number=""),
                name="email_or_phone_number",
                violation_error_message="Either email or phone number should be provided.",
            )
        ]

    name = models.CharField(
        max_length=100,
        validators=[
            RegexValidator(r"^[a-zA-Z]*$", "Only characters are allowed."),
            MinLengthValidator(3),
            validate_name,
        ],
    )
    email = models.EmailField(blank=True)
    phone_number = models.CharField(
        max_length=11,
        blank=True,
        validators=[
            RegexValidator(r"^09[0-9]+$", "Phone number should be 11 digits and start with 09."),
            MinLengthValidator(11),
        ],
    )
    message = models.TextField(
        validators=[
            MinLengthValidator(10),
        ],
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    resolved = models.BooleanField(default=False)

    def __str__(self):
        return f"{self.name} - {self.email}"

    def clean(self) -> None:
        # use this method to validate fields that depend on each other

        if self.name.lower() == "bagher" and self.email == "":
            raise ValidationError("Bagher is not allowed.")


class CSAT(models.Model):
    rating = models.IntegerField()
    comment = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"{self.rating} - {self.comment}"

contact/models.py
danialkeimasi/quera-bootcamp-django
from django.urls import path

from . import views

app_name = "contact"

urlpatterns = [
    path("", views.contact_view, name="contact"),
    path("csat/", views.csat_view, name="csat"),
]

contact/urls.py
danialkeimasi/quera-bootcamp-django
from django.http import HttpRequest
from django.shortcuts import render

from .forms import ContactForm, CSATForm


def contact_view(request: HttpRequest):
    if request.method == "POST":
        form = ContactForm(request.POST)
        if form.is_valid():
            form.save()
            return render(request, "contact/success.html")
    else:
        form = ContactForm(initial={"name": "Initial Name"})
    return render(request, "contact/form.html", context={"form": form})


def csat_view(request: HttpRequest):
    if request.method == "POST":
        form = CSATForm(request.POST)
        if form.is_valid():
            form.save()
            return render(request, "contact/success.html")
    else:
        form = CSATForm()
    return render(request, "contact/form.html", context={"form": form})

contact/views.py
danialkeimasi/quera-bootcamp-django
from django.apps import AppConfig


class CoreConfig(AppConfig):
    default_auto_field = "django.db.models.BigAutoField"
    name = "core"

    def ready(self) -> None:
        pass

core/apps.py
danialkeimasi/quera-bootcamp-django
from django.urls.converters import StringConverter


class DrinkConverter(StringConverter):
    regex = r"coke|water|dough"

core/converters.py
danialkeimasi/quera-bootcamp-django
from django import template

register = template.Library()


@register.filter(is_safe=True)
def echo_lower(value):
    return value.lower()

core/templatetags/core.py
danialkeimasi/quera-bootcamp-django
from django.urls import path, re_path
from django.urls.converters import register_converter

from . import converters, views

register_converter(converters.DrinkConverter, "drink")

app_name = "core"

urlpatterns = [
    path("", views.index_view, name="index"),
    path("hello/<str:name>", views.hello_view, name="hello"),
    path("sum/<int:a>/<int:b>", views.sum_view, name="sum"),
    path("slug/<slug:slug>", views.slug_view, name="slug"),
    path("uuid/<uuid:uuid>", views.uuid_view, name="uuid"),
    re_path(r"^drink/(?P<drink>coke|water|dough)/$", views.drink, name="land"),
    path("custom_drink/<drink:drink>/", views.drink, name="drink"),
]

core/urls.py
danialkeimasi/quera-bootcamp-django
from uuid import UUID

from django.http import HttpRequest
from django.shortcuts import HttpResponse, render
from django.template.loader import render_to_string


def index_view(request: HttpRequest):
    response_as_string = render_to_string("core/index.html", request=request)
    return HttpResponse(response_as_string)


def hello_view(request: HttpRequest, name: str):
    reserved_names = [
        "quera",
        "bootcamp",
        "college",
        "contest",
    ]
    return render(
        request,
        "core/hello.html",
        context={
            "name": name,
            "reserved_names": reserved_names,
            "is_quera": name.lower() in reserved_names,
        },
    )


def sum_view(request: HttpRequest, a: int, b: int):
    return HttpResponse(f"{a} + {b} = {a + b}")


def slug_view(request: HttpRequest, slug: str):
    return HttpResponse(f"Slug: {slug}")


def uuid_view(request: HttpRequest, uuid: UUID):
    return HttpResponse(f"UUID: {uuid}")


def drink(request: HttpRequest, drink: str):
    return HttpResponse(f"Drink: {drink}")

core/views.py
danialkeimasi/quera-bootcamp-django
"""
Django settings for project project.

Generated by 'django-admin startproject' using Django 4.2.5.

For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""

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/4.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-pr=%nldiuig=ns+3h&-3l0=&wuh(vh-=84y7aaae%f2o2()0^9"

# 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.humanize",
    "core",
    "contact",
]

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 = "project.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        "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 = "project.wsgi.application"


# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}


# Password validation
# https://docs.djangoproject.com/en/4.2/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/4.2/topics/i18n/

LANGUAGE_CODE = "en-us"

TIME_ZONE = "UTC"

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/

STATIC_URL = "static/"

# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

project/settings.py
danialkeimasi/quera-bootcamp-django
"""
URL configuration for project project.

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("core.urls")),
    path("contact/", include("contact.urls")),
]

project/urls.py
danialkeimasi/quera-bootcamp-django