diff --git a/.gitignore b/.gitignore index 4f72315..807c92a 100644 --- a/.gitignore +++ b/.gitignore @@ -163,3 +163,4 @@ cython_debug/ #.idea/ /documentation/slides/ +.idea diff --git a/documentation/00-course-plan.md b/documentation/00-course-plan.md new file mode 100644 index 0000000..5212548 --- /dev/null +++ b/documentation/00-course-plan.md @@ -0,0 +1,135 @@ +# Programme de Django Introduction + +TODO: Remplacer les images de code par des sections Markdown + + +--- + +## Découvrir Django + +- Design Pattern MVC : structure, utilisation +- Présentation de Django : paradigme, versions, documentation +- Autres frameworks web Python existants : Flask, Pylons, Pyramid etc. +- Environnements de développement intégrés +- Environnements de développement console +- Installation de Django et paquets annexes (applications Django) + +--- + +### Structure générale d'un projet Django + +- Structure générale d'un projet +- Structure générale d'une application + +Atelier : Création d'un projet et d'une application Django + +--- + +## Afficher des pages avec Django + +- Routage d'URL +- Écriture de vues +- Intrpduction aux Vues basées sur des classes +- Introduction au middleware + +--- + +## Templating (gabarits) avec Django + +- Principe du templating général +- Principe du templating avec Django +- Espaces réservés (interpolation) +- Filtres de templates +- Balises de templates (tags) et structures de contrôle +- Héritage de templates +- Balises spéciales : include, load, url et static +- Balises et filtres personnalisés + +--- + +Ateliers : + +- Créer et afficher un template dans la console +- Utiliser un contexte dans un template (+filtres) +- Utiliser des structures de contrôle +- Utiliser l'héritage et les blocs +- Découvrir les balises spéciales +- Écrire des filtres personnalisés + +--- + +## Saisir des données avec les formulaires + +- Définition de formulaires manuels +- Utilisation de formulaires +- Utilisation des formulaires dans les templates +- Valeurs par défaut des formulaires, traitement, sécurité etc. +- Validation de la saisie utilisateur +- Gestion des téléchargements et fichiers média + +--- + +## Persistance des données avec des bases de données (ORM) + +- ORM : intérêt, avantages +- ORM Django : configuration, fonctionnalités +- Définition des modèles de données +- Définition des relations entre tables (clés) +- Configuration et classe Meta +- Héritage, Proxy et modèles abstraits +- Interroger la base de données +- Enregistrer et modifier des données dans la base (+ concurrence) +- Système de migrations de schéma Django + +--- + +## Utiliser des formulaires avec la base de données + +- Définition de formulaires basés sur les modèles +- Utilisation des formulaires et validation +- Gestion des téléchargements + +--- + +## Interface d'administration de Django + +- Présentation de l'interface d'administration intégrée +- Configuration de l'administration + +--- + +## L'authentification et les utilisateurs + +- L'application d'authentification de Django +- S'authentifier programmatiquement dans une vue +- S'authentifier avec un formulaire + +--- + +## Traduction d'un projet Django (I18N) + +- Traduction dans le code Django +- Traduction dans les templates + +Ateliers : +- Ajout de traduction au code de projet +- Ajout de traduction dans les templates + +Atelier : I18N de l'application + +--- + +## Concepts avancés + +- Envoyer des emails +- Créer des vues de téléchargment +- Lancement de tests d'un projet Django (unittest) : + - Test Runner + - Test Client +- Déploiement d'un projet Django +- Interconnexion avec les réseaux sociaux (django-allauth) +- API REST en Django +- Interfaçage avec un framework front-end (ex. Vue.js ou Angular) +- Interfaçage avec un frawework front-end basé sur le contenu HTML (ex. HTMX) +- Intégration avec Git et une usine logicielle + diff --git a/documentation/01-discover.md b/documentation/01-discover.md new file mode 100644 index 0000000..a044cbf --- /dev/null +++ b/documentation/01-discover.md @@ -0,0 +1,140 @@ +--- +title: Découvrir Django +author: Steve Kossouho +--- + +# Découvrir Django + +---- + +## Frameworks web + +Pourquoi est-ce que les frameworks web existent ? + +- Avec un langage de programmation, si on a assez d'outils en standard, on pourrait très bien imaginer réinventer la roue; +- Or, réinventer la roue pour effectuer des tâches communes à de nombreux projets est une perte de temps et d'énergie; +- Écrire une fois le code, le réutiliser sur plusieurs projets semble beaucoup plus intelligent. + +---- + +### Que font les frameworks ? + +- Un framework est une bibliothèque qui propose un plan d'architecture pour réaliser un projet; +- Le développeur qui utilise un framework va devoir commencer son projet en utilisant une base de code spécifique au framework (des fichiers Python, des fichiers ini ou autres); +- Ensuite, il va étoffer son projet en utilisant généralement d'autres outils fournis par le framework. + +---- + +## Exemples de frameworks Python + +![Frameworks Web Python](assets/images/discover-frameworks.png) + +---- + +- **Sites web** : [Flask](https://flask.palletsprojects.com/en/2.2.x/) +- **Sites web** : [Web2py](http://www.web2py.com/init/default/index) +- **Sites web** : [Turbogears](https://turbogears.org/) +- **Sites web** : Pyramid (miniframework) +- **Réseau et programmation asynchrone** : [Twisted](https://twisted.org/) + +---- + +## Django et Design patterns + +Les **Design Patterns** sont un ensemble "standardisé" de bonnes pratiques liées au développement de projets ou d'algorithmes. Un design pattern connu au niveau projet est nommé **MVC**, ou **Model, View and Controller**. + +Le design pattern MVC consiste à découper le développement d'un projet avec des données et une interface graphique en 3 groupes de fichiers de code distincts… + +---- + +### MVC : Modèle + +La partie **Modèle** du MVC consiste à écrire dans des fichiers spécifiques, du code destiné à décrire la base de données et les traitements de base qui peuvent être effectués sur celle-ci. En général, la couche d'abstraction des données est décrite en Python sous la forme de classes nommées modèles. + +---- + +### MVC : Vue + +La partie **Vue** du MVC consiste, idéalement, à rédiger dans des fichiers spécifiques, le code destiné à afficher du contenu et une interface graphique au client. Autant que possible, dans un univers parfait, on séparerait complètement ce code de celui qui réagit aux interactions de l'utilisateur. + +---- + +### MVC : Contrôleur + +La partie **Contrôleur** du MVC est la partie la plus intelligente du lot. C'est celle qui s'occupe de l'intelligence spécifique au projet, et pas au métier décrit dans la partie modèle (elle n'est pas censée être réutilisable ailleurs que dans le projet). + +Elle peut décrire ce qui se passe quand l'utilisateur valide des formulaires, et appeler le modèle pour modifier les données. + +---- + +## Présentation de Django + +Django a été créé en 2003, dans les locaux du **Lawrence Journal-World Newspaper**, lorsque deux développeurs du journal en ligne ont commencé à utiliser le langage Python pour effectuer leurs tâches courantes. + +Dès 2005, alors que le développement du framework pour leurs besoins avait suffisamment évolué, le projet a été rendu public sous [licence BSD](https://fr.wikipedia.org/wiki/Licence_BSD). + +À partir de l'année 2009, le framework est devenu un outil de choix pour la création de nombreux projets web. Parmi les acteurs les plus célèbres utilisant Django, on trouve Instagram, Disqus ou encore plusieurs sites de Mozilla. + +---- + +### Arguments de vente + +- Prend en charge Python 3.8 et plus récents +- Le code source est libre, sous licence BSD; +- Accessible en ligne : [Github Django](https://github.com/django/django) +- **Attention** : Le copyright appartient à la Django Software Foundation; + +- Première release en 2005 : [Historique versions](https://en.wikipedia.org/wiki/Django_(web_framework)#Version_history) +- Version à jour : _5.1.2_ (Octobre 2024) +- Une nouvelle version mineure tous les 9 mois (18 mois de support); +- Une nouvelle version majeure toutes les 27 mois; +- Une version LTS (3 ans de support) à chaque dernière version mineure. + +---- + +## Environnements de développement intégrés + +Pour travailler avec Django, il existe plusieurs IDE recommandés : + +- [JetBrains® PyCharm](https://www.jetbrains.com/fr-fr/pycharm/) (Community ou Professional) +- [Visual Studio Code](https://code.visualstudio.com/) (avec extensions Python + Django) + +(PyCharm Community ne prend pas en charge le Javascript ni les outils spécifiques à Django) + +---- + +## Outils de développement pour la console + +Il est possible de travailler dans une console Python pour tester certains éléments de code (ex. tester le traitement des données en base de données dans notre projet Django). Certains packages externes Python nous offrent des consoles Python avancées, utilisables dans un terminal. + +- [iPython](https://ipython.org/) : console interactive, associée à Jupyter. +- Jupyter : console interactive utilisable depuis un navigateur. + +---- + +## Installation de Django et paquets annexes + +Pour installer Django sous Linux, le procédé est assez classique : + +1. Créer son répertoire de projet +2. Créer un environnement virtuel Python (via Pycharm) +3. Installer Django dans l'environnement virtuel + +---- + +### Installation de Django + +Installer Django + +```{.bash .numberLines} +pip install Django # installe la dernière version +``` + +. . . + +Créer son projet en utilisant les outils en ligne de commande de Django + +```{.bash .numberLines} +django-admin startproject +# En général, le répertoire du fichier principal sera ".", le répertoire courant. +``` diff --git a/documentation/02-structure.md b/documentation/02-structure.md new file mode 100644 index 0000000..528445c --- /dev/null +++ b/documentation/02-structure.md @@ -0,0 +1,276 @@ +--- +title: Projets et applications +author: Steve Kossouho +--- + +# Structure d'un site web avec Django + +---- + +## Notion de projet dans Django + +Dans le jargon de [Django]{.naming}, un projet équivaut à un site complet que vous souhaitez créer. +Par exemple, si vous souhaitez créer un nouveau site web à déployer en production sur un serveur, +vous devez créer un [projet]{.naming} associé à celui-ci. + +---- + +### Créer un nouveau projet + +Puisqu'il vous faut un projet pour votre prochain site web, vous devez le créer. +Le framework Django vous impose une structure générale, qui peut être créée en ligne de commande. +(les frameworks front-end tels que [vue.js]{.naming} par exemple, utilisent le même système de +[bootstrapping]{.naming}). + +Lorsque vous installez Django, le framework vous permet d'utiliser certains commandes dans un terminal. +Lesdites commandes vont vous permettre d'exécuter plusieurs tâches d'administration, dont la création de +la structure minimale d'un nouveau projet. + +---- + +L'outil principal de gestion de projets Django est disponible sous le nom `django-admin`{.bash}. +Sa [documentation](https://docs.djangoproject.com/en/dev/ref/django-admin/) indique qu'on peut l'utiliser +pour réaliser diverses tâches de base, dont la création d'un nouveau répertoire de projet : + +```bash {.number-lines} +django-admin startproject [répertoire de création, ou name par défaut] +``` + +---- + +Cette commande est nécessaire et fait partie du processus normal de création de site. +En général, si le répertoire contenant le projet à réaliser existe déjà (par exemple lorsqu'on travaille avec PyCharm), +il faut porter une attention à ne pas créer une arborescence inutile; il faudra créer le projet Django directement à la +racine du répertoire de projet PyCharm : + +```{.bash .numberLines} +django-admin startproject . +``` + +Notez le `.` afin d'indiquer à la commande `startproject` de ne pas créer un sous-répertoire inutile + +---- + +### Fichiers du package de projet + +La commande `startproject` de l'outil `django-admin` vous crée un répertoire avec le minimum syndical +pour démarrer un projet avec des bases fiables (sécurité, fonctionnalités). Les fichiers créés par cette commande +sont presque tous nécessaires au bon fonctionnement d'un site web écrit avec Django. + +---- + +| | Description | +|-------------------|--------------------------------------------------------------------------| +| `myproject/` | Répertoire racine du projet contenant les fichiers de configuration. | +| ├── `manage.py` | Script de gestion du projet Django (lancer le serveur, migrations). | +| └── `myproject/` | Répertoire contenant les paramètres et configurations du projet. | +| ├── `__init__.py` | Fichier vide marquant ce répertoire comme un package Python. | +| ├── `settings.py` | Fichier de configuration du projet Django (bases de données, etc.). | +| ├── `urls.py` | Fichier de routage des URLs pour les vues du projet. | +| ├── `asgi.py` | Point d’entrée pour le serveur ASGI (pour les applications asynchrones). | +| └── `wsgi.py` | Point d’entrée pour le serveur WSGI (pour les applications synchrones). | + + +---- + +#### Le fichier `manage.py` + +Ce fichier est un script proposant exactement la même chose que l'outil en ligne de commande `django-admin`, +mais en prenant en compte les paramètres spécifiques au projet, tels qu'indiqués dans le fichier `settings.py`. + +Par exemple, il peut être utilisé pour mettre à jour le schéma de la base de données d'un projet Django. +Lorsqu'un [projet]{.naming} est configuré pour utiliser des "plugins", ces derniers peuvent proposer de nouvelles commandes +utilisables avec l'outil. + +```bash {.numberLines} +# Lancer le serveur web de développement pour tester son projet +python manage.py runserver +``` + +---- + +#### Le fichier `settings.py` + +Ce fichier contient, sous la forme de simples constantes (ex. `CONSTANTE = valeur`{.python}), +la configuration actuelle de votre projet. Le nom des constantes à définir est documenté dans le détail +dans la [documentation officielle](https://docs.djangoproject.com/en/dev/topics/settings/), +mais la liste des paramètres disponibles est si longue qu'il faut la consulter uniquement +au besoin. + +Parmi les paramètres intéressants, vous pouvez configurer votre accès aux **bases de données**, +choisir les répertoires par défaut pour stocker des fichiers, les "plugins" utilisés par votre +projet, et globalement tous les éléments de votre site. + +---- + +#### Autres fichiers importants + +- `urls.py` : fichiers définissant les URL principales de votre site +- `asgi.py` : fichier de serveur asynchrone pour la mise en production +- `wsgi.py` : fichier de serveur synchrone pour la mise en production + +---- + +## Notion d'application dans Django + +Un concept intéressant de Django est que si vous avez un projet, +vous avez la possibilité d'y brancher, à l'instar d'un système de plugins, +des packages Python réutilisables que l'on appelle [applications]{.naming}. + +Une application Django est un **package Python** possédant une certaine structure (certains modules +sont détectés automatiquement par le framework Django). +Une application est normalement écrite pour définir un périmètre de fonctionnalités réutilisable +entre plusieurs projets. Par exemple, une application de _gestion de blog_, de _gestion d'images_, etc. + +---- + +### Applications fournies par Django + +Parmi les nombreuses applications fournies avec le framework Django, vous trouverez par exemple +des applications dédiées à plusieurs sujets : + +| Module | Description | +|------------------------------|-----------------------------------------------------------------| +| `django.contrib.auth` | Gestion des utilisateurs, groupes et permissions (à venir) | +| `django.contrib.admin` | Une interface d'administration avancée (à venir) | +| `django.contrib.messages` | Système de messages génériques pour les utilisateurs. | +| `django.contrib.staticfiles` | Gestion des fichiers statiques tels que les fichiers CSS et JS. | +| `django.contrib.sites` | Gestion des sites multiples dans une seule application Django. | + + +N'oubliez pas que Django est extensible et que de nombreuses autres applications tierces sont disponibles +via le Python Package Index (PyPI) pour répondre à des besoins plus spécifiques. + +---- + +### Créer une nouvelle application + +Pour créer une application, vous pouvez utiliser la commande suivante dans un terminal : + +```bash {.numberLines} +django-admin startapp +``` + +Un package Python avec certains fichiers par défaut sera créé. Si cette application est spécifique à +votre projet de site plutôt qu'à une fonctionnalité réutilisable, vous pouvez la créer dans votre répertoire +de projet, mais si vous souhaitez travailler sur votre application pour la réutiliser dans plusieurs projets, +vous pourriez la créer sans même avoir un projet Django pour la tester. + +Une application générale pour un projet est souvent nommée `core`, `main` ou `project` (si cela n'est pas déjà pris). + +Une application développée pour être réutilisable sera de préférence développée de façon à être installable +avec l'outil `pip`, mais ceci est un sujet avancé. + +---- + +### Fichiers créés dans le package d'application + +Lorsque vous créez une application Django pour l'utiliser dans un projet, un package est généré +contenant des fichiers de base, mais pas tous fondamentaux. + +| Fichier/Répertoire | Description | +|--------------------|-----------------------------------------------------------| +| `admin.py` | Module de configuration de l'administration. | +| `apps.py` | Module déclarant les informations de l'application. | +| `migrations/` | Package contenant les évolutions de la couche **Modèle**. | +| `models.py` | Module pour déclarer la couche **Modèle**. | +| `tests.py` | Module de tests automatisés (`unittest` par défaut). | +| `views.py` | Module pour déclarer la couche **Vue** et **Contrôleur**. | + + +---- + +#### Fichiers non créés avec l'application + +Certains modules ou répertoires sont souvent présents dans des applications Django, +et doivent être créés au besoin, manuellement : + +| Fichier/Répertoire | Description | +|---------------------|---------------------------------------------------------------| +| `fixtures/` | Répertoire contenant des dumps de données réutilisables. | +| `forms.py` | Module pour déclarer des formulaires. | +| `urls.py` | Module pour déclarer des routes locales à l'application. | +| `locale/` | Répertoire de gestion des catalogues de traduction. | +| `static/` | Répertoire contenant les fichiers statiques de l'application. | +| `templates/` | Répertoire contenant les templates de l'application. | + +---- + +### Associer une application à un projet + +Pour que Django reconnaisse que votre application est activée dans votre projet, ce qui permettra à +Django d'y détecter automatiquement certains fichiers (certains modules y seront détectés automatiquement), +vous devez préciser dans les paramètres de votre projet quelles applications vous souhaitez y inclure. + +Pour cela, vous devez ajouter le nom qualifié de package de votre application dans le paramètre `INSTALLED_APPS` +du module `settings.py` du projet : + +```python {.numberLines} +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "app_package_name", # le nom d'import de votre package +] +``` + +Ce paramètre est une liste contenant les applications activées pour votre site. La configuration +par défaut est généralement saine et il est désirable de partir de celle-ci. + +---- + +### Fichiers et répertoires standard + +En règle générale, on peut aussi, dans une application, ajouter des répertoires ou fichiers plus +ou moins standard, tels que : + +- `templates` : Répertoire de templates (découverts automatiquement par défaut) +- `static` : Répertoire de fichiers statiques spécifiques à l'application (ex. JS, CSS, images) + +Lorsque nous ferons notre premier contenu HTML, nous y reviendrons. + +---- + +## Commandes `django-admin` + +La commande `django-admin`, accessible partout, est disponible en ligne de commande dans le terminal et +accepte les actions suivantes, entre autres : + +- `startapp` : Créer une nouvelle application +- `startproject` : Créer un nouveau projet de site web + +---- + +### Commandes uniquement pour les projets + +Certaines commandes de l'outil `django-admin` sont dépendantes de la configuration d'un projet, +telle que précisée dans le module `settings.py`. C'est le cas par exemple des commandes suivantes : + +- `runserver` : lancer un serveur web de test du projet (utilise les paramètres) +- `makemigrations` et `migrate` pour gérer la base de données (voir chapitre **ORM**) +- `makemessages` et `compilemessages` pour les traductions (voir chapitre **Traduction**) + +Si votre commande nécessite d'avoir accès aux paramètres d'un projet Django, vous devez, au lieu +d'utiliser la commande `django-admin`, exécuter le module `manage.py` : + +```bash {.numberLines} +./manage.py runserver # ou encore +python manage.py runserver +``` + +L'ensemble des commandes disponibles par défaut se trouve dans la [documentation officielle](https://docs.djangoproject.com/en/dev/ref/django-admin/). + +---- + +## Récapitulatif + +Démarrer un projet minimal, c'est simple avec PyCharm (**Community**) : + +1. Créer un nouveau projet Python avec PyCharm +2. Installer Django : `pip install django`{.bash} +3. Démarrer un projet Django : `django-admin startproject .`{.bash} +4. Créer une application Django : `django-admin startapp `{.bash} +5. Ajouter l'application au projet : `INSTALLED_APPS = [..., "name2"]`{.py} diff --git a/documentation/03-urls-views.md b/documentation/03-urls-views.md new file mode 100644 index 0000000..af264b4 --- /dev/null +++ b/documentation/03-urls-views.md @@ -0,0 +1,565 @@ +--- +title: URLs dans Django +author: Steve Kossouho +--- + +# Afficher des pages avec Django + +---- + +## Routage d'URLs + +Tous les serveurs d'application web ont besoin de savoir comment afficher des pages à des URLs distinctes. + +Par exemple, si vous demandez au serveur à l'adresse `https://mon.adresse.com` l'URL dont le nom est `https://mon.adresse.com/resource`, le serveur doit pouvoir définir ce qu'il va faire de la ressource dont le chemin est `/resource`. (Et en général, il doit vous renvoyer un contenu avec le code de statut HTTP 404 s'il ne le peut pas) + +Pour ce faire, la majorité des systèmes de serveurs web mettent en place un système de routage d'URLs où le développeur configure quelles URLs seront acceptées par le serveur et ce qu'elles fournissent comme ressource. + +---- + +### Exemple de routage + +Si du côté du développeur, vous configurez votre site ainsi : + +- Le développeur configure le routage d'URL pour accepter l'URL `/about`; +- Le développeur indique que l'URL `/about` pointera vers une fonction qui renvoie le contenu à afficher au navigateur; + +Ce qui se produira lorsqu'une requête HTTP sera envoyée par votre navigateur web vers votre serveur web sera le suivant : + +- Vous demandez une ressource via votre navigateur, `/about` +- Le serveur vérifie dans ses configurations d'URL si l'URL demandée correspond à une route configurée +- Si **oui**, la fonction associée à l'URL est exécutée et une réponse peut être renvoyée au navigateur +- Si **non**, le serveur renverra si possible un contenu de page avec un code HTTP 404 (ressource non trouvée) + +---- + +## Exemple de routage d'URL dans un projet + +Lorsque nous avons un nouveau projet Django dans lequel travailler, nous pouvons déclarer des URLs et rédiger du code afin que lesdites URLs nous affichent un document (HTML ou non). + +La méthode imposée par le framework consiste à définir une variable possédant un nom spécifique, dans un module de votre projet qui par défaut est `urls.py` : + +- La variable qui recense les URLs principales de votre projet est nommée `urlpatterns`, et est une liste de valeurs configurées via des appels d'une fonction `path`; +- Le nom du module contenant les URLs principales est configurable via la variable `settings.ROOT_URLCONF`{.python}. + +---- + +L'exemple de code suivant est un exemple général de routes principales qu'il est possible de configurer dans Django. + +```python {.numberLines} +from django.urls import path, include + +urlpatterns = [ + # Les URLs de l'application `application` seront accessibles dans l'URL appli/ + path("appli/", include("application.urls")), + # L'URL par défaut du site reçoit un nom user-friendly (voir Cool URLs) + path("", view_function, name=""), + # Une URL peut indiquer des parties dynamiques acceptées par le routage + path("url/", view_dynamic, name=""), # URL dynamique +] +``` + +- [Documentation officielle](https://docs.djangoproject.com/en/dev/ref/urls/#path) de la fonction `django.urls.path` +- [Types d'emplacements dynamiques](https://docs.djangoproject.com/en/5.0/topics/http/urls/#path-converters) + +---- + +### La fonction `django.urls.path` + +Lorsque vous souhaitez définir des routes dans votre projet (ou votre application Django), vous ajoutez des résultats d'appels de la fonction `path` comme éléments de la liste contenue dans la variable `urlpatterns`. + +La fonction `path` possède la signature suivante : + +- `def path(route: str, view_function: Callable, kwargs: dict, name: str)`{.python} +- `route` : URL à configurer. Ne contient pas le `/` de début; +- `view_function` : référence de fonction à associer à l'URL lorsqu'on y accède; +- `name` : donner un nom à l'URL pour la référencer même si l'URL change de forme. + +---- + +### Écrire une fonction de vue + +Les fonctions de vue que vous associez à vos routes sont de simples fonctions, qui sont toujours appelées par le système de routage de Django de la même manière : + +```python {.numberLines} +from django.http import HttpResponse, HttpRequest + +def view_function(request: HttpRequest) -> HttpResponse: + return HttpResponse("Page content") +``` + +La fonction **doit** au moins posséder un premier argument positionnel nommé `request`, qui contiendra les données de requête HTTP, et retourner un objet de la classe `HttpResponse`, qui contiendra les informations de réponse HTTP (en-têtes, etc.). + +---- + +### Décrire une simple URL statique + +Pour définir une URL simple à laquelle on renvoie un contenu (page HTML ou autre document), il nous faut 2 choses, à savoir ajouter une route, et ajouter la fonction à associer à cette route : + +```python {.numberLines} +from django.urls import path + +urlpatterns = [ + path("chemin", reference_fonction, name="nom-de-reference") +] +``` + +L'argument `name` de la fonction `path` permet de référencer l'URL a posteriori non pas par son chemin absolu, mais par un nom. Cet argument est recommandé, car coder en dur des URLs partout dans notre code va causer des soucis de maintenabilité dès que vous souhaiterez modifier l'URL. + +```{.python} +from django.http import HttpResponse + +def reference_fonction(request): + return HttpResponse("Document content") +``` + +---- + +### Inclure les URLs fournies par une application + +Django propose des outils de routage avancés, et parmi ces outils, la réutilisation d'URLs définies dans une application Django. + +Supposons qu'une application Django propose son propre fichier `urls.py` +avec ses propres routes. Ces routes peuvent être intégrées à votre projet en les ajoutant comme sous-URLs d'une route définie dans votre projet. + +Si l'application se dédie à la gestion d'utilisateurs et définit trois sous-routes aux URLs : + +- `view/` +- `edit/` +- `report/` + +Vous pouvez les intégrer à une URL de votre projet, ex. `users/`, et y accéder via les URLs suivantes : + +- `users/view/` +- `users/edit/` +- `users/report/` + +---- + +Pour définir ce type d'URL, il vous faut : + +- Une application Django (à ajouter à `settings.INSTALLED_APPS`) +- Un module `urls` dans l'application avec une configuration d'URL +- Puis, dans le module `urls` de votre projet : + +```python {.numberLines} +from django.urls import path, include + +urlpatterns = [ + path("application/", include("application.urls", namespace="nom")) +] +``` + +[Documentation officielle de la fonction `include`](https://docs.djangoproject.com/en/4.1/ref/urls/#include) + +---- + +### Espace de nom d'inclusion d'URLs + +Lorsque vous utilisez la fonction `include`, vous pouvez donner un nom de "groupe" commun aux URLs incluses. +Pour cela, vous devez passer un argument `namespace` qui contiendra le nom de groupe des URLs. Vous devrez utiliser ce nom de groupe lorsque vous souhaiterez référencer ces URLs par la suite. + +**Note** : Lorsque vous précisez un `namespace` dans la fonction `include`, Django refusera votre définition, à moins que vous précisiez quel est le nom de l'application incluse. La façon la plus simple de faire est d'ajouter, dans le fichier `urls.py` de votre application, une variable nommée `app_name` contenant le nom de l'application. + +--- + +### Espace de noms d'URLs + +Exemple de code pour les espaces de noms : + +```python {.numberLines} +from django.urls import path, include +urlpatterns = [ + path("dossier/", include("application.urls", namespace="application")) +] +``` + +```python {.numberLines} +# Module `application.urls` +app_name = "application" # À définir pour l'espace de noms +urlpatterns = ["…"] +``` + +[Documentation de include](https://docs.djangoproject.com/fr/3.2/ref/urls/#include) + +---- + +### Éléments dynamiques d'une URL + +On peut, et il vaut mieux, pouvoir définir des URLs avec des portions dynamiques, ex. `users/view/`. +Pour faire cela, la fonction `path` nous offre une méthode simple et efficace pour définir lesdits emplacements; +il suffit de marquer la portion dynamique entre chevrons et suivre le format suivant : + +```python {.numberLines} +from django.urls import path +from application.views import view_reference + +urlpatterns = [ + path("view//", view_reference, name="view-name") +] +``` + +---- + +Dans l'exemple précédent, la mention `type` doit être remplacée par l'un des types reconnus par le moteur de routage de Django ([Formats reconnus pour les parties dynamiques d'URL](https://docs.djangoproject.com/fr/3.2/topics/http/urls/#path-converters)) + +- `str` : toute chaîne de caractères non vide, sans le slash +- `int` : toute chaîne représentant un entier positif +- `slug` : uniquement des lettres latines non accentuées, chiffres, `-`, `_` et aucun espace +- `uuid` : nombre sur 128 bits représenté en hexadécimal, groupé par 8-4-4-4-12 chiffres séparés par un tiret. +- `path` : toute chaîne de caractères non vide, slash inclus + +---- + +#### Fonction de vue et éléments dynamiques + +La fonction de vue ciblée par une définition d'URL contenant une ou plusieurs portions dynamiques devra accepter un argument portant le nom défini dans la définition d'URL. Si votre définition de route est ainsi : + +```python {.numberLines} +from django.urls import path +from application.views import view_reference + +urlpatterns = [ + path("view//", view_reference, name="view-name") +] +``` + +Alors votre définition de fonction de vue doit suivre la signature suivante : + +```python {.numberLines} +# Module application.views +from django.http import HttpResponse, HttpRequest + +def view_reference(request: HttpRequest, value: int = None): + """ + Fonction de vue. + + L'argument `value` est converti automatiquement depuis + une chaîne (dans l'URL) vers le type à reconnaître. + """ + return HttpResponse("Texte de la page.") +``` + +---- + +## Écriture de vues + +Une fois les routes définies, il faut les lier à des vues. Les vues sont de simples fonctions qui prennent au moins un argument positionnel nommé `request`. L'objet `request` contient des données relatives à la requête HTTP entrante, et la vue doit toujours renvoyer un objet de type `HttpResponse`. Il s'agit d'un contenu de réponse HTTP classique renvoyé au navigateur. + +```python +def view_example(request): + return HttpResponse("Texte à renvoyer.") +``` + +---- + +Lorsqu'une vue est appelée via une URL qui accepte des portions dynamiques, la signature de la vue doit accepter des arguments du même nom que les portions dynamiques de l'URL : + +```python +from django.urls import path +from somewhere import view_parametrized + +urlpatterns = [ + path("view//", view_parametrized, name="view-name") +] +``` + +```python +def view_parametrized(request, uuid_val=None): + # Récupérer l'objet possédant l'UUID ou renvoyer un + return HttpResponse("Page relative à l'UUID demandé.") +``` + +---- + +### Types de réponses disponibles + +Django offre plusieurs classes de réponses pour les vues. Voici un tableau de différentes classes pour renvoyer une page 404, 403, ou une redirection avec Django : + +| Type de renvoi | Description | +|-------------------------|---------------------------------------| +| `HttpResponse` | Contenu basique renvoyé au navigateur | +| `HttpResponseNotFound` | Page 404 | +| `HttpResponseForbidden` | Page 403 | +| `HttpResponseRedirect` | Redirection vers une autre URL | + +Il existe toutefois de meilleurs outils pour générer des pages 404, 403 et redirections. + +---- + +### Renvoyer des redirections + +Django offre une fonction de raccourci pour renvoyer des redirections. + +```python {.numberLines} +from django.shortcuts import redirect + +def view_example(request): + """Redirection vers une autre vue.""" + return redirect("url-name", permanent=True) +``` + +La fonction `redirect`{.py} prend en argument le nom de la route vers laquelle rediriger, et accepte facultativement un argument `permanent` qui indique si la redirection est permanente ou non. + +---- + +### HttpResponse, c'est bien mais léger + +Devoir renvoyer manuellement une instance de `HttpResponse` dans chaque vue peut sembler fastidieux, notamment si l'on souhaite générer des pages complexes et interactives. Naturellement, Django propose des fonctions utilitaires pour renvoyer des contenus qui ne sont pas directement présents dans du code Python, mais dans des fichiers séparés (on parle de [templates]{.naming}). + +Pas de panique, il existe des façons de renvoyer du contenu dans les vues sans créer d'objet `HttpResponse` de cette façon. On peut par exemple renvoyer le contenu d'un fichier de _template_ via des fonctions simples. (voir le chapitre sur les gabarits) + +---- + +## Vues génériques basées sur les classes + +Django 1.3 a introduit un système de vues qui sont basées sur un système de classes. +L'objectif était de rendre élégante l'écriture de vues en n'écrivant que des classes où il suffirait de préciser quelques attributs, +et ainsi créer facilement des pages intelligentes. + +Dans la pratique, ça n'est pas un si beau gain de temps que prévu : + +- Une vue de type fonction est souvent simple à comprendre, et s'écrit simplement. +- Il existe de nombreuses classes à connaître pour écrire des vues diverses. +- Ces classes-là possèdent également des méthodes qu'il faut connaître pour aller plus loin. +- La documentation sur le sujet est dense mais incomplète (notamment sur le système complexe de mixins) + +---- + +### Utilité des vues basées sur les classes + +Même si le système est trop complexe pour démarrer (il faut se documenter tout le temps), certaines classes de vues +sont utiles car assez concises : + +- `TemplateView` : permet de rendre rapidement un template (configurable) +- `ListView` : permet d'afficher une liste d'objets +- `DetailView` : permet d'afficher la page de détail d'un objet + +---- + +### Utiliser une classe de vue + +Si l'on définit une classe de vue pour afficher le contenu d'un template : + +```python +from django.views.generic import TemplateView + +class MyView(TemplateView): + template_name = "nom du template" +``` + +```python +from django.urls import path +urlpatterns = [ + path("view/", MyView.as_view(), name="view-name") +] +``` + +---- + +### Pages spéciales : 404 + +Imaginez que vous avez défini une URL dynamique pour afficher une page produit. L'URL existe sous la forme `view/`. Supposons qu'un visiteur arrive à une URL correspondant au format défini, mais que vous ne trouvez dans votre base de données aucun produit correspondant à la référence passée dans l'URL. + +Vous souhaiterez renvoyer une page 404 classique sur votre site web. Pour ne pas vous embêter, Django propose un mécanisme simple pour rendre automatiquement une page 404 (personnalisable); il suffit de lever manuellement une exception de type `Http404`. + +```python +from django.http import Http404 +def my_view(request, user_id): + user = find_user(user_id) + if user is None: + raise Http404() +``` + +---- + +## Le Concept de Middleware dans Django + +Un [middleware]{.naming} dans Django est une couche d'[intergiciel]{.naming} qui permet d'intercepter et de traiter les requêtes et réponses HTTP. Il s'agit d'une série de composants qui s'exécutent pendant le traitement des requêtes, offrant un moyen de modifier l'entrée ou la sortie globale de l'application. + +---- + +### Qu'est-ce qu'un Middleware ? + +Un middleware est une classe qui définit des méthodes pour intervenir à différentes étapes du cycle de traitement des requêtes et des réponses. Les méthodes les plus couramment utilisées sont : + +| Méthode | Description | +|--------------------------------|-----------------------------------------------------------------------------------------------------------------------| +| `__init__(self, get_response)` | Initialisation du middleware. | +| `__call__(self, request)` | Traitement de la requête avant qu'elle n'atteigne la vue, et de la réponse avant qu'elle ne soit renvoyée au client. | + +---- + +### Comment Fonctionne le Middleware ? + +Lorsqu'une requête est reçue, Django la passe à travers chaque middleware dans l'ordre défini dans le paramètre `MIDDLEWARE` de votre fichier `settings.py`. Chaque middleware peut : + +- Modifier la requête avant qu'elle n'atteigne la vue. +- Modifier la réponse avant qu'elle ne soit renvoyée au client. +- Gérer les exceptions. +- Traiter les réponses de templates. + +---- + +### Exemple de Middleware Intégré + +Django fournit plusieurs middlewares intégrés, tels que : + + +| Middleware | Description | +|------------------------------|------------------------------------------------------------------------------------------| +| `AuthenticationMiddleware` | Gère l'authentification des utilisateurs (ajoute un attribut `user` à la requête, etc.). | +| `SessionMiddleware` | Gère les sessions côté serveur. | +| `CommonMiddleware` | Ajoute des améliorations utiles, comme la gestion des en-têtes HTTP. | + +---- + +### Créer un Middleware Personnalisé + +Voici comment, par exemple, créer un middleware personnalisé qui enregistre le temps pris par une vue pour s'exécuter. + +```python {.numberLines} +# myapp/middleware.py + +import time + +class TimingMiddleware: + def __init__(self, get_response): + self.get_response = get_response + # Initialisation du middleware + + def __call__(self, request): + # Code exécuté pour chaque requête avant la vue + start_time = time.time() + + response = self.get_response(request) + + # Code exécuté pour chaque réponse après la vue + duration = time.time() - start_time + print(f"La vue a pris {duration:.2f} secondes.") + + return response +``` + +---- + +### Enregistrer le Middleware + +Pour que Django utilise votre middleware, vous devez l'ajouter à la liste `MIDDLEWARE` dans `settings.py`. + +```python {.numberLines} +# settings.py +MIDDLEWARE = [ + # ... autres middlewares ... + 'myapp.middleware.TimingMiddleware', +] +``` + +---- + +### Explication du Fonctionnement + + +| Méthode | Description | +|--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| +| `__init__(self, get_response)` | Django passe la fonction `get_response` au middleware. Cette fonction est appelée pour obtenir la réponse de la vue ou du middleware suivant. | +| `__call__(self, request)` | Cette méthode est appelée pour chaque requête. Vous pouvez y ajouter du code qui s'exécute avant et après l'appel de `get_response(request)`. | + +---- + +### Middleware avec Méthodes Additionnelles + +Vous pouvez également implémenter des méthodes spécifiques pour intervenir à différentes étapes spécifiques. + +```python {.numberLines} +class MyCustomMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def process_view(self, request, view_func, view_args, view_kwargs): + # Code exécuté juste avant l'appel de la vue + pass + + def process_exception(self, request, exception): + # Code exécuté si une exception est levée + pass + + def process_template_response(self, request, response): + # Code exécuté si la réponse est un TemplateResponse + return response +``` + +---- + +### Cycle de Traitement des Requêtes + +1. **Requête entrante**: Le middleware peut modifier ou rejeter la requête. +2. **Processus de la vue**: La requête est traitée par la vue. +3. **Réponse sortante**: Le middleware peut modifier la réponse avant qu'elle ne soit renvoyée. +4. **Réponse au client**: La réponse finale est envoyée au client. + +---- + +### Cas d'Utilisation Courants + +- **Authentification et Autorisation**: Contrôle d'accès aux vues. +- **Journalisation**: Enregistrement des requêtes et réponses pour le débogage. +- **Gestion des Exceptions**: Capture et traitement des erreurs. +- **Compression**: Compression des réponses pour réduire la taille des données transférées. + +---- + +### Exemples Pratiques + +Middleware pour Bloquer des IP + +```python {.numberLines} +# myapp/middleware.py + +class BlockIPMiddleware: + BLOCKED_IPS = ['192.168.1.1'] + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + ip = request.META.get('REMOTE_ADDR') + if ip in self.BLOCKED_IPS: + return HttpResponseForbidden("Votre IP a été bloquée.") + response = self.get_response(request) + return response +``` + +---- + +Middleware pour Ajouter des En-têtes HTTP + +```python {.numberLines} +# myapp/middleware.py + +class CustomHeaderMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + response = self.get_response(request) + response['X-Custom-Header'] = 'MaValeurPersonnalisee' + return response +``` + +---- + +![Exemple de flux de Middleware](assets/images/views-middleware-flow.png) + +---- + +## Divers : Débogage et accès + +Lorsque dans vos paramètres, `settings.DEBUG = False` (ce qui est la seule valeur acceptable en production) Django n'autorise plus l'accès à vos vues par défaut. + +Pour accéder à vos vues, vous devez définir explicitement une liste d'adresses IP qui ont accès à votre site dans `settings.ALLOWED_HOSTS`. Le paramètre accepte des adresses IPv4, IPv6 et noms d'hôte. La valeur `"*"` signifie "toutes adresses". + +```python {.numberLines} +# settings.py +ALLOWED_HOSTS = ("127.0.0.1", "machinea", "192.168.1.15") +``` diff --git a/documentation/04-templating.md b/documentation/04-templating.md new file mode 100644 index 0000000..046f91d --- /dev/null +++ b/documentation/04-templating.md @@ -0,0 +1,615 @@ +--- +title: Django +author: Steve Kossouho +--- + +# Templates avec Django + +:::notes +Préférer commencer par les vues, pour avoir des vues où utiliser des templates... +::: + +---- + +## Principe de templating + +Le concept de [templating]{.naming} (gabarits en français) consiste à utiliser des fichiers contenant du texte +avec des emplacements réservés, qui seront remplis à la demande pour afficher des données personnalisées à l'utilisateur +(par exemple, le système permet de générer un email de bienvenue commençant par « Bonjour `votre prénom` »). + +Ce concept est fourni par de nombreuses bibliothèques, et ce dans plusieurs langages (PHP, Javascript, C++, Python etc.) + +---- + +### Exemples de systèmes de templating en Python + +Il existe un certain nombre de moteurs de templating assez populaires en Python : + +- [Django](https://docs.djangoproject.com/en/3.2/topics/templates/) : Intégré à Django, propose un bon compromis entre performance et sécurité. +- [Jinja 3](https://jinja.palletsprojects.com/en/3.0.x/) : Basé sur Django (et Genshi ?), mais utilisable dans tout projet Python. *Instagram* +- [Mako](https://www.makotemplates.org/) : Performant, autorise le code Python brut. *Reddit* +- [Genshi](https://genshi.readthedocs.io/en/latest/) : Utilisé dans TurboGears + +Django est à l'origine de deux moteurs de templates populaires, tels que `Jinja` pour Python et `Twig` pour PHP qui en reprennent largement la syntaxe. + +---- + +## Principe de templating dans Django + +Un moteur de templates propose une façon simple et rapide de générer du contenu en rédigeant +des gabarits (des squelettes de document), des modèles où il ne reste plus qu'à renseigner des éléments manquants. +Il existe plusieurs façons de procéder lorsqu'on conçoit un langage de templates : + +- Un moteur peut offrir la possibilité d'insérer des expressions Python sans limite dans un document de template +- Un langage peut permettre d'écrire du Python brut pour générer du contenu +- Un langage peut offrir des fonctionnalités limitées pour éviter des injections de code ou l'écriture de code métier en dehors du code principal + +---- + +Les considérations suivantes ont mené à la création du moteur de templates de Django : + +- Les templates sont souvent rédigés et modifiés par des designers (HTML notamment) +- Écrire du code Python ne devrait se faire que dans des fichiers Python +- Exécuter du code Python en dehors de scripts Python normaux rend un programme plus dur à déboguer +- Un designer ou intégrateur ne devrait pas être tenu d'apprendre le Python + +---- + +Ainsi, le moteur de templates de Django repose sur des paradigmes simples : + +- Une syntaxe peu variée pour simplifier l'apprentissage (interpolation, attributs, filtres et balises) +- Pas une ligne de Python autorisée (même si la syntaxe est similaire) +- Système extensible, mais uniquement via du code Python + +---- + +## Le langage de templates de Django + +Toute la section qui va suivre concerne les éléments de langage spécifiques aux templates de Django. + +---- + +### Créer des templates + +Par défaut, Django ne sait trouver et exploiter des documents de templates que grâce à un paramètre `TEMPLATES` dans le fichier `settings` de votre projet : + +```python {.numberLines} +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], # Répertoires où trouver des templates + "APP_DIRS": True, # Utiliser le répertoire `templates` de mes applications + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + ], + }, + }, +] +``` + +---- + +- [Documentation sur le paramètre `TEMPLATES`](https://docs.djangoproject.com/en/dev/ref/settings/#templates) +- La clé `BACKEND` permet de choisir le moteur de templates de Django (parmi deux) +- La clé `APP_DIRS` permet d'indiquer si les répertoires `templates` des applications tierces sont automatiquement utilisés par le moteur. +- La clé `DIRS`, elle, permet de spécifier des répertoires supplémentaires dans lesquels le moteur de templates de Django ira automatiquement chercher des fichiers de templates. + +---- + +### Nommage des templates + +Dans Django, lorsque vous souhaitez afficher un template au navigateur, vous avez accès à une fonction de raccourci nommée `django.shortcuts.render(request, template, context)`. + +Si vous appelez cette fonction avec les arguments `render(request, "index.html", {"value": 3.14159})`{.py} : + +- Django va considérer le premier fichier `index.html` trouvé dans les répertoires `templates` des applications tierces (celles indiquées dans `settings.INSTALLED_APPS`) +- Le fichier `index.html` sera rendu en ayant accès aux données de `context` +- La fonction renverra à la fin un objet `HttpResponse` avec le document rendu. +- [Documentation sur la fonction `render`](https://docs.djangoproject.com/en/dev/topics/http/shortcuts/#render) + +---- + +#### Exemple de template + +```python {.numberLines} +from django.shortcuts import render + +def index(request): + return render(request, "index.html", {"value": 3.14159}) +``` + +Le template `index.html` peut être trouvé dans le dossier `templates` de votre application : + +```html {.numberLines} + + + + + + + Index + + +

Index

+

Value: {{ value }}

+ + +``` + +---- + +### Commentaires + +En HTML, la balise de commentaire s'écrit de la façon suivante : + +```html {.numberLines} + +``` + +C'est utile pour annoter un document, mais deux choses se produisent : + +- Le commentaire est visible dans le navigateur. +- Écrire ce type de commentaire dans un document qui n'est pas du HTML ne fonctionnera pas. + +Avec Django, on peut insérer un vrai commentaire de template via la balise suivante : + +```djangotemplate {.numberLines} +{# Texte de commentaire Django #} +``` + +Le commentaire ne sera jamais rendu par le moteur, au même titre que les commentaires Python sont ignorés par l'interpréteur. Essayez autant que possible de vous en servir pour plus facilement relire vos templates _a posteriori_. + +---- + +### Interpolation de données + +Lorsque vous effectuez le rendu d'un template, le seul moyen d'y insérer des données est de les passer via un [contexte]{.naming}. Un contexte est un simple **dictionnaire Python**, où les clés sont des noms qui seront utilisables uniquement dans le template. Si en Python, on rend un template : + +```python {.numberLines} +from django.shortcuts import render + +def view_func(request): + return render(request, "template.html", {"name": "Maurice"}) +``` + +Le template aura uniquement accès à une donnée nommée `name` et dont la valeur sera `"Maurice"`{.py}. (Voir [Documentation sur les modifieurs de contexte](https://docs.djangoproject.com/en/dev/ref/templates/api/#built-in-template-context-processors) pour savoir quels autres noms sont rendus disponibles dans le contexte de template) + +```djangotemplate {.numberLines} +Bonjour, {{ name }} +``` + +---- + +#### Interpoler un attribut d'une variable + +Lorsqu'une des variables du contexte est un objet qui contient des attributs, vous pouvez les interpoler avec la syntaxe suivante : + +```djangotemplate {.numberLines} +{{ var1.attribute }} +``` + +---- + +#### Interpoler un élément d'une variable (clé ou index) + +De la même façon, si votre variable de contexte est une **liste** ou un **dictionnaire** dont vous voulez récupérer un élement, et que vous connaissez son index ou bien sa clé, vous pouvez utiliser la syntaxe suivante : + +```djangotemplate {.numberLines} +{{ var1.nom_de_la_clé }} {# clé de dictionnaire qui est une chaîne #} +{{ var2.numéro_d'index }} {# index de séquence #} +``` + +---- + +#### Interpoler la valeur de retour d'une méthode + +Pour terminer, si votre variable de contexte est un objet dont vous voulez récupérer la valeur de retour d'une méthode, vous pouvez écrire : + +```djangotemplate +{{ var1.nom_de_la_methode }} +``` + +Cette méthode sera **toujours** appelée sans argument. + +---- + +#### Interpolation et limitations + +Le langage de templates de Django a des restrictions sur les interpolations : + +- Vous ne pouvez pas passer d'arguments lorsque vous appelez une méthode et c'est intentionnel, seules les méthodes acceptant uniquement des arguments facultatifs sont utilisables. +- Vous ne pouvez pas utiliser d'attributs ou méthodes commençant par `_`{.py} (considérées comme privées en Python). + +---- + +### Filtres de templates + +Lorsque vous effectuez une interpolation dans un template (`{{ ... }}`), vous avez la possibilité de transformer l'interpolation, souvent pour des questions de présentation. Pour effectuer ce genre de traitement sur une interpolation, vous devez utiliser des [filtres]{.naming}. + +Les filtres permettent de transformer une interpolation et fonctionnent sur chaînes, dates, listes et d'autres types. (voir [filtres intégrés à Django](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#built-in-filter-reference)) + +---- + +#### Filtre basique (sans argument) + +Un filtre s'utilise simplement en le "pipant" à l'expression à transformer. Le résultat affiché sera celui du filtre. +Il est possible d'utiliser plusieurs filtres successivement en les chaînant. + +```djangotemplate {.numberLines} +{# Utilisation du filtre upper, pour mettre en majuscules #} +

{{ name|upper }}

+``` + +---- + +#### Filtre avec argument + +Certains filtres acceptent un argument (**et un seul maximum**). Pour les utiliser, il suffit de faire suivre le nom du filtre par `:` et d'indiquer à la suite la valeur du paramètre, généralement sous forme d'une chaîne de caractères. Ceci est documenté pour chaque filtre disponible. + +```djangotemplate {.numberLines} +{# Utilisation du filtre date pour formater un objet datetime #} +

Date de création : {{ moment|date:"d m Y" }}

+``` + +---- + +#### Filtres fréquents + +Parmi les nombreux [filtres disponibles de base dans Django](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#built-in-filter-reference), certains seront plus souvent utilisés que d'autres, comme par exemple : + +| Filtre | Description | +|--------------------------------------------|----------------------------------------------------------------------------------------------------| +| `upper` et `lower` | Pour changer la casse du texte (majuscules et minuscules). | +| `default:value` | Si la valeur équivaut à `False`, utiliser `value` par défaut. | +| `default_if_none:value` | Si la valeur équivaut à `None`, utiliser `value` par défaut. | +| `date:format` | Interpoler un objet `datetime` avec un format spécifique. | +| `length` | Interpoler la longueur d'une collection. | +| `safe` | Ne pas échapper le contenu à interpoler. Permet d'insérer du HTML. | +| `truncatechars:num` et `truncatewords:num` | Couper et placer une ellipse (tronquer le texte après un certain nombre de caractères ou de mots). | + + +---- + +### Balises de templates + +Nous venons de voir que l'on pouvait "filtrer" du contenu provenant d'une interpolation. Et s'il était possible d'utiliser des +outils plus avancés, permettant de générer des données dynamiques, ou même de rendre du contenu conditionnel, par exemple ? + +Le moteur de templates de Django fournit un système de [balises]{.naming} qui nous permet d'accéder à des comportements plus avancés. +Grâce aux balises, nous allons pouvoir utiliser des boucles, conditionner le rendu d'un segment de template, configurer le rendu par le +moteur ou encore modifier le contexte de rendu... + +---- + +### Syntaxe des balises + +Pour utiliser des balises de gabarit, le plus important, c'est d'en connaitre la syntaxe. Pour utiliser une balise, il faut écrire : + +```djangotemplate {.numberLines} +{% tag_name argument1 argument2... %} +``` + +Entre les marqueurs `{% ... %}` il suffit d'indiquer le nom de la balise et d'y ajouter, séparés par des espaces, autant d'arguments que ce qu'accepte la balise. Certaines balises fonctionnent par paire, comme `{% if %}…{% endif %}`{.djangotemplate}, ou `{% for %}…{% endfor %}`{.djangotemplate}. Ces balises sont documentées dans la [documentation officielle de Django](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#built-in-tag-reference) + +---- + +### Balises natives + +Pour aborder le chapitre des balises, nous allons nous intéresser à certaines balises précises dédiées à des outils importants. + +---- + +#### Conditions + +| Balise | Description | +|---------------|-------------------------------------------------------------------------------------------| +| `{% if %}` | Évalue une condition et affiche le contenu si la condition est vraie. | +| `{% elif %}` | Alternative à `{% if %}` si la première condition est fausse. | +| `{% else %}` | Définit un bloc à afficher si aucune des conditions précédentes n'est vraie. | +| `{% endif %}` | Termine une instruction conditionnelle. | + +---- + +#### Boucles + +| Balise | Description | +|------------------------|-------------------------------------------------------------------------------------------| +| `{% for ... in ... %}` | Itère sur une séquence d'objets. | +| `{% empty %}` | Définit un bloc à afficher si la séquence est vide. | +| `{% endfor %}` | Termine une boucle. | + +---- + +#### Autres Balises Importantes + +| Balise | Description | +|---------------------------------------|----------------------------------------------------------------| +| `{% include %}` | Inclut un autre template. | +| `{% extends %}` | Indique que le template hérite d'un autre template. | +| `{% block %}` | Définit un bloc de contenu modifiable dans un template hérité. | +| `{% endblock %}` | Termine un bloc de contenu. | +| `{% url %}` | Génère une URL vers une vue nommée. | +| `{% csrf_token %}` | Génère un jeton CSRF pour sécuriser les formulaires. | +| `{% comment %}` et `{% endcomment %}` | Commente une section du template. | +| `{% load %}` | Charge une bibliothèque de tags personnalisés. | +| `{% with %}` et `{% endwith %}` | Assigne une ou plusieurs variables pour une portée limitée. | +| `{% endwith %}` | Termine le bloc `with`. | + +---- + +| Balise | Description | +|-----------------------|--------------------------------------------------------------------------------------| +| `{% autoescape %}` | Active ou désactive l'échappement automatique dans un bloc. | +| `{% endautoescape %}` | Termine le bloc `autoescape`. | +| `{% now %}` | Affiche la date et l'heure actuelles selon un format spécifié. | +| `{% spaceless %}` | Supprime les espaces blancs entre les balises HTML dans le contenu du bloc. | +| `{% endspaceless %}` | Termine le bloc `spaceless`. | +| `{% verbatim %}` | Ignore le rendu des balises Django dans le contenu du bloc. | +| `{% endverbatim %}` | Termine le bloc `verbatim`. | + +---- + +### Structures de contrôle : `if` + +Vous pouvez grace aux balises `{% if %}`{.djangotemplate} et `{% else %}`{.djangotemplate} construire un document dont certaines zones seront rendues conditionnellement. +La syntaxe de la balise est similitaire aux structures conditionnelles en Python. + +```djangotemplate {.numberLines} +{% if expression %} + Contenu si la condition est vraie +{% elif expression %} + Contenu si la premiere condition est fausse +{% else %} + Contenu si la condition est fausse +{% endif %} +``` + +---- + +### Structures de contrôle : `for` + +Si vous souhaitez générer du contenu pour chaque élément d'un itérable, vous pouvez utiliser la balise `{% for %}`{.djangotemplate}. La syntaxe +de la balise est similaire aux structures de boucle en Python, à l'exception de l'existence d'une clause `{% empty %}`{.djangotemplate}, +dont le contenu sera affiché si l'itérable est vide. + +```djangotemplate {.numberLines} +{% for name in iterable %} + Contenu de l'itération {{ name }} +{% empty %} + Contenu si la boucle est vide +{% endfor %} +``` + +---- + +### Inclusion de templates + +La balise `{% include %}`{.djangotemplate} permet d'inclure un autre template dans le template actuel. L'intérêt d'inclure des templates est de pouvoir les utiliser comme **composants** dans certains cas répétitifs. Il est possible de transmettre un contexte spécifique lors de l'inclusion de templates. + +```djangotemplate {.numberLines} +{% for user in users %} + {% include "path/to/user_template.html" with user=user %} +{% endfor %} +``` + +```djangotemplate {.numberLines} + + +

{{ user.username }}

+
+``` + +---- + +### Héritage de templates + +Dans un site web, beaucoup de pages partagent la même structure. Par exemple, sur de nombreux sites, la barre de navigation est identique et le pied de page également. Il semblerait redondant de devoir répéter dans les templates de diverses sections le même code HTML, notamment l'en-tête HTML ou le pied de page. + +Django propose un système d'héritage de templates, permettant de partager des blocs de contenu entre plusieurs templates. La fonctionnalité est offerte via deux balises, `{% block %}`{.djangotemplate} et `{% extends %}`{.djangotemplate}. + +---- + +Dans l'héritage de templates, vous avez deux notions de base : + +1. Un [template de base]{.naming} ou template parent. Ce template est le template principal du site, et permet de fournir le contenu commun aux autres templates. +2. Un [template enfant]{.naming}. Ce template est le template qui hérite du template de base, et qui ajoute ou modifie du contenu via des [blocs]{.naming}. + +---- + +#### Template de base + +```djangotemplate {.numberLines} + + + + + + + Document + + + {# Contenu commun aux templates enfants #} + {% block body %}{% endblock body %} + + +``` + +Un template de base propose un contenu fixe qui servira de base aux templates enfants. Ce même template proposera des duos de balises `{% block name %}`{.djangotemplate} et `{% endblock name %}`{.djangotemplate}, dont le contenu sera personnalisable par les templates enfants. + +---- + +Dans un [template de base]{.naming}, qui contient un document HTML classique, on a tendance à décrire des blocs personnalisables (via la balise `{% block %}`{.djangotemplate}) pour des sections du document qui sont souvent les mêmes, en voici une liste : + +- Le titre de la page (``) est souvent l'objet d'un bloc Django, nommé `title` ou `page_title` +- Si la page contient un titre ous forme de balise `<h1>`, son contenu peut faire l'objet d'un bloc Django, nommé `title` +- Dans la balise HTML `<head>`, on crée souvent un bloc Django destiné à ajouter des fichiers CSS, souvent nommé `css` +- Dans la balise HTML `<head>`, on crée souvent un bloc Django destiné à ajouter des fichiers JavaScript, souvent nommé `js` + +---- + +#### Template enfant + +```djangotemplate {.numberLines} +{% extends "path/to/base_template.html" %} +{% block body %} + Contenu de remplacement du block body, spécifique au template enfant +{% endblock body %} +``` + +Un template enfant hérite via la balise `{% extends %}`{.djangotemplate} de Django. Il peut ensuite uniquement modifier le contenu des blocs `{% block %}`{.djangotemplate} et `{% endblock %}`{.djangotemplate} de son template de base. Tout le reste du contenu du template enfant sera ignoré. + +---- + +#### Réutiliser des blocs dans un template enfant + +Dans un template enfant, lorsque vous personnalisez un bloc avec la balise `{% block %}`{.djangotemplate}, vous avez la possibilité de récupérer +le contenu original du bloc provenant du template parent, grâce à une variable de contexte nommée `block.super`{.djangotemplate}. + +```djangotemplate {.numberLines} +{% extends "path/to/base_template.html" %} +{% block body %} + {{ block.super }} + Contenu ajouté au block body, spécifique au template enfant +{% endblock body %} +``` + +---- + +#### Stratégies de création des templates + +En général, lorsque l'on souhaite exploiter les fonctionnalités d'héritage de templates, on essaie d'organiser l'arborescence des templates +de façon à faciliter leur maintenance. Quelques astuces à ce sujet : + +- Le **template de base** peut se trouver à la racine d'un dossier de templates personnalisé (voir la clé `["DIRS"]`{.python} de `settings.TEMPLATES`) +- Le **template de base** peut aussi se trouver à la racine du répertoire `templates` de l'application Django principale. +- Les autres templates se trouvent généralement dans un sous-dossier de `templates` portant le nom de l'application Django. +- Les autres templates peuvent être de plusieurs types : + - Template pour une page entière + - Template utilisé avec la balise Django `{% include %}`{.djangotemplate} + - Template pour générer le contenu d'e-mails + - etc. +- Tous ces templates peuvent être organisés dans des répertoires séparés indiquant leur usage. + +---- + +![Exemple d'organisation de répertoire de templates](assets/images/templating-folder-example.png) + +---- + +## Balises utiles + +Parmi les balises disponibles par défaut dans Django, quelques autres sont indispensables : + +| Balise | Description | +|-------------------------------------------------------------------|-------------------------------------------------| +| `{% url %}`{.djangotemplate} | Rendre une URL que l'on a nommée | +| `{% with %}`{.djangotemplate} et `{% endwith %}`{.djangotemplate} | Créer un contexte temporaire | +| `{% load %}`{.djangotemplate} | Chargement de tags et filtres | +| `{% csrf_token %}`{.djangotemplate} | Insérer un champ de sécurité dans un formulaire | + +---- + +### `{% url %}`{.djangotemplate} + +La balise `{% url %}`{.djangotemplate} permet de rendre une URL que l'on a nommée. Si nous avons défini l'URL suivante : + +```python {.numberLines} +from django.urls import path + +urlpatterns = [ + path("url/path/<type:param>", view_name, name="named"), +] +``` + +Nous pouvons insérer dans un template l'URL fixe, quelle qu'elle soit, en utilisant la balise `{% url %}`{.djangotemplate} comme suit. +Si l'URL exacte nécessite une valeur spécifique d'un argument ou de plusieurs arguments, nous pouvons utiliser la syntaxe suivante : + +```djangotemplate +<a href="{% url "named" param=value%}">Anchor text</a> +``` + +---- + +### `{% with %}`{.djangotemplate} + +Lorsque certaines expressions complexes sont nécessaires à plusieurs endroits dans un template, ex. : + +```djangotemplate {.numberLines} +{{ variable.method|filter1|filter2 }} +... +{{ variable.method|filter1|filter2 }} +``` + +On peut utiliser la balise `{% with %}`{.djangotemplate} pour créer des variables de contexte temporaires. +Cela permet d'utiliser un nom de variable simple pour référencer cette expression si longue à rédiger. + +```djangotemplate {.numberLines} +{% with name=variable.method|filter1|filter2 %} + {{ name }} + ... + {{ name }} +{% endwith %} +``` + +---- + +### `{% load %}`{.djangotemplate} + +Dans les templates de Django, la balise `{% load %}`{.djangotemplate} permet de charger des tags et des filtres personnalisés. +Par défaut, seuls les tags et filtres fournis par Django sont chargés. Lorsqu'une application fournit ses propres tags et filtres, il est possible de les charger via la balise `{% load %}`{.djangotemplate}, d'une manière similaire aux importations de modules Python. + +```djangotemplate {.numberLines} +{% load mytags %} +{% load myfilters %} +``` + +La balise sera généralement nécessaire lors de l'usage de fichiers statiques (ex. CSS et JavaScript). + +```djangotemplate {.numberLines} +{% load static %} +<link rel="stylesheet" href="{% static 'css/style.css' %}"> +``` + +---- + +## Balises et filtres personnalisés + +Django permet de développer ses propres filtres et tags, qui pourront être utilisables dans nos +templates. Une fois créés, nous pouvons les importer dans un template via la balise `{% load %}`{.djangotemplate}. + +Voir la [documentation officielle sur les filtres personnalisés](https://docs.djangoproject.com/fr/3.2/howto/custom-template-tags/#custom-template-tags-and-filters) + +:::notes +Faire la démo de l'écriture d'un filtre notamment et d'une balise simple, puis proposer un exercice. +::: + +---- + +## Fichiers statiques + +[Documentation de la gestion des fichiers statiques](https://docs.djangoproject.com/en/dev/howto/static-files/) + +Lorsque l'on sert du HTML, le document référence souvent des fichiers statiques tels que des feuilles de style CSS, des fichiers de code JavaScript ou encore des images spécifiques au site (bannière, logo, tout média qui n'est pas envoyé dynamiquement par les utilisateurs du site). + +Django simplifie l'utilisation de ces fichiers pour le développement, et offre également des commandes de gestion (`manage.py`) pour préparer +le passage en production. + +---- + +Il y a plusieurs étapes à suivre lorsque l'on souhaite utiliser des fichiers statiques en développement : + +1. Vérifier que `"django.contrib.staticfiles"` est présent `settings.INSTALLED_APPS` +2. Avoir un répertoire `static/` dans une ou plusieurs applications (ou créer un répertoire personnalisé) +3. Ajouter dans `settings.py` une liste dans `STATICFILES_DIRS` qui contient les répertoires `static` que vous avez créés +4. Ajouter `{% load static %}`{.djangotemplate} aux templates utilisant la balise `{% static %}`{.djangotemplate} +5. Référencer le chemin relatif de la ressource via `{% static "chemin/relatif.ext" %}` + +À l'exécution, et uniquement en mode `DEBUG = True`{.python}, le serveur de développement de Django sera capable +de trouver et servir correctement les ressources stockées dans les répertoires `static` au navigateur. + +---- + +![Utilisation de la balise static](assets/images/templating-tags-static.png) + diff --git a/documentation/05-forms.md b/documentation/05-forms.md new file mode 100644 index 0000000..b1cf17c --- /dev/null +++ b/documentation/05-forms.md @@ -0,0 +1,311 @@ +--- +title: Formulaires Django +author: Steve Kossouho +--- + +# Saisir des données avec les formulaires + +---- + +## Les formulaires HTML + +Le web n'a pas toujours eu de formulaires, mais c'est arrivé très tôt; Le navigateur [Mosaic du NCSA](https://en.wikipedia.org/wiki/NCSA_Mosaic) a introduit le +concept dans sa version 2.0, publiée en novembre 1993. Ils sont rentrés de facto dans la spécification HTML, qui a subi quelques améliorations depuis. + +En HTML, les formulaires permettent à l'utilisateur d'interagir et d'envoyer des données. +Django fournit une classe `Form`{.python} qui permet de facilement construire des formulaires et d'abstraire leur validation du contenu. + +---- + +## Les formulaires avec Django + +Pour pouvoir afficher des formulaires dans un site Django, il faut écrire des classes Python qui héritent d'une classe `Form`{.python} se trouvant dans le module `django.forms`{.python}. Les classes de formulaire décrivent les champs du formulaire à afficher, ainsi que leur type (texte, fichier, date, etc.) et leur contenu. + +```python {.numberLines} +from datetime import date +from django import forms + +class PersonForm(forms.Form): + """Exemple de formulaire avec des champs de types divers.""" + + first_name = forms.CharField(max_length=32, label="first name") + last_name = forms.CharField(max_length=32, label="last name") + birth_date = forms.DateField(initial=date(1990, 1, 1), label="birthday") + number = forms.IntegerField(label="phone number") + password = forms.CharField(widget=forms.PasswordInput, max_length=50, label="password") +``` + +Les classes de formulaire déclarent des attributs dont les types sont des instances de classes héritant de `Field`{.python}. + +[Documentation des champs de formulaires](https://docs.djangoproject.com/en/dev/ref/forms/fields/) + +---- + +## Utilisation de formulaires + +Une fois un formulaire défini, on peut l'utiliser dans une vue. + +```python {.numberLines} +from django.shortcuts import render +from demonstration.forms import PersonForm + +def view_person_form(request): + """Utilisation du formulaire PersonForm.""" + form = PersonForm(data=request.POST or None) + return render(request, "person-form.html", {"form": form}) +``` + +Lorsqu'une vue affiche un formulaire, elle doit l'initialiser. Un objet de formulaire peut être initialisé de deux manieres : + +- `PersonForm(data=request.POST)`{.python} si la requête HTTP est de type `POST`; +- `PersonForm()`{.python} si la requête HTTP est de type `GET`. + +Dans le premier cas, les données ``POST: dict``{.python} seront utilisées pour les associer à l'objet formulaire. +Dans le second cas, le formulaire sera vide et s'affichera sans contenu dans le template. + +---- + +## Utilisation des formulaires dans les templates + +Lorsque l'on a instancié un objet `Form`{.python}, nous l'envoyons au contexte du template. +Interpoler l'objet suffit à en afficher les champs. Les données liées à l'objet seront visibles dans les champs. + +```djangotemplate {.numberLines} +... +<body> + <h1>Person form</h1> + {# Envoyer le formulaire à l'URL courante avec la methode POST #} + <form method="post"> + {% csrf_token %} + {{ form }} + <input type="submit" value="OK"> + </form> +</body> +``` + +Notez que vous avez la responsabilité de fournir la balise `<form>`{.html} et la classique balise `<input type="submit">`{.html} pour faire fonctionner le formulaire. + +---- + +La représentation de l'objet `Form`{.python} dans un template peut prendre plusieurs formes. + +| Syntaxe | Description | +|-----------------------------------|---------------------------------------------------------------------------------------------| +| `{{ form }}`{.djangotemplate} | Affiche les champs dans des balises `<div>`{.html}. | +| `{{ form.as_p }}`{.djangotemplate} | Affiche les champs dans des balises `<p>`{.html}. | +| `{{ form.as_table }}`{.djangotemplate} | Affiche les champs dans des balises `<th>`{.html} et `<td>`{.html}. | + +Les options sont limitées mais suffisantes. Pour pouvoir utiliser un rendu compatible avec des bibliothèques CSS telles que Bootstrap, Bulma ou Foundation, on conseillera une application externe telle que [django-crispy-forms](https://django-crispy-forms.readthedocs.io/en/latest/index.html). + +---- + +### Attention lors du rendu + +Notez que l'interpolation d'un objet formulaire dans un template ne génère jamais les balises `<form></form>`{.html} nécessaires, +ni même la classique balise `<input type="submit">`{.html} pour faire fonctionner le formulaire. + +**La raison est simple** : les classes de formulaires existent notamment pour simpler +l'affichage et la validation des données. +Définir la destination de l'envoi des données et les boutons de validation est de la responsabilité du programmeur. +Techniquement, cela permet par exemple de cumuler plusieurs objets formulaire dans la même balise `<form>`{.html}, si besoin. + +---- + +## Validation des formulaires + +Un objet `Form`{.python} peut aussi gérer la validation des données. Par défaut, il est capable de s'assurer que tous les champs obligatoires +sont remplis et que les données sont valides. Nous verrons par la suite comment on peut personnaliser la validation. + +Pour confirmer dans la vue que des données expédiées par un formulaire sont correctes, nous devons utiliser la méthode `is_valid()`{.python}. + +```python {.numberLines} +from django.shortcuts import render, redirect +from demonstration.forms import PersonForm + +def view_person_form(request): + """Utilisation du formulaire PersonForm.""" + form = PersonForm(data=request.POST or None) + if form.is_valid(): + ... # Traitement des données + return redirect("accueil") + return render(request, "person-form.html", {"form": form}) +``` + +---- + +### La méthode `is_valid()`{.python} + +La méthode `is_valid()`{.python} d'un formulaire renvoie `True`{.python} si toutes les données associées au formulaire sont correctes et `False`{.python} sinon. +Cette méthode exécute plusieurs méthodes de validation de l'objet qui s'occupent entre autres de confronter les données entrantes et le type des champs. + +Si le formulaire contient des données considérées incorrectes, l'objet contiendra des attributs `.field_errors`{.python} et `.non_field_errors`{.python} contenant des messages d'erreur, +qui seront automatiquement affichés dans le template pour alerter l'utilisateur. + +---- + +```djangotemplate {.numberLines} +<form action="" method="post" name="person-form"> + {% csrf_token %} + <table class="form-table"> + {{ form.as_table }} + <tr> + <th></th> + <td><input type="submit" name="submit-form" value="Validate"></td> + </tr> + </table> +</form> +``` + +**Note** : La balise `{% csrf_token %}`{.djangotemplate} est une protection obligatoire lorsqu'on gère la méthode HTTP `POST`. Django est configuré par défaut pour qu'une page demandée via la méthode `POST` et qui ne possède pas une donnée de formulaire valide pour l'entrée `csrf_middleware_token` provoque une erreur HTTP 403. + +---- + +## Validation de la saisie utilisateur + +Django propose un système simple dans les formulaires permettant de contrôler la saisie de données des utilisateurs. +Le système consiste principalement à écrire des méthodes des types suivants : + +- `def clean_<field_name>() -> Any`{.python} : vérifie l'entrée du champ `field_name` +- `def clean() -> dict`{.python} : vérifier l'entrée de tous les champs + +---- + +Les méthodes `clean_<field_name>()`{.python} renvoient une donnée Python correspondant au type du champ, +ou peuvent lever une exception de type `ValidationError` si la donnée du champ est incorrecte ou non autorisée. + +La méthode `clean()`{.python}, elle, renvoie un dictionnaire de données, mais peut aussi vérifier +plusieurs champs à la fois. Elle peut également lever une `ValidationError`{.python}. +Dans ce cas, le formulaire contient des données dans `.non_field_errors`{.python}. + +:::notes +Jeter un œil à la documentation en ligne pour en voir un exemple. +::: + +---- + +```python {.numberLines} +from django import forms +class MyForm(forms.Form): + text = forms.CharField(max_length=10, label="Texte") + + def clean_text(self): + return self.data["text"] # Va aller dans le dictionnaire `self.cleaned_data` + + def clean(self): + return super().clean() # Renvoie un dictionnaire des données de champs "nettoyées" +``` + +---- + +## Gestion des téléchargements et fichiers média + +La gestion des téléchargements est toujours un cas un peu délicat, que Django permet de gérer. + +---- + +Pour gérer le téléchargement, il faut remplir quelques conditions : + +- Créer un répertoire +- Définir `settings.MEDIA_URL`{.python} et `settings.MEDIA_ROOT`{.python} +- Avoir un formulaire HTML avec la méthode `POST` et le type d'encodage + `<form enctype="multipart/form-data">`{.html}. + +Pour accéder aux fichiers uploadés : + +- Ajouter `+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)`{.python} aux URLs du + projet pour servir les fichiers (ne fonctionne pas si `settings.DEBUG = False`{.python}) + +---- + +```{.python .numberLines} +MEDIA_ROOT = BASE_DIR / "media" # par exemple +MEDIA_URL = "/media/" +``` + +```{.python .numberLines} +from django.conf import settings +from django.conf.urls.static import static +from django.urls import path, include + +urlpatterns = [ + path("", view_index, name="index"), +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +``` + +```{.djangotemplate .numberLines} +<form method="post" action="" enctype="multipart/form-data"> + {% csrf_token %} {{ form }} <input type="submit" value="OK"> +</form> +``` + +Le code (approximatif) à ajouter pour prendre en compte les fichiers média et pour pouvoir les +afficher. + +---- + +```{.python .numberLines} +class UploadForm(forms.Form): + """Example of file upload form.""" + image = forms.ImageField(max_length=128, required=True, label="image") + +def save_uploaded_file(request: HttpRequest) -> str: + """Custom method to process the upload of an image.""" + storage = FileSystemStorage() + image = request.FILES["image"] # l'attribut FILES est un dictionnaire de champs fichier + name = storage.save(None, image) + return storage.url(name) +``` + +Exemple de formulaire et d'une fonction à appeler pour sauver le fichier et récupérer son URL. + +---- + +```{.python .numberLines} +def view_upload_form(request: HttpRequest): # noqa + """View for the upload form.""" + form = UploadForm(request.POST, request.FILES) if request.method == "POST" else UploadForm() + if form.is_valid(): + image_url = save_uploaded_file(request) + return render(request, "template.html", {"form": form, "image_url": image_url}) +``` + +Exemple de vue utilisant notre formulaire + +---- + +## Divers : Formulaire de recherche + +L'écriture d'un formulaire de recherche est régie par les étapes suivantes : + +- Afficher (méthode GET) la page contenant uniquement le formulaire. +- Valider le formulaire vers la même page (POST ou GET). +- Si le formulaire est valide, ajouter les résultats au contexte du template. +- Dans le template, s'il y a des résultats, alors les afficher sous le formulaire. Sinon, n'afficher que le formulaire. + +---- + +## Divers : Applications tierces pour gérer les formulaires + +Il existe plusieurs applications dédiées à un affichage avancé des formulaires. La plus aboutie et réputée est `django-crispy-forms`. + +```bash {.numberLines} +pip install django-crispy-forms +``` + +[Page officielle de la documentation du package](https://django-crispy-forms.readthedocs.io/en/latest/) + +---- + +## Divers : Applications tierces pour déboguer vos pages + +Une application célèbre et utile pour déboguer vos pages en live est `django-debug-toolbar`. + +```bash {.numberLines} +pip install django-debug-toolbar +``` + +[Page officielle de la documentation du package](https://django-debug-toolbar.readthedocs.io/en/latest/) + +---- + +## Atelier diff --git a/documentation/06-orm-models.md b/documentation/06-orm-models.md new file mode 100644 index 0000000..963211f --- /dev/null +++ b/documentation/06-orm-models.md @@ -0,0 +1,1108 @@ +from django.http import Http404--- +title: Django +author: Steve Kossouho +--- + +# Persistance des données + +---- + +## ORM + +Encore très fréquemment, les projets utilisant une base de données utilisent du SQL brut. Les raisons +sont diverses : les développeurs sont plus à l'aise avec du SQL, le responsable de bases de données +insiste pour que la partie métier de l'intelligence du projet repose sur du code écrit directement +à l'intérieur de la base, ou encore le projet est très simple et ne nécessite pas plus que quelques requêtes. + +---- + +Dans la pratique, chaque SGBDR propose systématiquement sa propre variation de la syntaxe SQL standard, +qu'il s'agisse de `PRAGMA`{.sql}, de types non standard ou de fonctionnalités spécifiques. Si votre projet +est un minimum complexe, la probabilité que votre SQL soit incompatible avec d'autres moteurs de +base de données est très élevée. (grosso modo : vous êtes verrouillé) + +---- + +### ORM : concept + +Les ORM, ou **Object Relational Managers**, sont une couche d'abstraction dans les langages objet +pour gérer l'accès aux bases de données, généralement sans écrire de SQL. Ils nécessitent souvent +une courbe d'apprentissage plus ou moins abrupte, en plus de connaître les fondamentaux des bases de +données. En général, le « tarif d'entrée » demeure néanmoins très abordable. + +---- + +### ORM : avantages + +Les ORM présentent des avantages non négligeables sur le SQL brut : + +- Largement agnostiques (pas d'_a priori_ à part sur les fonctionnalités) +- Confortables à utiliser +- Systèmes de migrations (voir fin de chapitre) +- Requêtes basées sur le langage (Python ici) au lieu de SQL + +---- + +## ORM de Django + +L'ORM de Django est directement intégré au framework, et inutilisable sans un projet web spécifiquement +écrit pour Django. (les raisons sont diverses, ex. système de configuration, détection du code etc.) + +Cependant, cet ORM, étant intégré au framework, propose un système accessible et rapidement fonctionnel +pour communiquer avec une base de données. Pour prendre en main ce système, il y a peu d'étapes principales +à observer : + +1. Configurer les bases de données dans `settings.DATABASES`{.python} +2. Ajouter une _application_ dans `settings.INSTALLED_APPS`{.python} +3. Rédiger des classes de modèles dans ladite application + +---- + +### Paramètres sur les bases de données + +Première étape lorsque vous voulez utiliser des bases de données, configurer la ou les bases de données +que vous souhaitez exploiter dans votre projet. Django propose une variable de paramètres, `settings.DATABASES`. + +Cette variable est déjà définie par défaut pour utiliser SQLite, mais elle peut être +[configurée plus en détail](https://docs.djangoproject.com/en/dev/ref/settings/#databases), +pour pouvoir gérer en même temps plus bases de données, de types différents qui plus est ! + +```{.python .numberLines} +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'mydatabase', + } +} +``` + +Selon l'interface (`ENGINE`) que vous choisissez, les options disponibles peuvent varier et sont +documentées correctement sur le site officiel. + +---- + +## Définition des modèles de données + +Django détecte automatiquement la définition de vos modèles de données en allant les chercher dans +le module `models` des applications utilisées par votre projet. *(Notes : Ledit module `models` peut +aussi être défini sous la forme d'un package tant qu'il peut être importé et contient des définitions de modèle)* + +---- + +Pour définir un "modèle" de données, il faut, dans le module `models` de votre application, déclarer des classes héritant +de `django.db.models.Model` : + +```python {.numberLines} +from django.db import models + + +class MyModel(models.Model): + champ1 = models.CharField(max_length=32) +``` + +[Introduction sur les modèles](https://docs.djangoproject.com/fr/5.1/topics/db/models/) + +---- + +### Champs des modèles + +La définition d'une **classe** de données représente une table dans la base de données, et les champs de +la table sont représentés par des attributs dans la classe de modèle, qui sont des instances de certains types +spécifiques (`CharField` dans l'exemple précédent). + +Voici quelques exemples de champs : + +```python {.numberLines} +from django.db import models + +class Subscriber(models.Model): + name = models.CharField(max_length=32) # caractères + level = models.IntegerField(verbose_name="niveau") # nombre entier signé + is_active = models.BooleanField(default=True) # vrai ou faux +``` + +[Champs disponibles pour les modèles](https://docs.djangoproject.com/fr/5.1/ref/models/fields/#field-types) + +---- + + +### Concepts des ORM + +| Objet | Correspond à... | +|--------------------|----------------------------| +| Classe de modèle | Table de base de données | +| Attribut du modèle | Colonne dans la table | +| Instance du modèle | Enregistrement de la table | + + +---- + +### Arguments des champs de modèles + +Les initialiseurs de champs possèdent tous des arguments, certains communs à la plupart des champs, +certains obligatoires etc. (voir [Référence des champs de modèle](https://docs.djangoproject.com/en/dev/ref/models/fields/) dans +la documentation officielle) + +| Champ de Modèle | Attributs Obligatoires | +|-------------------|------------------------| +| `CharField` | `max_length` | +| `IntegerField` | Aucun | +| `BooleanField` | Aucun | +| `ForeignKey` | `to`, `on_delete` | +| `OneToOneField` | `to`, `on_delete` | +| `ManyToManyField` | `to` | + +---- + +### Arguments fréquemment utilisés + +| Arguments fréquemment utilisés | Description | +|--------------------------------|-----------------------------------------| +| `default` | Valeur par défaut | +| `verbose_name` | Nom de champ | +| `db_index` | Ajouter un index sur la base de données | +| `null` | Valeur `NULL` | +| `blank` | Valeur `''` ou sélection facultative | +| `unique` | Ajouter une contrainte unique | + +---- + +### Exemple de définition de modèle + +```python {.numberLines} +from django.db import models +from uuid import uuid4 +from datetime import date + +class Author(models.Model): + uuid = models.UUIDField(default=uuid4, db_index=True, verbose_name="UUID") + first_name = models.CharField(max_length=64, blank=False, verbose_name="first name") + last_name = models.CharField(max_length=64, blank=False, verbose_name="last name") + description = models.TextField(blank=True, verbose_name="description") + birth_date = models.DateField(default=date(2000, 1, 1), verbose_name="birth date") + registration_date = models.DateTimeField(auto_now_add=True, verbose_name="registration date") + + class Meta: # Django models metadata + verbose_name = "author" + verbose_name_plural = "authors" +``` + +Par défaut, chaque modèle contient automatiquement un champ `id` qui est la [clef primaire]{.naming} de la table. + +---- + +### Classe Meta des modèles (options) + +Lorsque l'on définit des classes de modèles et des champs pour ces modèles, on peut se rendre compte +qu'il n'y a pas de solution évidente pour exprimer des contraintes s'appliquant à plusieurs champs, par exemple. + +*Imaginez par exemple les [index sur plusieurs champs]{.naming}, l'[unicité sur plusieurs champs]{.naming}...* + +Django propose un système d'options pour nos classes de modèle, les [options Meta]{.naming}. +Ce système permet de définir plusieurs informations sur la table associée au modèle. Pour +les définir sur un modèle, vous devez déclarer une classe nommée `Meta` à l'interieur +de votre modèle, et y ajouter des [options](https://docs.djangoproject.com/en/dev/ref/models/options/) sous la forme d'attributs dans cette classe. + +---- + +```python +from django.db import models + + +class Person(models.Model): + first_name = models.CharField(max_length=32) + last_name = models.CharField(max_length=32) + + class Meta: + verbose_name = "personne" + verbose_name_plural = "personnes" + unique_together = [("first_name", "last_name")] +``` + +Vous pouvez déclarer de nombreux attributs à la classe `Meta`, tels que ceux décrits +dans la [documentation sur les options Meta](https://docs.djangoproject.com/en/dev/ref/models/options/). +Une bonne pratique est de définir au moins les attributs `verbose_name` et `verbose_name_plural`, +dont l'objectif est de fournir des noms décrivant les modèles au niveau de l'interface utilisateur. + +---- + +## Migrations + +Avant la sortie de Django 1.7 (2016), la gestion des modèles et de la base de données était +très simple; vous définissiez un modèle, et Django vous proposait une commande de gestion +`manage.py syncdb`, qui envoyait une commande SQL de création de table, selon ce que proposait +votre modèle. + +**Problème** : Si votre modèle venait à évoluer après avoir lancé la commande `manage.py syncdb`, +Django ne pouvait plus mettre à jour votre base de données avec les changements. + +Django 1.7 introduit la gestion des migrations, où vous pouvez faire évoluer votre modèle +et mettre à jour votre base de données de façon différentielle/cumulative. + +[Documentation sur les migrations](https://docs.djangoproject.com/en/dev/topics/migrations/) + +---- + +### Concept des migrations + +Une migration est un delta de modifiations à appliquer à une base de données pour que +son schéma soit cohérent avec le code des modèles. Imaginons le scénario suivant : + +Nous réalisons un site web avec des fiches de personnages pour un jeu. Nous avons une visibilité +faible sur les objectifs à long terme, donc nous fabriquons notre contenu au fur et à mesure. + +```python {.numberLines} +from django.db import models + +class Character(models.Model): + name = models.CharField(max_length=32) + race = models.CharField(max_length=32) + level = models.IntegerField() +``` + +---- + +Nous créons une première version du modèle `Character`, mais nous devons créer une migration avec la commande `manage.py makemigrations` +avant que la base de données ne soit raccord avec notre modèle. La migration est un fichier +qui indique à Django quelles modifications appliquer pour que la base corresponde au modèle. + +Dans notre cas, une migration initiale pour l'application sera créée et contiendra des +opérations pour créer la table `Character`. Nous modifierons la base avec la commande `manage.py migrate`, +qui va appliquer notre migration. + +---- + +Imaginons ensuite que nous nous rendions compte que nous souhaitons ajouter à notre modèle +des informations telles que des points de vie ou de mana. Nous créons alors deux nouveaux champs : + +```python {.numberLines} +from django.db import models + +class Character(models.Model): + name = models.CharField(max_length=32) + race = models.CharField(max_length=32) + level = models.IntegerField() + hp = models.IntegerField(default=10) # nouveau + mp = models.IntegerField(default=0) # nouveau +``` + +Si nous recréons une migration avec la commande `manage.py makemigrations`, nous obtenons +un second fichier de migration contenant des opérations pour ajouter ces champs. Pour parvenir à +créer une migration ou l'appliquer, il faudra souvent faire attention aux contraintes sur les champs, +ie. **un champ n'acceptant pas de valeurs nulles doit être créé avec une valeur par défaut**, etc. + +---- + +### Conservation des migrations + +Une fois que vous avez créé une migration, et que vous l'avez appliquée, **vous devez la conserver +dans votre application**; en effet, quelqu'un qui souhaite utiliser votre application dans son projet +doit mettre à jour sa base de données en appliquant les deltas de toutes les migrations. + +Ce système permet aussi bien à des développeurs ne possédant pas l'application qu'à +des développeurs en possédant une version ancienne d'introduire les nouveautés à leurs projets. + +---- + +## Interagir avec la base de données + +Une fois que l'on a défini nos premières classes de modèles, créé des migrations, et appliqué ces dernières, +on peut commencer à interroger la base pour récupérer des données ou en ajouter. Pour découvrir l'API +de l'ORM de Django, nous pouvons lancer un shell avec la commande `manage.py shell`. + +Malheureusement, ladite console oblige à importer ses modèles à chaque lancement (Django 5.2, prévu en avril 2025, introduira l'import automatique des modèles). + +Jusqu'à Django 5.1 inclus, vous pouvez vous faciliter l'existence avec deux bibliothèques Python/Django : + +| Package | Description | +|--------------------|-------------------------------------------------| +| `ipython` | shell Python avancé (autocompletion etc.) | +| `django_extensions`| shell Django avec import automatique de modèles | + +La [documentation de `django_extensions`](https://django-extensions.readthedocs.io/en/latest/installation_instructions.html) +précise comment démarrer. + +---- + +### Démarrer la console interactive Django + +Installez d'abord les bibliothèques : + +```bash {.numberLines} +pip install django-extensions ipython +``` + +Ajoutez `django_extensions` à `settings.INSTALLED_APPS`{.python} puis lancez la commande de gestion `shell_plus` : + +```bash {.numberLines} +python manage.py shell_plus +``` + +---- + +### Créer des enregistrements + +Considérons à nouveau notre modèle `Character` : + +```python {.numberLines} +from django.db import models + +class Character(models.Model): + name = models.CharField(max_length=32) + race = models.CharField(max_length=32) + level = models.IntegerField() + hp = models.IntegerField(default=10) + mp = models.IntegerField(default=0) +``` + +---- + +Il existe deux façons de créer un enregistrement : + +```python {.numberLines} +# Création indirecte +cyril = Character(name="Cyril", race="Elf", level=1, hp=50, mp=10) +lorna = Character(name="Lorna", race="Elf", level=1, hp=50, mp=10) +cyril.save() +lorna.save() +``` + +Nous créons deux instances de `Character` et les enregistrons dans la base de données. + +---- + +Nous pouvons également utiliser une méthode du modèle pour créer un enregistrement. +Nous passons par un attribut du modèle nommé `objects`, qui est un gestionnaire permettant +d'effectuer des requêtes sur la base de données. + +```python {.numberLines} +# Création directe +cyril = Character.objects.create(name="Cyril", race="Elf", level=1, hp=50, mp=10) +lorna = Character.objects.create(name="Lorna", race="Elf", level=1, hp=50, mp=10) +``` + +---- + +### Récupérer un enregistrement + +Il arrive régulièrement de souhaiter récupérer un enregistrement de la base, et un seul. +Par exemple, si une vue de votre projet permet d'afficher les informations d'une entité de +votre base de données, vous devez retrouver l'entité, ou afficher une page 404 si elle n'existe pas. + +Le gestionnaire `objects` du modèle possède une méthode `get()` qui permet de retrouver un enregistrement +et un seul. Par exemple : + +```python {.numberLines} +from django.http import Http404 + +def show_character(request, name: str): + try: + character = Character.objects.get(name=name) + ... + except Character.DoesNotExist: + raise Http404("Character not found") +``` + +L'exemple ci-dessus est une vue qui affiche une page 404 si l'enregistrement n'a pas été trouvé. + +---- + +### Récupérer tous les enregistrements d'une table + +L'usage le plus courant des bases de données est de requêter à la recherche de plusieurs enregistrements. +Django propose un type d'objet representant une liste d'enregistrements, appelé `QuerySet`. Chaque élément +d'un `QuerySet` est logiquement une instance du modèle. + +Un `QuerySet` est obtenu lorsqu'on appelle certaines méthodes sur le gestionnaire `objects` du modèle. +Un `QuerySet` possède également des méthodes communes au gestionnaire `objects`. + +```python {.numberLines} +queryset = Character.objects.all() +counter = queryset.count() # renvoie le nombre d'enregistrements + +# Un QuerySet est un itérateur +for character in queryset: + # Chaque élément est un objet de type Character + print(character.name) +``` + +---- + +### Filtrer les enregistrements + +Pour filtrer vos enregistrements correspondant à des critères de recherche, les gestionnaires +`objects` et les `QuerySet` possèdent des méthodes `filter` et `exclude`. Par exemple, si je souhaite récupérer les +enregistrements dont la colonne race possède **exactement** le texte `Elf` : + +```python {.numberLines} +queryset = Character.objects.filter(race="Elf") +``` + +Il suffit de passer des arguments possédant le nom de colonnes du modèle. +Vous pouvez passer plusieurs arguments pour filtrer davantage sur plusieurs colonnes (avec un opérateur `AND`). + +---- + +### Exclure des enregistrements + +C'est assez rare, mais si vous devez exclure des enregistrements, probablement d'un `QuerySet`, n'oubliez pas la méthode `exclude`. +Si vous souhaitez par exemple récupérer tous les elfes dont le nom n'est pas `"Lucas"` : + +```python {.numberLines} +queryset = Character.objects.filter(race="Elf").exclude(name="Lucas") +``` + +---- + +### Filtrer en `AND` et `OR` + +Pour filtrer simplement avec `filter`, vous pouvez passer des arguments nommés comme vos colonnes... mais cette +technique génère uniquement une combinaison de filtres avec l'opérateur `AND`. + +Pour choisir des combinaisons de filtres `AND` ou `OR`, vous devez plutôt passer aux méthodes `filter` ou `exclude` +un argument positionnel composé d'objets de type `Q` : + +```python {.numberLines} +from django.db.models import Q + +level1_elves = Character.objects.filter(Q(race="Elf") & Q(level=1)) +level1_or_11 = Character.objects.filter(Q(level=1) | Q(level=11)) +``` + +---- + +### Filtrer autrement qu'avec des valeurs exactes + +Jusque là, notre seul outil pour filtrer est d'utiliser des arguments nommés tels les colonnes de nos modèles. Cependant, +cette méthode ne nous permet de filtrer que par valeurs précises. Pourtant, il paraît facile d'imaginer que l'on voudrait +filtrer, par exemple, sur des personnages dont le niveau excède une valeur, ou dont le nom commence par une lettre. + +Django nous offre cette possibilité, en utilisant... encore des arguments nommés. Par exemple, si nous voulons obtenir +tous les personnages dont le niveau est supérieur ou égal à `10`, nous pouvons faire + +```python {.numberLines} +queryset = Character.objects.filter(level__gte=10) +``` + +Le texte `__` est utilisé par Django pour découper le nom de l'argument en plusieurs parties. La première partie, `level`, +indique que nous travaillons sur le champ `level` du modèle `Character`. La deuxième partie, `gte`, indique à Django que nous +voulons utiliser un opérateur de comparaison. Dans ce cas, le mot `gte` signifie `greater than or equal to`. + +---- + +Les opérateurs proposés par Django permettent de modifier la comparaison ou de calculer des transformations sur un champ : + +| Opérateur | Description | +|-----------|---------------------------| +| `__gt` | plus grand que | +| `__lt` | plus petit que | +| `__gte` | égal ou supérieur à | +| `__lte` | égal ou inférieur à | +| `__range` | intervalle (tuple) | +| `__in` | fait partie d'un ensemble | + +Ces opérateurs fonctionnent sur les champs de type `int`, `float`, `date` et `datetime`. +Potentiellement, on peut aussi utiliser ces opérateurs sur les champs de type `bool` et `str`. + +---- + +Pour les chaînes de caractères, Django offre des opérateurs supplémentaires : + +| Opérateur | Description | +|----------------------------|------------------------------------------------| +| `__exact` | identique à aucun opérateur | +| `__iexact` | rechercher en ignorant la casse des caractères | +| `__startswith/istartswith` | commence par cette chaîne | +| `__endswith/iendswith` | commence par cette chaîne | +| `__contains/icontains` | contient cette chaîne | + +---- + +Voici un exemple d'usage de cette catégorie d'opérateurs : + +```python {.numberLines} +# Récupérer les personnages ayant un nom contenant "Lucas" en ignornant la casse +queryset = Character.objects.filter(name__icontains="lucas") +``` + +---- + +Ce n'est pas fini, il existe aussi des opérateurs pour extraire des informations depuis des `datetime` et `date` : + +| Opérateur | Description | +|-------------|-------------------------------| +| `__year` | extrait l'annee | +| `__month` | extrait le mois | +| `__day` | extrait le jour | +| `__hour` | extrait l'heure | +| `__minute` | extrait les minutes | +| `__second` | extrait les secondes | +| `__week` | extrait la semaine | +| `__isoweek` | extrait la semaine ISO | +| `__weekday` | extrait le jour de la semaine | + +---- + +Voici un autre exemple, qui est intéressant lorsque des opérateurs récupèrent une partie d'une information : + +```python {.numberLines} +# Récupérer des rendez-vous de lundi et mardi +queryset = Appointment.objects.filter(when__weekday__in=(0, 1)) +``` + +Vous pouvez effectivement extraire le jour de la semaine d'une date (sous la forme d'un `int`{.python}) et comparer cette valeur en +y apposant un opérateur de comparaison. + +---- + +### Ordonner les enregistrements + +Vous pouvez organiser les résultats de vos `QuerySet` avec la méthode `order_by`. + +```python {.numberLines} +# Trier par nom croissant +by_name_asc = Character.objects.filter(race="Elf").order_by("name") +# Trier par nom, en ordre décroissant alphanumériquement (dépendant de la collation) +by_name_desc = Character.objects.filter(race="Elf").order_by("-name") +# Mélanger aléatoirement les résultats +random_order = Character.objects.filter(race="Elf").order_by("?") +# Trier par niveau croissant, puis par nom, en ordre décroissant +by_level_asc_and_name_desc = Character.objects.filter(race="Elf").order_by("level", "-name") +``` + +---- + +## Créer et gérer des relations entre enregistrements + +Les bases de données relationnelles ont la particularité de permettre la définition de relations entre tables. +Django vous propose des champs de modèles permettant de gérer les relations classiques. Il en existe 3 types : + +| Type de relation | Description | +|-------------------|------------------------------------| +| `OneToOneField` | une relation 1 à 1 | +| `ForeignKey` | une relation 1 à N | +| `ManyToManyField` | une relation plusieurs à plusieurs | + +---- + +## Relations 1 à 1 (`OneToOneField`) + +Une relation 1 à 1 vous permet de créer une relation bijective entre les éléments de deux tables. Par exemple, si +vous utilisez un modèle que vous n'avez pas l'autorisation de modifier, mais que vous avez besoin d'attacher des données +supplémentaires à celui-ci, vous pouvez créer un modèle avec une relation 1 à 1 vers celui-ci. + +```python {.numberLines} +from django.db import models + +class User(models.Model): + """Utilisateur. Modèle en lecture seule.""" + username = models.CharField(max_length=30) + +class Profile(models.Model): + # Un utilisateur peut avoir un seul profil + user = models.OneToOneField("application.User", on_delete=models.CASCADE) + picture = models.ImageField(max_length=256, null=True, upload_to='pictures', verbose_name='picture') +``` + +Ici, une clé étrangère avec contrainte d'unicité sera créée par Django. Le premier argument de `OneToOneField` est +une classe de modèle, ou un `str` contenant le nom du modèle (recommandé). Le deuxième argument est `on_delete`, qui permet de +définir ce qui doit se passer lorsque l'utilisateur est supprimé. Dans notre cas, on veut supprimer le profil aussi. + +---- + +### Valeurs possibles de l'argument `on_delete` + +Les champs `ForeignKey` et `OneToOneField` permettent de choisir le comportement à adopter lorsque +les enregistrements ciblés par une relation sont supprimés. + +| Valeur | Description | +|----------------------|-----------------------------------------------| +| `models.DO_NOTHING` | ne rien faire. Impossible la plupart du temps | +| `models.SET_NULL` | mettre `null`. Nécessite `null=True` | +| `models.SET_DEFAULT` | mettre une valeur `default` | +| `models.CASCADE` | supprimer en cascade | +| `models.PROTECT` | interdire la suppression | + +---- + +### Utilisation des relations 1 à 1 + +Une fois que vous avez un modèle possédant une clé étrangère en 1-1, vous avez accès la clé sur les deux modèles; dans notre cas, +vous aurez accès à : + +| Modèle | Champ du modèle | +|-----------|------------------------| +| `Profile` | `user` | +| `User` | `profile` (par défaut) | + + + +```python {.numberLines} +# Créer un utilisateur +user = User.objects.create(username="bob") +# Créer son profil +profile = Profile.objects.create(user=user) +# Afficher le profil de bob +print(user.profile) +# Afficher l'utilisateur du profil de bob +print(profile.user) +``` + +---- + +Attention toutefois, si l'utilisateur n'a pas de profil associé, `user.profile` générera une exception de type +`RelatedObjectDoesNotExist`. Il faudra intercepter ce cas de figure si nécessaire. + +```python {.numberLines} +try: + print(user.profile) +except Profile.DoesNotExist: + print("No profile for this user") +``` + +---- + +## Relations 1 à N (`ForeignKey`) + +Une relation 1 à N vous permet de créer une relation libre entre les éléments de deux tables. Par exemple, si +on souhaite associer une classe à nos personnages (la classe possédant des propriétés), on peut imaginer déclarer +un modèle de classe : + +```python {.numberLines} +from django.db import models + +class CharacterClass(models.Model): + name = models.CharField(max_length=30) + description = models.TextField() + +class Character(models.Model): + name = models.CharField(max_length=32) + race = models.CharField(max_length=32) + level = models.IntegerField() + character_class = models.ForeignKey("CharacterClass", on_delete=models.PROTECT, related_name="characters") +``` + +---- + +### L'argument `related_name` + +L'argument `related_name` permet de choisir le nom de la relation inverse, par exemple si vous avez un modèle de +personnage et un modèle de classe, vous pouvez choisir de nommer la relation inverse de `character_class` en +`characters`. Vous pouvez effectuer des requêtes avec les relations : + +```python {.numberLines} +# Récupérer tous les personnages de la classe Mage +characters = Character.objects.filter(character_class__name="Mage") + +# Récupérer toutes les classes ayant des personnages de race Elf +# Attention, il peut y avoir des doublons ! +classes = CharacterClass.objects.filter(characters__race="Elf") +``` + +**Si vous ne précisez pas** de `related_name`, Django choisira un nom automatiquement. Par exemple, si vous avez un modèle de +personnage `Character` et un modèle de classe `CharacterClass`, le nom de la relation inverse de `character_class` sera `character_set`. + +---- + +### Gestion des doublons + +Dès lors que vous effectuez des requêtes impliquant des relations (jointures en SQL), il peut y avoir des doublons. +Par exemple, si on souhaite récupérer les classes des personnages de race Elf, votre `QuerySet` pourrait contenir +des éléments en double; la raison est très simple : + +La table de résultats de la jointure générée par la base de données SQL ressemblera au tableau suivant +(les colonnes de gauche concernent `CharacterClass`, celles de droite `Character`) : + +| `id` | `name` | `description` | `id` | `name` | `race` | `level` | `character_class_id` | +|------|--------|---------------|------|--------|--------|---------|----------------------| +| 1 | Mage | Mage | 1 | Lorna | Elf | 1 | 1 | +| 1 | Mage | Mage | 2 | Cyril | Elf | 1 | 1 | +| 2 | Cleric | Cleric | 3 | Aaron | Elf | 1 | 2 | +| 2 | Cleric | Cleric | 4 | Diane | Elf | 1 | 2 | + +---- + +Vous récupérez donc deux fois la classe `Mage`, et deux fois la classe `Cleric`. Pour dédupliquer +les résultats, vous pouvez utiliser la méthode `distinct` sur votre `QuerySet` : + +```python {.numberLines} +# Récupérer toutes les classes ayant des personnages de race Elf +classes = CharacterClass.objects.filter(characters__race="Elf").distinct() +``` + +---- + +## Relations N à N (`ManyToManyField`) + +Les bases de donné + + + + + + +Les opérateurs présentés ci-dessus sont disponibles dans la documentation officielle de Django à +cette page : + +[Opérateurs de recherche](https://docs.djangoproject.com/en/dev/ref/models/querysets/#field-lookups) + +---- + +### Filtrage du type critère1 OU critère2 + +Par défaut, avec `filter`, passer plusieurs arguments de critères permet de retourner un `QuerySet` +qui répond à **tous** les critères (critère1 ET critère2 etc…). Pour combiner des critères avec des +opérateurs ET/OU, il faut utiliser +des [objets Q](https://docs.djangoproject.com/en/dev/topics/db/queries/#complex-lookups-with-q-objects) : + +```python +from django.models import Q + +MyModel.objects.filter(Q(name="Connelly") | Q(name="Hugo")) +``` + +---- + +### Récupérer un seul objet plutôt qu'un QuerySet + +Des fois, plutôt que de récupérer un `QuerySet`, on voudra faire une requête pour savoir si un et un +seul objet existe dans notre base (ex. trouver un objet avec un id particulier pour une page de +consultation) + +---- + +Le manager `Model.objects` propose une méthode `get()` qui permet de récupérer directement un objet +plutôt qu'un `QuerySet`, mais qui renvoie une erreur s'il n'existe pas un et un seul objet dans la +table répondant à nos critères. Elle s'utilise de la même façon que la méthode `filter()`. + +```python +# peut lever `ObjectDoesNotExist` ou `MultipleObjectsReturned` +item = MyModel.objects.get(id=19) +``` + +---- + + + +## Définition des relations entre tables + +Les bases de données relationnelles ont, comme leur nom l'indique, des champs dont le rôle est +de définir et contraindre des relations entre tables. Django prend ce type de champs en charge via des +classes de champs spécifiques : + +| Champ de Modèle | Description | +|-------------------|--------------------------------| +| `ForeignKey` | relation plusieurs à un | +| `OneToOneField` | relation un à un | +| `ManyToManyField` | relation plusieurs à plusieurs | + +---- + +### Exemple de modèle avec une relation + +```python {.numberLines} +from django.db import models + +class Category(models.Model): + """Catégorie d'article.""" + name = models.CharField(max_length=32) + +class Article(models.Model): + """Article de blog.""" + title = models.CharField(max_length=32) + category = models.ForeignKey("application.Category", on_delete=models.CASCADE) +``` + +---- + +Lorsqu'on ajoute un champ de relation, Django nous oblige à indiquer quelle stratégie +adopter lorsqu'un enregistrement ciblé par une relation est supprimé. Ce comportement doit être +indiqué via un argument nommé `on_delete` dans la définition du champ : + +```python +from django.db import models + +class MyModel(models.Model): + relation = models.ForeignKey("application.Model", on_delete=models.CASCADE) +``` + +---- + +Lorsqu'un enregistrement ciblé par une relation est supprimé, les stratégies les plus fréquemment +utilisées sont les suivantes : + +- `models.CASCADE` : supprimer également l'enregistrement qui contient la relation +- `models.SET_NULL` : si autorisé, retirer la relation de l'objet source +- `models.SET_DEFAULT` : si `default` est précisé, remplacer la relation par la valeur par défaut +- `models.PROTECT` : empêcher la suppression de la cible et lever une exception + +---- + + +Lorsque notre définition de modèles est prête, nous pouvons créer notre base et/ou nos tables en +utilisant des commandes de gestion de notre projet Django : + +```shell_script +./manage.py makemigrations <appname> # créer des migrations +./manage.py migrate # met à jour la base en suivant les migrations créées +``` + +---- + +## Héritage, proxy et modèles abstraits + +Avec Django, il est possible d'utiliser une classe de modèle héritant d'une autre. + +```python +class MyModel2(MyModel): + ... +``` + +---- + +### Héritage multi-tables + +Lorsque l'on hérite d'une autre classe de modèle de cette façon, Django fait une chose très +particulière : les objets de `MyModel2` possèdent un lien vers un objet de `MyModel1`. Le système +est compliqué à utiliser mais est bien documenté +sur [le site officiel](https://docs.djangoproject.com/en/dev/topics/db/models/#multi-table-inheritance) +. + +---- + +Plus intéressant à mon sens, Django propose ce que l'on appelle des modèles abstraits. +Il s'agit simplement de classes de modèles contenant des options `Meta` où l'attribut `abstract` est +à `True`. Qu'est-ce que ça veut dire en pratique ? + +---- + +Un modèle possédant une option `Meta.abstract = True` n'est en fait pas créé dans la base de +données, mais on peut en hériter et profiter de ses champs déjà créés. + +:::notes En montrer un exemple avec un modèle abstrait possédant uuid, nom et description. +::: + +---- + +Moins intéressant à un niveau débutant que le modèle abstrait, le modèle dit "proxy" permet +d'accéder aux données d'un autre modèle, mais permet d'ajouter ses propres méthodes sans toucher au +modèle original. (typiquement ce qui se produit lorsqu'on souhaite augmenter des modèles d'une +application que l'on a pas écrite) + +---- + +Pour écrire un modèle proxy, il suffit de définir son option Meta `proxy = True` : + +```python +from django.db import models + + +class MyModel(models.Model): + field1 = models.Field() # incorrect, juste pour l'exemple + + class Meta: + proxy = True +``` + +---- + + +---- + +### Requêter avec des `ForeignKey` et `ManyToManyField` etc. + +Lorsque vos modèles possèdent des champs de relations, Django vous fournit des moyens efficaces +d'effectuer des requêtes parcourant lesdites relations (comme vous le feriez en SQL avec des `JOIN`) +. + +---- + +Par exemple, si vous possédez un modèle `A` possédant une `ForeignKey` nommée `b` vers le modèle `B` +, vous pouvez filtrer vos résultats de `A` de plusieurs façons : + +```python +b_instance = "Imaginez une instance de B" +qs = A.objects.filter(b=b_instance) +qs = A.objects.filter(b__field="valeur") +qs = A.objects.filter(b__field2__gt=0) +``` + +---- + +Et encore mieux, avec l'existence d'une clé étrangère depuis `A` vers `B`, Django vous propose +implicitement de faire des requêtes sur `B` en filtrant sur des relations avec `A`, via le nom du +modèle en minuscules : + +```python +qs = B.objects.filter(a="Imaginez une instance de A") +qs = B.objects.filter(a__field=valeur) +qs = B.objects.filter(a__autre_relation__field=valeur) +``` + +---- + +Lorsque plusieurs relations miroir existent vers le même modèle, Django va vous afficher une erreur, +car le nom généré par Django des deux relations miroir provoque un conflit. Dans ce cas-là, il faut explicitement +configurer le nom d'attribut généré automatiquement pour la relation miroir : + +```python +from django.db import models + +# L'argument `related_name` permet de définir le nom du champ miroir dans la table cible +field = models.ForeignKey( + "app.Model", + on_delete=models.CASCADE, + related_name="nouveau_nom") +``` + +---- + +Une fois ce `related_name` défini, vos requêtes utilisant la relation miroir deviennent : + +```python +qs = B.objects.filter( < nouveau_nom > __field = valeur) +``` + +---- + +Les règles énoncées précédemment s'appliquent de la même façon aux champs `OneToOneField` +et `ManyToManyField`. + +---- + +## Enregistrer et modifier des données dans la base + +Pour créer de nouveaux objets dans notre table, c'est assez simple : + +```python +item = MyModel(field1="", field2=0…) +item.save() # crée le nouvel objet et lui attribue un nouvel `id` +``` + +---- + +Pour modifier et mettre à jour un objet, c'est simple. Récupérez un objet qui a déjà un `id` (avec +la méthode `get()` par exemple), modifiez ses champs et sauvez-le : + +```python +item = MyModel.objects.get(id=19) +item.field3 = "nouvelle valeur" +item.save() +``` + +---- + +On peut aussi mettre à jour tout un `QuerySet` d'un coup : + +```python +# Ici on va changer la valeur du champ `field1` dans toute la table +MyModel.objects.all().update(field1=valeur) +``` + +---- + +### Aparté sur les `ManyToManyField` + +Les champs `ManyToManyField` sont gérés de façon assez spécifique, car Django génère automatiquement +des tables intermédiaires dans la base de données. +La [documentation officielle](https://docs.djangoproject.com/en/dev/topics/db/examples/many_to_many/) +fournit des exemples de l'utilisation de ce champ. + +---- + +## Exécution de requêtes en SQL brut (déconseillé) + +Django autorise l'écriture de requêtes en utilisant du SQL, mais comme la documentation l'indique, +dans 95% des cas vous pourriez en réalité utiliser l'ORM pour faire ce que vous souhaitez. + +:::notes Aller sur la page de la doc officielle +::: + +---- + +## Système de migration de schéma + +![Concept de migrations](assets/images/orm-model-migration-concept.png) + +---- + +À chaque fois que vous modifiez vos modèles, il faut créer un delta, un fichier de migration qui +contient la différence entre l'état précédent de vos modèles et le nouvel état : + +```bash +./manage.py makemigrations <nom app> +``` + +---- + +Si cela est nécessaire, cela crée un nouveau fichier de migration dans le répertoire `migrations` de +notre application. +Vous pourrez ensuite, sur les serveurs où votre application a déjà été utilisée, parvenir facilement +à une base de données cohérente avec la nouvelle définition de vos modèles en utilisant : + +```bash +./manage.py migrate +``` + +---- + +## Modèles abstraits + +Les modèles abstraits, qu'est-ce que c'est ? Ce sont des classes de modèles qui peuvent vous servir +de base pour composer vos modèles. + +Par exemple, si vous souhaitez avoir quatre modèles qui ont tous en commun un champ `description` +, `uuid` et une méthode `get_short_description()`, vous pourriez définir quatre fois ces champs et +la même méthode... ou alors + +---- + +```python +from django.db import models +from uuid import uuid4 + + +class Model1(models.Model): + description = models.TextField(verbose_name="Description") + # Notez que la valeur par défaut est une référence de fonction + uuid = models.UUIDField(default=uuid4, verbose_name="UUID") + + def get_short_description(self): + return self.description[:100] +``` + +Sans compter les champs spécifiques à chaque modèle, il faudrait réécrire la même chose 4 fois ! + +---- + +```python +from django.db import models +from uuid import uuid4 + + +class BaseModel(models.Model): + description = models.TextField(verbose_name="Description") + uuid = models.UUIDField(default=uuid4, verbose_name="UUID") + + class Meta: + abstract = True # Ici, le modèle n'est pas pris en compte pour les migrations + + def get_short_description(self): + return self.description[:100] + + +class Model1(BaseModel): # On hérite de BaseModel et donc de Model + pass +``` + +Avec une classe abstraite, nous avons écrit les infos communes une seule fois, et hérité 4 fois. + +---- + +## Récapitulatif minimal des modèles + +1. Créer une application pour contenir un ou plusieurs modèles +2. Créer un ou plusieurs modèles héritant de `django.models.Model` +3. S'assurer que le modèle contient bien toutes les informations nécessaires +4. Créer un nouveau fichier de migration (cliché des modèles à un instant t) +5. Appliquer la définition des nouveaux fichiers de migration à la base diff --git a/documentation/07-model-forms.md b/documentation/07-model-forms.md new file mode 100644 index 0000000..ce2d768 --- /dev/null +++ b/documentation/07-model-forms.md @@ -0,0 +1,138 @@ +--- +title: Django +author: Steve Kossouho +--- + +# Utiliser des formulaires avec la base de données + +---- + +## Définition de formulaires basés sur les modèles + +### Qu'est-ce qu'un `ModelForm` ? + +Un `ModelForm` est une classe spéciale de formulaire dans Django qui génère automatiquement un formulaire à partir d'un modèle spécifié. + +```python +from django import forms +from .models import MonModel + +class MonModelForm(forms.ModelForm): + class Meta: + model = MonModel + fields = '__all__' +``` + +Ici, un formulaire est créé pour le modèle `MonModel`. L'option `fields = '__all__'` signifie que tous les champs du modèle sont inclus dans le formulaire. + +---- + +## Utilisation des formulaires et validation + +---- + +### Utilisation de `ModelForm` dans une vue + +Un `ModelForm` peut être utilisé dans une vue de la même manière qu'un formulaire normal. Voici un exemple de vue qui utilise un `ModelForm`. + +```python +from django.shortcuts import render +from .forms import MonModelForm + +def ma_vue(request): + if request.method == 'POST': + form = MonModelForm(request.POST) + if form.is_valid(): + form.save() + else: + form = MonModelForm() + return render(request, 'ma_template.html', {'form': form}) +``` + +Dans cet exemple, si la requête est une requête POST et que le formulaire est valide, le modèle associé est automatiquement enregistré en base de données grâce à la méthode `form.save()`. + +---- + +## Gestion des téléchargements + +---- + +### Téléchargements de fichiers avec `ModelForm` + +Pour gérer le téléchargement de fichiers avec un `ModelForm`, il est nécessaire d'utiliser un champ `FileField` ou `ImageField` dans le modèle. Il faut également passer `request.FILES` à l'instance `ModelForm`. + +```python +class MonModel(models.Model): + mon_fichier = models.FileField(upload_to='mes_fichiers/') + +# Dans la vue +def ma_vue(request): + if request.method == 'POST': + form = MonModelForm(request.POST, request.FILES) + if form.is_valid(): + form.save() + else: + form = MonModelForm() + return render(request, 'ma_template.html', {'form': form}) +``` + +Ici, si un fichier est téléchargé via le formulaire, il est automatiquement sauvegardé dans le répertoire `mes_fichiers/`. + +---- + +# Utiliser des formulaires avec la base de données + +Utiliser des formulaires qui correspondent à nos définitions de modèles est assez simple. Il suffit de créer une classe héritant de `django.forms.ModelForm`. Le formulaire correspondant contient automatiquement des champs +reflétant ceux de notre modèle. + +---- + +## Définition de formulaires basés sur les modèles + +![Définition d'un formulaire de modèle](assets/images/modelforms-definition.png) + +---- + +## Utilisation des formulaires et validation + +L'utilisation des formulaires de modèles ressemble beaucoup à l'utilisation de formulaires standard. Il existe toutefois quelques différences, notamment pour la gestion de l'enregistrement de données et la modification de données existantes. + +---- + +![Utilisation du formulaire non lié à un objet](assets/images/modelforms-new-instance.png) + +---- + +![Utilisation du formulaire lié à un objet](assets/images/modelforms-bind-instance.png) + +---- + +## Gestion des téléchargements + +Lorsque vous souhaitez gérer des téléchargements de fichier, vous devriez configurer deux paramètres dans votre projet, qui sont : + +- Le répertoire des fichiers média : `MEDIA_ROOT` +- L'URL de base pour les afficher, `MEDIA_URL` (par défaut `"/media/"`) +- Configurer les URLs pour servir des fichiers média : + - Ajouter `+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)` aux URLs du projet pour servir les fichiers (ne fonctionne pas si `DEBUG = False` +- Avoir un formulaire HTML avec la méthode `POST` et le type d'encodage `enctype="multipart/form-data"`. + +Sans `MEDIA_ROOT`, les fichiers seront uploadés et sauvés à la racine de votre projet (même pas dans un répertoire `media`), ce qui est fortement déconseillé. + +---- + +L'avantage par rapport aux formulaires standard est que la gestion du téléchargement des fichiers se fait automatiquement +lorsque vous persistez vos données de formulaire en faisant + +```python +form = MyForm(request.POST, request.FILES, instance=my_instance) # notez le request.FILES +form.save(commit=True) +``` + +---- + +Lorsque votre modèle possède des champs fichier, vous pouvez facilement accéder à l'URL du fichier uploadé en utilisant la propriété `url` du champ `FileField` ou `ImageField` : + +```djangotemplate +{{ item.picture_field.url }} {# URL commençant par settings.MEDIA_URL #} +``` diff --git a/documentation/08-admin.md b/documentation/08-admin.md new file mode 100644 index 0000000..596143f --- /dev/null +++ b/documentation/08-admin.md @@ -0,0 +1,50 @@ +--- +title: Django +author: Steve Kossouho +--- + +# L'interface d'administration de Django + +---- + +## Présentation de l'interface d'administration + +Ici, on navigue dans l'interface d'administration de Django. +Pour y accéder, il nous faut au moins un super-utilisateur dans notre base +de données, que l'on peut créer en ligne de commande : + +```bash= +./manage.py migrate # s'assurer qu'on a tout à jour +./manage.py createsuperuser +``` + +---- + +## Configuration de l'administration + +Lorsque l'on crée nos propres modèles, par défaut, on ne les voit pas s'afficher lorsque l'on navigue dans l'interface d'administration. +Heureusement, il est possible de configurer celle-ci afin qu'elle nous permette de gérer les données de nos modèles. (voir [documentation officielle](https://docs.djangoproject.com/fr/3.2/ref/contrib/admin/#modeladmin-objects)) + +---- + +La façon la plus simple d'enregistrer un modèle pour qu'il soit affiché est d'aller dans le fichier `admin.py` de notre application et d'y ajouter : + +![Ajouter un modèle à l'admin](assets/images/admin-register-simple.png) + +---- + +Cette méthode ajoute effectivement notre modèle à l'interface, mais l'affichage est plutôt rudimentaire. Il est possible de le personnaliser en s'y prenant autrement; on crée une classe de configuration de l'admin héritant de `admin.ModelAdmin`. + +---- + +![Ajout d'un modèle à l'admin (recommandé)](assets/images/admin-register-recommended.png) + +---- + +Vous pouvez personnaliser votre page de liste de modèle dans l'admin en écrivant votre classe `ModelAdmin` et en y ajoutant des attributs documentés sur [le site officiel Django](https://docs.djangoproject.com/en/dev/ref/contrib/admin/) + +---- + +### Pour aller plus loin + +La [documentation officielle de Django](https://docs.djangoproject.com/fr/3.2/ref/contrib/admin/actions/#admin-actions) montre comment définir ce que l'on appelle des actions, c'est-à-dire des fonctions qui peuvent être exécutées sur des instances de votre modèle via l'interface d'administration. diff --git a/documentation/09-auth.md b/documentation/09-auth.md new file mode 100644 index 0000000..7b63cec --- /dev/null +++ b/documentation/09-auth.md @@ -0,0 +1,93 @@ +--- +title: Django +author: Steve Kossouho +--- + +# L'authentification et les utilisateurs + +---- + +## L'application d'authentification de Django + +Django fournit directement dans le framework une application de gestion de l'authentification, avec les modèles associés. Généralement, ces modèles sont utilisés dans tout projet Django, et reconnus par toutes les applications tierces Django qui utilisent l'authentification ou les utilisateurs. + +Cette application est `django.contrib.auth`. Lorsqu'on l'utilise avec certains middleware ainsi que `django.contrib.session`, on a de bons outils pour se connecter en tant qu'une instance d'utilisateur de notre base. + +---- + +## S'authentifier programmatiquement + +Django fournit plusieurs fonctions pour se dé/connecter en tant qu'une instance de `User` : + +(Dans `django.contrib.auth`) + +- `authenticate` +- `login` +- `logout` + +---- + +`authenticate` permet de savoir si des identifiants de connexion sont valides. Par exemple : + +```python +from django.contrib.auth import login, logout, authenticate +user = authenticate(username="…", password="…") # ou email +``` + +S'ils le sont, la fonction renvoie l'utilisateur concerné, sinon la fonction renvoie `None`. + +---- + +`login` modifie la session HTTP pour indiquer que nous sommes connecté en tant qu'un utilisateur. (nous avons donc +besoin d'un objet `request`) + +```python +from django.contrib.auth import login, logout, authenticate +login(request, user) # user est une instance du modèle User +``` + +---- + +`logout` déconnecte tout utilisateur connecté dans la session. + +---- + +Grâce aux middleware et aux processeurs de contexte, depuis les vues ou dans les templates, on peut accéder facilement à l'instance de l'utilisateur actuellement connecté : + +```python +def view(request): + print(request.user) +``` + +```djangotemplate +{{ user }} {# ou request.user puisque request est aussi dispo #} +``` + +---- + +## S'authentifier avec un formulaire + +Django propose plusieurs vues pour gérer l'authentification, qui rendent des templates trouvés dans un répertoire `registration` (qu'il faut généralement redéfinir pour adapter le rendu de la page et du formulaire). + +On peut aussi s'authentifier manuellement en créant une simple vue utilisant un formulaire standard... + +---- + +Ce formulaire utiliserait `authenticate` pour valider les champs et afficher une erreur si nécessaire. La vue réutiliserait `authenticate` une fois le formulaire valide pour récupérer un utilisateur et le connecter. + +---- + +## Considérations sur les mots de passe + +Django enregistre normalement les utilisateurs en chiffrant le mot de passe. Cela se fait en interne en utilisant la clé secrète du paramètre `SECRET_KEY`. + +Si l'on manipule une instance du modèle utilisateur pour modifier son mot de passe et qu'on y assigne une valeur en clair, elle sera enregistrée en clair dans la base de données. + +Pour chiffrer correctement le mot de passe, il faut utiliser `user.set_password()` qui utilise le chiffrage. + +---- + +## Commandes pour l'authentification + +- `changepassword <username>` : changer un mot de passe +- `createsuperuser` : créer un utilisateur qui a tous les droits (et accès à l'admin entre autres). Cet utilisateur possède `.is_superuser == True` diff --git a/documentation/10-translation.md b/documentation/10-translation.md new file mode 100644 index 0000000..b3a4101 --- /dev/null +++ b/documentation/10-translation.md @@ -0,0 +1,52 @@ +--- +title: Django +author: Steve Kossouho +--- + +# Traduction d'un projet Django (I18N) + +---- + +## Traduction dans le code Django + +Pour marquer certaines chaînes dans une application comme étant traduisibles, il suffit de les passer comme arguments de la fonction `gettext_lazy`. Par convention, on importe la fonction en lui donnant l'alias `_`. + +```python +from django.utils.translation import gettext_lazy as _ +_("String to translate") # dans la langue de `LANGUAGE_CODE` +``` + +---- + +## Traduction dans les templates + +Pour accéder aux tags spécifiques à la traduction, vous devez charger les templatetags de l'application `i18n` : + +```djangotemplate +{% load i18n %} +``` + +---- + +![Traduction dans les templates](assets/images/translation-template-examples.png) + +---- + +## Commandes pour la traduction + +Dans le répertoire de l'application à traduire : + +- `django-admin makemessages -l fr` : fichier pour traducteur +- `django-admin compilemessages -l fr` : traductions compilées + +---- + +## Bénéficier automatiquement de la traduction + +Pour que Django affiche vos pages dans la langue de votre navigateur, il faut ajouter dans `MIDDLEWARE` une entrée `"django.middleware.locale.LocaleMiddleware"`. + +Le middleware récupère depuis les en-têtes HTTP les langues préférées du navigateur, et si possible, active la première langue dont une traduction existe. Cela nécessite donc que le navigateur soit configuré pour préférer l'affichage des pages dans votre langue préférée. + +:::notes +Faire la démo des langues préférées du navigateur avec Firefox +::: diff --git a/documentation/11-advanced.md b/documentation/11-advanced.md new file mode 100644 index 0000000..408877a --- /dev/null +++ b/documentation/11-advanced.md @@ -0,0 +1,88 @@ +--- +title: Django +author: Steve Kossouho +--- + +# Éléments avancés + +---- + +## Envoyer des emails + +Django propose de nombreuses façons d'envoyer des emails, mais la plus simple d'entre elles consiste à utiliser une fonction utilitaire nommée `send_mail` et configurer si nécessaire vos paramètres pour l'envoi de mail. La procédure est décrite dans la [documentation officielle](https://docs.djangoproject.com/en/dev/topics/email/). + +---- + +![Envoi d'un email en deux lignes](assets/images/advanced-send-mail.png) + +*(voir, dans le projet `advanced`, l'exemple dans le notebook Jupyter.)* + +---- + +## Créer des vues de téléchargement + +Pour proposer un fichier au téléchargement dans une vue, il faut renvoyer un objet `HttpResponse` dont le contenu est le fichier cible, et y ajouter les en-têtes HTTP nécessaires au navigateur. + +---- + +![Fichier à télécharger via Django](assets/images/advanced-file-download.png) + +---- + +## Lancement de tests d'un projet Django (unittest) + +En Python comme dans d'autres langages, il existe des frameworks pour effectuer des tests unitaires, d'intégration, et même des tests de bout-en-bout. En Java, le framework le plus connu est JUnit, et Python s'en est directement inspiré pour créer `unittest`, disponible par défaut. + +---- + +Pour effectuer des tests, Django utilise par défaut `unittest`. Ainsi, l'écriture de tests unitaires est identique à dans n'importe quel autre projet Python : + +- On écrit, dans un module de `tests`, des classes héritant de `TestCase`. +- Ces classes peuvent redéfinir `setUp()`, `tearDown()`, et `setUpClass` et `tearDownClass` pour configurer des "fixtures". +- Les tests unitaires sont écrits dans des méthodes dont le nom débute par `test_`. + +Pour démarrer les tests, il suffit ensuite de lancer `./manage.py test`. + +---- + +## Lancement de tests (test client) + +Lors des tests, on peut souhaiter vouloir tester la génération des pages de notre site (end-to-end), en, par exemple, testant qu'une URL ne renvoie pas de 404 ou que son contenu est attendu. + +Pour ce faire, Django propose un "client" de tests, décrit dans `django.test.Client`. + +---- + +Son utilisation est simple : + +![Utilisation du client de test Django](assets/images/advanced-test-client.png) + +---- + +## Déploiement d'un projet Django + +En production, on ne sert jamais une application Django en utilisant `runserver`. En général, on utilise une pile logicielle du type suivant : + +- Apache/Nginx servant sur le port 80 +- Serveur WSGI du type Gunicorn, qui sert l'application via un socket Unix ou un socket TCP en écoute sur localhost uniquement +- Nginx sert les ressources (statiques et média) publiques +- Nginx passe les autres demandes d'URLs au serveur WSGI + +Voici un [tutorial pour configurer la pile Nginx/Gunicorn](https://python.developpez.com/actu/94911/Mise-en-production-d-un-site-Django-en-utilisant-Nginx-et-Gunicorn/) + +---- + +## Interconnexion avec les réseaux sociaux + +En plus de l'authntification, vous pouvez lier des utilisateurs à des données de connexion relatives à des réseaux sociaux divers. + +L'application externe `django-allauth` propose une [documentation en ligne](https://django-allauth.readthedocs.io/en/latest/overview.html), et est l'application la plus maintenue pour gérer la connexion via de très nombreux réseaux sociaux ou services OAuth. + +---- + +## Divers : Procédure de tests + +Pour aller plus loin, vous pouvez utiliser le paquet externe `pytest` à la place de `unittest`, aussi bien pour vos projets Python que Django. +Si l'outil est simple à mettre en place pour Python, préférez l'assembler avec [Pytest-Django](https://pytest-django.readthedocs.io/en/latest/). + +*Portez attention au chapitre sur les bases de données de test avec pytest-django* diff --git a/documentation/99-extra-tools.md b/documentation/99-extra-tools.md new file mode 100644 index 0000000..26327a2 --- /dev/null +++ b/documentation/99-extra-tools.md @@ -0,0 +1,67 @@ +--- +title: Outils Django +author: Steve Kossouho +--- + +# Outils supplémentaires Django + +---- + +## Informations avancées de débogage en développement + +Même si ce n'est pas nécessaire, il peut être intéressant pour un développeur +Django d'avoir à disposition, lorsqu'il teste ses vues, des informations sur les +performances de sa vue : temps d'exécution, temps d'exécution des requêtes de base +de données, templates utilisés etc. + +[django-debug-toolbar](https://django-debug-toolbar.readthedocs.io/en/latest/) + +---- + +## Générer des graphiques (charts) + +- [django-chartjs](https://django-chartjs.readthedocs.io/en/latest/) +- [django-slick-reporting](https://django-slick-reporting.readthedocs.io/en/latest/) + +---- + +## Afficher des données en table et filtrer + +- django-tables2 : Afficher des tableaux depuis des modèles +- django-filter : Pouvoir filtrer un queryset via un formulaire + +---- + +## Convertir une page HTML en PDF + +Il existe quelques outils pour convertir des pages web HTML en ligne vers des fichiers PDF. +On utilise généralement des outils d'automatisation de navigateur, tels que Selenium. + +`Playwright` est actuellement l'un des outils les plus aboutis pour automatiser un navigateur. + +[Playwright](https://playwright.dev/python/docs/intro) + +---- + +```bash +pip install playwright +``` + +Installer playwright nécessite au moins 500 Mo d'espace libre sur votre système de fichiers. + +---- + +```python +from argparse import ArgumentParser +from playwright.sync_api import sync_playwright + +with sync_playwright() as pw: + browser = pw.chromium.launch() + context = browser.new_context() + page = context.new_page() # ouvre un onglet + page.goto("protocol://url") + page.pdf(format="A4", print_background=True, scale=1.0, path=f"filename.pdf") + browser.close() +``` + +Exemple d'automatisation diff --git a/documentation/99-important-settings.md b/documentation/99-important-settings.md new file mode 100644 index 0000000..a81f24d --- /dev/null +++ b/documentation/99-important-settings.md @@ -0,0 +1,75 @@ +--- +title: Paramètres fondamentaux de Django +author: Steve Kossouho +tags: [settings,django] +--- + +# Paramètres fondamentaux de Django + +Récapitulatif des paramètres les plus rencontrés sur les projets Django simples. + +---- + +## Catégories + +- Applications prises en charge +- Base de données +- Templates +- Traduction +- URLs +- Service de fichiers (Média, Statiques) + +---- + +## Applications prises en charge par le projet + +`INSTALLED_APPS` : Liste des applications utilisées par votre projet Django. + +Le système interne de Django utilise cette information pour découvrir automatiquement certaines +informations pour le projet; templates, modèles, admin, fichiers statiques etc. + +---- + +## Base de données + +`DATABASES` : Permet de configurer une ou plusieurs bases de données à utiliser avec le projet Django. + +La configuration la plus simple à mettre en place utilise une base de données SQLite3. Notez que pour une +utilisation en production, une base de données SQLite3 est fortement déconseillée (du moins pour une application +complexe autorisant la connexion de plusieurs clients simultanément). + +---- + +## Templates (Gabarits) + +`TEMPLATES` : Configurer les moteurs de gabarits utilisés par notre projet. Dans 99.9% des cas, la +configuration de base est suffisante, à quelques exceptions près. + +---- + +```python {.number-lines} +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", + ], + }, + }, +] +``` + +Configuration des moteurs de gabarits + +---- + +Parmi les clés présentes dans le paramètre `TEMPLATES`, se trouvent les clés suivantes : + +- `APP_DIRS` : Automatiquement prendre en compte les répertoires `templates` des applications présentes dans `INSTALLED_APPS`. +- `context_processors` : fonctions qui ajoutent des informations accessibles pendant le rendu d'un template. diff --git a/documentation/assets/diagrams/Django formation-Middleware.png b/documentation/assets/diagrams/Django formation-Middleware.png new file mode 100644 index 0000000..1e7a992 Binary files /dev/null and b/documentation/assets/diagrams/Django formation-Middleware.png differ diff --git a/documentation/assets/diagrams/Django formation.drawio b/documentation/assets/diagrams/Django formation.drawio new file mode 100644 index 0000000..93db688 --- /dev/null +++ b/documentation/assets/diagrams/Django formation.drawio @@ -0,0 +1 @@ +<mxfile host="Electron" modified="2021-04-22T06:24:48.667Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.5.1 Chrome/89.0.4389.82 Electron/12.0.1 Safari/537.36" etag="dzQ0P0qPcDWSRTx-KB1X" compressed="true" version="14.5.1" type="device" pages="3"><diagram id="GgAqahdueFiXzMDP5OXp" name="MVC">7VlNc5swEP01HOPhw9hwjO2kPbRTT9Jpm5NHBhmrFYgRwoH8+q5AGGESO6lJJ9Pm4Fi7Wklo973dJTaceVx84CjdfmYhpoZthoXhLAzbtsamB19SU9aaqW/WioiTUBm1ilvygJWyMctJiLOOoWCMCpJ2lQFLEhyIjg5xzu67ZhtGu6emKMI9xW2AaF/7nYRiW2s9e9rqP2ISbZuTrYlfz8SoMVY3ybYoZPeayrkynDlnTNSjuJhjKp3X+KVed/3E7P7BOE7EcxZMb74VIr/9dCNW0yDZLMfXFw8Xdr3LDtFcXVg9rCgbD5C4ctFsK2IKGguGSrUgcQTnUbKGvwEl6QpxAcMUsxRW29efCSUC8XL1ZbMhAear7yxGycqyvQI+ozSJ+pdQ99phLnChqdSlPmAWY8FLMFGzrvJvAzAl3mvRUqqtFqhGhxQ+ov2+rQthoLz4Ao+6pz2KsrSG6oYUGPaapYwkAvOrHXghUz7ew8UEIUTZVlpWghaITHD2C88ZZRw0CUtkoChaY7pkGRGEJaAOsNwcJqRPCUD704HBmgnBYs3gkpJITgiWghYpab/PhlDanGnYjmlanreoHzmVV4yLSCaDUZwFCI+qVSknGR4BRkBaoTSlcEx1+vnxt5wuAPxxDwDuIwBw7FcCwKQHAMiKxtwxLj1Jincs1FgIkUBrlOFVhBPMSTAAEibTLhI8t4cEZ9pHgvtaqcCyeuHGIVQXJcJFiShvMK2YcNXOzDjLk3AfY8bFlkUsgWAxGYQq2j+xEKUqlygXrIsF2JqXP9T6SriTwshtxEWhTy5KXVpCOMABMryV8snIZCznAT7iAOVsKAERFsccpSIgvXM00Lxy1q5bmweP27TH4FvMdzjn7+xt2Kul8FUmncPtAQgMVBy5pyg8dv4mhSdHKdxS82n6/szjtLFXcf0TRhdE/NDGGp9BaukshVITDsncZoY2GdzpWeMlmSEDZotL2WW3d6t010S6+czs4T8ze7hDJ49qKVwLlZpBxexM23kpFVob6k87yLWsg3b8lL133N5y3WP2MKifuMX6/upnwN/rpcKlbP2lM5MdK6u2xu93NTIWVW7qgriXfw7TVEzCsKYQzsgDWlf7mU1erSLhzgx38SiujhK4l4T2r4zqFEN/K3ssOZkj03cnnRioTvNlwGoj25iwzSYDhB/mqgHi5/fC9y3/f3tQ9JBzPMoCTlKxgkV4gJLlOid7zvFf7TmdswvWGdWprUh3ekE6uzoB9/6oQD1Sezs1K6Aoy+Ddo1u2rLPKVtPMnqxbkzfV9FrjXq74yhEROMZVspK72+YCxwig8pYTvjNUwr8A1E2b7RS9nbee8S33n21YB3iVfd2G9dnM998W880e8+cMvCebu9mYvr/36v/BZCEeqnVwPP9k6+AP0jqA2P7OUGea9tca5+o3</diagram><diagram id="Y5h737CUiNRvkyHO9ETn" name="Middleware">7Vnbcts2EP0aTZ+q4VWXR+vitB27dW1Pk/RFA5EghRgkWBCUqXx9FiAogiIjW6nU+sGesYRdLgBi9+wBFhq486T8wFG2uWUhpgPHCsuBuxg4ju1ZE/iSml2lGU+tShFzEmqjRvFAvmKtrM0KEuK8ZSgYo4JkbWXA0hQHoqVDnLPntlnEaHvWDMW4o3gIEO1qP5JQbCrtxBk3+l8wiTf1zPZoWj1JUG2sV5JvUMieDZW7HLhzzpioWkk5x1Q6r/bLn2JdLu1fV0HB8Pqf355GS8F+rga7PqXLfgkcp+KHh979ffUc4lX4e8THyb3j8ZvHR93F2iJaaH/ptYpd7UCSKA/PNiKhoLGhqVULksQwHyVr+AwoyVaIC2hmmGXQ27m+JZQIxHerP6KIBJivPrIEpSvbmZTwP8zS+JUr0x7YYi5wacRVr/QDZgkWfAcm+qmvF1aDVovPBgK0amMEv9Yhjbl4P27jV2ho157gZtvp+HmBwRUheGlEYfbZmkMrli0F6MMg4BBArUXGxYbFLEV02WhnnBUwnHwJC6TG5oaxTIftCxZipzMUFYK1g4pLIj7J7sOpo8XPSvS1tCj14ErYGcId5gQchbnWnRbVnBU8wMe8p2kD8RiLI3ZeZSd9dRQjHFMkyLZNEOeP+cuphfKsoryIlDJ0s4yRFNy43ILjch2XPe1Iz4Yo3+yDbAQvF5w94TmjTMYgZamEBEVrTO9YTgRhKagDnKoYzWQeEaDImwODNROCJYbBFSWxfCAkhmZIS/txIkJpPefAcS3LnkwW1StncolJGctNZZjkAcJD1SvjJMdDIAuQVijLKEyjZr8QEdhumwmmXocJ/B4mcJ1LocL7P3PbbiX2yDk5syE6fPfJFAyOkGIzlJLqsYKCb9UC7PPTg/dKerDdN8UPXocfHjA4qeDdLWHxBaUxe6ePmj4M3ljl0mncuRR/OKM2f0z8Dn94bpc//IudJEY/wB+t7LsQmRw/JZz7ROC+Nuf9N5Xz9XsbSX/PCqFOfIdJP98wUsrC66cC0hH6FN1jYZcBDL6G94EiCxsUbkaxk43fJ4qIpcLIU88Zj2ezmiBmKHiKFaYMk0j96a4aSLbcbfIMBSSNHxXU3KaU6C0pyDqRn2nEERBUEYiCy6qCMhSu1oiiVA61grkFfA/zbXwy0F7PA67X5gG/W1GMes4Ro4vxwOSdB5r0fpkHxm+LB/wOD/xV9HDAshzM3cHVNCiqM7IVvjOBwQQJS4lg/PLZ772x7N9fSf23VcT+8N+c9z+bz75z+FfSpe8Ixq9lgnMTgep6xTnaGQbqJJ4bI99JRQOosXNQljomJl40tyfWUXvb94/ZQ6N64QaB+5X/C1B2bzzuK/rKWJpL4lISvIlFkeIyfQPWg2RVWrxAU4dVRkLCsAI6zslXtFbjWXVZpALjzwb+ohd8R9OsQxT762o9y8C8Ee4jEGvoWK7uehqymtjWJiyKcoD4IYecIYJ1EhkRvFVOfUa8Z3e6ZSGJCMQC1tQK9DV4BxwmurF9sXTs7GMGAvrLyt7atQMWOCRSkoJZ/SuD1VNoBkRwUg6TZs2X21L8gytqf9LZUuz6sqi1p0xO3lNAbH6lqLDS/NbjLr8B</diagram><diagram id="q8YbaYhWte70mP5-ACzJ" name="Migrations">7Vldc6IwFP01PLYDBEQfV233Y7qznXFntn3qpBAhWyBMCIr76zeRBIjY1n6g69YX5Z7c3CT35CRXNMAkKT9TmEXfSYBiwzaD0gBTw7YtxxzyL4GsKsQbmRUQUhxIpwaY4T9IgsqtwAHKNUdGSMxwpoM+SVPkMw2DlJKl7jYnsT5qBkPUAWY+jLvoLxywqEKHttfgXxAOIzWyNRhVLQlUznIleQQDsmxB4MIAE0oIq56ScoJikTyVl7tvkOECnYHh/Gsaju0fjn11VgW7fEmXegkUpeydQ8vYOVuphKGA50+ahLKIhCSF8UWDjikp0gCJsCa3Gp8rQjIOWhz8jRhbyc0AC0Y4FLEklq2oxOym9XwrQp270pqWMvLaWCkjZXR10zZavYTZdFtbqp9f0MV6rmKwarFihRub45nMqiyRgvroCT9bbnBIQ/RkvGb/cOEhkiA+Zd6Roph7L/TZQamAsPZrWOYPkugXkC5nuYBxIUeaGhNgfBrNcYoZJilv7O4LmGeVPue4FAkdZ4hiPiFEOcZH5YJG1w3U5hvGOEz5s8+zvG6rpSQYCmAe1bspz6CP0/DneicBDuBkLXD1PcVJyBcd43v+CX2RrLsAUz4zIjJzGUAG72GOzvNFWBO+QJSh8hWUdxlSUUx5KKzUsSHtZXPGuBKKWscL8HridPAKHXMWMtEa4/ThPWXdSPm21bJd1n0rErxRaLLrNcF8OjX9wLE0+h1FvwpRnQCy1wa19TRezzboKHiG+DFX0P9AuXe5WArtWcDA1gXseh39Ko7b+nXNx7fKm/TrnfS71c/pRb+uq7O/b/06T+jXPul3B/264N/Sr+WcCul3lP1w10LaPmQhPdyhkD52Oe+5kLbNAxfS6iZokdphUF28kiydI42FZYQZElQI9yWFmSBxk/+cUfJQv51wn98RvRVFll7WWk6XjMEWMga9nardn6ofhgzX1snYpoz9kjE8litur1eVBXa9qwaHvKus0bGwdxQFyu6sewdlvfumoFuigFOJ8qISZXToEmXwcW/FzRJlGxn7vRW9j0tGp0TpjwxuNv+0Va9hmv8rwcVf</diagram></mxfile> \ No newline at end of file diff --git a/documentation/assets/diagrams/pattern-mvc.drawio b/documentation/assets/diagrams/pattern-mvc.drawio new file mode 100644 index 0000000..4764dab --- /dev/null +++ b/documentation/assets/diagrams/pattern-mvc.drawio @@ -0,0 +1 @@ +<mxfile host="Electron" modified="2021-04-16T12:07:18.643Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.5.1 Chrome/89.0.4389.82 Electron/12.0.1 Safari/537.36" etag="mRoy3h5prpVIwsehNFBr" compressed="true" version="14.5.1" type="device"><diagram id="GgAqahdueFiXzMDP5OXp" name="Page-1">7Vldc5s6EP01PMbDh7HNY2InvQ/t1JPcaZsnjwwyVisQIwkH8uvvCoQRJrGTmnQyvZlxPNrVSkK75+wuseXNk+ITR9n2C4swtVw7KixvYbmuM3ZdS33sqKw1M3tcK2JOIm3UKu7II9ZKW2tzEmHRMZSMUUmyrjJkaYpD2dEhztlD12zDaPfUDMW4p7gLEe1rv5NIbvUt3Gmr/weTeNuc7EyCeiZBjbG+idiiiD0YKu/a8uacMVmPkmKOqXJe45d63c0zs/sH4ziVL1kwvf1WyPzu861cTcN0sxzfXDxe6OjsEM31hfXDyrLxAEkqF11tZUJB48BQqxYkieE8StbwHVKSrRCXMMwwy2C1e/OFUCIRL1dfNxsSYr76zhKUrhx3VsDfKEvj/iX0vXaYS1wYKn2pT5glWPISTPSsr/2rATbV4oMRLa3aGoFqdEjjI97v27oQBtqLr/Cof9qjSGQ1VDekwLDXVcZIKjG/3oEXhPbxHi42CBESW2VZCUYghOTsF54zyjhoUpaqQFG0xnTJBJGEpaAOsdocJpRPCUD784HBmknJEsPgkpJYTUiWgRZpab/PhlDanGm5nm07s9mifuRMXTEpYpUMRokIER5VqzJOBB4BRkBaoSyjcEx1+vnxd7wuAIJxDwD+EwDw3DcCwKQHAMiK1tyzLmeKFB9YqLEQIYnWSOBVjFPMSTgAEibTLhJmfg8J3rSPBP+tUoHj9MKNI6guWoSLElneYlox4bqdueIsT6N9jBmXWxazFILFVBCqaP/EUpa6XKJcsi4WYGte/tDrK+FeCSO/EReFObkoTWkJ4QAHqPBWymcjI1jOQ3zEAdrZUAJiLI85SkdAeedooHnlrF23Ng8et2mPwXeY73DOP9jbsNdI4SuhnMPdAQgMVBz5pyg89v4khSdHKdxS83n6/syTrLHXcf0dRhdE/jDGBp9BaumshNIQDsncZoY2GdybWeM1mUEAs+Wl6rLbu1W6G6LcfGb2CF6YPfyhk0e1FK6FSsOgYrYwdl4qhdGGBtMOch3noB0/ZT87bu/4/jF7GNRP3GJ9f/Uz4D/rpcKlav2VM9MdK6u2Juh3NSoWVW7qgriXfw7TVEKiqKYQFuQRrav97CavVpHwryx/8SSujhK4l4T2r4z6FMt8K3sqOdkjO/AnnRjoTvN1wGoj25iwzUYAwg9z1QDxC3rh+5b/f3tQ9JhzPBIhJ5lcwSI8QMnyvZM95/iP9pze2QXrjOrUVqR7syCdXZ2Ae79VoJ6ovZ2aFVIkBLx7dMuWc1bZaprZk3Vr8q6aXmfcyxX/ckQkTnCVrNTurr3ACQKovOeE7w2V8C8AddNmO01v771nfMf/axvWAV5l37ZhfTHzg/fFfLvH/DkD76nm7mpMP957zf9gsggP1Tp4s+Bk6xAM0jqA2P7OUGea9tca7/o/</diagram></mxfile> \ No newline at end of file diff --git a/documentation/assets/diagrams/pattern-mvc.png b/documentation/assets/diagrams/pattern-mvc.png new file mode 100644 index 0000000..1440b88 Binary files /dev/null and b/documentation/assets/diagrams/pattern-mvc.png differ diff --git a/documentation/assets/diagrams/production.drawio b/documentation/assets/diagrams/production.drawio new file mode 100644 index 0000000..2bdf23d --- /dev/null +++ b/documentation/assets/diagrams/production.drawio @@ -0,0 +1,47 @@ +<mxfile host="Electron" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/24.7.17 Chrome/128.0.6613.36 Electron/32.0.1 Safari/537.36" version="24.7.17"> + <diagram name="Page-1" id="lALLRwfy-J3OwCSlcXKb"> + <mxGraphModel dx="1217" dy="670" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0"> + <root> + <mxCell id="0" /> + <mxCell id="1" parent="0" /> + <mxCell id="7tNKPUQDF-2DKlcSCBtx-3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="7tNKPUQDF-2DKlcSCBtx-1" target="7tNKPUQDF-2DKlcSCBtx-2"> + <mxGeometry relative="1" as="geometry" /> + </mxCell> + <mxCell id="7tNKPUQDF-2DKlcSCBtx-4" value="plop.com" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="7tNKPUQDF-2DKlcSCBtx-3"> + <mxGeometry x="-0.32" y="2" relative="1" as="geometry"> + <mxPoint as="offset" /> + </mxGeometry> + </mxCell> + <mxCell id="7tNKPUQDF-2DKlcSCBtx-1" value="" style="verticalLabelPosition=bottom;shadow=0;dashed=0;align=center;html=1;verticalAlign=top;strokeWidth=1;shape=mxgraph.mockup.containers.userFemale;strokeColor=#666666;strokeColor2=#008cff;" vertex="1" parent="1"> + <mxGeometry x="364" y="20" width="100" height="100" as="geometry" /> + </mxCell> + <mxCell id="7tNKPUQDF-2DKlcSCBtx-2" value="" style="image;html=1;image=img/lib/clip_art/computers/Server_Rack_128x128.png" vertex="1" parent="1"> + <mxGeometry x="374" y="220" width="80" height="80" as="geometry" /> + </mxCell> + <mxCell id="7tNKPUQDF-2DKlcSCBtx-6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="7tNKPUQDF-2DKlcSCBtx-5" target="7tNKPUQDF-2DKlcSCBtx-7"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="540" y="180" as="targetPoint" /> + </mxGeometry> + </mxCell> + <mxCell id="7tNKPUQDF-2DKlcSCBtx-10" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="7tNKPUQDF-2DKlcSCBtx-5" target="7tNKPUQDF-2DKlcSCBtx-9"> + <mxGeometry relative="1" as="geometry" /> + </mxCell> + <mxCell id="7tNKPUQDF-2DKlcSCBtx-5" value="Apache ou nginx" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1"> + <mxGeometry x="344" y="170" width="120" height="20" as="geometry" /> + </mxCell> + <mxCell id="7tNKPUQDF-2DKlcSCBtx-7" value="/static/..." style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1"> + <mxGeometry x="550" y="170" width="120" height="20" as="geometry" /> + </mxCell> + <mxCell id="7tNKPUQDF-2DKlcSCBtx-8" value="Django" style="outlineConnect=0;dashed=0;verticalLabelPosition=bottom;verticalAlign=top;align=center;html=1;shape=mxgraph.aws3.search_documents;fillColor=#F58534;gradientColor=none;" vertex="1" parent="1"> + <mxGeometry x="580" y="310" width="60" height="63" as="geometry" /> + </mxCell> + <mxCell id="7tNKPUQDF-2DKlcSCBtx-11" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="7tNKPUQDF-2DKlcSCBtx-9" target="7tNKPUQDF-2DKlcSCBtx-8"> + <mxGeometry relative="1" as="geometry" /> + </mxCell> + <mxCell id="7tNKPUQDF-2DKlcSCBtx-9" value="/users/..." style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1"> + <mxGeometry x="550" y="250" width="120" height="20" as="geometry" /> + </mxCell> + </root> + </mxGraphModel> + </diagram> +</mxfile> diff --git a/documentation/assets/images/admin-register-recommended.png b/documentation/assets/images/admin-register-recommended.png new file mode 100644 index 0000000..7814de8 Binary files /dev/null and b/documentation/assets/images/admin-register-recommended.png differ diff --git a/documentation/assets/images/admin-register-simple.png b/documentation/assets/images/admin-register-simple.png new file mode 100644 index 0000000..67ebb1e Binary files /dev/null and b/documentation/assets/images/admin-register-simple.png differ diff --git a/documentation/assets/images/advanced-file-download.png b/documentation/assets/images/advanced-file-download.png new file mode 100644 index 0000000..cc292b9 Binary files /dev/null and b/documentation/assets/images/advanced-file-download.png differ diff --git a/documentation/assets/images/advanced-send-mail.png b/documentation/assets/images/advanced-send-mail.png new file mode 100644 index 0000000..b8c2fad Binary files /dev/null and b/documentation/assets/images/advanced-send-mail.png differ diff --git a/documentation/assets/images/advanced-test-client.png b/documentation/assets/images/advanced-test-client.png new file mode 100644 index 0000000..30ae4a5 Binary files /dev/null and b/documentation/assets/images/advanced-test-client.png differ diff --git a/documentation/assets/images/discover-frameworks.png b/documentation/assets/images/discover-frameworks.png new file mode 100644 index 0000000..e03dfcc Binary files /dev/null and b/documentation/assets/images/discover-frameworks.png differ diff --git a/documentation/assets/images/forms-form-definition.png b/documentation/assets/images/forms-form-definition.png new file mode 100644 index 0000000..0da73e6 Binary files /dev/null and b/documentation/assets/images/forms-form-definition.png differ diff --git a/documentation/assets/images/forms-form-html-upload.png b/documentation/assets/images/forms-form-html-upload.png new file mode 100644 index 0000000..321e674 Binary files /dev/null and b/documentation/assets/images/forms-form-html-upload.png differ diff --git a/documentation/assets/images/forms-form-template.png b/documentation/assets/images/forms-form-template.png new file mode 100644 index 0000000..06326a6 Binary files /dev/null and b/documentation/assets/images/forms-form-template.png differ diff --git a/documentation/assets/images/forms-form-usage.png b/documentation/assets/images/forms-form-usage.png new file mode 100644 index 0000000..7fd736e Binary files /dev/null and b/documentation/assets/images/forms-form-usage.png differ diff --git a/documentation/assets/images/forms-media-urls.png b/documentation/assets/images/forms-media-urls.png new file mode 100644 index 0000000..88ba627 Binary files /dev/null and b/documentation/assets/images/forms-media-urls.png differ diff --git a/documentation/assets/images/modelforms-bind-instance.png b/documentation/assets/images/modelforms-bind-instance.png new file mode 100644 index 0000000..f303bb4 Binary files /dev/null and b/documentation/assets/images/modelforms-bind-instance.png differ diff --git a/documentation/assets/images/modelforms-definition.png b/documentation/assets/images/modelforms-definition.png new file mode 100644 index 0000000..62767b6 Binary files /dev/null and b/documentation/assets/images/modelforms-definition.png differ diff --git a/documentation/assets/images/modelforms-new-instance.png b/documentation/assets/images/modelforms-new-instance.png new file mode 100644 index 0000000..30e66ad Binary files /dev/null and b/documentation/assets/images/modelforms-new-instance.png differ diff --git a/documentation/assets/images/orm-model-base-definition.png b/documentation/assets/images/orm-model-base-definition.png new file mode 100644 index 0000000..da7bbc0 Binary files /dev/null and b/documentation/assets/images/orm-model-base-definition.png differ diff --git a/documentation/assets/images/orm-model-migration-concept.png b/documentation/assets/images/orm-model-migration-concept.png new file mode 100644 index 0000000..4bd9418 Binary files /dev/null and b/documentation/assets/images/orm-model-migration-concept.png differ diff --git a/documentation/assets/images/orm-model-relation-fields.png b/documentation/assets/images/orm-model-relation-fields.png new file mode 100644 index 0000000..a92193d Binary files /dev/null and b/documentation/assets/images/orm-model-relation-fields.png differ diff --git a/documentation/assets/images/pattern-mvc.png b/documentation/assets/images/pattern-mvc.png new file mode 100644 index 0000000..a0e3cbd Binary files /dev/null and b/documentation/assets/images/pattern-mvc.png differ diff --git a/documentation/assets/images/templating-control.png b/documentation/assets/images/templating-control.png new file mode 100644 index 0000000..469e52c Binary files /dev/null and b/documentation/assets/images/templating-control.png differ diff --git a/documentation/assets/images/templating-folder-example.png b/documentation/assets/images/templating-folder-example.png new file mode 100644 index 0000000..3ae2abf Binary files /dev/null and b/documentation/assets/images/templating-folder-example.png differ diff --git a/documentation/assets/images/templating-inheritance-base-block.png b/documentation/assets/images/templating-inheritance-base-block.png new file mode 100644 index 0000000..c12da8b Binary files /dev/null and b/documentation/assets/images/templating-inheritance-base-block.png differ diff --git a/documentation/assets/images/templating-inheritance-child-block.png b/documentation/assets/images/templating-inheritance-child-block.png new file mode 100644 index 0000000..9774db3 Binary files /dev/null and b/documentation/assets/images/templating-inheritance-child-block.png differ diff --git a/documentation/assets/images/templating-interpolation-view.png b/documentation/assets/images/templating-interpolation-view.png new file mode 100644 index 0000000..6a145ab Binary files /dev/null and b/documentation/assets/images/templating-interpolation-view.png differ diff --git a/documentation/assets/images/templating-interpolation.png b/documentation/assets/images/templating-interpolation.png new file mode 100644 index 0000000..4343d08 Binary files /dev/null and b/documentation/assets/images/templating-interpolation.png differ diff --git a/documentation/assets/images/templating-tags-static.png b/documentation/assets/images/templating-tags-static.png new file mode 100644 index 0000000..0459759 Binary files /dev/null and b/documentation/assets/images/templating-tags-static.png differ diff --git a/documentation/assets/images/translation-template-examples.png b/documentation/assets/images/translation-template-examples.png new file mode 100644 index 0000000..244a9b5 Binary files /dev/null and b/documentation/assets/images/translation-template-examples.png differ diff --git a/documentation/assets/images/urls-middleware-flow.svg b/documentation/assets/images/urls-middleware-flow.svg new file mode 100644 index 0000000..d7a4367 --- /dev/null +++ b/documentation/assets/images/urls-middleware-flow.svg @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?xml-stylesheet href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css" type="text/css"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" height="100%" stroke-linecap="round" viewBox="0 0 204.983 1206" style="max-width: 100%;"><g><path d="M102.492 62.001 C102.839 62.851, 102.324 64.433, 102.492 68.168 M102.492 62.001 C102.31 63.221, 102.311 64.74, 102.492 68.168 M102.492 68.168 C100.875 72.937, 104.081 84.846, 102.492 98.334 M102.492 68.168 C101.188 73.442, 103.195 84.64, 102.492 98.334 M102.492 98.334 C101.318 110.487, 102.109 121.576, 102.492 126.501 M102.492 98.334 C102.415 110.344, 101.748 120.211, 102.492 126.501 M102.492 126.501 C102.118 128.417, 102.025 129.5, 102.492 132.001 M102.492 126.501 C102.789 128.061, 102.794 129.273, 102.492 132.001" stroke="rgb(211, 211, 211)" stroke-width="1" fill="none" stroke-dasharray="0.5"></path></g><g><path d="M98.68 127.708 C98.68 127.708, 98.68 127.708, 98.68 127.708 M98.68 127.708 C98.68 127.708, 98.68 127.708, 98.68 127.708 M99.009 129.074 C99.038 128.846, 99.249 128.606, 99.442 128.177 M98.979 129.123 C99.229 128.772, 99.414 128.401, 99.481 128.284 M99.345 130.73 C99.619 130.159, 99.781 129.463, 100.931 128.044 M99.131 130.441 C99.795 129.555, 100.648 128.501, 100.994 128.029 M100.146 131.336 C100.967 129.873, 101.09 128.952, 102.406 127.532 M100.215 130.858 C100.524 130.311, 101.161 129.522, 102.184 127.701 M100.546 132.209 C101.309 131.709, 101.823 129.785, 103.036 128.659 M100.261 132.577 C101.232 130.879, 102.017 129.465, 103.359 128.106 M101.599 133.329 C102.391 131.384, 103.598 128.896, 103.944 128.458 M101.372 133.046 C102.191 131.783, 103.05 129.889, 104.427 128.154 M101.196 133.593 C103.048 132.547, 104.304 129.923, 106.554 127.514 M101.833 134.435 C102.261 132.525, 103.884 131.103, 105.977 127.483 M101.377 135.621 C103.492 134.142, 105.078 131.303, 106.513 127.91 M101.96 135.345 C103.883 132.977, 105.987 130.319, 107.033 127.971" stroke="rgb(211, 211, 211)" stroke-width="0.883" fill="none"></path><path d="M105.631 127.462 C105.146 130.117, 103.881 132.315, 101.9 136.778 M101.814 136.627 C101.1 133.835, 100.227 131.751, 98.463 127.735 M98.568 127.705 C102.334 127.464, 104.288 127.832, 106.791 128.688" stroke="rgb(211, 211, 211)" stroke-width="0.8" fill="none" stroke-dasharray="0.8 0.5"></path></g><g><path d="M102.492 190.001 C102.571 190.802, 102.815 191.904, 102.492 194.168 M102.492 194.168 C103.941 199.847, 103.817 204.894, 102.492 214.334 M102.492 214.334 C102.112 220.336, 101.249 230.318, 102.492 232.501 M102.492 232.501 C102.73 233.269, 102.63 234.133, 102.492 236.001" stroke="rgb(211, 211, 211)" stroke-width="1" fill="none" stroke-dasharray="0.5"></path></g><g><path d="M98.701 231.818 C98.701 231.818, 98.701 231.818, 98.701 231.818 M98.701 231.818 C98.701 231.818, 98.701 231.818, 98.701 231.818 M98.682 233.408 C98.819 232.876, 99.487 232.532, 100.127 231.892 M98.562 233.295 C98.864 232.893, 99.225 232.547, 100.117 232.012 M99.208 233.762 C100.044 233.395, 100.326 232.81, 101.251 232.302 M99.365 234.083 C100.009 233.407, 100.766 232.817, 101.491 232.096 M99.88 235.005 C101.06 233.6, 101.55 233.161, 103.279 231.947 M99.826 234.795 C100.73 234.09, 101.112 233.484, 102.984 232.143 M100.568 235.86 C101.532 234.638, 101.968 234.142, 104.468 231.979 M100.475 235.567 C101.455 234.468, 102.935 233.426, 104.15 231.978 M100.498 236.312 C101.706 235.237, 102.202 235.435, 105.399 232.286 M100.621 236.739 C102.289 235.109, 103.999 233.669, 105.751 232.22 M101.519 238.445 C103.289 236.289, 104.072 235.256, 107.476 232.217 M101.202 237.951 C103.836 235.888, 105.428 233.736, 107.204 232.528 M101.302 238.143 C103.245 236.994, 103.569 236.164, 105.221 234.691 M102 238.627 C102.903 237.175, 104.227 236.462, 105.532 235.218 M102.218 239.014 C103.591 238.712, 104.16 237.871, 104.71 236.918 M102.486 239.226 C103.45 238.523, 104.213 237.542, 104.778 237.162" stroke="rgb(211, 211, 211)" stroke-width="1.094" fill="none"></path><path d="M106.184 232.156 C104.915 235.918, 103.685 238.13, 101.717 240.812 M102.532 239.449 C100.733 237.819, 100.419 234.144, 97.613 232.343 M98.291 231.268 C100.858 232.315, 104.943 231.432, 106.186 232.649" stroke="rgb(211, 211, 211)" stroke-width="0.8" fill="none" stroke-dasharray="0.8 0.5"></path></g><g><path d="M102.492 294.001 C102.289 295.332, 102.463 297.586, 102.492 298.168 M102.492 298.168 C104.299 301.016, 101.601 310.101, 102.492 318.333 M102.492 318.333 C102.627 324.888, 100.647 334.798, 102.492 336.5 M102.492 336.5 C102.375 337.415, 102.542 338.784, 102.492 340" stroke="rgb(211, 211, 211)" stroke-width="1" fill="none" stroke-dasharray="0.5"></path></g><g><path d="M98.75 335.582 C98.75 335.582, 98.75 335.582, 98.75 335.582 M98.75 335.582 C98.75 335.582, 98.75 335.582, 98.75 335.582 M99.501 338.431 C100.107 337.535, 100.307 336.393, 101.067 335.862 M99.332 338.186 C99.824 337.838, 100.196 337.04, 100.956 335.682 M100.144 340.01 C101.263 339.663, 101.361 338.052, 102.919 336.319 M100.699 340.222 C101.116 339.113, 101.85 338.358, 103.333 336.067 M102.249 341.967 C102.547 340.001, 102.902 339.62, 105.399 336.834 M101.758 341.885 C103.162 339.987, 104.391 338.212, 105.556 336.166" stroke="rgb(211, 211, 211)" stroke-width="0.739" fill="none"></path><path d="M105.862 336.608 C105.011 339.041, 104.306 340.565, 103.205 344.452 M101.986 343.694 C101.77 341.301, 100.37 339.947, 99.266 336.806 M97.934 335.305 C100.947 335.655, 105.037 335.429, 106.763 336.199" stroke="rgb(211, 211, 211)" stroke-width="0.8" fill="none" stroke-dasharray="0.8 0.5"></path></g><g><path d="M102.492 398 C102.224 399.041, 102.686 400.885, 102.492 402.167 M102.492 398 C102.448 399.076, 102.456 399.846, 102.492 402.167 M102.492 402.167 C103.192 406.878, 100.992 413.083, 102.492 422.333 M102.492 402.167 C104.12 405.098, 103.543 416.82, 102.492 422.333 M102.492 422.333 C103.282 428.899, 103.145 436.102, 102.492 440.5 M102.492 422.333 C104.739 429.764, 103.324 437.001, 102.492 440.5 M102.492 440.5 C102.208 441.524, 102.171 442.76, 102.492 444 M102.492 440.5 C102.433 441.61, 102.427 442.646, 102.492 444" stroke="rgb(211, 211, 211)" stroke-width="1" fill="none" stroke-dasharray="0.5"></path></g><g><path d="M98.206 440.294 C98.206 440.294, 98.206 440.294, 98.206 440.294 M98.206 440.294 C98.206 440.294, 98.206 440.294, 98.206 440.294 M99.514 441.86 C99.725 441.213, 100.029 441.042, 100.971 440.392 M99.575 441.748 C100.033 441.271, 100.421 440.75, 101.063 440.17 M100.537 444.129 C101.419 442.453, 102.327 441.628, 103.707 439.929 M100.402 443.737 C101.342 443.062, 102.132 442.139, 103.62 440.035 M101.546 446.074 C102.831 443.684, 105.673 441.94, 107.149 439.779 M100.825 445.773 C102.554 444.04, 104.779 442.244, 106.414 440.112 M102.639 447.4 C102.802 447.02, 103.175 446.797, 104.133 445.809 M102.458 447.224 C102.88 446.8, 103.293 446.533, 103.896 445.881" stroke="rgb(211, 211, 211)" stroke-width="1.249" fill="none"></path><path d="M106.667 439.555 C105.508 441.714, 104.085 442.982, 103.286 448.742 M102.888 447.185 C101.09 445.673, 98.95 441.204, 98.369 440.203 M99.118 439.882 C100.595 439.209, 103.816 439.2, 106.832 439.868" stroke="rgb(211, 211, 211)" stroke-width="0.8" fill="none" stroke-dasharray="0.8 0.5"></path></g><g><path d="M102.492 502 C102.453 504.424, 103.112 505.691, 102.492 508.167 M102.492 508.167 C103.13 513.025, 101.057 525.111, 102.492 538.333 M102.492 538.333 C101.8 549.244, 102.099 561.627, 102.492 566.5 M102.492 566.5 C102.617 568.162, 102.086 569.671, 102.492 572" stroke="rgb(211, 211, 211)" stroke-width="1" fill="none" stroke-dasharray="0.5"></path></g><g><path d="M98.72 567.764 C98.72 567.764, 98.72 567.764, 98.72 567.764 M98.72 567.764 C98.72 567.764, 98.72 567.764, 98.72 567.764 M99.595 569.616 C100.242 569.009, 100.947 568.74, 101.306 567.862 M99.373 569.759 C100.261 568.914, 101.156 568.229, 101.443 567.835 M100.321 572.199 C101.506 570.97, 101.519 570.265, 104.391 567.733 M100.039 572.074 C101.481 570.33, 103.313 569.179, 104.095 567.742 M101.15 573.412 C103.249 571.709, 104.939 569.875, 106.137 568.596 M101.716 573.391 C103.042 572.391, 104.049 570.614, 106.37 568.331 M102.491 575.421 C102.601 575.104, 102.908 574.805, 103.642 573.968 M102.319 575.54 C102.785 575.134, 103.101 574.899, 103.857 574.089" stroke="rgb(211, 211, 211)" stroke-width="1.185" fill="none"></path><path d="M105.606 567.937 C105.505 570.084, 105.377 572.473, 103.01 576.607 M101.743 575.969 C101.754 572.103, 100.15 569.862, 98.562 568.257 M98.161 567.2 C100.381 568.431, 102.392 567.847, 106.578 567.245" stroke="rgb(211, 211, 211)" stroke-width="0.8" fill="none" stroke-dasharray="0.8 0.5"></path></g><g><path d="M102.492 630 C102.609 632.978, 102.868 635.312, 102.492 636.167 M102.492 636.167 C101.666 643.461, 103.336 653.048, 102.492 666.333 M102.492 666.333 C101.921 678.22, 102.095 690.775, 102.492 694.5 M102.492 694.5 C102.101 696.063, 101.997 697.957, 102.492 700" stroke="rgb(211, 211, 211)" stroke-width="1" fill="none" stroke-dasharray="0.5"></path></g><g><path d="M98.303 696.314 C98.303 696.314, 98.303 696.314, 98.303 696.314 M98.303 696.314 C98.303 696.314, 98.303 696.314, 98.303 696.314 M99.501 698.001 C100.003 697.847, 100.189 696.672, 101.194 695.34 M99.43 698.2 C99.948 697.128, 100.542 696.37, 101.113 695.534 M101.03 700.167 C101.924 698.122, 102.582 696.779, 103.553 695.341 M100.618 700.124 C101.405 698.545, 102.393 696.912, 103.255 695.855 M102.211 702.604 C103.179 699.941, 104.582 697.816, 104.951 696.621 M101.884 702.325 C102.759 700.062, 104.425 697.831, 105.805 695.719" stroke="rgb(211, 211, 211)" stroke-width="1.055" fill="none"></path><path d="M106.77 696.121 C104.222 699.254, 102.679 701.164, 102.135 703.373 M102.56 703.124 C101.328 700.828, 100.025 699.328, 99.151 696.092 M98.615 696.504 C100.358 696.696, 104.239 696.174, 106.492 696.151" stroke="rgb(211, 211, 211)" stroke-width="0.8" fill="none" stroke-dasharray="0.8 0.5"></path></g><g><path d="M102.492 758 C102.754 760.024, 102.704 761.509, 102.492 762.167 M102.492 758 C102.58 758.927, 102.341 759.904, 102.492 762.167 M102.492 762.167 C103.113 765.944, 101.817 775.999, 102.492 782.333 M102.492 762.167 C104.461 768.285, 103.321 774.821, 102.492 782.333 M102.492 782.333 C101.279 788.889, 100.544 796.268, 102.492 800.5 M102.492 782.333 C104.453 787.961, 104.324 796.231, 102.492 800.5 M102.492 800.5 C102.291 801.531, 102.547 802.761, 102.492 804 M102.492 800.5 C102.449 801.342, 102.498 802.118, 102.492 804" stroke="rgb(211, 211, 211)" stroke-width="1" fill="none" stroke-dasharray="0.5"></path></g><g><path d="M98.525 799.967 C98.525 799.967, 98.525 799.967, 98.525 799.967 M98.525 799.967 C98.525 799.967, 98.525 799.967, 98.525 799.967 M99.844 802.682 C101.111 802.255, 101.752 801.08, 102.553 800.324 M99.962 802.933 C101.139 801.596, 102.005 800.665, 102.763 799.912 M101.79 805.871 C103.51 803.178, 105.498 801.59, 106.392 800.651 M101.185 805.415 C103.664 803.071, 106.219 801.13, 107.113 799.988" stroke="rgb(211, 211, 211)" stroke-width="1.301" fill="none"></path><path d="M106.857 799.697 C104.363 803.793, 103.118 806.54, 103.354 808.307 M103.175 807.644 C101.864 805.209, 99.316 802.349, 97.869 800.272 M97.897 800.506 C101.133 799.671, 104.126 800.079, 106.776 800.162" stroke="rgb(211, 211, 211)" stroke-width="0.8" fill="none" stroke-dasharray="0.8 0.5"></path></g><g><path d="M102.492 862 C102.897 863.837, 102.363 865.35, 102.492 866.167 M102.492 862 C102.665 863.264, 102.447 864.699, 102.492 866.167 M102.492 866.167 C103.54 869.992, 100.98 878.988, 102.492 886.333 M102.492 866.167 C101.292 871.301, 101.755 879.834, 102.492 886.333 M102.492 886.333 C102.262 895.81, 102.864 902.382, 102.492 904.499 M102.492 886.333 C100.73 894.03, 104.163 899.416, 102.492 904.499 M102.492 904.499 C102.469 905.804, 102.767 906.278, 102.492 907.999 M102.492 904.499 C102.44 905.252, 102.347 906.067, 102.492 907.999" stroke="rgb(211, 211, 211)" stroke-width="1" fill="none" stroke-dasharray="0.5"></path></g><g><path d="M98.253 904.321 C98.253 904.321, 98.253 904.321, 98.253 904.321 M98.253 904.321 C98.253 904.321, 98.253 904.321, 98.253 904.321 M99.074 904.988 C99.211 904.612, 99.467 904.528, 99.713 904.174 M99.038 904.898 C99.223 904.709, 99.378 904.48, 99.67 904.083 M99.087 906.522 C99.892 905.892, 100.209 905.397, 101 904.106 M99.188 906.238 C99.722 905.674, 100.351 904.871, 101.105 903.911 M99.804 907.129 C100.528 905.819, 101.02 904.765, 102.238 903.984 M100.005 906.996 C100.882 906.002, 101.642 904.929, 102.555 903.648 M100.13 908.031 C101.846 906.908, 102.354 904.81, 102.763 904.208 M100.323 908.32 C101.289 906.797, 102.265 905.531, 103.219 904.323 M100.592 909.476 C102.36 907.739, 103.34 905.963, 104.773 903.638 M100.946 908.664 C102.237 907.272, 103.681 905.91, 104.362 904.345 M100.873 909.71 C103.401 908.15, 105.121 905.485, 106.503 904.6 M101.347 910.358 C102.581 908.229, 104.954 906.157, 106.382 904.284 M101.4 910.632 C102.941 908.633, 104.556 906.344, 106.178 905.172 M101.827 911.193 C104.167 908.992, 105.313 906.466, 106.545 904.415" stroke="rgb(211, 211, 211)" stroke-width="1.245" fill="none"></path><path d="M106.3 904.009 C106.062 906.382, 105.054 907.957, 103.076 911.785 M101.867 912.109 C101.728 909.822, 101.199 908.39, 99.257 903.876 M98.385 904.497 C100.175 904.527, 102.335 903.383, 106.334 903.338" stroke="rgb(211, 211, 211)" stroke-width="0.8" fill="none" stroke-dasharray="0.8 0.5"></path></g><g><path d="M102.492 965.999 C102.402 967.299, 102.782 968.412, 102.492 970.166 M102.492 970.166 C101.168 973.394, 104.025 983.282, 102.492 990.332 M102.492 990.332 C100.737 999.692, 103.975 1006.007, 102.492 1008.499 M102.492 1008.499 C102.791 1010.086, 102.383 1011.152, 102.492 1011.999" stroke="rgb(211, 211, 211)" stroke-width="1" fill="none" stroke-dasharray="0.5"></path></g><g><path d="M98.246 1008.254 C98.246 1008.254, 98.246 1008.254, 98.246 1008.254 M98.246 1008.254 C98.246 1008.254, 98.246 1008.254, 98.246 1008.254 M99.647 1009.739 C100.007 1009.396, 100.399 1008.9, 101.024 1008.363 M99.784 1009.704 C100.095 1009.106, 100.682 1008.635, 101.072 1008.265 M100.652 1011.321 C101.762 1010.852, 102.802 1009.991, 103.809 1008.176 M100.371 1011.959 C101.76 1010.335, 103.046 1009.485, 103.669 1007.974 M100.404 1013.132 C103.235 1011.618, 105.591 1010.462, 107.524 1008.038 M101.519 1013.741 C102.715 1012.854, 103.688 1010.897, 106.875 1008.283 M102.467 1015.421 C102.948 1015.029, 103.463 1014.533, 103.978 1013.828 M102.535 1015.179 C103.026 1014.801, 103.321 1014.475, 104.055 1013.829" stroke="rgb(211, 211, 211)" stroke-width="0.876" fill="none"></path><path d="M106.178 1008.417 C105.47 1010.247, 104.183 1012.734, 102.743 1015.896 M106.132 1007.794 C105.678 1009.76, 104.411 1012.444, 102.64 1015.948 M103.132 1015.257 C101.538 1014.081, 100.929 1011.091, 97.879 1007.671 M102.15 1015.704 C101.804 1014.06, 100.574 1012.521, 98.285 1007.734 M97.922 1008.062 C101.44 1008.059, 105.171 1007.732, 107.009 1008.173 M98.886 1007.719 C101.651 1007.727, 104.711 1007.72, 106.165 1007.913" stroke="rgb(211, 211, 211)" stroke-width="0.8" fill="none" stroke-dasharray="0.8 0.5"></path></g><g><path d="M102.492 1069.999 C102.932 1071.002, 102.964 1073.273, 102.492 1076.166 M102.492 1076.166 C104.105 1083.604, 103.522 1095.223, 102.492 1106.332 M102.492 1106.332 C104.102 1116.781, 102.902 1129.248, 102.492 1134.499 M102.492 1134.499 C102.663 1135.648, 102.593 1137.45, 102.492 1139.999" stroke="rgb(211, 211, 211)" stroke-width="1" fill="none" stroke-dasharray="0.5"></path></g><g><path d="M98.348 1136.155 C98.348 1136.155, 98.348 1136.155, 98.348 1136.155 M98.348 1136.155 C98.348 1136.155, 98.348 1136.155, 98.348 1136.155 M99.305 1138.447 C99.863 1137.372, 100.337 1136.503, 101.391 1135.79 M99.182 1138.334 C99.868 1137.557, 100.408 1136.851, 101.187 1135.916 M100.638 1139.839 C101.831 1138.442, 102.428 1137.052, 104.07 1136.276 M100.718 1139.451 C101.683 1138.626, 102.825 1137.208, 104.069 1136.17 M102.056 1140.907 C102.675 1139.769, 103.849 1139.461, 107.304 1135.18 M101.329 1141.965 C103.354 1139.708, 104.251 1138.519, 107.127 1136.088 M102.094 1143.65 C102.407 1143.48, 102.984 1143.263, 103.487 1142.435 M102.247 1143.861 C102.546 1143.364, 103.043 1142.765, 103.635 1142.289" stroke="rgb(211, 211, 211)" stroke-width="0.52" fill="none"></path><path d="M106.297 1136.465 C105.938 1137.037, 104.227 1139.41, 102.615 1144.667 M102.563 1144.031 C100.949 1141.649, 100.334 1137.614, 97.634 1136.405 M98.492 1135.5 C99.981 1136.165, 102.628 1135.454, 107.061 1135.458" stroke="rgb(211, 211, 211)" stroke-width="0.8" fill="none" stroke-dasharray="0.8 0.5"></path></g><g style="color: rgb(204, 204, 204); font-family: "trebuchet ms", verdana, arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400;" transform="matrix(0.999998, 0, 0, 0.999998, 39.5083, 87.0009)"><foreignObject height="24" width="125.96665954589844"><div class="labelBkg" xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>Requête entrante</p></span></div></foreignObject></g><g style="color: rgb(204, 204, 204); font-family: "trebuchet ms", verdana, arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400;" transform="matrix(0.999998, 0, 0, 0.999998, 3.20475e-06, 0.00100636)"><foreignObject height="0" width="0"><div class="labelBkg" xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g><g style="color: rgb(204, 204, 204); font-family: "trebuchet ms", verdana, arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400;" transform="matrix(0.999998, 0, 0, 0.999998, 3.20475e-06, 0.00100636)"><foreignObject height="0" width="0"><div class="labelBkg" xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g><g style="color: rgb(204, 204, 204); font-family: "trebuchet ms", verdana, arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400;" transform="matrix(0.999998, 0, 0, 0.999998, 3.20475e-06, 0.00100636)"><foreignObject height="0" width="0"><div class="labelBkg" xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g><g style="color: rgb(204, 204, 204); font-family: "trebuchet ms", verdana, arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400;" transform="matrix(0.999998, 0, 0, 0.999998, 45.9749, 527)"><foreignObject height="24" width="113.03334045410156"><div class="labelBkg" xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>Requête traitée</p></span></div></foreignObject></g><g style="color: rgb(204, 204, 204); font-family: "trebuchet ms", verdana, arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400;" transform="matrix(0.999998, 0, 0, 0.999998, 41.3583, 655)"><foreignObject height="24" width="122.26666259765625"><div class="labelBkg" xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>Réponse générée</p></span></div></foreignObject></g><g style="color: rgb(204, 204, 204); font-family: "trebuchet ms", verdana, arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400;" transform="matrix(0.999998, 0, 0, 0.999998, 3.20475e-06, 0.00100636)"><foreignObject height="0" width="0"><div class="labelBkg" xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g><g style="color: rgb(204, 204, 204); font-family: "trebuchet ms", verdana, arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400;" transform="matrix(0.999998, 0, 0, 0.999998, 3.20475e-06, 0.00100636)"><foreignObject height="0" width="0"><div class="labelBkg" xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g><g style="color: rgb(204, 204, 204); font-family: "trebuchet ms", verdana, arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400;" transform="matrix(0.999998, 0, 0, 0.999998, 3.20475e-06, 0.00100636)"><foreignObject height="0" width="0"><div class="labelBkg" xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g><g style="color: rgb(204, 204, 204); font-family: "trebuchet ms", verdana, arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400;" transform="matrix(0.999998, 0, 0, 0.999998, 40.3916, 1095)"><foreignObject height="24" width="124.19999694824219"><div class="labelBkg" xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>Réponse sortante</p></span></div></foreignObject></g><g><path d="M19.269 8.044 C19.269 8.044, 19.269 8.044, 19.269 8.044 M19.269 8.044 C19.269 8.044, 19.269 8.044, 19.269 8.044 M19.43 10.71 C20.452 9.885, 21.164 9.197, 21.841 7.564 M19.509 10.955 C20.129 10.109, 20.673 9.461, 21.989 7.76 M20.031 13.89 C20.559 12.472, 22.509 10.434, 24.844 8.373 M19.705 13.783 C21.677 11.454, 23.478 9.347, 24.762 7.65 M18.948 18.516 C21.408 15.635, 23.911 12.312, 26.569 7.84 M19.07 17.363 C21.418 15.183, 22.575 12.202, 27.785 7.268 M18.706 18.85 C22.339 16.649, 24.878 13.047, 29.189 7.904 M19.52 20.551 C22.103 16.195, 25.348 12.96, 30.011 8.132 M19.206 21.405 C23.329 19.977, 26.438 13.993, 32.344 9.827 M20.27 23.807 C23.299 17.062, 29.104 12.089, 32.943 9.064 M19.15 27.255 C21.31 22.731, 26.558 17.496, 33.641 6.979 M19.062 25.515 C25.718 18.981, 31.699 12.845, 34.68 8.594 M19.514 29.729 C26.405 21.867, 30.527 15.382, 38.367 7.924 M19.766 27.762 C25.774 21.691, 30.569 15.93, 38.514 7.937 M17.788 30.731 C22.837 25.84, 31.49 22.143, 39.53 6.316 M19.386 32.142 C25.241 25.991, 31.966 18.168, 41.677 7.175 M18.962 34.08 C25.127 28.953, 33.652 20.361, 42.002 8.559 M18.848 34.003 C27.883 24.901, 35.492 15.951, 43.882 7.309 M18.349 38.652 C28.36 28.138, 37.135 17.486, 45.359 6.848 M18.844 37.671 C27.34 30.586, 33.931 21.622, 47.08 9.03 M20.757 39.305 C28.469 28.198, 39.744 15.409, 48.893 10.028 M18.433 39.791 C27.741 32.212, 37.021 22.574, 49.642 7.543 M20.12 44.367 C25.682 34.462, 34.077 27.284, 52.52 9.331 M18.636 44.135 C26.193 36.496, 32.138 28.75, 51.43 7.436 M20.314 47.03 C27.738 37.452, 36.426 27.116, 56.243 7.41 M19.547 47.59 C31.021 34.599, 44.272 19.903, 54.488 8.456 M17.472 51.224 C31.675 34.995, 44.007 23.059, 56.044 7.216 M18.927 49.144 C29.628 38.226, 40.504 25.786, 56.307 9.349 M17.321 54.346 C30.09 38.867, 40.916 28.426, 58.385 9.772 M20.289 52.28 C31.233 39.589, 43.81 27.399, 59.387 7.283 M20.737 55.362 C29.84 40.981, 42.162 30.23, 61.232 8.041 M19.137 56.024 C36.535 37.858, 53.145 17.953, 63.409 8.372 M21.205 59.161 C33.678 40.917, 47.803 28.536, 65.446 7.295 M18.752 58.364 C36.395 38.442, 54.81 19.061, 65.771 8.565 M19.338 61.19 C31.702 48.897, 40.937 36.602, 67.678 6.677 M19.07 60.936 C30.938 50.448, 40.955 37.92, 67.402 7.066 M19.441 60.615 C36.493 48.229, 49.791 29.617, 72.061 9.005 M21.713 63.271 C36.684 45.357, 49.829 30.497, 70.721 7.834 M22.57 61.545 C35.143 50.112, 47.599 37.492, 74.085 7.794 M24.876 61.418 C42.582 40.867, 62.427 20.147, 73.5 7.762 M25.638 60.48 C43.267 46.627, 56.995 27.433, 77.53 7.94 M27.603 62.516 C44.017 43.611, 61.324 23.534, 75.982 7.598 M29.107 62.621 C40.865 48.406, 50.931 36.864, 79.229 6.337 M28.407 62.569 C42.655 46.718, 57.071 32.089, 79.677 8.216 M31.156 60.721 C48.968 43.606, 64.193 27.524, 80.465 6.836 M31.599 61.731 C43.083 48.954, 54.178 37.391, 82.066 8.711 M36.464 62.617 C47.659 47.9, 64.822 31.16, 86.287 9.069 M34.222 62.908 C44.181 51.659, 55.616 39.622, 83.525 7.049 M36.4 62.739 C54.249 43.65, 70.626 25.519, 87.142 9.766 M38.417 62.412 C54.474 46.193, 69.002 28.54, 87.367 7.581 M39.766 61.148 C57.296 43.699, 75.913 23.132, 88.633 9.905 M40.692 62.69 C57.031 43.415, 73.542 25.663, 89.499 7.527 M44.172 64.108 C56.153 49.283, 66.888 37.277, 93.288 8.737 M42.688 62.094 C55.689 48.441, 67.168 34.444, 91.889 7.141 M46.11 63.234 C64.819 44.889, 81.311 22.385, 95.848 8.774 M44.531 63.163 C56.525 50.357, 67.57 37.758, 94.815 8.921 M47.695 61.181 C68.211 42.35, 87.05 24.055, 96.493 7.882 M47.516 62.894 C64.391 46.101, 79.622 30.016, 97.674 7.874 M50.01 63.137 C66.43 42.587, 85.092 27.373, 101.537 7.319 M51.566 61.616 C61.931 51.922, 70.604 41.298, 100.344 7.362 M52.192 63.75 C69.163 46.08, 80.984 30.934, 101.44 8.791 M53.296 63.013 C72.043 43.047, 90.968 21.612, 102.767 7.505 M57.221 61.006 C71.887 43.126, 89.125 26.541, 104.549 8.465 M57.083 61.812 C70.719 46.19, 84.042 30.955, 106.534 7.512 M59.216 61.368 C76.059 42.089, 93.402 27.317, 108.849 7.304 M57.786 63.297 C76.646 42.395, 95.005 22.604, 108.006 8.096 M59.982 61.862 C72.702 48.039, 86.005 33.952, 113.087 7.16 M62.333 63.195 C73.945 48.644, 86.537 34.974, 111.932 8.725 M64.748 62.912 C82.228 43.295, 100.853 23.813, 112.924 9.29 M63.802 62.782 C82.824 42.545, 100.848 24.088, 114.345 6.796 M66.444 62.296 C84.152 44.971, 100.162 25.502, 114.508 9.705 M66.205 62.198 C84.257 44.108, 103.446 22.94, 116.83 7.664 M70.326 61.705 C88.482 43.421, 107.849 21.188, 120.686 9.764 M70.208 62.203 C85.505 44.796, 100.156 27.934, 120.013 7.932 M73.039 62.743 C88.203 46.162, 101.124 30.984, 121.704 7.86 M73.452 62.422 C83.831 48.179, 95.921 36.032, 122.123 7.509 M75.2 63.528 C87.747 45.828, 103.569 30.676, 126.389 7.368 M74.846 63.49 C89.051 47.354, 102.268 30.648, 124.632 7.22 M78.995 61.716 C94.238 45.68, 110.689 27.089, 128.577 9.504 M77.001 63.494 C87.809 50.677, 98.951 39.005, 127.371 8.042 M82.157 64.258 C99.441 40.58, 118.788 20.631, 131.73 5.819 M80.47 62.411 C90.338 51.915, 100.422 41.199, 130.959 7.835 M83.712 60.749 C100.037 46.98, 111.653 31.481, 133.104 7.768 M82.517 62.855 C97.021 47.014, 111.612 32.443, 131.649 7.721 M87.765 62.461 C98.714 47.163, 109.632 34.493, 135.726 9.984 M87.015 62.276 C105.925 40.616, 125.399 18.256, 135.271 7.429 M89.269 60.893 C103.939 42.281, 124.463 23.594, 137.215 8.037 M89.222 63.75 C106.619 41.822, 127.398 20.392, 137.227 8.579 M91.262 63.475 C108.263 46.423, 124.216 26.606, 142.879 9.109 M91.264 63.499 C104.707 48.417, 116.23 35.872, 140.673 8.648 M92.891 64.357 C104.771 49.186, 118.22 37.151, 145.076 8.219 M93.845 62.312 C109.95 46.343, 126.283 29.147, 143.17 7.151 M96.308 64.178 C109.803 47.775, 122.017 35.005, 144.289 9.957 M96.736 62.154 C108.873 48.433, 120.901 35.355, 146.739 9.304 M98.554 60.822 C109.162 49.569, 123.033 38.639, 148.662 7.001 M98.902 62.497 C117.195 43.122, 135.531 23.391, 149.782 8.257 M103.757 64.533 C111.687 49.097, 126.366 38.911, 153.528 7.69 M101.54 62.443 C120.094 44.167, 138.364 22.913, 150.765 7.423 M105.618 61.171 C120.998 46.118, 132.556 32.635, 154.676 7.623 M104.778 63.209 C124.314 42.898, 142.026 21.168, 155.206 7.256 M106.519 63.249 C122.366 46.849, 139.867 29.13, 155.877 8.273 M106.915 62.665 C127.215 40.649, 147.263 20.326, 157.886 8.23 M110.645 63.327 C125.995 44.54, 140.536 29.028, 158.853 6.71 M111.065 61.751 C125.391 45.167, 139.912 30.372, 160.299 8.111 M112.807 62.721 C128.358 43.448, 146.839 24.332, 162.685 7.22 M112.369 62.127 C129.391 46.064, 145.135 27.321, 161.366 8.959 M115.418 63.729 C133.816 41.068, 154.295 22.161, 163.671 9.588 M115.174 61.407 C129.703 45.847, 144.145 30.185, 164.512 8.857 M119.493 63.229 C134.935 46.53, 148.488 28.978, 168.494 6.158 M118.173 63.24 C130.573 47.163, 144.669 31.881, 167.4 7.422 M119.98 63.129 C131.19 53.475, 139.705 40.214, 171.996 8.077 M121.812 62.394 C134.327 49.298, 146.463 34.493, 170.693 8.579 M124.99 63.685 C138.436 43.733, 155.764 25.427, 173.346 6.242 M123.697 63.305 C134.888 51.695, 145.495 39.713, 173.848 7.354 M125.779 63.836 C135.302 52.48, 145.634 39.221, 176.957 7.741 M126.685 63.335 C137.523 50.477, 148.594 36.986, 176.015 8.454 M130.663 63.297 C141.943 51.383, 150.697 40.073, 178.886 9.337 M129.886 62.071 C142.452 48.191, 155.842 33.974, 179.354 8.899 M133.012 62.357 C147.32 43.398, 164.919 28.773, 179.857 7 M132.02 61.812 C150.49 41.748, 171.239 19.107, 181.051 8.779 M135.392 63.693 C153.657 40.727, 174.21 20.78, 185.448 6.287 M133.476 62.891 C150.167 45.598, 166.044 27.856, 183.487 8.387 M136.973 62.688 C149.526 49.45, 160.231 40.387, 186.528 9.271 M137.762 61.665 C148.214 50.644, 160.25 37.249, 186.317 8.344 M141.338 63.083 C151.998 49.676, 162.545 39.505, 186.289 10.352 M139.197 62.969 C150.976 52.308, 160.53 41.733, 185.916 10.65 M142.128 63.864 C158.237 44.107, 175.176 28.716, 188.478 15.64 M142.205 62.457 C151.604 51.774, 162.721 41.984, 187.26 15.009 M144.731 62.814 C156.726 50.842, 169.512 35.284, 186.213 19.211 M145.361 62.799 C154.782 51.732, 165.754 41.783, 185.906 18.32 M148.882 64.045 C158.262 50.628, 169.119 40.453, 187.251 19.457 M147.658 62.657 C161.979 48.115, 174.782 31.646, 185.418 21.416 M148.98 63.256 C163.78 47.44, 180.931 30.507, 186.755 24.238 M149.877 63.063 C158.742 54.878, 165.74 45.086, 186.67 24.044 M154.592 63.343 C167.135 50.612, 177.717 32.965, 186.813 28.004 M152.864 62.597 C165.588 47.906, 178.883 35.205, 186.278 26.889 M155.847 63.473 C163.339 56.232, 169.198 48.321, 186.817 29.265 M156.818 61.44 C168.139 50.561, 179.012 37.907, 186.142 30.162 M161.086 60.752 C165.005 53.865, 171.146 48.655, 184.528 31.066 M159.248 62.731 C168.917 51.442, 178.84 40.727, 186.771 32.77 M161.023 63.094 C172.002 53.546, 180.592 42.253, 186.3 37.326 M161.855 62.833 C171.173 52.029, 180.016 41.681, 185.895 34.413 M165.703 63.176 C171.168 57.32, 176.271 48.778, 184.452 36.844 M164.429 62.094 C172.383 52.452, 181.739 43.889, 186.227 38.059 M166.91 62.222 C173.856 57.347, 178.012 48.582, 185.584 41.18 M166.098 62.743 C173.734 55.732, 180.664 46.491, 185.578 41.143 M169.662 61.759 C174.837 56.594, 180.015 54.69, 185.49 44.591 M170.713 61.947 C176.211 56.047, 179.855 49.684, 186.562 45.366 M173.874 63.161 C175.718 55.388, 181.066 51.783, 187.172 48.325 M172.638 62.135 C175.744 58.326, 178.12 56.526, 185.172 47.063 M175.53 63.28 C179.841 59.505, 181.76 55.424, 185.89 49.347 M175.407 61.846 C178.782 59.262, 182.199 55.352, 186.802 50.273 M177.987 62.571 C180.809 58.924, 182.626 56.59, 186.706 52.047 M177.916 63.033 C180.254 59.459, 182.471 56.87, 186.424 53.231 M180.497 63.239 C182.879 60.602, 185.135 58.274, 185.767 55.546 M180.734 62.437 C183.266 60.347, 185.681 56.955, 186.937 56.021 M183.387 62.224 C184.31 61.352, 185.164 59.741, 186.291 59.319 M183.468 62.452 C183.683 61.715, 184.393 61.169, 186.112 59.353" stroke="rgb(31, 32, 32)" stroke-width="2.377" fill="none"></path><path d="M19.949 8.287 C79.248 5.396, 138.33 7.906, 187.298 7.793 M18.519 7.261 C53.254 6.297, 87.688 5.897, 185.86 7.229 M184.519 6.229 C187.248 28.025, 187.88 49.005, 186.22 62.029 M186.228 8.552 C186.919 27.093, 186.092 46.161, 185.454 62.554 M186.263 63.136 C130.077 59.985, 78.736 60.065, 18.413 60.754 M184.864 62.46 C147.292 63.046, 108.426 61.795, 18.392 62.533 M17.355 61.588 C18.367 47.488, 17.092 29.299, 19.349 8.341 M18.969 62.996 C19.096 41.116, 19.85 22.944, 19.595 8.99" stroke="rgb(204, 204, 204)" stroke-width="1" fill="none"></path></g><g style="color: rgb(204, 204, 204); font-family: "trebuchet ms", verdana, arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400;" transform="matrix(0.999998, 0, 0, 0.999998, 49.3083, 23.001)"><foreignObject height="24" width="106.36666870117188"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Requête Client</p></span></div></foreignObject></g><g><path d="M24.866 135.784 C24.866 135.784, 24.866 135.784, 24.866 135.784 M24.866 135.784 C24.866 135.784, 24.866 135.784, 24.866 135.784 M24.826 141.383 C26.656 139.348, 28.567 137.521, 30.024 136.281 M24.565 141.873 C25.619 140.494, 27.117 139.57, 29.461 136.276 M26.127 147.955 C26.746 143.317, 29.378 142.397, 33.897 136.025 M25.115 148.035 C29.773 143.675, 33.475 139.139, 35.558 135.565 M24.502 154.837 C29.845 148.366, 31.3 144.608, 41.392 134.479 M24.724 152.492 C29.37 147.217, 35.687 143.033, 41.752 136.463 M24.209 158.005 C32.231 154.505, 35.965 146.71, 44.674 136.972 M25.272 160.051 C30.707 152.448, 37.164 144.818, 46.909 136.922 M26.277 164.314 C32.357 156.774, 39.603 151.757, 51.312 135.886 M24.484 166.266 C32.062 158.022, 40.14 149.384, 51.169 135.782 M26.367 169.673 C38.032 157.322, 48.426 144.722, 55.599 137.626 M24.053 170.303 C36.786 160.317, 47.351 148.121, 57.421 136.189 M23.631 177.755 C35.704 164.494, 48.115 152.995, 62.827 136.333 M25.098 177.353 C40.42 162.036, 54.196 144.783, 62.17 135.925 M23.064 182.188 C37.159 167.964, 53.483 150.803, 68.09 137.3 M25.586 183.244 C34.109 173.603, 42.742 163.964, 67.274 136.197 M26.789 188.227 C45.613 169.118, 61.433 146.183, 73.812 137.462 M25.325 187.981 C42.927 168.5, 63.533 147.288, 73.016 134.964 M27.536 190.972 C44.711 171.325, 65.26 151.102, 79.472 136.194 M26.776 192.509 C43.16 174.974, 58.569 157.125, 79.759 134.743 M33.557 191.819 C49.522 174.332, 66.38 157.051, 83.782 134.331 M32.541 192.763 C48.125 173.254, 64.958 156.584, 84.681 136.297 M38.572 191.06 C57.253 172.035, 78.579 145.899, 88.429 135.16 M37.356 192.022 C51.974 178.97, 64.037 164.755, 89.211 135.879 M44.142 193.176 C54.585 177.386, 69.378 165.039, 96.787 135.691 M44.375 192.244 C56.154 179.263, 68.06 164.932, 95.976 136.724 M47.599 193.874 C60.001 179.041, 73.282 167.047, 100.498 137.508 M49.3 191.405 C62.609 178.784, 75.763 163.898, 101.33 137.222 M54.438 193.862 C71.69 173.199, 88.392 156.929, 108.134 136.118 M55.055 191.796 C66.764 178.993, 78.733 165.104, 106.21 135.546 M57.757 190.461 C73.845 177.625, 85.035 165.851, 111.238 134.074 M59.649 192.503 C79.844 170.369, 99.945 147.909, 111.775 135.262 M63.401 193.736 C83.897 170.082, 105.977 148.686, 115.318 136.381 M65.276 191.171 C76.377 180.707, 87.922 168.224, 116.667 135.7 M72.248 192.372 C91.3 172.562, 107.023 150.34, 123.629 137.347 M70.002 193.649 C87.03 174.153, 103.985 155.894, 121.834 135.097 M74.988 193.823 C88.248 176.742, 102.716 161.64, 128.622 135.46 M75.502 192.309 C92.757 176.259, 107.194 158.375, 129.164 135.627 M80.388 190.758 C98.535 169.929, 117.526 149.691, 133.76 134.958 M82.357 192.479 C94.847 177.388, 108.916 162.08, 134.128 136.629 M87.843 192.549 C101.146 178.003, 113.415 162.985, 138.988 135.784 M87.166 192.408 C104.166 174.264, 120.533 155.467, 137.852 135.465 M92.197 192.722 C110.16 169.433, 130.828 150.524, 145.371 136.174 M92.6 193.193 C110.58 172.004, 130.754 151.549, 144.541 135.841 M99.033 192.408 C114.08 177.163, 128.123 161.754, 148.689 137.135 M98.333 192.152 C118.04 171.843, 136.202 150.706, 149.07 136.922 M102.169 191.577 C119.869 174.409, 135.751 156.722, 155.144 135.955 M103.127 191.555 C118.982 177.379, 133.659 160.538, 155.26 135.86 M110.486 192.297 C120.85 176.286, 136.266 160.741, 159.782 135.677 M107.932 192.656 C121.741 177.612, 135.465 162.745, 160.625 134.914 M114.916 190.495 C134.308 170.059, 153.818 150.852, 166.278 136.213 M113.426 191.358 C129.095 175.592, 142.534 160.882, 165.683 136.427 M118.776 191.177 C134.82 175.53, 151.479 160.61, 172.831 135.513 M120.507 191.471 C132.302 179.804, 144.779 166.268, 170.695 136.561 M123.29 194.272 C142.662 172.574, 163.675 151.975, 175.36 135.807 M124.423 191.564 C142.226 173.28, 159.812 154.362, 177.329 135.062 M130.463 194.09 C145.944 173.489, 163.713 156.084, 181.543 138.744 M130.769 192.225 C149.266 170.959, 170.369 148.687, 182.126 137.487 M135.897 190.597 C145.751 178.962, 160.268 167.972, 183.129 142.688 M135.629 191.816 C154.072 172.503, 169.594 154.202, 181.179 142.943 M139.292 193.783 C149.3 182.038, 162.007 171.283, 182.865 150.375 M141.026 192.83 C150.084 181.543, 159.953 171.974, 180.818 147.745 M147.061 190.949 C154.568 183.027, 166.475 171.84, 180.654 155.648 M146.921 192.076 C154.805 183.704, 162.374 175.271, 181.399 155.562 M150.943 191.29 C160.816 181.106, 171.027 173.977, 180.192 160.936 M151.813 191.651 C160.593 183.571, 169.102 174.416, 180.891 160.256 M158.208 191.798 C163.152 186.703, 169.381 179.881, 179.205 166.728 M156.96 193.052 C163.183 187.243, 168.802 179.526, 181.916 166.341 M164.289 191.25 C170.759 186.631, 178.576 176.847, 181.764 174.458 M163.329 191.973 C168.411 185.048, 174.209 179.602, 181.097 171.757 M167.721 191.685 C172.164 187.632, 177.267 183.42, 180.129 178.133 M168.579 192.853 C172.142 188.209, 175.938 185.155, 182.226 178.583 M173.666 191.635 C177.553 188.797, 178.876 186.126, 181.147 184.848 M174.588 191.789 C176.834 189.334, 179.38 186.641, 181.301 185.021" stroke="rgb(31, 32, 32)" stroke-width="2.086" fill="none"></path><path d="M24.316 134.677 C60.05 133.554, 98.03 136.778, 180.483 136.97 M181.827 134.185 C179.339 146.373, 182.11 159.857, 182.215 188.151 M181.372 188.175 C121.063 188.866, 61.039 189.492, 26.219 191.634 M25.771 190.207 C24.164 173.845, 26.313 156.424, 26.41 134.78" stroke="rgb(204, 204, 204)" stroke-width="1" fill="none"></path></g><g style="color: rgb(204, 204, 204); font-family: "trebuchet ms", verdana, arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400;" transform="matrix(0.999998, 0, 0, 0.999998, 54.6666, 151.001)"><foreignObject height="24" width="95.64999389648438"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Middleware 1</p></span></div></foreignObject></g><g><path d="M24.703 239.954 C24.703 239.954, 24.703 239.954, 24.703 239.954 M24.703 239.954 C24.703 239.954, 24.703 239.954, 24.703 239.954 M24.368 243.665 C25.699 242.306, 26.012 241.11, 26.61 240.356 M24.287 243.356 C25.184 242.704, 26.236 241.397, 26.858 240.249 M24.527 245.822 C25.927 243.755, 27.851 241.677, 29.127 240.366 M24.895 246.102 C26.263 244.424, 27.057 243.46, 29.949 239.651 M24.301 249.854 C26.123 246.89, 29.217 246.024, 31.378 239.145 M25.187 250.487 C26.32 247.107, 29.212 244.139, 32.008 240.224 M25.989 253.766 C30.061 248.528, 33.455 242.471, 34.981 240.279 M25.362 252.849 C27.634 249.101, 30.459 245.251, 34.285 239.471 M26.079 256.623 C27.772 249.385, 31.267 244.037, 38.578 239.497 M24.653 256.727 C28.512 252.382, 31.351 248.338, 37.393 239.299 M25.313 259.548 C28.308 254.16, 32.646 250.879, 37.887 240.741 M24.633 260.418 C30.457 252.582, 36.335 245.419, 40.071 239.944 M25.869 264.204 C32.832 253.58, 38.138 245.641, 43.226 240.273 M23.897 262.041 C29.309 258.208, 33.327 252.369, 41.985 240.674 M23.837 264.414 C32.717 256.665, 41.511 247.492, 45.893 242.113 M23.676 266.065 C28.291 260.703, 34.682 254.569, 44.986 240.808 M26.562 270.524 C35.36 257.069, 44.413 247.338, 48.255 241.348 M25.073 269.829 C33.25 256.966, 43.015 246.386, 48.492 239.581 M23.46 271.707 C32.814 259.98, 43.021 249.985, 51.022 238.488 M24.947 273.237 C34.107 261.296, 43.635 247.868, 49.164 241.015 M22.458 274.876 C35.489 262.36, 45.568 248.214, 53.083 240.212 M24.355 276.993 C31.643 267.072, 39.191 255.894, 51.972 238.949 M23.012 280.98 C33.839 266.751, 46.817 252.059, 56.675 240.079 M24.704 279.184 C32.573 270.154, 39.898 259.695, 53.952 241.208 M24.24 281.207 C33.959 273.991, 40.77 261.918, 58.551 240.362 M25.289 282.395 C31.721 272.322, 40.617 261.795, 57.232 240.447 M25.851 286.102 C36.868 270.782, 47.909 252.092, 61.031 238.454 M24.164 285.971 C38.932 269.161, 51.133 252.683, 60.959 238.764 M26.649 288.189 C39.579 269.896, 53.377 254.353, 61.34 238.458 M23.849 289.488 C37.161 273.304, 51.366 256.037, 63.072 239.894 M23.04 291.298 C41.367 270.435, 56.244 250.635, 66.447 239.339 M23.756 292.083 C38.822 273.754, 54.168 253.434, 64.842 239.072 M25.589 292.662 C36.262 279.107, 46.874 264.81, 69.086 241.8 M26.298 294.107 C41.902 275.162, 56.37 255.301, 68.493 240.067 M28.421 294.148 C42.682 276.866, 56.371 260.795, 70.893 240.846 M27.426 294.403 C38.809 281.203, 48.493 266.811, 70.11 240.292 M32.11 294.074 C44.355 275.016, 57.458 256.913, 72.385 239.115 M31.03 293.071 C43.252 278.611, 55.807 262.588, 72.134 240.005 M31.851 292.729 C49.729 273.196, 66.438 251.245, 74.354 237.98 M34.157 292.754 C41.19 282.806, 51.758 271.715, 75.268 240.25 M34.863 293.869 C47.487 279.477, 58.659 265.434, 78.232 238.48 M35.878 294.489 C49.899 274.112, 64.08 255.427, 76.704 239.482 M37.262 292.293 C54.096 274.307, 69.642 250.778, 81.712 241.357 M38.681 294.561 C49.941 280.872, 60.907 265.645, 80.079 239.464 M41.175 293.272 C50.281 281.088, 64.363 264.567, 83.859 240.921 M40.351 294.287 C48.332 283.445, 57.99 270.743, 81.826 240.188 M42.291 293.056 C58.441 274.38, 70.223 255.99, 86.947 238.773 M44.185 293.787 C56.106 277.64, 68.708 261.748, 85.39 239.226 M45.869 293.846 C54.464 281.861, 63.391 273.83, 89.411 239.244 M45.162 293.626 C60.209 274.446, 77.105 255.424, 87.341 240.187 M47.718 293.931 C58.971 280.839, 71.614 264.076, 90.954 240.879 M47.823 293.973 C60.621 278.51, 73.833 262.413, 90.409 239.625 M50.919 294.589 C63.772 277.872, 77.384 259.455, 92.577 239.133 M50.785 294.972 C66.117 274.722, 79.146 255.995, 93.844 240.251 M52.056 295.451 C63.185 280.345, 77.245 264.793, 96.471 239.967 M54.571 293.744 C69.769 273.723, 84.549 253.479, 95.576 240.622 M55.274 295.335 C68.324 278.649, 79.368 265.786, 97.797 240.595 M55.442 295.206 C69.602 277.088, 82.241 260.669, 98.278 239.811 M58.351 294.108 C69.62 280.061, 82.865 263.492, 99.872 241.218 M58.564 294.802 C70.03 280.91, 80.899 265.019, 100.008 240.878 M60.131 294.57 C71.371 280.617, 80.701 266.687, 102.47 239.291 M61.171 293.389 C75.063 275.354, 88.309 257.997, 103.435 240.051 M63.122 294.655 C73.781 280.121, 81.317 267.925, 106.361 238.23 M64.57 293.359 C77.819 276.128, 92.194 257.345, 105.611 240.575 M68.581 293.347 C80.34 272.86, 96.542 255.107, 106.797 238.618 M67.061 293.102 C80.009 276.4, 93.88 259.462, 107.75 239.333 M69.268 296.067 C81.476 276.934, 96.577 261.076, 112.18 238.522 M69.434 294.174 C80.125 280.894, 91.022 265.628, 110.623 239.821 M72.792 293.565 C88.741 274.359, 103.585 249.133, 114.065 238.621 M71.318 293.897 C82.655 280.193, 92.59 267.764, 112.175 239.131 M72.382 292.814 C84.009 279.937, 96.85 262.158, 115.467 239.368 M74.458 294.464 C89.102 273.121, 104.776 253.032, 115.076 241.038 M76.058 293.43 C93.084 274.39, 107.903 254.444, 116.825 239.888 M76.07 294.397 C90.165 278.313, 101.909 261.752, 117.901 239.121 M76.946 295.817 C89.591 282.661, 95.592 270.765, 118.743 240.392 M77.951 294.858 C89.97 279.8, 98.86 267.399, 120.662 239.799 M80.481 292.46 C97.296 274.747, 114.868 252.603, 124.61 239.444 M81.523 293.828 C92.103 279.817, 103.357 265.13, 123.977 240.289 M84.691 295.35 C97.469 273.783, 110.219 258.221, 124.778 240.791 M83.639 293.639 C98.383 276.138, 111.804 256.855, 126.327 240.459 M85.191 294.957 C99.755 276.667, 114.941 257.592, 126.795 239.932 M86.165 293.386 C99.949 275.979, 113.52 259.531, 128.842 239.701 M87.769 296.355 C99.778 279.602, 109.851 269.167, 128.966 239.807 M89.349 295.165 C96.878 282.794, 105.788 272.115, 130.742 240.52 M92.138 296.002 C108.026 274.124, 121.044 255.642, 133.879 240.241 M90.949 294.673 C103.609 277.679, 116.309 261.731, 133.597 240.431 M94.94 295.245 C102.615 281.407, 113.335 270.091, 134.513 241.507 M94.248 292.853 C109.808 275.615, 124.129 255.678, 135.085 239.9 M96.734 293.165 C107.512 281.482, 118.762 269.711, 138.905 240.698 M96.957 293.337 C111.119 273.448, 126.719 254.872, 138.028 239.743 M99.63 295.442 C109.87 275.795, 123.443 262.825, 139.905 239.892 M98.603 294.237 C112.804 274.53, 128.556 255.355, 141.753 240.853 M101.573 292.843 C110.88 281.45, 123.568 266.215, 143.223 238.719 M101.674 293.196 C111.839 280.644, 120.92 268.298, 143.405 240.813 M102.458 295.17 C117.71 277.384, 128.161 263.705, 147.661 240.742 M104.829 292.985 C121.494 272.837, 136.668 253.008, 145.486 239.708 M106.319 293.054 C121.174 273.015, 138.31 252.084, 148.895 240.114 M106.14 294.78 C123.718 273.467, 140.033 252.255, 149.01 240.58 M110.58 294.675 C116.748 283.328, 125.779 271.205, 150.139 238.749 M108.449 294.023 C124.888 274.709, 140.159 255.501, 150.514 239.935 M110.689 295.105 C120.752 281.562, 127.96 272.851, 153.331 238.472 M110.754 293.984 C128.08 272.313, 144.237 251.31, 154.014 239.486 M114.453 291.947 C122.813 282.249, 130.751 270.548, 156.184 239.551 M114.834 292.99 C128.217 276.93, 142.119 257.593, 155.244 239.507 M116.738 293.599 C132.927 272.924, 148.227 250.901, 160.499 241.315 M116.705 294.692 C127.297 280.073, 138.989 264.819, 158.35 238.853 M119.603 295.384 C129.634 280.227, 142.668 265.881, 161.676 241.43 M119.952 293.133 C133.353 276.058, 144.899 258.829, 159.921 239.19 M122.837 295.399 C137.838 273.423, 151.871 255.851, 162.305 238.09 M121.534 293.479 C131.944 281.928, 141.098 270.072, 163.137 240.575 M124.544 294.991 C141.274 274.662, 155.826 253.318, 167.426 239.486 M123.829 295.028 C137.143 275.995, 151.84 260.505, 165.345 241.038 M128.282 294.039 C136.529 278.668, 147.126 265.188, 166.966 238.907 M127.368 294.429 C138.651 278.354, 151.718 261.743, 169.018 240.318 M129.029 293.372 C146.375 274.489, 162.416 253.533, 170.236 240.159 M129.994 294.892 C146.42 273.692, 161.576 252.945, 170.8 239.303 M130.77 294.368 C147.553 274.79, 160.391 255.688, 172.426 238.26 M131.941 293.665 C146.088 274.894, 161.802 256.503, 174.156 238.997 M133.882 292.909 C147.65 278.193, 155.879 265.461, 176.805 238.753 M133.692 293.979 C147.251 278.04, 157.758 262.391, 176.603 240.49 M136.594 292.42 C151.906 275.683, 166.328 259.032, 178.391 240.331 M137.276 294.289 C147.346 281.2, 156.658 268.681, 178.502 239.377 M139.49 294.186 C152.975 276.415, 168.474 253.941, 180.015 240.588 M140.295 294.123 C149.665 281.072, 161.06 267.173, 180.804 241.125 M144.022 295.548 C149.5 285.455, 159.981 271.315, 179.234 244.338 M143.068 293.965 C152.523 279.506, 164.33 263.682, 179.928 243.003 M144.157 295.557 C154.42 281.288, 166.082 266.087, 180.367 247.045 M145.128 295.095 C151.246 283.321, 159.829 272.945, 180.528 246.69 M146.833 292.099 C152.816 286.667, 162.762 275, 182.159 251.558 M147.971 294.857 C153.906 285.63, 162.064 274.349, 180.819 251.311 M149.095 294.783 C156.072 283.667, 166.832 272.826, 183.052 254.167 M148.598 295.055 C156.338 285.672, 164.065 276.254, 181.095 253.871 M154.11 292.713 C160.969 280.879, 171.748 269.332, 181.232 257.912 M151.718 294.455 C163.653 278.572, 174.389 264.076, 181.398 257.587 M154.509 293.891 C165.661 281.639, 173.732 270.33, 180.902 259.006 M154.641 293.728 C161.708 284.985, 169.369 276.867, 181.009 261.113 M157.34 293.513 C162.846 286.073, 172.161 274.32, 180.479 262.919 M156.598 294.877 C165.657 282.142, 174.597 270.886, 181.786 263.085 M161.828 293.211 C166.346 282.95, 174.351 277.784, 182.673 268.745 M160.448 293.308 C165.519 286.022, 171.617 280.506, 179.99 266.601 M161.036 294.723 C169.384 283.254, 176.051 273.754, 182.075 267.984 M162.334 293.375 C168.673 285.642, 175.161 278.154, 181.597 270.413 M166.2 294.59 C172.576 287.187, 177.692 279.44, 179.506 274.45 M165.446 294.434 C169.547 287.806, 174.847 280.187, 180.798 274.073 M168.621 295.785 C170.477 290.512, 176.295 284.209, 180.494 275.045 M166.28 295.209 C172.123 287.384, 177.705 282.09, 180.573 276.59 M170.005 292.946 C174.97 289.949, 178.996 285.01, 181.553 278.799 M170.087 294.099 C174.531 289.561, 177.447 283.861, 180.244 280.002 M172.206 293.911 C175.246 289.407, 177.532 287.529, 180.742 282.125 M172.238 293.596 C173.629 292.173, 175.355 290.027, 180.698 282.88 M174.563 294.584 C176.795 290.722, 178.602 287.678, 180.25 285.707 M174.995 294.349 C177.277 290.731, 180.054 288.3, 181.018 286.159 M176.98 294.456 C177.802 293.15, 178.803 292.038, 180.394 290.079 M177.393 294.314 C178.251 293.189, 179.709 291.697, 181.023 289.298 M180.022 294.03 C180.35 293.613, 180.51 293.466, 180.543 293.132 M180.004 293.993 C180.185 293.701, 180.482 293.393, 180.663 293.146" stroke="rgb(31, 32, 32)" stroke-width="1.407" fill="none"></path><path d="M25.673 238.046 C68.827 240.677, 111.499 243.06, 179.733 239.12 M24.15 239.666 C74.752 240.609, 125.563 242.086, 179.538 239.166 M180.926 238.629 C181.455 254.935, 180.65 266.857, 178.388 292.346 M179.978 239.292 C181.516 257.694, 180.19 274.963, 180.133 294.691 M179.013 292.864 C127.271 294.763, 75.556 297.328, 23.982 293.087 M180.709 294.416 C143.176 294.615, 107.196 294.56, 25.458 294.838 M23.715 294.481 C24.372 279.713, 24.747 269.2, 24.009 238.804 M24.911 293.146 C24.596 280.955, 24.957 266.461, 24.277 240.145" stroke="rgb(204, 204, 204)" stroke-width="1" fill="none"></path></g><g style="color: rgb(204, 204, 204); font-family: "trebuchet ms", verdana, arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400;" transform="matrix(0.999998, 0, 0, 0.999998, 54.6666, 255.001)"><foreignObject height="24" width="95.64999389648438"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Middleware 2</p></span></div></foreignObject></g><g><path d="M19.898 344.164 C19.898 344.164, 19.898 344.164, 19.898 344.164 M19.898 344.164 C19.898 344.164, 19.898 344.164, 19.898 344.164 M20.186 346.644 C20.852 346.353, 21.334 345.07, 23.022 344.123 M20.008 347.088 C20.75 346.405, 21.417 345.559, 22.675 343.909 M18.944 350.197 C21.538 348.11, 22.968 346.472, 25.287 343.3 M19.415 350.857 C21.053 349.362, 22.223 347.137, 25.736 343.337 M20.663 353.716 C21.561 349.852, 24.234 348.238, 28.824 345.134 M19.762 353.195 C22.234 351.093, 24.531 348.645, 27.315 343.583 M21.244 355.06 C24.084 354.219, 24.363 348.12, 29.453 343.941 M20.23 356.586 C23.362 352.375, 26.656 349.808, 30.897 343.173 M18.355 358.128 C26.792 354.23, 27.891 348.64, 31.424 346.271 M20.626 358.796 C21.72 357.606, 25.648 353.775, 33.58 344.683 M21.871 364.388 C24.813 359.031, 24.68 355.433, 34.581 345.016 M20.073 362.626 C27.011 354.688, 33.187 347.047, 34.822 344.024 M18.613 367.472 C25.609 363.246, 29.418 357.626, 38.527 344.313 M19.973 365.727 C26.317 358.07, 33.903 348.985, 38.263 344.604 M19.268 370.235 C23.133 362.429, 30.849 357.085, 41.984 344.225 M20.373 368.8 C23.517 363.144, 28.172 358.115, 39.842 343.362 M19.373 372.59 C26.798 363.24, 32.887 359.332, 42.738 342.338 M19.436 372.538 C27.924 360.849, 38.581 351.254, 43.736 343.382 M18.76 373.617 C28.923 368.036, 36.094 357.743, 47.563 343.489 M20.122 375.669 C28.053 366.196, 35.399 356.144, 45.405 344.414 M20.214 378.009 C28.441 365.262, 39.521 353.2, 47.097 345.749 M20.528 377.937 C29.674 369.122, 37.413 357.468, 48.061 343.687 M21.676 383.624 C27.214 372.789, 32.617 363.953, 53.034 344.004 M20.619 382.736 C31.4 368.413, 41.284 354.606, 50.631 343.911 M20.797 386.258 C30.974 372.804, 39.696 362.489, 54.545 342.088 M20.005 384.211 C28.152 375.168, 36.876 364.418, 53.95 343.338 M19.078 390.096 C29.957 377.52, 37.917 367.253, 55.957 344.733 M19.841 388.459 C28.731 377.549, 36.94 366.789, 56.517 343.23 M19.588 389.509 C30.72 376.597, 41.296 364.066, 57.124 344.972 M19.367 390.216 C35.916 372.786, 50.239 355.154, 58.119 342.991 M18.645 393.172 C34.175 377.476, 48.81 363.447, 61.54 343.711 M19.566 393.99 C32.421 379.434, 43.652 364.861, 61.949 343.259 M19.889 396.358 C36.35 379.183, 50.21 357.09, 63.324 344.978 M19.174 397.232 C31.01 383.739, 41.207 370.914, 64.453 343.738 M20.6 397.4 C34.279 382.176, 46.447 370.195, 66.415 345.065 M22.027 399.137 C38.068 378.072, 55.922 358.513, 66.662 344.254 M25.497 399.474 C35.612 384.184, 46.798 368.07, 69.434 341.791 M23.124 398.617 C39.185 380.391, 56.094 360.088, 69.582 344.56 M25.171 400.288 C42.833 380.634, 59.197 358.359, 70.561 345.402 M26.561 398.903 C41.293 380.514, 55.78 362.888, 72.282 343.896 M29.808 397.862 C40.203 388.198, 48.425 376.377, 74.668 344.608 M29.738 400.099 C43.52 379.793, 58.961 361.291, 75.507 343.405 M30.821 400.839 C41.894 385.995, 52.554 376.55, 78.479 345.183 M32.077 399.184 C45.466 382.31, 60.565 364.655, 75.752 343.766 M34.288 400.546 C44.947 385.316, 56 372.362, 78.805 343.469 M34.84 398.342 C52.09 378.01, 69.446 356.233, 79.646 344.804 M38.295 400.995 C46.266 383.164, 58.252 372.115, 81.392 345.529 M35.851 398.962 C48.537 386.28, 59.204 370.66, 81.791 343.622 M39.662 399.867 C55.065 383.059, 64.365 368.274, 83.635 345.619 M39.882 399.262 C52.408 382.667, 66.859 366.562, 84.926 343.384 M43.471 400.017 C59.21 378.182, 75.994 356.884, 88.161 343.972 M41.943 398.931 C58.146 380.362, 73.759 360.149, 86.98 343.623 M43.871 400.559 C55.409 388.282, 64.942 377.71, 91.124 342.917 M43.486 400.08 C57.714 383.149, 72.624 367.354, 90.999 343.343 M47.425 400.915 C63.637 375.731, 82.281 355.597, 92.681 342.847 M47.538 399.269 C57.694 386.696, 70.194 373.535, 91.946 344.21 M49.614 400.737 C68.647 377.561, 84.325 356.249, 94.383 343.396 M50.088 399.133 C65.841 381.011, 80.772 361.591, 94.527 343.52 M53.907 399.461 C69.653 379.971, 84.529 357.363, 98.16 344.74 M51.339 398.424 C66.973 381.644, 79.83 365.413, 98.634 343.771 M54.991 398.829 C69.595 378.286, 88.833 357.873, 98.184 342.861 M54.719 399.583 C68.07 382.775, 81.776 366.333, 100.976 344.732 M59.088 399.404 C66.897 387.748, 75.739 375.428, 102.712 345.444 M57.692 397.81 C71.48 383.332, 84.722 365.431, 103.05 344.647 M59.046 399.646 C72.552 382.406, 88.492 366.784, 107.609 342.509 M60.051 399.346 C70.23 387.043, 81.536 373.084, 104.812 343.48 M61.525 398.415 C78.786 380.193, 97.217 357.354, 107.74 343.428 M62.787 399.571 C80.05 377.637, 97.295 357.218, 108.227 343.359 M65.876 400.612 C76.022 385.962, 89.407 368.801, 109.286 343.457 M65.714 399.388 C80.314 382.13, 92.813 365.832, 109.958 344.562 M68.304 398.562 C79.756 387.051, 90.946 373.646, 111.695 342.589 M68.363 399.135 C86.396 377.468, 102.881 355.127, 113.624 344.422 M69.512 396.98 C82.799 386.851, 92.004 375.808, 114.481 345.928 M70.108 399.206 C84.843 381.364, 98.266 366.286, 115.644 345.006 M71.348 400.792 C83.925 384.848, 99.175 369.349, 116.792 342.315 M73.737 398.802 C89.643 379.593, 107.633 357.732, 119.104 343.107 M76.301 397.195 C89.833 384.402, 99 370.428, 119.08 344.407 M76.286 400.114 C90.615 379.629, 107.269 359.561, 120.365 345.341 M76.793 399.741 C92.883 379.419, 109.825 361.279, 121.813 343.766 M78.864 398.949 C90.992 382.568, 104.691 367.62, 123.502 344 M78.669 397.631 C98.278 377.623, 112.372 357.029, 126.887 342.155 M81.635 399.313 C92.055 386.093, 103.019 370.654, 126.4 343.309 M84.676 399.149 C95.851 385.407, 109.17 367.033, 126.828 346.196 M83.212 399.404 C99.293 378.351, 115.34 359.747, 127.861 344.184 M87.404 400.504 C103.226 380.517, 120.153 361.178, 131.81 343.515 M85.776 398.422 C104.036 377.941, 121.64 357.328, 131.308 344.502 M86.691 400.218 C101.135 384.627, 117.249 367.587, 134.356 344.255 M89.036 398.803 C101.862 383.479, 117.199 365.924, 133.828 344.155 M91.286 398.704 C105.281 380.766, 123.352 362.508, 135.688 343.788 M91.838 398.524 C105.897 382.022, 119.18 364.78, 135.952 344.953 M95.696 400.659 C109.984 376.864, 128.434 357.065, 140.817 342.602 M94.379 398.191 C108.674 382.236, 122.301 364.612, 138.708 343.79 M96.794 399.838 C113.114 380.385, 128.046 357.275, 143.367 344.655 M95.342 398.411 C111.622 380.074, 126.453 363.437, 142.365 343.651 M97.15 398.607 C109.495 384.413, 123.106 369.796, 142.62 343.943 M100.013 398.291 C115.793 380.371, 131.017 359.025, 143.989 344.489 M103.46 398.479 C118.72 380.035, 134.182 361.176, 148.619 344.656 M102.842 398.562 C117.873 377.888, 135.704 357.853, 147.078 343.504 M105.399 399.501 C114.126 384.341, 128.915 371.795, 150.626 344.982 M104.063 399.378 C113.916 387.472, 123.827 376.251, 150.251 344.413 M105.573 400.908 C118.379 386.278, 128.54 372.485, 152.487 345.115 M105.978 398.43 C120.365 382.809, 133.733 367.527, 151.697 343.612 M108.807 398.357 C125.35 379.493, 144.173 359.103, 153.479 343.804 M110.128 399.079 C118.827 386.31, 129.118 375.54, 154.179 344.823 M113.031 400.236 C122.754 388.521, 129.903 374.125, 155.938 342.525 M111.728 399.665 C127.708 380.833, 142.946 361.989, 156.883 344.624 M113.024 400.352 C131.927 381.496, 147.904 363.047, 158.223 345.797 M115.386 398.219 C124.556 387.647, 134.349 376.669, 160.536 344.09 M115.925 399.108 C133.6 379.475, 147.813 362.444, 160.963 343.434 M117.026 399.812 C128.629 386.507, 139.455 371.558, 162.845 344.603 M118.539 400.488 C131.17 387.694, 143.149 373.107, 163.864 345.735 M119.106 398.662 C130.401 385.761, 140.23 373.258, 164.551 343.606 M124.167 397.108 C138.242 380.253, 154.983 362.08, 167.978 344.646 M122.602 398.573 C138.098 380.078, 155.732 359.523, 167.467 343.262 M123.532 400.859 C142.286 375.552, 159.053 354.884, 170.665 344.263 M125.03 400.208 C141.052 380.657, 157.153 362.142, 169.663 344.528 M127.35 400.315 C142.669 380.869, 158.835 363.581, 172.698 344.632 M127.5 399.559 C137.482 386.606, 146.933 374.808, 173.065 344.784 M129.618 397.612 C146.484 380.04, 162.953 359.873, 176.932 345.252 M130.148 398.863 C147.2 377.676, 167.022 354.447, 176.183 344.293 M130.608 397.642 C145.768 386.761, 157.974 372.167, 179.757 345.698 M131.924 400.314 C145.981 382.395, 159.507 366.317, 178.183 344.725 M135.529 398.794 C155.215 377.535, 169.622 354.168, 179.117 345.14 M135.748 398.785 C152.26 378.599, 169.384 357.773, 181.171 344.744 M136.972 400.508 C156.074 378.953, 170.983 357.643, 184.486 344.681 M137.573 399.273 C153.241 380.229, 167.971 363.346, 182.314 344.711 M141.887 399.661 C151.707 382.917, 166.376 369.125, 184.186 345.2 M140.854 399.2 C156.482 378.278, 173.838 357.283, 186.124 345.409 M142.982 398.58 C157.616 381.449, 169.424 368.416, 186.169 345.658 M142.413 399.54 C155.389 385.004, 168.167 368.972, 185.546 346.526 M145.419 398.942 C156.347 385.062, 168.036 371.23, 183.724 352.176 M145.123 398.067 C159.015 383.025, 173.654 366.942, 184.684 349.999 M147.438 401.16 C159.139 386.153, 169.34 372.972, 184.666 353.496 M147.171 399.922 C155.84 389.579, 164.135 379.543, 186.587 352.741 M150.353 399.558 C161.009 388.074, 172.236 373.469, 184.177 355.431 M151.792 398.975 C160.268 388.248, 167.825 378.112, 186.185 357.894 M152.307 397.262 C162.858 387.898, 170.13 375.51, 185.35 360.497 M153.304 398.357 C162.374 390.175, 168.479 379.912, 185.885 360.528 M157.371 399.085 C166.58 388.547, 175.57 376.594, 186.202 365.386 M154.958 398.591 C166.305 385.056, 178.824 373.112, 185.992 364.298 M160.435 397.658 C167.485 388.923, 178.728 376.01, 184.918 366.494 M157.852 399.818 C165.821 389.998, 174.077 380.791, 185.052 366.424 M161.036 400.399 C167.45 394.308, 172.834 386.968, 185.039 370.763 M161.37 400.431 C169.714 388.51, 177.822 378.059, 186.649 369.403 M163.092 397.773 C170.412 390.347, 182.056 379.578, 184.276 372.254 M162.969 399.727 C170.72 392.002, 177.257 383.021, 185.828 373.175 M166.005 397.473 C171.856 393.392, 175.365 387.535, 187.544 374.24 M166.15 399.85 C173.959 390.042, 180.544 380.705, 185.733 375.516 M167.797 401.102 C172.287 395.984, 175.489 389.609, 185.016 378.331 M169.046 400.235 C174.94 392.685, 179.2 384.983, 184.331 378.542 M172.517 397.223 C174.197 394.496, 180.726 389.563, 187.022 380.706 M170.653 399.806 C176.196 392.308, 181.338 386.306, 186.274 381.344 M175.477 398.831 C176.269 396.187, 181.967 392.059, 184.675 386.176 M173.895 398.733 C176.072 396.529, 178.647 393.201, 184.836 385.013 M177.307 398.04 C178.265 396.64, 179.805 395.193, 184.487 389.721 M176.074 398.746 C178.341 395.637, 181.689 393.498, 185.132 388.037 M179.732 398.612 C181.081 396.474, 182.798 393.959, 184.998 392.315 M179.033 399.455 C180.914 396.466, 183.311 394.797, 185.481 391.765 M182.398 398.916 C183.36 397.573, 184.697 395.801, 185.313 395.175 M182.33 398.556 C182.822 397.833, 183.42 396.962, 185.53 394.967" stroke="rgb(31, 32, 32)" stroke-width="2.108" fill="none"></path><path d="M20.954 345.548 C64.262 344.463, 109.71 342.195, 185.243 342.654 M183.776 345.39 C186.942 362.836, 183.418 382.271, 186.288 399.043 M183.215 399.107 C130.469 397.545, 80.147 397.001, 19.027 398.979 M19.041 399.081 C18.571 384.332, 19.94 367.903, 21.125 342.565" stroke="rgb(204, 204, 204)" stroke-width="1" fill="none"></path></g><g style="color: rgb(204, 204, 204); font-family: "trebuchet ms", verdana, arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400;" transform="matrix(0.999998, 0, 0, 0.999998, 50.0333, 359)"><foreignObject height="24" width="104.91667175292969"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Middleware ...</p></span></div></foreignObject></g><g><path d="M23.976 447.735 C23.976 447.735, 23.976 447.735, 23.976 447.735 M23.976 447.735 C23.976 447.735, 23.976 447.735, 23.976 447.735 M24.521 453.521 C24.264 453.076, 25.879 451.106, 28.94 447.641 M23.657 454.098 C25.499 452.542, 26.431 450.724, 28.972 447.959 M24.993 461 C25.092 456.316, 29.63 452.091, 33.424 447.367 M23.124 460.213 C27.028 456.511, 29.549 452.865, 34.324 448.446 M24.698 464.592 C29.344 457.487, 37.066 454.256, 38.865 449.165 M24.926 466.164 C30.956 458.567, 36.267 451.932, 40.426 447.12 M25.19 474.28 C31.265 466.288, 36.638 454.811, 43.939 447.478 M23.714 473.318 C31.184 463.162, 40.714 453.011, 45.397 447.243 M22.578 477.177 C30.474 470.928, 40.896 459.934, 50.923 448.409 M23.855 479.286 C28.701 472.102, 34.579 464.975, 50.523 447.933 M22.398 483.803 C34.62 472.683, 47.242 459.485, 53.476 446.753 M23.731 484.747 C34.957 471.78, 44.271 461.932, 55.442 448.465 M22.542 490.19 C34.723 476.816, 44.605 466.222, 59.175 447.122 M24.727 491.055 C37.141 474.3, 49.836 458.797, 61.726 447.287 M23.335 499.322 C30.796 485.551, 41.034 476.795, 65.231 446.719 M23.85 497.825 C35.373 483.741, 44.255 472.274, 66.521 447.983 M25.64 503.784 C37.036 490.746, 48.944 476.778, 72.337 450.152 M24.747 502.795 C37.71 487.175, 50.375 471.265, 70.917 448.978 M28.532 500.818 C45.545 483.376, 59.575 466.942, 75.721 446.689 M29.535 502.761 C45.693 482.884, 62.501 463.129, 75.584 448.274 M32.958 501.132 C53.387 482.102, 68.906 463.193, 81.813 446.518 M34.501 502.353 C44.302 491.377, 55.854 479.987, 81.782 447.331 M40.477 503.371 C54.845 485.653, 71.259 467.642, 86.405 449.166 M41.133 503.406 C57.194 482.262, 73.586 463.187, 87.34 448.409 M46.572 502.294 C57.859 487.539, 73.092 472.815, 92.327 447.847 M45.255 502.275 C59.181 485.851, 74.49 467.019, 91.768 447.871 M50.39 500.358 C61.614 489.869, 70.974 477.768, 96.834 447.074 M51.952 502.917 C63.358 488.191, 73.41 474.954, 97.742 448.998 M54.544 502.598 C76.171 481.161, 92.494 458.881, 104.384 446.943 M56.501 502.717 C74.333 481.752, 93.785 459.231, 101.957 448.522 M60.975 504.38 C74.475 487.47, 91.044 471.68, 107.765 449.867 M61.242 503.548 C75.966 485.823, 88.589 470.171, 107.463 448.424 M68.279 501.538 C76.775 489.432, 85.232 477.734, 111.434 447.12 M66.862 502.874 C81.12 484.291, 96.486 467.165, 113.01 446.938 M72.345 503.21 C85.434 488.267, 94.951 477.471, 116.47 448.14 M70.64 502.306 C87.052 484.504, 101.478 465.505, 118.7 448.124 M76.569 501.211 C88.642 489.503, 101.477 476.19, 124.454 448.773 M76.374 501.775 C91.185 484.475, 107.15 468.212, 123.551 448.553 M82.024 500.733 C95.093 486.639, 108.978 470.909, 130.54 447.591 M81.798 501.993 C100.672 482.632, 118.35 461.43, 129.077 447.736 M85.892 501.506 C103.221 487.024, 115.19 467.352, 135.053 447 M87.076 502.959 C104.44 481.92, 122.359 460.533, 134.34 448.911 M91.82 503.521 C107.287 485.536, 122.171 471.235, 140.603 446.507 M93.278 502.674 C105.733 490.048, 115.699 475.924, 138.858 447.159 M98.713 503.991 C107.823 489.963, 117.065 480.109, 144.872 449.034 M97.943 502.716 C109.241 490.909, 119.536 479.034, 144.466 448.894 M104.732 501.712 C117.028 485.152, 130.632 469.5, 149.26 446.373 M103.44 502.979 C115.996 486.39, 128.82 471.269, 150.023 446.97 M109.47 501.203 C126.459 480.704, 142.884 461.708, 157.063 449.53 M108.243 502.89 C128.067 480.349, 144.826 458.638, 155.168 448.943 M112.324 500.344 C127.413 483.768, 143.34 466.645, 159.779 449.125 M114.087 502.652 C128.45 485.527, 142.445 469.587, 159.43 448.659 M119.596 503.848 C130.098 489.53, 142.967 472.59, 165.224 447.093 M119.186 503.125 C130.451 491.073, 139.009 479.03, 166.509 448.05 M124.154 503.662 C140.86 482.973, 154.8 463.918, 172.098 449.265 M124.208 502.969 C141.87 482.739, 159.919 461.035, 171.254 448.855 M130.683 504.262 C144.104 486.694, 160.438 468.412, 177.681 446.36 M130.515 501.524 C148.974 480.26, 165.895 459.204, 176.256 448.341 M135.264 503.556 C150.989 484.373, 164.12 468.927, 180.297 448.245 M135.029 503.816 C153.743 481.17, 172.512 459.388, 180.89 448.139 M140.039 500.944 C150.697 490.927, 159.937 476.378, 179.915 452.737 M140.056 502.012 C151.097 489.585, 160.448 477.285, 180.561 454.021 M146.946 501.011 C160.001 488.682, 170.464 471.469, 180.798 459.418 M145.528 502.749 C154.175 490.543, 163.858 480.207, 181.596 459.417 M152.019 503.554 C162.788 488.702, 172.603 477.155, 183.343 464.828 M151.944 503.182 C159.227 492.972, 167.906 482.822, 181.306 467.267 M157.126 503.463 C162.83 494.007, 168.215 485.468, 181.014 471.432 M155.542 502.328 C164.613 492.497, 174.567 482.21, 181.207 473.079 M162.639 502.227 C165.097 497.727, 172.398 488.628, 179.804 479.503 M160.036 502.288 C165.652 496.446, 170.721 489.826, 180.566 479.593 M165.006 501.96 C168.468 496.46, 173.854 493.263, 180.948 484.789 M167.417 502 C171.649 496.324, 177.692 489.056, 180.813 485.115 M172.809 502.786 C173.442 499.408, 178.425 495.659, 182.015 491.185 M171.087 502.574 C173.669 499.852, 176.01 496.698, 180.883 491.393 M177.291 501.938 C178.715 501.207, 179.487 499.295, 181.011 497.405 M177.428 502.048 C178.199 500.75, 179.875 499.252, 180.929 497.97" stroke="rgb(31, 32, 32)" stroke-width="2.261" fill="none"></path><path d="M22.95 446.504 C78.552 447.373, 135.348 448.276, 179.959 447.14 M181.862 449.22 C179.383 459.072, 181.568 470.248, 182.798 502.697 M181.202 500.2 C142.954 500.055, 109.828 501.834, 25.028 502.37 M21.927 503.695 C23.217 481.798, 23.006 462.16, 23.265 447.802" stroke="rgb(204, 204, 204)" stroke-width="1" fill="none"></path></g><g style="color: rgb(204, 204, 204); font-family: "trebuchet ms", verdana, arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400;" transform="matrix(0.999998, 0, 0, 0.999998, 53.7499, 463)"><foreignObject height="24" width="97.48333740234375"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Middleware N</p></span></div></foreignObject></g><g><path d="M59.818 575.688 C59.818 575.688, 59.818 575.688, 59.818 575.688 M59.818 575.688 C59.818 575.688, 59.818 575.688, 59.818 575.688 M59.256 584.629 C62.064 580.44, 64.159 578.872, 66.2 575.905 M59.894 584.7 C61.102 581.691, 63.178 579.442, 65.161 576.521 M60.943 593.776 C63.61 585.937, 68.571 582.141, 70.638 576.133 M60.659 591.466 C62.554 588.432, 66.915 583.697, 71.895 576.951 M58.482 599.825 C67.923 591.868, 74.213 580.898, 76.628 576.614 M58.827 600.373 C64.296 594.483, 70.015 586.494, 78.798 575.902 M60.155 606.824 C67.612 595.586, 77.478 584.501, 86.122 577.804 M60.556 609.437 C70.291 595.507, 80.269 582.553, 83.994 575.549 M60.158 618.266 C69.619 603.658, 81.574 586.273, 92.169 577.613 M59.845 617.299 C67.429 606.091, 75.224 596.34, 91.573 576.785 M59.218 624.567 C71.11 608.685, 86.558 590.154, 98.695 574.862 M59.68 624.607 C74.132 604.475, 89.277 586.686, 97.8 576.035 M62.392 630.486 C76.749 610.146, 93.017 589.442, 105.746 575.41 M61.402 629.327 C71.098 618.643, 79.091 609.351, 104 576.239 M66.898 632.087 C82.94 608.416, 99.892 587.42, 109.253 577.071 M68.342 630.939 C78.199 618.261, 88.904 604.658, 111.333 575.969 M72.152 629.71 C81.723 619.597, 91.084 607.292, 115.883 573.752 M73.536 630.082 C90.842 609.468, 105.946 588.307, 116.373 576.221 M81.91 629.298 C92.609 618.033, 103.848 604.013, 121.047 578.049 M79.656 629.987 C91.179 616.56, 103.95 601.973, 122.418 576.135 M86.943 629.869 C100.662 613.625, 112.313 597.642, 131.015 574.628 M85.766 630.291 C100.477 613.436, 114.379 593.832, 128.054 577.02 M92.259 630.916 C109.587 611.628, 123.142 594.674, 136.568 578.12 M93.815 629.939 C108.636 611.983, 122.489 592.795, 135.873 575.881 M99.26 630.768 C116.988 610.573, 132.624 590.563, 142.386 574.829 M99.314 630.827 C110.777 617.548, 120.698 602.121, 142.14 576.001 M104.566 630.366 C118.476 615.352, 133.406 597.979, 147.603 578.049 M106.713 630.224 C114.474 619.28, 122.779 608.698, 145.683 578.492 M114.043 629.972 C118.76 622.24, 126.515 608.749, 147.192 588.095 M113.049 629.892 C122.424 617.745, 132.873 603.524, 145.907 588.55 M119.549 628.586 C123.591 623.328, 129.373 616.635, 144.415 593.772 M118.814 630.501 C125.045 622.493, 129.667 615.45, 146.092 595.571 M124.36 629.102 C129.586 621.922, 136.066 617.99, 145.793 605.023 M125.085 630.289 C130.282 624.719, 134.958 617.429, 144.737 603.66 M129.206 632.51 C137.527 622.379, 142.098 618.841, 143.874 610.847 M130.961 630.082 C135.742 625.076, 140.922 619.059, 145.533 612.477 M138.244 630.747 C140.273 628.718, 141.18 625.031, 144.757 620.162 M137.661 630.634 C140.244 627.753, 142.243 623.201, 144.678 620.79 M143.514 630.608 C144.211 629.926, 145.333 628.689, 145.456 628.347 M143.649 630.62 C144.403 629.89, 145.181 628.812, 145.693 628.378" stroke="rgb(31, 32, 32)" stroke-width="2.94" fill="none"></path><path d="M61.55 577.521 C76.773 576.986, 92.515 577.154, 146.072 574.173 M60.337 575.132 C89.658 576.833, 120.293 575.95, 145.916 576.074 M146.23 574.785 C146.515 595.012, 144.099 612.437, 145.399 629.33 M144.694 575.482 C144.753 597.265, 145.956 618.599, 145.538 629.727 M144.007 628.37 C125.292 629.154, 104.845 630.859, 60.548 628.688 M145.43 629.73 C123.095 630.535, 98.411 629.549, 59.042 630.896 M59.49 631.39 C57.998 616.29, 59.568 602.055, 58.095 577.416 M59.484 629.366 C59.423 617.689, 59.663 603.875, 59.375 575.077" stroke="rgb(204, 204, 204)" stroke-width="1" fill="none"></path></g><g style="color: rgb(204, 204, 204); font-family: "trebuchet ms", verdana, arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400;" transform="matrix(0.999998, 0, 0, 0.999998, 89.5749, 591)"><foreignObject height="24" width="25.833328247070312"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Vue</p></span></div></foreignObject></g><g><path d="M23.542 704.352 C23.542 704.352, 23.542 704.352, 23.542 704.352 M23.542 704.352 C23.542 704.352, 23.542 704.352, 23.542 704.352 M23.692 709.881 C25.138 708.033, 25.651 706.65, 26.609 703.907 M23.517 710.042 C24.69 709.024, 25.453 707.564, 27.296 704.444 M22.251 715.867 C24.591 712.349, 28.801 710.586, 31.834 703.871 M23.311 715.373 C25.1 713.217, 26.954 710.316, 30.663 704.273 M24.734 721.994 C25.764 717.738, 31.098 710.83, 35.321 704.81 M23.595 722.115 C25.854 717.078, 28.821 714.168, 35.114 703.078 M23.216 725.706 C27.248 721.503, 32.522 712.992, 38.346 703.505 M23.7 726.953 C29.373 718.686, 34.127 710.076, 37.372 705.285 M25.581 735.079 C29.791 724.806, 34.475 716.781, 42.13 705.171 M23.325 733.671 C30.652 723.32, 37.036 712.715, 41.942 703.987 M24.294 738.619 C31.188 730.501, 36.722 717.504, 43.306 702.802 M23.213 738.858 C31.967 724.468, 39.951 711.437, 44.379 703.751 M22.366 745.507 C32.445 730.213, 40.709 712.868, 48.004 702.184 M22.948 744.664 C29.02 734.788, 35.712 727.161, 48.145 703.274 M22.118 751.119 C31.646 737.748, 38.756 728.473, 52.32 703.245 M23.629 750.859 C30.483 740.573, 36.317 729.471, 51.624 704.07 M22.038 757.306 C34.3 741.65, 42.412 724.839, 56.925 704.837 M23.476 756.529 C32.685 740.509, 41.278 726.24, 55.317 704.598 M26.617 758.352 C37.968 744.097, 44.342 726.375, 56.86 702.248 M26.864 758.595 C36.184 741.23, 46.868 723.628, 58.651 703.779 M29.363 757.592 C38.173 746.208, 43.273 733.811, 60.368 705.376 M29.447 758.07 C36.324 747.262, 42.78 735.314, 62.866 704.228 M33.09 758.641 C39.592 747.135, 45.498 736.258, 65.359 705.428 M33.357 757.066 C42.545 742.483, 50.958 728.753, 66.135 703.249 M37.02 758.974 C45.487 746.224, 53.388 730.763, 70.556 702.711 M36.417 757.868 C48.997 737.892, 61.631 716.68, 68.462 703.387 M40.092 757.358 C50.284 737.325, 62.401 717.225, 70.792 704.003 M40.418 759.181 C51.244 738.889, 63.989 720.105, 72.441 704.141 M43.653 760.105 C53.51 740.444, 63.518 724.407, 77.64 702.64 M43.49 757.285 C51.512 744.961, 59.071 733.841, 76.243 703.149 M47.829 758.721 C53.586 748.359, 62.227 735.798, 78.926 705.16 M47.971 757.391 C55.303 743.609, 64.679 729.958, 80.49 704.307 M50.913 757.499 C61.275 740.734, 74.537 719.982, 83.498 704.04 M51.596 757.91 C62.544 738.664, 75.822 718.09, 81.718 704.348 M53.813 758.44 C67.291 739.498, 77.313 721.086, 86.194 703.549 M53.564 757.607 C67.883 737.519, 79.824 715.968, 85.719 704.684 M56.998 757.113 C66.768 743.977, 75.391 729.16, 88.149 702.724 M58.317 758.956 C65.522 745.286, 72.065 733.025, 89.508 703.822 M62.955 759.887 C74.554 737.025, 82.05 719.585, 93.172 703.007 M62.094 758.897 C73.67 737.619, 84.534 718.404, 93.409 703.056 M65.685 759.074 C75.661 738.602, 90.212 717.284, 98.648 701.828 M64.863 758.592 C75.047 739.445, 86.429 722.577, 96.594 703.225 M68.816 759.309 C76.703 746.923, 83.199 732.793, 100.207 702.333 M69.043 757.641 C77.925 742.852, 87.462 726.718, 99.815 703.9 M71.238 759.83 C77.065 747.921, 85.121 736.147, 104.263 703.499 M71.61 757.685 C85.432 737.632, 97.525 716.849, 104.249 703.762 M76.413 759.91 C83.159 746.691, 90.787 729.658, 106.205 702.484 M76.14 759.058 C83.254 744.3, 90.006 731.439, 107.014 704.113 M78.556 759.073 C88.186 741.896, 94.549 728.908, 109.747 702.833 M78.764 758.725 C89.631 741.458, 98.427 725.436, 111.431 704.012 M80.514 759.857 C90.303 747.696, 95.634 732.172, 112.466 702.346 M82.495 759.242 C91.44 741.908, 100.751 727.431, 114.103 704.904 M85.561 758.155 C92.68 747.34, 98.378 735.378, 118.464 703.952 M85.327 758.958 C98.185 736.786, 110.006 715.509, 118.095 703.289 M89.328 758.15 C98.274 737.824, 112.244 722.409, 119.419 704.049 M88.482 757.423 C102.101 737.942, 113.613 715.336, 122.217 704.042 M91.452 758.99 C105.626 736.368, 115.679 718.542, 124.701 701.854 M93.473 757.697 C100.728 745.203, 106.646 733.762, 125.495 704.67 M97.711 757.012 C108.64 739.457, 115.812 723.414, 127.401 702.382 M96.717 757.974 C105.391 741.139, 116.585 726.813, 128.459 705.103 M99.246 759.633 C108.207 745.605, 114.682 731.749, 131.537 702.764 M98.903 757.641 C107.519 745.921, 114.99 733.025, 132.567 704.707 M102.67 759.482 C110.669 745.232, 118.474 733.028, 136.581 702.373 M103.325 757.54 C109.545 746.626, 115.953 735.808, 135.519 704.115 M106.684 758.022 C113.275 747.379, 121.742 733.788, 140.309 702.874 M106.451 757.292 C113.505 745.98, 121.713 733.247, 139.748 703.914 M111.479 757.618 C121.347 741.931, 129.758 726.454, 141.515 702.817 M109.016 757.914 C118.919 744.564, 125.867 731.165, 141.539 704.164 M114.106 757.418 C123.334 744.338, 133.383 729.319, 143.645 705.42 M114.25 758.567 C119.111 746.484, 126.637 733.503, 146.068 705.196 M117.459 759.793 C126.318 740.101, 135.938 724.13, 148.149 704.926 M117.678 758.258 C124.89 746.215, 130.031 735.624, 149.288 704.831 M122 756.171 C129.009 738.566, 143.033 723.014, 151.153 704.918 M120.235 758.963 C130.345 741.777, 140.084 722.789, 152.291 704.567 M123.325 756.153 C133.937 744.487, 142.729 727.329, 156.398 701.955 M124.354 757.203 C134.19 741.87, 143.69 725.989, 155.913 703.793 M125.683 759.021 C139.138 740.095, 145.193 725.435, 157.992 704.663 M128.141 758.358 C135.145 746.23, 141.051 733.378, 158.682 703.907 M132.871 758.115 C142.306 737.577, 153.053 718.626, 164.901 705.882 M130.989 757.841 C140.374 742.738, 149.644 727.009, 162.978 704.733 M135.955 756.413 C142.423 747.98, 147.709 735.39, 168.495 704.196 M133.705 758.244 C141.561 746.424, 148.663 735.975, 166.332 703.034 M136.932 758.034 C148.901 741.414, 158.865 725.594, 170.376 705.314 M138.836 758.025 C148.551 742.092, 157.007 725.285, 170.385 703.363 M142.79 757.451 C150.951 738.013, 166.126 718.407, 171.746 705.81 M140.49 757.655 C152.189 741.779, 162.026 726.405, 173.458 705.239 M145.798 759.887 C156.587 742.044, 164.626 723.934, 177.339 705.834 M144.595 758.462 C156.744 738.537, 168.357 719.292, 177.488 704.083 M146.642 757.814 C154.494 745.575, 163.091 730.313, 182.057 703.725 M148.409 758.031 C157.718 741.666, 168.221 726.913, 180.144 703.802 M150.232 756.805 C161.033 740.582, 174.592 722.3, 180.951 708.202 M151.515 757.731 C163.593 738.732, 173.784 721.515, 182.303 708.356 M154.387 759.456 C163.264 747.735, 170.363 732.537, 183.314 712.172 M156.12 759.086 C160.476 750.327, 166.896 739.834, 182.365 714.181 M160.182 757.405 C165.766 743.175, 177.942 729.574, 179.706 719.752 M159.778 758.293 C166.293 745.532, 173.418 733.343, 181.456 720.622 M161.29 758.861 C167.62 750.163, 173.629 742.093, 182.151 725.265 M162.083 759.039 C168.727 748.333, 173.481 738.853, 182.627 725.864 M164.09 758.224 C169.612 750.253, 175.526 744.633, 181.527 731.996 M166.034 758.688 C172.493 748.594, 178.218 739.679, 182.048 731.283 M170.357 758.036 C174.485 752.806, 176.375 745.924, 182.627 736.952 M168.229 759.022 C173.428 752.614, 176.888 746.605, 181.629 738.341 M173.01 756.865 C175.168 755.952, 178.284 750.763, 182.889 743.223 M172.812 758.854 C174.817 753.511, 178.089 748.948, 180.734 743.999 M176.911 757.807 C177.768 756.474, 179.99 753.155, 180.971 750.221 M176.1 757.875 C177.185 756.044, 178.437 753.624, 181.654 749.21 M180.15 758.355 C180.17 757.648, 180.448 756.793, 181.573 755.284 M179.858 758.17 C180.31 757.415, 180.531 756.974, 181.385 755.611" stroke="rgb(31, 32, 32)" stroke-width="2.589" fill="none"></path><path d="M24.527 703.39 C65.291 705.498, 106.653 703.656, 182.741 704.389 M183.102 704.663 C181.353 714.074, 182.099 727.869, 181.338 757.277 M181.679 758.772 C143.101 759.877, 105.322 758.834, 23.594 756.398 M25.235 756.87 C23.227 742.338, 21.971 725.723, 22.439 705.427" stroke="rgb(204, 204, 204)" stroke-width="1" fill="none"></path></g><g style="color: rgb(204, 204, 204); font-family: "trebuchet ms", verdana, arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400;" transform="matrix(0.999998, 0, 0, 0.999998, 53.7499, 719)"><foreignObject height="24" width="97.48333740234375"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Middleware N</p></span></div></foreignObject></g><g><path d="M19.848 808.156 C19.848 808.156, 19.848 808.156, 19.848 808.156 M19.848 808.156 C19.848 808.156, 19.848 808.156, 19.848 808.156 M20.611 810.093 C21.855 809.071, 22.329 808.22, 23.32 807.993 M20.401 810.514 C21.441 809.431, 22.082 808.704, 23.561 807.563 M20.048 813.013 C21.348 812.113, 24.575 810.208, 25.893 808.072 M20.157 812.823 C22.623 811.615, 24.314 809.165, 26.035 808.22 M19.439 816.98 C24.361 812.596, 25.776 809.184, 29.547 808.265 M20.424 816.376 C23.981 813.485, 27.586 809.378, 29.354 808.214 M21.169 820.38 C23.258 814.566, 29.868 810.303, 33.66 806.722 M19.627 818.946 C22.561 816.313, 25.69 813.403, 33.401 807.605 M20.413 821.142 C25.223 814.701, 30.232 809.944, 35.789 807.738 M19.31 820.196 C22.515 817.213, 27.288 815.534, 35.437 807.398 M19.093 824.855 C25.875 820.397, 29.845 814.78, 38.55 809.923 M19.432 822.995 C25.238 818.709, 30.779 814.429, 38.937 809.14 M18.29 826.607 C26.537 821.317, 32.403 817.756, 40.432 806.352 M19.516 826.078 C27.511 818.675, 37.678 812.183, 42.148 807.688 M19.553 827.078 C26.484 820.25, 34.243 815.701, 42.926 806.619 M20.501 828.596 C27.834 821.646, 37.79 814.709, 44.352 807.155 M20.004 832.281 C29.138 824.708, 37.024 814.9, 49.336 809.646 M19.757 830.707 C29.805 823.12, 40.215 813.398, 49.182 807.36 M20.221 835.901 C28.779 827.923, 36.006 822.12, 52.648 807.233 M19.373 834.93 C29.507 824.95, 40.601 816.225, 52.027 808.223 M20.352 836.973 C32.694 828.881, 44.395 816.108, 55.326 806.266 M19.78 836.416 C28.712 831.105, 34.684 824.507, 53.359 809.098 M18.634 840.157 C35.017 825.737, 48.127 815.281, 59.078 808.555 M19.695 839.685 C34.9 827.986, 48.987 814.68, 56.937 808.212 M20.252 841.798 C34.193 828.199, 48.421 815.666, 59.522 809.992 M19.163 842.083 C33.168 832.119, 45.678 821.378, 60.295 809.015 M21.735 845.46 C35.572 831.41, 53.477 818.867, 63.889 807.458 M21.122 844.859 C30.066 834.713, 41.554 825.709, 62.34 808.521 M20.234 845.465 C34.954 834.09, 52.177 818.839, 67.08 806.463 M19.641 846.914 C31.89 836.986, 44.513 826.624, 65.77 808.509 M18.638 848.84 C36.037 834.758, 50.782 821.729, 68.408 808.919 M19.188 849.771 C30.903 842.065, 40.024 832.681, 69.713 808.963 M22.101 853.984 C38.316 835.67, 58.093 821.073, 71.693 808.532 M20.555 852.049 C37.303 838.022, 54.674 823.881, 73.104 808.098 M22.014 856.723 C30.035 846.593, 44.15 834.732, 74.297 807.016 M20.478 855.857 C38.88 840.157, 56.781 825.58, 75.139 807.789 M21.28 856.066 C37.594 840.436, 60.739 822.918, 80.203 806.898 M19.556 857.177 C38.904 841.14, 59.119 825.237, 79.65 808.695 M20.432 861.705 C37.37 843.954, 58.659 829.964, 82.652 808.206 M19.719 860.839 C36.395 846.431, 53.986 832.458, 82.491 807.354 M21.301 862.292 C38.021 845.758, 57.439 831.503, 83.346 809.891 M21.041 862.829 C43.195 842.932, 66.177 823.81, 86.11 808.135 M22.509 863.2 C39.383 846.474, 59.81 832.379, 85.986 806.32 M24.719 862.697 C41.656 846.103, 59.06 831.593, 87.4 807.567 M26.329 862.231 C49.642 843.373, 71.65 825.96, 89.809 806.181 M26.693 861.837 C46.618 846.133, 66.526 829.746, 92.168 807.038 M31.48 861.066 C45.92 850.44, 60.59 837.667, 93.095 808.658 M30.431 862.131 C45.638 848.835, 61.882 836.217, 93.464 808.636 M34.603 860.496 C59.861 840.536, 82.519 820.4, 97.449 808.634 M33.97 862.152 C52.034 845.509, 70.765 830.993, 96.436 808.293 M36.262 863.781 C62.513 842.372, 84.823 820.57, 99.758 808.879 M35.753 862.107 C59.134 843.712, 81.15 823.473, 100.3 808.745 M39.796 861.979 C55.183 849.809, 69.287 838.454, 104.914 808.367 M39.089 862.241 C65.033 841.673, 88.659 820.542, 103.322 807.567 M44.18 863.327 C58.411 848.163, 73.174 836.304, 107.831 808.096 M42.348 862.906 C57.796 847.693, 74.942 835.512, 107.634 806.804 M43.373 864.605 C66.591 845.445, 87.762 825.33, 109.499 807.416 M45.559 862.22 C66.985 843.212, 90.484 824.577, 109.045 808.954 M48.183 861.737 C63.401 850.797, 80.954 837.922, 112.614 806.453 M48.812 862.264 C62.161 851.656, 75.081 840.065, 112.899 807.456 M52.117 860.692 C76.745 838.883, 100.768 819.661, 114.553 806.65 M51.026 863.109 C73.495 845.043, 94.29 828.06, 116.775 806.961 M53.108 864.456 C67.934 851.27, 82.975 837.014, 120.084 808.851 M53.327 863.234 C71.378 848.272, 88.07 835.108, 119.727 807.854 M55.922 861.338 C80.985 843.568, 102.262 824.762, 123.687 808.481 M56.891 863.389 C71.002 852.091, 85.524 840.61, 122.497 808.575 M61.357 864.367 C81.095 842.447, 105.461 825.123, 126.78 809.607 M61.248 862.888 C74.727 850.298, 90.667 837.524, 125.713 808.728 M65.164 863.709 C85.054 846.74, 103.741 829.672, 126.955 808.851 M65.174 861.798 C81.23 847.644, 97.641 833.774, 128.78 807.593 M68.93 863.407 C87.401 848.047, 105.904 832.01, 130.07 806.587 M67.127 861.97 C91.917 840.24, 117.746 820.123, 131.663 807.737 M70.73 861.478 C93.389 843.142, 118.792 821.819, 136.8 807.078 M70.47 862.874 C89.113 846.546, 108.244 831.353, 135.679 808.019 M74.515 860.372 C92.619 843.337, 114.11 828.189, 138.93 806.638 M73.335 863.124 C96.947 842.142, 122.705 822.469, 137.73 809.121 M76.913 862.586 C100.481 841.975, 122.588 824.366, 142.464 807.902 M76.08 863.377 C92.575 849.109, 109.291 834.249, 141.12 806.905 M80.843 863.689 C94.887 850.24, 110.088 836.604, 145.161 808.014 M78.726 862.582 C104.152 842.18, 130.357 819.74, 143.219 808.335 M82.281 864.089 C100.007 847.271, 114.021 833.884, 145.91 808.984 M83.347 862.678 C96.882 850.007, 113.574 836.19, 147.781 808.673 M85.97 862.885 C106.099 843.29, 125.409 828.98, 150.18 808.757 M84.908 863.533 C104.684 846.941, 123.014 831.31, 149.475 808.217 M88.268 861.754 C113.861 843.481, 136.223 820.361, 155.053 808.458 M89.044 861.616 C107.679 848.073, 124.619 831.399, 152.825 808.849 M93.212 862.095 C109.32 848.982, 123.648 834.818, 157.554 806.544 M92.563 861.552 C116.95 842.086, 143.221 819.628, 157.462 808.527 M95.549 863.463 C115.29 843.218, 134.375 828.025, 158.475 807.335 M94.288 861.812 C118.81 841.838, 143.538 822.402, 160.507 808.311 M99.139 860.771 C118.686 843.389, 137.723 826.989, 163.53 808.544 M97.544 861.843 C114.396 848.182, 129.634 835.483, 162.438 807.617 M102.974 860.684 C115.483 847.415, 132.513 835.458, 167.607 806.791 M100.732 862.29 C120.806 846.405, 142.482 829.084, 165.815 807.73 M104.732 864.29 C122.646 848.998, 138.466 833.852, 169.909 808.689 M103.602 863.271 C127.891 842.965, 153.713 822.615, 169.294 807.317 M108.537 864.418 C133.004 840.845, 157.064 820.063, 169.975 808.597 M107.519 862.981 C122.484 849.505, 135.581 838.066, 172.194 808.356 M109.684 863.202 C128.017 846.596, 149.163 830.634, 175.933 806.044 M109.643 862.4 C128.571 846.582, 147.233 831.653, 174.525 808.394 M115.24 863.084 C132.727 844.227, 152.408 828.956, 180.102 807.113 M113.755 861.527 C132.217 846.306, 151.069 829.973, 177.131 809.064 M116.342 862.423 C134.404 848.281, 154.868 830.741, 180.529 808.674 M117.307 861.677 C129.89 852.159, 142.875 840.918, 180.84 808.042 M121.271 862.793 C133.101 851.267, 147.853 835.784, 182.856 806.149 M118.877 862.925 C136.541 848.038, 155.05 832.686, 183.921 807.993 M124.885 863.325 C145.629 846.506, 166.067 828.626, 183.427 809.419 M122.593 862.933 C136.264 851.264, 149.385 840.95, 184.896 810.951 M125.346 862.511 C147.771 843.109, 165.77 829.394, 184.524 813.588 M125.127 861.991 C144.401 846.915, 164.567 830.357, 185.601 811.792 M127.499 863.891 C139.74 852.61, 156.284 841.276, 183.477 814.942 M128.16 863.063 C144.817 849.004, 162.311 835.366, 186.218 815.212 M134.05 860.781 C151.746 846.005, 168.239 830.206, 185.786 819.074 M132.276 862.399 C146.867 851.415, 160.652 838.276, 184.095 817.548 M135.447 860.849 C152.286 848.001, 166.358 837.878, 185.248 819.374 M134.831 861.874 C146.068 854.983, 155.298 845.237, 186.593 819.924 M137.006 861.209 C156.527 846.844, 170.804 833.971, 187.109 823.272 M138.43 862.722 C149.365 852.476, 159.591 843.416, 185.163 822.454 M141.545 861.294 C156.486 850.653, 172.137 838.405, 183.363 826.936 M140.574 863.069 C151.911 853.669, 162.657 844.242, 184.923 825.502 M143.998 862.258 C153.655 853.834, 163.497 845.942, 184.562 827.638 M143.527 862.192 C155.039 853.855, 165.976 844.847, 186.329 826.934 M147.421 861.017 C158.052 855.562, 169.429 844.137, 185.313 831.621 M147.531 863.077 C158.313 851.963, 170.956 843.81, 185.125 830.245 M151.605 864.149 C160.501 853.207, 173.01 843.079, 184.794 831.902 M151.704 862.214 C158.598 855.851, 167.521 848.583, 184.866 833.953 M155.22 861.818 C163.353 853.941, 176.47 841.505, 183.838 837.197 M152.863 861.838 C160.114 857.644, 168.27 850.912, 186.023 836.096 M156.08 862.546 C168.774 852.14, 175.916 846.926, 184.933 839.808 M156.572 862.39 C168.063 852.765, 177.437 843.719, 186.362 839.201 M158.94 864.419 C170.301 854.318, 177.813 847.891, 185.107 842.738 M160.614 863.01 C166.934 857.216, 172.067 851.149, 184.385 842.242 M163.076 863.181 C167.316 856.023, 175.346 852.186, 183.765 842.058 M163.531 863.169 C169.317 857.481, 174.746 851.857, 185.613 843.166 M166.359 863.081 C171.563 856.464, 178.343 854.653, 183.831 847.731 M165.676 863.167 C174.273 856.474, 181.354 849.382, 186.205 846.375 M170.145 862.646 C171.936 858.64, 179.032 853.974, 186.089 849.188 M169.773 861.847 C175.263 857.397, 180.439 852.387, 184.446 849.852 M174.179 863.662 C178.811 858.66, 183.342 854.573, 184.824 851.708 M172.704 861.555 C177.299 859.733, 179.677 855.205, 185.626 851.853 M174.656 862.758 C177.693 860.685, 180.249 860.016, 184.779 853.385 M175.057 862.529 C178.278 860.008, 181.442 857.99, 185.462 854.054 M178.293 862.232 C180.771 860.326, 184.143 858.326, 186.139 857.415 M178.795 863.192 C179.873 861.091, 181.333 859.998, 185.061 856.974 M182.003 862.65 C182.858 861.589, 184.601 860.277, 185.214 859.836 M181.94 862.198 C182.655 861.717, 183.231 861.062, 185.132 859.814" stroke="rgb(31, 32, 32)" stroke-width="2.989" fill="none"></path><path d="M19.555 809.77 C59.088 808.385, 101.213 807.112, 182.999 809.798 M183.256 806.621 C186.955 824.736, 183.671 844.382, 184.953 860.194 M183.878 861.511 C143.54 862.856, 105.928 862.413, 20.321 862.09 M19.541 861.451 C18.62 839.788, 21.476 820.083, 20.275 807.65" stroke="rgb(204, 204, 204)" stroke-width="1" fill="none"></path></g><g style="color: rgb(204, 204, 204); font-family: "trebuchet ms", verdana, arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400;" transform="matrix(0.999998, 0, 0, 0.999998, 50.0333, 823)"><foreignObject height="24" width="104.91667175292969"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Middleware ...</p></span></div></foreignObject></g><g><path d="M24.763 911.838 C24.763 911.838, 24.763 911.838, 24.763 911.838 M24.763 911.838 C24.763 911.838, 24.763 911.838, 24.763 911.838 M24.997 918.195 C25.45 915.776, 27.026 915.132, 27.775 912.221 M24.852 917.632 C25.685 916.329, 26.219 914.836, 28.23 911.476 M24.347 923.396 C25.691 921.394, 27.243 918.183, 32.154 912.863 M24.579 923.285 C26.482 919.709, 29.453 916.567, 31.613 912.952 M24.396 930.481 C26.727 924.317, 29.955 918.893, 34.746 912.896 M24.506 929.05 C28.787 922.901, 33.163 914.815, 34.392 912.133 M24.496 934.051 C28.308 929.163, 34.269 921.635, 39.327 913.209 M24.347 934.767 C29.421 927.281, 33.77 919.865, 38.207 912.685 M26.408 942.198 C29.439 932.973, 31.163 929.244, 42.587 913.056 M25.334 941.53 C30.692 930.805, 36.769 921.161, 42.634 912.07 M25.541 945.771 C34.201 933.704, 41.42 921.409, 43.986 911.061 M25.369 945.961 C30.046 938.906, 34.108 930.78, 46.444 910.927 M24.917 952.49 C30.707 943.816, 37.212 933.194, 49.012 914.258 M25.527 953.325 C33.826 937.474, 44.576 921.555, 49.874 911.514 M23.853 959.738 C34.871 943.251, 44.145 926.331, 53.094 912.504 M25.787 958.82 C33.566 944.253, 41.571 930.083, 51.886 911.139 M24.752 965.164 C36.212 944.44, 47.67 927.861, 54.835 911.002 M25.575 965.122 C32.086 952.508, 38.919 941.28, 55.437 912.375 M26.199 966.522 C34.994 954.241, 40.732 943.035, 60.788 910.59 M26.068 967.985 C37.811 947.754, 49.922 929.375, 59.576 910.916 M30.944 966.842 C40.708 948.706, 48.433 932.02, 65.136 911.303 M30.943 967.67 C39.082 950.255, 49.084 934.283, 62.776 911.737 M31.651 965.323 C44.267 948.638, 52.713 933.964, 66.169 913.693 M33.162 966.27 C46.887 946.189, 58.561 926.352, 66.337 912.267 M35.86 966.241 C50.066 947.548, 62.119 926.372, 71.181 911.273 M36.57 967.308 C47.976 949.059, 59.231 932.025, 69.619 912.217 M39.536 968.231 C53.34 945.79, 66.424 923.386, 72.31 912.148 M40.162 966.358 C53.676 945.482, 64.005 924.907, 74.407 911.562 M44.069 967.686 C52.864 953.696, 61.99 936.974, 78.18 911.299 M43.614 968.396 C52.867 952.264, 62.89 937.213, 77.57 911.24 M47.567 966.024 C61.59 946.873, 73.031 925.346, 80.521 910.995 M48.264 966.928 C59.435 948.215, 70.978 928.8, 79.639 912.305 M53.052 968.616 C64.515 946.342, 78.639 924.324, 84.255 913.205 M51.794 966.931 C58.569 956.199, 65.698 942.858, 84.488 911.365 M55.933 968.562 C62.931 951.113, 74.915 934.954, 89.25 912.043 M53.948 967.057 C63.552 954.557, 71.767 939.995, 87.817 912.223 M59.569 966.595 C71.092 945.24, 80.314 926.158, 92.955 910.459 M57.405 965.979 C68.157 949.457, 79.189 932.502, 90.944 912.682 M61.103 969.166 C71.366 945.507, 86.665 928.474, 96.356 912.345 M62.127 967.489 C72.267 949.609, 82.987 930.255, 94.752 912.201 M65.091 965.723 C72.394 955.283, 77.051 944.616, 99.336 914.258 M64.288 966.685 C72.47 954.781, 80.03 942.266, 96.975 912.042 M69.68 968.969 C80.071 948.083, 90.018 930.896, 103.155 911.237 M67.825 967.587 C81.117 947.451, 93.182 926.086, 101.318 911.801 M72.248 967.574 C82.506 950.4, 92.21 932.512, 104.455 913.469 M73.16 967.257 C83.957 948.004, 93.077 929.204, 104.377 911.139 M74.031 967.119 C89.266 945.74, 100.57 926.407, 108.362 913.124 M76.083 966.21 C87.859 944.839, 100.297 923.174, 107.778 910.951 M79.801 967.023 C87.219 955.805, 91.705 940.873, 110.911 910.696 M79.637 967.939 C91 948.222, 101.649 930.105, 111.413 911.334 M81.205 969.025 C88.24 956.185, 97.752 944.365, 115.487 911.242 M82.795 966.558 C93.752 946.958, 106.368 925.97, 114.755 913.163 M86.936 968.134 C95.854 948.698, 107.696 933.246, 117.906 913.801 M86.447 966.308 C93.24 956.572, 99.807 945.104, 118.656 912.484 M91.445 966.243 C100.423 946.533, 111.423 931.399, 120.991 913.026 M90.562 966.384 C100.662 946.65, 112.758 928.291, 121.742 912.442 M92.628 968.187 C104.034 947.33, 114.954 930.441, 126.185 912.932 M94.099 967.586 C105.913 945.921, 118.03 924.788, 125.631 912.343 M97.359 969.115 C105.228 953.222, 114.233 933.883, 130.563 913.589 M95.497 968.448 C103.538 953.891, 112.878 941.197, 130.111 910.811 M99.793 965.688 C113.4 949.818, 121.3 930.852, 133.126 912.743 M100.594 966.731 C109.336 952.276, 118.033 936.986, 132.149 911.523 M101.83 965.397 C116.331 948.811, 127.593 926.514, 135.602 912.483 M102.587 966.658 C112.047 955.255, 118.895 942.473, 136.006 912.461 M105.944 968.39 C122.196 943.379, 134.708 921.996, 140.046 913.417 M106.44 967.465 C113.693 955.867, 120.571 943.178, 140.04 910.996 M110.733 967.437 C117.402 954.714, 125.394 941.18, 143.566 913.511 M110.37 968.324 C122.808 947.531, 134.238 926.271, 144.591 911.954 M114.622 968.039 C128.684 947.532, 137.738 925.133, 146.654 911.949 M112.941 967.274 C126.336 948.688, 137.717 929.37, 146.267 911.544 M117.234 968.051 C127.39 954.732, 134.169 940.346, 151.328 912.309 M117.979 966.576 C127.301 952.126, 135.328 936.044, 151.292 913.056 M119.161 967.911 C125.815 955.453, 136.246 943.543, 155.328 913.204 M121.219 967.704 C134.201 944.838, 147.441 924.093, 154.184 911.365 M126.102 967.157 C136.058 945.985, 148.113 926.827, 155.589 911.972 M125.188 966.934 C137.889 944.427, 150.425 922.728, 157.514 912.128 M128.947 966.876 C138.288 948.909, 148.35 933.063, 161.758 910.867 M128.58 968.461 C139.3 948.574, 150.767 929.413, 160.814 912.401 M131.37 967.235 C141.526 951.68, 148.94 936.002, 162.876 911.858 M130.538 967.61 C142.022 948.9, 153.663 929.425, 163.79 911.786 M133.626 966.276 C141.24 954.329, 148.346 942.387, 167.479 913.693 M134.871 968.017 C144.911 949.274, 156.344 931.729, 167.721 913.251 M137.532 967.758 C150.226 947.299, 166.219 925.597, 172.725 912.17 M138.495 966.452 C147.362 952.637, 155.61 936.754, 172.334 911.856 M142.407 965.652 C156.395 944.08, 166.69 924.71, 176.774 912.35 M143.069 966.673 C154.796 946.754, 166.686 924.608, 174.99 911.117 M145.65 967.054 C155.816 951.336, 166.665 933.342, 177.596 913.637 M145.037 967.281 C156.073 950.074, 165.199 933.141, 179.356 910.753 M147.998 969.24 C156.944 955.846, 161.662 944.643, 182.41 911.704 M149.66 967.796 C158.633 952.15, 169.436 935.223, 182.318 912.099 M152.179 968.45 C159.15 957.778, 164.397 944.884, 183.173 919.296 M153.238 967.656 C163.348 948.841, 172.363 931.635, 181.535 919.195 M155.175 965.904 C164.161 952.505, 172.584 939.386, 183.033 925.767 M155.575 967.923 C163.726 955.243, 171.052 941.564, 182.454 924.637 M160.499 964.956 C164.632 957.48, 172.042 944.622, 182.744 931.626 M159.82 965.985 C165.437 958.353, 170.063 948.271, 181.887 930.745 M162.951 965.881 C169.368 956.231, 177.462 944.824, 181.92 933.801 M162.347 967.899 C168.866 956.199, 176.555 944.031, 181.926 935.38 M167.691 965.634 C172.67 957.951, 176.577 949.485, 181.111 939.612 M166.702 966.732 C172.087 957.149, 178.058 946.508, 181.386 941.25 M169.758 968.172 C175.522 960.899, 176.856 952.553, 182.581 945.986 M169.866 966.473 C172.919 959.834, 177.593 953.582, 181.624 946.583 M174.273 966.351 C176.893 961.722, 178.938 958.988, 182.38 952.627 M172.787 966.485 C175.782 964.507, 176.947 961.093, 181.385 953.024 M177.998 967.342 C179.377 963.864, 179.456 962.34, 182.084 958.675 M177 966.611 C178.883 964.263, 180.255 961.282, 181.948 958.657" stroke="rgb(31, 32, 32)" stroke-width="2.412" fill="none"></path><path d="M24.15 911.098 C74.205 912.319, 123.156 912.736, 180.909 913.956 M179.826 910.821 C181.803 921.304, 179.198 932.669, 182.043 964.371 M178.977 964.116 C140.493 966.129, 103.85 966.863, 23.917 967.98 M26.654 967.11 C24.469 946.126, 23.533 930.144, 24.473 911.53" stroke="rgb(204, 204, 204)" stroke-width="1" fill="none"></path></g><g style="color: rgb(204, 204, 204); font-family: "trebuchet ms", verdana, arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400;" transform="matrix(0.999998, 0, 0, 0.999998, 54.6666, 926.999)"><foreignObject height="24" width="95.64999389648438"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Middleware 2</p></span></div></foreignObject></g><g><path d="M24.512 1016.224 C24.512 1016.224, 24.512 1016.224, 24.512 1016.224 M24.512 1016.224 C24.512 1016.224, 24.512 1016.224, 24.512 1016.224 M25.607 1025.608 C25.923 1022.461, 29.695 1018.77, 31.278 1015.052 M24.606 1025.167 C27.283 1021.389, 29.785 1018.271, 30.504 1016.307 M24.477 1032.618 C29.909 1025.316, 34.873 1019.144, 36.366 1014.17 M24.864 1033.365 C28.033 1027.88, 32.339 1022.427, 37.607 1015.412 M24.245 1042.141 C28.68 1038.015, 31.802 1028.95, 43.547 1017.128 M24.374 1043.01 C31.24 1033.459, 35.805 1025.48, 43.498 1014.917 M26.106 1052.176 C34.477 1035.939, 45.895 1022.238, 50.08 1017.495 M25.097 1050.666 C32.415 1040.549, 40.357 1029.824, 48.455 1015.737 M26.225 1058.114 C32.355 1048.369, 39.959 1037.086, 54.675 1016.21 M23.954 1060.732 C35.18 1044.381, 45.788 1027.344, 55.678 1016.835 M24.985 1070.42 C35.634 1051.48, 46.309 1036.258, 60.683 1014.141 M25.523 1069.386 C34.121 1054.892, 43.951 1040.126, 60.562 1015.471 M29.728 1071.681 C40.146 1056.203, 49.384 1041.039, 64.996 1015.094 M29.157 1069.724 C41.333 1054.422, 53.077 1036.963, 67.357 1015.401 M35.983 1072.011 C43.806 1056.893, 51.991 1046.009, 72.363 1015.004 M36.786 1069.615 C44.657 1056.998, 55.077 1042.36, 73.75 1016.788 M41.165 1072.337 C51.465 1057.13, 63.554 1040.568, 78.097 1016.663 M41.856 1070.683 C52.215 1057.734, 61.901 1042.388, 79.214 1015.649 M47.322 1069.957 C58.344 1059.164, 67.686 1044.653, 83.726 1015.956 M47.895 1069.537 C57.039 1057.718, 66.747 1045.168, 85.566 1016.006 M53.296 1071.915 C65.286 1055.777, 75.481 1036.524, 89.487 1014.211 M53.572 1070.821 C66.417 1051.009, 80.334 1031.48, 90.591 1016.929 M58.846 1068.673 C71.274 1052.54, 83.683 1033.558, 96.85 1017.059 M60.609 1070.483 C74.246 1048.787, 89.349 1027.445, 98.5 1016.53 M66.3 1069.938 C77.583 1055.115, 91.036 1035.93, 102.13 1017.78 M66.781 1070.145 C78.551 1053.032, 92.039 1033.249, 104.125 1016.686 M74.482 1068.505 C85.629 1050.531, 94.471 1036.035, 108.924 1017.972 M71.886 1069.64 C81.49 1058.132, 87.942 1045.302, 108.547 1016.506 M78.509 1068.794 C88.594 1055.573, 99.903 1042.586, 117.56 1017.851 M77.347 1071.258 C90.006 1052.468, 101.201 1034.968, 116.125 1015.679 M85.341 1069.257 C96.192 1051.84, 111.056 1033.648, 121.903 1017.358 M85.04 1070.468 C98.491 1049.463, 112.733 1027.735, 122.413 1015.86 M88.833 1069.647 C105.327 1050.061, 119.763 1029.221, 128.347 1017.188 M90.897 1069.526 C97.538 1058.984, 106.526 1046.783, 128.083 1015.037 M95.01 1069.118 C106.497 1055.635, 118.754 1041.267, 135.441 1014.745 M96.296 1071.132 C108.437 1053.176, 120.517 1034.562, 134.205 1015.762 M101.863 1068.265 C110.098 1057.409, 120.278 1043.618, 138.849 1015.912 M103.037 1069.435 C116.634 1050.967, 129.609 1032.717, 139.765 1016.244 M110.242 1071.271 C117.714 1055.599, 129.865 1039.639, 146.957 1016.719 M109.25 1069.877 C115.602 1058.504, 124.743 1046.713, 145.285 1016.387 M115.044 1070.857 C128.009 1051.984, 139.866 1033.815, 153.339 1014.441 M115.022 1069.439 C127.125 1052.763, 140.631 1033.323, 153.065 1016.126 M118.852 1071.992 C131.241 1053.737, 141.116 1039.219, 159.366 1016.295 M120.427 1069.708 C128.496 1057.624, 136.439 1045.061, 158.226 1016.306 M129.032 1070.387 C140.613 1050.176, 150.747 1033.612, 164.394 1017.576 M127.042 1070.255 C137.823 1053.875, 148.918 1037.48, 163.238 1016.521 M133.732 1071.491 C141.731 1059.25, 150.363 1043.513, 169.651 1017.746 M133.026 1070.53 C147.576 1049.497, 160.898 1029.121, 170.177 1016.489 M140.228 1070.225 C151.89 1052.518, 163.463 1036.882, 176.183 1018.11 M139.492 1069.383 C149.897 1055.155, 160.543 1039.653, 175.671 1015.979 M145.787 1069.103 C156.667 1056.101, 165.681 1042.032, 180.327 1019.927 M146.012 1069.593 C156.818 1051.684, 169.923 1035.584, 181.722 1017.891 M152.824 1071.471 C160.095 1057.881, 167.578 1046.022, 179.808 1025.551 M151.587 1070.402 C162.54 1054.386, 171.548 1038.482, 180.232 1026.47 M158.478 1069.668 C162.461 1060.181, 171.843 1049.323, 181.019 1035.026 M157.514 1069.214 C166.458 1059.501, 174.073 1046.577, 180.323 1034.908 M163.137 1070.053 C167.915 1063.787, 173.035 1056.826, 179.628 1045.32 M162.967 1069.812 C167.701 1062.694, 174.363 1054.956, 181.786 1044.745 M170.839 1069.766 C173.294 1065.925, 176.458 1060.573, 181.161 1054 M169.868 1070.724 C173.071 1064.636, 175.132 1060.423, 181.335 1053.723 M175.462 1070.779 C177.745 1068.426, 179.617 1064.569, 181.616 1061.986 M175.134 1070.517 C176.955 1068.374, 179.056 1065.27, 180.834 1062.246" stroke="rgb(31, 32, 32)" stroke-width="1.719" fill="none"></path><path d="M26.417 1016.606 C83.524 1015.805, 144.903 1013.084, 180.419 1017.286 M23.763 1015.558 C79.153 1015.868, 132.102 1015.419, 179.799 1016.21 M181.988 1016.946 C180.04 1033.095, 178.688 1049.146, 179.164 1070.603 M180.947 1015.1 C179.887 1031.707, 180.796 1046.754, 180.314 1070.166 M179.969 1071.77 C132.787 1068.602, 89.054 1069.668, 22.995 1069.675 M180.636 1070.791 C133.346 1069.83, 85.905 1069.759, 25.617 1069.358 M24.978 1070.803 C24.432 1058.87, 25.384 1048.172, 24.544 1016.175 M24.003 1070.536 C25.179 1052.253, 25.639 1032.874, 24.356 1015.003" stroke="rgb(204, 204, 204)" stroke-width="1" fill="none"></path></g><g style="color: rgb(204, 204, 204); font-family: "trebuchet ms", verdana, arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400;" transform="matrix(0.999998, 0, 0, 0.999998, 54.6666, 1031)"><foreignObject height="24" width="95.64999389648438"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Middleware 1</p></span></div></foreignObject></g><g><path d="M7.983 1144.029 C7.983 1144.029, 7.983 1144.029, 7.983 1144.029 M7.983 1144.029 C7.983 1144.029, 7.983 1144.029, 7.983 1144.029 M7.706 1149.179 C8.897 1149.045, 9.494 1147.262, 11.972 1143.563 M7.929 1149.72 C8.991 1148.033, 10.495 1145.866, 11.855 1144.082 M9.007 1155.664 C11.392 1150.267, 14.612 1146.71, 15.27 1142.729 M7.42 1155.759 C9.972 1152.589, 11.468 1149.805, 15.859 1144.171 M6.566 1160.106 C9.442 1157.867, 12.944 1152.636, 19.638 1144.915 M8.816 1161.993 C10.586 1156.651, 14.011 1151.78, 17.321 1143.727 M6.491 1168.227 C13.727 1160.692, 17.171 1151.297, 22.264 1144.104 M7.204 1167.169 C11.068 1161.742, 14.551 1155.247, 22.551 1144.632 M8.314 1173.287 C12.581 1168.102, 16.545 1158.475, 26.416 1143.127 M7.644 1173.175 C12.62 1163.826, 17.53 1155.415, 25.275 1144.619 M6.422 1177.958 C12.725 1170.486, 16.574 1163.911, 27.605 1143.979 M8.904 1178.048 C15.609 1167.591, 22.65 1155.413, 29.066 1143.341 M9.557 1187.141 C18.721 1170.199, 27.628 1155.855, 30.93 1143.712 M6.857 1185.245 C14.173 1175.993, 19.196 1166.766, 32.981 1144.319 M6.773 1191.891 C12.705 1182.262, 22.282 1169.406, 36.289 1142.868 M7.883 1191.607 C15.943 1176.702, 26.673 1160.026, 35.263 1144.819 M6.303 1196.956 C15.965 1184.031, 23.307 1172.446, 40.322 1144.546 M8.598 1197.59 C15.149 1181.921, 23.898 1167.64, 38.77 1143.815 M9.552 1197.704 C21.582 1179.612, 34.03 1160.948, 43.717 1145.696 M10.846 1198.475 C18.674 1184.067, 25.747 1172.172, 42.481 1143.423 M14.222 1199.108 C23.137 1183.699, 30.305 1169.33, 46.584 1144.809 M14.11 1198.098 C21.82 1183.554, 31.756 1170.863, 45.855 1142.875 M17.863 1196.792 C27.326 1181.522, 38.793 1162.917, 50.482 1142.597 M18.127 1198.219 C25.911 1182.719, 36.311 1166.464, 49.361 1144.303 M20.207 1198.162 C31.463 1177.702, 42.818 1159.043, 51.802 1145.467 M21.988 1197.895 C34.155 1176.601, 47.32 1156.316, 53.791 1143.843 M24.4 1198.01 C31.125 1186.821, 39.195 1173.979, 57.445 1142.831 M25.428 1197.775 C32.482 1184.681, 39.326 1172.359, 55.912 1143.486 M26.888 1196.692 C37.081 1184.51, 45.802 1170.45, 59.708 1145.15 M27.615 1197.059 C37.308 1183.069, 46.001 1168.34, 60.607 1143.731 M30.956 1196.644 C44.18 1179.194, 55.032 1159.484, 63.989 1144.262 M32.041 1198.775 C41.357 1181.198, 52.787 1163.118, 63.311 1144.496 M33.87 1198.424 C45.808 1178.01, 59.572 1160.467, 68.238 1145.777 M34.892 1199.065 C42.631 1183.931, 51.522 1170.251, 67.557 1144.981 M38.172 1199.11 C47.401 1181.453, 57.139 1166.135, 70.749 1143.293 M38.745 1198.382 C47.71 1180.153, 58.964 1163.272, 70.756 1144.415 M41.316 1196.419 C51.752 1182.314, 60.45 1166.878, 73.1 1145.238 M41.711 1198.243 C49.66 1184.46, 58.948 1170.029, 74.299 1143.551 M45.709 1198.669 C51.873 1186.542, 59.361 1174.898, 77.175 1143.62 M45.857 1199.063 C53.014 1186.801, 58.288 1175.946, 77.105 1144.758 M50.956 1196.671 C59.145 1179.954, 70.959 1156.62, 80.735 1144.027 M49.833 1198.097 C59.155 1181.313, 69.175 1162.611, 81.762 1144.087 M54.246 1200.145 C64.255 1177.816, 75.108 1158.901, 86.127 1142.575 M51.83 1199.004 C61.071 1181.645, 71.973 1165.669, 84.44 1143.735 M54.872 1196.36 C68.781 1179.523, 76.389 1160.663, 88.209 1141.958 M55.908 1198.461 C65.62 1182.517, 76.052 1165.578, 88.152 1142.819 M58.778 1198.242 C71.149 1178.062, 81.552 1158.297, 90.906 1143.118 M60.079 1198.3 C66.521 1185.018, 74.009 1173.765, 92.756 1143.929 M62.051 1198.595 C75.742 1180.012, 84.91 1162.198, 95.953 1143.806 M62.823 1198.216 C74.641 1179.441, 85.556 1159.839, 95.299 1145.136 M67.422 1198.345 C78.753 1178.398, 90.203 1155.364, 99.758 1143.249 M67.271 1198.208 C74.825 1184.989, 81.65 1172.54, 97.689 1144.581 M70.451 1197.38 C77.072 1182.053, 87.518 1170.916, 101.394 1143.517 M69.961 1198.415 C76.49 1185.983, 83.621 1175.059, 103.099 1144.316 M72.978 1197.435 C81.35 1186.119, 88.399 1174.288, 104.754 1143.512 M73.142 1198.87 C84.447 1181.327, 93.437 1162.667, 106.265 1143.846 M78.345 1198.349 C89.54 1179.137, 102.097 1159.156, 107.734 1143.374 M76.021 1199.329 C84.212 1186.744, 91.444 1173.685, 109.504 1143.693 M81.496 1196.81 C85.826 1184.315, 96.97 1173.778, 111.766 1143.432 M80.354 1198.194 C88.192 1185.903, 95.976 1174.399, 111.597 1144.798 M85.954 1199.37 C93.443 1183.814, 99.334 1171.498, 114.572 1142.718 M84.721 1197.816 C92.18 1184.837, 99.219 1171.1, 115.357 1144.813 M89.564 1199.229 C92.582 1185.053, 100.227 1175.8, 117.892 1142.553 M88.089 1198.904 C99.426 1177.354, 112.88 1156.897, 119.57 1143.98 M89.05 1198.345 C103.327 1176.401, 113.996 1156.559, 122.844 1142.149 M91.333 1199.249 C98.905 1185.509, 105.286 1172.093, 123.7 1143.603 M95.856 1196.489 C103.914 1179.2, 115.116 1160.838, 124.912 1144.895 M94.024 1198.116 C102.322 1185.265, 108.758 1173.568, 126.899 1144.232 M99.838 1200.046 C106.483 1184.145, 112.937 1171.997, 131.663 1142.295 M96.952 1197.338 C111.26 1177.805, 122.56 1157.983, 129.651 1144.693 M101.909 1197.831 C111.623 1179.929, 123.235 1162.457, 134.467 1142.92 M101.527 1197.588 C112.566 1178.442, 123.66 1159.711, 134.161 1143.985 M103.577 1199.039 C114.573 1183.464, 120.719 1170.001, 138.581 1145.368 M105.366 1197.24 C112.442 1183.374, 121.888 1168.638, 136.716 1144.174 M109.144 1200.494 C118.231 1181.3, 126.683 1166.773, 138.817 1142.718 M107.811 1198.089 C119.916 1177.439, 131.708 1157.554, 140.586 1144.796 M110.971 1197.617 C123.579 1181.551, 132.688 1162.633, 144.443 1143.298 M110.782 1197.368 C117.373 1186.592, 124.349 1175.49, 144.355 1143.513 M114.792 1196.275 C125.138 1180.697, 137.766 1162.562, 147.172 1145.504 M114.583 1198.118 C127.499 1178.67, 139.454 1158.403, 146.435 1144.102 M119.742 1197.157 C130.095 1177.745, 142.59 1157.732, 151.869 1143.059 M119.933 1197.315 C125.817 1184.401, 135.095 1170.947, 150.753 1144.603 M121.262 1199.971 C132.774 1182.408, 144.193 1164.11, 155.592 1145.861 M122.211 1199.233 C129.254 1187.781, 136.345 1174.421, 154.966 1143.632 M127.339 1200.24 C134.288 1182.913, 140.875 1172.681, 156.395 1142.894 M126.405 1198.424 C133.646 1183.825, 142.932 1166.219, 157.649 1144.419 M130.518 1196.754 C138.466 1180.558, 150.56 1159.759, 162.346 1144.465 M128.557 1198.705 C141.757 1179.298, 151.965 1159.297, 160.659 1144.647 M133.461 1197.178 C141.031 1181.751, 151.944 1165.819, 163.092 1142.18 M133.166 1198.081 C142.583 1182.024, 151.865 1164.231, 165.286 1142.96 M137.769 1198.906 C147.202 1179.837, 155.131 1166.242, 168.853 1144.275 M136.923 1197.562 C150.22 1177.968, 161.75 1155.108, 168.11 1143.597 M140.764 1198.998 C149.999 1181.43, 160.752 1162.569, 171.559 1145.694 M140.495 1198.84 C152.143 1178.418, 163.939 1157.295, 170.813 1144.787 M142.361 1196.927 C153.632 1180.193, 163.55 1161.162, 176.332 1142.452 M142.829 1198.969 C151.853 1185.241, 158.239 1171.421, 174.506 1144.635 M147.905 1196.746 C156.88 1182.845, 162.25 1170.877, 178.017 1142.424 M145.814 1198.115 C158.013 1179.573, 169.87 1159.688, 179.083 1143.98 M149.139 1196.142 C158.025 1183.307, 163.932 1173.481, 180.84 1143.137 M151.236 1197.223 C156.978 1186.642, 163.174 1175.19, 183.275 1144.231 M154.357 1196.956 C163.214 1183.526, 169.772 1168.577, 184.187 1145.473 M153.919 1199.185 C164.648 1181.223, 174.652 1161.467, 186.102 1143.748 M155.97 1198.048 C164.438 1182.931, 172.167 1170.506, 188.597 1144.715 M156.422 1197.963 C166.391 1180.586, 177.216 1162.906, 189.607 1143.799 M160.729 1199.397 C165.564 1186.997, 172.63 1174.325, 192.846 1142.801 M161.116 1198.218 C169.589 1183.772, 175.809 1171.021, 193.106 1143.8 M163.489 1196.064 C173.149 1186.956, 180.752 1172.334, 196.347 1145.725 M164.044 1197.188 C171.843 1186.744, 177.774 1174.898, 195.57 1142.994 M166.602 1197.565 C173.357 1186.57, 185.382 1171.389, 195.881 1149.106 M166.79 1199.058 C175.01 1184.555, 184.27 1170.784, 197.766 1147.495 M169.691 1200.33 C179.651 1179.321, 190.374 1163.54, 199.175 1153.583 M170.26 1198.702 C180.097 1183.209, 187.544 1169.718, 197.242 1153.745 M174.804 1198.661 C181.035 1186.807, 189.645 1176.675, 197.699 1159.486 M173.842 1199.232 C179.126 1188.477, 185.95 1179.778, 196.953 1159.586 M177.714 1199.567 C186.796 1187.777, 192.007 1175.804, 198.285 1166.395 M178.118 1198.192 C185.147 1185.417, 193.922 1172.39, 198.308 1165.671 M180.393 1199.408 C185.451 1189.266, 190.802 1178.736, 198.963 1171.94 M180.778 1197.236 C186.416 1187.419, 192.849 1179.169, 196.85 1171.641 M183.001 1197.004 C187.863 1191.726, 192.707 1184.361, 195.454 1177.702 M185.433 1198.176 C190.092 1190.983, 194.92 1183.134, 196.124 1178.176 M189.562 1197.554 C191.941 1195.099, 194.515 1191.305, 198.024 1183.886 M188.874 1197.747 C191.454 1193.998, 194.26 1188.682, 197.719 1183.266 M191.632 1197.48 C194.066 1195.401, 194.474 1193.75, 198.014 1188.945 M192.375 1198.602 C193.819 1195.15, 195.821 1191.777, 197.536 1189.29 M195.799 1197.819 C196.387 1197.086, 196.651 1195.656, 197.385 1195.163 M195.692 1197.864 C196.078 1197.168, 196.274 1196.766, 197.222 1195.287" stroke="rgb(31, 32, 32)" stroke-width="1.319" fill="none"></path><path d="M7.74 1142.188 C48.329 1145.823, 88.731 1145.682, 198.719 1143.649 M8.315 1143.033 C77.923 1144.784, 145.86 1145.143, 196.212 1143.2 M196.359 1145.09 C195.588 1167.294, 195.317 1187.217, 196.491 1197.208 M197.424 1144.154 C197.881 1155.021, 196.349 1166.244, 197.831 1197.112 M198.135 1197.06 C150.128 1197.6, 108.242 1198.004, 6.919 1196.334 M197.66 1197.548 C137.299 1198.4, 75.25 1198.202, 8.94 1198.744 M9.127 1198.6 C8.218 1179.584, 10.373 1160.381, 6.693 1144.193 M7.923 1197.062 C8.291 1181.894, 8.967 1163.565, 8.966 1143.868" stroke="rgb(204, 204, 204)" stroke-width="1" fill="none"></path></g><g style="color: rgb(204, 204, 204); font-family: "trebuchet ms", verdana, arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400;" transform="matrix(0.999998, 0, 0, 0.999998, 37.9999, 1159)"><foreignObject height="24" width="128.98333740234375"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Réponse au Client</p></span></div></foreignObject></g></svg> \ No newline at end of file diff --git a/documentation/assets/images/views-middleware-diagram.png b/documentation/assets/images/views-middleware-diagram.png new file mode 100644 index 0000000..b190b13 Binary files /dev/null and b/documentation/assets/images/views-middleware-diagram.png differ diff --git a/documentation/assets/images/views-middleware-flow.png b/documentation/assets/images/views-middleware-flow.png new file mode 100644 index 0000000..d35c011 Binary files /dev/null and b/documentation/assets/images/views-middleware-flow.png differ diff --git a/documentation/assets/images/views-urlpatterns-arguments.png b/documentation/assets/images/views-urlpatterns-arguments.png new file mode 100644 index 0000000..e8b9b8f Binary files /dev/null and b/documentation/assets/images/views-urlpatterns-arguments.png differ diff --git a/documentation/assets/images/views-urlpatterns-base.png b/documentation/assets/images/views-urlpatterns-base.png new file mode 100644 index 0000000..283cfba Binary files /dev/null and b/documentation/assets/images/views-urlpatterns-base.png differ diff --git a/file-watchers.xml b/file-watchers.xml new file mode 100644 index 0000000..30bb158 --- /dev/null +++ b/file-watchers.xml @@ -0,0 +1,82 @@ +<TaskOptions> + <TaskOptions> + <option name="arguments" value="--highlight-style=/home/steve/Code/dependencies/reveal/pandoc/monokaipdf.theme -N -V fontsize:12pt -V geometry:margin=2cm -V mainfont:"Museo Sans" -V colorlinks:true -o slides/office/$FileNameWithoutExtension$.docx -t docx $FileName$" /> + <option name="checkSyntaxErrors" value="true" /> + <option name="description" /> + <option name="exitCodeBehavior" value="ERROR" /> + <option name="fileExtension" value="md" /> + <option name="immediateSync" value="false" /> + <option name="name" value="Markdown → Office" /> + <option name="output" value="" /> + <option name="outputFilters"> + <array /> + </option> + <option name="outputFromStdout" value="false" /> + <option name="program" value="pandoc" /> + <option name="runOnExternalChanges" value="true" /> + <option name="scopeName" value="Documentation" /> + <option name="trackOnlyRoot" value="true" /> + <option name="workingDir" value="$FileDir$" /> + <envs /> + </TaskOptions> + <TaskOptions> + <option name="arguments" value="-t revealjs -s -o slides/html/$FileNameWithoutExtension$.html $FileName$ --standalone --embed-resources --highlight-style /home/steve/Code/dependencies/reveal/pandoc/monokai.theme --columns=120 -V revealjs-url=/home/steve/Code/dependencies/reveal/ -V theme=training -V slidenum=true -V hash=true -V width=1920 -V height=900 -V slideNumber:true -V previewLinks:true" /> + <option name="checkSyntaxErrors" value="true" /> + <option name="description" /> + <option name="exitCodeBehavior" value="ERROR" /> + <option name="fileExtension" value="md" /> + <option name="immediateSync" value="false" /> + <option name="name" value="Markdown → HTML" /> + <option name="output" value="" /> + <option name="outputFilters"> + <array /> + </option> + <option name="outputFromStdout" value="false" /> + <option name="program" value="pandoc" /> + <option name="runOnExternalChanges" value="true" /> + <option name="scopeName" value="Documentation" /> + <option name="trackOnlyRoot" value="true" /> + <option name="workingDir" value="$FileDir$" /> + <envs /> + </TaskOptions> + <TaskOptions> + <option name="arguments" value="-r "$ProjectName$ Slides.zip" html pdf office" /> + <option name="checkSyntaxErrors" value="true" /> + <option name="description" /> + <option name="exitCodeBehavior" value="ERROR" /> + <option name="fileExtension" value="*" /> + <option name="immediateSync" value="false" /> + <option name="name" value="Slides → ZIP" /> + <option name="output" value="$ProjectFileDir$/documentation/slides/" /> + <option name="outputFilters"> + <array /> + </option> + <option name="outputFromStdout" value="false" /> + <option name="program" value="zip" /> + <option name="runOnExternalChanges" value="true" /> + <option name="scopeName" value="Slides" /> + <option name="trackOnlyRoot" value="true" /> + <option name="workingDir" value="$ProjectFileDir$/documentation/slides/" /> + <envs /> + </TaskOptions> + <TaskOptions> + <option name="arguments" value="--highlight-style=/home/steve/Code/dependencies/reveal/pandoc/monokaipdf.theme --toc --pdf-engine=lualatex -N -V fontsize:12pt -V geometry:margin=2cm -V colorlinks:true -V mainfont:Cabin -V monofont:"JetBrains Mono" -o slides/pdf/$FileNameWithoutExtension$.pdf $FileName$" /> + <option name="checkSyntaxErrors" value="true" /> + <option name="description" /> + <option name="exitCodeBehavior" value="ERROR" /> + <option name="fileExtension" value="md" /> + <option name="immediateSync" value="false" /> + <option name="name" value="Markdown → PDF" /> + <option name="output" value="" /> + <option name="outputFilters"> + <array /> + </option> + <option name="outputFromStdout" value="false" /> + <option name="program" value="pandoc" /> + <option name="runOnExternalChanges" value="true" /> + <option name="scopeName" value="Documentation" /> + <option name="trackOnlyRoot" value="true" /> + <option name="workingDir" value="$FileDir$" /> + <envs /> + </TaskOptions> +</TaskOptions> \ No newline at end of file diff --git a/logo.svg b/logo.svg new file mode 100644 index 0000000..71d2a6d --- /dev/null +++ b/logo.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="256px" height="256px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"> + <g> + <rect fill="#092E20" x="0" y="0" width="256" height="256" rx="28"></rect> + <path d="M186.377012,94.197636 L186.377012,160.424478 C186.377012,183.243286 184.707519,194.187745 179.699037,203.649406 C175.060358,212.741266 168.937684,218.490548 156.323731,224.798721 L129.794937,212.183571 C142.410087,206.247593 148.531564,201.05481 152.427049,193.074749 C156.509231,184.91278 157.808923,175.451119 157.808923,150.593015 L157.808923,94.197636 L186.377012,94.197636 Z M140.928486,50.0787207 L140.928486,182.316986 C126.272844,185.099476 115.512688,186.212472 103.826231,186.212472 C68.9487718,186.212472 50.7686431,170.445031 50.7686431,140.205054 C50.7686431,111.079269 70.0629644,92.1583404 99.9295492,92.1583404 C104.567032,92.1583404 108.091519,92.5281423 112.359199,93.6411381 L112.359199,50.0787207 L140.928486,50.0787207 Z M102.713236,115.160254 C88.243093,115.160254 79.8944275,124.065418 79.8944275,139.647359 C79.8944275,154.860696 87.8720944,163.208164 102.527736,163.208164 C105.680028,163.208164 108.278215,163.022665 112.359199,162.467364 L112.359199,116.643052 C109.020212,115.531253 106.237722,115.160254 102.713236,115.160254 Z M186.377012,50.2307105 L186.377012,79.5419941 L157.808923,79.5419941 L157.808923,50.2307105 L186.377012,50.2307105 Z" fill="#FFFFFD"></path> + </g> +</svg> \ No newline at end of file diff --git a/source/README.md b/source/README.md new file mode 100644 index 0000000..5a10f33 --- /dev/null +++ b/source/README.md @@ -0,0 +1,31 @@ +# Projets de démo Django + +Chacun des répertoires ci-dessous peut être récupéré comme projet Django, à savoir : + +- `advanced` : fonctions avancées (URL téléchargement) +- `authentication` : bouts de code pour gérer les utilisateurs +- `forms` : test des formulaires +- `orm` : projet d'interface utilisant des données en base +- `templating` : utilisation du langage de templates +- `translation` : traduction + +Les répertoires contiennent un fichier `manage.py` et peuvent être lancés avec la commande habituelle : + +```bash +python manage.py runserver +``` + +Vérifiez quand même les fichiers `urls.py` pour savoir quelles URLs sont disponibles pour chaque projet (l'un d'entre eux ne répond pas à l'URL http://127.0.0.1/ mais répond à d'autres URLs). + +--- + +Les répertoires de projet peuvent être ouverts avec PyCharm (contiennent un répertoire `.idea`). Vous devrez peut-être créer un environnement virtuel pour ces projets. Pour cela : + +- rendez-vous dans le menu `File` → `Settings` +- dans la catégorie `Project` → `Python Interpreter`, vérifiez que l'interpréteur est dans un répertoire relatif à votre projet, ou créez un nouvel interpréteur `virtualenv`. + +Avant de pouvoir exécuter le projet, vous devez installer les dépendances présentes dans requirements.txt : + +```{.bash .numberLines} +pip install -r requirements.txt +``` diff --git a/source/__init__.py b/source/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/advanced/.idea/Advanced.iml b/source/advanced/.idea/Advanced.iml new file mode 100644 index 0000000..881c382 --- /dev/null +++ b/source/advanced/.idea/Advanced.iml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="PYTHON_MODULE" version="4"> + <component name="FacetManager"> + <facet type="django" name="Django"> + <configuration> + <option name="rootFolder" value="$MODULE_DIR$" /> + <option name="settingsModule" value="advanced/settings.py" /> + <option name="manageScript" value="manage.py" /> + <option name="environment" value="<map/>" /> + <option name="doNotUseTestRunner" value="false" /> + <option name="trackFilePattern" value="" /> + </configuration> + </facet> + </component> + <component name="NewModuleRootManager"> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$" isTestSource="false" /> + <excludeFolder url="file://$MODULE_DIR$/.idea" /> + <excludeFolder url="file://$MODULE_DIR$/jupyter/.ipynb_checkpoints" /> + </content> + <orderEntry type="jdk" jdkName="Python 3 (training)" jdkType="Python SDK" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> + <component name="PyDocumentationSettings"> + <option name="format" value="GOOGLE" /> + <option name="myDocStringFormat" value="Google" /> + </component> + <component name="TemplatesService"> + <option name="TEMPLATE_CONFIGURATION" value="Django" /> + </component> +</module> \ No newline at end of file diff --git a/source/advanced/.idea/inspectionProfiles/Project_Default.xml b/source/advanced/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..09dbac8 --- /dev/null +++ b/source/advanced/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,12 @@ +<component name="InspectionProjectProfileManager"> + <profile version="1.0"> + <option name="myName" value="Project Default" /> + <inspection_tool class="GrazieInspection" enabled="false" level="TYPO" enabled_by_default="false" /> + <inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" /> + <inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false"> + <option name="processCode" value="true" /> + <option name="processLiterals" value="true" /> + <option name="processComments" value="true" /> + </inspection_tool> + </profile> +</component> \ No newline at end of file diff --git a/source/advanced/.idea/inspectionProfiles/profiles_settings.xml b/source/advanced/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/source/advanced/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ +<component name="InspectionProjectProfileManager"> + <settings> + <option name="USE_PROJECT_PROFILE" value="false" /> + <version value="1.0" /> + </settings> +</component> \ No newline at end of file diff --git a/source/advanced/.idea/modules.xml b/source/advanced/.idea/modules.xml new file mode 100644 index 0000000..2f3a95e --- /dev/null +++ b/source/advanced/.idea/modules.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/.idea/advanced.iml" filepath="$PROJECT_DIR$/.idea/advanced.iml" /> + </modules> + </component> +</project> \ No newline at end of file diff --git a/source/advanced/.idea/workspace.xml b/source/advanced/.idea/workspace.xml new file mode 100644 index 0000000..055225f --- /dev/null +++ b/source/advanced/.idea/workspace.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectViewState"> + <option name="hideEmptyMiddlePackages" value="true" /> + <option name="showExcludedFiles" value="false" /> + <option name="showLibraryContents" value="true" /> + </component> + <component name="PropertiesComponent"> + <property name="settings.editor.selected.configurable" value="configurable.group.tools" /> + </component> +</project> \ No newline at end of file diff --git a/source/advanced/advanced/__init__.py b/source/advanced/advanced/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/advanced/advanced/asgi.py b/source/advanced/advanced/asgi.py new file mode 100644 index 0000000..9365178 --- /dev/null +++ b/source/advanced/advanced/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for advanced 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', 'advanced.settings') + +application = get_asgi_application() diff --git a/source/advanced/advanced/settings.py b/source/advanced/advanced/settings.py new file mode 100644 index 0000000..60466a6 --- /dev/null +++ b/source/advanced/advanced/settings.py @@ -0,0 +1,139 @@ +""" +Django settings for advanced project. + +Generated by 'django-admin startproject' using Django 3.2. + +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/ +""" + +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/3.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-4f#(qt450m(73!m#q%6jhl*t@0_%xn4$)ing$)-2qey-^bv*xy" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django_extensions", + "various", +] + +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", + "django.middleware.locale.LocaleMiddleware", +] + +ROOT_URLCONF = "advanced.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 = "advanced.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "database.sqlite3", + } +} + + +# 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 = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = "/static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# Email configuration + +# Set a backend in console for demonstration purposes. +# The correct backend should be SMTP in production. +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + +# Media configuration + +MEDIA_URL = "/media/" +MEDIA_ROOT = BASE_DIR / "media" diff --git a/source/advanced/advanced/urls.py b/source/advanced/advanced/urls.py new file mode 100644 index 0000000..8294b1e --- /dev/null +++ b/source/advanced/advanced/urls.py @@ -0,0 +1,26 @@ +"""advanced 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.conf import settings +from django.conf.urls.static import static +from django.contrib import admin +from django.urls import path + +from various.views import view_file_download + +urlpatterns = [ + path("admin/", admin.site.urls), + path("download", view_file_download, name="download"), +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/source/advanced/advanced/wsgi.py b/source/advanced/advanced/wsgi.py new file mode 100644 index 0000000..0120b6a --- /dev/null +++ b/source/advanced/advanced/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for advanced 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', 'advanced.settings') + +application = get_wsgi_application() diff --git a/source/advanced/database.sqlite3 b/source/advanced/database.sqlite3 new file mode 100644 index 0000000..4062c10 Binary files /dev/null and b/source/advanced/database.sqlite3 differ diff --git a/source/advanced/jupyter/Various examples.ipynb b/source/advanced/jupyter/Various examples.ipynb new file mode 100644 index 0000000..d58f3f0 --- /dev/null +++ b/source/advanced/jupyter/Various examples.ipynb @@ -0,0 +1,81 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "id": "fc76bc88", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Content-Type: text/plain; charset=\"utf-8\"\n", + "MIME-Version: 1.0\n", + "Content-Transfer-Encoding: 7bit\n", + "Subject: Title\n", + "From: noreply@example.aa\n", + "To: noreply@example.aa\n", + "Date: Fri, 16 Apr 2021 22:44:20 -0000\n", + "Message-ID: <161861306067.29777.1833249302107691814@manjaro>\n", + "\n", + "Body of the email.\n", + "-------------------------------------------------------------------------------\n" + ] + }, + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Simple example to send a text email\n", + "from django.core.mail import send_mail\n", + "send_mail(\"Title\", \"Body of the email.\", \"noreply@example.aa\", [\"noreply@example.aa\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "badac700", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Maggle\n" + ] + } + ], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Django Shell-Plus", + "language": "python", + "name": "django_extensions" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/source/advanced/manage.py b/source/advanced/manage.py new file mode 100644 index 0000000..5afd7d1 --- /dev/null +++ b/source/advanced/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', 'advanced.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/source/advanced/media/django-upload.jpg b/source/advanced/media/django-upload.jpg new file mode 100644 index 0000000..7143b79 Binary files /dev/null and b/source/advanced/media/django-upload.jpg differ diff --git a/source/advanced/various/__init__.py b/source/advanced/various/__init__.py new file mode 100644 index 0000000..17a41f9 --- /dev/null +++ b/source/advanced/various/__init__.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class VariousConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "various" + + +default_app_config = "various.VariousConfig" diff --git a/source/advanced/various/admin/__init__.py b/source/advanced/various/admin/__init__.py new file mode 100644 index 0000000..2c0b532 --- /dev/null +++ b/source/advanced/various/admin/__init__.py @@ -0,0 +1 @@ +from .room import * diff --git a/source/advanced/various/admin/room.py b/source/advanced/various/admin/room.py new file mode 100644 index 0000000..e62a11f --- /dev/null +++ b/source/advanced/various/admin/room.py @@ -0,0 +1,51 @@ +from django.contrib import admin +from django.db import models +from django.db.models import F +from django.http import HttpRequest +from django.utils.translation import gettext_lazy as _ + +from various.models import Room + + +@admin.register(Room) +class RoomAdmin(admin.ModelAdmin): + """ + Admin configuration for rooms. + + """ + + list_display = ["id", "name", "length", "width", "height", "get_volume_display"] + list_editable = ["name", "length", "width", "height"] + actions = ["action_fix_minimum"] + + def get_queryset(self, request): + """ + Change queryset to add a computed field for volume. + + Args: + request: HTTP + + """ + return super().get_queryset(request).annotate(volume=F("width") * F("length") * F("height")) + + def get_volume_display(self, obj: Room) -> str: + return f"{obj.get_volume()} cm³" + + get_volume_display.short_description = _("area") + get_volume_display.admin_order_field = "volume" + + def action_fix_minimum(self, request: HttpRequest, queryset: models.QuerySet): + """ + Change room dimensions to have at least 1cm in every axis. + + Args: + request: HTTP request. + queryset: selected rooms. + + """ + for room in queryset: # type: Room + room.width = max(1, room.width) + room.length = max(1, room.length) + room.height = max(1, room.height) + room.save() + self.message_user(request, _("The selected rooms have been updated.")) diff --git a/source/advanced/various/locale/fr/LC_MESSAGES/django.po b/source/advanced/various/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 0000000..4ec48a4 --- /dev/null +++ b/source/advanced/various/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,59 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-04-18 11:30+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +#: various/admin/room.py:34 +msgid "area" +msgstr "aire" + +#: various/admin/room.py:51 +msgid "The selected rooms have been updated." +msgstr "Les salles sélectionnées ont été mises à jour." + +#: various/models/room.py:11 +msgid "name" +msgstr "nom" + +#: various/models/room.py:12 +msgid "description" +msgstr "description" + +#: various/models/room.py:13 various/models/room.py:14 +#: various/models/room.py:15 +msgid "centimeters" +msgstr "centimètres" + +#: various/models/room.py:13 +msgid "width" +msgstr "largeur" + +#: various/models/room.py:14 +msgid "length" +msgstr "longueur" + +#: various/models/room.py:15 +msgid "height" +msgstr "hauteur" + +#: various/models/room.py:18 +msgid "room" +msgstr "salle" + +#: various/models/room.py:19 +msgid "rooms" +msgstr "salles" diff --git a/source/advanced/various/migrations/0001_initial.py b/source/advanced/various/migrations/0001_initial.py new file mode 100644 index 0000000..d424785 --- /dev/null +++ b/source/advanced/various/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2 on 2021-04-18 11:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Room', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=32, unique=True, verbose_name='name')), + ('description', models.TextField(blank=True, verbose_name='description')), + ('width', models.PositiveIntegerField(default=0, help_text='centimeters', verbose_name='width')), + ('length', models.PositiveIntegerField(default=0, help_text='centimeters', verbose_name='length')), + ('height', models.PositiveIntegerField(default=0, help_text='centimeters', verbose_name='height')), + ], + options={ + 'verbose_name': 'room', + 'verbose_name_plural': 'rooms', + }, + ), + ] diff --git a/source/advanced/various/migrations/__init__.py b/source/advanced/various/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/advanced/various/models/__init__.py b/source/advanced/various/models/__init__.py new file mode 100644 index 0000000..2c0b532 --- /dev/null +++ b/source/advanced/various/models/__init__.py @@ -0,0 +1 @@ +from .room import * diff --git a/source/advanced/various/models/room.py b/source/advanced/various/models/room.py new file mode 100644 index 0000000..5680fc5 --- /dev/null +++ b/source/advanced/various/models/room.py @@ -0,0 +1,49 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +class Room(models.Model): + """ + Room definition. + + """ + + name = models.CharField(max_length=32, blank=False, unique=True, verbose_name=_("name")) + description = models.TextField(blank=True, verbose_name=_("description")) + width = models.PositiveIntegerField(default=0, help_text=_("centimeters"), verbose_name=_("width")) + length = models.PositiveIntegerField(default=0, help_text=_("centimeters"), verbose_name=_("length")) + height = models.PositiveIntegerField(default=0, help_text=_("centimeters"), verbose_name=_("height")) + + class Meta: + verbose_name = _("room") + verbose_name_plural = _("rooms") + + def get_area(self) -> int: + """ + Get the area of the room. + + Returns: + Area of the room in square centimeters. + + """ + return self.width * self.length + + def get_volume(self) -> int: + """ + Get the volume of the room. + + Returns: + Volume of the room in cube centimeters. + + """ + return self.width * self.length * self.height + + def is_empty(self) -> bool: + """ + Tell if the room is empty (has no volume). + + Returns: + Whether the room has no volume (zero). + + """ + return self.width * self.length * self.height == 0 diff --git a/source/advanced/various/tests/__init__.py b/source/advanced/various/tests/__init__.py new file mode 100644 index 0000000..2c0b532 --- /dev/null +++ b/source/advanced/various/tests/__init__.py @@ -0,0 +1 @@ +from .room import * diff --git a/source/advanced/various/tests/room.py b/source/advanced/various/tests/room.py new file mode 100644 index 0000000..5f2e463 --- /dev/null +++ b/source/advanced/various/tests/room.py @@ -0,0 +1,55 @@ +from django import test + +from various.models import Room + + +class RoomTestCase(test.TestCase): + """ + Basic test case for rooms. + + This method is executed before every `test_` function. + To run those automatic tests, in a terminal, just run + + `./manage.py test` + + """ + + def setUp(self) -> None: + self.room1 = Room(name="Kitchen", width=260, length=320, height=250) # basic + self.room2 = Room(name="Fake", width=0, length=320, height=250) # a 2D object + + @classmethod + def setUpClass(cls): + """Cette méthode est exécutée une seule fois avant ous les tests.""" + pass + + @classmethod + def tearDownClass(cls): + pass + + def tearDown(self) -> None: + """ + End unit test. + + Is executed after every `test_` method. + + """ + + def test_base_room(self): + """ + Basic test using the fixture set up in the `setUp` method. + + """ + self.assertEqual(self.room2.get_volume(), 0) + self.assertEqual(self.room1.get_area(), 83200) + + def test_dummy_page(self): + """ + Test the Django test client. + + Used to test that pages of the projet answer properly. + + """ + client = test.Client() + response = client.get("/admin/") + self.assertNotEqual(response.status_code, 404) diff --git a/source/advanced/various/views.py b/source/advanced/various/views.py new file mode 100644 index 0000000..aff6c19 --- /dev/null +++ b/source/advanced/various/views.py @@ -0,0 +1,21 @@ +from django.core.files.storage import DefaultStorage +from django.http import HttpRequest, HttpResponse + + +def view_file_download(request: HttpRequest) -> HttpResponse: + """ + Serve a media file like a download. + + Args: + request: HTTP request. + + Returns: + Media file as an attachment to download. + + """ + storage = DefaultStorage() # Objet capable de manipuler des fichiers média + with storage.open("django-upload.jpg", "rb") as file: # relative to MEDIA_ROOT + response = HttpResponse(file, content_type="image/jpeg") + # Use list notation to set headers + response["Content-Disposition"] = "attachment; filename=django-upload.jpg" + return response diff --git a/source/authentication/.idea/Authentication.iml b/source/authentication/.idea/Authentication.iml new file mode 100644 index 0000000..cd30fe3 --- /dev/null +++ b/source/authentication/.idea/Authentication.iml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="PYTHON_MODULE" version="4"> + <component name="FacetManager"> + <facet type="django" name="Django"> + <configuration> + <option name="rootFolder" value="$MODULE_DIR$" /> + <option name="settingsModule" value="authentication/settings.py" /> + <option name="manageScript" value="manage.py" /> + <option name="environment" value="<map/>" /> + <option name="doNotUseTestRunner" value="false" /> + <option name="trackFilePattern" value="" /> + </configuration> + </facet> + </component> + <component name="NewModuleRootManager"> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$" isTestSource="false" /> + <excludeFolder url="file://$MODULE_DIR$/.idea" /> + </content> + <orderEntry type="jdk" jdkName="Python 3 (training)" jdkType="Python SDK" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> + <component name="PyDocumentationSettings"> + <option name="format" value="GOOGLE" /> + <option name="myDocStringFormat" value="Google" /> + </component> + <component name="TemplatesService"> + <option name="TEMPLATE_CONFIGURATION" value="Django" /> + </component> +</module> \ No newline at end of file diff --git a/source/authentication/.idea/inspectionProfiles/Project_Default.xml b/source/authentication/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..09dbac8 --- /dev/null +++ b/source/authentication/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,12 @@ +<component name="InspectionProjectProfileManager"> + <profile version="1.0"> + <option name="myName" value="Project Default" /> + <inspection_tool class="GrazieInspection" enabled="false" level="TYPO" enabled_by_default="false" /> + <inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" /> + <inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false"> + <option name="processCode" value="true" /> + <option name="processLiterals" value="true" /> + <option name="processComments" value="true" /> + </inspection_tool> + </profile> +</component> \ No newline at end of file diff --git a/source/authentication/.idea/inspectionProfiles/profiles_settings.xml b/source/authentication/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/source/authentication/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ +<component name="InspectionProjectProfileManager"> + <settings> + <option name="USE_PROJECT_PROFILE" value="false" /> + <version value="1.0" /> + </settings> +</component> \ No newline at end of file diff --git a/source/authentication/.idea/modules.xml b/source/authentication/.idea/modules.xml new file mode 100644 index 0000000..7407847 --- /dev/null +++ b/source/authentication/.idea/modules.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/.idea/authentication.iml" filepath="$PROJECT_DIR$/.idea/authentication.iml" /> + </modules> + </component> +</project> \ No newline at end of file diff --git a/source/authentication/.idea/workspace.xml b/source/authentication/.idea/workspace.xml new file mode 100644 index 0000000..055225f --- /dev/null +++ b/source/authentication/.idea/workspace.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectViewState"> + <option name="hideEmptyMiddlePackages" value="true" /> + <option name="showExcludedFiles" value="false" /> + <option name="showLibraryContents" value="true" /> + </component> + <component name="PropertiesComponent"> + <property name="settings.editor.selected.configurable" value="configurable.group.tools" /> + </component> +</project> \ No newline at end of file diff --git a/source/authentication/authentication.sqlite3 b/source/authentication/authentication.sqlite3 new file mode 100644 index 0000000..4e6e060 Binary files /dev/null and b/source/authentication/authentication.sqlite3 differ diff --git a/source/authentication/authentication/__init__.py b/source/authentication/authentication/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/authentication/authentication/asgi.py b/source/authentication/authentication/asgi.py new file mode 100644 index 0000000..1c6bccd --- /dev/null +++ b/source/authentication/authentication/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for authentication 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', 'authentication.settings') + +application = get_asgi_application() diff --git a/source/authentication/authentication/settings.py b/source/authentication/authentication/settings.py new file mode 100644 index 0000000..f665f66 --- /dev/null +++ b/source/authentication/authentication/settings.py @@ -0,0 +1,130 @@ +""" +Django settings for authentication project. + +Generated by 'django-admin startproject' using Django 3.2. + +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/ +""" + +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/3.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-fjg#b7qx5e5g3e2vcfb@eg9b3!xy1c+nix5*y=k7h6j&&pc)e8" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "users", +] + +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 = "authentication.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 = "authentication.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "authentication.sqlite3", + } +} + + +# 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 = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = "/static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# Authentication settings + +LOGIN_REDIRECT_URL = "/profile" diff --git a/source/authentication/authentication/urls.py b/source/authentication/authentication/urls.py new file mode 100644 index 0000000..ab9dcc0 --- /dev/null +++ b/source/authentication/authentication/urls.py @@ -0,0 +1,27 @@ +"""authentication 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, include + +from users.views import view_user, view_authentication_code + +urlpatterns = [ + path("admin/", admin.site.urls), + # https://docs.djangoproject.com/fr/3.1/topics/auth/default/#module-django.contrib.auth.views + path("", include("django.contrib.auth.urls")), + path("profile", view_user), + path("codedemo", view_authentication_code), +] diff --git a/source/authentication/authentication/wsgi.py b/source/authentication/authentication/wsgi.py new file mode 100644 index 0000000..1df3d2b --- /dev/null +++ b/source/authentication/authentication/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for authentication 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', 'authentication.settings') + +application = get_wsgi_application() diff --git a/source/authentication/manage.py b/source/authentication/manage.py new file mode 100644 index 0000000..b995e56 --- /dev/null +++ b/source/authentication/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', 'authentication.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/source/authentication/users/__init__.py b/source/authentication/users/__init__.py new file mode 100644 index 0000000..02a960b --- /dev/null +++ b/source/authentication/users/__init__.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class UsersConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "users" + + +default_app_config = "users.UsersConfig" diff --git a/source/authentication/users/templates/registration/login.html b/source/authentication/users/templates/registration/login.html new file mode 100644 index 0000000..6c8c7bb --- /dev/null +++ b/source/authentication/users/templates/registration/login.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Login + + +
+ {% csrf_token %} + {{ form.as_p }} + +
+ + diff --git a/source/authentication/users/templates/users/user-code-page.html b/source/authentication/users/templates/users/user-code-page.html new file mode 100644 index 0000000..47ac1be --- /dev/null +++ b/source/authentication/users/templates/users/user-code-page.html @@ -0,0 +1,14 @@ + + + + + User + + +{% if user.is_anonymous %} + You are not connected with a user. +{% else %} + Welcome, you are connected as {{ user.username }} +{% endif %} + + diff --git a/source/authentication/users/templates/users/user-page.html b/source/authentication/users/templates/users/user-page.html new file mode 100644 index 0000000..47ac1be --- /dev/null +++ b/source/authentication/users/templates/users/user-page.html @@ -0,0 +1,14 @@ + + + + + User + + +{% if user.is_anonymous %} + You are not connected with a user. +{% else %} + Welcome, you are connected as {{ user.username }} +{% endif %} + + diff --git a/source/authentication/users/views.py b/source/authentication/users/views.py new file mode 100644 index 0000000..cfc3fd6 --- /dev/null +++ b/source/authentication/users/views.py @@ -0,0 +1,22 @@ +from annoying.decorators import render_to +from django.contrib.auth import login, logout, authenticate + + +@render_to("users/user-page.html") +def view_user(request): + return {} + + +@render_to("users/user-code-page.html") +def view_authentication_code(request): + # First, logout if we're already connected + logout(request) + # Show that we have no connected user session for the request + print(request.user) + # Check authentication with the current settings + user = authenticate(username="root", password="root") + # A user is returned only if the credentials are correct + if user is not None: + # Login the obtained user in the request + login(request, user) + return {"auth_user": user} diff --git a/source/forms/.idea/Forms.iml b/source/forms/.idea/Forms.iml new file mode 100644 index 0000000..746fbf3 --- /dev/null +++ b/source/forms/.idea/Forms.iml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/forms/.idea/inspectionProfiles/Project_Default.xml b/source/forms/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..09dbac8 --- /dev/null +++ b/source/forms/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/source/forms/.idea/inspectionProfiles/profiles_settings.xml b/source/forms/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/source/forms/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/source/forms/.idea/modules.xml b/source/forms/.idea/modules.xml new file mode 100644 index 0000000..ce2b651 --- /dev/null +++ b/source/forms/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/source/forms/.idea/workspace.xml b/source/forms/.idea/workspace.xml new file mode 100644 index 0000000..19d58a6 --- /dev/null +++ b/source/forms/.idea/workspace.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/source/forms/demonstration/__init__.py b/source/forms/demonstration/__init__.py new file mode 100644 index 0000000..acdb142 --- /dev/null +++ b/source/forms/demonstration/__init__.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class DemonstrationConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "demonstration" + + +default_app_config = "demonstration.DemonstrationConfig" diff --git a/source/forms/demonstration/forms/__init__.py b/source/forms/demonstration/forms/__init__.py new file mode 100644 index 0000000..fb70d89 --- /dev/null +++ b/source/forms/demonstration/forms/__init__.py @@ -0,0 +1,3 @@ +from .person import PersonForm +from .search import SearchForm +from .upload import UploadForm diff --git a/source/forms/demonstration/forms/person.py b/source/forms/demonstration/forms/person.py new file mode 100644 index 0000000..e9f470e --- /dev/null +++ b/source/forms/demonstration/forms/person.py @@ -0,0 +1,13 @@ +from datetime import date + +from django import forms + + +class PersonForm(forms.Form): + """Example of form for person information.""" + + first_name = forms.CharField(max_length=32, label="first name") + last_name = forms.CharField(max_length=32, label="last name") + birth_date = forms.DateField(initial=date(1990, 1, 1), label="birthday") + phone_number = forms.CharField(max_length=12, label="phone number") + password = forms.CharField(widget=forms.PasswordInput, max_length=50, label="password") diff --git a/source/forms/demonstration/forms/search.py b/source/forms/demonstration/forms/search.py new file mode 100644 index 0000000..82aca17 --- /dev/null +++ b/source/forms/demonstration/forms/search.py @@ -0,0 +1,24 @@ +from typing import List + +from django import forms +from django.core.exceptions import ValidationError + + +class SearchForm(forms.Form): + """Example of search form.""" + + FORBIDDEN_WORDS: List[str] = ["lemon", "hat", "car"] + query = forms.CharField(max_length=32, label="query", required=True) + + def clean_query(self) -> str: + """ + Validate the query field. + + Returns: + The value for the "cleaned" query field. + + """ + value: str = self.cleaned_data["query"] + if value.lower() in self.FORBIDDEN_WORDS: + raise ValidationError(f"Search term cannot be one of the following: {self.FORBIDDEN_WORDS}") + return value diff --git a/source/forms/demonstration/forms/upload.py b/source/forms/demonstration/forms/upload.py new file mode 100644 index 0000000..42e6ec9 --- /dev/null +++ b/source/forms/demonstration/forms/upload.py @@ -0,0 +1,26 @@ +from django import forms +from django.core.files.storage import FileSystemStorage +from django.http import HttpRequest + + +class UploadForm(forms.Form): + """Example of file upload form.""" + + image = forms.ImageField(max_length=128, required=True, label="image") + + @staticmethod + def save_uploaded_file(request: HttpRequest) -> str: + """ + Custom method to process the upload of the image. + + Args: + request: HTTP request. + + Returns: + URL of the new uploaded image. + + """ + storage = FileSystemStorage() + image = request.FILES["image"] + name = storage.save(None, image) + return storage.url(name) diff --git a/source/forms/demonstration/static/demonstration/demonstration.css b/source/forms/demonstration/static/demonstration/demonstration.css new file mode 100644 index 0000000..72f0194 --- /dev/null +++ b/source/forms/demonstration/static/demonstration/demonstration.css @@ -0,0 +1,77 @@ +:root { + --head-bg-color: #222; + --head-fg-color: #fff; + --head-ln-color: #3cf; +} + +html, body { + height: 100%; + min-height: 100%; + width: 100%; +} + +body { + margin: 0; + display: grid; + grid-template-areas: "header" "content" "footer"; + grid-template-rows: auto 1fr auto; + grid-template-columns: 100%; + font-family: "Roboto", "Lucida Grande", "DejaVu Sans", "Bitstream Vera Sans", Verdana, Arial, sans-serif; +} + +section#header { + grid-area: header; +} + +section#content { + grid-area: content; +} + +section#footer { + grid-area: footer; +} + + +section#header, section#footer { + background-color: var(--head-bg-color); + color: var(--head-fg-color); +} + +section#header a, section#footer a { + color: var(--head-ln-color); + text-decoration: none; +} + +div.body { + width: 1024px; + margin: 1.5em auto; +} + +table.form-table { + width: 100%; +} + +table.form-table th { + text-align: left; +} + +table.form-table td { + text-align: right; +} + +table.form-table td > input[type=text] { + width: 100%; + box-sizing: border-box; + +} + +input[type=submit] { + padding: 0.5em 4em; + margin: 0; + background-color: crimson; + color: white; + font-size: 125%; + border: maroon 1px solid; + border-radius: 0.5em; + box-shadow: coral 0 0 0 1px inset; +} diff --git a/source/forms/demonstration/templates/demonstration/base.html b/source/forms/demonstration/templates/demonstration/base.html new file mode 100644 index 0000000..114c35f --- /dev/null +++ b/source/forms/demonstration/templates/demonstration/base.html @@ -0,0 +1,33 @@ +{% load static %} {# The load tag enables template tags from other Django apps #} + + + + + {% block title %} | Django Demonstration{% endblock title %} + + + + +
+
+ {% block body %} + Base content in a overridable block. + {% endblock body %} +
+
+ + + + diff --git a/source/forms/demonstration/templates/demonstration/index.html b/source/forms/demonstration/templates/demonstration/index.html new file mode 100644 index 0000000..1a32ba0 --- /dev/null +++ b/source/forms/demonstration/templates/demonstration/index.html @@ -0,0 +1,15 @@ + + + + + Forms demonstration + + +

Sections of form rendering

+ + + diff --git a/source/forms/demonstration/templates/demonstration/person_form_view.html b/source/forms/demonstration/templates/demonstration/person_form_view.html new file mode 100644 index 0000000..3926886 --- /dev/null +++ b/source/forms/demonstration/templates/demonstration/person_form_view.html @@ -0,0 +1,15 @@ +{% extends "demonstration/base.html" %} + +{% block body %} +
+ {% csrf_token %} + + {{ form.as_table }} + + + + +
+
+
+{% endblock body %} diff --git a/source/forms/demonstration/templates/demonstration/search_form_view.html b/source/forms/demonstration/templates/demonstration/search_form_view.html new file mode 100644 index 0000000..c423b6f --- /dev/null +++ b/source/forms/demonstration/templates/demonstration/search_form_view.html @@ -0,0 +1,22 @@ +{% extends "demonstration/base.html" %} + +{% block body %} +
+ + {{ form.as_table }} + + + + +
+
+
+ {{ results|length }} results. +
    + {% for item in results %} +
  • {{ item }}
  • + {% empty %} +
  • No item found.
  • + {% endfor %} +
+{% endblock body %} diff --git a/source/forms/demonstration/templates/demonstration/upload_form_view.html b/source/forms/demonstration/templates/demonstration/upload_form_view.html new file mode 100644 index 0000000..580c1f5 --- /dev/null +++ b/source/forms/demonstration/templates/demonstration/upload_form_view.html @@ -0,0 +1,21 @@ +{% extends "demonstration/base.html" %} + +{% block body %} +
+ {% csrf_token %} + + {{ form.as_table }} + + + + +
+
+
+ {% if image_url %} + The image was successfully uploaded at {{ image_url }} +

+ Uploaded image +

+ {% endif %} +{% endblock body %} diff --git a/source/forms/demonstration/urls.py b/source/forms/demonstration/urls.py new file mode 100644 index 0000000..476ba33 --- /dev/null +++ b/source/forms/demonstration/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from demonstration.views import view_search_form, view_person_form, view_upload_form + +urlpatterns = [ + path("search/", view_search_form, name="search"), + path("person/", view_person_form, name="person"), + path("upload/", view_upload_form, name="upload"), +] diff --git a/source/forms/demonstration/views.py b/source/forms/demonstration/views.py new file mode 100644 index 0000000..8609c19 --- /dev/null +++ b/source/forms/demonstration/views.py @@ -0,0 +1,81 @@ +from typing import List, Optional + +from annoying.decorators import render_to +from django.http import HttpRequest, HttpResponseRedirect + +from demonstration.forms import SearchForm, PersonForm, UploadForm + +NAMES: List[str] = [ + "Jean", + "Paul", + "Robert", + "Julien", + "Nicolas", + "François", + "Julie", + "Marie", + "Anne", + "Évelyne", + "Jeanne", + "Claire", +] + + +@render_to("demonstration/index.html") +def view_index(request: HttpRequest): # noqa + return {} + + +@render_to("demonstration/search_form_view.html") +def view_search_form(request: HttpRequest): # noqa + """ + View for the search form. + + Args: + request: HTTP request. + + Returns: + Data for rendering the template, as a context dictionary. + + """ + form = SearchForm(request.GET) if request.GET else SearchForm() + results: List[str] = [] + if form.is_valid(): + query: str = form.cleaned_data["query"].lower() + results = [item for item in NAMES if query in item.lower()] + return {"form": form, "results": results} + + +@render_to("demonstration/person_form_view.html") +def view_person_form(request: HttpRequest): # noqa + """ + View for the person form. + + Args: + request: HTTP request. + + Returns: + Data for rendering the template, as a context dictionary. + + """ + form = PersonForm(request.POST) if request.method == "POST" else PersonForm() + return {"form": form} + + +@render_to("demonstration/upload_form_view.html") +def view_upload_form(request: HttpRequest): # noqa + """ + View for the upload form. + + Args: + request: HTTP request. + + Returns: + Data for rendering the template, as a context dictionary. + + """ + form = UploadForm(request.POST, request.FILES) if request.method == "POST" else UploadForm() + image_url: Optional[str] = None + if form.is_valid(): + image_url = form.save_uploaded_file(request) + return {"form": form, "image_url": image_url} diff --git a/source/forms/forms/__init__.py b/source/forms/forms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/forms/forms/asgi.py b/source/forms/forms/asgi.py new file mode 100644 index 0000000..0282b3f --- /dev/null +++ b/source/forms/forms/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for forms 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', 'forms.settings') + +application = get_asgi_application() diff --git a/source/forms/forms/settings.py b/source/forms/forms/settings.py new file mode 100644 index 0000000..52a6e23 --- /dev/null +++ b/source/forms/forms/settings.py @@ -0,0 +1,131 @@ +""" +Django settings for forms project. + +Generated by 'django-admin startproject' using Django 3.2. + +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/ +""" + +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/3.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-fp_2(q#373yxtz=sn7!58+x##q_qan$ef#itrz10qj5=e1@w_0" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "demonstration", +] + +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 = "forms.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 = "forms.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + # "default": { + # "ENGINE": "django.db.backends.sqlite3", + # "NAME": BASE_DIR / "db.sqlite3", + # } +} + + +# 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 = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = "/static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# Media paths + +MEDIA_URL = "/media/" +MEDIA_ROOT = BASE_DIR / "media" diff --git a/source/forms/forms/urls.py b/source/forms/forms/urls.py new file mode 100644 index 0000000..f5ca584 --- /dev/null +++ b/source/forms/forms/urls.py @@ -0,0 +1,25 @@ +"""forms 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.conf import settings +from django.conf.urls.static import static +from django.urls import path, include + +from demonstration.views import view_index + +urlpatterns = [ + path("demonstration/", include("demonstration.urls")), + path("", view_index, name="index"), +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/source/forms/forms/wsgi.py b/source/forms/forms/wsgi.py new file mode 100644 index 0000000..1c036c0 --- /dev/null +++ b/source/forms/forms/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for forms 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', 'forms.settings') + +application = get_wsgi_application() diff --git a/source/forms/manage.py b/source/forms/manage.py new file mode 100644 index 0000000..abd04b5 --- /dev/null +++ b/source/forms/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', 'forms.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/source/forms/media/signature.jpg b/source/forms/media/signature.jpg new file mode 100644 index 0000000..2555b0b Binary files /dev/null and b/source/forms/media/signature.jpg differ diff --git a/source/forms/media/signature_I7MUlqN.jpg b/source/forms/media/signature_I7MUlqN.jpg new file mode 100644 index 0000000..2555b0b Binary files /dev/null and b/source/forms/media/signature_I7MUlqN.jpg differ diff --git a/source/forms/media/signature_L4qBlGP.jpg b/source/forms/media/signature_L4qBlGP.jpg new file mode 100644 index 0000000..2555b0b Binary files /dev/null and b/source/forms/media/signature_L4qBlGP.jpg differ diff --git a/source/forms/media/signature_enWsGyI.jpg b/source/forms/media/signature_enWsGyI.jpg new file mode 100644 index 0000000..2555b0b Binary files /dev/null and b/source/forms/media/signature_enWsGyI.jpg differ diff --git a/source/orm/.idea/ORM.iml b/source/orm/.idea/ORM.iml new file mode 100644 index 0000000..4e1e352 --- /dev/null +++ b/source/orm/.idea/ORM.iml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/orm/.idea/inspectionProfiles/Project_Default.xml b/source/orm/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..09dbac8 --- /dev/null +++ b/source/orm/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/source/orm/.idea/inspectionProfiles/profiles_settings.xml b/source/orm/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/source/orm/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/source/orm/.idea/modules.xml b/source/orm/.idea/modules.xml new file mode 100644 index 0000000..0d8a8dd --- /dev/null +++ b/source/orm/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/source/orm/.idea/workspace.xml b/source/orm/.idea/workspace.xml new file mode 100644 index 0000000..055225f --- /dev/null +++ b/source/orm/.idea/workspace.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/source/orm/database.sqlite3 b/source/orm/database.sqlite3 new file mode 100644 index 0000000..222b107 Binary files /dev/null and b/source/orm/database.sqlite3 differ diff --git a/source/orm/jupyter/ORM Demonstration.ipynb b/source/orm/jupyter/ORM Demonstration.ipynb new file mode 100644 index 0000000..425444a --- /dev/null +++ b/source/orm/jupyter/ORM Demonstration.ipynb @@ -0,0 +1,63 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c23b09c8", + "metadata": {}, + "source": [ + "Default use of **ORM** to get objects of a model." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c1729427", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + ", , , , , , , ]>" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "people = Person.objects.all()\n", + "people" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20b337f5", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Django Shell-Plus", + "language": "python", + "name": "django_extensions" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/source/orm/library/__init__.py b/source/orm/library/__init__.py new file mode 100644 index 0000000..f23bc32 --- /dev/null +++ b/source/orm/library/__init__.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class LibraryConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "library" + + +default_app_config = "library.LibraryConfig" diff --git a/source/orm/library/admin/__init__.py b/source/orm/library/admin/__init__.py new file mode 100644 index 0000000..246574f --- /dev/null +++ b/source/orm/library/admin/__init__.py @@ -0,0 +1,4 @@ +from .author import AuthorAdmin +from .book import BookAdmin +from .genre import GenreAdmin +from .person import PersonAdmin diff --git a/source/orm/library/admin/author.py b/source/orm/library/admin/author.py new file mode 100644 index 0000000..a3847b0 --- /dev/null +++ b/source/orm/library/admin/author.py @@ -0,0 +1,14 @@ +from django.contrib import admin + +from library.models import Author + + +@admin.register(Author) +class AuthorAdmin(admin.ModelAdmin): + """ + Admin for book authors. + + """ + + list_display = ["id", "uuid", "first_name", "last_name"] + list_per_page = 25 diff --git a/source/orm/library/admin/book.py b/source/orm/library/admin/book.py new file mode 100644 index 0000000..408e86a --- /dev/null +++ b/source/orm/library/admin/book.py @@ -0,0 +1,16 @@ +from django.contrib import admin + +from library.models import Book + + +@admin.register(Book) +class BookAdmin(admin.ModelAdmin): + """ + Admin for books. + + """ + + list_display = ["id", "name", "isbn", "genre", "year"] + list_editable = ["year", "genre"] + list_filter = ["genre"] + list_per_page = 25 diff --git a/source/orm/library/admin/genre.py b/source/orm/library/admin/genre.py new file mode 100644 index 0000000..fa2a8e4 --- /dev/null +++ b/source/orm/library/admin/genre.py @@ -0,0 +1,14 @@ +from django.contrib import admin + +from library.models import Genre + + +@admin.register(Genre) +class GenreAdmin(admin.ModelAdmin): + """ + Admin for book genres. + + """ + + list_display = ["id", "uuid", "code_name", "name"] + list_per_page = 25 diff --git a/source/orm/library/admin/person.py b/source/orm/library/admin/person.py new file mode 100644 index 0000000..42cbef2 --- /dev/null +++ b/source/orm/library/admin/person.py @@ -0,0 +1,14 @@ +from django.contrib import admin + +from library.models import Person + + +@admin.register(Person) +class PersonAdmin(admin.ModelAdmin): + """ + Admin for people. + + """ + + list_display = ["id", "uuid", "user", "role", "first_name", "last_name"] + list_per_page = 25 diff --git a/source/orm/library/fixtures/users.json.gz b/source/orm/library/fixtures/users.json.gz new file mode 100644 index 0000000..4485385 Binary files /dev/null and b/source/orm/library/fixtures/users.json.gz differ diff --git a/source/orm/library/forms/__init__.py b/source/orm/library/forms/__init__.py new file mode 100644 index 0000000..18f8dec --- /dev/null +++ b/source/orm/library/forms/__init__.py @@ -0,0 +1 @@ +from .person import PersonForm diff --git a/source/orm/library/forms/person.py b/source/orm/library/forms/person.py new file mode 100644 index 0000000..b19e03d --- /dev/null +++ b/source/orm/library/forms/person.py @@ -0,0 +1,15 @@ +from django import forms + +from library.models import Person + + +class PersonForm(forms.ModelForm): + """ + Django form for the Person model. + + """ + + class Meta: + model = Person + exclude = ("user", "uuid") # facultatif + fields = "__all__" # soit "__all__" soit une liste de champs qui seront visibles diff --git a/source/orm/library/library-assets.zip b/source/orm/library/library-assets.zip new file mode 100644 index 0000000..6269ecb Binary files /dev/null and b/source/orm/library/library-assets.zip differ diff --git a/source/orm/library/migrations/0001_initial.py b/source/orm/library/migrations/0001_initial.py new file mode 100644 index 0000000..7c0e676 --- /dev/null +++ b/source/orm/library/migrations/0001_initial.py @@ -0,0 +1,101 @@ +# Generated by Django 3.2 on 2021-04-12 14:11 + +import datetime +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Author', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, verbose_name='UUID')), + ('first_name', models.CharField(max_length=64, verbose_name='first name')), + ('last_name', models.CharField(max_length=64, verbose_name='last name')), + ('description', models.TextField(blank=True, verbose_name='description')), + ('birth_date', models.DateField(default=datetime.date(2000, 1, 1), verbose_name='birth date')), + ('registration_date', models.DateTimeField(auto_now_add=True, verbose_name='registration date')), + ], + options={ + 'verbose_name': 'book author', + 'verbose_name_plural': 'book authors', + }, + ), + migrations.CreateModel( + name='Genre', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, verbose_name='UUID')), + ('code_name', models.CharField(max_length=64, unique=True, verbose_name='code name')), + ('name', models.CharField(max_length=64, verbose_name='name')), + ('description', models.TextField(blank=True, verbose_name='description')), + ('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='creation date')), + ], + options={ + 'verbose_name': 'genre', + 'verbose_name_plural': 'genres', + }, + ), + migrations.CreateModel( + name='Person', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('role', models.PositiveSmallIntegerField(choices=[(0, 'employee'), (1, 'client')], db_index=True, default=0, verbose_name='role')), + ('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, verbose_name='UUID')), + ('first_name', models.CharField(max_length=64, verbose_name='first name')), + ('last_name', models.CharField(max_length=64, verbose_name='last name')), + ('birth_date', models.DateField(default=datetime.date(2000, 1, 1), verbose_name='birth date')), + ('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='creation date')), + ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='person', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'person', + 'verbose_name_plural': 'people', + }, + ), + migrations.CreateModel( + name='Book', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, verbose_name='UUID')), + ('isbn', models.CharField(max_length=64, verbose_name='ISBN')), + ('name', models.CharField(max_length=128, verbose_name='name')), + ('description', models.TextField(blank=True, verbose_name='description')), + ('registration_date', models.DateTimeField(auto_now_add=True, verbose_name='creation date')), + ('authors', models.ManyToManyField(related_name='books', to='library.Author', verbose_name='authors')), + ('genre', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='books', to='library.genre', verbose_name='genre')), + ], + options={ + 'verbose_name': 'book', + 'verbose_name_plural': 'books', + }, + ), + migrations.CreateModel( + name='Loan', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, verbose_name='UUID')), + ('date', models.DateTimeField(auto_now_add=True, verbose_name='date')), + ('expected_return', models.DateTimeField(null=True, verbose_name='expected return date')), + ('borrowed', models.NullBooleanField(default=True, verbose_name='borrowed')), + ('book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='loans', to='library.book', verbose_name='book')), + ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='loans', to='library.person', verbose_name='person')), + ], + options={ + 'verbose_name': 'book loan', + 'verbose_name_plural': 'book loans', + 'unique_together': {('book', 'borrowed')}, + }, + ), + ] diff --git a/source/orm/library/migrations/0002_add_book_year.py b/source/orm/library/migrations/0002_add_book_year.py new file mode 100644 index 0000000..6e1eeb3 --- /dev/null +++ b/source/orm/library/migrations/0002_add_book_year.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2 on 2021-04-12 14:58 + +from django.db import migrations, models +import library.models.book + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='book', + name='year', + field=models.PositiveIntegerField(db_index=True, default=1950, null=True, verbose_name='published'), + ), + migrations.AlterField( + model_name='book', + name='isbn', + field=models.CharField(default=library.models.book.generate_isbn, max_length=64, verbose_name='ISBN'), + ), + ] diff --git a/source/orm/library/migrations/0003_person_picture.py b/source/orm/library/migrations/0003_person_picture.py new file mode 100644 index 0000000..cae8f06 --- /dev/null +++ b/source/orm/library/migrations/0003_person_picture.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2 on 2021-04-12 22:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0002_add_book_year'), + ] + + operations = [ + migrations.AddField( + model_name='person', + name='picture', + field=models.ImageField(max_length=256, null=True, upload_to='pictures', verbose_name='picture'), + ), + ] diff --git a/source/orm/library/migrations/0004_blankable_person_picture.py b/source/orm/library/migrations/0004_blankable_person_picture.py new file mode 100644 index 0000000..0ffd8ee --- /dev/null +++ b/source/orm/library/migrations/0004_blankable_person_picture.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2 on 2021-04-13 17:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0003_person_picture'), + ] + + operations = [ + migrations.AlterField( + model_name='person', + name='picture', + field=models.ImageField(blank=True, max_length=256, null=True, upload_to='pictures', verbose_name='picture'), + ), + ] diff --git a/source/orm/library/migrations/__init__.py b/source/orm/library/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/orm/library/models/__init__.py b/source/orm/library/models/__init__.py new file mode 100644 index 0000000..5709ee0 --- /dev/null +++ b/source/orm/library/models/__init__.py @@ -0,0 +1,5 @@ +from .author import Author +from .book import Book +from .genre import Genre +from .loan import Loan +from .person import Person diff --git a/source/orm/library/models/author.py b/source/orm/library/models/author.py new file mode 100644 index 0000000..25f2800 --- /dev/null +++ b/source/orm/library/models/author.py @@ -0,0 +1,28 @@ +from datetime import date +from uuid import uuid4 + +from django.db import models + + +class Author(models.Model): + """ + Model for book authors. + + """ + + uuid = models.UUIDField(default=uuid4, db_index=True, verbose_name="UUID") + first_name = models.CharField(max_length=64, blank=False, verbose_name="first name") + last_name = models.CharField(max_length=64, blank=False, verbose_name="last name") + description = models.TextField(blank=True, verbose_name="description") + birth_date = models.DateField(default=date(2000, 1, 1), verbose_name="birth date") + registration_date = models.DateTimeField(auto_now_add=True, verbose_name="registration date") + + class Meta: + verbose_name = "book author" + verbose_name_plural = "book authors" + + def __str__(self): + return f"{self.get_full_name()} ({self.uuid})" + + def get_full_name(self): + return f"{self.first_name} {self.last_name}" diff --git a/source/orm/library/models/book.py b/source/orm/library/models/book.py new file mode 100644 index 0000000..ad06e3e --- /dev/null +++ b/source/orm/library/models/book.py @@ -0,0 +1,70 @@ +import random +import string +from uuid import uuid4 + +from django.db import models +from django.utils import timezone + + +def generate_isbn() -> str: + """ + Generate a random ISBN number. + + Returns: + A 13-digit string. + + """ + digits = string.digits + return "".join(random.choice(digits) for _ in range(13)) + + +class BookQuerySet(models.QuerySet): + """ + QuerySet class for books. + + """ + + def available(self) -> models.QuerySet: + """ + Get books available for a loan. + + Excludes books with at least a loan whose `borrowed` field is `True`. + Given the constraints, only one loan for a book can have the `borrowed` status + to `True`. + + """ + return self.exclude(loans__borrowed=True) + + def late_returns(self) -> models.QuerySet: + """ + Get books that should have been returned by now. + + """ + now = timezone.now() + return self.filter(loans__expected_return__lt=now, borrowed=True) + + +class Book(models.Model): + """ + Description of a book. + + """ + + uuid = models.UUIDField(default=uuid4, db_index=True, verbose_name="UUID") + isbn = models.CharField(max_length=64, default=generate_isbn, blank=False, verbose_name="ISBN") + name = models.CharField(max_length=128, blank=False, verbose_name="name") + description = models.TextField(blank=True, verbose_name="description") + registration_date = models.DateTimeField(auto_now_add=True, verbose_name="creation date") + year = models.PositiveIntegerField(default=1950, null=True, db_index=True, verbose_name="published") + authors = models.ManyToManyField("library.Author", related_name="books", verbose_name="authors") + genre = models.ForeignKey( + "library.Genre", on_delete=models.SET_NULL, null=True, related_name="books", verbose_name="genre" + ) + objects = BookQuerySet.as_manager() + + class Meta: + verbose_name = "book" + verbose_name_plural = "books" + + def __str__(self): + return f"{self.name} ({self.isbn})" diff --git a/source/orm/library/models/genre.py b/source/orm/library/models/genre.py new file mode 100644 index 0000000..2bd1e79 --- /dev/null +++ b/source/orm/library/models/genre.py @@ -0,0 +1,23 @@ +from uuid import uuid4 + +from django.db import models + + +class Genre(models.Model): + """ + Book genre. + + """ + + uuid = models.UUIDField(default=uuid4, db_index=True, verbose_name="UUID") + code_name = models.CharField(max_length=64, blank=False, unique=True, verbose_name="code name") + name = models.CharField(max_length=64, blank=False, verbose_name="name") + description = models.TextField(blank=True, verbose_name="description") + creation_date = models.DateTimeField(auto_now_add=True, verbose_name="creation date") + + class Meta: + verbose_name = "genre" + verbose_name_plural = "genres" + + def __str__(self): + return f"{self.name}" diff --git a/source/orm/library/models/loan.py b/source/orm/library/models/loan.py new file mode 100644 index 0000000..e517ae9 --- /dev/null +++ b/source/orm/library/models/loan.py @@ -0,0 +1,29 @@ +from uuid import uuid4 + +from django.db import models + + +class Loan(models.Model): + """ + Model for book loans. + + """ + + uuid = models.UUIDField(default=uuid4, db_index=True, verbose_name="UUID") + person = models.ForeignKey("library.Person", on_delete=models.CASCADE, related_name="loans", verbose_name="person") + book = models.ForeignKey("library.Book", on_delete=models.CASCADE, related_name="loans", verbose_name="book") + date = models.DateTimeField(auto_now_add=True, verbose_name="date") + expected_return = models.DateTimeField(null=True, verbose_name="expected return date") + borrowed = models.BooleanField(null=True, default=True, verbose_name="borrowed") + + class Meta: + verbose_name = "book loan" + verbose_name_plural = "book loans" + unique_together = [("book", "borrowed")] + + def __str__(self): + return f"Book loan: {self.person} → {self.book}" + + def return_book(self): + self.borrowed = None + self.save() diff --git a/source/orm/library/models/person.py b/source/orm/library/models/person.py new file mode 100644 index 0000000..413e807 --- /dev/null +++ b/source/orm/library/models/person.py @@ -0,0 +1,74 @@ +from datetime import date, timedelta +from typing import Optional +from uuid import uuid4 + +from django.db import models, IntegrityError +from django.utils import timezone + +import library + + +class PersonQuerySet(models.QuerySet): + """ + Manager for people. + + """ + + def employees(self) -> models.QuerySet: + """Get only employees.""" + return self.filter(role=Person.Role.EMPLOYEE) + + def clients(self) -> models.QuerySet: + """Get only clients.""" + return self.filter(role=Person.Role.CLIENT) + + +class Person(models.Model): + """ + Base class for people, employees and clients. + + """ + + class Role(models.IntegerChoices): + EMPLOYEE = 0, "employee" + CLIENT = 1, "client" + + role = models.PositiveSmallIntegerField(default=0, choices=Role.choices, db_index=True, verbose_name="role") + user = models.OneToOneField("auth.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="person") + uuid = models.UUIDField(default=uuid4, db_index=True, verbose_name="UUID") + first_name = models.CharField(max_length=64, blank=False, verbose_name="first name") + last_name = models.CharField(max_length=64, blank=False, verbose_name="last name") + birth_date = models.DateField(default=date(2000, 1, 1), verbose_name="birth date") + creation_date = models.DateTimeField(auto_now_add=True, verbose_name="creation date") + picture = models.ImageField(max_length=256, null=True, blank=True, upload_to="pictures", verbose_name="picture") + objects = PersonQuerySet.as_manager() + + class Meta: + verbose_name = "person" + verbose_name_plural = "people" + + def __str__(self): + return f"{self.get_full_name()} ({self.get_role_display()})" + + def get_full_name(self): + return f"{self.first_name} {self.last_name}" + + def borrow(self, book: "library.models.Book", duration: int = 7) -> Optional["library.models.Loan"]: + """ + Borrow a book. + + Args: + book: book instance to borrow + duration: expected duration of loan in days + + Returns: + If the book can be borrowed, return the new `Loan` object. + If not, return `None`. + + """ + try: + deadline = timezone.now() + timedelta(days=duration) + loan = self.loans.create(person=self, book=book, expected_return=deadline) + return loan + except IntegrityError: + return None diff --git a/source/orm/library/static.zip b/source/orm/library/static.zip new file mode 100644 index 0000000..59c9ee2 Binary files /dev/null and b/source/orm/library/static.zip differ diff --git a/source/orm/library/static/library/assets/css/fontawesome-all.min.css b/source/orm/library/static/library/assets/css/fontawesome-all.min.css new file mode 100644 index 0000000..b7d052b --- /dev/null +++ b/source/orm/library/static/library/assets/css/fontawesome-all.min.css @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.9.0 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +.fa,.fab,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{animation:fa-spin 2s infinite linear}.fa-pulse{animation:fa-spin 1s infinite steps(8)}@keyframes fa-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";transform:scaleX(-1)}.fa-flip-vertical{transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adobe:before{content:"\f778"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-handshake:before{content:"\f2b5"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-wizard:before{content:"\f6e8"}.fa-haykal:before{content:"\f666"}.fa-hdd:before{content:"\f0a0"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}@font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:normal;font-display:auto;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands"}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:auto;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-weight:400}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:auto;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.far,.fas{font-family:"Font Awesome 5 Free"}.fa,.fas{font-weight:900} \ No newline at end of file diff --git a/source/orm/library/static/library/assets/css/images/intro.svg b/source/orm/library/static/library/assets/css/images/intro.svg new file mode 100644 index 0000000..c736cc8 --- /dev/null +++ b/source/orm/library/static/library/assets/css/images/intro.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/orm/library/static/library/assets/css/main.css b/source/orm/library/static/library/assets/css/main.css new file mode 100644 index 0000000..d230424 --- /dev/null +++ b/source/orm/library/static/library/assets/css/main.css @@ -0,0 +1,3943 @@ +@import url(fontawesome-all.min.css); + +/* + Hyperspace by HTML5 UP + html5up.net | @ajlkn + Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +*/ + +html, body, div, span, applet, object, +iframe, h1, h2, h3, h4, h5, h6, p, blockquote, +pre, a, abbr, acronym, address, big, cite, +code, del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, b, +u, i, center, dl, dt, dd, ol, ul, li, fieldset, +form, label, legend, table, caption, tbody, +tfoot, thead, tr, th, td, article, aside, +canvas, details, embed, figure, figcaption, +footer, header, hgroup, menu, nav, output, ruby, +section, summary, time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} + +small { + font-size: 60%; +} + +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} + +body { + line-height: 1; +} + +ol, ul { + list-style: none; +} + +blockquote, q { + quotes: none; +} + +blockquote:before, blockquote:after, q:before, q:after { + content: ''; + content: none; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +body { + -webkit-text-size-adjust: none; +} + +mark { + background-color: transparent; + color: inherit; +} + +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +input, select, textarea { + /*-moz-appearance: none;*/ + /*-webkit-appearance: none;*/ + /*-ms-appearance: none;*/ + /*appearance: none;*/ +} + +/* Basic */ + +@-ms-viewport { + width: device-width; +} + +body { + -ms-overflow-style: scrollbar; +} + +@media screen and (max-width: 480px) { + + html, body { + min-width: 320px; + } + +} + +html { + box-sizing: border-box; +} + +*, *:before, *:after { + box-sizing: inherit; +} + +body { + background: #312450; +} + +body.is-preload *, body.is-preload *:before, body.is-preload *:after { + -moz-animation: none !important; + -webkit-animation: none !important; + -ms-animation: none !important; + animation: none !important; + -moz-transition: none !important; + -webkit-transition: none !important; + -ms-transition: none !important; + transition: none !important; +} + +/* Type */ + +body, input, select, textarea { + color: rgba(255, 255, 255, 0.55); + font-family: "Corbel", "Cantarell", "Roboto", "Lucida Grande", "DejaVu Sans", "Bitstream Vera Sans", Verdana, Arial, sans-serif; + font-size: 16.5pt; + font-weight: normal; + line-height: 1.75; +} + +@media screen and (max-width: 1680px) { + + body, input, select, textarea { + font-size: 13pt; + } + +} + +@media screen and (max-width: 1280px) { + + body, input, select, textarea { + font-size: 12pt; + } + +} + +@media screen and (max-width: 360px) { + + body, input, select, textarea { + font-size: 11pt; + } + +} + +a { + -moz-transition: color 0.2s ease, border-bottom-color 0.2s ease; + -webkit-transition: color 0.2s ease, border-bottom-color 0.2s ease; + -ms-transition: color 0.2s ease, border-bottom-color 0.2s ease; + transition: color 0.2s ease, border-bottom-color 0.2s ease; + border-bottom: dotted 1px rgba(255, 255, 255, 0.35); + color: inherit; + text-decoration: none; +} + +a:hover { + border-bottom-color: transparent; + color: #ffffff; +} + +strong, b { + color: #ffffff; + font-weight: bold; +} + +em, i { + font-style: italic; +} + +p { + margin: 0 0 2em 0; +} + +h1, h2, h3, h4, h5, h6 { + color: #ffffff; + font-weight: bold; + line-height: 1.5; + margin: 0 0 0.5em 0; +} + +h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { + color: inherit; + text-decoration: none; +} + +h1 { + font-size: 2.75em; +} + +h1.major { + margin: 0 0 1.3em 0; + position: relative; + padding-bottom: 0.35em; +} + +h1.major:after { + background-image: -moz-linear-gradient(to right, #5e42a6, #b74e91); + background-image: -webkit-linear-gradient(to right, #5e42a6, #b74e91); + background-image: -ms-linear-gradient(to right, #5e42a6, #b74e91); + background-image: linear-gradient(to right, #5e42a6, #b74e91); + -moz-transition: max-width 0.2s ease; + -webkit-transition: max-width 0.2s ease; + -ms-transition: max-width 0.2s ease; + transition: max-width 0.2s ease; + border-radius: 0.2em; + bottom: 0; + content: ''; + height: 0.05em; + position: absolute; + right: 0; + width: 100%; +} + +h2 { + font-size: 1.75em; +} + +h3 { + font-size: 1.1em; +} + +h4 { + font-size: 1em; +} + +h5 { + font-size: 0.8em; +} + +h6 { + font-size: 0.6em; +} + +@media screen and (max-width: 736px) { + + h1 { + font-size: 2em; + } + + h2 { + font-size: 1.25em; + } + + h3 { + font-size: 1em; + } + + h4 { + font-size: 0.8em; + } + + h5 { + font-size: 0.6em; + } + + h6 { + font-size: 0.6em; + } + +} + +sub { + font-size: 0.8em; + position: relative; + top: 0.5em; +} + +sup { + font-size: 0.8em; + position: relative; + top: -0.5em; +} + +blockquote { + border-left: solid 4px rgba(255, 255, 255, 0.15); + font-style: italic; + margin: 0 0 2em 0; + padding: 0.5em 0 0.5em 2em; +} + +code { + background: rgba(255, 255, 255, 0.05); + border-radius: 0.25em; + border: solid 1px rgba(255, 255, 255, 0.15); + font-family: "Courier New", monospace; + font-size: 0.9em; + margin: 0 0.25em; + padding: 0.25em 0.65em; +} + +pre { + -webkit-overflow-scrolling: touch; + font-family: "Courier New", monospace; + font-size: 0.9em; + margin: 0 0 2em 0; +} + +pre code { + display: block; + line-height: 1.75em; + padding: 1em 1.5em; + overflow-x: auto; +} + +hr { + border: 0; + border-bottom: solid 1px rgba(255, 255, 255, 0.15); + margin: 2em 0; +} + +hr.major { + margin: 3em 0; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* Row */ + +.row { + display: flex; + flex-wrap: wrap; + box-sizing: border-box; + align-items: stretch; +} + +.row > * { + box-sizing: border-box; +} + +.row.gtr-uniform > * > :last-child { + margin-bottom: 0; +} + +.row.aln-left { + justify-content: flex-start; +} + +.row.aln-center { + justify-content: center; +} + +.row.aln-right { + justify-content: flex-end; +} + +.row.aln-top { + align-items: flex-start; +} + +.row.aln-middle { + align-items: center; +} + +.row.aln-bottom { + align-items: flex-end; +} + +.row > .imp { + order: -1; +} + +.row > .col-1 { + width: 8.33333%; +} + +.row > .off-1 { + margin-left: 8.33333%; +} + +.row > .col-2 { + width: 16.66667%; +} + +.row > .off-2 { + margin-left: 16.66667%; +} + +.row > .col-3 { + width: 25%; +} + +.row > .off-3 { + margin-left: 25%; +} + +.row > .col-4 { + width: 33.33333%; +} + +.row > .off-4 { + margin-left: 33.33333%; +} + +.row > .col-5 { + width: 41.66667%; +} + +.row > .off-5 { + margin-left: 41.66667%; +} + +.row > .col-6 { + width: 50%; +} + +.row > .off-6 { + margin-left: 50%; +} + +.row > .col-7 { + width: 58.33333%; +} + +.row > .off-7 { + margin-left: 58.33333%; +} + +.row > .col-8 { + width: 66.66667%; +} + +.row > .off-8 { + margin-left: 66.66667%; +} + +.row > .col-9 { + width: 75%; +} + +.row > .off-9 { + margin-left: 75%; +} + +.row > .col-10 { + width: 83.33333%; +} + +.row > .off-10 { + margin-left: 83.33333%; +} + +.row > .col-11 { + width: 91.66667%; +} + +.row > .off-11 { + margin-left: 91.66667%; +} + +.row > .col-12 { + width: 100%; +} + +.row > .off-12 { + margin-left: 100%; +} + +.row.gtr-0 { + margin-top: 0; + margin-left: 0em; +} + +.row.gtr-0 > * { + padding: 0 0 0 0em; +} + +.row.gtr-0.gtr-uniform { + margin-top: 0em; +} + +.row.gtr-0.gtr-uniform > * { + padding-top: 0em; +} + +.row.gtr-25 { + margin-top: 0; + margin-left: -0.375em; +} + +.row.gtr-25 > * { + padding: 0 0 0 0.375em; +} + +.row.gtr-25.gtr-uniform { + margin-top: -0.375em; +} + +.row.gtr-25.gtr-uniform > * { + padding-top: 0.375em; +} + +.row.gtr-50 { + margin-top: 0; + margin-left: -0.75em; +} + +.row.gtr-50 > * { + padding: 0 0 0 0.75em; +} + +.row.gtr-50.gtr-uniform { + margin-top: -0.75em; +} + +.row.gtr-50.gtr-uniform > * { + padding-top: 0.75em; +} + +.row { + margin-top: 0; + margin-left: -1.5em; +} + +.row > * { + padding: 0 0 0 1.5em; +} + +.row.gtr-uniform { + margin-top: -1.5em; +} + +.row.gtr-uniform > * { + padding-top: 1.5em; +} + +.row.gtr-150 { + margin-top: 0; + margin-left: -2.25em; +} + +.row.gtr-150 > * { + padding: 0 0 0 2.25em; +} + +.row.gtr-150.gtr-uniform { + margin-top: -2.25em; +} + +.row.gtr-150.gtr-uniform > * { + padding-top: 2.25em; +} + +.row.gtr-200 { + margin-top: 0; + margin-left: -3em; +} + +.row.gtr-200 > * { + padding: 0 0 0 3em; +} + +.row.gtr-200.gtr-uniform { + margin-top: -3em; +} + +.row.gtr-200.gtr-uniform > * { + padding-top: 3em; +} + +@media screen and (max-width: 1680px) { + + .row { + display: flex; + flex-wrap: wrap; + box-sizing: border-box; + align-items: stretch; + } + + .row > * { + box-sizing: border-box; + } + + .row.gtr-uniform > * > :last-child { + margin-bottom: 0; + } + + .row.aln-left { + justify-content: flex-start; + } + + .row.aln-center { + justify-content: center; + } + + .row.aln-right { + justify-content: flex-end; + } + + .row.aln-top { + align-items: flex-start; + } + + .row.aln-middle { + align-items: center; + } + + .row.aln-bottom { + align-items: flex-end; + } + + .row > .imp-xlarge { + order: -1; + } + + .row > .col-1-xlarge { + width: 8.33333%; + } + + .row > .off-1-xlarge { + margin-left: 8.33333%; + } + + .row > .col-2-xlarge { + width: 16.66667%; + } + + .row > .off-2-xlarge { + margin-left: 16.66667%; + } + + .row > .col-3-xlarge { + width: 25%; + } + + .row > .off-3-xlarge { + margin-left: 25%; + } + + .row > .col-4-xlarge { + width: 33.33333%; + } + + .row > .off-4-xlarge { + margin-left: 33.33333%; + } + + .row > .col-5-xlarge { + width: 41.66667%; + } + + .row > .off-5-xlarge { + margin-left: 41.66667%; + } + + .row > .col-6-xlarge { + width: 50%; + } + + .row > .off-6-xlarge { + margin-left: 50%; + } + + .row > .col-7-xlarge { + width: 58.33333%; + } + + .row > .off-7-xlarge { + margin-left: 58.33333%; + } + + .row > .col-8-xlarge { + width: 66.66667%; + } + + .row > .off-8-xlarge { + margin-left: 66.66667%; + } + + .row > .col-9-xlarge { + width: 75%; + } + + .row > .off-9-xlarge { + margin-left: 75%; + } + + .row > .col-10-xlarge { + width: 83.33333%; + } + + .row > .off-10-xlarge { + margin-left: 83.33333%; + } + + .row > .col-11-xlarge { + width: 91.66667%; + } + + .row > .off-11-xlarge { + margin-left: 91.66667%; + } + + .row > .col-12-xlarge { + width: 100%; + } + + .row > .off-12-xlarge { + margin-left: 100%; + } + + .row.gtr-0 { + margin-top: 0; + margin-left: 0em; + } + + .row.gtr-0 > * { + padding: 0 0 0 0em; + } + + .row.gtr-0.gtr-uniform { + margin-top: 0em; + } + + .row.gtr-0.gtr-uniform > * { + padding-top: 0em; + } + + .row.gtr-25 { + margin-top: 0; + margin-left: -0.375em; + } + + .row.gtr-25 > * { + padding: 0 0 0 0.375em; + } + + .row.gtr-25.gtr-uniform { + margin-top: -0.375em; + } + + .row.gtr-25.gtr-uniform > * { + padding-top: 0.375em; + } + + .row.gtr-50 { + margin-top: 0; + margin-left: -0.75em; + } + + .row.gtr-50 > * { + padding: 0 0 0 0.75em; + } + + .row.gtr-50.gtr-uniform { + margin-top: -0.75em; + } + + .row.gtr-50.gtr-uniform > * { + padding-top: 0.75em; + } + + .row { + margin-top: 0; + margin-left: -1.5em; + } + + .row > * { + padding: 0 0 0 1.5em; + } + + .row.gtr-uniform { + margin-top: -1.5em; + } + + .row.gtr-uniform > * { + padding-top: 1.5em; + } + + .row.gtr-150 { + margin-top: 0; + margin-left: -2.25em; + } + + .row.gtr-150 > * { + padding: 0 0 0 2.25em; + } + + .row.gtr-150.gtr-uniform { + margin-top: -2.25em; + } + + .row.gtr-150.gtr-uniform > * { + padding-top: 2.25em; + } + + .row.gtr-200 { + margin-top: 0; + margin-left: -3em; + } + + .row.gtr-200 > * { + padding: 0 0 0 3em; + } + + .row.gtr-200.gtr-uniform { + margin-top: -3em; + } + + .row.gtr-200.gtr-uniform > * { + padding-top: 3em; + } + +} + +@media screen and (max-width: 1280px) { + + .row { + display: flex; + flex-wrap: wrap; + box-sizing: border-box; + align-items: stretch; + } + + .row > * { + box-sizing: border-box; + } + + .row.gtr-uniform > * > :last-child { + margin-bottom: 0; + } + + .row.aln-left { + justify-content: flex-start; + } + + .row.aln-center { + justify-content: center; + } + + .row.aln-right { + justify-content: flex-end; + } + + .row.aln-top { + align-items: flex-start; + } + + .row.aln-middle { + align-items: center; + } + + .row.aln-bottom { + align-items: flex-end; + } + + .row > .imp-large { + order: -1; + } + + .row > .col-1-large { + width: 8.33333%; + } + + .row > .off-1-large { + margin-left: 8.33333%; + } + + .row > .col-2-large { + width: 16.66667%; + } + + .row > .off-2-large { + margin-left: 16.66667%; + } + + .row > .col-3-large { + width: 25%; + } + + .row > .off-3-large { + margin-left: 25%; + } + + .row > .col-4-large { + width: 33.33333%; + } + + .row > .off-4-large { + margin-left: 33.33333%; + } + + .row > .col-5-large { + width: 41.66667%; + } + + .row > .off-5-large { + margin-left: 41.66667%; + } + + .row > .col-6-large { + width: 50%; + } + + .row > .off-6-large { + margin-left: 50%; + } + + .row > .col-7-large { + width: 58.33333%; + } + + .row > .off-7-large { + margin-left: 58.33333%; + } + + .row > .col-8-large { + width: 66.66667%; + } + + .row > .off-8-large { + margin-left: 66.66667%; + } + + .row > .col-9-large { + width: 75%; + } + + .row > .off-9-large { + margin-left: 75%; + } + + .row > .col-10-large { + width: 83.33333%; + } + + .row > .off-10-large { + margin-left: 83.33333%; + } + + .row > .col-11-large { + width: 91.66667%; + } + + .row > .off-11-large { + margin-left: 91.66667%; + } + + .row > .col-12-large { + width: 100%; + } + + .row > .off-12-large { + margin-left: 100%; + } + + .row.gtr-0 { + margin-top: 0; + margin-left: 0em; + } + + .row.gtr-0 > * { + padding: 0 0 0 0em; + } + + .row.gtr-0.gtr-uniform { + margin-top: 0em; + } + + .row.gtr-0.gtr-uniform > * { + padding-top: 0em; + } + + .row.gtr-25 { + margin-top: 0; + margin-left: -0.375em; + } + + .row.gtr-25 > * { + padding: 0 0 0 0.375em; + } + + .row.gtr-25.gtr-uniform { + margin-top: -0.375em; + } + + .row.gtr-25.gtr-uniform > * { + padding-top: 0.375em; + } + + .row.gtr-50 { + margin-top: 0; + margin-left: -0.75em; + } + + .row.gtr-50 > * { + padding: 0 0 0 0.75em; + } + + .row.gtr-50.gtr-uniform { + margin-top: -0.75em; + } + + .row.gtr-50.gtr-uniform > * { + padding-top: 0.75em; + } + + .row { + margin-top: 0; + margin-left: -1.5em; + } + + .row > * { + padding: 0 0 0 1.5em; + } + + .row.gtr-uniform { + margin-top: -1.5em; + } + + .row.gtr-uniform > * { + padding-top: 1.5em; + } + + .row.gtr-150 { + margin-top: 0; + margin-left: -2.25em; + } + + .row.gtr-150 > * { + padding: 0 0 0 2.25em; + } + + .row.gtr-150.gtr-uniform { + margin-top: -2.25em; + } + + .row.gtr-150.gtr-uniform > * { + padding-top: 2.25em; + } + + .row.gtr-200 { + margin-top: 0; + margin-left: -3em; + } + + .row.gtr-200 > * { + padding: 0 0 0 3em; + } + + .row.gtr-200.gtr-uniform { + margin-top: -3em; + } + + .row.gtr-200.gtr-uniform > * { + padding-top: 3em; + } + +} + +@media screen and (max-width: 980px) { + + .row { + display: flex; + flex-wrap: wrap; + box-sizing: border-box; + align-items: stretch; + } + + .row > * { + box-sizing: border-box; + } + + .row.gtr-uniform > * > :last-child { + margin-bottom: 0; + } + + .row.aln-left { + justify-content: flex-start; + } + + .row.aln-center { + justify-content: center; + } + + .row.aln-right { + justify-content: flex-end; + } + + .row.aln-top { + align-items: flex-start; + } + + .row.aln-middle { + align-items: center; + } + + .row.aln-bottom { + align-items: flex-end; + } + + .row > .imp-medium { + order: -1; + } + + .row > .col-1-medium { + width: 8.33333%; + } + + .row > .off-1-medium { + margin-left: 8.33333%; + } + + .row > .col-2-medium { + width: 16.66667%; + } + + .row > .off-2-medium { + margin-left: 16.66667%; + } + + .row > .col-3-medium { + width: 25%; + } + + .row > .off-3-medium { + margin-left: 25%; + } + + .row > .col-4-medium { + width: 33.33333%; + } + + .row > .off-4-medium { + margin-left: 33.33333%; + } + + .row > .col-5-medium { + width: 41.66667%; + } + + .row > .off-5-medium { + margin-left: 41.66667%; + } + + .row > .col-6-medium { + width: 50%; + } + + .row > .off-6-medium { + margin-left: 50%; + } + + .row > .col-7-medium { + width: 58.33333%; + } + + .row > .off-7-medium { + margin-left: 58.33333%; + } + + .row > .col-8-medium { + width: 66.66667%; + } + + .row > .off-8-medium { + margin-left: 66.66667%; + } + + .row > .col-9-medium { + width: 75%; + } + + .row > .off-9-medium { + margin-left: 75%; + } + + .row > .col-10-medium { + width: 83.33333%; + } + + .row > .off-10-medium { + margin-left: 83.33333%; + } + + .row > .col-11-medium { + width: 91.66667%; + } + + .row > .off-11-medium { + margin-left: 91.66667%; + } + + .row > .col-12-medium { + width: 100%; + } + + .row > .off-12-medium { + margin-left: 100%; + } + + .row.gtr-0 { + margin-top: 0; + margin-left: 0em; + } + + .row.gtr-0 > * { + padding: 0 0 0 0em; + } + + .row.gtr-0.gtr-uniform { + margin-top: 0em; + } + + .row.gtr-0.gtr-uniform > * { + padding-top: 0em; + } + + .row.gtr-25 { + margin-top: 0; + margin-left: -0.375em; + } + + .row.gtr-25 > * { + padding: 0 0 0 0.375em; + } + + .row.gtr-25.gtr-uniform { + margin-top: -0.375em; + } + + .row.gtr-25.gtr-uniform > * { + padding-top: 0.375em; + } + + .row.gtr-50 { + margin-top: 0; + margin-left: -0.75em; + } + + .row.gtr-50 > * { + padding: 0 0 0 0.75em; + } + + .row.gtr-50.gtr-uniform { + margin-top: -0.75em; + } + + .row.gtr-50.gtr-uniform > * { + padding-top: 0.75em; + } + + .row { + margin-top: 0; + margin-left: -1.5em; + } + + .row > * { + padding: 0 0 0 1.5em; + } + + .row.gtr-uniform { + margin-top: -1.5em; + } + + .row.gtr-uniform > * { + padding-top: 1.5em; + } + + .row.gtr-150 { + margin-top: 0; + margin-left: -2.25em; + } + + .row.gtr-150 > * { + padding: 0 0 0 2.25em; + } + + .row.gtr-150.gtr-uniform { + margin-top: -2.25em; + } + + .row.gtr-150.gtr-uniform > * { + padding-top: 2.25em; + } + + .row.gtr-200 { + margin-top: 0; + margin-left: -3em; + } + + .row.gtr-200 > * { + padding: 0 0 0 3em; + } + + .row.gtr-200.gtr-uniform { + margin-top: -3em; + } + + .row.gtr-200.gtr-uniform > * { + padding-top: 3em; + } + +} + +@media screen and (max-width: 736px) { + + .row { + display: flex; + flex-wrap: wrap; + box-sizing: border-box; + align-items: stretch; + } + + .row > * { + box-sizing: border-box; + } + + .row.gtr-uniform > * > :last-child { + margin-bottom: 0; + } + + .row.aln-left { + justify-content: flex-start; + } + + .row.aln-center { + justify-content: center; + } + + .row.aln-right { + justify-content: flex-end; + } + + .row.aln-top { + align-items: flex-start; + } + + .row.aln-middle { + align-items: center; + } + + .row.aln-bottom { + align-items: flex-end; + } + + .row > .imp-small { + order: -1; + } + + .row > .col-1-small { + width: 8.33333%; + } + + .row > .off-1-small { + margin-left: 8.33333%; + } + + .row > .col-2-small { + width: 16.66667%; + } + + .row > .off-2-small { + margin-left: 16.66667%; + } + + .row > .col-3-small { + width: 25%; + } + + .row > .off-3-small { + margin-left: 25%; + } + + .row > .col-4-small { + width: 33.33333%; + } + + .row > .off-4-small { + margin-left: 33.33333%; + } + + .row > .col-5-small { + width: 41.66667%; + } + + .row > .off-5-small { + margin-left: 41.66667%; + } + + .row > .col-6-small { + width: 50%; + } + + .row > .off-6-small { + margin-left: 50%; + } + + .row > .col-7-small { + width: 58.33333%; + } + + .row > .off-7-small { + margin-left: 58.33333%; + } + + .row > .col-8-small { + width: 66.66667%; + } + + .row > .off-8-small { + margin-left: 66.66667%; + } + + .row > .col-9-small { + width: 75%; + } + + .row > .off-9-small { + margin-left: 75%; + } + + .row > .col-10-small { + width: 83.33333%; + } + + .row > .off-10-small { + margin-left: 83.33333%; + } + + .row > .col-11-small { + width: 91.66667%; + } + + .row > .off-11-small { + margin-left: 91.66667%; + } + + .row > .col-12-small { + width: 100%; + } + + .row > .off-12-small { + margin-left: 100%; + } + + .row.gtr-0 { + margin-top: 0; + margin-left: 0em; + } + + .row.gtr-0 > * { + padding: 0 0 0 0em; + } + + .row.gtr-0.gtr-uniform { + margin-top: 0em; + } + + .row.gtr-0.gtr-uniform > * { + padding-top: 0em; + } + + .row.gtr-25 { + margin-top: 0; + margin-left: -0.375em; + } + + .row.gtr-25 > * { + padding: 0 0 0 0.375em; + } + + .row.gtr-25.gtr-uniform { + margin-top: -0.375em; + } + + .row.gtr-25.gtr-uniform > * { + padding-top: 0.375em; + } + + .row.gtr-50 { + margin-top: 0; + margin-left: -0.75em; + } + + .row.gtr-50 > * { + padding: 0 0 0 0.75em; + } + + .row.gtr-50.gtr-uniform { + margin-top: -0.75em; + } + + .row.gtr-50.gtr-uniform > * { + padding-top: 0.75em; + } + + .row { + margin-top: 0; + margin-left: -1.5em; + } + + .row > * { + padding: 0 0 0 1.5em; + } + + .row.gtr-uniform { + margin-top: -1.5em; + } + + .row.gtr-uniform > * { + padding-top: 1.5em; + } + + .row.gtr-150 { + margin-top: 0; + margin-left: -2.25em; + } + + .row.gtr-150 > * { + padding: 0 0 0 2.25em; + } + + .row.gtr-150.gtr-uniform { + margin-top: -2.25em; + } + + .row.gtr-150.gtr-uniform > * { + padding-top: 2.25em; + } + + .row.gtr-200 { + margin-top: 0; + margin-left: -3em; + } + + .row.gtr-200 > * { + padding: 0 0 0 3em; + } + + .row.gtr-200.gtr-uniform { + margin-top: -3em; + } + + .row.gtr-200.gtr-uniform > * { + padding-top: 3em; + } + +} + +@media screen and (max-width: 480px) { + + .row { + display: flex; + flex-wrap: wrap; + box-sizing: border-box; + align-items: stretch; + } + + .row > * { + box-sizing: border-box; + } + + .row.gtr-uniform > * > :last-child { + margin-bottom: 0; + } + + .row.aln-left { + justify-content: flex-start; + } + + .row.aln-center { + justify-content: center; + } + + .row.aln-right { + justify-content: flex-end; + } + + .row.aln-top { + align-items: flex-start; + } + + .row.aln-middle { + align-items: center; + } + + .row.aln-bottom { + align-items: flex-end; + } + + .row > .imp-xsmall { + order: -1; + } + + .row > .col-1-xsmall { + width: 8.33333%; + } + + .row > .off-1-xsmall { + margin-left: 8.33333%; + } + + .row > .col-2-xsmall { + width: 16.66667%; + } + + .row > .off-2-xsmall { + margin-left: 16.66667%; + } + + .row > .col-3-xsmall { + width: 25%; + } + + .row > .off-3-xsmall { + margin-left: 25%; + } + + .row > .col-4-xsmall { + width: 33.33333%; + } + + .row > .off-4-xsmall { + margin-left: 33.33333%; + } + + .row > .col-5-xsmall { + width: 41.66667%; + } + + .row > .off-5-xsmall { + margin-left: 41.66667%; + } + + .row > .col-6-xsmall { + width: 50%; + } + + .row > .off-6-xsmall { + margin-left: 50%; + } + + .row > .col-7-xsmall { + width: 58.33333%; + } + + .row > .off-7-xsmall { + margin-left: 58.33333%; + } + + .row > .col-8-xsmall { + width: 66.66667%; + } + + .row > .off-8-xsmall { + margin-left: 66.66667%; + } + + .row > .col-9-xsmall { + width: 75%; + } + + .row > .off-9-xsmall { + margin-left: 75%; + } + + .row > .col-10-xsmall { + width: 83.33333%; + } + + .row > .off-10-xsmall { + margin-left: 83.33333%; + } + + .row > .col-11-xsmall { + width: 91.66667%; + } + + .row > .off-11-xsmall { + margin-left: 91.66667%; + } + + .row > .col-12-xsmall { + width: 100%; + } + + .row > .off-12-xsmall { + margin-left: 100%; + } + + .row.gtr-0 { + margin-top: 0; + margin-left: 0em; + } + + .row.gtr-0 > * { + padding: 0 0 0 0em; + } + + .row.gtr-0.gtr-uniform { + margin-top: 0em; + } + + .row.gtr-0.gtr-uniform > * { + padding-top: 0em; + } + + .row.gtr-25 { + margin-top: 0; + margin-left: -0.375em; + } + + .row.gtr-25 > * { + padding: 0 0 0 0.375em; + } + + .row.gtr-25.gtr-uniform { + margin-top: -0.375em; + } + + .row.gtr-25.gtr-uniform > * { + padding-top: 0.375em; + } + + .row.gtr-50 { + margin-top: 0; + margin-left: -0.75em; + } + + .row.gtr-50 > * { + padding: 0 0 0 0.75em; + } + + .row.gtr-50.gtr-uniform { + margin-top: -0.75em; + } + + .row.gtr-50.gtr-uniform > * { + padding-top: 0.75em; + } + + .row { + margin-top: 0; + margin-left: -1.5em; + } + + .row > * { + padding: 0 0 0 1.5em; + } + + .row.gtr-uniform { + margin-top: -1.5em; + } + + .row.gtr-uniform > * { + padding-top: 1.5em; + } + + .row.gtr-150 { + margin-top: 0; + margin-left: -2.25em; + } + + .row.gtr-150 > * { + padding: 0 0 0 2.25em; + } + + .row.gtr-150.gtr-uniform { + margin-top: -2.25em; + } + + .row.gtr-150.gtr-uniform > * { + padding-top: 2.25em; + } + + .row.gtr-200 { + margin-top: 0; + margin-left: -3em; + } + + .row.gtr-200 > * { + padding: 0 0 0 3em; + } + + .row.gtr-200.gtr-uniform { + margin-top: -3em; + } + + .row.gtr-200.gtr-uniform > * { + padding-top: 3em; + } + +} + +/* Box */ + +.box { + border-radius: 0.25em; + border: solid 1px rgba(255, 255, 255, 0.15); + margin-bottom: 2em; + padding: 1.5em; +} + +.box > :last-child, +.box > :last-child > :last-child, +.box > :last-child > :last-child > :last-child { + margin-bottom: 0; +} + +.box.alt { + border: 0; + border-radius: 0; + padding: 0; +} + +/* Button */ + +input[type="submit"], +input[type="reset"], +input[type="button"], +button, +.button { + -moz-appearance: none; + -webkit-appearance: none; + -ms-appearance: none; + appearance: none; + -moz-transition: border-color 0.2s ease; + -webkit-transition: border-color 0.2s ease; + -ms-transition: border-color 0.2s ease; + transition: border-color 0.2s ease; + background-color: transparent; + border: solid 1px !important; + border-color: rgba(255, 255, 255, 0.15) !important; + border-radius: 3em; + color: #ffffff !important; + cursor: pointer; + display: inline-block; + font-size: 0.6em; + font-weight: bold; + height: calc(4.75em + 2px); + letter-spacing: 0.25em; + line-height: 4.75em; + outline: 0; + padding: 0 3.75em; + position: relative; + text-align: center; + text-decoration: none; + text-transform: uppercase; + white-space: nowrap; +} + +input[type="submit"]:after, +input[type="reset"]:after, +input[type="button"]:after, +button:after, +.button:after { + -moz-transform: scale(0.25); + -webkit-transform: scale(0.25); + -ms-transform: scale(0.25); + transform: scale(0.25); + pointer-events: none; + -moz-transition: opacity 0.2s ease, -moz-transform 0.2s ease; + -webkit-transition: opacity 0.2s ease, -webkit-transform 0.2s ease; + -ms-transition: opacity 0.2s ease, -ms-transform 0.2s ease; + transition: opacity 0.2s ease, transform 0.2s ease; + background: #ffffff; + border-radius: 3em; + content: ''; + height: 100%; + left: 0; + opacity: 0; + position: absolute; + top: 0; + width: 100%; +} + +input[type="submit"].icon:before, +input[type="reset"].icon:before, +input[type="button"].icon:before, +button.icon:before, +.button.icon:before { + margin-right: 0.75em; +} + +input[type="submit"].fit, +input[type="reset"].fit, +input[type="button"].fit, +button.fit, +.button.fit { + width: 100%; +} + +input[type="submit"].small, +input[type="reset"].small, +input[type="button"].small, +button.small, +.button.small { + font-size: 0.4em; +} + +input[type="submit"].large, +input[type="reset"].large, +input[type="button"].large, +button.large, +.button.large { + font-size: 0.8em; +} + +input[type="submit"].primary, +input[type="reset"].primary, +input[type="button"].primary, +button.primary, +.button.primary { + background-color: #ffffff; + color: #312450 !important; +} + +input[type="submit"].primary:after, +input[type="reset"].primary:after, +input[type="button"].primary:after, +button.primary:after, +.button.primary:after { + display: none; +} + +input[type="submit"].disabled, input[type="submit"]:disabled, +input[type="reset"].disabled, +input[type="reset"]:disabled, +input[type="button"].disabled, +input[type="button"]:disabled, +button.disabled, +button:disabled, +.button.disabled, +.button:disabled { + cursor: default; + opacity: 0.5; + pointer-events: none; +} + +input[type="submit"]:hover, +input[type="reset"]:hover, +input[type="button"]:hover, +button:hover, +.button:hover { + border-color: rgba(255, 255, 255, 0.55) !important; +} + +input[type="submit"]:hover:after, +input[type="reset"]:hover:after, +input[type="button"]:hover:after, +button:hover:after, +.button:hover:after { + opacity: 0.05; + -moz-transform: scale(1); + -webkit-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); +} + +input[type="submit"]:hover:active, +input[type="reset"]:hover:active, +input[type="button"]:hover:active, +button:hover:active, +.button:hover:active { + border-color: #ffffff !important; +} + +input[type="submit"]:hover:active:after, +input[type="reset"]:hover:active:after, +input[type="button"]:hover:active:after, +button:hover:active:after, +.button:hover:active:after { + opacity: 0.1; +} + +/* Features */ + +.features { + display: -moz-flex; + display: -webkit-flex; + display: -ms-flex; + display: flex; + -moz-flex-wrap: wrap; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + border-radius: 0.25em; + border: solid 1px rgba(255, 255, 255, 0.15); + background: rgba(255, 255, 255, 0.05); + margin: 0 0 2em 0; +} + +.features section { + padding: 3.5em 3em 1em 7em; + width: 50%; + border-top: solid 1px rgba(255, 255, 255, 0.15); + position: relative; +} + +.features section:nth-child(-n + 2) { + border-top-width: 0; +} + +.features section:nth-child(2n) { + border-left: solid 1px rgba(255, 255, 255, 0.15); +} + +.features section .icon { + -moz-transition: opacity 0.5s ease, -moz-transform 0.5s ease; + -webkit-transition: opacity 0.5s ease, -webkit-transform 0.5s ease; + -ms-transition: opacity 0.5s ease, -ms-transform 0.5s ease; + transition: opacity 0.5s ease, transform 0.5s ease; + -moz-transition-delay: 1s; + -webkit-transition-delay: 1s; + -ms-transition-delay: 1s; + transition-delay: 1s; + -moz-transform: scale(1); + -webkit-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + position: absolute; + left: 3em; + top: 3em; + opacity: 1; +} + +.features section:nth-child(1) .icon { + -moz-transition-delay: 0.15s; + -webkit-transition-delay: 0.15s; + -ms-transition-delay: 0.15s; + transition-delay: 0.15s; +} + +.features section:nth-child(2) .icon { + -moz-transition-delay: 0.3s; + -webkit-transition-delay: 0.3s; + -ms-transition-delay: 0.3s; + transition-delay: 0.3s; +} + +.features section:nth-child(3) .icon { + -moz-transition-delay: 0.45s; + -webkit-transition-delay: 0.45s; + -ms-transition-delay: 0.45s; + transition-delay: 0.45s; +} + +.features section:nth-child(4) .icon { + -moz-transition-delay: 0.6s; + -webkit-transition-delay: 0.6s; + -ms-transition-delay: 0.6s; + transition-delay: 0.6s; +} + +.features section:nth-child(5) .icon { + -moz-transition-delay: 0.75s; + -webkit-transition-delay: 0.75s; + -ms-transition-delay: 0.75s; + transition-delay: 0.75s; +} + +.features section:nth-child(6) .icon { + -moz-transition-delay: 0.9s; + -webkit-transition-delay: 0.9s; + -ms-transition-delay: 0.9s; + transition-delay: 0.9s; +} + +.features section:nth-child(7) .icon { + -moz-transition-delay: 1.05s; + -webkit-transition-delay: 1.05s; + -ms-transition-delay: 1.05s; + transition-delay: 1.05s; +} + +.features section:nth-child(8) .icon { + -moz-transition-delay: 1.2s; + -webkit-transition-delay: 1.2s; + -ms-transition-delay: 1.2s; + transition-delay: 1.2s; +} + +.features section:nth-child(9) .icon { + -moz-transition-delay: 1.35s; + -webkit-transition-delay: 1.35s; + -ms-transition-delay: 1.35s; + transition-delay: 1.35s; +} + +.features section:nth-child(10) .icon { + -moz-transition-delay: 1.5s; + -webkit-transition-delay: 1.5s; + -ms-transition-delay: 1.5s; + transition-delay: 1.5s; +} + +.features section:nth-child(11) .icon { + -moz-transition-delay: 1.65s; + -webkit-transition-delay: 1.65s; + -ms-transition-delay: 1.65s; + transition-delay: 1.65s; +} + +.features section:nth-child(12) .icon { + -moz-transition-delay: 1.8s; + -webkit-transition-delay: 1.8s; + -ms-transition-delay: 1.8s; + transition-delay: 1.8s; +} + +.features section:nth-child(13) .icon { + -moz-transition-delay: 1.95s; + -webkit-transition-delay: 1.95s; + -ms-transition-delay: 1.95s; + transition-delay: 1.95s; +} + +.features section:nth-child(14) .icon { + -moz-transition-delay: 2.1s; + -webkit-transition-delay: 2.1s; + -ms-transition-delay: 2.1s; + transition-delay: 2.1s; +} + +.features section:nth-child(15) .icon { + -moz-transition-delay: 2.25s; + -webkit-transition-delay: 2.25s; + -ms-transition-delay: 2.25s; + transition-delay: 2.25s; +} + +.features section:nth-child(16) .icon { + -moz-transition-delay: 2.4s; + -webkit-transition-delay: 2.4s; + -ms-transition-delay: 2.4s; + transition-delay: 2.4s; +} + +.features section:nth-child(17) .icon { + -moz-transition-delay: 2.55s; + -webkit-transition-delay: 2.55s; + -ms-transition-delay: 2.55s; + transition-delay: 2.55s; +} + +.features section:nth-child(18) .icon { + -moz-transition-delay: 2.7s; + -webkit-transition-delay: 2.7s; + -ms-transition-delay: 2.7s; + transition-delay: 2.7s; +} + +.features section:nth-child(19) .icon { + -moz-transition-delay: 2.85s; + -webkit-transition-delay: 2.85s; + -ms-transition-delay: 2.85s; + transition-delay: 2.85s; +} + +.features section:nth-child(20) .icon { + -moz-transition-delay: 3s; + -webkit-transition-delay: 3s; + -ms-transition-delay: 3s; + transition-delay: 3s; +} + +.features.inactive section .icon { + -moz-transform: scale(0.5); + -webkit-transform: scale(0.5); + -ms-transform: scale(0.5); + transform: scale(0.5); + opacity: 0; +} + +@media screen and (max-width: 980px) { + + .features { + display: block; + } + + .features section { + border-top-width: 1px !important; + border-left-width: 0 !important; + width: 100%; + } + + .features section:first-child { + border-top-width: 0 !important; + } + +} + +@media screen and (max-width: 736px) { + + .features section { + padding: 2.5em 1.5em 0.1em 5.5em; + } + + .features section .icon { + left: 1.5em; + top: 2em; + } + +} + +@media screen and (max-width: 480px) { + + .features section { + padding: 2em 1.5em 0.1em 1.5em; + } + + .features section .icon { + left: 0; + position: relative; + top: 0; + } + +} + +/* Form */ + +form { + margin: 0 0 2em 0; +} + +form > :last-child { + margin-bottom: 0; +} + +form > .fields { + display: -moz-flex; + display: -webkit-flex; + display: -ms-flex; + display: flex; + -moz-flex-wrap: wrap; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + width: calc(100% + 3em); + margin: -1.5em 0 2em -1.5em; +} + +form > .fields > .field { + -moz-flex-grow: 0; + -webkit-flex-grow: 0; + -ms-flex-grow: 0; + flex-grow: 0; + -moz-flex-shrink: 0; + -webkit-flex-shrink: 0; + -ms-flex-shrink: 0; + flex-shrink: 0; + padding: 1.5em 0 0 1.5em; + width: calc(100% - 1.5em); +} + +form > .fields > .field.half { + width: calc(50% - 0.75em); +} + +form > .fields > .field.third { + width: calc(100% / 3 - 0.5em); +} + +form > .fields > .field.quarter { + width: calc(25% - 0.375em); +} + +@media screen and (max-width: 480px) { + + form > .fields { + width: calc(100% + 3em); + margin: -1.5em 0 2em -1.5em; + } + + form > .fields > .field { + padding: 1.5em 0 0 1.5em; + width: calc(100% - 1.5em); + } + + form > .fields > .field.half { + width: calc(100% - 1.5em); + } + + form > .fields > .field.third { + width: calc(100% - 1.5em); + } + + form > .fields > .field.quarter { + width: calc(100% - 1.5em); + } + +} + +label { + color: #ffffff; + font-weight: bold; + line-height: 1.5; + margin: 0 0 0.7em 0; + display: block; + font-size: 1.1em; +} + +input[type="text"], +input[type="password"], +input[type="email"], +input[type="tel"], +select, +textarea { + -moz-appearance: none; + -webkit-appearance: none; + -ms-appearance: none; + appearance: none; + background: rgba(255, 255, 255, 0.05); + border-radius: 0.25em; + border: none; + border: solid 1px rgba(255, 255, 255, 0.15); + color: inherit; + display: block; + outline: 0; + padding: 0 1em; + text-decoration: none; + width: 100%; +} + +input[type="text"]:invalid, +input[type="password"]:invalid, +input[type="email"]:invalid, +input[type="tel"]:invalid, +select:invalid, +textarea:invalid { + box-shadow: none; +} + +input[type="text"]:focus, +input[type="password"]:focus, +input[type="email"]:focus, +input[type="tel"]:focus, +select:focus, +textarea:focus { + border-color: #ffffff; + box-shadow: 0 0 0 1px #ffffff; +} + +select { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' preserveAspectRatio='none' viewBox='0 0 40 40'%3E%3Cpath d='M9.4,12.3l10.4,10.4l10.4-10.4c0.2-0.2,0.5-0.4,0.9-0.4c0.3,0,0.6,0.1,0.9,0.4l3.3,3.3c0.2,0.2,0.4,0.5,0.4,0.9 c0,0.4-0.1,0.6-0.4,0.9L20.7,31.9c-0.2,0.2-0.5,0.4-0.9,0.4c-0.3,0-0.6-0.1-0.9-0.4L4.3,17.3c-0.2-0.2-0.4-0.5-0.4-0.9 c0-0.4,0.1-0.6,0.4-0.9l3.3-3.3c0.2-0.2,0.5-0.4,0.9-0.4S9.1,12.1,9.4,12.3z' fill='rgba(255, 255, 255, 0.15)' /%3E%3C/svg%3E"); + background-size: 1.25rem; + background-repeat: no-repeat; + background-position: calc(100% - 1rem) center; + height: 2.75em; + padding-right: 2.75em; + text-overflow: ellipsis; +} + +select option { + color: #ffffff; + background: #312450; +} + +select:focus::-ms-value { + background-color: transparent; +} + +select::-ms-expand { + display: none; +} + +input[type="text"], +input[type="password"], +input[type="email"], +select { + height: 2.75em; +} + +textarea { + padding: 0.75em 1em; +} + +body.is-ie textarea { + min-height: 10em; +} + +input[type="checkbox"], +input[type="radio"] { + display: block; + /*float: left;*/ + /*margin-right: -2em;*/ + /*opacity: 0;*/ + width: 1em; + z-index: -1; +} + +input[type="checkbox"] + label, +input[type="radio"] + label { + text-decoration: none; + color: rgba(255, 255, 255, 0.55); + cursor: pointer; + display: inline-block; + font-size: 1em; + font-weight: normal; + padding-left: 2.4em; + padding-right: 0.75em; + position: relative; +} + +input[type="checkbox"] + label:before, +input[type="radio"] + label:before { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: inline-block; + font-style: normal; + font-variant: normal; + text-rendering: auto; + line-height: 1; + text-transform: none !important; + font-family: 'Font Awesome 5 Free'; + font-weight: 900; +} + +input[type="checkbox"] + label:before, +input[type="radio"] + label:before { + background: rgba(255, 255, 255, 0.05); + border-radius: 0.25em; + border: solid 1px rgba(255, 255, 255, 0.15); + content: ''; + display: inline-block; + font-size: 0.8em; + height: 2.0625em; + left: 0; + line-height: 2.0625em; + position: absolute; + text-align: center; + top: 0; + width: 2.0625em; +} + +input[type="checkbox"]:checked + label:before, +input[type="radio"]:checked + label:before { + background: #ffffff; + border-color: #ffffff; + color: #b74e91; + content: '\f00c'; +} + +input[type="checkbox"]:focus + label:before, +input[type="radio"]:focus + label:before { + border-color: #ffffff; + box-shadow: 0 0 0 1px #ffffff; +} + +input[type="checkbox"] + label:before { + border-radius: 0.25em; +} + +input[type="radio"] + label:before { + border-radius: 100%; +} + +::-webkit-input-placeholder { + color: rgba(255, 255, 255, 0.35) !important; + opacity: 1.0; +} + +:-moz-placeholder { + color: rgba(255, 255, 255, 0.35) !important; + opacity: 1.0; +} + +::-moz-placeholder { + color: rgba(255, 255, 255, 0.35) !important; + opacity: 1.0; +} + +:-ms-input-placeholder { + color: rgba(255, 255, 255, 0.35) !important; + opacity: 1.0; +} + +/* Icon */ + +.icon { + text-decoration: none; + border-bottom: none; + position: relative; +} + +.icon:before { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: inline-block; + font-style: normal; + font-variant: normal; + text-rendering: auto; + line-height: 1; + text-transform: none !important; + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.icon > .label { + display: none; +} + +.icon:before { + line-height: inherit; +} + +.icon.solid:before { + font-weight: 900; +} + +.icon.brands:before { + font-family: 'Font Awesome 5 Brands'; +} + +.icon.major { + width: 2.5em; + height: 2.5em; + display: block; + background: #ffffff; + border-radius: 100%; + color: #312450; + text-align: center; + line-height: 2.5em; + margin: 0 0 1.3em 0; +} + +.icon.major:before { + font-size: 1.25em; +} + +.wrapper.style1 .icon.major:before { + color: #5e42a6; +} + +.wrapper.style1-alt .icon.major:before { + color: #493382; +} + +.wrapper.style2 .icon.major:before { + color: #5052b5; +} + +.wrapper.style2-alt .icon.major:before { + color: #3e4094; +} + +.wrapper.style3 .icon.major:before { + color: #b74e91; +} + +.wrapper.style3-alt .icon.major:before { + color: #953d75; +} + +/* Image */ + +.image { + border-radius: 0.25em; + border: 0; + display: inline-block; + position: relative; +} + +.image img { + border-radius: 0.25em; + display: block; +} + +.image.left, .image.right { + max-width: 40%; +} + +.image.left img, .image.right img { + width: 100%; +} + +.image.left { + float: left; + margin: 0 1.5em 1em 0; + top: 0.25em; +} + +.image.right { + float: right; + margin: 0 0 1em 1.5em; + top: 0.25em; +} + +.image.fit { + display: block; + margin: 0 0 2em 0; + width: 100%; +} + +.image.fit img { + width: 100%; +} + +.image.main { + display: block; + margin: 0 0 3em 0; + width: 100%; +} + +.image.main img { + width: 100%; +} + +/* List */ + +ol { + list-style: decimal; + margin: 0 0 2em 0; + padding-left: 1.25em; +} + +ol li { + padding-left: 0.25em; +} + +ul { + list-style: disc; + margin: 0 0 2em 0; + padding-left: 1em; +} + +ul li { + padding-left: 0.5em; +} + +ul.alt { + list-style: none; + padding-left: 0; +} + +ul.alt li { + border-top: solid 1px rgba(255, 255, 255, 0.15); + padding: 0.5em 0; +} + +ul.alt li:first-child { + border-top: 0; + padding-top: 0; +} + +dl { + margin: 0 0 2em 0; +} + +dl dt { + display: block; + font-weight: bold; + margin: 0 0 1em 0; +} + +dl dd { + margin-left: 2em; +} + +/* Actions */ + +ul.actions { + display: -moz-flex; + display: -webkit-flex; + display: -ms-flex; + display: flex; + cursor: default; + list-style: none; + margin-left: -1em; + padding-left: 0; +} + +ul.actions li { + padding: 0 0 0 1em; + vertical-align: middle; +} + +ul.actions.special { + -moz-justify-content: center; + -webkit-justify-content: center; + -ms-justify-content: center; + justify-content: center; + width: 100%; + margin-left: 0; +} + +ul.actions.special li:first-child { + padding-left: 0; +} + +ul.actions.stacked { + -moz-flex-direction: column; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + margin-left: 0; +} + +ul.actions.stacked li { + padding: 1.3em 0 0 0; +} + +ul.actions.stacked li:first-child { + padding-top: 0; +} + +ul.actions.fit { + width: calc(100% + 1em); +} + +ul.actions.fit li { + -moz-flex-grow: 1; + -webkit-flex-grow: 1; + -ms-flex-grow: 1; + flex-grow: 1; + -moz-flex-shrink: 1; + -webkit-flex-shrink: 1; + -ms-flex-shrink: 1; + flex-shrink: 1; + width: 100%; +} + +ul.actions.fit li > * { + width: 100%; +} + +ul.actions.fit.stacked { + width: 100%; +} + +@media screen and (max-width: 480px) { + + ul.actions:not(.fixed) { + -moz-flex-direction: column; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + margin-left: 0; + width: 100% !important; + } + + ul.actions:not(.fixed) li { + -moz-flex-grow: 1; + -webkit-flex-grow: 1; + -ms-flex-grow: 1; + flex-grow: 1; + -moz-flex-shrink: 1; + -webkit-flex-shrink: 1; + -ms-flex-shrink: 1; + flex-shrink: 1; + padding: 1em 0 0 0; + text-align: center; + width: 100%; + } + + ul.actions:not(.fixed) li > * { + width: 100%; + } + + ul.actions:not(.fixed) li:first-child { + padding-top: 0; + } + + ul.actions:not(.fixed) li input[type="submit"], + ul.actions:not(.fixed) li input[type="reset"], + ul.actions:not(.fixed) li input[type="button"], + ul.actions:not(.fixed) li button, + ul.actions:not(.fixed) li .button { + width: 100%; + } + + ul.actions:not(.fixed) li input[type="submit"].icon:before, + ul.actions:not(.fixed) li input[type="reset"].icon:before, + ul.actions:not(.fixed) li input[type="button"].icon:before, + ul.actions:not(.fixed) li button.icon:before, + ul.actions:not(.fixed) li .button.icon:before { + margin-left: -0.5rem; + } + +} + +/* Contact */ + +ul.contact { + list-style: none; + padding: 0; +} + +ul.contact > li { + padding: 0; + margin: 1.5em 0 0 0; +} + +ul.contact > li:first-child { + margin-top: 0; +} + +/* Icons */ + +ul.icons { + cursor: default; + list-style: none; + padding-left: 0; +} + +ul.icons li { + display: inline-block; + padding: 0 0.75em 0 0; +} + +ul.icons li:last-child { + padding-right: 0; +} + +ul.icons li > a, ul.icons li > span { + border: 0; +} + +ul.icons li > a .label, ul.icons li > span .label { + display: none; +} + +/* Menu */ + +ul.menu { + list-style: none; + padding: 0; +} + +ul.menu > li { + border-left: solid 1px rgba(255, 255, 255, 0.15); + display: inline-block; + line-height: 1; + margin-left: 1.5em; + padding: 0 0 0 1.5em; +} + +ul.menu > li:first-child { + border-left: 0; + margin: 0; + padding-left: 0; +} + +@media screen and (max-width: 480px) { + + ul.menu > li { + border-left: 0; + display: block; + line-height: inherit; + margin: 0.5em 0 0 0; + padding-left: 0; + } + +} + +/* Section/Article */ + +section.special, article.special { + text-align: center; +} + +header p { + color: rgba(255, 255, 255, 0.35); + position: relative; + margin: 0 0 1.5em 0; +} + +header h2 + p { + font-size: 1.25em; + margin-top: -1em; + line-height: 1.5em; +} + +header h3 + p { + font-size: 1.1em; + margin-top: -0.8em; + line-height: 1.5em; +} + +header h4 + p, +header h5 + p, +header h6 + p { + font-size: 0.9em; + margin-top: -0.6em; + line-height: 1.5em; +} + +/* Split */ + +.split { + display: -moz-flex; + display: -webkit-flex; + display: -ms-flex; + display: flex; +} + +.split > * { + width: calc(50% - 2.5em); +} + +.split > :nth-child(2n - 1) { + padding-right: 2.5em; + border-right: solid 1px rgba(255, 255, 255, 0.15); +} + +.split > :nth-child(2n) { + padding-left: 2.5em; +} + +.split.style1 > :nth-child(2n - 1) { + width: calc(66.66666% - 2.5em); +} + +.split.style1 > :nth-child(2n) { + width: calc(33.33333% - 2.5em); +} + +@media screen and (max-width: 1680px) { + + .split > * { + width: calc(50% - 2em); + } + + .split > :nth-child(2n - 1) { + padding-right: 2em; + } + + .split > :nth-child(2n) { + padding-left: 2em; + } + + .split.style1 > :nth-child(2n - 1) { + width: calc(66.66666% - 2em); + } + + .split.style1 > :nth-child(2n) { + width: calc(33.33333% - 2em); + } + +} + +@media screen and (max-width: 980px) { + + .split { + display: block; + } + + .split > * { + border-top: solid 1px rgba(255, 255, 255, 0.15); + margin: 4em 0 0 0; + padding: 4em 0 0 0; + width: 100% !important; + } + + .split > :nth-child(2n - 1) { + border-right: 0; + padding-right: 0; + } + + .split > :nth-child(2n) { + padding-left: 0; + } + + .split > :first-child { + border-top: 0; + margin-top: 0; + padding-top: 0; + } + +} + +@media screen and (max-width: 736px) { + + .split > * { + margin: 3em 0 0 0; + padding: 3em 0 0 0; + } + +} + +/* Spotlights */ + +.spotlights > section { + display: -moz-flex; + display: -webkit-flex; + display: -ms-flex; + display: flex; + -moz-flex-direction: row; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + min-height: 22.5em; +} + +body.is-ie .spotlights > section { + min-height: 0; +} + +.spotlights > section > .image { + background-position: center center; + background-size: cover; + border-radius: 0; + display: block; + position: relative; + width: 25em; +} + +.spotlights > section > .image img { + border-radius: 0; + display: block; +} + +.spotlights > section > .image:before { + -moz-transition: opacity 1s ease; + -webkit-transition: opacity 1s ease; + -ms-transition: opacity 1s ease; + transition: opacity 1s ease; + background: rgba(49, 36, 80, 0.9); + content: ''; + display: block; + height: 100%; + left: 0; + opacity: 0; + position: absolute; + top: 0; + width: 100%; +} + +.spotlights > section > .content { + padding: 4em 5em 2em 5em; + display: -moz-flex; + display: -webkit-flex; + display: -ms-flex; + display: flex; + -moz-flex-direction: column; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -moz-justify-content: center; + -webkit-justify-content: center; + -ms-justify-content: center; + justify-content: center; + width: 50em; + -ms-flex: 1; +} + +.spotlights > section > .content > .inner { + -moz-transform: translateX(0) translateY(0); + -webkit-transform: translateX(0) translateY(0); + -ms-transform: translateX(0) translateY(0); + transform: translateX(0) translateY(0); + -moz-transition: opacity 1s ease, -moz-transform 1s ease; + -webkit-transition: opacity 1s ease, -webkit-transform 1s ease; + -ms-transition: opacity 1s ease, -ms-transform 1s ease; + transition: opacity 1s ease, transform 1s ease; + opacity: 1; +} + +.spotlights > section:nth-child(2) { + background-color: rgba(0, 0, 0, 0.05); +} + +.spotlights > section:nth-child(3) { + background-color: rgba(0, 0, 0, 0.1); +} + +.spotlights > section.inactive > .image:before, +body.is-preload .spotlights > section > .image:before { + opacity: 1; +} + +.spotlights > section.inactive > .content > .inner, +body.is-preload .spotlights > section > .content > .inner { + -moz-transform: translateX(-1em); + -webkit-transform: translateX(-1em); + -ms-transform: translateX(-1em); + transform: translateX(-1em); + opacity: 0; +} + +@media screen and (max-width: 1680px) { + + .spotlights > section > .content { + padding: 4em 4em 2em 4em; + } + +} + +@media screen and (max-width: 980px) { + + .spotlights > section { + display: block; + } + + .spotlights > section > .image { + width: 100%; + height: 50vh; + } + + .spotlights > section > .content { + width: 100%; + } + + .spotlights > section.inactive > .content > .inner, + body.is-preload .spotlights > section > .content > .inner { + -moz-transform: translateY(1em); + -webkit-transform: translateY(1em); + -ms-transform: translateY(1em); + transform: translateY(1em); + } + +} + +@media screen and (max-width: 736px) { + + .spotlights > section > .image { + height: 50vh; + min-height: 15em; + } + + .spotlights > section > .content { + padding: 3em 2em 1em 2em; + } + +} + +/* Table */ + +.table-wrapper { + -webkit-overflow-scrolling: touch; + overflow-x: auto; +} + +table { + margin: 0 0 2em 0; + width: 100%; +} + +table tbody tr { + border: solid 1px rgba(255, 255, 255, 0.15); + border-left: 0; + border-right: 0; +} + +table tbody tr:nth-child(2n + 1) { + background-color: rgba(255, 255, 255, 0.05); +} + +table td { + padding: 0.75em 0.75em; +} + +table th { + color: #ffffff; + font-size: 1em; + font-weight: bold; + padding: 0 0.75em 0.75em 0.75em; + text-align: left; +} + +table thead { + border-bottom: solid 2px rgba(255, 255, 255, 0.15); +} + +table tfoot { + border-top: solid 2px rgba(255, 255, 255, 0.15); +} + +table.alt { + border-collapse: separate; +} + +table.alt tbody tr td { + border: solid 1px rgba(255, 255, 255, 0.15); + border-left-width: 0; + border-top-width: 0; +} + +table.alt tbody tr td:first-child { + border-left-width: 1px; +} + +table.alt tbody tr:first-child td { + border-top-width: 1px; +} + +table.alt thead { + border-bottom: 0; +} + +table.alt tfoot { + border-top: 0; +} + +/* Wrapper */ + +.wrapper { + position: relative; +} + +.wrapper > .inner { + padding: 5em 5em 3em 5em; + max-width: 100%; + width: 75em; +} + +@media screen and (max-width: 1680px) { + + .wrapper > .inner { + padding: 4em 4em 2em 4em; + } + +} + +@media screen and (max-width: 1280px) { + + .wrapper > .inner { + width: 100%; + } + +} + +@media screen and (max-width: 736px) { + + .wrapper > .inner { + padding: 3em 2em 1em 2em; + } + +} + +.wrapper.alt { + background-color: #261c3e; +} + +.wrapper.style1 { + background-color: #5e42a6; +} + +.wrapper.style1-alt { + background-color: #493382; +} + +.wrapper.style2 { + background-color: #5052b5; +} + +.wrapper.style2-alt { + background-color: #3e4094; +} + +.wrapper.style3 { + background-color: #b74e91; +} + +.wrapper.style3-alt { + background-color: #953d75; +} + +.wrapper.fullscreen { + display: -moz-flex; + display: -webkit-flex; + display: -ms-flex; + display: flex; + -moz-flex-direction: column; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -moz-justify-content: center; + -webkit-justify-content: center; + -ms-justify-content: center; + justify-content: center; + min-height: 100vh; +} + +body.is-ie .wrapper.fullscreen { + height: 100vh; +} + +@media screen and (max-width: 1280px) { + + .wrapper.fullscreen { + min-height: calc(100vh - 2.5em); + } + + body.is-ie .wrapper.fullscreen { + height: calc(100vh - 2.5em); + } + +} + +@media screen and (max-width: 736px) { + + .wrapper.fullscreen { + padding: 2em 0; + min-height: 0; + } + + body.is-ie .wrapper.fullscreen { + height: auto; + } + +} + +.wrapper.fade-up > .inner { + -moz-transform: translateY(0); + -webkit-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + -moz-transition: opacity 1s ease, -moz-transform 1s ease; + -webkit-transition: opacity 1s ease, -webkit-transform 1s ease; + -ms-transition: opacity 1s ease, -ms-transform 1s ease; + transition: opacity 1s ease, transform 1s ease; + opacity: 1.0; +} + +.wrapper.fade-up.inactive > .inner, +body.is-preload .wrapper.fade-up > .inner { + opacity: 0; + -moz-transform: translateY(1em); + -webkit-transform: translateY(1em); + -ms-transform: translateY(1em); + transform: translateY(1em); +} + +.wrapper.fade-down > .inner { + -moz-transform: translateY(0); + -webkit-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + -moz-transition: opacity 1s ease, -moz-transform 1s ease; + -webkit-transition: opacity 1s ease, -webkit-transform 1s ease; + -ms-transition: opacity 1s ease, -ms-transform 1s ease; + transition: opacity 1s ease, transform 1s ease; + opacity: 1.0; +} + +.wrapper.fade-down.inactive > .inner, +body.is-preload .wrapper.fade-down > .inner { + opacity: 0; + -moz-transform: translateY(-1em); + -webkit-transform: translateY(-1em); + -ms-transform: translateY(-1em); + transform: translateY(-1em); +} + +.wrapper.fade > .inner { + -moz-transition: opacity 1s ease; + -webkit-transition: opacity 1s ease; + -ms-transition: opacity 1s ease; + transition: opacity 1s ease; + opacity: 1.0; +} + +.wrapper.fade.inactive > .inner, +body.is-preload .wrapper.fade > .inner { + opacity: 0; +} + +/* Header */ + +#header { + display: -moz-flex; + display: -webkit-flex; + display: -ms-flex; + display: flex; + background-color: #5e42a6; + cursor: default; + padding: 1.75em 2em; +} + +#header > .title { + border: 0; + color: #ffffff; + display: block; + font-size: 1.25em; + font-weight: bold; +} + +#header > nav { + -moz-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + text-align: right; +} + +#header > nav > ul { + margin: 0; + padding: 0; +} + +#header > nav > ul > li { + display: inline-block; + margin-left: 1.75em; + padding: 0; + vertical-align: middle; +} + +#header > nav > ul > li:first-child { + margin-left: 0; +} + +#header > nav > ul > li a { + border: 0; + color: rgba(255, 255, 255, 0.35); + display: inline-block; + font-size: 0.6em; + font-weight: bold; + letter-spacing: 0.25em; + text-transform: uppercase; +} + +#header > nav > ul > li a:hover { + color: rgba(255, 255, 255, 0.55); +} + +#header > nav > ul > li a.active { + color: #ffffff; +} + +@media screen and (max-width: 736px) { + + #header { + padding: 1em 2em; + } + +} + +@media screen and (max-width: 480px) { + + #header { + display: block; + padding: 0 2em; + text-align: left; + } + + #header .title { + font-size: 1.25em; + padding: 1em 0; + } + + #header > nav { + border-top: solid 1px rgba(255, 255, 255, 0.15); + text-align: inherit; + } + + #header > nav > ul > li { + margin-left: 1.5em; + } + + #header > nav > ul > li a { + height: 6em; + line-height: 6em; + } + +} + +/* Wrapper (main) */ + +#sidebar + #wrapper { + margin-left: 18em; +} + +@media screen and (max-width: 1280px) { + + #sidebar + #wrapper { + margin-left: 0; + padding-top: 3.5em; + } + +} + +@media screen and (max-width: 736px) { + + #sidebar + #wrapper { + padding-top: 0; + } + +} + +#header + #wrapper > .wrapper > .inner { + margin: 0 auto; +} + +/* Footer */ + +#sidebar + #wrapper + #footer { + margin-left: 18em; +} + +@media screen and (max-width: 1280px) { + + #sidebar + #wrapper + #footer { + margin-left: 0; + } + +} + +#footer > .inner a { + border-bottom-color: rgba(255, 255, 255, 0.15); +} + +#footer > .inner a:hover { + border-bottom-color: transparent; +} + +#footer > .inner .menu { + font-size: 0.8em; + color: rgba(255, 255, 255, 0.15); +} + +#header + #wrapper + #footer > .inner { + margin: 0 auto; +} + +/* Sidebar */ + +#sidebar { + padding: 2.5em 2.5em 0.5em 2.5em; + background: #312450; + cursor: default; + height: 100vh; + left: 0; + overflow-x: hidden; + overflow-y: auto; + position: fixed; + text-align: right; + top: 0; + width: 18em; + z-index: 10000; +} + +#sidebar > .inner { + display: -moz-flex; + display: -webkit-flex; + display: -ms-flex; + display: flex; + -moz-flex-direction: column; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -moz-justify-content: center; + -webkit-justify-content: center; + -ms-justify-content: center; + justify-content: center; + -moz-transform: translateY(0); + -webkit-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + -moz-transition: opacity 1s ease; + -webkit-transition: opacity 1s ease; + -ms-transition: opacity 1s ease; + transition: opacity 1s ease; + min-height: 100%; + opacity: 1; + width: 100%; +} + +body.is-ie #sidebar > .inner { + height: 100%; +} + +#sidebar nav > ul { + list-style: none; + padding: 0; +} + +#sidebar nav > ul > li { + -moz-transform: translateY(0); + -webkit-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + -moz-transition: opacity 0.15s ease, -moz-transform 0.75s ease; + -webkit-transition: opacity 0.15s ease, -webkit-transform 0.75s ease; + -ms-transition: opacity 0.15s ease, -ms-transform 0.75s ease; + transition: opacity 0.15s ease, transform 0.75s ease; + margin: 1.5em 0 0 0; + opacity: 1; + padding: 0; + position: relative; +} + +#sidebar nav > ul > li:first-child { + margin: 0; +} + +#sidebar nav > ul > li:nth-child(1) { + -moz-transition-delay: 0.45s; + -webkit-transition-delay: 0.45s; + -ms-transition-delay: 0.45s; + transition-delay: 0.45s; +} + +#sidebar nav > ul > li:nth-child(2) { + -moz-transition-delay: 0.65s; + -webkit-transition-delay: 0.65s; + -ms-transition-delay: 0.65s; + transition-delay: 0.65s; +} + +#sidebar nav > ul > li:nth-child(3) { + -moz-transition-delay: 0.85s; + -webkit-transition-delay: 0.85s; + -ms-transition-delay: 0.85s; + transition-delay: 0.85s; +} + +#sidebar nav > ul > li:nth-child(4) { + -moz-transition-delay: 1.05s; + -webkit-transition-delay: 1.05s; + -ms-transition-delay: 1.05s; + transition-delay: 1.05s; +} + +#sidebar nav > ul > li:nth-child(5) { + -moz-transition-delay: 1.25s; + -webkit-transition-delay: 1.25s; + -ms-transition-delay: 1.25s; + transition-delay: 1.25s; +} + +#sidebar nav > ul > li:nth-child(6) { + -moz-transition-delay: 1.45s; + -webkit-transition-delay: 1.45s; + -ms-transition-delay: 1.45s; + transition-delay: 1.45s; +} + +#sidebar nav > ul > li:nth-child(7) { + -moz-transition-delay: 1.65s; + -webkit-transition-delay: 1.65s; + -ms-transition-delay: 1.65s; + transition-delay: 1.65s; +} + +#sidebar nav > ul > li:nth-child(8) { + -moz-transition-delay: 1.85s; + -webkit-transition-delay: 1.85s; + -ms-transition-delay: 1.85s; + transition-delay: 1.85s; +} + +#sidebar nav > ul > li:nth-child(9) { + -moz-transition-delay: 2.05s; + -webkit-transition-delay: 2.05s; + -ms-transition-delay: 2.05s; + transition-delay: 2.05s; +} + +#sidebar nav > ul > li:nth-child(10) { + -moz-transition-delay: 2.25s; + -webkit-transition-delay: 2.25s; + -ms-transition-delay: 2.25s; + transition-delay: 2.25s; +} + +#sidebar nav > ul > li:nth-child(11) { + -moz-transition-delay: 2.45s; + -webkit-transition-delay: 2.45s; + -ms-transition-delay: 2.45s; + transition-delay: 2.45s; +} + +#sidebar nav > ul > li:nth-child(12) { + -moz-transition-delay: 2.65s; + -webkit-transition-delay: 2.65s; + -ms-transition-delay: 2.65s; + transition-delay: 2.65s; +} + +#sidebar nav > ul > li:nth-child(13) { + -moz-transition-delay: 2.85s; + -webkit-transition-delay: 2.85s; + -ms-transition-delay: 2.85s; + transition-delay: 2.85s; +} + +#sidebar nav > ul > li:nth-child(14) { + -moz-transition-delay: 3.05s; + -webkit-transition-delay: 3.05s; + -ms-transition-delay: 3.05s; + transition-delay: 3.05s; +} + +#sidebar nav > ul > li:nth-child(15) { + -moz-transition-delay: 3.25s; + -webkit-transition-delay: 3.25s; + -ms-transition-delay: 3.25s; + transition-delay: 3.25s; +} + +#sidebar nav > ul > li:nth-child(16) { + -moz-transition-delay: 3.45s; + -webkit-transition-delay: 3.45s; + -ms-transition-delay: 3.45s; + transition-delay: 3.45s; +} + +#sidebar nav > ul > li:nth-child(17) { + -moz-transition-delay: 3.65s; + -webkit-transition-delay: 3.65s; + -ms-transition-delay: 3.65s; + transition-delay: 3.65s; +} + +#sidebar nav > ul > li:nth-child(18) { + -moz-transition-delay: 3.85s; + -webkit-transition-delay: 3.85s; + -ms-transition-delay: 3.85s; + transition-delay: 3.85s; +} + +#sidebar nav > ul > li:nth-child(19) { + -moz-transition-delay: 4.05s; + -webkit-transition-delay: 4.05s; + -ms-transition-delay: 4.05s; + transition-delay: 4.05s; +} + +#sidebar nav > ul > li:nth-child(20) { + -moz-transition-delay: 4.25s; + -webkit-transition-delay: 4.25s; + -ms-transition-delay: 4.25s; + transition-delay: 4.25s; +} + +#sidebar nav a { + -moz-transition: color 0.2s ease; + -webkit-transition: color 0.2s ease; + -ms-transition: color 0.2s ease; + transition: color 0.2s ease; + border: 0; + color: rgba(255, 255, 255, 0.35); + display: block; + font-size: 0.6em; + font-weight: bold; + letter-spacing: 0.25em; + line-height: 1.75; + outline: 0; + padding: 1.35em 0; + position: relative; + text-decoration: none; + text-transform: uppercase; +} + +#sidebar nav a:before, #sidebar nav a:after { + border-radius: 0.2em; + bottom: 0; + content: ''; + height: 0.2em; + position: absolute; + right: 0; + width: 100%; +} + +#sidebar nav a:before { + background: #3c2c62; +} + +#sidebar nav a:after { + background-image: -moz-linear-gradient(to right, #5e42a6, #b74e91); + background-image: -webkit-linear-gradient(to right, #5e42a6, #b74e91); + background-image: -ms-linear-gradient(to right, #5e42a6, #b74e91); + background-image: linear-gradient(to right, #5e42a6, #b74e91); + -moz-transition: max-width 0.2s ease; + -webkit-transition: max-width 0.2s ease; + -ms-transition: max-width 0.2s ease; + transition: max-width 0.2s ease; + max-width: 0; +} + +#sidebar nav a:hover { + color: rgba(255, 255, 255, 0.55); +} + +#sidebar nav a.active { + color: #ffffff; +} + +#sidebar nav a.active:after { + max-width: 100%; +} + +body.is-preload #sidebar > .inner { + opacity: 0; +} + +body.is-preload #sidebar nav ul li { + -moz-transform: translateY(2em); + -webkit-transform: translateY(2em); + -ms-transform: translateY(2em); + transform: translateY(2em); + opacity: 0; +} + +@media screen and (max-width: 1280px) { + + #sidebar { + height: 3.5em; + left: 0; + line-height: 3.5em; + overflow: hidden; + padding: 0; + text-align: center; + top: 0; + width: 100%; + } + + #sidebar > .inner { + -moz-flex-direction: row; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -moz-align-items: -moz-stretch; + -webkit-align-items: -webkit-stretch; + -ms-align-items: -ms-stretch; + align-items: stretch; + height: inherit; + line-height: inherit; + } + + #sidebar nav { + height: inherit; + line-height: inherit; + } + + #sidebar nav ul { + display: -moz-flex; + display: -webkit-flex; + display: -ms-flex; + display: flex; + height: inherit; + line-height: inherit; + margin: 0; + } + + #sidebar nav ul li { + display: block; + height: inherit; + line-height: inherit; + margin: 0 0 0 2em; + padding: 0; + } + + #sidebar nav a { + height: inherit; + line-height: inherit; + padding: 0; + } + + #sidebar nav a:after { + background-image: none; + background-color: #b74e91; + } + +} + +@media screen and (max-width: 736px) { + + #sidebar { + display: none; + } + +} + +/* Intro */ + +#intro { + background-attachment: fixed; + background-image: url("images/intro.svg"); + background-position: top right; + background-repeat: no-repeat; + background-size: 100% 100%; +} + +#intro p { + font-size: 1.25em; +} + +@media screen and (max-width: 980px) { + + #intro p br { + display: none; + } + +} + +@media screen and (max-width: 736px) { + + #intro p { + font-size: 1em; + } + +} + +@media screen and (max-width: 1280px) { + + #intro { + background-attachment: scroll; + } + +} + +img.person-image { + object-fit: cover; + border-radius: 100%; + vertical-align: middle; + margin-bottom: 0.5em; + margin-right: 0.5em; +} + +img.medium { + width: 128px; + height: 128px; +} + +img.small { + width: 128px; + height: 128px; +} + +img.tiny { + width: 64px; + height: 64px; +} + +span.label { + font-size: 75%; + background-color: black; + color: lightgrey; + padding: 0.25em 0.5em; + margin: 0 1em; + border-radius: 0.33em; +} diff --git a/source/orm/library/static/library/assets/css/noscript.css b/source/orm/library/static/library/assets/css/noscript.css new file mode 100644 index 0000000..a086824 --- /dev/null +++ b/source/orm/library/static/library/assets/css/noscript.css @@ -0,0 +1,43 @@ +/* + Hyperspace by HTML5 UP + html5up.net | @ajlkn + Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +*/ + +/* Spotlights */ + + .spotlights > section > .image:before { + opacity: 0 !important; + } + + .spotlights > section > .content > .inner { + -moz-transform: none !important; + -webkit-transform: none !important; + -ms-transform: none !important; + transform: none !important; + opacity: 1 !important; + } + +/* Wrapper */ + + .wrapper > .inner { + opacity: 1 !important; + -moz-transform: none !important; + -webkit-transform: none !important; + -ms-transform: none !important; + transform: none !important; + } + +/* Sidebar */ + + #sidebar > .inner { + opacity: 1 !important; + } + + #sidebar nav > ul > li { + -moz-transform: none !important; + -webkit-transform: none !important; + -ms-transform: none !important; + transform: none !important; + opacity: 1 !important; + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/js/breakpoints.min.js b/source/orm/library/static/library/assets/js/breakpoints.min.js new file mode 100644 index 0000000..32419cc --- /dev/null +++ b/source/orm/library/static/library/assets/js/breakpoints.min.js @@ -0,0 +1,2 @@ +/* breakpoints.js v1.0 | @ajlkn | MIT licensed */ +var breakpoints=function(){"use strict";function e(e){t.init(e)}var t={list:null,media:{},events:[],init:function(e){t.list=e,window.addEventListener("resize",t.poll),window.addEventListener("orientationchange",t.poll),window.addEventListener("load",t.poll),window.addEventListener("fullscreenchange",t.poll)},active:function(e){var n,a,s,i,r,d,c;if(!(e in t.media)){if(">="==e.substr(0,2)?(a="gte",n=e.substr(2)):"<="==e.substr(0,2)?(a="lte",n=e.substr(2)):">"==e.substr(0,1)?(a="gt",n=e.substr(1)):"<"==e.substr(0,1)?(a="lt",n=e.substr(1)):"!"==e.substr(0,1)?(a="not",n=e.substr(1)):(a="eq",n=e),n&&n in t.list)if(i=t.list[n],Array.isArray(i)){if(r=parseInt(i[0]),d=parseInt(i[1]),isNaN(r)){if(isNaN(d))return;c=i[1].substr(String(d).length)}else c=i[0].substr(String(r).length);if(isNaN(r))switch(a){case"gte":s="screen";break;case"lte":s="screen and (max-width: "+d+c+")";break;case"gt":s="screen and (min-width: "+(d+1)+c+")";break;case"lt":s="screen and (max-width: -1px)";break;case"not":s="screen and (min-width: "+(d+1)+c+")";break;default:s="screen and (max-width: "+d+c+")"}else if(isNaN(d))switch(a){case"gte":s="screen and (min-width: "+r+c+")";break;case"lte":s="screen";break;case"gt":s="screen and (max-width: -1px)";break;case"lt":s="screen and (max-width: "+(r-1)+c+")";break;case"not":s="screen and (max-width: "+(r-1)+c+")";break;default:s="screen and (min-width: "+r+c+")"}else switch(a){case"gte":s="screen and (min-width: "+r+c+")";break;case"lte":s="screen and (max-width: "+d+c+")";break;case"gt":s="screen and (min-width: "+(d+1)+c+")";break;case"lt":s="screen and (max-width: "+(r-1)+c+")";break;case"not":s="screen and (max-width: "+(r-1)+c+"), screen and (min-width: "+(d+1)+c+")";break;default:s="screen and (min-width: "+r+c+") and (max-width: "+d+c+")"}}else s="("==i.charAt(0)?"screen and "+i:i;t.media[e]=!!s&&s}return t.media[e]!==!1&&window.matchMedia(t.media[e]).matches},on:function(e,n){t.events.push({query:e,handler:n,state:!1}),t.active(e)&&n()},poll:function(){var e,n;for(e=0;e0:!!("ontouchstart"in window),e.mobile="wp"==e.os||"android"==e.os||"ios"==e.os||"bb"==e.os}};return e.init(),e}();!function(e,n){"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?module.exports=n():e.browser=n()}(this,function(){return browser}); diff --git a/source/orm/library/static/library/assets/js/jquery.min.js b/source/orm/library/static/library/assets/js/jquery.min.js new file mode 100644 index 0000000..a1c07fd --- /dev/null +++ b/source/orm/library/static/library/assets/js/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],E=C.document,r=Object.getPrototypeOf,s=t.slice,g=t.concat,u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.4.1",k=function(e,t){return new k.fn.init(e,t)},p=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function d(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp($),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+$),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ne=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(m.childNodes),m.childNodes),t[m.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&((e?e.ownerDocument||e:m)!==C&&T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!A[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&U.test(t)){(s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=k),o=(l=h(t)).length;while(o--)l[o]="#"+s+" "+xe(l[o]);c=l.join(","),f=ee.test(t)&&ye(e.parentNode)||e}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){A(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[k]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:m;return r!==C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),m!==C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=k,!C.getElementsByName||!C.getElementsByName(k).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+k+"-]").length||v.push("~="),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+k+"+*").length||v.push(".#.+[+~]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",$)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument===m&&y(m,e)?-1:t===C||t.ownerDocument===m&&y(m,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===C?-1:t===C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]===m?-1:s[r]===m?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if((e.ownerDocument||e)!==C&&T(e),d.matchesSelector&&E&&!A[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){A(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=p[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&p(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?k.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?k.grep(e,function(e){return e===n!==r}):"string"!=typeof n?k.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(k.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:L.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof k?t[0]:t,k.merge(this,k.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),D.test(r[1])&&k.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(k):k.makeArray(e,this)}).prototype=k.fn,q=k(E);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}k.fn.extend({has:function(e){var t=k(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?k.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;nx",y.noCloneChecked=!!me.cloneNode(!0).lastChild.defaultValue;var Te=/^key/,Ce=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ee=/^([^.]*)(?:\.(.+)|)/;function ke(){return!0}function Se(){return!1}function Ne(e,t){return e===function(){try{return E.activeElement}catch(e){}}()==("focus"===t)}function Ae(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Ae(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Se;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return k().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=k.guid++)),e.each(function(){k.event.add(this,t,i,r,n)})}function De(e,i,o){o?(Q.set(e,i,!1),k.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=Q.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(k.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),Q.set(this,i,r),t=o(this,i),this[i](),r!==(n=Q.get(this,i))||t?Q.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n.value}else r.length&&(Q.set(this,i,{value:k.event.trigger(k.extend(r[0],k.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Q.get(e,i)&&k.event.add(e,i,ke)}k.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.get(t);if(v){n.handler&&(n=(o=n).handler,i=o.selector),i&&k.find.matchesSelector(ie,i),n.guid||(n.guid=k.guid++),(u=v.events)||(u=v.events={}),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof k&&k.event.triggered!==e.type?k.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(R)||[""]).length;while(l--)d=g=(s=Ee.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=k.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=k.event.special[d]||{},c=k.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&k.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),k.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.hasData(e)&&Q.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(R)||[""]).length;while(l--)if(d=g=(s=Ee.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){f=k.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||k.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)k.event.remove(e,d+t[l],n,r,!0);k.isEmptyObject(u)&&Q.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=k.event.fix(e),u=new Array(arguments.length),l=(Q.get(this,"events")||{})[s.type]||[],c=k.event.special[s.type]||{};for(u[0]=s,t=1;t\x20\t\r\n\f]*)[^>]*)\/>/gi,qe=/\s*$/g;function Oe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&k(e).children("tbody")[0]||e}function Pe(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Re(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Me(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(Q.hasData(e)&&(o=Q.access(e),a=Q.set(t,o),l=o.events))for(i in delete a.handle,a.events={},l)for(n=0,r=l[i].length;n")},clone:function(e,t,n){var r,i,o,a,s,u,l,c=e.cloneNode(!0),f=oe(e);if(!(y.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||k.isXMLDoc(e)))for(a=ve(c),r=0,i=(o=ve(e)).length;r").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Vt,Gt=[],Yt=/(=)\?(?=&|$)|\?\?/;k.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Gt.pop()||k.expando+"_"+kt++;return this[e]=!0,e}}),k.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Yt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Yt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Yt,"$1"+r):!1!==e.jsonp&&(e.url+=(St.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||k.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?k(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Gt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Vt=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Vt.childNodes.length),k.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=D.exec(e))?[t.createElement(i[1])]:(i=we([e],t,o),o&&o.length&&k(o).remove(),k.merge([],i.childNodes)));var r,i,o},k.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(k.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},k.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){k.fn[t]=function(e){return this.on(t,e)}}),k.expr.pseudos.animated=function(t){return k.grep(k.timers,function(e){return t===e.elem}).length},k.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=k.css(e,"position"),c=k(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=k.css(e,"top"),u=k.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,k.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},k.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){k.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===k.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===k.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=k(e).offset()).top+=k.css(e,"borderTopWidth",!0),i.left+=k.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-k.css(r,"marginTop",!0),left:t.left-i.left-k.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===k.css(e,"position"))e=e.offsetParent;return e||ie})}}),k.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;k.fn[t]=function(e){return _(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),k.each(["top","left"],function(e,n){k.cssHooks[n]=ze(y.pixelPosition,function(e,t){if(t)return t=_e(e,n),$e.test(t)?k(e).position()[n]+"px":t})}),k.each({Height:"height",Width:"width"},function(a,s){k.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){k.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return _(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?k.css(e,t,i):k.style(e,t,n,i)},s,n?e:void 0,n)}})}),k.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){k.fn[n]=function(e,t){return 01){for(var r=0;r=i&&o>=t};break;case"bottom":h=function(t,e,n,i,o){return n>=i&&o>=n};break;case"middle":h=function(t,e,n,i,o){return e>=i&&o>=e};break;case"top-only":h=function(t,e,n,i,o){return i>=t&&n>=i};break;case"bottom-only":h=function(t,e,n,i,o){return n>=o&&o>=t};break;default:case"default":h=function(t,e,n,i,o){return n>=i&&o>=t}}return c=function(t){var i,o,l,s,r,a,u=this.state,h=!1,c=this.$element.offset();i=n.height(),o=t+i/2,l=t+i,s=this.$element.outerHeight(),r=c.top+e(this.options.top,s,i),a=c.top+s-e(this.options.bottom,s,i),h=this.test(t,o,l,r,a),h!=u&&(this.state=h,h?this.options.enter&&this.options.enter.apply(this.element):this.options.leave&&this.options.leave.apply(this.element)),this.options.scroll&&this.options.scroll.apply(this.element,[(o-r)/(a-r)])},p={id:a,options:u,test:h,handler:c,state:null,element:this,$element:s,timeoutId:null},o[a]=p,s.data("_scrollexId",p.id),p.options.initialize&&p.options.initialize.apply(this),s},jQuery.fn.unscrollex=function(){var e=t(this);if(0==this.length)return e;if(this.length>1){for(var n=0;n1){for(o=0;o 0) { + + var $sidebar_a = $sidebar.find('a'); + + $sidebar_a + .addClass('scrolly') + .on('click', function() { + + var $this = $(this); + + // External link? Bail. + if ($this.attr('href').charAt(0) != '#') + return; + + // Deactivate all links. + $sidebar_a.removeClass('active'); + + // Activate link *and* lock it (so Scrollex doesn't try to activate other links as we're scrolling to this one's section). + $this + .addClass('active') + .addClass('active-locked'); + + }) + .each(function() { + + var $this = $(this), + id = $this.attr('href'), + $section = $(id); + + // No section for this link? Bail. + if ($section.length < 1) + return; + + // Scrollex. + $section.scrollex({ + mode: 'middle', + top: '-20vh', + bottom: '-20vh', + initialize: function() { + + // Deactivate section. + $section.addClass('inactive'); + + }, + enter: function() { + + // Activate section. + $section.removeClass('inactive'); + + // No locked links? Deactivate all links and activate this section's one. + if ($sidebar_a.filter('.active-locked').length == 0) { + + $sidebar_a.removeClass('active'); + $this.addClass('active'); + + } + + // Otherwise, if this section's link is the one that's locked, unlock it. + else if ($this.hasClass('active-locked')) + $this.removeClass('active-locked'); + + } + }); + + }); + + } + + // Scrolly. + $('.scrolly').scrolly({ + speed: 1000, + offset: function() { + + // If <=large, >small, and sidebar is present, use its height as the offset. + if (breakpoints.active('<=large') + && !breakpoints.active('<=small') + && $sidebar.length > 0) + return $sidebar.height(); + + return 0; + + } + }); + + // Spotlights. + $('.spotlights > section') + .scrollex({ + mode: 'middle', + top: '-10vh', + bottom: '-10vh', + initialize: function() { + + // Deactivate section. + $(this).addClass('inactive'); + + }, + enter: function() { + + // Activate section. + $(this).removeClass('inactive'); + + } + }) + .each(function() { + + var $this = $(this), + $image = $this.find('.image'), + $img = $image.find('img'), + x; + + // Assign image. + $image.css('background-image', 'url(' + $img.attr('src') + ')'); + + // Set background position. + if (x = $img.data('position')) + $image.css('background-position', x); + + // Hide . + $img.hide(); + + }); + + // Features. + $('.features') + .scrollex({ + mode: 'middle', + top: '-20vh', + bottom: '-20vh', + initialize: function() { + + // Deactivate section. + $(this).addClass('inactive'); + + }, + enter: function() { + + // Activate section. + $(this).removeClass('inactive'); + + } + }); + +})(jQuery); \ No newline at end of file diff --git a/source/orm/library/static/library/assets/js/util.js b/source/orm/library/static/library/assets/js/util.js new file mode 100644 index 0000000..bdb8e9f --- /dev/null +++ b/source/orm/library/static/library/assets/js/util.js @@ -0,0 +1,587 @@ +(function($) { + + /** + * Generate an indented list of links from a nav. Meant for use with panel(). + * @return {jQuery} jQuery object. + */ + $.fn.navList = function() { + + var $this = $(this); + $a = $this.find('a'), + b = []; + + $a.each(function() { + + var $this = $(this), + indent = Math.max(0, $this.parents('li').length - 1), + href = $this.attr('href'), + target = $this.attr('target'); + + b.push( + '' + + '' + + $this.text() + + '' + ); + + }); + + return b.join(''); + + }; + + /** + * Panel-ify an element. + * @param {object} userConfig User config. + * @return {jQuery} jQuery object. + */ + $.fn.panel = function(userConfig) { + + // No elements? + if (this.length == 0) + return $this; + + // Multiple elements? + if (this.length > 1) { + + for (var i=0; i < this.length; i++) + $(this[i]).panel(userConfig); + + return $this; + + } + + // Vars. + var $this = $(this), + $body = $('body'), + $window = $(window), + id = $this.attr('id'), + config; + + // Config. + config = $.extend({ + + // Delay. + delay: 0, + + // Hide panel on link click. + hideOnClick: false, + + // Hide panel on escape keypress. + hideOnEscape: false, + + // Hide panel on swipe. + hideOnSwipe: false, + + // Reset scroll position on hide. + resetScroll: false, + + // Reset forms on hide. + resetForms: false, + + // Side of viewport the panel will appear. + side: null, + + // Target element for "class". + target: $this, + + // Class to toggle. + visibleClass: 'visible' + + }, userConfig); + + // Expand "target" if it's not a jQuery object already. + if (typeof config.target != 'jQuery') + config.target = $(config.target); + + // Panel. + + // Methods. + $this._hide = function(event) { + + // Already hidden? Bail. + if (!config.target.hasClass(config.visibleClass)) + return; + + // If an event was provided, cancel it. + if (event) { + + event.preventDefault(); + event.stopPropagation(); + + } + + // Hide. + config.target.removeClass(config.visibleClass); + + // Post-hide stuff. + window.setTimeout(function() { + + // Reset scroll position. + if (config.resetScroll) + $this.scrollTop(0); + + // Reset forms. + if (config.resetForms) + $this.find('form').each(function() { + this.reset(); + }); + + }, config.delay); + + }; + + // Vendor fixes. + $this + .css('-ms-overflow-style', '-ms-autohiding-scrollbar') + .css('-webkit-overflow-scrolling', 'touch'); + + // Hide on click. + if (config.hideOnClick) { + + $this.find('a') + .css('-webkit-tap-highlight-color', 'rgba(0,0,0,0)'); + + $this + .on('click', 'a', function(event) { + + var $a = $(this), + href = $a.attr('href'), + target = $a.attr('target'); + + if (!href || href == '#' || href == '' || href == '#' + id) + return; + + // Cancel original event. + event.preventDefault(); + event.stopPropagation(); + + // Hide panel. + $this._hide(); + + // Redirect to href. + window.setTimeout(function() { + + if (target == '_blank') + window.open(href); + else + window.location.href = href; + + }, config.delay + 10); + + }); + + } + + // Event: Touch stuff. + $this.on('touchstart', function(event) { + + $this.touchPosX = event.originalEvent.touches[0].pageX; + $this.touchPosY = event.originalEvent.touches[0].pageY; + + }) + + $this.on('touchmove', function(event) { + + if ($this.touchPosX === null + || $this.touchPosY === null) + return; + + var diffX = $this.touchPosX - event.originalEvent.touches[0].pageX, + diffY = $this.touchPosY - event.originalEvent.touches[0].pageY, + th = $this.outerHeight(), + ts = ($this.get(0).scrollHeight - $this.scrollTop()); + + // Hide on swipe? + if (config.hideOnSwipe) { + + var result = false, + boundary = 20, + delta = 50; + + switch (config.side) { + + case 'left': + result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX > delta); + break; + + case 'right': + result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX < (-1 * delta)); + break; + + case 'top': + result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY > delta); + break; + + case 'bottom': + result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY < (-1 * delta)); + break; + + default: + break; + + } + + if (result) { + + $this.touchPosX = null; + $this.touchPosY = null; + $this._hide(); + + return false; + + } + + } + + // Prevent vertical scrolling past the top or bottom. + if (($this.scrollTop() < 0 && diffY < 0) + || (ts > (th - 2) && ts < (th + 2) && diffY > 0)) { + + event.preventDefault(); + event.stopPropagation(); + + } + + }); + + // Event: Prevent certain events inside the panel from bubbling. + $this.on('click touchend touchstart touchmove', function(event) { + event.stopPropagation(); + }); + + // Event: Hide panel if a child anchor tag pointing to its ID is clicked. + $this.on('click', 'a[href="#' + id + '"]', function(event) { + + event.preventDefault(); + event.stopPropagation(); + + config.target.removeClass(config.visibleClass); + + }); + + // Body. + + // Event: Hide panel on body click/tap. + $body.on('click touchend', function(event) { + $this._hide(event); + }); + + // Event: Toggle. + $body.on('click', 'a[href="#' + id + '"]', function(event) { + + event.preventDefault(); + event.stopPropagation(); + + config.target.toggleClass(config.visibleClass); + + }); + + // Window. + + // Event: Hide on ESC. + if (config.hideOnEscape) + $window.on('keydown', function(event) { + + if (event.keyCode == 27) + $this._hide(event); + + }); + + return $this; + + }; + + /** + * Apply "placeholder" attribute polyfill to one or more forms. + * @return {jQuery} jQuery object. + */ + $.fn.placeholder = function() { + + // Browser natively supports placeholders? Bail. + if (typeof (document.createElement('input')).placeholder != 'undefined') + return $(this); + + // No elements? + if (this.length == 0) + return $this; + + // Multiple elements? + if (this.length > 1) { + + for (var i=0; i < this.length; i++) + $(this[i]).placeholder(); + + return $this; + + } + + // Vars. + var $this = $(this); + + // Text, TextArea. + $this.find('input[type=text],textarea') + .each(function() { + + var i = $(this); + + if (i.val() == '' + || i.val() == i.attr('placeholder')) + i + .addClass('polyfill-placeholder') + .val(i.attr('placeholder')); + + }) + .on('blur', function() { + + var i = $(this); + + if (i.attr('name').match(/-polyfill-field$/)) + return; + + if (i.val() == '') + i + .addClass('polyfill-placeholder') + .val(i.attr('placeholder')); + + }) + .on('focus', function() { + + var i = $(this); + + if (i.attr('name').match(/-polyfill-field$/)) + return; + + if (i.val() == i.attr('placeholder')) + i + .removeClass('polyfill-placeholder') + .val(''); + + }); + + // Password. + $this.find('input[type=password]') + .each(function() { + + var i = $(this); + var x = $( + $('
') + .append(i.clone()) + .remove() + .html() + .replace(/type="password"/i, 'type="text"') + .replace(/type=password/i, 'type=text') + ); + + if (i.attr('id') != '') + x.attr('id', i.attr('id') + '-polyfill-field'); + + if (i.attr('name') != '') + x.attr('name', i.attr('name') + '-polyfill-field'); + + x.addClass('polyfill-placeholder') + .val(x.attr('placeholder')).insertAfter(i); + + if (i.val() == '') + i.hide(); + else + x.hide(); + + i + .on('blur', function(event) { + + event.preventDefault(); + + var x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]'); + + if (i.val() == '') { + + i.hide(); + x.show(); + + } + + }); + + x + .on('focus', function(event) { + + event.preventDefault(); + + var i = x.parent().find('input[name=' + x.attr('name').replace('-polyfill-field', '') + ']'); + + x.hide(); + + i + .show() + .focus(); + + }) + .on('keypress', function(event) { + + event.preventDefault(); + x.val(''); + + }); + + }); + + // Events. + $this + .on('submit', function() { + + $this.find('input[type=text],input[type=password],textarea') + .each(function(event) { + + var i = $(this); + + if (i.attr('name').match(/-polyfill-field$/)) + i.attr('name', ''); + + if (i.val() == i.attr('placeholder')) { + + i.removeClass('polyfill-placeholder'); + i.val(''); + + } + + }); + + }) + .on('reset', function(event) { + + event.preventDefault(); + + $this.find('select') + .val($('option:first').val()); + + $this.find('input,textarea') + .each(function() { + + var i = $(this), + x; + + i.removeClass('polyfill-placeholder'); + + switch (this.type) { + + case 'submit': + case 'reset': + break; + + case 'password': + i.val(i.attr('defaultValue')); + + x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]'); + + if (i.val() == '') { + i.hide(); + x.show(); + } + else { + i.show(); + x.hide(); + } + + break; + + case 'checkbox': + case 'radio': + i.attr('checked', i.attr('defaultValue')); + break; + + case 'text': + case 'textarea': + i.val(i.attr('defaultValue')); + + if (i.val() == '') { + i.addClass('polyfill-placeholder'); + i.val(i.attr('placeholder')); + } + + break; + + default: + i.val(i.attr('defaultValue')); + break; + + } + }); + + }); + + return $this; + + }; + + /** + * Moves elements to/from the first positions of their respective parents. + * @param {jQuery} $elements Elements (or selector) to move. + * @param {bool} condition If true, moves elements to the top. Otherwise, moves elements back to their original locations. + */ + $.prioritize = function($elements, condition) { + + var key = '__prioritize'; + + // Expand $elements if it's not already a jQuery object. + if (typeof $elements != 'jQuery') + $elements = $($elements); + + // Step through elements. + $elements.each(function() { + + var $e = $(this), $p, + $parent = $e.parent(); + + // No parent? Bail. + if ($parent.length == 0) + return; + + // Not moved? Move it. + if (!$e.data(key)) { + + // Condition is false? Bail. + if (!condition) + return; + + // Get placeholder (which will serve as our point of reference for when this element needs to move back). + $p = $e.prev(); + + // Couldn't find anything? Means this element's already at the top, so bail. + if ($p.length == 0) + return; + + // Move element to top of parent. + $e.prependTo($parent); + + // Mark element as moved. + $e.data(key, $p); + + } + + // Moved already? + else { + + // Condition is true? Bail. + if (condition) + return; + + $p = $e.data(key); + + // Move element back to its original location (using our placeholder). + $e.insertAfter($p); + + // Unmark element as moved. + $e.removeData(key); + + } + + }); + + }; + +})(jQuery); \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/base/_page.scss b/source/orm/library/static/library/assets/sass/base/_page.scss new file mode 100644 index 0000000..58f0f0e --- /dev/null +++ b/source/orm/library/static/library/assets/sass/base/_page.scss @@ -0,0 +1,47 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* Basic */ + + // MSIE: Required for IEMobile. + @-ms-viewport { + width: device-width; + } + + // MSIE: Prevents scrollbar from overlapping content. + body { + -ms-overflow-style: scrollbar; + } + + // Ensures page width is always >=320px. + @include breakpoint('<=xsmall') { + html, body { + min-width: 320px; + } + } + + // Set box model to border-box. + // Based on css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice + html { + box-sizing: border-box; + } + + *, *:before, *:after { + box-sizing: inherit; + } + + body { + background: _palette(bg); + + // Stops initial animations until page loads. + &.is-preload { + *, *:before, *:after { + @include vendor('animation', 'none !important'); + @include vendor('transition', 'none !important'); + } + } + + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/base/_reset.scss b/source/orm/library/static/library/assets/sass/base/_reset.scss new file mode 100644 index 0000000..b8d5029 --- /dev/null +++ b/source/orm/library/static/library/assets/sass/base/_reset.scss @@ -0,0 +1,76 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +// Reset. +// Based on meyerweb.com/eric/tools/css/reset (v2.0 | 20110126 | License: public domain) + + html, body, div, span, applet, object, + iframe, h1, h2, h3, h4, h5, h6, p, blockquote, + pre, a, abbr, acronym, address, big, cite, + code, del, dfn, em, img, ins, kbd, q, s, samp, + small, strike, strong, sub, sup, tt, var, b, + u, i, center, dl, dt, dd, ol, ul, li, fieldset, + form, label, legend, table, caption, tbody, + tfoot, thead, tr, th, td, article, aside, + canvas, details, embed, figure, figcaption, + footer, header, hgroup, menu, nav, output, ruby, + section, summary, time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; + } + + article, aside, details, figcaption, figure, + footer, header, hgroup, menu, nav, section { + display: block; + } + + body { + line-height: 1; + } + + ol, ul { + list-style:none; + } + + blockquote, q { + quotes: none; + + &:before, + &:after { + content: ''; + content: none; + } + } + + table { + border-collapse: collapse; + border-spacing: 0; + } + + body { + -webkit-text-size-adjust: none; + } + + mark { + background-color: transparent; + color: inherit; + } + + input::-moz-focus-inner { + border: 0; + padding: 0; + } + + input, select, textarea { + -moz-appearance: none; + -webkit-appearance: none; + -ms-appearance: none; + appearance: none; + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/base/_typography.scss b/source/orm/library/static/library/assets/sass/base/_typography.scss new file mode 100644 index 0000000..9e6c9b4 --- /dev/null +++ b/source/orm/library/static/library/assets/sass/base/_typography.scss @@ -0,0 +1,200 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* Type */ + + body, input, select, textarea { + color: _palette(fg); + font-family: _font(family); + font-size: 16.5pt; + font-weight: _font(weight); + line-height: 1.75; + + @include breakpoint('<=xlarge') { + font-size: 13pt; + } + + @include breakpoint('<=large') { + font-size: 12pt; + } + + @include breakpoint('<=xxsmall') { + font-size: 11pt; + } + } + + a { + @include vendor('transition', ( + 'color #{_duration(transition)} ease', + 'border-bottom-color #{_duration(transition)} ease' + )); + border-bottom: dotted 1px _palette(fg-light); + color: inherit; + text-decoration: none; + + &:hover { + border-bottom-color: transparent; + color: _palette(fg-bold); + } + } + + strong, b { + color: _palette(fg-bold); + font-weight: _font(weight-bold); + } + + em, i { + font-style: italic; + } + + p { + margin: 0 0 _size(element-margin) 0; + } + + h1, h2, h3, h4, h5, h6 { + color: _palette(fg-bold); + font-weight: _font(weight-bold); + line-height: 1.5; + margin: 0 0 (_size(element-margin) * 0.25) 0; + + a { + color: inherit; + text-decoration: none; + } + } + + h1 { + font-size: 2.75em; + + &.major { + margin: 0 0 (_size(element-margin) * 0.65) 0; + position: relative; + padding-bottom: 0.35em; + + &:after { + @include vendor('background-image', 'linear-gradient(to right, #{_palette(accent1)}, #{_palette(accent3)})'); + @include vendor('transition', 'max-width #{_duration(transition)} ease'); + border-radius: 0.2em; + bottom: 0; + content: ''; + height: 0.05em; + position: absolute; + right: 0; + width: 100%; + } + } + } + + h2 { + font-size: 1.75em; + } + + h3 { + font-size: 1.1em; + } + + h4 { + font-size: 1em; + } + + h5 { + font-size: 0.8em; + } + + h6 { + font-size: 0.6em; + } + + @include breakpoint('<=small') { + h1 { + font-size: 2em; + } + + h2 { + font-size: 1.25em; + } + + h3 { + font-size: 1em; + } + + h4 { + font-size: 0.8em; + } + + h5 { + font-size: 0.6em; + } + + h6 { + font-size: 0.6em; + } + } + + sub { + font-size: 0.8em; + position: relative; + top: 0.5em; + } + + sup { + font-size: 0.8em; + position: relative; + top: -0.5em; + } + + blockquote { + border-left: solid (_size(border-width) * 4) _palette(border); + font-style: italic; + margin: 0 0 _size(element-margin) 0; + padding: (_size(element-margin) / 4) 0 (_size(element-margin) / 4) _size(element-margin); + } + + code { + background: _palette(border-bg); + border-radius: _size(border-radius); + border: solid _size(border-width) _palette(border); + font-family: _font(family-fixed); + font-size: 0.9em; + margin: 0 0.25em; + padding: 0.25em 0.65em; + } + + pre { + -webkit-overflow-scrolling: touch; + font-family: _font(family-fixed); + font-size: 0.9em; + margin: 0 0 _size(element-margin) 0; + + code { + display: block; + line-height: 1.75em; + padding: 1em 1.5em; + overflow-x: auto; + } + } + + hr { + border: 0; + border-bottom: solid _size(border-width) _palette(border); + margin: _size(element-margin) 0; + + &.major { + margin: (_size(element-margin) * 1.5) 0; + } + } + + .align-left { + text-align: left; + } + + .align-center { + text-align: center; + } + + .align-right { + text-align: right; + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/components/_actions.scss b/source/orm/library/static/library/assets/sass/components/_actions.scss new file mode 100644 index 0000000..0e273f3 --- /dev/null +++ b/source/orm/library/static/library/assets/sass/components/_actions.scss @@ -0,0 +1,101 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* Actions */ + + ul.actions { + @include vendor('display', 'flex'); + cursor: default; + list-style: none; + margin-left: (_size(element-margin) * -0.5); + padding-left: 0; + + li { + padding: 0 0 0 (_size(element-margin) * 0.5); + vertical-align: middle; + } + + &.special { + @include vendor('justify-content', 'center'); + width: 100%; + margin-left: 0; + + li { + &:first-child { + padding-left: 0; + } + } + } + + &.stacked { + @include vendor('flex-direction', 'column'); + margin-left: 0; + + li { + padding: (_size(element-margin) * 0.65) 0 0 0; + + &:first-child { + padding-top: 0; + } + } + } + + &.fit { + width: calc(100% + #{_size(element-margin) * 0.5}); + + li { + @include vendor('flex-grow', '1'); + @include vendor('flex-shrink', '1'); + width: 100%; + + > * { + width: 100%; + } + } + + &.stacked { + width: 100%; + } + } + + @include breakpoint('<=xsmall') { + &:not(.fixed) { + @include vendor('flex-direction', 'column'); + margin-left: 0; + width: 100% !important; + + li { + @include vendor('flex-grow', '1'); + @include vendor('flex-shrink', '1'); + padding: (_size(element-margin) * 0.5) 0 0 0; + text-align: center; + width: 100%; + + > * { + width: 100%; + } + + &:first-child { + padding-top: 0; + } + + input[type="submit"], + input[type="reset"], + input[type="button"], + button, + .button { + width: 100%; + + &.icon { + &:before { + margin-left: -0.5rem; + } + } + } + } + } + } + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/components/_box.scss b/source/orm/library/static/library/assets/sass/components/_box.scss new file mode 100644 index 0000000..dbb92e1 --- /dev/null +++ b/source/orm/library/static/library/assets/sass/components/_box.scss @@ -0,0 +1,26 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* Box */ + + .box { + border-radius: _size(border-radius); + border: solid _size(border-width) _palette(border); + margin-bottom: _size(element-margin); + padding: 1.5em; + + > :last-child, + > :last-child > :last-child, + > :last-child > :last-child > :last-child { + margin-bottom: 0; + } + + &.alt { + border: 0; + border-radius: 0; + padding: 0; + } + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/components/_button.scss b/source/orm/library/static/library/assets/sass/components/_button.scss new file mode 100644 index 0000000..7875a5d --- /dev/null +++ b/source/orm/library/static/library/assets/sass/components/_button.scss @@ -0,0 +1,106 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* Button */ + + input[type="submit"], + input[type="reset"], + input[type="button"], + button, + .button { + @include vendor('appearance', 'none'); + @include vendor('transition', ( + 'border-color #{_duration(transition)} ease' + )); + background-color: transparent; + border: solid 1px !important; + border-color: _palette(border) !important; + border-radius: 3em; + color: _palette(fg-bold) !important; + cursor: pointer; + display: inline-block; + font-size: 0.6em; + font-weight: _font(weight-bold); + height: calc(4.75em + 2px); + letter-spacing: _font(kerning-alt); + line-height: 4.75em; + outline: 0; + padding: 0 3.75em; + position: relative; + text-align: center; + text-decoration: none; + text-transform: uppercase; + white-space: nowrap; + + &:after { + @include vendor('transform', 'scale(0.25)'); + @include vendor('pointer-events', 'none'); + @include vendor('transition', ( + 'opacity #{_duration(transition)} ease', + 'transform #{_duration(transition)} ease' + )); + background: _palette(fg-bold); + border-radius: 3em; + content: ''; + height: 100%; + left: 0; + opacity: 0; + position: absolute; + top: 0; + width: 100%; + } + + &.icon { + &:before { + margin-right: 0.75em; + } + } + + &.fit { + width: 100%; + } + + &.small { + font-size: 0.4em; + } + + &.large { + font-size: 0.8em; + } + + &.primary { + background-color: _palette(fg-bold); + color: _palette(bg) !important; + + &:after { + display: none; + } + } + + &.disabled, + &:disabled { + cursor: default; + opacity: 0.5; + @include vendor('pointer-events', 'none'); + } + + &:hover { + border-color: _palette(fg) !important; + + &:after { + opacity: 0.05; + @include vendor('transform', 'scale(1)'); + } + + &:active { + border-color: _palette(fg-bold) !important; + + &:after { + opacity: 0.1; + } + } + } + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/components/_contact.scss b/source/orm/library/static/library/assets/sass/components/_contact.scss new file mode 100644 index 0000000..ec54ed5 --- /dev/null +++ b/source/orm/library/static/library/assets/sass/components/_contact.scss @@ -0,0 +1,21 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* Contact */ + + ul.contact { + list-style: none; + padding: 0; + + > li { + padding: 0; + margin: 1.5em 0 0 0; + + &:first-child { + margin-top: 0; + } + } + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/components/_features.scss b/source/orm/library/static/library/assets/sass/components/_features.scss new file mode 100644 index 0000000..8f01a73 --- /dev/null +++ b/source/orm/library/static/library/assets/sass/components/_features.scss @@ -0,0 +1,98 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* Features */ + + .features { + @include vendor('display', 'flex'); + @include vendor('flex-wrap', 'wrap'); + border-radius: _size(border-radius); + border: solid 1px _palette(border); + background: _palette(border-bg); + margin: 0 0 _size(element-margin) 0; + + section { + @include padding(3em, 3em, (0.5em, 0, 0, 4em)); + width: 50%; + border-top: solid 1px _palette(border); + position: relative; + + &:nth-child(-n + 2) { + border-top-width: 0; + } + + &:nth-child(2n) { + border-left: solid 1px _palette(border); + } + + .icon { + @include vendor('transition', ( + 'opacity #{_duration(activation) * 0.5} ease', + 'transform #{_duration(activation) * 0.5} ease' + )); + @include vendor('transition-delay', '1s'); + @include vendor('transform', 'scale(1)'); + position: absolute; + left: 3em; + top: 3em; + opacity: 1; + } + + @for $i from 1 through _misc(max-features) { + &:nth-child(#{$i}) { + .icon { + @include vendor('transition-delay', '#{(_duration(transition) * 0.75 * $i)}'); + } + } + } + } + + &.inactive { + section { + .icon { + @include vendor('transform', 'scale(0.5)'); + opacity: 0; + } + } + } + + @include breakpoint('<=medium') { + display: block; + + section { + border-top-width: 1px !important; + border-left-width: 0 !important; + width: 100%; + + &:first-child { + border-top-width: 0 !important; + } + } + } + + @include breakpoint('<=small') { + section { + @include padding(2em, 1.5em, (0.5em, 0, 0, 4em)); + + .icon { + left: 1.5em; + top: 2em; + } + } + } + + @include breakpoint('<=xsmall') { + section { + @include padding(2em, 1.5em, (0, 0, 0, 0)); + + .icon { + left: 0; + position: relative; + top: 0; + } + } + } + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/components/_form.scss b/source/orm/library/static/library/assets/sass/components/_form.scss new file mode 100644 index 0000000..2425cb7 --- /dev/null +++ b/source/orm/library/static/library/assets/sass/components/_form.scss @@ -0,0 +1,237 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* Form */ + + form { + margin: 0 0 _size(element-margin) 0; + + > :last-child { + margin-bottom: 0; + } + + > .fields { + $gutter: (_size(element-margin) * 0.75); + + @include vendor('display', 'flex'); + @include vendor('flex-wrap', 'wrap'); + width: calc(100% + #{$gutter * 2}); + margin: ($gutter * -1) 0 _size(element-margin) ($gutter * -1); + + > .field { + @include vendor('flex-grow', '0'); + @include vendor('flex-shrink', '0'); + padding: $gutter 0 0 $gutter; + width: calc(100% - #{$gutter * 1}); + + &.half { + width: calc(50% - #{$gutter * 0.5}); + } + + &.third { + width: calc(#{100% / 3} - #{$gutter * (1 / 3)}); + } + + &.quarter { + width: calc(25% - #{$gutter * 0.25}); + } + } + } + + @include breakpoint('<=xsmall') { + > .fields { + $gutter: (_size(element-margin) * 0.75); + + width: calc(100% + #{$gutter * 2}); + margin: ($gutter * -1) 0 _size(element-margin) ($gutter * -1); + + > .field { + padding: $gutter 0 0 $gutter; + width: calc(100% - #{$gutter * 1}); + + &.half { + width: calc(100% - #{$gutter * 1}); + } + + &.third { + width: calc(100% - #{$gutter * 1}); + } + + &.quarter { + width: calc(100% - #{$gutter * 1}); + } + } + } + } + } + + label { + color: _palette(fg-bold); + font-weight: _font(weight-bold); + line-height: 1.5; + margin: 0 0 (_size(element-margin) * 0.35) 0; + display: block; + font-size: 1.1em; + } + + input[type="text"], + input[type="password"], + input[type="email"], + input[type="tel"], + select, + textarea { + @include vendor('appearance', 'none'); + background: _palette(border-bg); + border-radius: _size(border-radius); + border: none; + border: solid _size(border-width) _palette(border); + color: inherit; + display: block; + outline: 0; + padding: 0 1em; + text-decoration: none; + width: 100%; + + &:invalid { + box-shadow: none; + } + + &:focus { + border-color: _palette(fg-bold); + box-shadow: 0 0 0 _size(border-width) _palette(fg-bold); + } + } + + select { + background-image: svg-url(""); + background-size: 1.25rem; + background-repeat: no-repeat; + background-position: calc(100% - 1rem) center; + height: _size(element-height); + padding-right: _size(element-height); + text-overflow: ellipsis; + + option { + color: _palette(fg-bold); + background: _palette(bg); + } + + &:focus { + &::-ms-value { + background-color: transparent; + } + } + + &::-ms-expand { + display: none; + } + } + + input[type="text"], + input[type="password"], + input[type="email"], + select { + height: _size(element-height); + } + + textarea { + padding: 0.75em 1em; + + body.is-ie & { + min-height: 10em; + } + } + + input[type="checkbox"], + input[type="radio"], { + @include vendor('appearance', 'none'); + display: block; + float: left; + margin-right: -2em; + opacity: 0; + width: 1em; + z-index: -1; + + & + label { + @include icon(false, solid); + color: _palette(fg); + cursor: pointer; + display: inline-block; + font-size: 1em; + font-weight: _font(weight); + padding-left: (_size(element-height) * 0.6) + 0.75em; + padding-right: 0.75em; + position: relative; + + &:before { + background: _palette(border-bg); + border-radius: _size(border-radius); + border: solid _size(border-width) _palette(border); + content: ''; + display: inline-block; + font-size: 0.8em; + height: (_size(element-height) * 0.75); + left: 0; + line-height: (_size(element-height) * 0.75); + position: absolute; + text-align: center; + top: 0; + width: (_size(element-height) * 0.75); + } + } + + &:checked + label { + &:before { + background: _palette(fg-bold); + border-color: _palette(fg-bold); + color: _palette(accent3); + content: '\f00c'; + } + } + + &:focus + label { + &:before { + border-color: _palette(fg-bold); + box-shadow: 0 0 0 _size(border-width) _palette(fg-bold); + } + } + } + + input[type="checkbox"] { + & + label { + &:before { + border-radius: _size(border-radius); + } + } + } + + input[type="radio"] { + & + label { + &:before { + border-radius: 100%; + } + } + } + + ::-webkit-input-placeholder { + color: _palette(fg-light) !important; + opacity: 1.0; + } + + :-moz-placeholder { + color: _palette(fg-light) !important; + opacity: 1.0; + } + + ::-moz-placeholder { + color: _palette(fg-light) !important; + opacity: 1.0; + } + + :-ms-input-placeholder { + color: _palette(fg-light) !important; + opacity: 1.0; + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/components/_icon.scss b/source/orm/library/static/library/assets/sass/components/_icon.scss new file mode 100644 index 0000000..639bb61 --- /dev/null +++ b/source/orm/library/static/library/assets/sass/components/_icon.scss @@ -0,0 +1,73 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* Icon */ + + .icon { + @include icon; + border-bottom: none; + position: relative; + + > .label { + display: none; + } + + &:before { + line-height: inherit; + } + + &.solid { + &:before { + font-weight: 900; + } + } + + &.brands { + &:before { + font-family: 'Font Awesome 5 Brands'; + } + } + + &.major { + width: 2.5em; + height: 2.5em; + display: block; + background: _palette(fg-bold); + border-radius: 100%; + color: _palette(bg); + text-align: center; + line-height: 2.5em; + margin: 0 0 (_size(element-margin) * 0.65) 0; + + &:before { + font-size: 1.25em; + + .wrapper.style1 & { + color: _palette(accent1); + } + + .wrapper.style1-alt & { + color: _palette(accent1-alt); + } + + .wrapper.style2 & { + color: _palette(accent2); + } + + .wrapper.style2-alt & { + color: _palette(accent2-alt); + } + + .wrapper.style3 & { + color: _palette(accent3); + } + + .wrapper.style3-alt & { + color: _palette(accent3-alt); + } + } + } + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/components/_icons.scss b/source/orm/library/static/library/assets/sass/components/_icons.scss new file mode 100644 index 0000000..b0aaf37 --- /dev/null +++ b/source/orm/library/static/library/assets/sass/components/_icons.scss @@ -0,0 +1,30 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* Icons */ + + ul.icons { + cursor: default; + list-style: none; + padding-left: 0; + + li { + display: inline-block; + padding: 0 0.75em 0 0; + + &:last-child { + padding-right: 0; + } + + > a, > span { + border: 0; + + .label { + display: none; + } + } + } + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/components/_image.scss b/source/orm/library/static/library/assets/sass/components/_image.scss new file mode 100644 index 0000000..07c2978 --- /dev/null +++ b/source/orm/library/static/library/assets/sass/components/_image.scss @@ -0,0 +1,60 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* Image */ + + .image { + border-radius: _size(border-radius); + border: 0; + display: inline-block; + position: relative; + + img { + border-radius: _size(border-radius); + display: block; + } + + &.left, + &.right { + max-width: 40%; + + img { + width: 100%; + } + } + + &.left { + float: left; + margin: 0 1.5em 1em 0; + top: 0.25em; + } + + &.right { + float: right; + margin: 0 0 1em 1.5em; + top: 0.25em; + } + + &.fit { + display: block; + margin: 0 0 _size(element-margin) 0; + width: 100%; + + img { + width: 100%; + } + } + + &.main { + display: block; + margin: 0 0 (_size(element-margin) * 1.5) 0; + width: 100%; + + img { + width: 100%; + } + } + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/components/_list.scss b/source/orm/library/static/library/assets/sass/components/_list.scss new file mode 100644 index 0000000..a68bc05 --- /dev/null +++ b/source/orm/library/static/library/assets/sass/components/_list.scss @@ -0,0 +1,56 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* List */ + + ol { + list-style: decimal; + margin: 0 0 _size(element-margin) 0; + padding-left: 1.25em; + + li { + padding-left: 0.25em; + } + } + + ul { + list-style: disc; + margin: 0 0 _size(element-margin) 0; + padding-left: 1em; + + li { + padding-left: 0.5em; + } + + &.alt { + list-style: none; + padding-left: 0; + + li { + border-top: solid _size(border-width) _palette(border); + padding: 0.5em 0; + + &:first-child { + border-top: 0; + padding-top: 0; + } + } + } + } + + dl { + margin: 0 0 _size(element-margin) 0; + + dt { + display: block; + font-weight: _font(weight-bold); + margin: 0 0 (_size(element-margin) * 0.5) 0; + } + + dd { + margin-left: _size(element-margin); + } + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/components/_menu.scss b/source/orm/library/static/library/assets/sass/components/_menu.scss new file mode 100644 index 0000000..c9c95dc --- /dev/null +++ b/source/orm/library/static/library/assets/sass/components/_menu.scss @@ -0,0 +1,36 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* Menu */ + + ul.menu { + list-style: none; + padding: 0; + + > li { + border-left: solid 1px _palette(border); + display: inline-block; + line-height: 1; + margin-left: 1.5em; + padding: 0 0 0 1.5em; + + &:first-child { + border-left: 0; + margin: 0; + padding-left: 0; + } + } + + @include breakpoint('<=xsmall') { + > li { + border-left: 0; + display: block; + line-height: inherit; + margin: 0.5em 0 0 0; + padding-left: 0; + } + } + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/components/_row.scss b/source/orm/library/static/library/assets/sass/components/_row.scss new file mode 100644 index 0000000..b79847a --- /dev/null +++ b/source/orm/library/static/library/assets/sass/components/_row.scss @@ -0,0 +1,31 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* Row */ + + .row { + @include html-grid(1.5em); + + @include breakpoint('<=xlarge') { + @include html-grid(1.5em, 'xlarge'); + } + + @include breakpoint('<=large') { + @include html-grid(1.5em, 'large'); + } + + @include breakpoint('<=medium') { + @include html-grid(1.5em, 'medium'); + } + + @include breakpoint('<=small') { + @include html-grid(1.5em, 'small'); + } + + @include breakpoint('<=xsmall') { + @include html-grid(1.5em, 'xsmall'); + } + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/components/_section.scss b/source/orm/library/static/library/assets/sass/components/_section.scss new file mode 100644 index 0000000..98be049 --- /dev/null +++ b/source/orm/library/static/library/assets/sass/components/_section.scss @@ -0,0 +1,41 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* Section/Article */ + + section, article { + &.special { + text-align: center; + } + } + + header { + p { + color: _palette(fg-light); + position: relative; + margin: 0 0 (_size(element-margin) * 0.75) 0; + } + + h2 + p { + font-size: 1.25em; + margin-top: (_size(element-margin) * -0.5); + line-height: 1.5em; + } + + h3 + p { + font-size: 1.1em; + margin-top: (_size(element-margin) * -0.4); + line-height: 1.5em; + } + + h4 + p, + h5 + p, + h6 + p { + font-size: 0.9em; + margin-top: (_size(element-margin) * -0.3); + line-height: 1.5em; + } + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/components/_split.scss b/source/orm/library/static/library/assets/sass/components/_split.scss new file mode 100644 index 0000000..4f2227f --- /dev/null +++ b/source/orm/library/static/library/assets/sass/components/_split.scss @@ -0,0 +1,91 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* Split */ + + .split { + @include vendor('display', 'flex'); + + > * { + width: calc(50% - 2.5em); + } + + > :nth-child(2n - 1) { + padding-right: 2.5em; + border-right: solid 1px _palette(border); + } + + > :nth-child(2n) { + padding-left: 2.5em; + } + + &.style1 { + > :nth-child(2n - 1) { + width: calc(66.66666% - 2.5em); + } + + > :nth-child(2n) { + width: calc(33.33333% - 2.5em); + } + } + + @include breakpoint('<=xlarge') { + > * { + width: calc(50% - 2em); + } + + > :nth-child(2n - 1) { + padding-right: 2em; + } + + > :nth-child(2n) { + padding-left: 2em; + } + + &.style1 { + > :nth-child(2n - 1) { + width: calc(66.66666% - 2em); + } + + > :nth-child(2n) { + width: calc(33.33333% - 2em); + } + } + } + + @include breakpoint('<=medium') { + display: block; + + > * { + border-top: solid 1px _palette(border); + margin: 4em 0 0 0; + padding: 4em 0 0 0; + width: 100% !important; + } + + > :nth-child(2n - 1) { + border-right: 0; + padding-right: 0; + } + + > :nth-child(2n) { + padding-left: 0; + } + + > :first-child { + border-top: 0; + margin-top: 0; + padding-top: 0; + } + } + + @include breakpoint('<=small') { + > * { + margin: 3em 0 0 0; + padding: 3em 0 0 0; + } + } + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/components/_spotlights.scss b/source/orm/library/static/library/assets/sass/components/_spotlights.scss new file mode 100644 index 0000000..2a64192 --- /dev/null +++ b/source/orm/library/static/library/assets/sass/components/_spotlights.scss @@ -0,0 +1,131 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* Spotlights */ + + .spotlights { + > section { + @include vendor('display', 'flex'); + @include vendor('flex-direction', 'row'); + min-height: 22.5em; + + body.is-ie & { + min-height: 0; + } + + > .image { + background-position: center center; + background-size: cover; + border-radius: 0; + display: block; + position: relative; + width: 25em; + + img { + border-radius: 0; + display: block; + } + + &:before { + @include vendor('transition', 'opacity #{_duration(activation)} ease'); + background: transparentize(_palette(bg), 0.1); + content: ''; + display: block; + height: 100%; + left: 0; + opacity: 0; + position: absolute; + top: 0; + width: 100%; + } + } + + > .content { + @include padding(4em, 5em); + @include vendor('display', 'flex'); + @include vendor('flex-direction', 'column'); + @include vendor('justify-content', 'center'); + width: #{_size(inner-width) - 25em}; + -ms-flex: 1; + + > .inner { + @include vendor('transform', 'translateX(0) translateY(0)'); + @include vendor('transition', ( + 'opacity #{_duration(activation)} ease', + 'transform #{_duration(activation)} ease' + )); + opacity: 1; + } + } + + &:nth-child(1) { + } + + &:nth-child(2) { + background-color: rgba(0,0,0,0.05); + } + + &:nth-child(3) { + background-color: rgba(0,0,0,0.1); + } + + &.inactive, + body.is-preload & { + > .image { + &:before { + opacity: 1; + } + } + + > .content { + > .inner { + @include vendor('transform', 'translateX(-1em)'); + opacity: 0; + } + } + } + + @include breakpoint('<=xlarge') { + > .content { + @include padding(4em, 4em); + } + } + + @include breakpoint('<=medium') { + display: block; + + > .image { + width: 100%; + height: 50vh; + } + + > .content { + width: 100%; + } + + &.inactive, + body.is-preload & { + > .content { + > .inner { + @include vendor('transform', 'translateY(1em)'); + } + } + } + } + + @include breakpoint('<=small') { + > .image { + height: 50vh; + min-height: 15em; + } + + > .content { + @include padding(3em, 2em); + } + } + } + } + diff --git a/source/orm/library/static/library/assets/sass/components/_table.scss b/source/orm/library/static/library/assets/sass/components/_table.scss new file mode 100644 index 0000000..0bbe87e --- /dev/null +++ b/source/orm/library/static/library/assets/sass/components/_table.scss @@ -0,0 +1,81 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* Table */ + + .table-wrapper { + -webkit-overflow-scrolling: touch; + overflow-x: auto; + } + + table { + margin: 0 0 _size(element-margin) 0; + width: 100%; + + tbody { + tr { + border: solid _size(border-width) _palette(border); + border-left: 0; + border-right: 0; + + &:nth-child(2n + 1) { + background-color: _palette(border-bg); + } + } + } + + td { + padding: 0.75em 0.75em; + } + + th { + color: _palette(fg-bold); + font-size: 1em; + font-weight: _font(weight-bold); + padding: 0 0.75em 0.75em 0.75em; + text-align: left; + } + + thead { + border-bottom: solid (_size(border-width) * 2) _palette(border); + } + + tfoot { + border-top: solid (_size(border-width) * 2) _palette(border); + } + + &.alt { + border-collapse: separate; + + tbody { + tr { + td { + border: solid _size(border-width) _palette(border); + border-left-width: 0; + border-top-width: 0; + + &:first-child { + border-left-width: _size(border-width); + } + } + + &:first-child { + td { + border-top-width: _size(border-width); + } + } + } + } + + thead { + border-bottom: 0; + } + + tfoot { + border-top: 0; + } + } + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/components/_wrapper.scss b/source/orm/library/static/library/assets/sass/components/_wrapper.scss new file mode 100644 index 0000000..d42b4d2 --- /dev/null +++ b/source/orm/library/static/library/assets/sass/components/_wrapper.scss @@ -0,0 +1,139 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* Wrapper */ + + .wrapper { + position: relative; + + > .inner { + @include padding(5em, 5em); + max-width: 100%; + width: _size(inner-width); + + @include breakpoint('<=xlarge') { + @include padding(4em, 4em); + } + + @include breakpoint('<=large') { + width: 100%; + } + + @include breakpoint('<=small') { + @include padding(3em, 2em); + } + } + + &.alt { + background-color: _palette(bg-alt); + } + + &.style1 { + background-color: _palette(accent1); + } + + &.style1-alt { + background-color: _palette(accent1-alt); + } + + &.style2 { + background-color: _palette(accent2); + } + + &.style2-alt { + background-color: _palette(accent2-alt); + } + + &.style3 { + background-color: _palette(accent3); + } + + &.style3-alt { + background-color: _palette(accent3-alt); + } + + &.fullscreen { + @include vendor('display', 'flex'); + @include vendor('flex-direction', 'column'); + @include vendor('justify-content', 'center'); + min-height: 100vh; + + body.is-ie & { + height: 100vh; + } + + @include breakpoint('<=large') { + min-height: calc(100vh - 2.5em); + + body.is-ie & { + height: calc(100vh - 2.5em); + } + } + + @include breakpoint('<=small') { + padding: 2em 0; + min-height: 0; + + body.is-ie & { + height: auto; + } + } + } + + &.fade-up { + > .inner { + @include vendor('transform', 'translateY(0)'); + @include vendor('transition', ( + 'opacity #{_duration(activation)} ease', + 'transform #{_duration(activation)} ease' + )); + opacity: 1.0; + } + + &.inactive, + body.is-preload & { + > .inner { + opacity: 0; + @include vendor('transform', 'translateY(1em)'); + } + } + } + + &.fade-down { + > .inner { + @include vendor('transform', 'translateY(0)'); + @include vendor('transition', ( + 'opacity #{_duration(activation)} ease', + 'transform #{_duration(activation)} ease' + )); + opacity: 1.0; + } + + &.inactive, + body.is-preload & { + > .inner { + opacity: 0; + @include vendor('transform', 'translateY(-1em)'); + } + } + } + + &.fade { + > .inner { + @include vendor('transition', ( + 'opacity #{_duration(activation)} ease' + )); + opacity: 1.0; + } + + &.inactive, + body.is-preload & { + > .inner { + opacity: 0; + } + } + } + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/layout/_footer.scss b/source/orm/library/static/library/assets/sass/layout/_footer.scss new file mode 100644 index 0000000..dbaf77d --- /dev/null +++ b/source/orm/library/static/library/assets/sass/layout/_footer.scss @@ -0,0 +1,38 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* Footer */ + + #footer { + #sidebar + #wrapper + & { + margin-left: _size(sidebar-width); + + @include breakpoint('<=large') { + margin-left: 0; + } + } + + > .inner { + a { + border-bottom-color: _palette(border); + + &:hover { + border-bottom-color: transparent; + } + } + + .menu { + font-size: 0.8em; + color: _palette(border); + } + } + + #header + #wrapper + & { + > .inner { + margin: 0 auto; + } + } + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/layout/_header.scss b/source/orm/library/static/library/assets/sass/layout/_header.scss new file mode 100644 index 0000000..6316776 --- /dev/null +++ b/source/orm/library/static/library/assets/sass/layout/_header.scss @@ -0,0 +1,92 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* Header */ + + #header { + @include vendor('display', 'flex'); + background-color: _palette(accent1); + cursor: default; + padding: 1.75em 2em; + + > .title { + border: 0; + color: _palette(fg-bold); + display: block; + font-size: 1.25em; + font-weight: _font(weight-bold); + } + + > nav { + @include vendor('flex', '1'); + text-align: right; + + > ul { + margin: 0; + padding: 0; + + > li { + display: inline-block; + margin-left: 1.75em; + padding: 0; + vertical-align: middle; + + &:first-child { + margin-left: 0; + } + + a { + border: 0; + color: _palette(fg-light); + display: inline-block; + font-size: 0.6em; + font-weight: _font(weight-bold); + letter-spacing: _font(kerning-alt); + text-transform: uppercase; + + &:hover { + color: _palette(fg); + } + + &.active { + color: _palette(fg-bold); + } + } + } + } + } + + @include breakpoint('<=small') { + padding: 1em 2em; + } + + @include breakpoint('<=xsmall') { + display: block; + padding: 0 2em; + text-align: left; + + .title { + font-size: 1.25em; + padding: 1em 0; + } + + > nav { + border-top: solid 1px _palette(border); + text-align: inherit; + + > ul { + > li { + margin-left: 1.5em; + + a { + height: 6em; + line-height: 6em; + } + } + } + } + } + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/layout/_intro.scss b/source/orm/library/static/library/assets/sass/layout/_intro.scss new file mode 100644 index 0000000..6a1869a --- /dev/null +++ b/source/orm/library/static/library/assets/sass/layout/_intro.scss @@ -0,0 +1,33 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* Intro */ + + #intro { + background-attachment: fixed; + background-image: url('images/intro.svg'); + background-position: top right; + background-repeat: no-repeat; + background-size: 100% 100%; + + p { + font-size: 1.25em; + + @include breakpoint('<=medium') { + br { + display: none; + } + } + + @include breakpoint('<=small') { + font-size: 1em; + } + } + + @include breakpoint('<=large') { + background-attachment: scroll; + } + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/layout/_sidebar.scss b/source/orm/library/static/library/assets/sass/layout/_sidebar.scss new file mode 100644 index 0000000..0ae5c33 --- /dev/null +++ b/source/orm/library/static/library/assets/sass/layout/_sidebar.scss @@ -0,0 +1,185 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* Sidebar */ + + #sidebar { + @include padding(2.5em, 2.5em); + background: _palette(bg); + cursor: default; + height: 100vh; + left: 0; + overflow-x: hidden; + overflow-y: auto; + position: fixed; + text-align: right; + top: 0; + width: _size(sidebar-width); + z-index: _misc(z-index-base); + + > .inner { + @include vendor('display', 'flex'); + @include vendor('flex-direction', 'column'); + @include vendor('justify-content', 'center'); + @include vendor('transform', 'translateY(0)'); + @include vendor('transition', ( + 'opacity #{_duration(activation)} ease', + )); + min-height: 100%; + opacity: 1; + width: 100%; + + body.is-ie & { + height: 100%; + } + } + + nav { + > ul { + list-style: none; + padding: 0; + + > li { + @include vendor('transform', 'translateY(0)'); + @include vendor('transition', ( + 'opacity #{_duration(activation) * 0.15} ease', + 'transform #{_duration(activation) * 0.75} ease' + )); + margin: 1.5em 0 0 0; + opacity: 1; + padding: 0; + position: relative; + + &:first-child { + margin: 0; + } + + @for $i from 1 through _misc(max-sidebar-links) { + &:nth-child(#{$i}) { + @include vendor('transition-delay', '#{(_duration(activation) * 0.2 * $i) + (_duration(activation) * 0.25)}'); + } + } + } + } + + a { + @include vendor('transition', 'color #{_duration(transition)} ease'); + border: 0; + color: _palette(fg-light); + display: block; + font-size: 0.6em; + font-weight: _font(weight-bold); + letter-spacing: _font(kerning-alt); + line-height: 1.75; + outline: 0; + padding: 1.35em 0; + position: relative; + text-decoration: none; + text-transform: uppercase; + + &:before, + &:after { + border-radius: 0.2em; + bottom: 0; + content: ''; + height: 0.2em; + position: absolute; + right: 0; + width: 100%; + } + + &:before { + background: lighten(_palette(bg), 5); + } + + &:after { + @include vendor('background-image', 'linear-gradient(to right, #{_palette(accent1)}, #{_palette(accent3)})'); + @include vendor('transition', 'max-width #{_duration(transition)} ease'); + max-width: 0; + } + + &:hover { + color: _palette(fg); + } + + &.active { + color: _palette(fg-bold); + + &:after { + max-width: 100%; + } + } + } + } + + body.is-preload & { + > .inner { + opacity: 0; + } + + nav { + ul { + li { + @include vendor('transform', 'translateY(2em)'); + opacity: 0; + } + } + } + } + + @include breakpoint('<=large') { + height: _size(sidebar-height); + left: 0; + line-height: _size(sidebar-height); + overflow: hidden; + padding: 0; + text-align: center; + top: 0; + width: 100%; + + > .inner { + @include vendor('flex-direction', 'row'); + @include vendor('align-items', 'stretch'); + height: inherit; + line-height: inherit; + } + + nav { + height: inherit; + line-height: inherit; + + ul { + @include vendor('display', 'flex'); + height: inherit; + line-height: inherit; + margin: 0; + + li { + display: block; + height: inherit; + line-height: inherit; + margin: 0 0 0 2em; + padding: 0; + } + } + + a { + height: inherit; + line-height: inherit; + padding: 0; + + &:after { + background-image: none; + background-color: _palette(accent3); + } + } + } + } + + @include breakpoint('<=small') { + display: none; + } + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/layout/_wrapper.scss b/source/orm/library/static/library/assets/sass/layout/_wrapper.scss new file mode 100644 index 0000000..7b39d30 --- /dev/null +++ b/source/orm/library/static/library/assets/sass/layout/_wrapper.scss @@ -0,0 +1,30 @@ +/// +/// Hyperspace by HTML5 UP +/// html5up.net | @ajlkn +/// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +/// + +/* Wrapper (main) */ + + #wrapper { + #sidebar + & { + margin-left: _size(sidebar-width); + + @include breakpoint('<=large') { + margin-left: 0; + padding-top: _size(sidebar-height); + } + + @include breakpoint('<=small') { + padding-top: 0; + } + } + + #header + & { + > .wrapper { + > .inner { + margin: 0 auto; + } + } + } + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/libs/_breakpoints.scss b/source/orm/library/static/library/assets/sass/libs/_breakpoints.scss new file mode 100644 index 0000000..c5301d8 --- /dev/null +++ b/source/orm/library/static/library/assets/sass/libs/_breakpoints.scss @@ -0,0 +1,223 @@ +// breakpoints.scss v1.0 | @ajlkn | MIT licensed */ + +// Vars. + + /// Breakpoints. + /// @var {list} + $breakpoints: () !global; + +// Mixins. + + /// Sets breakpoints. + /// @param {map} $x Breakpoints. + @mixin breakpoints($x: ()) { + $breakpoints: $x !global; + } + + /// Wraps @content in a @media block targeting a specific orientation. + /// @param {string} $orientation Orientation. + @mixin orientation($orientation) { + @media screen and (orientation: #{$orientation}) { + @content; + } + } + + /// Wraps @content in a @media block using a given query. + /// @param {string} $query Query. + @mixin breakpoint($query: null) { + + $breakpoint: null; + $op: null; + $media: null; + + // Determine operator, breakpoint. + + // Greater than or equal. + @if (str-slice($query, 0, 2) == '>=') { + + $op: 'gte'; + $breakpoint: str-slice($query, 3); + + } + + // Less than or equal. + @elseif (str-slice($query, 0, 2) == '<=') { + + $op: 'lte'; + $breakpoint: str-slice($query, 3); + + } + + // Greater than. + @elseif (str-slice($query, 0, 1) == '>') { + + $op: 'gt'; + $breakpoint: str-slice($query, 2); + + } + + // Less than. + @elseif (str-slice($query, 0, 1) == '<') { + + $op: 'lt'; + $breakpoint: str-slice($query, 2); + + } + + // Not. + @elseif (str-slice($query, 0, 1) == '!') { + + $op: 'not'; + $breakpoint: str-slice($query, 2); + + } + + // Equal. + @else { + + $op: 'eq'; + $breakpoint: $query; + + } + + // Build media. + @if ($breakpoint and map-has-key($breakpoints, $breakpoint)) { + + $a: map-get($breakpoints, $breakpoint); + + // Range. + @if (type-of($a) == 'list') { + + $x: nth($a, 1); + $y: nth($a, 2); + + // Max only. + @if ($x == null) { + + // Greater than or equal (>= 0 / anything) + @if ($op == 'gte') { + $media: 'screen'; + } + + // Less than or equal (<= y) + @elseif ($op == 'lte') { + $media: 'screen and (max-width: ' + $y + ')'; + } + + // Greater than (> y) + @elseif ($op == 'gt') { + $media: 'screen and (min-width: ' + ($y + 1) + ')'; + } + + // Less than (< 0 / invalid) + @elseif ($op == 'lt') { + $media: 'screen and (max-width: -1px)'; + } + + // Not (> y) + @elseif ($op == 'not') { + $media: 'screen and (min-width: ' + ($y + 1) + ')'; + } + + // Equal (<= y) + @else { + $media: 'screen and (max-width: ' + $y + ')'; + } + + } + + // Min only. + @else if ($y == null) { + + // Greater than or equal (>= x) + @if ($op == 'gte') { + $media: 'screen and (min-width: ' + $x + ')'; + } + + // Less than or equal (<= inf / anything) + @elseif ($op == 'lte') { + $media: 'screen'; + } + + // Greater than (> inf / invalid) + @elseif ($op == 'gt') { + $media: 'screen and (max-width: -1px)'; + } + + // Less than (< x) + @elseif ($op == 'lt') { + $media: 'screen and (max-width: ' + ($x - 1) + ')'; + } + + // Not (< x) + @elseif ($op == 'not') { + $media: 'screen and (max-width: ' + ($x - 1) + ')'; + } + + // Equal (>= x) + @else { + $media: 'screen and (min-width: ' + $x + ')'; + } + + } + + // Min and max. + @else { + + // Greater than or equal (>= x) + @if ($op == 'gte') { + $media: 'screen and (min-width: ' + $x + ')'; + } + + // Less than or equal (<= y) + @elseif ($op == 'lte') { + $media: 'screen and (max-width: ' + $y + ')'; + } + + // Greater than (> y) + @elseif ($op == 'gt') { + $media: 'screen and (min-width: ' + ($y + 1) + ')'; + } + + // Less than (< x) + @elseif ($op == 'lt') { + $media: 'screen and (max-width: ' + ($x - 1) + ')'; + } + + // Not (< x and > y) + @elseif ($op == 'not') { + $media: 'screen and (max-width: ' + ($x - 1) + '), screen and (min-width: ' + ($y + 1) + ')'; + } + + // Equal (>= x and <= y) + @else { + $media: 'screen and (min-width: ' + $x + ') and (max-width: ' + $y + ')'; + } + + } + + } + + // String. + @else { + + // Missing a media type? Prefix with "screen". + @if (str-slice($a, 0, 1) == '(') { + $media: 'screen and ' + $a; + } + + // Otherwise, use as-is. + @else { + $media: $a; + } + + } + + } + + // Output. + @media #{$media} { + @content; + } + + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/libs/_functions.scss b/source/orm/library/static/library/assets/sass/libs/_functions.scss new file mode 100644 index 0000000..f563aab --- /dev/null +++ b/source/orm/library/static/library/assets/sass/libs/_functions.scss @@ -0,0 +1,90 @@ +/// Removes a specific item from a list. +/// @author Hugo Giraudel +/// @param {list} $list List. +/// @param {integer} $index Index. +/// @return {list} Updated list. +@function remove-nth($list, $index) { + + $result: null; + + @if type-of($index) != number { + @warn "$index: #{quote($index)} is not a number for `remove-nth`."; + } + @else if $index == 0 { + @warn "List index 0 must be a non-zero integer for `remove-nth`."; + } + @else if abs($index) > length($list) { + @warn "List index is #{$index} but list is only #{length($list)} item long for `remove-nth`."; + } + @else { + + $result: (); + $index: if($index < 0, length($list) + $index + 1, $index); + + @for $i from 1 through length($list) { + + @if $i != $index { + $result: append($result, nth($list, $i)); + } + + } + + } + + @return $result; + +} + +/// Gets a value from a map. +/// @author Hugo Giraudel +/// @param {map} $map Map. +/// @param {string} $keys Key(s). +/// @return {string} Value. +@function val($map, $keys...) { + + @if nth($keys, 1) == null { + $keys: remove-nth($keys, 1); + } + + @each $key in $keys { + $map: map-get($map, $key); + } + + @return $map; + +} + +/// Gets a duration value. +/// @param {string} $keys Key(s). +/// @return {string} Value. +@function _duration($keys...) { + @return val($duration, $keys...); +} + +/// Gets a font value. +/// @param {string} $keys Key(s). +/// @return {string} Value. +@function _font($keys...) { + @return val($font, $keys...); +} + +/// Gets a misc value. +/// @param {string} $keys Key(s). +/// @return {string} Value. +@function _misc($keys...) { + @return val($misc, $keys...); +} + +/// Gets a palette value. +/// @param {string} $keys Key(s). +/// @return {string} Value. +@function _palette($keys...) { + @return val($palette, $keys...); +} + +/// Gets a size value. +/// @param {string} $keys Key(s). +/// @return {string} Value. +@function _size($keys...) { + @return val($size, $keys...); +} \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/libs/_html-grid.scss b/source/orm/library/static/library/assets/sass/libs/_html-grid.scss new file mode 100644 index 0000000..7438a8c --- /dev/null +++ b/source/orm/library/static/library/assets/sass/libs/_html-grid.scss @@ -0,0 +1,149 @@ +// html-grid.scss v1.0 | @ajlkn | MIT licensed */ + +// Mixins. + + /// Initializes the current element as an HTML grid. + /// @param {mixed} $gutters Gutters (either a single number to set both column/row gutters, or a list to set them individually). + /// @param {mixed} $suffix Column class suffix (optional; either a single suffix or a list). + @mixin html-grid($gutters: 1.5em, $suffix: '') { + + // Initialize. + $cols: 12; + $multipliers: 0, 0.25, 0.5, 1, 1.50, 2.00; + $unit: 100% / $cols; + + // Suffixes. + $suffixes: null; + + @if (type-of($suffix) == 'list') { + $suffixes: $suffix; + } + @else { + $suffixes: ($suffix); + } + + // Gutters. + $guttersCols: null; + $guttersRows: null; + + @if (type-of($gutters) == 'list') { + + $guttersCols: nth($gutters, 1); + $guttersRows: nth($gutters, 2); + + } + @else { + + $guttersCols: $gutters; + $guttersRows: 0; + + } + + // Row. + display: flex; + flex-wrap: wrap; + box-sizing: border-box; + align-items: stretch; + + // Columns. + > * { + box-sizing: border-box; + } + + // Gutters. + &.gtr-uniform { + > * { + > :last-child { + margin-bottom: 0; + } + } + } + + // Alignment. + &.aln-left { + justify-content: flex-start; + } + + &.aln-center { + justify-content: center; + } + + &.aln-right { + justify-content: flex-end; + } + + &.aln-top { + align-items: flex-start; + } + + &.aln-middle { + align-items: center; + } + + &.aln-bottom { + align-items: flex-end; + } + + // Step through suffixes. + @each $suffix in $suffixes { + + // Suffix. + @if ($suffix != '') { + $suffix: '-' + $suffix; + } + @else { + $suffix: ''; + } + + // Row. + + // Important. + > .imp#{$suffix} { + order: -1; + } + + // Columns, offsets. + @for $i from 1 through $cols { + > .col-#{$i}#{$suffix} { + width: $unit * $i; + } + + > .off-#{$i}#{$suffix} { + margin-left: $unit * $i; + } + } + + // Step through multipliers. + @each $multiplier in $multipliers { + + // Gutters. + $class: null; + + @if ($multiplier != 1) { + $class: '.gtr-' + ($multiplier * 100); + } + + &#{$class} { + margin-top: ($guttersRows * $multiplier * -1); + margin-left: ($guttersCols * $multiplier * -1); + + > * { + padding: ($guttersRows * $multiplier) 0 0 ($guttersCols * $multiplier); + } + + // Uniform. + &.gtr-uniform { + margin-top: $guttersCols * $multiplier * -1; + + > * { + padding-top: $guttersCols * $multiplier; + } + } + + } + + } + + } + + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/libs/_mixins.scss b/source/orm/library/static/library/assets/sass/libs/_mixins.scss new file mode 100644 index 0000000..a331483 --- /dev/null +++ b/source/orm/library/static/library/assets/sass/libs/_mixins.scss @@ -0,0 +1,78 @@ +/// Makes an element's :before pseudoelement a FontAwesome icon. +/// @param {string} $content Optional content value to use. +/// @param {string} $category Optional category to use. +/// @param {string} $where Optional pseudoelement to target (before or after). +@mixin icon($content: false, $category: regular, $where: before) { + + text-decoration: none; + + &:#{$where} { + + @if $content { + content: $content; + } + + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: inline-block; + font-style: normal; + font-variant: normal; + text-rendering: auto; + line-height: 1; + text-transform: none !important; + + @if ($category == brands) { + font-family: 'Font Awesome 5 Brands'; + } + @elseif ($category == solid) { + font-family: 'Font Awesome 5 Free'; + font-weight: 900; + } + @else { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; + } + + } + +} + +/// Applies padding to an element, taking the current element-margin value into account. +/// @param {mixed} $tb Top/bottom padding. +/// @param {mixed} $lr Left/right padding. +/// @param {list} $pad Optional extra padding (in the following order top, right, bottom, left) +/// @param {bool} $important If true, adds !important. +@mixin padding($tb, $lr, $pad: (0,0,0,0), $important: null) { + + @if $important { + $important: '!important'; + } + + $x: 0.1em; + + @if unit(_size(element-margin)) == 'rem' { + $x: 0.1rem; + } + + padding: ($tb + nth($pad,1)) ($lr + nth($pad,2)) max($x, $tb - _size(element-margin) + nth($pad,3)) ($lr + nth($pad,4)) #{$important}; + +} + +/// Encodes a SVG data URL so IE doesn't choke (via codepen.io/jakob-e/pen/YXXBrp). +/// @param {string} $svg SVG data URL. +/// @return {string} Encoded SVG data URL. +@function svg-url($svg) { + + $svg: str-replace($svg, '"', '\''); + $svg: str-replace($svg, '%', '%25'); + $svg: str-replace($svg, '<', '%3C'); + $svg: str-replace($svg, '>', '%3E'); + $svg: str-replace($svg, '&', '%26'); + $svg: str-replace($svg, '#', '%23'); + $svg: str-replace($svg, '{', '%7B'); + $svg: str-replace($svg, '}', '%7D'); + $svg: str-replace($svg, ';', '%3B'); + + @return url("data:image/svg+xml;charset=utf8,#{$svg}"); + +} \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/libs/_vars.scss b/source/orm/library/static/library/assets/sass/libs/_vars.scss new file mode 100644 index 0000000..8a844da --- /dev/null +++ b/source/orm/library/static/library/assets/sass/libs/_vars.scss @@ -0,0 +1,49 @@ +// Misc. + $misc: ( + z-index-base: 10000, + max-features: 20, + max-sidebar-links: 20 + ); + +// Duration. + $duration: ( + transition: 0.2s, + activation: 1s + ); + +// Size. + $size: ( + border-radius: 0.25em, + border-width: 1px, + element-height: 2.75em, + element-margin: 2em, + sidebar-width: 18em, + sidebar-height: 3.5em, // when <=large is active + inner-width: 75em + ); + +// Font. + $font: ( + family: (Arial, Helvetica, sans-serif), + family-fixed: ('Courier New', monospace), + weight: normal, + weight-bold: bold, + kerning-alt: 0.25em + ); + +// Palette. + $palette: ( + bg: #312450, + bg-alt: darken(#312450, 5), + fg: rgba(255,255,255,0.55), + fg-bold: #ffffff, + fg-light: rgba(255,255,255,0.35), + border: rgba(255,255,255,0.15), + border-bg: rgba(255,255,255,0.05), + accent1: #5e42a6, + accent1-alt: darken(#5e42a6, 10), + accent2: #5052b5, + accent2-alt: darken(#5052b5, 10), + accent3: #b74e91, + accent3-alt: darken(#b74e91, 10) + ); \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/libs/_vendor.scss b/source/orm/library/static/library/assets/sass/libs/_vendor.scss new file mode 100644 index 0000000..6599a3f --- /dev/null +++ b/source/orm/library/static/library/assets/sass/libs/_vendor.scss @@ -0,0 +1,376 @@ +// vendor.scss v1.0 | @ajlkn | MIT licensed */ + +// Vars. + + /// Vendor prefixes. + /// @var {list} + $vendor-prefixes: ( + '-moz-', + '-webkit-', + '-ms-', + '' + ); + + /// Properties that should be vendorized. + /// Data via caniuse.com, github.com/postcss/autoprefixer, and developer.mozilla.org + /// @var {list} + $vendor-properties: ( + + // Animation. + 'animation', + 'animation-delay', + 'animation-direction', + 'animation-duration', + 'animation-fill-mode', + 'animation-iteration-count', + 'animation-name', + 'animation-play-state', + 'animation-timing-function', + + // Appearance. + 'appearance', + + // Backdrop filter. + 'backdrop-filter', + + // Background image options. + 'background-clip', + 'background-origin', + 'background-size', + + // Box sizing. + 'box-sizing', + + // Clip path. + 'clip-path', + + // Filter effects. + 'filter', + + // Flexbox. + 'align-content', + 'align-items', + 'align-self', + 'flex', + 'flex-basis', + 'flex-direction', + 'flex-flow', + 'flex-grow', + 'flex-shrink', + 'flex-wrap', + 'justify-content', + 'order', + + // Font feature. + 'font-feature-settings', + 'font-language-override', + 'font-variant-ligatures', + + // Font kerning. + 'font-kerning', + + // Fragmented borders and backgrounds. + 'box-decoration-break', + + // Grid layout. + 'grid-column', + 'grid-column-align', + 'grid-column-end', + 'grid-column-start', + 'grid-row', + 'grid-row-align', + 'grid-row-end', + 'grid-row-start', + 'grid-template-columns', + 'grid-template-rows', + + // Hyphens. + 'hyphens', + 'word-break', + + // Masks. + 'mask', + 'mask-border', + 'mask-border-outset', + 'mask-border-repeat', + 'mask-border-slice', + 'mask-border-source', + 'mask-border-width', + 'mask-clip', + 'mask-composite', + 'mask-image', + 'mask-origin', + 'mask-position', + 'mask-repeat', + 'mask-size', + + // Multicolumn. + 'break-after', + 'break-before', + 'break-inside', + 'column-count', + 'column-fill', + 'column-gap', + 'column-rule', + 'column-rule-color', + 'column-rule-style', + 'column-rule-width', + 'column-span', + 'column-width', + 'columns', + + // Object fit. + 'object-fit', + 'object-position', + + // Regions. + 'flow-from', + 'flow-into', + 'region-fragment', + + // Scroll snap points. + 'scroll-snap-coordinate', + 'scroll-snap-destination', + 'scroll-snap-points-x', + 'scroll-snap-points-y', + 'scroll-snap-type', + + // Shapes. + 'shape-image-threshold', + 'shape-margin', + 'shape-outside', + + // Tab size. + 'tab-size', + + // Text align last. + 'text-align-last', + + // Text decoration. + 'text-decoration-color', + 'text-decoration-line', + 'text-decoration-skip', + 'text-decoration-style', + + // Text emphasis. + 'text-emphasis', + 'text-emphasis-color', + 'text-emphasis-position', + 'text-emphasis-style', + + // Text size adjust. + 'text-size-adjust', + + // Text spacing. + 'text-spacing', + + // Transform. + 'transform', + 'transform-origin', + + // Transform 3D. + 'backface-visibility', + 'perspective', + 'perspective-origin', + 'transform-style', + + // Transition. + 'transition', + 'transition-delay', + 'transition-duration', + 'transition-property', + 'transition-timing-function', + + // Unicode bidi. + 'unicode-bidi', + + // User select. + 'user-select', + + // Writing mode. + 'writing-mode', + + ); + + /// Values that should be vendorized. + /// Data via caniuse.com, github.com/postcss/autoprefixer, and developer.mozilla.org + /// @var {list} + $vendor-values: ( + + // Cross fade. + 'cross-fade', + + // Element function. + 'element', + + // Filter function. + 'filter', + + // Flexbox. + 'flex', + 'inline-flex', + + // Grab cursors. + 'grab', + 'grabbing', + + // Gradients. + 'linear-gradient', + 'repeating-linear-gradient', + 'radial-gradient', + 'repeating-radial-gradient', + + // Grid layout. + 'grid', + 'inline-grid', + + // Image set. + 'image-set', + + // Intrinsic width. + 'max-content', + 'min-content', + 'fit-content', + 'fill', + 'fill-available', + 'stretch', + + // Sticky position. + 'sticky', + + // Transform. + 'transform', + + // Zoom cursors. + 'zoom-in', + 'zoom-out', + + ); + +// Functions. + + /// Removes a specific item from a list. + /// @author Hugo Giraudel + /// @param {list} $list List. + /// @param {integer} $index Index. + /// @return {list} Updated list. + @function remove-nth($list, $index) { + + $result: null; + + @if type-of($index) != number { + @warn "$index: #{quote($index)} is not a number for `remove-nth`."; + } + @else if $index == 0 { + @warn "List index 0 must be a non-zero integer for `remove-nth`."; + } + @else if abs($index) > length($list) { + @warn "List index is #{$index} but list is only #{length($list)} item long for `remove-nth`."; + } + @else { + + $result: (); + $index: if($index < 0, length($list) + $index + 1, $index); + + @for $i from 1 through length($list) { + + @if $i != $index { + $result: append($result, nth($list, $i)); + } + + } + + } + + @return $result; + + } + + /// Replaces a substring within another string. + /// @author Hugo Giraudel + /// @param {string} $string String. + /// @param {string} $search Substring. + /// @param {string} $replace Replacement. + /// @return {string} Updated string. + @function str-replace($string, $search, $replace: '') { + + $index: str-index($string, $search); + + @if $index { + @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace); + } + + @return $string; + + } + + /// Replaces a substring within each string in a list. + /// @param {list} $strings List of strings. + /// @param {string} $search Substring. + /// @param {string} $replace Replacement. + /// @return {list} Updated list of strings. + @function str-replace-all($strings, $search, $replace: '') { + + @each $string in $strings { + $strings: set-nth($strings, index($strings, $string), str-replace($string, $search, $replace)); + } + + @return $strings; + + } + +// Mixins. + + /// Wraps @content in vendorized keyframe blocks. + /// @param {string} $name Name. + @mixin keyframes($name) { + + @-moz-keyframes #{$name} { @content; } + @-webkit-keyframes #{$name} { @content; } + @-ms-keyframes #{$name} { @content; } + @keyframes #{$name} { @content; } + + } + + /// Vendorizes a declaration's property and/or value(s). + /// @param {string} $property Property. + /// @param {mixed} $value String/list of value(s). + @mixin vendor($property, $value) { + + // Determine if property should expand. + $expandProperty: index($vendor-properties, $property); + + // Determine if value should expand (and if so, add '-prefix-' placeholder). + $expandValue: false; + + @each $x in $value { + @each $y in $vendor-values { + @if $y == str-slice($x, 1, str-length($y)) { + + $value: set-nth($value, index($value, $x), '-prefix-' + $x); + $expandValue: true; + + } + } + } + + // Expand property? + @if $expandProperty { + @each $vendor in $vendor-prefixes { + #{$vendor}#{$property}: #{str-replace-all($value, '-prefix-', $vendor)}; + } + } + + // Expand just the value? + @elseif $expandValue { + @each $vendor in $vendor-prefixes { + #{$property}: #{str-replace-all($value, '-prefix-', $vendor)}; + } + } + + // Neither? Treat them as a normal declaration. + @else { + #{$property}: #{$value}; + } + + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/main.scss b/source/orm/library/static/library/assets/sass/main.scss new file mode 100644 index 0000000..6cf6845 --- /dev/null +++ b/source/orm/library/static/library/assets/sass/main.scss @@ -0,0 +1,58 @@ +@import 'libs/vars'; +@import 'libs/functions'; +@import 'libs/mixins'; +@import 'libs/vendor'; +@import 'libs/breakpoints'; +@import 'libs/html-grid'; +@import 'fontawesome-all.min.css'; + +/* + Hyperspace by HTML5 UP + html5up.net | @ajlkn + Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +*/ + +// Breakpoints. + + @include breakpoints(( + xlarge: ( 1281px, 1680px ), + large: ( 981px, 1280px ), + medium: ( 737px, 980px ), + small: ( 481px, 736px ), + xsmall: ( 361px, 480px ), + xxsmall: ( null, 360px ) + )); + +// Base. + + @import 'base/reset'; + @import 'base/page'; + @import 'base/typography'; + +// Component. + + @import 'components/row'; + @import 'components/box'; + @import 'components/button'; + @import 'components/features'; + @import 'components/form'; + @import 'components/icon'; + @import 'components/image'; + @import 'components/list'; + @import 'components/actions'; + @import 'components/contact'; + @import 'components/icons'; + @import 'components/menu'; + @import 'components/section'; + @import 'components/split'; + @import 'components/spotlights'; + @import 'components/table'; + @import 'components/wrapper'; + +// Layout. + + @import 'layout/header'; + @import 'layout/wrapper'; + @import 'layout/footer'; + @import 'layout/sidebar'; + @import 'layout/intro'; \ No newline at end of file diff --git a/source/orm/library/static/library/assets/sass/noscript.scss b/source/orm/library/static/library/assets/sass/noscript.scss new file mode 100644 index 0000000..8a22e4f --- /dev/null +++ b/source/orm/library/static/library/assets/sass/noscript.scss @@ -0,0 +1,57 @@ +@import 'libs/vars'; +@import 'libs/functions'; +@import 'libs/mixins'; +@import 'libs/vendor'; +@import 'libs/breakpoints'; +@import 'libs/html-grid'; + +/* + Hyperspace by HTML5 UP + html5up.net | @ajlkn + Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +*/ + +/* Spotlights */ + + .spotlights { + > section { + > .image { + &:before { + opacity: 0 !important; + } + } + + > .content { + > .inner { + @include vendor('transform', 'none !important'); + opacity: 1 !important; + } + } + } + } + +/* Wrapper */ + + .wrapper { + > .inner { + opacity: 1 !important; + @include vendor('transform', 'none !important'); + } + } + +/* Sidebar */ + + #sidebar { + > .inner { + opacity: 1 !important; + } + + nav { + > ul { + > li { + @include vendor('transform', 'none !important'); + opacity: 1 !important; + } + } + } + } \ No newline at end of file diff --git a/source/orm/library/static/library/assets/webfonts/fa-brands-400.eot b/source/orm/library/static/library/assets/webfonts/fa-brands-400.eot new file mode 100644 index 0000000..e79f40f Binary files /dev/null and b/source/orm/library/static/library/assets/webfonts/fa-brands-400.eot differ diff --git a/source/orm/library/static/library/assets/webfonts/fa-brands-400.svg b/source/orm/library/static/library/assets/webfonts/fa-brands-400.svg new file mode 100644 index 0000000..ba0d850 --- /dev/null +++ b/source/orm/library/static/library/assets/webfonts/fa-brands-400.svg @@ -0,0 +1,3442 @@ + + + + + +Created by FontForge 20190112 at Tue Jun 4 15:16:44 2019 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/orm/library/static/library/assets/webfonts/fa-brands-400.ttf b/source/orm/library/static/library/assets/webfonts/fa-brands-400.ttf new file mode 100644 index 0000000..217ffe9 Binary files /dev/null and b/source/orm/library/static/library/assets/webfonts/fa-brands-400.ttf differ diff --git a/source/orm/library/static/library/assets/webfonts/fa-brands-400.woff b/source/orm/library/static/library/assets/webfonts/fa-brands-400.woff new file mode 100644 index 0000000..a2d8025 Binary files /dev/null and b/source/orm/library/static/library/assets/webfonts/fa-brands-400.woff differ diff --git a/source/orm/library/static/library/assets/webfonts/fa-brands-400.woff2 b/source/orm/library/static/library/assets/webfonts/fa-brands-400.woff2 new file mode 100644 index 0000000..e27b0bf Binary files /dev/null and b/source/orm/library/static/library/assets/webfonts/fa-brands-400.woff2 differ diff --git a/source/orm/library/static/library/assets/webfonts/fa-regular-400.eot b/source/orm/library/static/library/assets/webfonts/fa-regular-400.eot new file mode 100644 index 0000000..d62be2f Binary files /dev/null and b/source/orm/library/static/library/assets/webfonts/fa-regular-400.eot differ diff --git a/source/orm/library/static/library/assets/webfonts/fa-regular-400.svg b/source/orm/library/static/library/assets/webfonts/fa-regular-400.svg new file mode 100644 index 0000000..751083e --- /dev/null +++ b/source/orm/library/static/library/assets/webfonts/fa-regular-400.svg @@ -0,0 +1,803 @@ + + + + + +Created by FontForge 20190112 at Tue Jun 4 15:16:44 2019 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/orm/library/static/library/assets/webfonts/fa-regular-400.ttf b/source/orm/library/static/library/assets/webfonts/fa-regular-400.ttf new file mode 100644 index 0000000..eb3cb5e Binary files /dev/null and b/source/orm/library/static/library/assets/webfonts/fa-regular-400.ttf differ diff --git a/source/orm/library/static/library/assets/webfonts/fa-regular-400.woff b/source/orm/library/static/library/assets/webfonts/fa-regular-400.woff new file mode 100644 index 0000000..43b1a9a Binary files /dev/null and b/source/orm/library/static/library/assets/webfonts/fa-regular-400.woff differ diff --git a/source/orm/library/static/library/assets/webfonts/fa-regular-400.woff2 b/source/orm/library/static/library/assets/webfonts/fa-regular-400.woff2 new file mode 100644 index 0000000..b9344a7 Binary files /dev/null and b/source/orm/library/static/library/assets/webfonts/fa-regular-400.woff2 differ diff --git a/source/orm/library/static/library/assets/webfonts/fa-solid-900.eot b/source/orm/library/static/library/assets/webfonts/fa-solid-900.eot new file mode 100644 index 0000000..c77baa8 Binary files /dev/null and b/source/orm/library/static/library/assets/webfonts/fa-solid-900.eot differ diff --git a/source/orm/library/static/library/assets/webfonts/fa-solid-900.svg b/source/orm/library/static/library/assets/webfonts/fa-solid-900.svg new file mode 100644 index 0000000..627128b --- /dev/null +++ b/source/orm/library/static/library/assets/webfonts/fa-solid-900.svg @@ -0,0 +1,4649 @@ + + + + + +Created by FontForge 20190112 at Tue Jun 4 15:16:44 2019 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/orm/library/static/library/assets/webfonts/fa-solid-900.ttf b/source/orm/library/static/library/assets/webfonts/fa-solid-900.ttf new file mode 100644 index 0000000..c6c3dd4 Binary files /dev/null and b/source/orm/library/static/library/assets/webfonts/fa-solid-900.ttf differ diff --git a/source/orm/library/static/library/assets/webfonts/fa-solid-900.woff b/source/orm/library/static/library/assets/webfonts/fa-solid-900.woff new file mode 100644 index 0000000..77c1786 Binary files /dev/null and b/source/orm/library/static/library/assets/webfonts/fa-solid-900.woff differ diff --git a/source/orm/library/static/library/assets/webfonts/fa-solid-900.woff2 b/source/orm/library/static/library/assets/webfonts/fa-solid-900.woff2 new file mode 100644 index 0000000..e30fb67 Binary files /dev/null and b/source/orm/library/static/library/assets/webfonts/fa-solid-900.woff2 differ diff --git a/source/orm/library/static/library/images/no-image.png b/source/orm/library/static/library/images/no-image.png new file mode 100644 index 0000000..3e2820a Binary files /dev/null and b/source/orm/library/static/library/images/no-image.png differ diff --git a/source/orm/library/static/library/images/pic01.jpg b/source/orm/library/static/library/images/pic01.jpg new file mode 100644 index 0000000..ef01b3f Binary files /dev/null and b/source/orm/library/static/library/images/pic01.jpg differ diff --git a/source/orm/library/static/library/images/pic02.jpg b/source/orm/library/static/library/images/pic02.jpg new file mode 100644 index 0000000..e78cb6e Binary files /dev/null and b/source/orm/library/static/library/images/pic02.jpg differ diff --git a/source/orm/library/static/library/images/pic03.jpg b/source/orm/library/static/library/images/pic03.jpg new file mode 100644 index 0000000..e039b33 Binary files /dev/null and b/source/orm/library/static/library/images/pic03.jpg differ diff --git a/source/orm/library/static/library/images/pic04.jpg b/source/orm/library/static/library/images/pic04.jpg new file mode 100644 index 0000000..664a4ef Binary files /dev/null and b/source/orm/library/static/library/images/pic04.jpg differ diff --git a/source/orm/library/static/library/images/pic05.jpg b/source/orm/library/static/library/images/pic05.jpg new file mode 100644 index 0000000..2c66361 Binary files /dev/null and b/source/orm/library/static/library/images/pic05.jpg differ diff --git a/source/orm/library/static/library/images/pic06.jpg b/source/orm/library/static/library/images/pic06.jpg new file mode 100644 index 0000000..eb05dca Binary files /dev/null and b/source/orm/library/static/library/images/pic06.jpg differ diff --git a/source/orm/library/templates/library/base/elements.html b/source/orm/library/templates/library/base/elements.html new file mode 100644 index 0000000..92ec3dc --- /dev/null +++ b/source/orm/library/templates/library/base/elements.html @@ -0,0 +1,363 @@ + + + + + Elements - Hyperspace by HTML5 UP + + + + + + + + + + + +
+ + +
+
+

Elements

+ + +
+

Text

+

This is bold and this is strong. This is italic and this is emphasized. + This is superscript text and this is subscript text. + This is underlined and this is code: for (;;) { ... }. Finally, this is a link.

+
+

Nunc lacinia ante nunc ac lobortis. Interdum adipiscing gravida odio porttitor sem non mi integer non faucibus ornare mi ut ante amet placerat aliquet. Volutpat eu sed ante lacinia sapien lorem accumsan varius montes viverra nibh in adipiscing blandit tempus accumsan.

+
+

Heading Level 2

+

Heading Level 3

+

Heading Level 4

+
+

Blockquote

+
Fringilla nisl. Donec accumsan interdum nisi, quis tincidunt felis sagittis eget tempus euismod. Vestibulum ante ipsum primis in faucibus vestibulum. Blandit adipiscing eu felis iaculis volutpat ac adipiscing accumsan faucibus. Vestibulum ante ipsum primis in faucibus lorem ipsum dolor sit amet nullam adipiscing eu felis.
+

Preformatted

+
i = 0;
+
+while (!deck.isInOrder()) {
+    print 'Iteration ' + i;
+    deck.shuffle();
+    i++;
+}
+
+print 'It took ' + i + ' iterations to sort the deck.';
+
+ + +
+

Lists

+
+
+

Unordered

+
    +
  • Dolor pulvinar etiam.
  • +
  • Sagittis adipiscing.
  • +
  • Felis enim feugiat.
  • +
+

Alternate

+
    +
  • Dolor pulvinar etiam.
  • +
  • Sagittis adipiscing.
  • +
  • Felis enim feugiat.
  • +
+
+
+

Ordered

+
    +
  1. Dolor pulvinar etiam.
  2. +
  3. Etiam vel felis viverra.
  4. +
  5. Felis enim feugiat.
  6. +
  7. Dolor pulvinar etiam.
  8. +
  9. Etiam vel felis lorem.
  10. +
  11. Felis enim et feugiat.
  12. +
+

Icons

+ +
+
+

Actions

+
+
+ + + + +
+
+ + +
+
+
+ + +
+

Table

+

Default

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameDescriptionPrice
Item OneAnte turpis integer aliquet porttitor.29.99
Item TwoVis ac commodo adipiscing arcu aliquet.19.99
Item Three Morbi faucibus arcu accumsan lorem.29.99
Item FourVitae integer tempus condimentum.19.99
Item FiveAnte turpis integer aliquet porttitor.29.99
100.00
+
+ +

Alternate

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameDescriptionPrice
Item OneAnte turpis integer aliquet porttitor.29.99
Item TwoVis ac commodo adipiscing arcu aliquet.19.99
Item Three Morbi faucibus arcu accumsan lorem.29.99
Item FourVitae integer tempus condimentum.19.99
Item FiveAnte turpis integer aliquet porttitor.29.99
100.00
+
+
+ + +
+

Buttons

+ + + + + +
    +
  • Disabled
  • +
  • Disabled
  • +
+
+ + +
+

Form

+
+
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
    +
  • +
  • +
+
+
+
+
+ + +
+

Image

+

Fit

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

Left & Right

+

Fringilla nisl. Donec accumsan interdum nisi, quis tincidunt felis sagittis eget. tempus euismod. Vestibulum ante ipsum primis in faucibus vestibulum. Blandit adipiscing eu felis iaculis volutpat ac adipiscing accumsan eu faucibus. Integer ac pellentesque praesent tincidunt felis sagittis eget. tempus euismod. Vestibulum ante ipsum primis in faucibus vestibulum. Blandit adipiscing eu felis iaculis volutpat ac adipiscing accumsan eu faucibus. Integer ac pellentesque praesent. Donec accumsan interdum nisi, quis tincidunt felis sagittis eget. tempus euismod. Vestibulum ante ipsum primis in faucibus vestibulum. Blandit adipiscing eu felis iaculis volutpat ac adipiscing accumsan eu faucibus. Integer ac pellentesque praesent tincidunt felis sagittis eget. tempus euismod. Vestibulum ante ipsum primis in faucibus vestibulum. Blandit adipiscing eu felis iaculis volutpat ac adipiscing accumsan eu faucibus. Integer ac pellentesque praesent. Blandit adipiscing eu felis iaculis volutpat ac adipiscing accumsan eu faucibus. Integer ac pellentesque praesent tincidunt felis sagittis eget. tempus euismod. Vestibulum ante ipsum primis in faucibus vestibulum. Blandit adipiscing eu felis iaculis volutpat ac adipiscing accumsan eu faucibus. Integer ac pellentesque praesent.

+

Fringilla nisl. Donec accumsan interdum nisi, quis tincidunt felis sagittis eget. tempus euismod. Vestibulum ante ipsum primis in faucibus vestibulum. Blandit adipiscing eu felis iaculis volutpat ac adipiscing accumsan eu faucibus. Integer ac pellentesque praesent tincidunt felis sagittis eget. tempus euismod. Vestibulum ante ipsum primis in faucibus vestibulum. Blandit adipiscing eu felis iaculis volutpat ac adipiscing accumsan eu faucibus. Integer ac pellentesque praesent. Donec accumsan interdum nisi, quis tincidunt felis sagittis eget. tempus euismod. Vestibulum ante ipsum primis in faucibus vestibulum. Blandit adipiscing eu felis iaculis volutpat ac adipiscing accumsan eu faucibus. Integer ac pellentesque praesent tincidunt felis sagittis eget. tempus euismod. Vestibulum ante ipsum primis in faucibus vestibulum. Blandit adipiscing eu felis iaculis volutpat ac adipiscing accumsan eu faucibus. Integer ac pellentesque praesent. Blandit adipiscing eu felis iaculis volutpat ac adipiscing accumsan eu faucibus. Integer ac pellentesque praesent tincidunt felis sagittis eget. tempus euismod. Vestibulum ante ipsum primis in faucibus vestibulum. Blandit adipiscing eu felis iaculis volutpat ac adipiscing accumsan eu faucibus. Integer ac pellentesque praesent.

+
+ +
+
+ +
+ + +
+
+ +
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/source/orm/library/templates/library/base/generic.html b/source/orm/library/templates/library/base/generic.html new file mode 100644 index 0000000..4ce1cd9 --- /dev/null +++ b/source/orm/library/templates/library/base/generic.html @@ -0,0 +1,69 @@ +{% load static %} + + + + + {% block title %}ORM Demonstration Project{% endblock %} + + + + + + + + + + + +
+ + +
+
+ {% if messages %} + {% for message in messages %} +

{{ message }}

+ {% endfor %} + {% endif %} +

{% block content_title %}{% endblock content_title %}

+ {% block content %}{% endblock content %} +
+
+ +
+ + +
+
+ +
+
+ + + + + + + + + + + + diff --git a/source/orm/library/templates/library/base/index.html b/source/orm/library/templates/library/base/index.html new file mode 100644 index 0000000..4cbe7a2 --- /dev/null +++ b/source/orm/library/templates/library/base/index.html @@ -0,0 +1,210 @@ +{% load static %} + + + + + Hyperspace by HTML5 UP + + + + + + + + + + + +
+ + +
+
+

Hyperspace

+

Just another fine responsive site template designed by HTML5 UP
+ and released for free under the Creative Commons.

+ +
+
+ + +
+
+ +
+
+

Sed ipsum dolor

+

Phasellus convallis elit id ullamcorper pulvinar. Duis aliquam turpis mauris, eu ultricies erat malesuada quis. Aliquam dapibus.

+ +
+
+
+
+ +
+
+

Feugiat consequat

+

Phasellus convallis elit id ullamcorper pulvinar. Duis aliquam turpis mauris, eu ultricies erat malesuada quis. Aliquam dapibus.

+ +
+
+
+
+ +
+
+

Ultricies aliquam

+

Phasellus convallis elit id ullamcorper pulvinar. Duis aliquam turpis mauris, eu ultricies erat malesuada quis. Aliquam dapibus.

+ +
+
+
+
+ + +
+
+

What we do

+

Phasellus convallis elit id ullamcorper pulvinar. Duis aliquam turpis mauris, eu ultricies erat malesuada quis. Aliquam dapibus, lacus eget hendrerit bibendum, urna est aliquam sem, sit amet imperdiet est velit quis lorem.

+
+
+ +

Lorem ipsum amet

+

Phasellus convallis elit id ullam corper amet et pulvinar. Duis aliquam turpis mauris, sed ultricies erat dapibus.

+
+
+ +

Aliquam sed nullam

+

Phasellus convallis elit id ullam corper amet et pulvinar. Duis aliquam turpis mauris, sed ultricies erat dapibus.

+
+
+ +

Sed erat ullam corper

+

Phasellus convallis elit id ullam corper amet et pulvinar. Duis aliquam turpis mauris, sed ultricies erat dapibus.

+
+
+ +

Veroeros quis lorem

+

Phasellus convallis elit id ullam corper amet et pulvinar. Duis aliquam turpis mauris, sed ultricies erat dapibus.

+
+
+ +

Urna quis bibendum

+

Phasellus convallis elit id ullam corper amet et pulvinar. Duis aliquam turpis mauris, sed ultricies erat dapibus.

+
+
+ +

Aliquam urna dapibus

+

Phasellus convallis elit id ullam corper amet et pulvinar. Duis aliquam turpis mauris, sed ultricies erat dapibus.

+
+
+ +
+
+ + +
+
+

Get in touch

+

Phasellus convallis elit id ullamcorper pulvinar. Duis aliquam turpis mauris, eu ultricies erat malesuada quis. Aliquam dapibus, lacus eget hendrerit bibendum, urna est aliquam sem, sit amet imperdiet est velit quis lorem.

+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+
+
+
+ +
+ + +
+
+ +
+
+ + + + + + + + + + + + diff --git a/source/orm/library/templates/library/includes/book-list.html b/source/orm/library/templates/library/includes/book-list.html new file mode 100644 index 0000000..142615b --- /dev/null +++ b/source/orm/library/templates/library/includes/book-list.html @@ -0,0 +1,15 @@ +

{{ title }}

+
    + {% for book in books %} +
  • + {{ book.name }} +
    + + {{ book.year }} + by {% for author in book.authors.all %} + {{ author.get_full_name }} (born {{ author.birth_date }}) + {% endfor %} + +
  • + {% endfor %} +
diff --git a/source/orm/library/templates/library/includes/person-list.html b/source/orm/library/templates/library/includes/person-list.html new file mode 100644 index 0000000..54543a6 --- /dev/null +++ b/source/orm/library/templates/library/includes/person-list.html @@ -0,0 +1,17 @@ +{% load static %} +

{{ title }}

+
    + {% for person in people %} +
  • + {% if person.picture %} + {{ person.get_full_name }} + {% else %} + {{ person.get_full_name }} + {% endif %} + {{ person.first_name }} {{ person.last_name }} + {{ person.get_role_display|title }} + Edit +
  • + {% endfor %} +
diff --git a/source/orm/library/templates/library/index-page.html b/source/orm/library/templates/library/index-page.html new file mode 100644 index 0000000..ecbe770 --- /dev/null +++ b/source/orm/library/templates/library/index-page.html @@ -0,0 +1,9 @@ +{% extends "library/base/generic.html" %} + +{% block title %}Library Dashboard{% endblock title %} +{% block content_title %}Dashboard of library{% endblock content_title %} + +{% block content %} + {% include "library/includes/person-list.html" with title="Registered people" %} + {% include "library/includes/book-list.html" with title="Known books" %} +{% endblock %} diff --git a/source/orm/library/templates/library/person-edit-page.html b/source/orm/library/templates/library/person-edit-page.html new file mode 100644 index 0000000..cf22564 --- /dev/null +++ b/source/orm/library/templates/library/person-edit-page.html @@ -0,0 +1,20 @@ +{% extends "library/base/generic.html" %} + +{% block title %}Edit {{ person.get_full_name }}{% endblock title %} +{% block content_title %}Edition of {{ person.get_full_name }}{% endblock content_title %} + +{% block content %} + {% if person.picture %} + {{ person.get_full_name }} + {% endif %} +
+ {% csrf_token %} + + + {{ form.as_table }} + +
+ {{ form.first_name }} + +
+{% endblock %} diff --git a/source/orm/library/urls.py b/source/orm/library/urls.py new file mode 100644 index 0000000..0e98e49 --- /dev/null +++ b/source/orm/library/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from library.views import view_index, edit_person + +app_name = "library" +urlpatterns = [ + path("", view_index, name="index"), + path("person//edit/", edit_person, name="person-edit"), +] diff --git a/source/orm/library/views/__init__.py b/source/orm/library/views/__init__.py new file mode 100644 index 0000000..a52e687 --- /dev/null +++ b/source/orm/library/views/__init__.py @@ -0,0 +1,2 @@ +from .base import * +from .person import * diff --git a/source/orm/library/views/base.py b/source/orm/library/views/base.py new file mode 100644 index 0000000..10586d6 --- /dev/null +++ b/source/orm/library/views/base.py @@ -0,0 +1,13 @@ +from annoying.decorators import render_to +from django.http import HttpRequest +from django.views.decorators.cache import cache_page + +from library.models import Person, Book + + +@cache_page(30) +@render_to("library/index-page.html") +def view_index(request: HttpRequest) -> dict: # noqa + people = Person.objects.all() + books = Book.objects.all() + return {"people": people, "books": books} diff --git a/source/orm/library/views/person.py b/source/orm/library/views/person.py new file mode 100644 index 0000000..2d4c129 --- /dev/null +++ b/source/orm/library/views/person.py @@ -0,0 +1,30 @@ +from typing import Union + +from annoying.decorators import render_to +from django.contrib import messages +from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned +from django.http import HttpRequest, Http404, HttpResponseRedirect, HttpResponse + +from library.forms import PersonForm +from library.models import Person + + +@render_to("library/person-edit-page.html") +def edit_person(request: HttpRequest, uuid: str) -> Union[dict, HttpResponse]: # noqa + try: + person = Person.objects.get(uuid=uuid) + form = ( + PersonForm(instance=person) + if request.method == "GET" + else PersonForm(request.POST, files=request.FILES, instance=person) + ) + if form.is_valid(): + # Save the form data in the database, automatically saves files + recorded = form.save(commit=True) + # Use the messages application to display a message on the next view. + messages.success(request, f"{recorded} was successfully updated.") + return HttpResponseRedirect("") + return {"person": person, "form": form} + except (ObjectDoesNotExist, MultipleObjectsReturned): + # Ask Django view system to render the 404 page. + raise Http404() diff --git a/source/orm/manage.py b/source/orm/manage.py new file mode 100644 index 0000000..98bc644 --- /dev/null +++ b/source/orm/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', 'orm.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/source/orm/media/pictures/ben-richards.jpg b/source/orm/media/pictures/ben-richards.jpg new file mode 100644 index 0000000..6948783 Binary files /dev/null and b/source/orm/media/pictures/ben-richards.jpg differ diff --git a/source/orm/media/pictures/hans-gruber.jpg b/source/orm/media/pictures/hans-gruber.jpg new file mode 100644 index 0000000..73e4c25 Binary files /dev/null and b/source/orm/media/pictures/hans-gruber.jpg differ diff --git a/source/orm/media/pictures/jill-valentine.jpg b/source/orm/media/pictures/jill-valentine.jpg new file mode 100644 index 0000000..375d890 Binary files /dev/null and b/source/orm/media/pictures/jill-valentine.jpg differ diff --git a/source/orm/media/pictures/lily-aldrin.jpg b/source/orm/media/pictures/lily-aldrin.jpg new file mode 100644 index 0000000..752f2cc Binary files /dev/null and b/source/orm/media/pictures/lily-aldrin.jpg differ diff --git a/source/orm/orm/__init__.py b/source/orm/orm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/orm/orm/asgi.py b/source/orm/orm/asgi.py new file mode 100644 index 0000000..39f570b --- /dev/null +++ b/source/orm/orm/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for orm 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', 'orm.settings') + +application = get_asgi_application() diff --git a/source/orm/orm/settings.py b/source/orm/orm/settings.py new file mode 100644 index 0000000..0ccf5d2 --- /dev/null +++ b/source/orm/orm/settings.py @@ -0,0 +1,141 @@ +""" +Django settings for orm project. + +Generated by 'django-admin startproject' using Django 3.2. + +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 + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" + +# 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 = "django-insecure-r=!!xd$ss4&14j#pj&!@w0ks!x@6-t*f*v03iu+a+)rjqza$@z" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django_extensions", + "library", +] + +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 = "orm.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 = "orm.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "database.sqlite3", + } +} + + +# 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 = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = "/static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# Media paths + +MEDIA_URL = "/media/" +MEDIA_ROOT = BASE_DIR / "media" + +# Caching setup +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + } +} diff --git a/source/orm/orm/urls.py b/source/orm/orm/urls.py new file mode 100644 index 0000000..70535ad --- /dev/null +++ b/source/orm/orm/urls.py @@ -0,0 +1,24 @@ +"""orm 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.conf import settings +from django.conf.urls.static import static +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path("admin/", admin.site.urls), + path("library/", include("library.urls", namespace="library")), +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/source/orm/orm/wsgi.py b/source/orm/orm/wsgi.py new file mode 100644 index 0000000..41a2706 --- /dev/null +++ b/source/orm/orm/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for orm 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', 'orm.settings') + +application = get_wsgi_application() diff --git a/source/requirements.txt b/source/requirements.txt new file mode 100644 index 0000000..09ce4b2 --- /dev/null +++ b/source/requirements.txt @@ -0,0 +1,13 @@ +django<4.3 # Our web framework +dj-cmd # Tool to ease command line with django +django-annoying # for render decorator + +black # code auto-format +aiohttp # dependency for blackd +aiohttp-cors # dependency for blackd +pytz # timezone management +pillow # image management +jupyter # notepad for python +ipython # advanced python shell +django-extensions # extensions for Django +pymemcache # to demonstrate caching diff --git a/source/templating/.idea/Templating.iml b/source/templating/.idea/Templating.iml new file mode 100644 index 0000000..ae4ee0a --- /dev/null +++ b/source/templating/.idea/Templating.iml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/templating/.idea/inspectionProfiles/Project_Default.xml b/source/templating/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..09dbac8 --- /dev/null +++ b/source/templating/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/source/templating/.idea/inspectionProfiles/profiles_settings.xml b/source/templating/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/source/templating/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/source/templating/.idea/modules.xml b/source/templating/.idea/modules.xml new file mode 100644 index 0000000..86eff7f --- /dev/null +++ b/source/templating/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/source/templating/.idea/workspace.xml b/source/templating/.idea/workspace.xml new file mode 100644 index 0000000..19d58a6 --- /dev/null +++ b/source/templating/.idea/workspace.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/source/templating/demonstration/__init__.py b/source/templating/demonstration/__init__.py new file mode 100644 index 0000000..00044df --- /dev/null +++ b/source/templating/demonstration/__init__.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class DemonstrationConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'demonstration' + + +default_app_config = "demonstration.DemonstrationConfig" diff --git a/source/templating/demonstration/static/demonstration/demonstration.css b/source/templating/demonstration/static/demonstration/demonstration.css new file mode 100644 index 0000000..36c2416 --- /dev/null +++ b/source/templating/demonstration/static/demonstration/demonstration.css @@ -0,0 +1,48 @@ +:root { + --head-bg-color: #222; + --head-fg-color: #fff; + --head-ln-color: #3cf; +} + +html, body { + height: 100%; + min-height: 100%; + width: 100%; +} + +body { + margin: 0; + display: grid; + grid-template-areas: "header" "content" "footer"; + grid-template-rows: auto 1fr auto; + grid-template-columns: 100%; + font-family: "Roboto", "Lucida Grande", "DejaVu Sans", "Bitstream Vera Sans", Verdana, Arial, sans-serif; +} + +section#header { + grid-area: header; +} + +section#content { + grid-area: content; +} + +section#footer { + grid-area: footer; +} + + +section#header, section#footer { + background-color: var(--head-bg-color); + color: var(--head-fg-color); +} + +section#header a, section#footer a { + color: var(--head-ln-color); + text-decoration: none; +} + +div.body { + width: 1024px; + margin: 1.5em auto; +} diff --git a/source/templating/demonstration/templates/demonstration/basic/control.html b/source/templating/demonstration/templates/demonstration/basic/control.html new file mode 100644 index 0000000..54664ea --- /dev/null +++ b/source/templating/demonstration/templates/demonstration/basic/control.html @@ -0,0 +1,17 @@ + + + + + Template with control structures + + +Use of a control structure to parse a list of values: +
    + {% for item in values %} + {% if item != 3 %} {# Do not show the item with a value of 3 #} +
  • Item n.{{ forloop.counter }}: {{ item }}
  • + {% endif %} + {% endfor %} +
+ + diff --git a/source/templating/demonstration/templates/demonstration/basic/filters.html b/source/templating/demonstration/templates/demonstration/basic/filters.html new file mode 100644 index 0000000..5455367 --- /dev/null +++ b/source/templating/demonstration/templates/demonstration/basic/filters.html @@ -0,0 +1,18 @@ + + + + + Template with filtering + + +

Use different filters on values:

+{# Filter for strings #} +
    + {% for item in strings %} +
  • {{ item|title }}
  • + {% endfor %} +
+{# Filter for dates #} +

{{ moment|date:"Y m d" }}

+ + diff --git a/source/templating/demonstration/templates/demonstration/basic/plain.txt b/source/templating/demonstration/templates/demonstration/basic/plain.txt new file mode 100644 index 0000000..8bb18a4 --- /dev/null +++ b/source/templating/demonstration/templates/demonstration/basic/plain.txt @@ -0,0 +1,7 @@ +This is a plain text file rendered as a template. +Django can use any text file to render a template, whether it's an XML, HTML, RST or TXT file. + +The only specificity in rendering a template is that we can use values, control structures and +a few tricks to make the rendering more dynamic. + +Templates are generally used in Django to render HTML, text and custom email bodies. diff --git a/source/templating/demonstration/templates/demonstration/basic/variables.html b/source/templating/demonstration/templates/demonstration/basic/variables.html new file mode 100644 index 0000000..02ce4e2 --- /dev/null +++ b/source/templating/demonstration/templates/demonstration/basic/variables.html @@ -0,0 +1,22 @@ + + + + + Template with variables + + +This is an HTML file rendered as a template, with interpolated variables. +Django can use any text file to render a template, whether it's an XML, HTML, RST or TXT file. + +The only specificity in rendering a template is that we can use values, control structures and +a few tricks to make the rendering more dynamic. + +Templates are generally used in Django to render HTML, text and custom email bodies. + +Variables passed to the template context: +
    +
  • Variable1: {{ variable1 }}
  • +
  • Variable2: {{ variable2 }}
  • +
+ + diff --git a/source/templating/demonstration/templates/demonstration/index.html b/source/templating/demonstration/templates/demonstration/index.html new file mode 100644 index 0000000..416ae02 --- /dev/null +++ b/source/templating/demonstration/templates/demonstration/index.html @@ -0,0 +1,19 @@ + + + + + Templating demonstration + + +

Sections of templating

+ + + diff --git a/source/templating/demonstration/templates/demonstration/inheritance/base.html b/source/templating/demonstration/templates/demonstration/inheritance/base.html new file mode 100644 index 0000000..c49db64 --- /dev/null +++ b/source/templating/demonstration/templates/demonstration/inheritance/base.html @@ -0,0 +1,33 @@ +{% load static %} {# The load tag enables template tags from other Django apps #} + + + + + {% block title %} | Django Demonstration{% endblock title %} + + + + +
+
+ {% block body %} + Base content in a overridable block. + {% endblock body %} +
+
+ + + + diff --git a/source/templating/demonstration/templates/demonstration/inheritance/inherited.html b/source/templating/demonstration/templates/demonstration/inheritance/inherited.html new file mode 100644 index 0000000..bd2958c --- /dev/null +++ b/source/templating/demonstration/templates/demonstration/inheritance/inherited.html @@ -0,0 +1,8 @@ +{% extends "demonstration/inheritance/base.html" %} +{% load static %} {# The load tag enables template tags from other Django apps #} + +{% block title %}Inherited page{{ block.super }}{% endblock title %} + +{% block body %} +Change body block from the original in the overridden block. +{% endblock body %} diff --git a/source/templating/demonstration/templates/demonstration/translation/translated.html b/source/templating/demonstration/templates/demonstration/translation/translated.html new file mode 100644 index 0000000..751cce5 --- /dev/null +++ b/source/templating/demonstration/templates/demonstration/translation/translated.html @@ -0,0 +1,9 @@ +{% extends "demonstration/inheritance/base.html" %} +{% load i18n %} {# The i18n tag enables template tags for internationalization #} + +{% block title %}{% translate "Inherited page" %}{{ block.super }}{% endblock title %} + +{% block body %} +

{% translate "First string for translation." %}

+

{% blocktranslate %}Second string with placeholder: {{ value }}{% endblocktranslate %}

+{% endblock body %} diff --git a/source/templating/demonstration/templatetags/__init__.py b/source/templating/demonstration/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/templating/demonstration/templatetags/demonstration.py b/source/templating/demonstration/templatetags/demonstration.py new file mode 100644 index 0000000..94e6a81 --- /dev/null +++ b/source/templating/demonstration/templatetags/demonstration.py @@ -0,0 +1,36 @@ +from datetime import datetime + +from django import template +from django.template.defaultfilters import stringfilter +from pytz import UTC + +register = template.Library() + + +@register.filter +@stringfilter +def decorate(value, _arg=None): + """ + Template filter to decorate the value with hyphens. + + Args: + value: Any value that can be displayed and filtered. + _arg: Option passed for the filter, if any + + Returns: + Decorated value, like ---- + + """ + return f"--{value}--" + + +@register.simple_tag +def current_time(): + """ + Template tag that returns the current time, with the UTC timezone. + + Returns: + Now in UTC. + + """ + return datetime.now(tz=UTC) diff --git a/source/templating/demonstration/urls.py b/source/templating/demonstration/urls.py new file mode 100644 index 0000000..d4e7502 --- /dev/null +++ b/source/templating/demonstration/urls.py @@ -0,0 +1,14 @@ +from django.urls import path + +from demonstration.views import * + + +urlpatterns = [ + path("untemplated/", view_untemplated, name="untemplated"), + path("basic/plain/", view_basic_plain, name="basic-plain"), + path("basic/variables/", view_basic_variables, name="basic-variables"), + path("basic/control/", view_basic_control, name="basic-control"), + path("basic/control/class/", BasicTemplateView.as_view(), name="basic-control-class"), + path("basic/filters/", view_basic_filters, name="basic-filters"), + path("inheritance/base/", view_inheritance_base, name="inheritance-base"), +] diff --git a/source/templating/demonstration/views/__init__.py b/source/templating/demonstration/views/__init__.py new file mode 100644 index 0000000..6365d8f --- /dev/null +++ b/source/templating/demonstration/views/__init__.py @@ -0,0 +1,3 @@ +from .index import * +from .basic import * +from .inheritance import * diff --git a/source/templating/demonstration/views/basic.py b/source/templating/demonstration/views/basic.py new file mode 100644 index 0000000..278c853 --- /dev/null +++ b/source/templating/demonstration/views/basic.py @@ -0,0 +1,55 @@ +from annoying.decorators import render_to +from django.http import HttpRequest, HttpResponse +from django.views.generic import TemplateView +from pytz import UTC + + +def view_untemplated(request: HttpRequest) -> HttpResponse: + """Return a base view for demonstration purposes.""" + return HttpResponse("Page content returned") + + +@render_to("demonstration/basic/plain.txt") +def view_basic_plain(request: HttpRequest) -> dict: + """Show a plain text rendered as a template.""" + return {} + + +@render_to("demonstration/basic/variables.html") +def view_basic_variables(request: HttpRequest) -> dict: + """Show a template using variable interpolation.""" + return {"variable1": "Text number 1.", "variable2": 599.31} + + +@render_to("demonstration/basic/control.html") +def view_basic_control(request: HttpRequest) -> dict: + """Use control structures.""" + return {"values": [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]} + + +@render_to("demonstration/basic/filters.html") +def view_basic_filters(request: HttpRequest) -> dict: + """Use template filters.""" + from datetime import datetime + + return {"strings": ["text in lowercase", "basic text."], "moment": datetime(2020, 1, 1, tzinfo=UTC)} + + +@render_to("demonstration/basic/tags.html") +def view_basic_tags(request: HttpRequest) -> dict: + """Use template tags.""" + return {"numbers": [1, 2, 3, 4, 5, 6, 7, 8]} + + +class BasicTemplateView(TemplateView): + """ + Equivalent of the `view_basic_control` view. + + See Also: + Documentation to know how to use class-based views: + - https://docs.djangoproject.com/fr/3.2/topics/class-based-views/ + + """ + + template_name = "demonstration/basic/control.html" + extra_context = {"values": [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]} diff --git a/source/templating/demonstration/views/index.py b/source/templating/demonstration/views/index.py new file mode 100644 index 0000000..17a82f8 --- /dev/null +++ b/source/templating/demonstration/views/index.py @@ -0,0 +1,8 @@ +from annoying.decorators import render_to +from django.http import HttpRequest + + +@render_to("demonstration/index.html") +def view_index(request: HttpRequest) -> dict: + """Show the list of all sections.""" + return {} diff --git a/source/templating/demonstration/views/inheritance.py b/source/templating/demonstration/views/inheritance.py new file mode 100644 index 0000000..d25f155 --- /dev/null +++ b/source/templating/demonstration/views/inheritance.py @@ -0,0 +1,8 @@ +from annoying.decorators import render_to +from django.http import HttpRequest + + +@render_to("demonstration/inheritance/base.html") +def view_inheritance_base(request: HttpRequest) -> dict: + """Show the list of all sections.""" + return {} diff --git a/source/templating/manage.py b/source/templating/manage.py new file mode 100644 index 0000000..cf1e904 --- /dev/null +++ b/source/templating/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', 'templating.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/source/templating/templating/__init__.py b/source/templating/templating/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/templating/templating/asgi.py b/source/templating/templating/asgi.py new file mode 100644 index 0000000..1808a73 --- /dev/null +++ b/source/templating/templating/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for templating 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', 'templating.settings') + +application = get_asgi_application() diff --git a/source/templating/templating/settings.py b/source/templating/templating/settings.py new file mode 100644 index 0000000..957e0ff --- /dev/null +++ b/source/templating/templating/settings.py @@ -0,0 +1,83 @@ +""" +Django settings for templating project. + +Generated by 'django-admin startproject' using Django 3.2. + +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/ +""" + +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/3.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-e08sa^6gqf-m!z5jmgt(hba^tg25+ce^6f#qx41o*2$q0=h+6%" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.staticfiles", + "demonstration", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "templating.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", + ], + }, + }, +] + +WSGI_APPLICATION = "templating.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = {} + + +# Internationalization +# https://docs.djangoproject.com/en/3.2/topics/i18n/ + +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" +USE_I18N = True +USE_L10N = True +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = "/static/" diff --git a/source/templating/templating/urls.py b/source/templating/templating/urls.py new file mode 100644 index 0000000..bb79394 --- /dev/null +++ b/source/templating/templating/urls.py @@ -0,0 +1,23 @@ +"""templating 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.urls import path, include + +from demonstration.views import view_index + +urlpatterns = [ + path("demonstration/", include("demonstration.urls")), + path("", view_index, name="index"), +] diff --git a/source/templating/templating/wsgi.py b/source/templating/templating/wsgi.py new file mode 100644 index 0000000..fed6311 --- /dev/null +++ b/source/templating/templating/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for templating 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', 'templating.settings') + +application = get_wsgi_application() diff --git a/source/translation/.idea/Translation.iml b/source/translation/.idea/Translation.iml new file mode 100644 index 0000000..6600464 --- /dev/null +++ b/source/translation/.idea/Translation.iml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/translation/.idea/inspectionProfiles/Project_Default.xml b/source/translation/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..09dbac8 --- /dev/null +++ b/source/translation/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/source/translation/.idea/inspectionProfiles/profiles_settings.xml b/source/translation/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/source/translation/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/source/translation/.idea/modules.xml b/source/translation/.idea/modules.xml new file mode 100644 index 0000000..68c256d --- /dev/null +++ b/source/translation/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/source/translation/.idea/workspace.xml b/source/translation/.idea/workspace.xml new file mode 100644 index 0000000..055225f --- /dev/null +++ b/source/translation/.idea/workspace.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/source/translation/README.md b/source/translation/README.md new file mode 100644 index 0000000..b229386 --- /dev/null +++ b/source/translation/README.md @@ -0,0 +1,11 @@ +# Procedure for generating translations + +1. Define the default language in your `settings.py`. +1. All your strings should be in the default language. +1. Optional: Define available languages by adding the `LANGUAGES` setting if you want. + +# For a Django app + +- Create `locale` directory in your Django app. Django won't create it by itself. +- Check your app is registered in `INSTALLED_APPS`. +- `makemessages -l ` diff --git a/source/translation/birds/__init__.py b/source/translation/birds/__init__.py new file mode 100644 index 0000000..2e035f1 --- /dev/null +++ b/source/translation/birds/__init__.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class BirdsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "birds" + + +default_app_config = "birds.BirdsConfig" diff --git a/source/translation/birds/admin/__init__.py b/source/translation/birds/admin/__init__.py new file mode 100644 index 0000000..285f613 --- /dev/null +++ b/source/translation/birds/admin/__init__.py @@ -0,0 +1 @@ +from .bird import BirdAdmin diff --git a/source/translation/birds/admin/bird.py b/source/translation/birds/admin/bird.py new file mode 100644 index 0000000..d65cdb4 --- /dev/null +++ b/source/translation/birds/admin/bird.py @@ -0,0 +1,15 @@ +from django.contrib import admin + +from birds.models import Bird + + +@admin.register(Bird) +class BirdAdmin(admin.ModelAdmin): + """ + Administration configuration for birds. + + """ + + list_display = ["id", "uuid", "vernacular_name", "scientific_name", "weight"] + list_editable = ["vernacular_name", "scientific_name", "weight"] + readonly_fields = ["uuid"] diff --git a/source/translation/birds/locale/fr/LC_MESSAGES/django.po b/source/translation/birds/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 0000000..de2bb14 --- /dev/null +++ b/source/translation/birds/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,92 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-04-23 16:28+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: birds/models/bird.py:13 +msgid "UUID" +msgstr "UUID" + +#: birds/models/bird.py:14 +msgid "vernacular name" +msgstr "nom courant" + +#: birds/models/bird.py:15 +msgid "scientific name" +msgstr "nom scientifique" + +#: birds/models/bird.py:16 +msgid "weight" +msgstr "poids" + +#: birds/models/bird.py:17 +msgctxt "bird (adjective)" +msgid "extinct" +msgstr "disparu" + +#: birds/models/bird.py:20 +msgid "bird" +msgstr "oiseau" + +#: birds/models/bird.py:21 +msgid "birds" +msgstr "oiseaux" + +#: birds/templates/birds/translation-page.html:7 +msgid "example page" +msgstr "page d'exemple" + +#: birds/templates/birds/translation-page.html:11 +msgid "The crazy dog" +msgstr "Le chien fou" + +#: birds/templates/birds/translation-page.html:12 +msgctxt "task completion" +msgid "done" +msgstr "terminée" + +#: birds/templates/birds/translation-page.html:13 +#, fuzzy, python-format +#| msgid "The page title is %(text_title)s." +msgid "The page title is %(str_title)s." +msgstr "Le titre de la page est %(str_title)s." + +#: birds/templates/birds/translation-page.html:15 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " There is %(counter)s bird on the roof.\n" +#| " " +#| msgid_plural "" +#| "\n" +#| " There are %(counter)s birds on the roof.\n" +#| " " +msgid "" +"\n" +" There is %(counter)s bird on the roof.\n" +" " +msgid_plural "" +"\n" +" There are %(counter)s birds on the roof.\n" +" " +msgstr[0] "" +"\n" +"Il y a %(counter)s oiseau sur le toit." +msgstr[1] "" +"\n" +"Il y a %(counter)s oiseaux sur le toit." diff --git a/source/translation/birds/migrations/0001_initial.py b/source/translation/birds/migrations/0001_initial.py new file mode 100644 index 0000000..8cc0528 --- /dev/null +++ b/source/translation/birds/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2 on 2021-04-15 16:24 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Bird', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, verbose_name='UUID')), + ('vernacular_name', models.CharField(max_length=64, unique=True, verbose_name='vernacular name')), + ('scientific_name', models.CharField(max_length=64, unique=True, verbose_name='scientific name')), + ('weight', models.PositiveIntegerField(default=100, verbose_name='weight')), + ('is_extinct', models.BooleanField(default=False, verbose_name='extinct')), + ], + options={ + 'verbose_name': 'bird', + 'verbose_name_plural': 'birds', + }, + ), + ] diff --git a/source/translation/birds/migrations/__init__.py b/source/translation/birds/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/translation/birds/models/__init__.py b/source/translation/birds/models/__init__.py new file mode 100644 index 0000000..e96d1f7 --- /dev/null +++ b/source/translation/birds/models/__init__.py @@ -0,0 +1 @@ +from .bird import Bird diff --git a/source/translation/birds/models/bird.py b/source/translation/birds/models/bird.py new file mode 100644 index 0000000..6ec8473 --- /dev/null +++ b/source/translation/birds/models/bird.py @@ -0,0 +1,22 @@ +from uuid import uuid4 + +from django.db import models +from django.utils.translation import gettext_lazy as _, pgettext_lazy + + +class Bird(models.Model): + """ + Bird definition. + + """ + + uuid = models.UUIDField(default=uuid4, verbose_name=_("UUID")) + vernacular_name = models.CharField(max_length=64, unique=True, verbose_name=_("vernacular name")) + scientific_name = models.CharField(max_length=64, unique=True, verbose_name=_("scientific name")) + weight = models.PositiveIntegerField(default=100, verbose_name=_("weight")) + is_extinct = models.BooleanField(default=False, verbose_name=pgettext_lazy("bird (adjective)", "extinct")) + + class Meta: + verbose_name = _("bird") + verbose_name_plural = _("birds") + app_label = "birds" diff --git a/source/translation/birds/templates/birds/translation-page.html b/source/translation/birds/templates/birds/translation-page.html new file mode 100644 index 0000000..582679c --- /dev/null +++ b/source/translation/birds/templates/birds/translation-page.html @@ -0,0 +1,22 @@ +{% load i18n %} +{# Context: bird_count (int) #} + + + + + {% translate "example page" as str_title %} + {{ str_title|title }} + + +

{% translate "The crazy dog" %}

+

Task status: {% translate "done" context "task completion" %}

+

{% blocktranslate %}The page title is {{ str_title }}.{% endblocktranslate %}

+

+ {% blocktranslate count counter=bird_count %} + There is {{ counter }} bird on the roof. + {% plural %} + There are {{ counter }} birds on the roof. + {% endblocktranslate %} +

+ + diff --git a/source/translation/birds/tests.py b/source/translation/birds/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/source/translation/birds/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/source/translation/birds/views/__init__.py b/source/translation/birds/views/__init__.py new file mode 100644 index 0000000..9b5ed21 --- /dev/null +++ b/source/translation/birds/views/__init__.py @@ -0,0 +1 @@ +from .base import * diff --git a/source/translation/birds/views/base.py b/source/translation/birds/views/base.py new file mode 100644 index 0000000..5cd2f2c --- /dev/null +++ b/source/translation/birds/views/base.py @@ -0,0 +1,9 @@ +from annoying.decorators import render_to +from django.conf import settings +from django.http import HttpRequest + + +@render_to("birds/translation-page.html") +def view_translation_page(request: HttpRequest) -> dict: + print(settings.LANGUAGES) + return {"bird_count": 1} diff --git a/source/translation/database.sqlite3 b/source/translation/database.sqlite3 new file mode 100644 index 0000000..c8935d6 Binary files /dev/null and b/source/translation/database.sqlite3 differ diff --git a/source/translation/manage.py b/source/translation/manage.py new file mode 100644 index 0000000..3713280 --- /dev/null +++ b/source/translation/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", "translation.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/source/translation/translation/__init__.py b/source/translation/translation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/translation/translation/asgi.py b/source/translation/translation/asgi.py new file mode 100644 index 0000000..e36ad88 --- /dev/null +++ b/source/translation/translation/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for translation 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", "translation.settings") + +application = get_asgi_application() diff --git a/source/translation/translation/settings.py b/source/translation/translation/settings.py new file mode 100644 index 0000000..8ada93f --- /dev/null +++ b/source/translation/translation/settings.py @@ -0,0 +1,143 @@ +""" +Django settings for translation project. + +Generated by 'django-admin startproject' using Django 3.2. + +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/ +""" + +from pathlib import Path +import os + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" + +# 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 = "django-insecure-r=!!xd$ss4&14j#pj&!@w0ks!x@6-t*f*v03iu+a+)rjqza$@z" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django_extensions", + "birds", +] + +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", + "django.middleware.locale.LocaleMiddleware", # Added to enable translation in views +] + +ROOT_URLCONF = "translation.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 = "translation.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "database.sqlite3", + } +} + + +# 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 = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = "/static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# Media paths + +MEDIA_URL = "/media/" +MEDIA_ROOT = BASE_DIR / "media" + +# Caching setup +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", + "LOCATION": "127.0.0.1:11211", + } +} diff --git a/source/translation/translation/urls.py b/source/translation/translation/urls.py new file mode 100644 index 0000000..e3580b0 --- /dev/null +++ b/source/translation/translation/urls.py @@ -0,0 +1,26 @@ +"""translation 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.conf import settings +from django.conf.urls.static import static +from django.contrib import admin +from django.urls import path, include + +from birds.views import view_translation_page + +urlpatterns = [ + path("admin/", admin.site.urls), + path("", view_translation_page), +] diff --git a/source/translation/translation/wsgi.py b/source/translation/translation/wsgi.py new file mode 100644 index 0000000..e99a7f6 --- /dev/null +++ b/source/translation/translation/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for translation 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", "translation.settings") + +application = get_wsgi_application()