This commit is contained in:
2024-07-20 11:36:35 +02:00
commit 74b43cd8fa
81 changed files with 3131 additions and 0 deletions

30
.env.sample Normal file
View File

@@ -0,0 +1,30 @@
# should be prod or dev
ENV='dev'
# secret
SECRET='CHANGE_ME'
# Allowed hosts, domain's allowed, comma separated
ALLOWED_HOSTS='127.0.0.1, localhost'
# CSRF Trusted origins
CSRF_TRUSTED_ORIGINS='http://127.0.0.1, http://localhost'
### DEV RELATED
# VITE
DEV_SERVER_HOST='127.0.0.1'
DEV_SERVER_PORT='8080'
### DB related
# db engine, should be 'sqlite', 'pgsql' (psycopg[binary] required)
DB_ENGINE='sqlite3'
# SQLITE
SQLITE_REL_PATH='db.sqlite3'
# PostgreSQL, docker compose .env file convention
#POSTGRES_HOST=localhost
#POSTGRES_PORT=5432
#POSTGRES_DB=changeme
#POSTGRES_USER=changeme
#POSTGRES_PASSWORD=changeme

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.idea/
db.sqlite3
.env

21
README.md Normal file
View File

@@ -0,0 +1,21 @@
## First run
* install https://nodejs.org/en (tested with version 20)
* install https://www.python.org/ (tested with version 3.12)
* `cp .env.sample .env`
* modify .env
* follow requirements sections
## Requirements python
* ### for dev : `pip install -r requirements/dev.txt`
* ### for prod : `pip install -r requirements/prod.txt`
## Requirements nodejs
* ### Linux `cd frontend && npm install && cd -`
* ### Windows `pushd .; cd frontend; npm install; popd`
## Run project
* ### for dev :
* in base dir `python manage.py runserver`
* in frontend dir `npm run dev`
* ### for prod : in frontend dir `npm run build`

BIN
_static/images/no_image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 KiB

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="600px" height="400px" viewBox="0.5 0.5 600 400" enable-background="new 0.5 0.5 600 400" xml:space="preserve">
<g>
<path fill="#454545" d="M43.261,165.3V76.771h12.478l28.372,44.79c6.567,10.376,11.69,19.702,15.893,28.765l0.263-0.132 c-1.051-11.821-1.314-22.592-1.314-36.384V76.77h10.77v88.53H98.165l-28.108-44.921c-6.173-9.851-12.084-19.965-16.55-29.553 l-0.394,0.131c0.657,11.165,0.92,21.804,0.92,36.515V165.3H43.261z"/>
<path fill="#454545" d="M186.822,132.988c0,23.511-16.287,33.757-31.655,33.757c-17.207,0-30.473-12.61-30.473-32.707 c0-21.278,13.923-33.756,31.524-33.756C174.475,100.282,186.822,113.549,186.822,132.988z M136.384,133.645 c0,13.923,8.012,24.431,19.308,24.431c11.033,0,19.308-10.377,19.308-24.694c0-10.77-5.385-24.43-19.045-24.43 S136.384,121.561,136.384,133.645z"/>
<path fill="#454545" d="M242.116,83.864c0.131,3.94-2.758,7.093-7.355,7.093c-4.072,0-6.962-3.153-6.962-7.093 c0-4.072,3.021-7.225,7.224-7.225C239.358,76.64,242.116,79.792,242.116,83.864z M229.244,165.3v-63.573h11.558V165.3H229.244z"/>
<path fill="#454545" d="M259.978,118.934c0-6.568-0.132-11.953-0.526-17.207h10.114l0.526,10.245h0.394 c3.546-6.042,9.457-11.69,19.965-11.69c8.67,0,15.237,5.254,17.996,12.741h0.262c1.97-3.546,4.466-6.305,7.093-8.274 c3.809-2.89,8.012-4.466,14.055-4.466c8.406,0,20.884,5.517,20.884,27.584V165.3h-11.296v-35.989 c0-12.216-4.466-19.571-13.791-19.571c-6.568,0-11.69,4.86-13.661,10.508c-0.525,1.576-0.919,3.678-0.919,5.779V165.3h-11.296 v-38.091c0-10.114-4.467-17.469-13.267-17.469c-7.225,0-12.478,5.779-14.317,11.558c-0.657,1.708-0.919,3.678-0.919,5.648V165.3 h-11.296v-46.366C259.979,118.934,259.978,118.934,259.978,118.934z"/>
<path fill="#454545" d="M404.722,165.3l-0.919-8.012h-0.393c-3.548,4.991-10.377,9.457-19.44,9.457 c-12.873,0-19.44-9.064-19.44-18.258c0-15.368,13.66-23.774,38.223-23.643v-1.314c0-5.254-1.445-14.711-14.449-14.711 c-5.91,0-12.083,1.839-16.55,4.729l-2.626-7.618c5.253-3.415,12.872-5.648,20.884-5.648c19.44,0,24.168,13.266,24.168,26.007 v23.774c0,5.517,0.262,10.902,1.051,15.237L404.722,165.3L404.722,165.3z M403.015,132.857 c-12.609-0.263-26.927,1.97-26.927,14.317c0,7.487,4.992,11.033,10.902,11.033c8.276,0,13.529-5.254,15.368-10.639 c0.394-1.182,0.657-2.496,0.657-3.678L403.015,132.857L403.015,132.857z"/>
<path fill="#454545" d="M487.602,101.727c-0.262,4.597-0.525,9.72-0.525,17.469v36.909c0,14.579-2.891,23.512-9.063,29.028 c-6.173,5.779-15.106,7.618-23.118,7.618c-7.618,0-16.024-1.839-21.148-5.254l2.891-8.799c4.203,2.626,10.77,4.991,18.651,4.991 c11.821,0,20.49-6.173,20.49-22.197v-7.093h-0.262c-3.546,5.911-10.377,10.64-20.227,10.64c-15.762,0-27.057-13.398-27.057-30.999 c0-21.541,14.054-33.756,28.633-33.756c11.033,0,17.075,5.779,19.834,11.033h0.262l0.525-9.589L487.602,101.727L487.602,101.727z M475.649,126.815c0-1.97-0.132-3.678-0.657-5.254c-2.102-6.699-7.75-12.216-16.156-12.216c-11.033,0-18.915,9.326-18.915,24.037 c0,12.478,6.305,22.855,18.783,22.855c7.093,0,13.529-4.466,16.025-11.821c0.657-1.97,0.919-4.203,0.919-6.173v-11.428 C475.648,126.815,475.649,126.815,475.649,126.815z"/>
<path fill="#454545" d="M512.686,135.615c0.264,15.63,10.246,22.066,21.804,22.066c8.274,0,13.266-1.445,17.601-3.284l1.969,8.275 c-4.071,1.839-11.032,3.941-21.146,3.941c-19.572,0-31.261-12.872-31.261-32.049s11.296-34.282,29.815-34.282 c20.753,0,26.27,18.257,26.27,29.948c0,2.364-0.263,4.203-0.394,5.385L512.686,135.615L512.686,135.615z M546.575,127.34 c0.131-7.356-3.021-18.783-16.025-18.783c-11.69,0-16.813,10.77-17.732,18.783H546.575z"/>
<path fill="#454545" d="M100.266,322.915l-0.919-8.012h-0.394c-3.546,4.991-10.377,9.457-19.44,9.457 c-12.872,0-19.439-9.063-19.439-18.258c0-15.367,13.66-23.773,38.222-23.643v-1.314c0-5.254-1.445-14.71-14.449-14.71 c-5.91,0-12.084,1.839-16.55,4.728l-2.627-7.618c5.254-3.416,12.873-5.649,20.884-5.649c19.44,0,24.168,13.266,24.168,26.008 v23.773c0,5.517,0.263,10.902,1.051,15.237L100.266,322.915L100.266,322.915z M98.559,290.472 c-12.609-0.262-26.927,1.97-26.927,14.317c0,7.487,4.991,11.033,10.902,11.033c8.274,0,13.529-5.254,15.368-10.639 c0.394-1.183,0.657-2.495,0.657-3.678C98.559,301.505,98.559,290.472,98.559,290.472z"/>
<path fill="#454545" d="M131.789,259.343l12.478,35.726c2.102,5.779,3.809,11.033,5.122,16.288h0.394 c1.445-5.254,3.283-10.508,5.385-16.288l12.347-35.726h12.084l-24.956,63.572H143.61l-24.168-63.572H131.789z"/>
<path fill="#454545" d="M225.175,322.915l-0.919-8.012h-0.394c-3.546,4.991-10.377,9.457-19.44,9.457 c-12.872,0-19.439-9.063-19.439-18.258c0-15.367,13.66-23.773,38.222-23.643v-1.314c0-5.254-1.445-14.71-14.449-14.71 c-5.91,0-12.084,1.839-16.55,4.728l-2.627-7.618c5.254-3.416,12.873-5.649,20.884-5.649c19.44,0,24.168,13.266,24.168,26.008 v23.773c0,5.517,0.263,10.902,1.051,15.237L225.175,322.915L225.175,322.915z M223.468,290.472 c-12.609-0.262-26.927,1.97-26.927,14.317c0,7.487,4.991,11.033,10.902,11.033c8.274,0,13.529-5.254,15.368-10.639 c0.394-1.183,0.657-2.495,0.657-3.678L223.468,290.472L223.468,290.472z"/>
<path fill="#454545" d="M266.154,241.479c0.131,3.941-2.758,7.093-7.355,7.093c-4.072,0-6.961-3.152-6.961-7.093 c0-4.072,3.021-7.225,7.224-7.225C263.396,234.255,266.154,237.407,266.154,241.479z M253.282,322.915v-63.572h11.558v63.572 H253.282z"/>
<path fill="#454545" d="M284.016,229.657h11.558v93.258h-11.558V229.657z"/>
<path fill="#454545" d="M350.214,322.915l-0.919-8.012h-0.393c-3.546,4.991-10.377,9.457-19.44,9.457 c-12.873,0-19.44-9.063-19.44-18.258c0-15.367,13.661-23.773,38.223-23.643v-1.314c0-5.254-1.445-14.71-14.449-14.71 c-5.91,0-12.083,1.839-16.55,4.728l-2.626-7.618c5.253-3.416,12.872-5.649,20.884-5.649c19.44,0,24.168,13.266,24.168,26.008 v23.773c0,5.517,0.262,10.902,1.051,15.237L350.214,322.915L350.214,322.915z M348.507,290.472 c-12.609-0.262-26.927,1.97-26.927,14.317c0,7.487,4.992,11.033,10.902,11.033c8.276,0,13.529-5.254,15.368-10.639 c0.394-1.183,0.657-2.495,0.657-3.678L348.507,290.472L348.507,290.472z"/>
<path fill="#454545" d="M377.796,322.915c0.262-4.335,0.525-10.771,0.525-16.418v-76.84h11.428v39.93h0.262 c4.073-7.093,11.428-11.69,21.673-11.69c15.762,0,26.927,13.134,26.795,32.443c0,22.722-14.316,34.019-28.503,34.019 c-9.194,0-16.549-3.548-21.278-11.954h-0.394l-0.525,10.508h-9.983V322.915z M389.749,297.434c0,1.444,0.262,2.89,0.525,4.203 c2.233,8.012,8.932,13.529,17.339,13.529c12.083,0,19.308-9.852,19.308-24.43c0-12.741-6.568-23.643-18.914-23.643 c-7.881,0-15.237,5.385-17.601,14.185c-0.262,1.314-0.657,2.89-0.657,4.728L389.749,297.434L389.749,297.434z"/>
<path fill="#454545" d="M453.055,229.657h11.56v93.258h-11.56V229.657z"/>
<path fill="#454545" d="M490.488,293.23c0.264,15.63,10.246,22.067,21.804,22.067c8.274,0,13.266-1.445,17.601-3.284l1.969,8.274 c-4.071,1.839-11.032,3.941-21.146,3.941c-19.572,0-31.261-12.872-31.261-32.049s11.296-34.282,29.815-34.282 c20.753,0,26.27,18.258,26.27,29.947c0,2.365-0.262,4.203-0.394,5.386L490.488,293.23L490.488,293.23z M524.377,284.955 c0.131-7.355-3.021-18.782-16.025-18.782c-11.69,0-16.813,10.77-17.732,18.782H524.377z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.2 KiB

0
app/__init__.py Normal file
View File

16
app/asgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
ASGI config for app 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/5.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
application = get_asgi_application()

166
app/settings.py Normal file
View File

@@ -0,0 +1,166 @@
"""
Django settings for app project.
Generated by 'django-admin startproject' using Django 5.0.4.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""
from pathlib import Path
from dotenv import load_dotenv
from os import getenv
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(BASE_DIR / ".env")
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = getenv("SECRET", "not_secure")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = getenv("ENV", "prod") == "dev"
ALLOWED_HOSTS = [host.strip() for host in getenv("ALLOWED_HOSTS").split(",") if host]
CSRF_TRUSTED_ORIGINS = [host.strip() for host in getenv("CSRF_TRUSTED_ORIGINS").split(",") if host]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
"corsheaders",
"django_vite",
"user",
"home",
"blog"
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
"corsheaders.middleware.CorsMiddleware",
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'app.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'app.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
match getenv("DB_ENGINE"):
case "pgsql":
DB = {
"ENGINE": "django.db.backends.postgresql",
"NAME": getenv("DB_NAME"),
"USER": getenv("DB_USER"),
"PASSWORD": getenv("DB_PASSWORD"),
"HOST": getenv("DB_HOST", "localhost"),
"PORT": int(getenv("DB_PORT", 5432)),
}
case _:
DB = {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / getenv("SQLITE_REL_PATH", "db.sqlite3"),
}
DATABASES = {
'default': DB
}
# Password validation
# https://docs.djangoproject.com/en/5.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/5.0/topics/i18n/
LANGUAGE_CODE = 'fr-fr'
TIME_ZONE = 'Europe/Paris'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
STATIC_URL = 'static/'
STATICFILES_DIRS = []
# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL = "user.User"
LOGIN_REDIRECT_URL = "/"
LOGIN_URL = "/user/login/"
LOGOUT_REDIRECT_URL = "/user/login/"
SESSION_COOKIE_AGE = 60 * 60 * 24 * 30
DJANGO_VITE = {
"default": {
"dev_mode": DEBUG,
"manifest_path": BASE_DIR / "frontend/dist/manifest.json",
"dev_server_host": getenv("DEV_SERVER_HOST", "localhost"),
"dev_server_port": int(getenv("DEV_SERVER_PORT", 8080)),
}
}
STATICFILES_DIRS += BASE_DIR / "frontend/dist",

25
app/urls.py Normal file
View File

@@ -0,0 +1,25 @@
"""
URL configuration for app project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.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, include
urlpatterns = [
path('admin/', admin.site.urls),
path("", include("home.urls", namespace="home")),
path("blog/", include("blog.urls", namespace="blog")),
]

16
app/wsgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
WSGI config for app 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/5.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
application = get_wsgi_application()

0
blog/__init__.py Normal file
View File

26
blog/admin.py Normal file
View File

@@ -0,0 +1,26 @@
from django.contrib import admin
from ordered_model.admin import OrderedModelAdmin
from .models import Article, Tag, Category
from .forms import ArticleForm
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
form = ArticleForm
list_display = ('title', 'description', 'author')
exclude = ["author"]
def save_model(self, request, obj, form, change):
obj.author = request.user
return super().save_model(request, obj, form, change)
@admin.register(Category)
class CategoryAdmin(OrderedModelAdmin):
list_display = ('name', 'move_up_down_links')
admin.site.register(Tag)

6
blog/apps.py Normal file
View File

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

13
blog/forms.py Normal file
View File

@@ -0,0 +1,13 @@
from django import forms
from tinymce.widgets import TinyMCE
from blog.models import Article
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
exclude = "__all__"
widgets = {
"content": TinyMCE()
}

View File

@@ -0,0 +1,32 @@
{% extends "blog/layout.html" %}
{% block css %}
{{ super() }}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/toolbar/prism-toolbar.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/line-numbers/prism-line-numbers.min.css">
{% endblock %}
{% block js %}
{{ super() }}
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-core.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/line-numbers/prism-line-numbers.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/toolbar/prism-toolbar.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/show-language/prism-show-language.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/copy-to-clipboard/prism-copy-to-clipboard.min.js"></script>
{% endblock %}
{% block content %}
<article>
<header>
<h2>
{{ article.title }}
</h2>
</header>
<p>{{ article.description.replace("\n", "<br />")|safe }}</p>
<div class="blog_content line-numbers">
{{ article.content|safe }}
</div>
</article>
{% endblock %}

View File

@@ -0,0 +1,34 @@
{% extends "blog/layout.html" %}
{% block content %}
<div id="article_list">
{% for article in articles %}
<a href="{{ url("blog:article_detail", args=[article.pk, slugify(article.title)]) }}">
<article>
{% set article_image %}
<section class="article_image">
{# <img src="{{ static("images/no_mage_600_x_400.svg") }}">#}
<img src="{{ static("images/no_image.png") }}" alt="No image"/>
</section>
{% endset %}
{% set article_text %}
<section class="article_text">
<header>
<h2>{{ article.title }}</h2>
</header>
<p>{{ article.description.replace("\n", "<br/>")|safe }}</p>
</section>
{% endset %}
{{ article_image }}
{{ article_text }}
</article>
</a>
{% else %}
<p>Aucun article</p>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block css %}
{{ super() }}
{{ stylesheet_pack("blog") }}
{% endblock %}
{% block js %}
{{ super() }}
{{ javascript_pack("blog") }}
{% endblock %}

View File

@@ -0,0 +1,54 @@
# Generated by Django 5.0.7 on 2024-07-19 08:31
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Article',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('description', models.TextField()),
('content', models.TextField()),
('date_created', models.DateTimeField(auto_now_add=True)),
('date_modified', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='Category',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order', models.PositiveIntegerField(db_index=True, editable=False, verbose_name='order')),
('name', models.CharField(max_length=255)),
],
options={
'verbose_name_plural': 'Categories',
'ordering': ('order',),
'abstract': False,
},
),
migrations.CreateModel(
name='Comment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField()),
('date_created', models.DateTimeField(auto_now_add=True)),
('date_modified', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='Tag',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
],
),
]

View File

@@ -0,0 +1,48 @@
# Generated by Django 5.0.7 on 2024-07-19 08:31
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('blog', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='article',
name='author',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='article',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category'),
),
migrations.AddField(
model_name='comment',
name='article',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article'),
),
migrations.AddField(
model_name='comment',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='comment',
name='response_to',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.comment'),
),
migrations.AddField(
model_name='article',
name='tags',
field=models.ManyToManyField(blank=True, to='blog.tag'),
),
]

View File

44
blog/models.py Normal file
View File

@@ -0,0 +1,44 @@
from django.db import models
from ordered_model.models import OrderedModel
class Article(models.Model):
title = models.CharField(max_length=255)
description = models.TextField()
content = models.TextField()
date_created = models.DateTimeField(auto_now_add=True)
date_modified = models.DateTimeField(auto_now=True)
category = models.ForeignKey("Category", on_delete=models.CASCADE, null=True, blank=True)
tags = models.ManyToManyField("Tag", blank=True)
author = models.ForeignKey("user.User", on_delete=models.CASCADE, null=True, blank=True)
def __str__(self):
return self.title
class Category(OrderedModel):
name = models.CharField(max_length=255)
class Meta(OrderedModel.Meta):
verbose_name_plural = "Categories"
def __str__(self):
return self.name
class Tag(models.Model):
name = models.CharField(max_length=255)
def __str__(self):
return self.name
class Comment(models.Model):
author = models.ForeignKey("user.User", on_delete=models.CASCADE)
article = models.ForeignKey("Article", on_delete=models.CASCADE)
content = models.TextField()
date_created = models.DateTimeField(auto_now_add=True)
date_modified = models.DateTimeField(auto_now=True)
response_to = models.ForeignKey("self", on_delete=models.CASCADE, null=True, blank=True)

View File

@@ -0,0 +1,32 @@
{% extends "blog/layout.html" %}
{% block css %}
{{ super() }}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/toolbar/prism-toolbar.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/line-numbers/prism-line-numbers.min.css">
{% endblock %}
{% block js %}
{{ super() }}
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-core.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/line-numbers/prism-line-numbers.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/toolbar/prism-toolbar.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/show-language/prism-show-language.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/copy-to-clipboard/prism-copy-to-clipboard.min.js"></script>
{% endblock %}
{% block content %}
<article>
<header>
<h2>
{{ article.title }}
</h2>
</header>
<p>{{ article.description.replace("\n", "<br />")|safe }}</p>
<div class="blog_content line-numbers">
{{ article.content|safe }}
</div>
</article>
{% endblock %}

View File

@@ -0,0 +1,34 @@
{% extends "blog/layout.html" %}
{% load static %}
{% block content %}
<div id="article_list">
{% for article in articles %}
<a href="{% url "blog:article_detail" article.pk article.title|slugify %}">
<article>
{# {% set article_image %}#}
{# <section class="article_image">#}
{# <img src="{{ static("images/no_mage_600_x_400.svg") }}">#}
{# <img src="{% static "images/no_image.png" %}" alt="No image"/>#}
{# </section>#}
{# {% endset %}#}
{##}
{# {% set article_text %}#}
{# <section class="article_text">#}
{# <header>#}
{# <h2>{{ article.title }}</h2>#}
{# </header>#}
{# <p>{{ article.description|safe|linebreaks }}</p>#}
{# </section>#}
{# {% endset %}#}
{# {{ article_image }}#}
{# {{ article_text }}#}
</article>
</a>
{% empty %}
<p>Aucun article</p>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block css %}
{{ super }}
{# {{ stylesheet_pack("blog") }}#}
{% endblock %}
{% block js %}
{{ super }}
{# {{ javascript_pack("blog") }}#}
{% endblock %}

3
blog/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

9
blog/urls.py Normal file
View File

@@ -0,0 +1,9 @@
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
path("", views.ArticleListView.as_view(), name="article_list"),
path("<int:pk>-<str:slug>/", views.ArticleDetailView.as_view(), name="article_detail"),
]

15
blog/views.py Normal file
View File

@@ -0,0 +1,15 @@
from django.views.generic import ListView, DetailView
from blog.models import Article
class ArticleListView(ListView):
model = Article
template_name = 'blog/article_list.html'
context_object_name = 'articles'
class ArticleDetailView(DetailView):
model = Article
template_name = 'blog/article_detail.html'
context_object_name = 'article'

24
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

1098
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
frontend/package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"autoprefixer": "^10.4.19",
"normalize.css": "^8.0.1",
"postcss": "^8.4.38",
"postcss-import": "^16.1.0",
"postcss-simple-vars": "^7.0.1",
"vite": "^5.2.0"
}
}

View File

@@ -0,0 +1,7 @@
module.exports = (ctx) => ({
plugins: [
require('postcss-import')(),
require('postcss-simple-vars')(),
require("autoprefixer")(),
]
})

View File

@@ -0,0 +1,35 @@
import 'vite/modulepreload-polyfill'
// import "../styles/app.css"
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
(function(){
// DOM is ready.
setTimeout(async () => {
const response = await fetch("/hit/", {
method: "POST",
headers: {
"Content-Type": "application/json",
'X-CSRFToken': getCookie("csrftoken")
},
mode: 'same-origin',
body: JSON.stringify({
})
});
}, 1000)
})();

View File

@@ -0,0 +1,5 @@
import "../components/sidebar";
window.document.addEventListener("DOMContentLoaded", function () {
window.console.log("dom ready 2");
});

View File

@@ -0,0 +1,2 @@
import "../styles/blog.css"

View File

@@ -0,0 +1 @@
// import "../styles/cv.css"

View File

@@ -0,0 +1 @@
window.console.log("sidebar is loaded");

View File

@@ -0,0 +1,48 @@
@import "normalize.css";
html::-webkit-scrollbar {
display: none;
}
html{
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
body{
background-color: rgba(43, 43, 42, 0.8);
overflow-y: scroll;
}
ul{
list-style-type: none;
}
#main_header{
background-color: white;
margin-top: 0;
}
#main_header nav{
max-width: 90%;
margin: 0 auto;
padding: 0;
}
#main_header ul{
margin-top: 0;
display: flex;
justify-content: center;
}
#main_header li {
padding: 1rem;
}
#main_app{
max-width: 90%;
min-height: 50rem;
margin: 0 auto;
padding: 1rem;
background-color: white;
}

View File

@@ -0,0 +1,38 @@
/* hello */
#article_list{
/*background-color: grey;*/
}
#article_list article{
/*background-color: green;*/
display: flex;
border: 0.1rem solid green;
margin: 1rem;
/*height: 10rem;*/
}
#article_list a{
text-decoration: none;
color: black;
}
.article_image{
width: 30%;
background-color: grey;
}
.article_image img{
/*image-;*/
width: 100%;
height: 100%;
}
.article_text{
width: 70%;
margin: 1rem;
}
.article_text h2{
text-align: center;
}

150
frontend/src/styles/cv.css Normal file
View File

@@ -0,0 +1,150 @@
#cv_main{
display: flex;
justify-content: normal;
flex-direction: row;
}
#cv_main > #cv_left{
background-color: beige;
max-width: 30%;
padding: 1rem;
}
#cv_main > #cv_right{
background-color: cadetblue;
min-width: 70%;
padding: 1rem;
}
article header{
display: flex;
flex-direction: row;
align-items: center;
background-color: #e0e0e0;
justify-content: center;
position: relative;
}
article header>div:first-child{
margin-left: 1rem;
position: absolute;
left: 0;
text-align: center;
}
.cv_header_title{
margin: auto;
display: flex;
flex-direction: column;
align-items: center;
}
.cv_header_title>*{
padding: 0;
margin: 0.1rem;
}
.cv_content_competence{
max-width: 30%;
display: grid;
grid-template-columns: repeat(4, 2fr);
column-gap: 1rem;
row-gap: 1rem;
}
/*article header{*/
/* display: flex;*/
/* flex-direction: row;*/
/* align-items: center;*/
/* background-color: #e0e0e0;*/
/* justify-content: center;*/
/* position: relative;*/
/*}*/
/*article header>div:first-child{*/
/* margin-left: 1rem;*/
/*}*/
/*.cv_header_title{*/
/* margin: auto;*/
/* display: flex;*/
/* flex-direction: column;*/
/* align-items: center;*/
/*}*/
/*.cv_header_title>*{*/
/* padding: 0;*/
/* margin: 0.1rem;*/
/*}*/
/*.g_pas_idee {*/
/* position: absolute;*/
/* left: 0;*/
/* background-color: red;*/
/* text-align: center;*/
/*}*/
/*@media screen and (max-width: 700px) {*/
/* .g_pas_idee {*/
/* position: inherit;*/
/* width: 100%;*/
/* }*/
/* article header{*/
/* flex-direction: column;*/
/* }*/
/*}*/
/*article header{*/
/* display: grid;*/
/* grid-template-columns: 3fr 5fr;*/
/* align-items: center;*/
/* background-color: #e0e0e0;*/
/*}*/
/*article header>div:first-child{*/
/* margin-left: 1rem;*/
/*}*/
/*.cv_header_title{*/
/* text-align: center;*/
/* margin-right: auto;*/
/*}*/
/*.cv_header_title>h2,h3{*/
/* margin: .5rem;*/
/*}*/
/*article header>div{*/
/* max-width: 60%;*/
/* text-align: center;*/
/*}*/
/*.cv_header_title{*/
/* display: flex;*/
/* flex-direction: column;*/
/*}*/
.cv_content h4 {
text-decoration: underline;
}
@media (max-width: 800px) {
#cv_main{
flex-direction: column;
}
#cv_main > #cv_left{
max-width: 90%;
}
#cv_main > #cv_right{
min-width: 90%;
padding: 1rem;
}
}

View File

@@ -0,0 +1,17 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
//.jumbotron {
// // should be relative path of the entry scss file
// background-image: url("../../vendors/images/sample.jpg");
// background-size: cover;
//}
//
//.btn-blue {
// @apply inline-block px-4 py-2;
// @apply font-semibold rounded-lg shadow-md;
// @apply bg-blue-500 text-white;
// @apply hover:bg-blue-700 focus:outline-none;
// @apply focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75;
//}

49
frontend/vite.config.js Normal file
View File

@@ -0,0 +1,49 @@
import {defineConfig, loadEnv} from "vite";
import {resolve, join} from "path";
// const postcssConfig = {
// plugins: [
// require('postcss-import')(),
// require('postcss-simple-vars')(),
// require('autoprefixer')(),
// ],
// };
export default defineConfig((mode) => {
const env = loadEnv(mode, "..", ""),
SRC_DIR = resolve("./src"),
OUT_DIR = resolve("./dist")
return {
plugins: [
],
resolve: {
alias: {
"@": resolve(SRC_DIR)
}
},
root: SRC_DIR,
base: "/static/",
// css: {
// postcss: postcssConfig
// },
server: {
host: env.DEV_SERVER_HOST,
port: env.DEV_SERVER_PORT
},
build: {
manifest: "manifest.json",
emptyOutDir: true,
outDir: OUT_DIR,
rollupOptions: {
input: {
app: join(SRC_DIR, "application/app.js"),
app2: join(SRC_DIR, "application/app2.js"),
blog: join(SRC_DIR, "application/blog.js"),
cv: join(SRC_DIR, "application/cv.js")
}
}
}
}
})

241
frontend/yarn.lock Normal file
View File

@@ -0,0 +1,241 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@esbuild/win32-x64@0.20.2":
version "0.20.2"
resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz"
integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==
"@rollup/rollup-win32-x64-msvc@4.17.2":
version "4.17.2"
resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz"
integrity sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==
"@types/estree@1.0.5":
version "1.0.5"
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz"
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
autoprefixer@^10.4.19:
version "10.4.19"
resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz"
integrity sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==
dependencies:
browserslist "^4.23.0"
caniuse-lite "^1.0.30001599"
fraction.js "^4.3.7"
normalize-range "^0.1.2"
picocolors "^1.0.0"
postcss-value-parser "^4.2.0"
browserslist@^4.23.0, "browserslist@>= 4.21.0":
version "4.23.0"
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz"
integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==
dependencies:
caniuse-lite "^1.0.30001587"
electron-to-chromium "^1.4.668"
node-releases "^2.0.14"
update-browserslist-db "^1.0.13"
caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001599:
version "1.0.30001617"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001617.tgz"
integrity sha512-mLyjzNI9I+Pix8zwcrpxEbGlfqOkF9kM3ptzmKNw5tizSyYwMe+nGLTqMK9cO+0E+Bh6TsBxNAaHWEM8xwSsmA==
electron-to-chromium@^1.4.668:
version "1.4.762"
resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.762.tgz"
integrity sha512-rrFvGweLxPwwSwJOjIopy3Vr+J3cIPtZzuc74bmlvmBIgQO3VYJDvVrlj94iKZ3ukXUH64Ex31hSfRTLqvjYJQ==
esbuild@^0.20.1:
version "0.20.2"
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz"
integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==
optionalDependencies:
"@esbuild/aix-ppc64" "0.20.2"
"@esbuild/android-arm" "0.20.2"
"@esbuild/android-arm64" "0.20.2"
"@esbuild/android-x64" "0.20.2"
"@esbuild/darwin-arm64" "0.20.2"
"@esbuild/darwin-x64" "0.20.2"
"@esbuild/freebsd-arm64" "0.20.2"
"@esbuild/freebsd-x64" "0.20.2"
"@esbuild/linux-arm" "0.20.2"
"@esbuild/linux-arm64" "0.20.2"
"@esbuild/linux-ia32" "0.20.2"
"@esbuild/linux-loong64" "0.20.2"
"@esbuild/linux-mips64el" "0.20.2"
"@esbuild/linux-ppc64" "0.20.2"
"@esbuild/linux-riscv64" "0.20.2"
"@esbuild/linux-s390x" "0.20.2"
"@esbuild/linux-x64" "0.20.2"
"@esbuild/netbsd-x64" "0.20.2"
"@esbuild/openbsd-x64" "0.20.2"
"@esbuild/sunos-x64" "0.20.2"
"@esbuild/win32-arm64" "0.20.2"
"@esbuild/win32-ia32" "0.20.2"
"@esbuild/win32-x64" "0.20.2"
escalade@^3.1.2:
version "3.1.2"
resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz"
integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==
fraction.js@^4.3.7:
version "4.3.7"
resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz"
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
hasown@^2.0.0:
version "2.0.2"
resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz"
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
dependencies:
function-bind "^1.1.2"
is-core-module@^2.13.0:
version "2.13.1"
resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz"
integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==
dependencies:
hasown "^2.0.0"
nanoid@^3.3.7:
version "3.3.7"
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
node-releases@^2.0.14:
version "2.0.14"
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz"
integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==
normalize-range@^0.1.2:
version "0.1.2"
resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz"
integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==
normalize.css@^8.0.1:
version "8.0.1"
resolved "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz"
integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==
path-parse@^1.0.7:
version "1.0.7"
resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
pify@^2.3.0:
version "2.3.0"
resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz"
integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
postcss-import@^16.1.0:
version "16.1.0"
resolved "https://registry.npmjs.org/postcss-import/-/postcss-import-16.1.0.tgz"
integrity sha512-7hsAZ4xGXl4MW+OKEWCnF6T5jqBw80/EE9aXg1r2yyn1RsVEU8EtKXbijEODa+rg7iih4bKf7vlvTGYR4CnPNg==
dependencies:
postcss-value-parser "^4.0.0"
read-cache "^1.0.0"
resolve "^1.1.7"
postcss-simple-vars@^7.0.1:
version "7.0.1"
resolved "https://registry.npmjs.org/postcss-simple-vars/-/postcss-simple-vars-7.0.1.tgz"
integrity sha512-5GLLXaS8qmzHMOjVxqkk1TZPf1jMqesiI7qLhnlyERalG0sMbHIbJqrcnrpmZdKCLglHnRHoEBB61RtGTsj++A==
postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
version "4.2.0"
resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@^8.0.0, postcss@^8.1.0, postcss@^8.2.1, postcss@^8.4.38:
version "8.4.38"
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz"
integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
dependencies:
nanoid "^3.3.7"
picocolors "^1.0.0"
source-map-js "^1.2.0"
read-cache@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz"
integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==
dependencies:
pify "^2.3.0"
resolve@^1.1.7:
version "1.22.8"
resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz"
integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
dependencies:
is-core-module "^2.13.0"
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
rollup@^4.13.0:
version "4.17.2"
resolved "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz"
integrity sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==
dependencies:
"@types/estree" "1.0.5"
optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.17.2"
"@rollup/rollup-android-arm64" "4.17.2"
"@rollup/rollup-darwin-arm64" "4.17.2"
"@rollup/rollup-darwin-x64" "4.17.2"
"@rollup/rollup-linux-arm-gnueabihf" "4.17.2"
"@rollup/rollup-linux-arm-musleabihf" "4.17.2"
"@rollup/rollup-linux-arm64-gnu" "4.17.2"
"@rollup/rollup-linux-arm64-musl" "4.17.2"
"@rollup/rollup-linux-powerpc64le-gnu" "4.17.2"
"@rollup/rollup-linux-riscv64-gnu" "4.17.2"
"@rollup/rollup-linux-s390x-gnu" "4.17.2"
"@rollup/rollup-linux-x64-gnu" "4.17.2"
"@rollup/rollup-linux-x64-musl" "4.17.2"
"@rollup/rollup-win32-arm64-msvc" "4.17.2"
"@rollup/rollup-win32-ia32-msvc" "4.17.2"
"@rollup/rollup-win32-x64-msvc" "4.17.2"
fsevents "~2.3.2"
source-map-js@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz"
integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
supports-preserve-symlinks-flag@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
update-browserslist-db@^1.0.13:
version "1.0.15"
resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.15.tgz"
integrity sha512-K9HWH62x3/EalU1U6sjSZiylm9C8tgq2mSvshZpqc7QE69RaA2qjhkW2HlNA0tFpEbtyFz7HTqbSdN4MSwUodA==
dependencies:
escalade "^3.1.2"
picocolors "^1.0.0"
vite@^5.2.0:
version "5.2.11"
resolved "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz"
integrity sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==
dependencies:
esbuild "^0.20.1"
postcss "^8.4.38"
rollup "^4.13.0"
optionalDependencies:
fsevents "~2.3.3"

0
home/__init__.py Normal file
View File

3
home/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
home/apps.py Normal file
View File

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

8
home/forms.py Normal file
View File

@@ -0,0 +1,8 @@
from django import forms
class ContactForm(forms.Form):
full_name = forms.CharField(max_length=255, required=True, label="Nom/Prénom ou entreprise")
email = forms.EmailField(required=True)
subject = forms.CharField(max_length=255, required=True, label="Sujet")
message = forms.CharField(widget=forms.Textarea, required=True)

24
home/middleware.py Normal file
View File

@@ -0,0 +1,24 @@
from django.middleware.csrf import rotate_token
class SessionOnHitMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if not request.user.is_authenticated and not request.session.session_key:
request.session.save()
response = self.get_response(request)
return response
class CsrfOnHitMiddleWare:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if 'CSRF_COOKIE' not in request.META:
rotate_token(request)
response = self.get_response(request)
return response

View File

@@ -0,0 +1,27 @@
# Generated by Django 5.0.7 on 2024-07-19 08:31
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Hit',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('path', models.CharField(max_length=255)),
('parameters', models.JSONField(default=dict)),
('session_id', models.CharField(max_length=255)),
('ip_address', models.GenericIPAddressField()),
('user_agent', models.CharField(max_length=255)),
('date_created', models.DateTimeField(auto_now_add=True)),
],
),
]

View File

15
home/models.py Normal file
View File

@@ -0,0 +1,15 @@
from django.db import models
import uuid
class Hit(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
path = models.CharField(max_length=255)
parameters = models.JSONField(default=dict)
session_id = models.CharField(max_length=255)
ip_address = models.GenericIPAddressField(max_length=255)
user_agent = models.CharField(max_length=255)
date_created = models.DateTimeField(auto_now_add=True)

View File

@@ -0,0 +1,28 @@
{% extends "home/layout.html" %}
{% block content %}
<form method="post" action="{% url "home:contact" %}">
{{ csrf_input }}
<div id="form_{{ form.full_name.id_for_label }}">
{{ form.full_name.errors }}
{{ form.full_name.label_tag }}
{{ form.full_name }}
</div>
<div id="form_{{ form.email.id_for_label }}">
{{ form.email.errors }}
{{ form.email.label_tag }}
{{ form.email }}
</div>
<div id="form_{{ form.subject.id_for_label }}">
{{ form.subject.errors }}
{{ form.subject.label_tag }}
{{ form.subject }}
</div>
<div id="form_{{ form.message.id_for_label }}">
{{ form.message.errors }}
{{ form.message.label_tag }}
{{ form.message }}
</div>
<input type="submit" value="Envoyer">
</form>
{% endblock %}

View File

@@ -0,0 +1,5 @@
{% extends "home/layout.html" %}
{% block content %}
{% endblock %}

204
home/templates/home/cv.html Normal file
View File

@@ -0,0 +1,204 @@
{% extends "home/layout.html" %}
{% load django_vite %}
{% block css %}
{{ super }}
<link rel="stylesheet" href="{% vite_asset_url "styles/cv.css" %}">
{% endblock %}
{% block js %}
{% vite_asset "application/cv.js" %}
{% endblock %}
{% block content %}
<div id="cv_main">
<div id="cv_left">
<div id="cv_presentation">
<h2>Fabien ROUSSEAU</h2>
<h3>Développeur Python/Django</h3>
<p>
Passionné d'informatique. J'ai décidé d'en faire mon métier en apprenant par moi-même l'électronique, le réseau puis par la suite le développement
Aujourd'hui je souhaiterais poursuivre ce parcours au sein d'une entreprise dont je partage les valeurs qui m'accompagnera dans un processus de validation et certification de mon expérience et de mes compétences
</p>
</div>
<div id="cv_contact">
<h3>Coodonnées</h3>
<ul>
<li><a href="https://www.linkedin.com/in/44-fabien-rousseau/">LinkedIN</a></li>
<li><a href="{% url "home:contact" %}">Me contacter</a></li>
</ul>
</div>
<div id="cv_competence">
<h3>Compétences</h3>
<h4>Language et framework</h4>
<ul>
<li>Python</li>
<li>Django</li>
<li>Django Rest Framework</li>
<li>Django Channels</li>
<li>Jinja</li>
<li>Celery</li>
<li>VueJS</li>
<li>NuxtJS</li>
<li>Vuetify</li>
<li>OCPP</li>
</ul>
<h4>Système d'exploitation</h4>
<ul>
<li>Linux</li>
<li>Windows</li>
</ul>
<h4>Outils</h4>
<ul>
<li>Docker</li>
<li>Git</li>
<li>Kanban</li>
</ul>
<h4>Base de données</h4>
<ul>
<li>PostgreSQL</li>
<li>MySQL</li>
<li>Redis</li>
</ul>
</div>
<div id="cv_strong">
<h3>Points forts</h3>
<ul>
<li>Passionné de développement</li>
<li>Polyvalent et adaptabilité</li>
<li>Travaille efficacement en équipe</li>
<li>Consciencieux</li>
</ul>
</div>
</div>
<div id="cv_right">
<article>
<header>
<div>
<p>Avril 2017 à Maintenant</p>
</div>
<div class="cv_header_title">
<h2>Développeur</h2>
<h3>Auto-entrepreneur - Nantes - Full remote</h3>
</div>
</header>
<div class="cv_content">
<h4>Services : </h4>
<ul>
<li>Développement applicatif</li>
<li>Conseil en informatique</li>
<li>Hébergement et maintenance web</li>
</ul>
<h4>Missions : </h4>
<ul>
<li>Site web de suivi dactualité footballistique</li>
<li>Site de suivi des résultats et statistique footballistique</li>
<li>Site dhébergement de CV pour sportifs amateurs</li>
<li>Site de gestion de formation dans le domaine des sports aquatiques</li>
<li>Conseil divers en informatique</li>
<li>Intervention en soutien pour maintenance dinfrastructure</li>
<li>intervention pour migration de sites</li>
<li>Site vitrine dentreprise denfouissement électrique et gaz</li>
<li>Mise en place de scrapping pour entreprise</li>
</ul>
<h4>Compétences technique : </h4>
<ul class="cv_content_competence">
<li>Python</li>
<li>Django</li>
<li>Django REST FRAMEWORK</li>
<li>Django Channels</li>
<li>Celery</li>
<li>PostgreSQL</li>
<li>Redis</li>
<li>VueJS</li>
<li>NuxtJS</li>
<li>Vuetify</li>
<li>Docker</li>
<li>Docker compose</li>
</ul>
</div>
</article>
<article>
<header>
<div>
<p>Octobre 2021 à Novembre 2023</p>
</div>
<div class="cv_header_title">
<h2>Lead Développeur</h2>
<h3>PredicVision - Bordeaux - Full remote</h3>
</div>
</header>
<div class="cv_content">
<h4>Objectif : </h4>
<p>
Création dune plateforme afin danalyser et maintenir un parc de bornes de rechargement pour véhicules électriques.
</p>
<h4>Compétences Techniques : </h4>
<ul class="cv_content_competence">
<li>Python</li>
<li>Django</li>
<li>Django REST FRAMEWORK</li>
<li>Django Channels</li>
<li>Celery</li>
<li>OCPP</li>
<li>PostgreSQL</li>
<li>Redis</li>
<li>VueJS</li>
<li>NuxtJS</li>
<li>Vuetify</li>
<li>Docker</li>
<li>Docker compose</li>
</ul>
<h4>Compétences supplémentaires : </h4>
<ul>
<li>Gitlab</li>
<li>Kanban</li>
<li>conseil</li>
<li>réunion</li>
<li>veille technique</li>
<li>étude de faisabilité</li>
<li>présentation</li>
</ul>
</div>
</article>
<article>
<header>
<div>
<p>Septembre 2020 à Octobre 2021</p>
</div>
<div class="cv_header_title">
<h2>Développeur Django</h2>
<h3>FMC Production - Nantes - Hybride</h3>
</div>
</header>
<div class="cv_content">
<h4>Objectif :</h4>
<p>
Maintient dune plateforme e-learning et création dune plateforme de conférence live et VOD pour le monde médical
</p>
<h4>Compétences Techniques : </h4>
<ul class="cv_content_competence">
<li>Python</li>
<li>Django</li>
<li>Django REST FRAMEWORK</li>
<li>Django Channels</li>
<li>Celery</li>
<li>PostgreSQL</li>
<li>Redis</li>
<li>Docker</li>
<li>Docker compose</li>
</ul>
<h4>Compétences supplémentaires : </h4>
<ul>
<li>Gitlab</li>
<li>Daily</li>
</ul>
</div>
</article>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,17 @@
{% extends "home/layout.html" %}
{% block content %}
<h2>Présentation</h2>
<p>
Développeur passionné d'informatique depuis le plus jeune âge.
Cette page sera un condenser d'une vitrine de moi même,
des prestations que je peux offrir
ainsi qu'une multitude de partage de connaissance à travers mon blog
</p>
<h2>En quête d'un projet sur la DATA</h2>
<p>
Ayant une expérience dans la data,
je suis en recherche d'un projet qui approfondira mes connaissances sur le sujet
avec pourquoi pas un cumul de connaissances sur le web
</p>
{% endblock %}

View File

@@ -0,0 +1 @@
{% extends "base.html" %}

View File

@@ -0,0 +1,19 @@
{% extends "home/layout.html" %}
{% block content %}
<p>
Prestation de service ou de maintenance Python/Django/Flask/FastAPI :
facturation à la journée à raison de 450€ par jour en moyenne (ce prix peut varier si c'est un projet qui dur dans le temps)
</p>
<ul>
<li>Création de script</li>
<li>Création de site simple comme complex</li>
<li>Création de graphique (chart) sur des grands comme des petits volumes de donnée</li>
<li>Mise à jour de sécurité</li>
<li>Correction de bug</li>
<li>Modification de votre projet existant</li>
<li>Migration</li>
<li>Optimisation</li>
</ul>
{% endblock %}

3
home/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

14
home/urls.py Normal file
View File

@@ -0,0 +1,14 @@
from django.urls import path
from . import views
app_name = 'home'
urlpatterns = [
path('', views.IndexView.as_view(), name='index'),
path("cv/", views.CVView.as_view(), name='cv'),
path("service/", views.ServiceView.as_view(), name='service'),
path("contact/", views.ContactView.as_view(), name='contact'),
path("contact_valid/", views.ContactValidView.as_view(), name='contact_valid'),
path("hit/", views.hit, name="hit")
]

50
home/views.py Normal file
View File

@@ -0,0 +1,50 @@
from django.views.generic import TemplateView, FormView
from django.http import HttpResponse
from ipware import get_client_ip
from urllib import parse
from .forms import ContactForm
from .models import Hit
class IndexView(TemplateView):
template_name = 'home/index.html'
class CVView(TemplateView):
template_name = 'home/cv.html'
class ServiceView(TemplateView):
template_name = "home/service.html"
class ContactView(FormView):
template_name = 'home/contact.html'
form_class = ContactForm
class ContactValidView(TemplateView):
template_name = "home/contact_success.html"
def hit(request):
if request.method == "POST":
ip_address, _ = get_client_ip(request)
ip_address = '.'.join(ip_address.split('.')[:-1]+["0"])
path_parser = parse.urlsplit(request.headers["referer"])
path = path_parser.path
parameters = dict(parse.parse_qsl(path_parser.query))
user_agent = request.headers["user-agent"]
session_id = request.session.session_key
Hit.objects.create(
path=path_parser.path,
parameters=parameters,
session_id=session_id,
ip_address=ip_address,
user_agent=user_agent,
)
return HttpResponse("")

22
manage.py Normal file
View 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', 'app.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()

3
pytest.ini Normal file
View File

@@ -0,0 +1,3 @@
[pytest]
DJANGO_SETTINGS_MODULE = app.settings
python_files = test_*

10
requirements/common.txt Normal file
View File

@@ -0,0 +1,10 @@
asgiref==3.8.*
Django==5.0.*
django-cors-headers==4.4.*
sqlparse==0.5.*
tzdata==2024.1
django-vite==3.0.*
python-dotenv==1.0.*
django-ordered-model==3.7.*
django-tinymce==4.1.*
django-ipware==7.0.*

2
requirements/dev.txt Normal file
View File

@@ -0,0 +1,2 @@
-r common.txt
pytest-django==4.8.*

1
requirements/prod.txt Normal file
View File

@@ -0,0 +1 @@
-r common.txt

45
templates/base.html Normal file
View File

@@ -0,0 +1,45 @@
{% load django_vite %}
{% load static %}
<!doctype html>
<html lang="fr">
<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">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{% block base_title %}devpanel - {% block title %}Accueil{% endblock %}{% endblock %}</title>
{% block base_css %}
<link rel="stylesheet" href="{% vite_asset_url "styles/app.css" %}">
{% block css %}
{% endblock %}
{% endblock %}
</head>
<body>
<header id="main_header">
<nav>
<ul>
<li><a href="{% url "home:index" %}">Présentation</a></li>
<li><a href="{% url "home:cv" %}">CV</a></li>
<li><a href="{% url "home:service" %}">Services</a></li>
<li><a href="{% url "blog:article_list" %}">Blog (WIP)</a></li>
<li><a href="{% url "home:contact" %}">Contact</a></li>
</ul>
</nav>
</header>
<main id="main_app">
{% block content %}
{% endblock %}
</main>
<footer id="main_footer">
</footer>
{% block base_js %}
{% vite_asset "application/app.js" %}
{% block js %}
{% endblock %}
{% endblock %}
</body>
</html>

0
tests/__init__.py Normal file
View File

0
tests/blog/__init__.py Normal file
View File

View File

0
tests/blog/test_views.py Normal file
View File

0
tests/home/__init__.py Normal file
View File

51
tests/home/test_views.py Normal file
View File

@@ -0,0 +1,51 @@
from django.test import Client
from django.urls import reverse
import pytest
from pytest_django import asserts
def test_index():
c = Client()
response = c.get(reverse('home:index'))
assert response.status_code == 200
asserts.assertTemplateUsed(response, 'home/index.html')
def test_cv():
c = Client()
response = c.get(reverse('home:cv'))
assert response.status_code == 200
asserts.assertTemplateUsed(response, 'home/cv.html')
def test_service():
c = Client()
response = c.get(reverse('home:service'))
assert response.status_code == 200
asserts.assertTemplateUsed(response, 'home/service.html')
def test_contact():
c = Client()
response = c.get(reverse('home:contact'))
assert response.status_code == 200
asserts.assertTemplateUsed(response, 'home/contact.html')
def test_contact_submit_valid():
c = Client()
data = {"full_name": "foo bar", "email": "foo@bar.com", "subject": "test", "message": "test"}
response = c.post(reverse('home:contact'), data=data)
asserts.assertRedirects(response, reverse('home:contact_valid'))
def test_contact_submit_invalid():
c = Client()
data = {"bad": "form"}
response = c.post(reverse('home:contact'), data=data)
assert response.status_code == 200

0
tests/user/__init__.py Normal file
View File

0
user/__init__.py Normal file
View File

27
user/admin.py Normal file
View File

@@ -0,0 +1,27 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import User
from .forms import UserCreationForm, UserChangeForm
@admin.register(User)
class UserAdmin(BaseUserAdmin):
add_form = UserCreationForm
form = UserChangeForm
model = User
list_display = ["email", "is_staff", "is_superuser", "is_active"]
list_filter = ["email", "is_staff", "is_superuser", "is_active"]
fieldsets = [
[None, {"fields": ["email", "password"]}],
("permissions", {"fields": ["is_staff", "is_active", "is_superuser"]})
]
add_fieldsets = [
[None, {
"classes": ["wide"],
"fields": ["email", "password1", "password2", "is_staff", "is_active", "is_superuser"]
}]
]
search_fields = ["email"]
ordering = ["email"]

6
user/apps.py Normal file
View File

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

11
user/forms.py Normal file
View File

@@ -0,0 +1,11 @@
from django.contrib.auth import forms as BaseAuthForms
from .models import User
class UserCreationForm(BaseAuthForms.UserCreationForm):
pass
class UserChangeForm(BaseAuthForms.UserChangeForm):
pass

View File

@@ -0,0 +1,44 @@
# Generated by Django 5.0.7 on 2024-07-19 08:31
import django.contrib.auth.validators
import django.utils.timezone
import user.models
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('email', models.EmailField(max_length=254, unique=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', user.models.UsernameUserManager()),
],
),
]

View File

71
user/models.py Normal file
View File

@@ -0,0 +1,71 @@
from django.db import models
from django.contrib.auth.models import AbstractUser, BaseUserManager
# class EmailUserManager(BaseUserManager):
# use_in_migrations = True
#
# def _create_user(self, email, password, **extra_fields):
# if not email:
# raise ValueError("Un email doit être défini")
#
# email = self.normalize_email(email)
# user = self.model(email=email, **extra_fields)
# user.set_password(password)
# user.save(using=self.db)
# return user
#
# def create_user(self, username, email, password=None, **extra_fields):
# extra_fields.setdefault("is_staff", False)
# extra_fields.setdefault("is_superuser", False)
# return self._create_user(username, email, password, **extra_fields)
#
# def create_superuser(self, email, password, **extra_fields):
# extra_fields.setdefault("is_staff", True)
# extra_fields.setdefault("is_superuser", True)
#
# if extra_fields.get("is_staff") is not True:
# raise ValueError("Superuser doit être staff à True")
# if extra_fields.get("is_superuser") is not True:
# raise ValueError("SuperUser doit être is_superuser à True")
#
# return self._create_user(email, password, **extra_fields)
class UsernameUserManager(BaseUserManager):
use_in_migrations = True
def _create_user(self, username, email, password, **extra_fields):
if not username:
raise ValueError("Un username doit être défini")
if not email:
raise ValueError("Un email doit être défini")
email = self.normalize_email(email)
user = self.model(username=username, email=email, **extra_fields)
user.set_password(password)
user.save(using=self.db)
return user
def create_user(self, username, email, password=None, **extra_fields):
extra_fields.setdefault("is_staff", False)
extra_fields.setdefault("is_superuser", False)
return self._create_user(username, email, password, **extra_fields)
def create_superuser(self, username, email, password, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser doit être staff à True")
if extra_fields.get("is_superuser") is not True:
raise ValueError("SuperUser doit être is_superuser à True")
return self._create_user(username, email, password, **extra_fields)
class User(AbstractUser):
email = models.EmailField(unique=True)
REQUIRED_FIELDS = []
objects = UsernameUserManager()

3
user/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
user/views.py Normal file
View File

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