diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..62d680a --- /dev/null +++ b/Caddyfile @@ -0,0 +1,11 @@ +http://event.localhost + +root * /usr/src/app/ + +@notStatic { + not path /static/* /media/* +} + +reverse_proxy @notStatic django:8000 + +file_server diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..19f9977 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM debian:11 + +# Prevents Python from writing pyc files to disc (equivalent to python -B option) +ENV PYTHONDONTWRITEBYTECODE 1 +# Prevents Python from buffering stdout and stderr (equivalent to python -u option) +ENV PYTHONUNBUFFERED 1 + +# set work directory +WORKDIR /usr/src/app + +# set time +RUN ln -fs /usr/share/zoneinfo/Europe/Madrid /etc/localtime +RUN dpkg-reconfigure -f noninteractive tzdata + +# install software +RUN apt update +RUN apt install -y build-essential python3-dev libpq-dev python3-pip gettext + + +# install dependencies +RUN pip3 install --upgrade pip +COPY ./requirements.txt . +RUN pip3 install -r requirements.txt + +# launcher +COPY django-launcher.sh /django-launcher.sh +RUN chmod +x /django-launcher.sh + diff --git a/TODO b/TODO new file mode 100644 index 0000000..ba3e4cd --- /dev/null +++ b/TODO @@ -0,0 +1,3 @@ +- Add script with fake data. +- List talks. +- Single talk. \ No newline at end of file diff --git a/app/website/__init__.py b/app/website/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/website/admin.py b/app/website/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/app/website/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/app/website/apps.py b/app/website/apps.py new file mode 100644 index 0000000..b8fd484 --- /dev/null +++ b/app/website/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class WebsiteConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'app.website' diff --git a/app/website/migrations/__init__.py b/app/website/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/website/models.py b/app/website/models.py new file mode 100644 index 0000000..fc41821 --- /dev/null +++ b/app/website/models.py @@ -0,0 +1,77 @@ +from django.db import models + + +class Profile(AbstractBaseUser): + + """User model""" + + email = models.EmailField("Email", unique=True) + full_name = models.CharField( + max_length=100, verbose_name="Nombre y apellidos", default="Sapps" + ) + avatar = models.ImageField(verbose_name="Avatar", upload_to="uploads/avatars/") + + USERNAME_FIELD = "email" # make the user log in with the email + + def __str__(self): + return self.email + + +class Category(models.Model): + + """Category model""" + + name = models.CharField(max_length=100, verbose_name="Nombre") + + class Meta: + ordering = ("name",) + verbose_name = "Categoria" + verbose_name_plural = "Categorias" + + def __str__(self): + return self.name + + +class Talk(models.Model): + + """Talk model""" + + title = models.CharField(max_length=100, verbose_name="Título") + category = models.ForeignKey( + Category, + on_delete=models.SET_NULL, + null=True, + related_name="Categoría", + verbose_name="Categoría", + ) + author = models.ForeignKey( + Profile, + on_delete=models.SET_NULL, + null=True, + related_name="author", + verbose_name="Autor", + ) + image = models.ImageField(verbose_name="Imagen", upload_to="uploads/articles/") + is_draft = models.BooleanField(default=True, verbose_name="¿Es un borrador?") + content = tinymce_models.HTMLField(verbose_name="Contenido") + created_at = models.DateTimeField(auto_now=True, verbose_name="Creado") + + @property + def slug(self): + return slugify(self.title) + + @property + def reading_time_min(self): + # https://help.medium.com/hc/en-us/articles/214991667-Read-time + READING_SPEED_OF_AN_ADULT = 265 + return ceil( + len(strip_tags(self.content).split(" ")) / READING_SPEED_OF_AN_ADULT + ) + + class Meta: + ordering = ("-created_at",) + verbose_name = "Charla" + verbose_name_plural = "Charlas" + + def __str__(self): + return self.title diff --git a/app/website/tests.py b/app/website/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/app/website/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/app/website/views.py b/app/website/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/app/website/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/django-launcher.sh b/django-launcher.sh new file mode 100644 index 0000000..2e4b8f1 --- /dev/null +++ b/django-launcher.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# Collect static files +echo "Collect static files" +python3 manage.py collectstatic --noinput + +# Apply database migrations +echo "Apply database migrations" +python3 manage.py makemigrations +python3 manage.py migrate + +# Start server +echo "Starting server" +## With WebSockets +uvicorn --host 0.0.0.0 --port 8000 --reload event.asgi:application diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..3d2a495 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,75 @@ +version: '3.1' + +services: + + postgresql: + image: postgres + restart: "no" + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: event + ports: + - 5432:5432 + + django: + build: + context: ./ + dockerfile: ./Dockerfile + restart: unless-stopped + entrypoint: /django-launcher.sh + volumes: + - .:/usr/src/app/ + environment: + DEBUG: "True" + ALLOWED_HOSTS: "event.localhost" + SECRET_KEY: "my-secret" + DB_ENGINE: "django.db.backends.postgresql" + DB_NAME: "event" + DB_USER: "postgres" + DB_PASSWORD: "postgres" + DB_HOST: "postgresql" + DB_PORT: "5432" + DOMAIN: "event.localhost" + DOMAIN_URL: "http://event.localhost" + STATIC_URL: "/static/" + STATIC_ROOT: "static" + MEDIA_URL: "/media/" + REDIS_HOST: "redis" + REDIS_PORT: "6379" + EMAIL_HOST: "mailhog" + EMAIL_USE_TLS: "False" + EMAIL_PORT: "1025" + EMAIL_USER: "" + EMAIL_PASSWORD: "" + expose: + - 8000 + depends_on: + - postgresql + + caddy: + image: caddy:alpine + restart: unless-stopped + ports: + - 80:80 + - 443:443 + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile + - ./../caddy_data:/data + - .:/usr/src/app/ + depends_on: + - django + + redis: + image: redis:alpine + restart: unless-stopped + expose: + - 6379 + + mailhog: + image: mailhog/mailhog:latest + restart: unless-stopped + expose: + - 1025 + ports: + - 8025:8025 diff --git a/event/__init__.py b/event/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/event/asgi.py b/event/asgi.py new file mode 100644 index 0000000..0602b7e --- /dev/null +++ b/event/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for event project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'event.settings') + +application = get_asgi_application() diff --git a/event/settings.py b/event/settings.py new file mode 100644 index 0000000..8f91087 --- /dev/null +++ b/event/settings.py @@ -0,0 +1,168 @@ +""" +Django settings for gotrucki project. + +Generated by 'django-admin startproject' using Django 3.2.9. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.2/ref/settings/ +""" +import os +from pathlib import Path +from django.db.backends.signals import connection_created + +# 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/3.2/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 = [] + +# ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS").split(",") +if not os.environ.get("ALLOWED_HOSTS") == None: + ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS").split(",") + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django_extensions', + 'channels', + 'app.website', +] + +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 = 'event.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, "app", "templates")], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + + +# Database +# https://docs.djangoproject.com/en/3.2/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/3.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/3.2/topics/i18n/ + +LANGUAGE_CODE = 'es-es' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = False + +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/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") + + +# Email configuration +EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS") == "True" +EMAIL_HOST = os.environ.get("EMAIL_HOST") +EMAIL_PORT = os.environ.get("EMAIL_PORT") +EMAIL_HOST_USER = os.environ.get("EMAIL_USER") +EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_PASSWORD") +DOMAIN_HOST = os.environ.get("DOMAIN_HOST") +DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL") + +# Realtime +ASGI_APPLICATION = "asgi.application" +CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels_redis.core.RedisChannelLayer", + "CONFIG": { + "hosts": [(os.environ.get("REDIS_HOST"), os.environ.get("REDIS_PORT"))], + }, + }, +} + + + + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +if DEBUG: + CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.dummy.DummyCache", + } + } diff --git a/event/urls.py b/event/urls.py new file mode 100644 index 0000000..17f72c2 --- /dev/null +++ b/event/urls.py @@ -0,0 +1,21 @@ +"""event URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.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 path + +urlpatterns = [ + path('admin/', admin.site.urls), +] diff --git a/event/wsgi.py b/event/wsgi.py new file mode 100644 index 0000000..e191a6b --- /dev/null +++ b/event/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for event 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/3.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'event.settings') + +application = get_wsgi_application() diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..bdcac66 --- /dev/null +++ b/manage.py @@ -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', 'event.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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3d2675e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,29 @@ +# Django +django===3.2.10 +django-extensions===3.1.3 +# PostgreSQL driver +psycopg2===2.9.1 +# Servidor para Django con Websockets +uvicorn===0.13.4 +websockets===9.1 +# Channels +channels==2.4.0 +asgiref===3.3.4 +# Conector de Redis para Channels +channels_redis===3.2.0 +# Django REST framework +djangorestframework +markdown +django-filter +# Template +## Componentes - https://mitchel.me/slippers/ +slippers +# WYSIWYG editor Python Django admin +django-tinymce===3.3.0 +# Testing +pytest-django +pytest +# Pillow +Pillow===8.2.0 +# Linter +black