First commit
This commit is contained in:
parent
8a9aebdf89
commit
12ba5461fd
18
Caddyfile
Normal file
18
Caddyfile
Normal file
@ -0,0 +1,18 @@
|
||||
http://hello.localhost {
|
||||
|
||||
root * /usr/src/app/
|
||||
encode gzip zstd
|
||||
|
||||
@notStatic {
|
||||
not path /static/* /media/*
|
||||
}
|
||||
|
||||
reverse_proxy @notStatic django:8000
|
||||
|
||||
file_server /static/*
|
||||
file_server /media/*
|
||||
}
|
||||
|
||||
http://webmail.localhost {
|
||||
reverse_proxy mailhog:8025
|
||||
}
|
16
Dockerfile
Normal file
16
Dockerfile
Normal file
@ -0,0 +1,16 @@
|
||||
# Image
|
||||
FROM python:3.10
|
||||
|
||||
# Display the Python output through the terminal
|
||||
ENV PYTHONUNBUFFERED: 1
|
||||
|
||||
# Set work directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Add Python dependencies
|
||||
## Update pip
|
||||
RUN pip install --upgrade pip
|
||||
## Copy requirements
|
||||
COPY requirements.txt ./requirements.txt
|
||||
## Install requirements
|
||||
RUN pip3 install -r requirements.txt
|
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
0
app/app_template/__init__.py
Normal file
0
app/app_template/__init__.py
Normal file
116
app/app_template/actions.py
Normal file
116
app/app_template/actions.py
Normal file
@ -0,0 +1,116 @@
|
||||
from .forms import LoginForm, SignupForm
|
||||
from asgiref.sync import async_to_sync
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
from channels.auth import login, logout
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import authenticate
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def send_page(self, page):
|
||||
"""Render HTML and send page to client"""
|
||||
|
||||
# Prepare context data for page
|
||||
context = {}
|
||||
match page:
|
||||
case "login":
|
||||
context = {"form": LoginForm()}
|
||||
case "signup":
|
||||
context = {"form": SignupForm()}
|
||||
|
||||
# Add user to context if logged in
|
||||
if "user" in self.scope:
|
||||
context.update({ "user": self.scope["user"]})
|
||||
context.update({"active_nav": page})
|
||||
|
||||
# Render HTML nav and send to client
|
||||
self.send_html({
|
||||
"selector": "#nav",
|
||||
"html": render_to_string("components/_nav.html", context),
|
||||
})
|
||||
|
||||
# Render HTML page and send to client
|
||||
self.send_html({
|
||||
"selector": "#main",
|
||||
"html": render_to_string(f"pages/{page}.html", context),
|
||||
"url": reverse(page),
|
||||
})
|
||||
|
||||
# Hidrate page
|
||||
match page:
|
||||
case "home":
|
||||
update_TODO(self)
|
||||
|
||||
|
||||
def action_signup(self, data):
|
||||
"""Sign up user"""
|
||||
form = SignupForm(data)
|
||||
user_exist = User.objects.filter(email=data["email"]).exists()
|
||||
if form.is_valid() and data["password"] == data["password_confirm"] and not user_exist:
|
||||
# Create user
|
||||
user = User.objects.create_user(data["username"], data["email"], data["password"])
|
||||
user.is_active = True
|
||||
user.save()
|
||||
# Login user
|
||||
send_page(self, "login")
|
||||
else:
|
||||
# Send form errors
|
||||
self.send_html({
|
||||
"selector": "#main",
|
||||
"html": render_to_string("pages/signup.html", {"form": form, "user_exist": user_exist, "passwords_do_not_match": data["password"] != data["password_confirm"]}),
|
||||
"append": False,
|
||||
"url": reverse("signup")
|
||||
})
|
||||
|
||||
|
||||
def action_login(self, data):
|
||||
"""Log in user"""
|
||||
form = LoginForm(data)
|
||||
user = authenticate(username=data["email"], password=data["password"])
|
||||
if form.is_valid() and user:
|
||||
async_to_sync(login)(self.scope, user)
|
||||
self.scope["session"].save()
|
||||
send_page(self, "profile")
|
||||
else:
|
||||
self.send_html({
|
||||
"selector": "#main",
|
||||
"html": render_to_string("pages/login.html", {"form": form, "user_does_not_exist": user is None}),
|
||||
"append": False,
|
||||
"url": reverse("login")
|
||||
})
|
||||
|
||||
|
||||
def action_logout(self):
|
||||
"""Log out user"""
|
||||
async_to_sync(logout)(self.scope)
|
||||
self.scope["session"].save()
|
||||
send_page(self, "login")
|
||||
|
||||
|
||||
def add_lap(self):
|
||||
"""Add lap to Home page"""
|
||||
# Send current time to client
|
||||
self.send_html({
|
||||
"selector": "#laps",
|
||||
"html": render_to_string("components/_lap.html", {"time": datetime.now()}),
|
||||
"append": True,
|
||||
})
|
||||
|
||||
|
||||
def add_task(self, data):
|
||||
"""Add task from TODO section"""
|
||||
# Add task to list
|
||||
self.scope["session"]["tasks"].append(data["task"])
|
||||
self.scope["session"].save()
|
||||
# Update task list
|
||||
update_TODO(self)
|
||||
|
||||
|
||||
def update_TODO(self):
|
||||
"""Update TODO list"""
|
||||
self.send_html({
|
||||
"selector": "#todo",
|
||||
"html": render_to_string("components/_tasks.html", {"tasks": self.scope["session"]["tasks"]}),
|
||||
"append": False,
|
||||
})
|
6
app/app_template/apps.py
Normal file
6
app/app_template/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SimpleAppConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "app.app_template"
|
35
app/app_template/backends.py
Normal file
35
app/app_template/backends.py
Normal file
@ -0,0 +1,35 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.backends import BaseBackend
|
||||
|
||||
|
||||
class EmailBackend(BaseBackend):
|
||||
"""
|
||||
Email authentication backend
|
||||
"""
|
||||
|
||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||
"""
|
||||
Authenticate a user based on email address as the user name.
|
||||
"""
|
||||
if "@" in username:
|
||||
kwargs = {"email": username}
|
||||
else:
|
||||
kwargs = {"username": username}
|
||||
try:
|
||||
user = User.objects.get(**kwargs)
|
||||
if user.check_password(password):
|
||||
return user
|
||||
except User.DoesNotExist:
|
||||
return None
|
||||
|
||||
def get_user(self, user_id):
|
||||
try:
|
||||
return User.objects.get(pk=user_id)
|
||||
except User.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
# Future re-implementation with token for auto login
|
||||
class TokenBackend(BaseBackend):
|
||||
def authenticate(self, request, token=None):
|
||||
pass
|
55
app/app_template/consumers.py
Normal file
55
app/app_template/consumers.py
Normal file
@ -0,0 +1,55 @@
|
||||
# app/app_template/consumers.py
|
||||
from channels.generic.websocket import JsonWebsocketConsumer
|
||||
import app.app_template.actions as actions
|
||||
|
||||
|
||||
class ExampleConsumer(JsonWebsocketConsumer):
|
||||
|
||||
def connect(self):
|
||||
"""Event when client connects"""
|
||||
# Accept the connection
|
||||
self.accept()
|
||||
# Make session task list
|
||||
if "tasks" not in self.scope["session"]:
|
||||
self.scope["session"]["tasks"] = []
|
||||
self.scope["session"].save()
|
||||
|
||||
|
||||
def disconnect(self, close_code):
|
||||
"""Event when client disconnects"""
|
||||
pass
|
||||
|
||||
def receive_json(self, data_received):
|
||||
"""
|
||||
Event when data is received
|
||||
All information will arrive in 2 variables:
|
||||
"action", with the action to be taken
|
||||
"data" with the information
|
||||
"""
|
||||
# Get the data
|
||||
data = data_received["data"]
|
||||
# Depending on the action we will do one task or another.
|
||||
match data_received["action"]:
|
||||
case "Change page":
|
||||
actions.send_page(self, data["page"])
|
||||
case "Signup":
|
||||
actions.action_signup(self, data)
|
||||
case "Login":
|
||||
actions.action_login(self, data)
|
||||
case "Logout":
|
||||
actions.action_logout(self)
|
||||
case "Add lap":
|
||||
actions.add_lap(self)
|
||||
case "Add task":
|
||||
actions.add_task(self, data)
|
||||
|
||||
|
||||
def send_html(self, event):
|
||||
"""Event: Send html to client"""
|
||||
data = {
|
||||
"selector": event["selector"],
|
||||
"html": event["html"],
|
||||
"append": "append" in event and event["append"],
|
||||
"url": event["url"] if "url" in event else "",
|
||||
}
|
||||
self.send_json(data)
|
39
app/app_template/forms.py
Normal file
39
app/app_template/forms.py
Normal file
@ -0,0 +1,39 @@
|
||||
from django import forms
|
||||
|
||||
|
||||
class LoginForm(forms.Form):
|
||||
email = forms.CharField(
|
||||
label="Email",
|
||||
max_length=255,
|
||||
widget=forms.EmailInput(attrs={"id": "login-email", "class": "input"}),
|
||||
)
|
||||
password = forms.CharField(
|
||||
label="Password",
|
||||
max_length=255,
|
||||
widget=forms.PasswordInput(attrs={"id": "login-password", "class": "input"}),
|
||||
)
|
||||
|
||||
|
||||
class SignupForm(forms.Form):
|
||||
username = forms.CharField(
|
||||
label="Username",
|
||||
max_length=255,
|
||||
widget=forms.TextInput(attrs={"id": "signup-username", "class": "input"}),
|
||||
)
|
||||
email = forms.EmailField(
|
||||
label="Email",
|
||||
max_length=255,
|
||||
widget=forms.EmailInput(attrs={"id": "signup-email", "class": "input"}),
|
||||
)
|
||||
password = forms.CharField(
|
||||
label="Password",
|
||||
max_length=255,
|
||||
widget=forms.PasswordInput(attrs={"id": "signup-password", "class": "input"}),
|
||||
)
|
||||
password_confirm = forms.CharField(
|
||||
label="Confirm Password",
|
||||
max_length=255,
|
||||
widget=forms.PasswordInput(
|
||||
attrs={"id": "signup-password-confirm", "class": "input"}
|
||||
),
|
||||
)
|
0
app/app_template/migrations/__init__.py
Normal file
0
app/app_template/migrations/__init__.py
Normal file
3
app/app_template/models.py
Normal file
3
app/app_template/models.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
23
app/app_template/templates/base.html
Normal file
23
app/app_template/templates/base.html
Normal file
@ -0,0 +1,23 @@
|
||||
{% load static %}
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<title>Example website</title>
|
||||
<link rel="stylesheet" href="{% static 'css/main.css' %}">
|
||||
<script defer src="{% static 'js/index.js' %}"></script>
|
||||
</head>
|
||||
<body
|
||||
data-host="{{ request.get_host }}"
|
||||
data-scheme="{{ request.scheme }}"
|
||||
>
|
||||
<div class="container">
|
||||
<header>
|
||||
<nav id="nav" class="nav">{% include 'components/_nav.html' %}</nav>
|
||||
</header>
|
||||
<main id="main">{% include page %}</main>
|
||||
<footer class="footer">My footer</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
54
app/app_template/templates/components/_nav.html
Normal file
54
app/app_template/templates/components/_nav.html
Normal file
@ -0,0 +1,54 @@
|
||||
<ul class="nav__ul" data-controller="navbar">
|
||||
{# Links always visible #}
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="nav__link nav__link--page{% if active_nav == "home" %} active{% endif %}"
|
||||
data-target="home"
|
||||
>
|
||||
Home
|
||||
</a>
|
||||
</li>
|
||||
{% if user.is_authenticated %}
|
||||
{# Links only if identified #}
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="nav__link nav__link--page{% if active_nav == "profile" %} active{% endif %}"
|
||||
data-target="profile"
|
||||
>
|
||||
Profile
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="nav__link"
|
||||
id="logout"
|
||||
>
|
||||
Logout
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
{# Links only if not identified #}
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="nav__link nav__link--page{% if active_nav == "login" %} active{% endif %}"
|
||||
data-target="login"
|
||||
>
|
||||
Login
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="nav__link nav__link--page{% if active_nav == "signup" %} active{% endif %}"
|
||||
data-target="signup"
|
||||
data-action="click->message#displayUpdateForm"
|
||||
>
|
||||
Signup
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
3
app/app_template/templates/components/_tasks.html
Normal file
3
app/app_template/templates/components/_tasks.html
Normal file
@ -0,0 +1,3 @@
|
||||
{% for task in tasks %}
|
||||
<li>{{ task }}</li>
|
||||
{% endfor %}
|
1
app/app_template/templates/pages/404.html
Normal file
1
app/app_template/templates/pages/404.html
Normal file
@ -0,0 +1 @@
|
||||
<h1>404</h1>
|
17
app/app_template/templates/pages/home.html
Normal file
17
app/app_template/templates/pages/home.html
Normal file
@ -0,0 +1,17 @@
|
||||
<section>
|
||||
<h1>Welcome to an example of browsing with WebSockets over the Wire</h1>
|
||||
<p>You will be able to experience a simple structure with a registration, a login and a private page.</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Laps</h2>
|
||||
<p>
|
||||
<button id="add-lap">Add lap</button>
|
||||
</p>
|
||||
<ul id="laps"></ul>
|
||||
</section>
|
||||
<section>
|
||||
<h2>TODO</h2>
|
||||
<input type="text" id="task">
|
||||
<button id="add-task">Add task</button>
|
||||
<ul id="todo"></ul>
|
||||
</section>
|
8
app/app_template/templates/pages/login.html
Normal file
8
app/app_template/templates/pages/login.html
Normal file
@ -0,0 +1,8 @@
|
||||
<h1>Login</h1>
|
||||
<form id="login-form">
|
||||
{% if user_does_not_exist %}
|
||||
<h2>The user does not exist or the password is wrong.</h2>
|
||||
{% endif %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" class="button" value="Login">
|
||||
</form>
|
5
app/app_template/templates/pages/profile.html
Normal file
5
app/app_template/templates/pages/profile.html
Normal file
@ -0,0 +1,5 @@
|
||||
<h1>Profile</h1>
|
||||
<h2>Username</h2>
|
||||
<p>{{ user.username }}</p>
|
||||
<h2>Email</h2>
|
||||
<p>{{ user.email }}</p>
|
11
app/app_template/templates/pages/signup.html
Normal file
11
app/app_template/templates/pages/signup.html
Normal file
@ -0,0 +1,11 @@
|
||||
<h1>Signup</h1>
|
||||
<form id="signup-form">
|
||||
{% if user_exist %}
|
||||
<p>Email already exist</p>
|
||||
{% endif %}
|
||||
{% if passwords_do_not_match %}
|
||||
<p>Passwords do not match.</p>
|
||||
{% endif %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" class="button" value="Signup">
|
||||
</form>
|
41
app/app_template/views.py
Normal file
41
app/app_template/views.py
Normal file
@ -0,0 +1,41 @@
|
||||
from django.shortcuts import render, redirect
|
||||
from .forms import LoginForm, SignupForm
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
|
||||
def home(request):
|
||||
return render(
|
||||
request,
|
||||
"base.html",
|
||||
{
|
||||
"page": "pages/home.html",
|
||||
"active_nav": "home",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def login(request):
|
||||
return render(
|
||||
request,
|
||||
"base.html",
|
||||
{"page": "pages/login.html", "active_nav": "login", "form": LoginForm()},
|
||||
)
|
||||
|
||||
|
||||
def signup(request):
|
||||
return render(
|
||||
request,
|
||||
"base.html",
|
||||
{"page": "pages/signup.html", "active_nav": "signup", "form": SignupForm()},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def profile(request):
|
||||
return render(
|
||||
request, "base.html", {"page": "pages/profile.html", "active_nav": "profile"}
|
||||
)
|
||||
|
||||
|
||||
def page_not_found(request, exception):
|
||||
return render(request, "base.html", {"page": "pages/404.html"})
|
12
django-launcher.sh
Normal file
12
django-launcher.sh
Normal file
@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Collect static files
|
||||
python3 manage.py collectstatic --noinput
|
||||
|
||||
# Apply database migrations
|
||||
python3 manage.py migrate
|
||||
|
||||
# Start server with debug mode
|
||||
python3 manage.py runserver 0.0.0.0:8000
|
||||
# Start server with production mode
|
||||
#daphne -b 0.0.0.0 -p 8000 project_template.asgi:application
|
74
docker-compose.yaml
Normal file
74
docker-compose.yaml
Normal file
@ -0,0 +1,74 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
|
||||
django:
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: ./Dockerfile
|
||||
entrypoint: bash ./django-launcher.sh
|
||||
volumes:
|
||||
- .:/usr/src/app/
|
||||
environment:
|
||||
DEBUG: "True"
|
||||
ALLOWED_HOSTS: hello.localhost
|
||||
SECRET_KEY: mysecret
|
||||
DB_ENGINE: django.db.backends.postgresql
|
||||
DB_NAME: hello_db
|
||||
DB_USER: postgres
|
||||
DB_PASSWORD: postgres
|
||||
DB_HOST: postgresql
|
||||
DB_PORT: 5432
|
||||
DOMAIN: hello.localhost
|
||||
DOMAIN_URL: http://hello.localhost
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: 6379
|
||||
STATIC_URL: /static/
|
||||
STATIC_ROOT: static
|
||||
MEDIA_URL: /media/
|
||||
DEFAULT_FROM_EMAIL: no-reply@hello.localhost
|
||||
EMAIL_HOST: mailhog
|
||||
EMAIL_USE_TLS: "False"
|
||||
EMAIL_USE_SSL: "False"
|
||||
EMAIL_PORT: 1025
|
||||
EMAIL_USER:
|
||||
EMAIL_PASSWORD:
|
||||
expose:
|
||||
- 8000
|
||||
depends_on:
|
||||
- postgresql
|
||||
- redis
|
||||
|
||||
postgresql:
|
||||
image: postgres
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: hello_db
|
||||
volumes:
|
||||
- ./postgres_data:/var/lib/postgresql/data/
|
||||
expose:
|
||||
- 5432
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
expose:
|
||||
- 6379
|
||||
|
||||
caddy:
|
||||
image: caddy:alpine
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
volumes:
|
||||
- .:/usr/src/app/
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
- ./caddy_data:/data
|
||||
depends_on:
|
||||
- django
|
||||
|
||||
mailhog:
|
||||
image: mailhog/mailhog:latest
|
||||
expose:
|
||||
- 1025
|
||||
- 8025
|
22
manage.py
Executable file
22
manage.py
Executable file
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project_template.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
0
project_template/__init__.py
Normal file
0
project_template/__init__.py
Normal file
29
project_template/asgi.py
Normal file
29
project_template/asgi.py
Normal file
@ -0,0 +1,29 @@
|
||||
# project_template/asgi.py
|
||||
import os
|
||||
import django
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project_template.settings")
|
||||
from django.conf import settings
|
||||
django.setup()
|
||||
from django.core.asgi import get_asgi_application
|
||||
from channels.security.websocket import OriginValidator
|
||||
from channels.auth import AuthMiddlewareStack
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
from django.urls import re_path
|
||||
from app.app_template.consumers import ExampleConsumer
|
||||
|
||||
|
||||
application = ProtocolTypeRouter(
|
||||
{
|
||||
# Django's ASGI application to handle traditional HTTP requests
|
||||
"http": get_asgi_application(),
|
||||
# WebSocket handler
|
||||
"websocket": OriginValidator(AuthMiddlewareStack(
|
||||
URLRouter(
|
||||
[
|
||||
re_path(r"^ws/example/$", ExampleConsumer.as_asgi()),
|
||||
]
|
||||
)
|
||||
), settings.ALLOWED_HOSTS)
|
||||
}
|
||||
)
|
160
project_template/settings.py
Normal file
160
project_template/settings.py
Normal file
@ -0,0 +1,160 @@
|
||||
"""
|
||||
Django settings for project_template project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 4.0.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/4.0/ref/settings/
|
||||
"""
|
||||
import os
|
||||
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.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = os.environ.get("SECRET_KEY")
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = os.environ.get("DEBUG", "True") == "True"
|
||||
|
||||
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS").split(",")
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"channels",
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"app.app_template",
|
||||
]
|
||||
|
||||
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_template.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_template.wsgi.application"
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": os.environ.get("DB_ENGINE"),
|
||||
"NAME": os.environ.get("DB_NAME"),
|
||||
"USER": os.environ.get("DB_USER"),
|
||||
"PASSWORD": os.environ.get("DB_PASSWORD"),
|
||||
"HOST": os.environ.get("DB_HOST"),
|
||||
"PORT": os.environ.get("DB_PORT"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.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/4.0/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.0/howto/static-files/
|
||||
|
||||
STATIC_ROOT = os.environ.get("STATIC_ROOT")
|
||||
STATIC_URL = os.environ.get("STATIC_URL")
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
|
||||
MEDIA_URL = os.environ.get("MEDIA_URL")
|
||||
|
||||
DOMAIN = os.environ.get("DOMAIN")
|
||||
DOMAIN_URL = os.environ.get("DOMAIN_URL")
|
||||
CSRF_TRUSTED_ORIGINS = [DOMAIN_URL]
|
||||
|
||||
"""EMAIL CONFIG"""
|
||||
DEFAULT_FROM_EMAIL = os.environ.get("EMAIL_ADDRESS")
|
||||
EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS") == "True"
|
||||
EMAIL_USE_SSL = os.environ.get("EMAIL_USE_SSL") == "True"
|
||||
EMAIL_HOST = os.environ.get("EMAIL_HOST")
|
||||
EMAIL_PORT = os.environ.get("EMAIL_PORT")
|
||||
EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER")
|
||||
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD")
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||
"CONFIG": {
|
||||
"hosts": [(os.environ.get("REDIS_HOST"), os.environ.get("REDIS_PORT"))],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ASGI_APPLICATION = "project_template.asgi.application"
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'app.app_template.backends.EmailBackend',
|
||||
)
|
28
project_template/urls.py
Normal file
28
project_template/urls.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""project_template URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/4.0/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 path
|
||||
from app.app_template import views
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.home, name="home"),
|
||||
path("login/", views.login, name="login"),
|
||||
path("signup/", views.signup, name="signup"),
|
||||
path("profile/", views.profile, name="profile"),
|
||||
path("admin/", admin.site.urls),
|
||||
]
|
||||
|
||||
handler404 = "app.app_template.views.page_not_found"
|
16
project_template/wsgi.py
Normal file
16
project_template/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for project_template project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project_template.settings")
|
||||
|
||||
application = get_wsgi_application()
|
15
requirements.txt
Normal file
15
requirements.txt
Normal file
@ -0,0 +1,15 @@
|
||||
# Django
|
||||
django===4.0
|
||||
# Django Server
|
||||
daphne===3.0.2
|
||||
asgiref===3.4.1
|
||||
# Manipulate images
|
||||
Pillow===8.2.0
|
||||
# Kit utilities
|
||||
django-extensions===3.1.3
|
||||
# PostgreSQL driver
|
||||
psycopg2===2.9.1
|
||||
# Django Channels
|
||||
channels==3.0.4
|
||||
# Redis Layer
|
||||
channels_redis===3.2.0
|
69
static/css/main.css
Executable file
69
static/css/main.css
Executable file
@ -0,0 +1,69 @@
|
||||
|
||||
|
||||
/* Global styles */
|
||||
:root {
|
||||
--color__background: #f6f4f3;
|
||||
--color__gray: #ccc;
|
||||
--color__black: #000;
|
||||
--color__active: #00a0ff;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: var(--color__background);
|
||||
}
|
||||
|
||||
/* General classes for small components */
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
padding: 1rem 0;
|
||||
max-width: 40rem;
|
||||
}
|
||||
|
||||
.nav__ul {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav__link.active {
|
||||
color: var(--color__active);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: var(--color__gray);
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
filter: brightness(90%);
|
||||
}
|
||||
|
||||
.input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
padding: .5rem;
|
||||
resize: none;
|
||||
border: 1px solid var(--color__gray);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 1rem;
|
||||
text-align: center;
|
||||
}
|
28
static/js/controllers/add_form_controller.js
Normal file
28
static/js/controllers/add_form_controller.js
Normal file
@ -0,0 +1,28 @@
|
||||
import { Controller } from "../vendors/stimulus.js"
|
||||
import { sendData } from "../webSocketsCli.js"
|
||||
|
||||
export default class extends Controller {
|
||||
|
||||
static targets = [ "author", "text" ]
|
||||
|
||||
/**
|
||||
* Send new message
|
||||
* @param {Event} event
|
||||
* @return {void}
|
||||
*/
|
||||
add(event) {
|
||||
event.preventDefault();
|
||||
// Prepare the information we will send
|
||||
const newData = {
|
||||
"action": "add message",
|
||||
"data": {
|
||||
"author": this.authorTarget.value,
|
||||
"text": this.textTarget.value
|
||||
}
|
||||
};
|
||||
// Send the data to the server
|
||||
sendData(newData, window.myWebSocket);
|
||||
// Clear message form
|
||||
this.textTarget.value = "";
|
||||
}
|
||||
}
|
116
static/js/controllers/message_controller.js
Normal file
116
static/js/controllers/message_controller.js
Normal file
@ -0,0 +1,116 @@
|
||||
import { Controller } from "../vendors/stimulus.js"
|
||||
import { sendData } from "../webSocketsCli.js"
|
||||
|
||||
export default class extends Controller {
|
||||
|
||||
static targets = [ "item", "paginator" ]
|
||||
|
||||
connect() {
|
||||
this.enableInfiniteScroll();
|
||||
}
|
||||
|
||||
/*
|
||||
FUNCTIONS
|
||||
*/
|
||||
|
||||
/**
|
||||
* Switches to the next page when the last message is displayed.
|
||||
*/
|
||||
enableInfiniteScroll() {
|
||||
const lastMessage = this.itemTargets.at(-1);
|
||||
// Turn the page when the last message is displayed.
|
||||
const observerLastMessage = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
if (!this.isLastPage()) this.goToNextPage();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observerLastMessage.observe(lastMessage);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get current page stored in #paginator as dataset
|
||||
* @returns {number}
|
||||
*/
|
||||
getCurrentPage() {
|
||||
return parseInt(this.paginatorTarget.dataset.page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we are on the last page
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isLastPage() {
|
||||
return parseInt(this.paginatorTarget.dataset.totalPages) === this.getCurrentPage();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Switch to the next page
|
||||
* @param {Event} event
|
||||
* @return {void}
|
||||
*/
|
||||
goToNextPage(event) {
|
||||
// Prepare the information we will send
|
||||
const newData = {
|
||||
"action": "list messages",
|
||||
"data": {
|
||||
"page": this.getCurrentPage() + 1,
|
||||
}
|
||||
};
|
||||
// Send the data to the server
|
||||
sendData(newData, myWebSocket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the update form
|
||||
* @param {Event} event
|
||||
* @return {void}
|
||||
*/
|
||||
displayUpdateForm(event) {
|
||||
const message = {
|
||||
"action": "open edit page",
|
||||
"data": {
|
||||
"id": event.target.dataset.id
|
||||
}
|
||||
};
|
||||
sendData(message, window.myWebSocket);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update message
|
||||
* @param {Event} event
|
||||
* @return {void}
|
||||
*/
|
||||
updateMessage(event) {
|
||||
event.preventDefault();
|
||||
const message = {
|
||||
"action": "update message",
|
||||
"data": {
|
||||
"id": event.target.dataset.id,
|
||||
"author": event.target.querySelector("#message-form__author--update").value,
|
||||
"text": event.target.querySelector("#message-form__text--update").value
|
||||
}
|
||||
};
|
||||
sendData(message, myWebSocket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete message
|
||||
* @param {Event} event
|
||||
* @return {void}
|
||||
*/
|
||||
deleteMessage(event) {
|
||||
const message = {
|
||||
"action": "delete message",
|
||||
"data": {
|
||||
"id": event.target.dataset.id
|
||||
}
|
||||
};
|
||||
sendData(message, window.myWebSocket);
|
||||
}
|
||||
}
|
28
static/js/controllers/navbarController.js
Normal file
28
static/js/controllers/navbarController.js
Normal file
@ -0,0 +1,28 @@
|
||||
import { Controller } from "../vendors/stimulus.js"
|
||||
import { sendData } from "../webSocketsCli.js"
|
||||
|
||||
export default class extends Controller {
|
||||
|
||||
static targets = [ "page" ]
|
||||
|
||||
/**
|
||||
* Send new message
|
||||
* @param {Event} event
|
||||
* @return {void}
|
||||
*/
|
||||
add(event) {
|
||||
event.preventDefault();
|
||||
// Prepare the information we will send
|
||||
const newData = {
|
||||
"action": "add message",
|
||||
"data": {
|
||||
"author": this.authorTarget.value,
|
||||
"text": this.textTarget.value
|
||||
}
|
||||
};
|
||||
// Send the data to the server
|
||||
sendData(newData, window.myWebSocket);
|
||||
// Clear message form
|
||||
this.textTarget.value = "";
|
||||
}
|
||||
}
|
25
static/js/controllers/update_form_controller.js
Normal file
25
static/js/controllers/update_form_controller.js
Normal file
@ -0,0 +1,25 @@
|
||||
import { Controller } from "../vendors/stimulus.js"
|
||||
import { sendData } from "../webSocketsCli.js"
|
||||
|
||||
export default class extends Controller {
|
||||
|
||||
static targets = [ "author", "text" ]
|
||||
|
||||
/**
|
||||
* Update message
|
||||
* @param {Event} event
|
||||
* @return {void}
|
||||
*/
|
||||
update(event) {
|
||||
event.preventDefault();
|
||||
const message = {
|
||||
"action": "update message",
|
||||
"data": {
|
||||
"id": event.target.dataset.id,
|
||||
"author": this.authorTarget.value,
|
||||
"text": this.textTarget.value
|
||||
}
|
||||
};
|
||||
sendData(message, myWebSocket);
|
||||
}
|
||||
}
|
15
static/js/main.js
Executable file
15
static/js/main.js
Executable file
@ -0,0 +1,15 @@
|
||||
import {connect, startEvents} from './webSocketsCli.js';
|
||||
import { Application } from "./vendors/stimulus.js"
|
||||
import navbarController from "./controllers/navbar.js"
|
||||
/*
|
||||
INITIALIZATION
|
||||
*/
|
||||
|
||||
// WebSocket connection
|
||||
connect();
|
||||
startEvents();
|
||||
|
||||
// Stimulus
|
||||
window.Stimulus = Application.start();
|
||||
// Register all controllers
|
||||
Stimulus.register("navbar", navbarController);
|
1944
static/js/vendors/stimulus.js
vendored
Normal file
1944
static/js/vendors/stimulus.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
50
static/js/webSocketsCli.js
Normal file
50
static/js/webSocketsCli.js
Normal file
@ -0,0 +1,50 @@
|
||||
/*
|
||||
FUNCTIONS
|
||||
*/
|
||||
|
||||
/**
|
||||
* Connect to WebSockets server (SocialNetworkConsumer)
|
||||
* @param {string} url - WebSockets server url
|
||||
* @return {WebSocket}
|
||||
*/
|
||||
export function connect(url=`${document.body.dataset.scheme === 'http' ? 'ws' : 'wss'}://${ document.body.dataset.host }/ws/social-network/`) {
|
||||
window.myWebSocket = new WebSocket(url);
|
||||
return window.myWebSocket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send data to WebSockets server
|
||||
* @param {string} message
|
||||
* @param {WebSocket} webSocket
|
||||
* @return {void}
|
||||
*/
|
||||
export function sendData(message, webSocket) {
|
||||
webSocket.send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
/*
|
||||
EVENTS
|
||||
*/
|
||||
|
||||
/**
|
||||
* On WebSockets server connection
|
||||
* @param {WebSocket} webSocket
|
||||
* @return {void}
|
||||
*/
|
||||
export function startEvents(webSocket=window.myWebSocket) {
|
||||
// Event when a new message is received by WebSockets
|
||||
webSocket.addEventListener("message", (event) => {
|
||||
// Parse the data received
|
||||
const data = JSON.parse(event.data);
|
||||
// Renders the HTML received from the Consumer
|
||||
const newFragment = document.createRange().createContextualFragment(data.html);
|
||||
const target = document.querySelector(data.selector);
|
||||
if (data.append) {
|
||||
target.appendChild(newFragment);
|
||||
} else {
|
||||
target.replaceChildren(newFragment);
|
||||
}
|
||||
// Update URL
|
||||
history.pushState({}, '', data.url)
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user