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