Compare commits
24 Commits
Etape2
...
updatedNgi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d8e1035e8 | ||
|
|
781b9972f3 | ||
|
|
0acb6ee271 | ||
|
|
bf241435be | ||
|
|
d6f422ea17 | ||
|
|
204c890eeb | ||
|
|
51aeb014f4 | ||
|
|
1d8792b6e0 | ||
|
|
6c08c063e1 | ||
|
|
2291a85b79 | ||
|
|
0976663ea6 | ||
|
|
d7ac18bb62 | ||
|
|
eb4d3caa8d | ||
|
|
94c9a56a6a | ||
|
|
e37f99144b | ||
|
|
fda23a778e | ||
|
|
7fe633c476 | ||
|
|
180fd75947 | ||
|
|
a0e07036c1 | ||
|
|
8141dcee8c | ||
|
|
4490d222c7 | ||
|
|
004e414802 | ||
|
|
9f820449af | ||
|
|
8bc54e787b |
376
Consignes.md
Normal file
376
Consignes.md
Normal file
@@ -0,0 +1,376 @@
|
||||
# Projet TLC: Ops et Cloud
|
||||
|
||||
Le but du projet est de mettre en place une chaîne de déploiement automatisée d'une application Web développée dans des technologies cloud-native sur une infrastructure que vous pilotez.
|
||||
|
||||
L'application est une application de type doodle like qui offre la possibilité de déployer aussi une intégration avec un service de type etherpad pour la prise de notes de réunions. Nous souhaitons que vous fassiez au minimum le travail pour:
|
||||
- Faciliter son déploiement à l'aide de containers
|
||||
- Faciliter sa configurabilité au déploiement (serveur smtp à utiliser, connexion à l'etherpad, ...)
|
||||
- Faciliter sa sécurisation au moment du déploiement
|
||||
|
||||
>Voici le [repo de code source](https://github.com/barais/doodlestudent) de cette application. Vous pouvez regarder le code source, le *back* est développé en [quarkus.io](https://quarkus.io/) et le front en [angular](https://angular.io/).
|
||||
> <details>
|
||||
> <summary>Vidéos de présentation</summary>
|
||||
>
|
||||
> - [Présentation des fonctionnalités de l'application](https://drive.google.com/file/d/1IxQC6lq0UPP_-n4IGt2TGvTRIO9M8enn/view?usp=sharing)
|
||||
> - [Présentation de l'architecture de l'application](https://drive.google.com/file/d/16gqFYmhwok8GWgiOskYjP-oTeP_sDwu-/view?usp=sharing)
|
||||
> - [Revue de code de l'application](https://drive.google.com/file/d/10RLkHWjJxnLi1ltRmNDCIymW9eM7Tn87/view?usp=sharing)
|
||||
> - [Compréhension du projet]()
|
||||
>
|
||||
> </details>
|
||||
|
||||
Les tâches 1 à 4 décrivent le minimum à faire pour ce projet.
|
||||
Vous devez ensuite choisir au moins deux des aventures proposées pour aller plus loin.
|
||||
|
||||
## Aventures au choix
|
||||
|
||||
- **Aventure 1**: Mettre en place un mécanisme de déploiement continu qui permette de déployer la nouvelle version de l'application automatiquement si un nouveau commit est activé sur le back ou le front. L'idée de ce travail est d'explorer les liens entre pratique devops (intégration/déploiement continu) et outil de déploiement en mode cloud native.
|
||||
- **Aventure 2**: Mettre en place des outils de monitoring avancés de cette application et des dashboards associés à ce monitoring.
|
||||
- **Aventure 3**: Utiliser K8S comme orchestrateur pour le déploiement de nos containers.
|
||||
- **Aventure 4**: Mettre en place un identity provider et/ou une passerelle d'API pour faire de l'accounting sur l'API de l'application.
|
||||
|
||||
# Modalités de rendu
|
||||
|
||||
Tous les rendus TLC se font à travers le gitlab de l'ISTIC : https://gitlab2.istic.univ-rennes1.fr/
|
||||
|
||||
Si vous ne l'avez pas déjà fait, créez sur Gitlab ISTIC un groupe nommé `TLC_2025_<votre_nom>_<votre_prenom>`
|
||||
|
||||
Pour chaque TP, vous devrez créer un projet dans ce groupe (il est fortement conseillé de fork le repo du projet en le plaçant dans votre groupe gitlab), nommé `TP<numero>_<votre_nom>_<votre_prenom>` et le projet finale nommé `Projet_<votre_nom>_<votre_prenom>`
|
||||
|
||||
Pour chaque TP, vous devrez ajouter votre enseignant en tant que membre du projet avec le rôle de "Reporter" pour permettre la correction.
|
||||
|
||||
## Backlog du projet
|
||||
|
||||
### Niveau 1: minimum à faire
|
||||
|
||||
[//]: # (- **Tâche 0**: Créer une machine virtuelle (https://vm.istic.univ-rennes1.fr/ séléctionner **ubuntu20** comme image de base) et demandez l'accès externe vers le port *80* (http) et *443* (https) de votre machine virtuelle par le [helpdesk, catégorie ISTIC-ESIR - Tous problèmes informatiques](https://assistance.univ-rennes1.fr/), l'accès au port 22 se fera au travers du [VPN](https://istic.univ-rennes1.fr/intranet/services#section-4). Partager moi l'adresse IP de la machine et le sous domaine souhaité [ici](https://forms.gle/a7EfKraasGpTYAXa6) (sous domaine de *diverse-team.fr*). (Je mets jour quand c'est fait le [google doc joint](https://docs.google.com/spreadsheets/d/14Yv-3ujq9OL0HuYkj4Wlp9Piw5075b6iEKSf-yJWytc/edit?usp=sharing)))
|
||||
|
||||
|
||||
- **Tâche 1**: Fournir un ou plusieurs Dockerfile(s) et un ou plusieurs docker-compose(s) pour cette application permettant de facilement déployer et configurer l'application. (Faites un fork du projet avant.)
|
||||
|
||||
<details>
|
||||
<summary>Liens utiles</summary>
|
||||
|
||||
- [Synchroniser votre fork GitHub](https://nearsoft.com/blog/how-to-synchronize-your-github-fork/)
|
||||
- [Documentation Docker Compose](https://docs.docker.com/compose/)
|
||||
- [Documentation Dockerfile](https://docs.docker.com/engine/reference/builder/)
|
||||
|
||||
</details>
|
||||
|
||||
:warning: Vous aurez du mal à avoir un fonctionnement correct à cette étape-là. En effet, le code du front va faire ces requêtes *REST* à la même adresse que celui qui lui a fourni le code html, css et js pour éviter les problèmes de CORS. Il est donc nécessaire de se forcer à configurer le serveur nginx qui délivre le front pour faire *proxy_pass* quand il reçoit une requête sur la route */api* ou une sous-route de */api*. Ne vous inquiétez pas, on configure cela à l'étape suivante.
|
||||
|
||||
- **Tâche 2**: Configurer le serveur Web du Front pour qu'il soit capable de servir de point d'entrée à l'ensemble des requêtes puis qu'il l
|
||||
- es *route* vers le bon service de Back. Il est possible de mettre en place un serveur Web spécifique pour gérer ce routing (on le nomme alors la gateway d'API).
|
||||
- On peut aussi dans notre cas se servir du fichier nginx du front pour router les requêtes.
|
||||
|
||||
<details>
|
||||
<summary>Exemples de fichiers de configuration nginx</summary>
|
||||
|
||||
```txt
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name doodle.tlc.fr;
|
||||
|
||||
location /api {
|
||||
proxy_pass http://api:8080/api;
|
||||
proxy_set_header Host $http_host;
|
||||
}
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html?$args;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```txt
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name myadmin.tlc.fr;
|
||||
|
||||
location / {
|
||||
proxy_pass http://myadmin:80;
|
||||
proxy_set_header Host $http_host;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```txt
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name pad.tlc.fr;
|
||||
|
||||
location / {
|
||||
proxy_pass http://etherpad:9001;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
- **Tâche 3**: Déployer correctement une première fois votre application en configurant convenablement la partie DNS pour le reverse proxy, letsencrypt
|
||||
pour le certificat côté serveur et ufw pour le firewall sur votre machine virtuelle.
|
||||
|
||||
En gros, vous allez prendre votre fichier docker-compose, votre fichier de configuration nginx, votre front et mettre cela sur votre VM. Mettre les bonnes variables de configuration
|
||||
dans ces deux fichiers. Vous aurez besoin soit de builder les images sur la VM soit de pushé vos images sur le docker_hub afin de pouvoir les *puller* depuis votre VM.
|
||||
|
||||
- **Tâche 4**: Documenter, à l'aide d'un diagramme de déploiement UML ou autre notation, le déploiement réalisé pour le moment sur votre machine virtuelle.
|
||||
|
||||
<details>
|
||||
<summary>Vidéos d'aide</summary>
|
||||
|
||||
- [Construction d'un DockerFile pour le front]()
|
||||
- [Construction d'un DockerFile pour le back]()
|
||||
- [Définition d'un docker-compose pour le déploiement de l'application]()
|
||||
- [Déploiement de l'application avec K8S]() ([note de présentation]())
|
||||
|
||||
</details>
|
||||
|
||||
Vous pouvez maintenant choisir au minimum deux de ces 4 aventures :
|
||||
|
||||
### Aventure 1: Déploiement continu à l'aide de gitlabCI et déploiement à l'aide de Docker
|
||||
|
||||
- **Aventure 1**: Mettre en place un mécanisme de déploiement continu qui permette de déployer la nouvelle version de l'application automatiquement si un nouveau commit est activé sur le back ou le front. Vous pourrez mettre en place votre outil d'intégration continue sur la même VM même si c'est une pratique que nous ne suivrions pas pour un déploiement réel. Vous avez le choix pour l'outil d'intégration continue.
|
||||
|
||||
<details>
|
||||
<summary>Outils d'intégration continue</summary>
|
||||
|
||||
- Vous pouvez utiliser gitlabCI de l'ISTIC si vous avez mis le code dans un repo du gitlab de l'ISTIC. Vous pouvez alors créer votre propre runner gitlab ci (https://docs.gitlab.com/runner/) ou utiliser travis sur github ou les runners de gitlab.com. Vous pouvez aussi déployer un jenkins sur votre machine si vous préférez.
|
||||
|
||||
</details>
|
||||
|
||||
### Aventure 2: Chaîne de monitoring de l'application en production
|
||||
|
||||
- **Aventure 2**: Mettre en place la chaîne de monitoring de l'application à l'aide de Promotheus et grafana comme front. Mettre en place le sous domaine et le certificat letsencrypt pour grafana. Mettre en place munin pour la surveillance de votre machine virtuelle.
|
||||
|
||||
<details>
|
||||
<summary>Liens utiles</summary>
|
||||
|
||||
- [Monitoring Quarkus avec Prometheus](http://www.mastertheboss.com/soa-cloud/quarkus/monitoring-quarkus-with-prometheus)
|
||||
|
||||
</details>
|
||||
|
||||
### Aventure 3: Utilisation de K8S comme orchestrateur d'un petit cluster
|
||||
|
||||
> Pour cette étape, il est conseillé de se construire son infra avec Vagrant at microk8s. Le playbook réalisé dans le TP ansible peut vous aider à mettre en place l'environnement.
|
||||
|
||||
- **Aventure 3**: Utiliser kubernetes pour déployer l'ensemble des micro-services. Le but est de pouvoir comparer et comprendre les abstractions supplémentaires fournies par kubernetes par rapport à un simple déploiement d'une application sur un seul noeud à l'aide de docker et docker-compose. En particulier, on souhaite ajouter de la redondance pour la partie back de l'application (redondance uniquement sur ce micro-service là. Surtout pas sur la BD relationnelle, c'est plus compliqué et pas complètement transparent). Déployer donc microk8s sur votre VM. Utiliser kubernetes pour mettre en place un minimum de redondance pour le back de l'application (on ne clusterisera pas la BD dans un premier temps).
|
||||
|
||||
<details>
|
||||
<summary>Liens utiles</summary>
|
||||
|
||||
- [Microk8s](https://microk8s.io/)
|
||||
- Notes sur microk8s : voir fichier k8s.pdf
|
||||
|
||||
</details>
|
||||
|
||||
### Aventure 4: Mise en place d'un outil d'authentification et d'une passerelle d'API
|
||||
|
||||
- **Aventure 4**: Mettre en place [keycloak](https://www.keycloak.org/getting-started/getting-started-docker) pour un accès sécurisé à vos ressources Web et/ou une passerelle d'API pour faire de l'accounting sur l'API de l'application. Je vous propose l'utilisation d'[ambassador](https://www.getambassador.io/).
|
||||
|
||||
<details>
|
||||
<summary>Liens utiles</summary>
|
||||
|
||||
- [Ambassador API Gateway avec microk8s](https://blog.getambassador.io/explore-the-ambassador-api-gateway-with-microk8s-f75a7a295113)
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
:warning: Il va falloir lire beaucoup de documentation, poser beaucoup de questions, faire des schémas pour comprendre ce que vous êtes en train de faire. Utilisez les tickets github dès que quelque chose ne marche pas comme vous le souhaitez.
|
||||
|
||||
## Rendu Projet
|
||||
|
||||
Pour le rendu, je propose d'utiliser le formulaire suivant: https://forms.gle/NsBtwLK81CtYBDsN7. Comme d'habitude, un petit readme à la racine des différents repos.
|
||||
|
||||
Pensez bien à ranger votre dépôt git, à mettre des commentaires dans vos fichiers de configuration, à bien documenter vos choix et à expliquer ce que vous avez fait.
|
||||
|
||||
Au minimum, il faut avoir dans votre git les rendus pour les tâches 1 à 4.
|
||||
- Plusieurs Dockerfile(s) pour le front et le back. Les propositions de Dockerfile _light_ seront mieux évaluées.
|
||||
- Un docker-compose pour le déploiement de toute la stack: front, back, etc.
|
||||
- Un fichier de configuration nginx pour le front.
|
||||
- Explication de la stack accompagnée d'un diagramme.
|
||||
|
||||
- Pour chaque aventure choisies, les fichiers de configuration et les scripts nécessaires.
|
||||
|
||||
|
||||
### Date de rendu **16/02 23h59 ferme**
|
||||
|
||||
...
|
||||
|
||||
## Other relevant videos
|
||||
|
||||
<details>
|
||||
<summary>Vidéos</summary>
|
||||
|
||||
- [What are cloud native apps](https://www.youtube.com/watch?v=fp9_ubiKqFU)
|
||||
- [Cloud native applications](https://www.youtube.com/watch?v=nyQAkR84RHM&ab_channel=ParallelWireless)
|
||||
- [Cloud Native DevOps Explained](https://www.youtube.com/watch?v=FzERTm_j2wE)
|
||||
- [What are Microservices?](https://www.youtube.com/watch?v=CdBtNQZH8a4)
|
||||
- [Advantages of Using OpenAPI to design APIs](https://www.youtube.com/watch?v=Fyuk50fFllM&ab_channel=SmartBear)
|
||||
- [What is DevOps?](https://www.youtube.com/watch?v=_I94-tJlovg&ab_channel=RackspaceTechnology)
|
||||
- [What is Continuous Integration?](https://www.youtube.com/watch?v=1er2cjUq1UI&frags=wn)
|
||||
- [What is Continuous Delivery?](https://www.youtube.com/watch?v=2TTU5BB-k9U)
|
||||
- [What is Infrastructure as Code?](https://www.youtube.com/watch?v=zWw2wuiKd5o&list=PLOspHqNVtKACSagAEeIY20NMVLNeQ1ZJx&index=3&ab_channel=IBMCloud)
|
||||
- [Cloud-native Java for this decade with Quarkus](https://www.youtube.com/watch?v=KJZt0uc5OiM)
|
||||
- [Principles of Antifragile Software](https://www.youtube.com/watch?v=FXGWSyRtipY&ab_channel=MartinMonperrus)
|
||||
- [Mastering Chaos - A Netflix Guide to Microservices](https://www.youtube.com/watch?v=CZ3wIuvmHeM&ab_channel=InfoQ)
|
||||
- [Getting started with Chaos Engineering](https://www.youtube.com/watch?v=3Oc4-cMkGJY&ab_channel=NDCConferences)
|
||||
|
||||
</details>
|
||||
|
||||
## References
|
||||
|
||||
<details>
|
||||
<summary>Références</summary>
|
||||
|
||||
[1] BALALAIE, Armin, HEYDARNOORI, Abbas, et JAMSHIDI, Pooyan. Microservices architecture enables devops: Migration to a cloud-native architecture. Ieee Software, 2016, vol. 33, no 3, p. 42-52.
|
||||
|
||||
[2] https://www.redhat.com/en/resources/eight-steps-cloud-native-application-development-brief
|
||||
|
||||
[3] D. S. Linthicum, "Cloud-Native Applications and Cloud Migration: The Good, the Bad, and the Points Between," in IEEE Cloud Computing, vol. 4, no. 5, pp. 12-14, September/October 2017, doi: 10.1109/MCC.2017.4250932.
|
||||
|
||||
</details>
|
||||
|
||||
## Questions réponses
|
||||
|
||||
<details>
|
||||
<summary>Liens utiles</summary>
|
||||
|
||||
- [Comprendre la notion de nom de domaine]()
|
||||
- [Mettre en place un serveur Web](https://www.youtube.com/watch?v=cfJh8vdKuQU&list=PLjwdMgw5TTLUnvhOKLcpCG8ORQsfE7uB4&ab_channel=Grafikart.fr)
|
||||
- [Configurer nginx](https://www.youtube.com/watch?v=YD_exb9aPZU&ab_channel=Grafikart.fr)
|
||||
- [Nginx: Mettre en place letsencrypt](https://www.youtube.com/watch?v=NXyE3mayrtg&ab_channel=Grafikart.fr)
|
||||
- [Nginx : Se protéger des attaques Flood](https://www.youtube.com/watch?v=ge768xOLQJs&list=PLjwdMgw5TTLUnvhOKLcpCG8ORQsfE7uB4&index=22&ab_channel=Grafikart.fr)
|
||||
- [Fail2ban c'est quoi](https://www.youtube.com/watch?v=-rmK50PbqCY&ab_channel=Grafikart.fr)
|
||||
- [un firewall c'est quoi: configuration de ufw](https://www.grafikart.fr/tutoriels/ufw-696), [vidéo](https://www.youtube.com/watch?v=XnfMAU3zIew&ab_channel=Grafikart.fr)
|
||||
- [une autre présentation de docker](https://www.youtube.com/watch?v=XgKOC6X8W28&ab_channel=Grafikart.fr)
|
||||
- [Pas mal de tutos assez bien fait](https://www.youtube.com/c/grafikart/playlists)
|
||||
- [POC en local avec Docker et Traefik](https://www.youtube.com/watch?v=QF4ZF857m44&ab_channel=TechworldwithNana)
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Appendix</summary>
|
||||
|
||||
- :warning: **Tâche 2 bis (NON Obligatoire, UNIQUEMENT pour ceux que cela intéressent)**,
|
||||
|
||||
Pour servir d'API Gwateway, il y a un super projet que j'adore qui s'appelle **bunkerweb**. C'est un container docker avec nginx qui se configure au travers de variable d'environement et qui inclus de base plein de mécanisme de sécurité pour nginx, la génération des certificats ... mais il entraine pas mal de doc à lire.
|
||||
|
||||
:warning: **UNIQUEMENT pour ceux que cela intéressent (j'insite )**
|
||||
|
||||
configurer convenablement https://github.com/bunkerity/bunkerweb au sein de votre docker-compose pour sécuriser votre application. Tester localement le déploiement de l'application.
|
||||
|
||||
**nginx bunkerity/bunkerweb** va jouer le rôle de passerelle d'API, c'est-à-dire que toutes les requêtes passent par lui et il a la charge de fournir les fichiers html, css et js du front, mais aussi de servir de proxy vers les autres services.
|
||||
|
||||
Pour cela, il est nécessaire de configurer finement ce serveur nginx qui tourne dans ce container. Il faudra *monter* ce fichier de configuration au moment du lancement de ce container. (voir https://github.com/bunkerity/bunkerized-nginx/blob/700dfc0184f8d12e15f3ca3d6e5ca08befccd561/examples/wordpress/docker-compose.yml#L14)
|
||||
|
||||
Vous trouverez ci-dessous un exemple de fichier de configuration nginx (fichier api.conf par exemple dans le répertoire *server-confs* là où se trouve votre docker-compose.yml).
|
||||
|
||||
```txt
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# L'idée ici est que toute requête qui arrive sur /api ou une sous
|
||||
# route d'API passe par ici et en gros on fait un if selon que cette
|
||||
# requête est arrivé sur l'adresse IP en demandant à parler à
|
||||
# doodle.diverse-team.fr ou à phpmyadmin.diverse-team.fr ou à
|
||||
# pad.diverse-team.fr
|
||||
location /api {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
if ($host = doodle.diverse-team.fr) {
|
||||
proxy_pass http://doodleback:8080$request_uri;
|
||||
}
|
||||
if ($host = pad.diverse-team.fr) {
|
||||
|
||||
proxy_pass http://etherpad:9001$request_uri;
|
||||
}
|
||||
if ($host = phpmyadmin.diverse-team.fr) {
|
||||
proxy_pass http://myadmin$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# L'idée ici est que toute requête qui arrive sur / ou une sous
|
||||
# route de / qui n'a pas été traité avant passe par ici et en gros
|
||||
# on fait un if selon que cette requête est arrivé sur l'adresse IP
|
||||
# en demandant à parler à phpmyadmin.diverse-team.fr ou à
|
||||
# pad.diverse-team.fr et pour tout le reste on fourni les ressources
|
||||
# statiques du front
|
||||
|
||||
location / {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_redirect off;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
|
||||
if ($host = pad.diverse-team.fr) {
|
||||
|
||||
proxy_pass http://etherpad:9001$request_uri;
|
||||
}
|
||||
if ($host = phpmyadmin.diverse-team.fr) {
|
||||
proxy_pass http://myadmin$request_uri;
|
||||
}
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Il est aussi nécessaire de configurer finement les variables d'environement de ce container. Je vous met un exemple avec des explications
|
||||
|
||||
```txt
|
||||
environment:
|
||||
# Ensemble des endpoints pour lequel il générera des certificats
|
||||
- SERVER_NAME=doodle.<Your_Domain>.fr pad.<Your_Domain>.fr phpmyadmin.<Your_Domain>.fr
|
||||
- SERVE_FILES=yes
|
||||
- DISABLE_DEFAULT_SERVER=no
|
||||
# Transmission de l'IP publique vers les containers
|
||||
- PROXY_REAL_IP=yes
|
||||
# - MAX_CLIENT_SIZE=100m
|
||||
# - USE_ANTIBOT=captcha
|
||||
# Configuration de letencrypt
|
||||
- AUTO_LETS_ENCRYPT=yes
|
||||
# redirect du 80 vers 443 automatique
|
||||
- REDIRECT_HTTP_TO_HTTPS=yes
|
||||
- USE_LIMIT_REQ=no
|
||||
# Désactivation http2
|
||||
- HTTP2=no
|
||||
# Paramétrage des entêtes http
|
||||
- FEATURE_POLICY=accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; camera 'none'; display-capture 'none'; document-domain 'none'; encrypted-media 'none'; fullscreen 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; picture-in-picture 'none'; speaker 'none'; sync-xhr 'self'; usb 'none'; vibrate 'none'; vr 'none'
|
||||
myadmin:
|
||||
image: phpmyadmin
|
||||
# ports:
|
||||
# - "8082:80"
|
||||
environment:
|
||||
# pour phpmyadmin url externe
|
||||
- PMA_ABSOLUTE_URI=https://<YOUR_PHPMYADMIN_URL>
|
||||
```
|
||||
|
||||
</details>
|
||||
381
README.md
381
README.md
@@ -1,372 +1,19 @@
|
||||
# Projet TLC: Ops et Cloud
|
||||
# Projet de TLC
|
||||
|
||||
Le but du projet est de mettre en place une chaîne de déploiement automatisée d'une application Web développée dans des technologies cloud-native sur une infrastructure que vous pilotez.
|
||||
## Compte Rendu
|
||||
|
||||
L'application est une application de type doodle like qui offre la possibilité de déployer aussi une intégration avec un service de type etherpad pour la prise de notes de réunions. Nous souhaitons que vous fassiez au minimum le travail pour:
|
||||
- Faciliter son déploiement à l'aide de containers
|
||||
- Faciliter sa configurabilité au déploiement (serveur smtp à utiliser, connexion à l'etherpad, ...)
|
||||
- Faciliter sa sécurisation au moment du déploiement
|
||||
Réalisé par :
|
||||
- Alexandre CHEVALIER
|
||||
- Minh Vu
|
||||
- Alexis LEBOEUF
|
||||
- Thibaut ROCHAS
|
||||
- Yann BLANDIN
|
||||
- Yvan DOUIS
|
||||
- Axel BELLAY
|
||||
|
||||
>Voici le [repo de code source](https://github.com/barais/doodlestudent) de cette application. Vous pouvez regarder le code source, le *back* est développé en [quarkus.io](https://quarkus.io/) et le front en [angular](https://angular.io/).
|
||||
> <details>
|
||||
> <summary>Vidéos de présentation</summary>
|
||||
>
|
||||
> - [Présentation des fonctionnalités de l'application](https://drive.google.com/file/d/1IxQC6lq0UPP_-n4IGt2TGvTRIO9M8enn/view?usp=sharing)
|
||||
> - [Présentation de l'architecture de l'application](https://drive.google.com/file/d/16gqFYmhwok8GWgiOskYjP-oTeP_sDwu-/view?usp=sharing)
|
||||
> - [Revue de code de l'application](https://drive.google.com/file/d/10RLkHWjJxnLi1ltRmNDCIymW9eM7Tn87/view?usp=sharing)
|
||||
> - [Compréhension du projet]()
|
||||
>
|
||||
> </details>
|
||||
|
||||
Les tâches 1 à 4 décrivent le minimum à faire pour ce projet.
|
||||
Vous devez ensuite choisir au moins deux des aventures proposées pour aller plus loin.
|
||||
Travail effectué :
|
||||
Tâches 1 - 3
|
||||
|
||||
## Aventures au choix
|
||||
|
||||
- **Aventure 1**: Mettre en place un mécanisme de déploiement continu qui permette de déployer la nouvelle version de l'application automatiquement si un nouveau commit est activé sur le back ou le front. L'idée de ce travail est d'explorer les liens entre pratique devops (intégration/déploiement continu) et outil de déploiement en mode cloud native.
|
||||
- **Aventure 2**: Mettre en place des outils de monitoring avancés de cette application et des dashboards associés à ce monitoring.
|
||||
- **Aventure 3**: Utiliser K8S comme orchestrateur pour le déploiement de nos containers.
|
||||
- **Aventure 4**: Mettre en place un identity provider et/ou une passerelle d'API pour faire de l'accounting sur l'API de l'application.
|
||||
|
||||
# Modalités de rendu
|
||||
|
||||
Tous les rendus TLC se font à travers le gitlab de l'ISTIC : https://gitlab2.istic.univ-rennes1.fr/
|
||||
|
||||
Si vous ne l'avez pas déjà fait, créez sur Gitlab ISTIC un groupe nommé `TLC_2025_<votre_nom>_<votre_prenom>`
|
||||
|
||||
Pour chaque TP, vous devrez créer un projet dans ce groupe (il est fortement conseillé de fork le repo du projet en le plaçant dans votre groupe gitlab), nommé `TP<numero>_<votre_nom>_<votre_prenom>` et le projet finale nommé `Projet_<votre_nom>_<votre_prenom>`
|
||||
|
||||
Pour chaque TP, vous devrez ajouter votre enseignant en tant que membre du projet avec le rôle de "Reporter" pour permettre la correction.
|
||||
|
||||
## Backlog du projet
|
||||
|
||||
### Niveau 1: minimum à faire
|
||||
|
||||
[//]: # (- **Tâche 0**: Créer une machine virtuelle (https://vm.istic.univ-rennes1.fr/ séléctionner **ubuntu20** comme image de base) et demandez l'accès externe vers le port *80* (http) et *443* (https) de votre machine virtuelle par le [helpdesk, catégorie ISTIC-ESIR - Tous problèmes informatiques](https://assistance.univ-rennes1.fr/), l'accès au port 22 se fera au travers du [VPN](https://istic.univ-rennes1.fr/intranet/services#section-4). Partager moi l'adresse IP de la machine et le sous domaine souhaité [ici](https://forms.gle/a7EfKraasGpTYAXa6) (sous domaine de *diverse-team.fr*). (Je mets jour quand c'est fait le [google doc joint](https://docs.google.com/spreadsheets/d/14Yv-3ujq9OL0HuYkj4Wlp9Piw5075b6iEKSf-yJWytc/edit?usp=sharing)))
|
||||
|
||||
|
||||
- **Tâche 1**: Fournir un ou plusieurs Dockerfile(s) et un ou plusieurs docker-compose(s) pour cette application permettant de facilement déployer et configurer l'application. (Faites un fork du projet avant.)
|
||||
|
||||
<details>
|
||||
<summary>Liens utiles</summary>
|
||||
|
||||
- [Synchroniser votre fork GitHub](https://nearsoft.com/blog/how-to-synchronize-your-github-fork/)
|
||||
- [Documentation Docker Compose](https://docs.docker.com/compose/)
|
||||
- [Documentation Dockerfile](https://docs.docker.com/engine/reference/builder/)
|
||||
|
||||
</details>
|
||||
|
||||
:warning: Vous aurez du mal à avoir un fonctionnement correct à cette étape-là. En effet, le code du front va faire ces requêtes *REST* à la même adresse que celui qui lui a fourni le code html, css et js pour éviter les problèmes de CORS. Il est donc nécessaire de se forcer à configurer le serveur nginx qui délivre le front pour faire *proxy_pass* quand il reçoit une requête sur la route */api* ou une sous-route de */api*. Ne vous inquiétez pas, on configure cela à l'étape suivante.
|
||||
|
||||
- **Tâche 2**: Configurer le serveur Web du Front pour qu'il soit capable de servir de point d'entrée à l'ensemble des requêtes puis qu'il les *route* vers le bon service de Back. Il est possible de mettre en place un serveur Web spécifique pour gérer ce routing (on le nomme alors la gateway d'API). On peut aussi dans notre cas se servir du fichier nginx du front pour router les requêtes.
|
||||
|
||||
<details>
|
||||
<summary>Exemples de fichiers de configuration nginx</summary>
|
||||
|
||||
```txt
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name doodle.tlc.fr;
|
||||
|
||||
location /api {
|
||||
proxy_pass http://api:8080/api;
|
||||
proxy_set_header Host $http_host;
|
||||
}
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html?$args;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```txt
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name myadmin.tlc.fr;
|
||||
|
||||
location / {
|
||||
proxy_pass http://myadmin:80;
|
||||
proxy_set_header Host $http_host;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```txt
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name pad.tlc.fr;
|
||||
|
||||
location / {
|
||||
proxy_pass http://etherpad:9001;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
- **Tâche 3**: Déployer correctement une première fois votre application en configurant convenablement la partie DNS pour le reverse proxy, letsencrypt pour le certificat côté serveur et ufw pour le firewall sur votre machine virtuelle.
|
||||
|
||||
En gros, vous allez prendre votre fichier docker-compose, votre fichier de configuration nginx, votre front et mettre cela sur votre VM. Mettre les bonnes variables de configuration dans ces deux fichiers. Vous aurez besoin soit de builder les images sur la VM soit de pushé vos images sur le docker_hub afin de pouvoir les *puller* depuis votre VM.
|
||||
|
||||
- **Tâche 4**: Documenter, à l'aide d'un diagramme de déploiement UML ou autre notation, le déploiement réalisé pour le moment sur votre machine virtuelle.
|
||||
|
||||
<details>
|
||||
<summary>Vidéos d'aide</summary>
|
||||
|
||||
- [Construction d'un DockerFile pour le front]()
|
||||
- [Construction d'un DockerFile pour le back]()
|
||||
- [Définition d'un docker-compose pour le déploiement de l'application]()
|
||||
- [Déploiement de l'application avec K8S]() ([note de présentation]())
|
||||
|
||||
</details>
|
||||
|
||||
Vous pouvez maintenant choisir au minimum deux de ces 4 aventures :
|
||||
|
||||
### Aventure 1: Déploiement continu à l'aide de gitlabCI et déploiement à l'aide de Docker
|
||||
|
||||
- **Aventure 1**: Mettre en place un mécanisme de déploiement continu qui permette de déployer la nouvelle version de l'application automatiquement si un nouveau commit est activé sur le back ou le front. Vous pourrez mettre en place votre outil d'intégration continue sur la même VM même si c'est une pratique que nous ne suivrions pas pour un déploiement réel. Vous avez le choix pour l'outil d'intégration continue.
|
||||
|
||||
<details>
|
||||
<summary>Outils d'intégration continue</summary>
|
||||
|
||||
- Vous pouvez utiliser gitlabCI de l'ISTIC si vous avez mis le code dans un repo du gitlab de l'ISTIC. Vous pouvez alors créer votre propre runner gitlab ci (https://docs.gitlab.com/runner/) ou utiliser travis sur github ou les runners de gitlab.com. Vous pouvez aussi déployer un jenkins sur votre machine si vous préférez.
|
||||
|
||||
</details>
|
||||
|
||||
### Aventure 2: Chaîne de monitoring de l'application en production
|
||||
|
||||
- **Aventure 2**: Mettre en place la chaîne de monitoring de l'application à l'aide de Promotheus et grafana comme front. Mettre en place le sous domaine et le certificat letsencrypt pour grafana. Mettre en place munin pour la surveillance de votre machine virtuelle.
|
||||
|
||||
<details>
|
||||
<summary>Liens utiles</summary>
|
||||
|
||||
- [Monitoring Quarkus avec Prometheus](http://www.mastertheboss.com/soa-cloud/quarkus/monitoring-quarkus-with-prometheus)
|
||||
|
||||
</details>
|
||||
|
||||
### Aventure 3: Utilisation de K8S comme orchestrateur d'un petit cluster
|
||||
|
||||
> Pour cette étape, il est conseillé de se construire son infra avec Vagrant at microk8s. Le playbook réalisé dans le TP ansible peut vous aider à mettre en place l'environnement.
|
||||
|
||||
- **Aventure 3**: Utiliser kubernetes pour déployer l'ensemble des micro-services. Le but est de pouvoir comparer et comprendre les abstractions supplémentaires fournies par kubernetes par rapport à un simple déploiement d'une application sur un seul noeud à l'aide de docker et docker-compose. En particulier, on souhaite ajouter de la redondance pour la partie back de l'application (redondance uniquement sur ce micro-service là. Surtout pas sur la BD relationnelle, c'est plus compliqué et pas complètement transparent). Déployer donc microk8s sur votre VM. Utiliser kubernetes pour mettre en place un minimum de redondance pour le back de l'application (on ne clusterisera pas la BD dans un premier temps).
|
||||
|
||||
<details>
|
||||
<summary>Liens utiles</summary>
|
||||
|
||||
- [Microk8s](https://microk8s.io/)
|
||||
- Notes sur microk8s : voir fichier k8s.pdf
|
||||
|
||||
</details>
|
||||
|
||||
### Aventure 4: Mise en place d'un outil d'authentification et d'une passerelle d'API
|
||||
|
||||
- **Aventure 4**: Mettre en place [keycloak](https://www.keycloak.org/getting-started/getting-started-docker) pour un accès sécurisé à vos ressources Web et/ou une passerelle d'API pour faire de l'accounting sur l'API de l'application. Je vous propose l'utilisation d'[ambassador](https://www.getambassador.io/).
|
||||
|
||||
<details>
|
||||
<summary>Liens utiles</summary>
|
||||
|
||||
- [Ambassador API Gateway avec microk8s](https://blog.getambassador.io/explore-the-ambassador-api-gateway-with-microk8s-f75a7a295113)
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
:warning: Il va falloir lire beaucoup de documentation, poser beaucoup de questions, faire des schémas pour comprendre ce que vous êtes en train de faire. Utilisez les tickets github dès que quelque chose ne marche pas comme vous le souhaitez.
|
||||
|
||||
## Rendu Projet
|
||||
|
||||
Pour le rendu, je propose d'utiliser le formulaire suivant: https://forms.gle/NsBtwLK81CtYBDsN7. Comme d'habitude, un petit readme à la racine des différents repos.
|
||||
|
||||
Pensez bien à ranger votre dépôt git, à mettre des commentaires dans vos fichiers de configuration, à bien documenter vos choix et à expliquer ce que vous avez fait.
|
||||
|
||||
Au minimum, il faut avoir dans votre git les rendus pour les tâches 1 à 4.
|
||||
- Plusieurs Dockerfile(s) pour le front et le back. Les propositions de Dockerfile _light_ seront mieux évaluées.
|
||||
- Un docker-compose pour le déploiement de toute la stack: front, back, etc.
|
||||
- Un fichier de configuration nginx pour le front.
|
||||
- Explication de la stack accompagnée d'un diagramme.
|
||||
|
||||
- Pour chaque aventure choisies, les fichiers de configuration et les scripts nécessaires.
|
||||
|
||||
|
||||
### Date de rendu **16/02 23h59 ferme**
|
||||
|
||||
...
|
||||
|
||||
## Other relevant videos
|
||||
|
||||
<details>
|
||||
<summary>Vidéos</summary>
|
||||
|
||||
- [What are cloud native apps](https://www.youtube.com/watch?v=fp9_ubiKqFU)
|
||||
- [Cloud native applications](https://www.youtube.com/watch?v=nyQAkR84RHM&ab_channel=ParallelWireless)
|
||||
- [Cloud Native DevOps Explained](https://www.youtube.com/watch?v=FzERTm_j2wE)
|
||||
- [What are Microservices?](https://www.youtube.com/watch?v=CdBtNQZH8a4)
|
||||
- [Advantages of Using OpenAPI to design APIs](https://www.youtube.com/watch?v=Fyuk50fFllM&ab_channel=SmartBear)
|
||||
- [What is DevOps?](https://www.youtube.com/watch?v=_I94-tJlovg&ab_channel=RackspaceTechnology)
|
||||
- [What is Continuous Integration?](https://www.youtube.com/watch?v=1er2cjUq1UI&frags=wn)
|
||||
- [What is Continuous Delivery?](https://www.youtube.com/watch?v=2TTU5BB-k9U)
|
||||
- [What is Infrastructure as Code?](https://www.youtube.com/watch?v=zWw2wuiKd5o&list=PLOspHqNVtKACSagAEeIY20NMVLNeQ1ZJx&index=3&ab_channel=IBMCloud)
|
||||
- [Cloud-native Java for this decade with Quarkus](https://www.youtube.com/watch?v=KJZt0uc5OiM)
|
||||
- [Principles of Antifragile Software](https://www.youtube.com/watch?v=FXGWSyRtipY&ab_channel=MartinMonperrus)
|
||||
- [Mastering Chaos - A Netflix Guide to Microservices](https://www.youtube.com/watch?v=CZ3wIuvmHeM&ab_channel=InfoQ)
|
||||
- [Getting started with Chaos Engineering](https://www.youtube.com/watch?v=3Oc4-cMkGJY&ab_channel=NDCConferences)
|
||||
|
||||
</details>
|
||||
|
||||
## References
|
||||
|
||||
<details>
|
||||
<summary>Références</summary>
|
||||
|
||||
[1] BALALAIE, Armin, HEYDARNOORI, Abbas, et JAMSHIDI, Pooyan. Microservices architecture enables devops: Migration to a cloud-native architecture. Ieee Software, 2016, vol. 33, no 3, p. 42-52.
|
||||
|
||||
[2] https://www.redhat.com/en/resources/eight-steps-cloud-native-application-development-brief
|
||||
|
||||
[3] D. S. Linthicum, "Cloud-Native Applications and Cloud Migration: The Good, the Bad, and the Points Between," in IEEE Cloud Computing, vol. 4, no. 5, pp. 12-14, September/October 2017, doi: 10.1109/MCC.2017.4250932.
|
||||
|
||||
</details>
|
||||
|
||||
## Questions réponses
|
||||
|
||||
<details>
|
||||
<summary>Liens utiles</summary>
|
||||
|
||||
- [Comprendre la notion de nom de domaine]()
|
||||
- [Mettre en place un serveur Web](https://www.youtube.com/watch?v=cfJh8vdKuQU&list=PLjwdMgw5TTLUnvhOKLcpCG8ORQsfE7uB4&ab_channel=Grafikart.fr)
|
||||
- [Configurer nginx](https://www.youtube.com/watch?v=YD_exb9aPZU&ab_channel=Grafikart.fr)
|
||||
- [Nginx: Mettre en place letsencrypt](https://www.youtube.com/watch?v=NXyE3mayrtg&ab_channel=Grafikart.fr)
|
||||
- [Nginx : Se protéger des attaques Flood](https://www.youtube.com/watch?v=ge768xOLQJs&list=PLjwdMgw5TTLUnvhOKLcpCG8ORQsfE7uB4&index=22&ab_channel=Grafikart.fr)
|
||||
- [Fail2ban c'est quoi](https://www.youtube.com/watch?v=-rmK50PbqCY&ab_channel=Grafikart.fr)
|
||||
- [un firewall c'est quoi: configuration de ufw](https://www.grafikart.fr/tutoriels/ufw-696), [vidéo](https://www.youtube.com/watch?v=XnfMAU3zIew&ab_channel=Grafikart.fr)
|
||||
- [une autre présentation de docker](https://www.youtube.com/watch?v=XgKOC6X8W28&ab_channel=Grafikart.fr)
|
||||
- [Pas mal de tutos assez bien fait](https://www.youtube.com/c/grafikart/playlists)
|
||||
- [POC en local avec Docker et Traefik](https://www.youtube.com/watch?v=QF4ZF857m44&ab_channel=TechworldwithNana)
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Appendix</summary>
|
||||
|
||||
- :warning: **Tâche 2 bis (NON Obligatoire, UNIQUEMENT pour ceux que cela intéressent)**,
|
||||
|
||||
Pour servir d'API Gwateway, il y a un super projet que j'adore qui s'appelle **bunkerweb**. C'est un container docker avec nginx qui se configure au travers de variable d'environement et qui inclus de base plein de mécanisme de sécurité pour nginx, la génération des certificats ... mais il entraine pas mal de doc à lire.
|
||||
|
||||
:warning: **UNIQUEMENT pour ceux que cela intéressent (j'insite )**
|
||||
|
||||
configurer convenablement https://github.com/bunkerity/bunkerweb au sein de votre docker-compose pour sécuriser votre application. Tester localement le déploiement de l'application.
|
||||
|
||||
**nginx bunkerity/bunkerweb** va jouer le rôle de passerelle d'API, c'est-à-dire que toutes les requêtes passent par lui et il a la charge de fournir les fichiers html, css et js du front, mais aussi de servir de proxy vers les autres services.
|
||||
|
||||
Pour cela, il est nécessaire de configurer finement ce serveur nginx qui tourne dans ce container. Il faudra *monter* ce fichier de configuration au moment du lancement de ce container. (voir https://github.com/bunkerity/bunkerized-nginx/blob/700dfc0184f8d12e15f3ca3d6e5ca08befccd561/examples/wordpress/docker-compose.yml#L14)
|
||||
|
||||
Vous trouverez ci-dessous un exemple de fichier de configuration nginx (fichier api.conf par exemple dans le répertoire *server-confs* là où se trouve votre docker-compose.yml).
|
||||
|
||||
```txt
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# L'idée ici est que toute requête qui arrive sur /api ou une sous
|
||||
# route d'API passe par ici et en gros on fait un if selon que cette
|
||||
# requête est arrivé sur l'adresse IP en demandant à parler à
|
||||
# doodle.diverse-team.fr ou à phpmyadmin.diverse-team.fr ou à
|
||||
# pad.diverse-team.fr
|
||||
location /api {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
if ($host = doodle.diverse-team.fr) {
|
||||
proxy_pass http://doodleback:8080$request_uri;
|
||||
}
|
||||
if ($host = pad.diverse-team.fr) {
|
||||
|
||||
proxy_pass http://etherpad:9001$request_uri;
|
||||
}
|
||||
if ($host = phpmyadmin.diverse-team.fr) {
|
||||
proxy_pass http://myadmin$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# L'idée ici est que toute requête qui arrive sur / ou une sous
|
||||
# route de / qui n'a pas été traité avant passe par ici et en gros
|
||||
# on fait un if selon que cette requête est arrivé sur l'adresse IP
|
||||
# en demandant à parler à phpmyadmin.diverse-team.fr ou à
|
||||
# pad.diverse-team.fr et pour tout le reste on fourni les ressources
|
||||
# statiques du front
|
||||
|
||||
location / {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_redirect off;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
|
||||
if ($host = pad.diverse-team.fr) {
|
||||
|
||||
proxy_pass http://etherpad:9001$request_uri;
|
||||
}
|
||||
if ($host = phpmyadmin.diverse-team.fr) {
|
||||
proxy_pass http://myadmin$request_uri;
|
||||
}
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Il est aussi nécessaire de configurer finement les variables d'environement de ce container. Je vous met un exemple avec des explications
|
||||
|
||||
```txt
|
||||
environment:
|
||||
# Ensemble des endpoints pour lequel il générera des certificats
|
||||
- SERVER_NAME=doodle.<Your_Domain>.fr pad.<Your_Domain>.fr phpmyadmin.<Your_Domain>.fr
|
||||
- SERVE_FILES=yes
|
||||
- DISABLE_DEFAULT_SERVER=no
|
||||
# Transmission de l'IP publique vers les containers
|
||||
- PROXY_REAL_IP=yes
|
||||
# - MAX_CLIENT_SIZE=100m
|
||||
# - USE_ANTIBOT=captcha
|
||||
# Configuration de letencrypt
|
||||
- AUTO_LETS_ENCRYPT=yes
|
||||
# redirect du 80 vers 443 automatique
|
||||
- REDIRECT_HTTP_TO_HTTPS=yes
|
||||
- USE_LIMIT_REQ=no
|
||||
# Désactivation http2
|
||||
- HTTP2=no
|
||||
# Paramétrage des entêtes http
|
||||
- FEATURE_POLICY=accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; camera 'none'; display-capture 'none'; document-domain 'none'; encrypted-media 'none'; fullscreen 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; picture-in-picture 'none'; speaker 'none'; sync-xhr 'self'; usb 'none'; vibrate 'none'; vr 'none'
|
||||
myadmin:
|
||||
image: phpmyadmin
|
||||
# ports:
|
||||
# - "8082:80"
|
||||
environment:
|
||||
# pour phpmyadmin url externe
|
||||
- PMA_ABSOLUTE_URI=https://<YOUR_PHPMYADMIN_URL>
|
||||
```
|
||||
|
||||
</details>
|
||||
Utilisation de l'IA :
|
||||
IA utilisé pour aider à la résolution de divers problèmes rencontrés.
|
||||
0
.gitmodules → ansible/files/.gitmodules
vendored
0
.gitmodules → ansible/files/.gitmodules
vendored
132
ansible/files/dockercompose/docker-compose.yml
Normal file
132
ansible/files/dockercompose/docker-compose.yml
Normal file
@@ -0,0 +1,132 @@
|
||||
services:
|
||||
|
||||
etherpad:
|
||||
image: etherpad/etherpad:1.9.7
|
||||
ports:
|
||||
- "9001:9001"
|
||||
volumes:
|
||||
- ../doodlestudent/api/APIKEY.txt:/opt/etherpad-lite/APIKEY.txt
|
||||
networks:
|
||||
- app-network
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "node -e \"require('http').get('http://127.0.0.1:9001', res => { process.exit(res.statusCode===200?0:1) }).on('error', ()=>process.exit(1))\"" ]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
start_period: 15s
|
||||
retries: 5
|
||||
|
||||
mail:
|
||||
image: bytemark/smtp:latest
|
||||
restart: always
|
||||
ports:
|
||||
- "2525:25"
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
db:
|
||||
image: mysql:8.4
|
||||
ports:
|
||||
- "3306:3306"
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=root
|
||||
- MYSQL_DATABASE=tlc
|
||||
- MYSQL_USER=tlc
|
||||
- MYSQL_PASSWORD=tlc
|
||||
healthcheck:
|
||||
test: [ "CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot" ]
|
||||
interval: 10s
|
||||
timeout: 1m
|
||||
retries: 5
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
back:
|
||||
image: benarbause/doodleback-with-quarkus:latest
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ../doodlestudent/api:/app
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
etherpad:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- app-network
|
||||
restart:
|
||||
unless-stopped
|
||||
|
||||
#https://dev.to/rafi021/how-to-set-up-a-monitoring-stack-with-prometheus-grafana-and-node-exporter-using-docker-compose-17cc
|
||||
#https://belginux.com/monitoring-docker-grafana-prometheus-cadvisor/
|
||||
#https://github.com/Einsteinish/Docker-Compose-Prometheus-and-Grafana/tree/master
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: prometheus
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ../prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
command:
|
||||
- "--config.file=/etc/prometheus/prometheus.yml"
|
||||
depends_on:
|
||||
- back
|
||||
- db
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:12.3
|
||||
container_name: grafana
|
||||
ports:
|
||||
- "3000:3000"
|
||||
#restart: unless-stopped
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
- ../grafana/provisioning:/etc/grafana/provisioning
|
||||
depends_on:
|
||||
- prometheus
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_USER=tlc
|
||||
- GF_SECURITY_ADMIN_PASSWORD=tlc
|
||||
- GF_PLUGINS_PREINSTALL=grafana-munin-datasource # Telechargement du plugin via var d'env
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
front:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: dockerfiles/front/Dockerfile
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ../doodlestudent/front:/app
|
||||
- ../certbot/www:/var/www/certbot:ro
|
||||
- ../certbot/conf:/etc/letsencrypt:ro
|
||||
environment:
|
||||
- FLASK_ENV=development
|
||||
depends_on:
|
||||
- back
|
||||
- db
|
||||
- etherpad
|
||||
- mail
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
# https://stackoverflow.com/questions/57591868/how-correctly-install-ssl-certificate-using-certbot-in-docker
|
||||
certbot:
|
||||
image: certbot/certbot:v5.3.1
|
||||
container_name: certbot
|
||||
depends_on:
|
||||
- front
|
||||
volumes:
|
||||
- ../certbot/www/:/var/www/certbot/:rw
|
||||
- ../certbot/conf/:/etc/letsencrypt/:rw
|
||||
|
||||
|
||||
networks: # Declare the network to be used by the services
|
||||
app-network: # Is a user-defined network
|
||||
name: app-network #Name of the network (optional, Docker Compose will generate one if not provided)
|
||||
|
||||
volumes : # Declare the volume to be used by the Grafana service (NE PAS SUPPRIMER !!)
|
||||
grafana-data: # Name of the volume
|
||||
@@ -12,7 +12,7 @@ WORKDIR /app
|
||||
RUN ./mvnw dependency:resolve
|
||||
|
||||
COPY doodlestudent/api/src /app/src
|
||||
RUN ./mvnw package -Pnative -DskipTests
|
||||
RUN ./mvnw package -Pnative -DskipTests -Dquarkus.native.builder-opportunities=2 -Dquarkus.native.native-image-xmx=3g
|
||||
|
||||
RUN ls -l target
|
||||
|
||||
@@ -13,12 +13,8 @@ RUN npx ng build
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY --from=build /app/dist/tlcfront /usr/share/nginx/html
|
||||
COPY nginxFront.conf /etc/nginx/conf.d/default.conf
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
|
||||
#docker build -f dockerfiles/front/Dockerfile -t front_image .
|
||||
#docker run --rm -p8080:80 front_image
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
22
ansible/files/doodlestudent/README.french.md
Normal file
22
ansible/files/doodlestudent/README.french.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Doodle in quarkus
|
||||
|
||||
Ce repository est une application de type doodle développée avec quarkus.io pour le back et angular pour le front.
|
||||
|
||||
Elle initialise automatiquement un pad pour la réunion et un salon de discussion.
|
||||
|
||||
Le but est de faire travailler les étudiants sur la partie déploiement de ce type d'application dite cloud native.
|
||||
|
||||
Votre mission est de mettre en production une telle application en permettant
|
||||
- qu'à chaque commit sur ce repository, si les tests passent, alors nous déployons automatiquement une nouvelle version dans un contexte (Continuous Deployement)
|
||||
- que l'application doit être monitorer finement.
|
||||
- que l'application redémarre automatiquement en cas de crash du serveur ou de crash d'un des services de l'application.
|
||||
- que Les accès doivent http doivent utiliser https.
|
||||
|
||||
|
||||
Une démo de l'application est accessible [ici](https://doodle.diverse-team.fr).
|
||||
|
||||
- Voici une petite [vidéo](https://drive.google.com/file/d/1GQbdgq2CHcddTlcoHqM5Zc8Dw5o_eeLg/preview) de présentation des fonctionnalités de l'application.
|
||||
- Voici une petite [vidéo](https://drive.google.com/file/d/1l5UAsU5_q-oshwEW6edZ4UvQjN3-tzwi/preview) de présentation de l'architecture de l'application.
|
||||
- Voici une petite [vidéo](https://drive.google.com/file/d/1jxYNfJdtd4r_pDbOthra360ei8Z17tX_/preview) de revue de code de l'application.
|
||||
|
||||
Un descriptif du cours, des TPs et des étapes du projet est lui accessible [ici](https://hackmd.diverse-team.fr/s/SJqu5DjSD)
|
||||
15
ansible/files/doodlestudent/Readme.md
Normal file
15
ansible/files/doodlestudent/Readme.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Remote meetings planning
|
||||
|
||||
This project is used in a course on the *ops* part at the [University of Rennes](https://www.univ-rennes1.fr/), France. It is a kind of doodle clone developed in so-called "native cloud" technologies in order to allow students to work on a continuous deployment chain in a containerized environment. Among the feature, the application automatically initializes a pad for the meeting and a chat room for the meeting participants.
|
||||
|
||||
- The [back](https://github.com/barais/doodlestudent/tree/main/api) is developed using the [quarkus.io](https://quarkus.io/) framework.
|
||||
- The [front](https://github.com/barais/doodlestudent/tree/main/front) is developed in [angular](https://angular.io/) using the [primeng](https://www.primefaces.org/primeng/) angular UI component library and the [fullcalendar](https://fullcalendar.io/) graphical component.
|
||||
|
||||
A demo of the application is available [here](https://doodle.diverse-team.fr/).
|
||||
|
||||
Three videos (in french) are available. They present:
|
||||
- the [main application feature](https://drive.google.com/file/d/1GQbdgq2CHcddTlcoHqM5Zc8Dw5o_eeLg/preview),
|
||||
- its [architecture](https://drive.google.com/file/d/1l5UAsU5_q-oshwEW6edZ4UvQjN3-tzwi/preview)
|
||||
- and a [short code review](https://drive.google.com/file/d/1jxYNfJdtd4r_pDbOthra360ei8Z17tX_/preview) .
|
||||
|
||||
For french native speaker that wants to follow the course. The course web page is available [here](https://hackmd.diverse-team.fr/s/SJqu5DjSD).
|
||||
1
ansible/files/doodlestudent/api/.dockerignore
Normal file
1
ansible/files/doodlestudent/api/.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
37
ansible/files/doodlestudent/api/.gitignore
vendored
Normal file
37
ansible/files/doodlestudent/api/.gitignore
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
#Maven
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
release.properties
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
# Eclipse
|
||||
.project
|
||||
.classpath
|
||||
.settings/
|
||||
bin/
|
||||
|
||||
# IntelliJ
|
||||
.idea
|
||||
*.ipr
|
||||
*.iml
|
||||
*.iws
|
||||
|
||||
# NetBeans
|
||||
nb-configuration.xml
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode
|
||||
.factorypath
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# Vim
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# patch
|
||||
*.orig
|
||||
*.rej
|
||||
|
||||
18
ansible/files/doodlestudent/api/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
18
ansible/files/doodlestudent/api/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip
|
||||
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
|
||||
1
ansible/files/doodlestudent/api/APIKEY.txt
Normal file
1
ansible/files/doodlestudent/api/APIKEY.txt
Normal file
@@ -0,0 +1 @@
|
||||
19d89ca52bc0fa4f19d6325464d9d7a032649b9fa68c111514627081e2784b4a
|
||||
53
ansible/files/doodlestudent/api/README.md
Normal file
53
ansible/files/doodlestudent/api/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# code-with-quarkus project
|
||||
|
||||
This project uses Quarkus, the Supersonic Subatomic Java Framework.
|
||||
|
||||
If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ .
|
||||
|
||||
## Running the application in dev mode
|
||||
|
||||
You can run your application in dev mode that enables live coding using:
|
||||
```shell script
|
||||
docker-compose up --detach
|
||||
# Wait the correct start of the docker services and then
|
||||
./mvnw compile quarkus:dev
|
||||
```
|
||||
|
||||
To stop the application and its dependencies, type `ctrl+c` in the bash session and run `docker-compose down`.
|
||||
|
||||
## Packaging and running the application
|
||||
|
||||
The application can be packaged using:
|
||||
```shell script
|
||||
./mvnw package
|
||||
```
|
||||
It produces the `code-with-quarkus-1.0.0-SNAPSHOT-runner.jar` file in the `/target` directory.
|
||||
Be aware that it’s not an _über-jar_ as the dependencies are copied into the `target/lib` directory.
|
||||
If you want to build an _über-jar_, execute the following command:
|
||||
```shell script
|
||||
./mvnw clean package -Dquarkus.package.type=uber-jar
|
||||
```
|
||||
|
||||
The application is now runnable using `java -jar target/code-with-quarkus-1.0.0-SNAPSHOT-runner.jar`.
|
||||
|
||||
## Creating a native executable
|
||||
|
||||
You can create a native executable using:
|
||||
```shell script
|
||||
./mvnw package -Pnative
|
||||
```
|
||||
|
||||
Or, if you don't have GraalVM installed, you can run the native executable build in a container using:
|
||||
```shell script
|
||||
./mvnw package -Pnative -Dquarkus.native.container-build=true
|
||||
```
|
||||
|
||||
You can then execute your native executable with: `./target/code-with-quarkus-1.0.0-SNAPSHOT-runner`
|
||||
|
||||
If you want to learn more about building native executables, please consult https://quarkus.io/guides/maven-tooling.html.
|
||||
|
||||
# RESTEasy JAX-RS
|
||||
|
||||
Guide: https://quarkus.io/guides/rest-json
|
||||
|
||||
|
||||
22
ansible/files/doodlestudent/api/docker-compose.yaml
Normal file
22
ansible/files/doodlestudent/api/docker-compose.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
db:
|
||||
image: mysql
|
||||
ports:
|
||||
- "3306:3306"
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=root
|
||||
- MYSQL_DATABASE=tlc
|
||||
- MYSQL_USER=tlc
|
||||
- MYSQL_PASSWORD=tlc
|
||||
etherpad:
|
||||
image: etherpad/etherpad
|
||||
ports:
|
||||
- "9001:9001"
|
||||
volumes:
|
||||
- ./APIKEY.txt:/opt/etherpad-lite/APIKEY.txt
|
||||
mail:
|
||||
image: bytemark/smtp
|
||||
restart: always
|
||||
ports:
|
||||
- "2525:25"
|
||||
308
ansible/files/doodlestudent/api/mvnw
vendored
Executable file
308
ansible/files/doodlestudent/api/mvnw
vendored
Executable file
@@ -0,0 +1,308 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.2.0
|
||||
#
|
||||
# Required ENV vars:
|
||||
# ------------------
|
||||
# JAVA_HOME - location of a JDK home dir
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
# e.g. to debug Maven itself, use
|
||||
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
if [ -z "$MAVEN_SKIP_RC" ] ; then
|
||||
|
||||
if [ -f /usr/local/etc/mavenrc ] ; then
|
||||
. /usr/local/etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f /etc/mavenrc ] ; then
|
||||
. /etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.mavenrc" ] ; then
|
||||
. "$HOME/.mavenrc"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# OS specific support. $var _must_ be set to either true or false.
|
||||
cygwin=false;
|
||||
darwin=false;
|
||||
mingw=false
|
||||
case "$(uname)" in
|
||||
CYGWIN*) cygwin=true ;;
|
||||
MINGW*) mingw=true;;
|
||||
Darwin*) darwin=true
|
||||
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
|
||||
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
if [ -x "/usr/libexec/java_home" ]; then
|
||||
JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
|
||||
else
|
||||
JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
if [ -r /etc/gentoo-release ] ; then
|
||||
JAVA_HOME=$(java-config --jre-home)
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched
|
||||
if $cygwin ; then
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
|
||||
fi
|
||||
|
||||
# For Mingw, ensure paths are in UNIX format before anything is touched
|
||||
if $mingw ; then
|
||||
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
|
||||
JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
javaExecutable="$(which javac)"
|
||||
if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
|
||||
# readlink(1) is not available as standard on Solaris 10.
|
||||
readLink=$(which readlink)
|
||||
if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
|
||||
if $darwin ; then
|
||||
javaHome="$(dirname "\"$javaExecutable\"")"
|
||||
javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
|
||||
else
|
||||
javaExecutable="$(readlink -f "\"$javaExecutable\"")"
|
||||
fi
|
||||
javaHome="$(dirname "\"$javaExecutable\"")"
|
||||
javaHome=$(expr "$javaHome" : '\(.*\)/bin')
|
||||
JAVA_HOME="$javaHome"
|
||||
export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$JAVACMD" ] ; then
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
echo "Error: JAVA_HOME is not defined correctly." >&2
|
||||
echo " We cannot execute $JAVACMD" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
echo "Warning: JAVA_HOME environment variable is not set."
|
||||
fi
|
||||
|
||||
# traverses directory structure from process work directory to filesystem root
|
||||
# first directory with .mvn subdirectory is considered project base directory
|
||||
find_maven_basedir() {
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
echo "Path not specified to find_maven_basedir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
basedir="$1"
|
||||
wdir="$1"
|
||||
while [ "$wdir" != '/' ] ; do
|
||||
if [ -d "$wdir"/.mvn ] ; then
|
||||
basedir=$wdir
|
||||
break
|
||||
fi
|
||||
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
|
||||
if [ -d "${wdir}" ]; then
|
||||
wdir=$(cd "$wdir/.." || exit 1; pwd)
|
||||
fi
|
||||
# end of workaround
|
||||
done
|
||||
printf '%s' "$(cd "$basedir" || exit 1; pwd)"
|
||||
}
|
||||
|
||||
# concatenates all lines of a file
|
||||
concat_lines() {
|
||||
if [ -f "$1" ]; then
|
||||
# Remove \r in case we run on Windows within Git Bash
|
||||
# and check out the repository with auto CRLF management
|
||||
# enabled. Otherwise, we may read lines that are delimited with
|
||||
# \r\n and produce $'-Xarg\r' rather than -Xarg due to word
|
||||
# splitting rules.
|
||||
tr -s '\r\n' ' ' < "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
log() {
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
printf '%s\n' "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
|
||||
if [ -z "$BASE_DIR" ]; then
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
|
||||
log "$MAVEN_PROJECTBASEDIR"
|
||||
|
||||
##########################################################################################
|
||||
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
# This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
##########################################################################################
|
||||
wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
|
||||
if [ -r "$wrapperJarPath" ]; then
|
||||
log "Found $wrapperJarPath"
|
||||
else
|
||||
log "Couldn't find $wrapperJarPath, downloading it ..."
|
||||
|
||||
if [ -n "$MVNW_REPOURL" ]; then
|
||||
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
else
|
||||
wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
fi
|
||||
while IFS="=" read -r key value; do
|
||||
# Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
|
||||
safeValue=$(echo "$value" | tr -d '\r')
|
||||
case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
|
||||
esac
|
||||
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
log "Downloading from: $wrapperUrl"
|
||||
|
||||
if $cygwin; then
|
||||
wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
|
||||
fi
|
||||
|
||||
if command -v wget > /dev/null; then
|
||||
log "Found wget ... using wget"
|
||||
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
else
|
||||
wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
elif command -v curl > /dev/null; then
|
||||
log "Found curl ... using curl"
|
||||
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
|
||||
else
|
||||
curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
else
|
||||
log "Falling back to using Java to download"
|
||||
javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
|
||||
javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
|
||||
# For Cygwin, switch paths to Windows format before running javac
|
||||
if $cygwin; then
|
||||
javaSource=$(cygpath --path --windows "$javaSource")
|
||||
javaClass=$(cygpath --path --windows "$javaClass")
|
||||
fi
|
||||
if [ -e "$javaSource" ]; then
|
||||
if [ ! -e "$javaClass" ]; then
|
||||
log " - Compiling MavenWrapperDownloader.java ..."
|
||||
("$JAVA_HOME/bin/javac" "$javaSource")
|
||||
fi
|
||||
if [ -e "$javaClass" ]; then
|
||||
log " - Running MavenWrapperDownloader.java ..."
|
||||
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
##########################################################################################
|
||||
# End of extension
|
||||
##########################################################################################
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven wrapper jar file
|
||||
wrapperSha256Sum=""
|
||||
while IFS="=" read -r key value; do
|
||||
case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
|
||||
esac
|
||||
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
if [ -n "$wrapperSha256Sum" ]; then
|
||||
wrapperSha256Result=false
|
||||
if command -v sha256sum > /dev/null; then
|
||||
if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
|
||||
wrapperSha256Result=true
|
||||
fi
|
||||
elif command -v shasum > /dev/null; then
|
||||
if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
|
||||
wrapperSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
|
||||
echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
|
||||
exit 1
|
||||
fi
|
||||
if [ $wrapperSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
|
||||
echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin; then
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
|
||||
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
|
||||
MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
|
||||
fi
|
||||
|
||||
# Provide a "standardized" way to retrieve the CLI args that will
|
||||
# work with both Windows and non-Windows executions.
|
||||
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
|
||||
export MAVEN_CMD_LINE_ARGS
|
||||
|
||||
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
# shellcheck disable=SC2086 # safe args
|
||||
exec "$JAVACMD" \
|
||||
$MAVEN_OPTS \
|
||||
$MAVEN_DEBUG_OPTS \
|
||||
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
|
||||
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
|
||||
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
|
||||
205
ansible/files/doodlestudent/api/mvnw.cmd
vendored
Normal file
205
ansible/files/doodlestudent/api/mvnw.cmd
vendored
Normal file
@@ -0,0 +1,205 @@
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.2.0
|
||||
@REM
|
||||
@REM Required ENV vars:
|
||||
@REM JAVA_HOME - location of a JDK home dir
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
|
||||
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
|
||||
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
@REM e.g. to debug Maven itself, use
|
||||
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
|
||||
@echo off
|
||||
@REM set title of command window
|
||||
title %0
|
||||
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
|
||||
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
|
||||
|
||||
@REM set %HOME% to equivalent of $HOME
|
||||
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
|
||||
|
||||
@REM Execute a user defined script before this one
|
||||
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
|
||||
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
|
||||
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
|
||||
:skipRcPre
|
||||
|
||||
@setlocal
|
||||
|
||||
set ERROR_CODE=0
|
||||
|
||||
@REM To isolate internal variables from possible post scripts, we use another setlocal
|
||||
@setlocal
|
||||
|
||||
@REM ==== START VALIDATION ====
|
||||
if not "%JAVA_HOME%" == "" goto OkJHome
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME not found in your environment. >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
:OkJHome
|
||||
if exist "%JAVA_HOME%\bin\java.exe" goto init
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME is set to an invalid directory. >&2
|
||||
echo JAVA_HOME = "%JAVA_HOME%" >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
@REM ==== END VALIDATION ====
|
||||
|
||||
:init
|
||||
|
||||
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
|
||||
@REM Fallback to current working directory if not found.
|
||||
|
||||
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
|
||||
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
|
||||
|
||||
set EXEC_DIR=%CD%
|
||||
set WDIR=%EXEC_DIR%
|
||||
:findBaseDir
|
||||
IF EXIST "%WDIR%"\.mvn goto baseDirFound
|
||||
cd ..
|
||||
IF "%WDIR%"=="%CD%" goto baseDirNotFound
|
||||
set WDIR=%CD%
|
||||
goto findBaseDir
|
||||
|
||||
:baseDirFound
|
||||
set MAVEN_PROJECTBASEDIR=%WDIR%
|
||||
cd "%EXEC_DIR%"
|
||||
goto endDetectBaseDir
|
||||
|
||||
:baseDirNotFound
|
||||
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
|
||||
cd "%EXEC_DIR%"
|
||||
|
||||
:endDetectBaseDir
|
||||
|
||||
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
|
||||
|
||||
@setlocal EnableExtensions EnableDelayedExpansion
|
||||
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
|
||||
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
|
||||
|
||||
:endReadAdditionalConfig
|
||||
|
||||
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
|
||||
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
|
||||
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
|
||||
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
|
||||
)
|
||||
|
||||
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
if exist %WRAPPER_JAR% (
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Found %WRAPPER_JAR%
|
||||
)
|
||||
) else (
|
||||
if not "%MVNW_REPOURL%" == "" (
|
||||
SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
)
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Couldn't find %WRAPPER_JAR%, downloading it ...
|
||||
echo Downloading from: %WRAPPER_URL%
|
||||
)
|
||||
|
||||
powershell -Command "&{"^
|
||||
"$webclient = new-object System.Net.WebClient;"^
|
||||
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
|
||||
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
|
||||
"}"^
|
||||
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
|
||||
"}"
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Finished downloading %WRAPPER_JAR%
|
||||
)
|
||||
)
|
||||
@REM End of extension
|
||||
|
||||
@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
|
||||
SET WRAPPER_SHA_256_SUM=""
|
||||
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
|
||||
)
|
||||
IF NOT %WRAPPER_SHA_256_SUM%=="" (
|
||||
powershell -Command "&{"^
|
||||
"$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
|
||||
"If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
|
||||
" Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
|
||||
" Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
|
||||
" Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
|
||||
" exit 1;"^
|
||||
"}"^
|
||||
"}"
|
||||
if ERRORLEVEL 1 goto error
|
||||
)
|
||||
|
||||
@REM Provide a "standardized" way to retrieve the CLI args that will
|
||||
@REM work with both Windows and non-Windows executions.
|
||||
set MAVEN_CMD_LINE_ARGS=%*
|
||||
|
||||
%MAVEN_JAVA_EXE% ^
|
||||
%JVM_CONFIG_MAVEN_PROPS% ^
|
||||
%MAVEN_OPTS% ^
|
||||
%MAVEN_DEBUG_OPTS% ^
|
||||
-classpath %WRAPPER_JAR% ^
|
||||
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
|
||||
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
|
||||
if ERRORLEVEL 1 goto error
|
||||
goto end
|
||||
|
||||
:error
|
||||
set ERROR_CODE=1
|
||||
|
||||
:end
|
||||
@endlocal & set ERROR_CODE=%ERROR_CODE%
|
||||
|
||||
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
|
||||
@REM check for post script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
|
||||
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
|
||||
:skipRcPost
|
||||
|
||||
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
|
||||
if "%MAVEN_BATCH_PAUSE%"=="on" pause
|
||||
|
||||
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
|
||||
|
||||
cmd /C exit /B %ERROR_CODE%
|
||||
266
ansible/files/doodlestudent/api/pom.xml
Normal file
266
ansible/files/doodlestudent/api/pom.xml
Normal file
@@ -0,0 +1,266 @@
|
||||
<?xml version="1.0"?>
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>fr.istic</groupId>
|
||||
<artifactId>tlcdemoApp</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<properties>
|
||||
<compiler-plugin.version>3.11.0</compiler-plugin.version>
|
||||
<maven.compiler.release>17</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
|
||||
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
|
||||
<quarkus.platform.version>3.7.1</quarkus.platform.version>
|
||||
<skipITs>true</skipITs>
|
||||
<surefire-plugin.version>3.0.0</surefire-plugin.version>
|
||||
</properties>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>${quarkus.platform.group-id}</groupId>
|
||||
<artifactId>${quarkus.platform.artifact-id}</artifactId>
|
||||
<version>${quarkus.platform.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-arc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-junit5</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.rest-assured</groupId>
|
||||
<artifactId>rest-assured</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-hibernate-orm</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-config-yaml</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-hibernate-orm-rest-data-panache</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-resteasy</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-scheduler</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jdbc-mysql</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-smallrye-metrics</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-smallrye-opentracing</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-hibernate-validator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-smallrye-openapi</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jackson</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-resteasy-jackson</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-mailer</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-spring-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-flyway</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.jexcelapi</groupId>
|
||||
<artifactId>jxl</artifactId>
|
||||
<version>2.6.12</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.gjerull.etherpad</groupId>
|
||||
<artifactId>etherpad_lite_client</artifactId>
|
||||
<version>1.2.13</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.mnode.ical4j/ical4j -->
|
||||
<dependency>
|
||||
<groupId>org.mnode.ical4j</groupId>
|
||||
<artifactId>ical4j</artifactId>
|
||||
<version>3.0.20</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpasyncclient -->
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpasyncclient</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/javax.cache/cache-api -->
|
||||
<dependency>
|
||||
<groupId>javax.cache</groupId>
|
||||
<artifactId>cache-api</artifactId>
|
||||
<version>1.1.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.jsr107.ri/cache-ri-impl -->
|
||||
<dependency>
|
||||
<groupId>org.jsr107.ri</groupId>
|
||||
<artifactId>cache-ri-impl</artifactId>
|
||||
<version>1.1.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>commons-logging-jboss-logging</artifactId>
|
||||
<version>1.0.0.Final</version>
|
||||
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.logmanager</groupId>
|
||||
<artifactId>log4j-jboss-logmanager</artifactId>
|
||||
<version>1.2.0.Final</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jboss.slf4j</groupId>
|
||||
<artifactId>slf4j-jboss-logging</artifactId>
|
||||
<version>1.2.1.Final</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-mysql</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>${quarkus.platform.group-id}</groupId>
|
||||
<artifactId>quarkus-maven-plugin</artifactId>
|
||||
<version>${quarkus.platform.version}</version>
|
||||
<extensions>true</extensions>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>build</goal>
|
||||
<goal>generate-code</goal>
|
||||
<goal>generate-code-tests</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>${compiler-plugin.version}</version>
|
||||
<configuration>
|
||||
<compilerArgs>
|
||||
<arg>-parameters</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${surefire-plugin.version}</version>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||
<maven.home>${maven.home}</maven.home>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>${surefire-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>integration-test</goal>
|
||||
<goal>verify</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
|
||||
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||
<maven.home>${maven.home}</maven.home>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>native</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>native</name>
|
||||
</property>
|
||||
</activation>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>${surefire-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>integration-test</goal>
|
||||
<goal>verify</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
|
||||
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||
<maven.home>${maven.home}</maven.home>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<properties>
|
||||
<quarkus.native.additional-build-args>--initialize-at-run-time=org.apache.http.impl.auth.NTLMEngineImpl,-H:ReflectionConfigurationFiles=reflection-config.json,-H:IncludeResourceBundles=sun.util.resources.TimeZoneNames</quarkus.native.additional-build-args>
|
||||
<skipITs>false</skipITs>
|
||||
<quarkus.package.type>native</quarkus.package.type>
|
||||
</properties>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
@@ -0,0 +1,97 @@
|
||||
####
|
||||
# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
|
||||
#
|
||||
# Before building the container image run:
|
||||
#
|
||||
# ./mvnw package
|
||||
#
|
||||
# Then, build the image with:
|
||||
#
|
||||
# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/tlcdemoApp-jvm .
|
||||
#
|
||||
# Then run the container using:
|
||||
#
|
||||
# docker run -i --rm -p 8080:8080 quarkus/tlcdemoApp-jvm
|
||||
#
|
||||
# If you want to include the debug port into your docker image
|
||||
# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005.
|
||||
# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005
|
||||
# when running the container
|
||||
#
|
||||
# Then run the container using :
|
||||
#
|
||||
# docker run -i --rm -p 8080:8080 quarkus/tlcdemoApp-jvm
|
||||
#
|
||||
# This image uses the `run-java.sh` script to run the application.
|
||||
# This scripts computes the command line to execute your Java application, and
|
||||
# includes memory/GC tuning.
|
||||
# You can configure the behavior using the following environment properties:
|
||||
# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class")
|
||||
# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options
|
||||
# in JAVA_OPTS (example: "-Dsome.property=foo")
|
||||
# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is
|
||||
# used to calculate a default maximal heap memory based on a containers restriction.
|
||||
# If used in a container without any memory constraints for the container then this
|
||||
# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio
|
||||
# of the container available memory as set here. The default is `50` which means 50%
|
||||
# of the available memory is used as an upper boundary. You can skip this mechanism by
|
||||
# setting this value to `0` in which case no `-Xmx` option is added.
|
||||
# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This
|
||||
# is used to calculate a default initial heap memory based on the maximum heap memory.
|
||||
# If used in a container without any memory constraints for the container then this
|
||||
# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio
|
||||
# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx`
|
||||
# is used as the initial heap size. You can skip this mechanism by setting this value
|
||||
# to `0` in which case no `-Xms` option is added (example: "25")
|
||||
# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS.
|
||||
# This is used to calculate the maximum value of the initial heap memory. If used in
|
||||
# a container without any memory constraints for the container then this option has
|
||||
# no effect. If there is a memory constraint then `-Xms` is limited to the value set
|
||||
# here. The default is 4096MB which means the calculated value of `-Xms` never will
|
||||
# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096")
|
||||
# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output
|
||||
# when things are happening. This option, if set to true, will set
|
||||
# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true").
|
||||
# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example:
|
||||
# true").
|
||||
# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787").
|
||||
# - CONTAINER_CORE_LIMIT: A calculated core limit as described in
|
||||
# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2")
|
||||
# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024").
|
||||
# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion.
|
||||
# (example: "20")
|
||||
# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking.
|
||||
# (example: "40")
|
||||
# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection.
|
||||
# (example: "4")
|
||||
# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus
|
||||
# previous GC times. (example: "90")
|
||||
# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20")
|
||||
# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100")
|
||||
# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should
|
||||
# contain the necessary JRE command-line options to specify the required GC, which
|
||||
# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC).
|
||||
# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080")
|
||||
# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080")
|
||||
# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be
|
||||
# accessed directly. (example: "foo.example.com,bar.example.com")
|
||||
#
|
||||
###
|
||||
FROM registry.access.redhat.com/ubi8/openjdk-17:1.16
|
||||
|
||||
ENV LANGUAGE='en_US:en'
|
||||
|
||||
|
||||
# We make four distinct layers so if there are application changes the library layers can be re-used
|
||||
COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/
|
||||
COPY --chown=185 target/quarkus-app/*.jar /deployments/
|
||||
COPY --chown=185 target/quarkus-app/app/ /deployments/app/
|
||||
COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/
|
||||
|
||||
EXPOSE 8080
|
||||
USER 185
|
||||
ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
|
||||
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
|
||||
|
||||
ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
####
|
||||
# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode
|
||||
#
|
||||
# Before building the container image run:
|
||||
#
|
||||
# mvn package -Pnative -Dquarkus.native.container-build=true
|
||||
#
|
||||
# Then, build the image with:
|
||||
#
|
||||
# docker build -f src/main/docker/Dockerfile.native -t barais/doodleback-with-quarkus .
|
||||
#
|
||||
# Then run the container using:
|
||||
#
|
||||
# docker run -i --rm -p 8080:8080 barais/doodleback-with-quarkus
|
||||
#
|
||||
###
|
||||
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8
|
||||
WORKDIR /work/
|
||||
RUN chown 1001 /work \
|
||||
&& chmod "g+rwX" /work \
|
||||
&& chown 1001:root /work
|
||||
COPY --chown=1001:root target/*-runner /work/application
|
||||
|
||||
EXPOSE 8080
|
||||
USER 1001
|
||||
|
||||
# ENV quarkus_datasource_jdbc_url "jdbc:mysql://db:3306/tlc?useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&createDatabaseIfNotExist=true&serverTimezone=Europe/Paris"
|
||||
# ENV quarkus_datasource_username tlc
|
||||
# ENV quarkus_datasource_password tlc
|
||||
# ENV quarkus_hibernate_orm_database_generation update
|
||||
# ENV quarkus_mailer_from olivier.barais@gmail.com
|
||||
# ENV quarkus_mailer_host localhost
|
||||
# ENV quarkus_mailer_port 2525
|
||||
# ENV quarkus_mailer_ssl false
|
||||
# ENV quarkus_mailer_username ""
|
||||
# ENV quarkus_mailer_password ""
|
||||
# ENV quarkus_mailer_mock true
|
||||
# ENV doodle_usepad false
|
||||
# ENV doodle_padUrl http://etherpad:9001/
|
||||
# ENV doodle_padApiKey "changeit"
|
||||
# ENV doodle_organizermail "olivier.barais@gmail.com"
|
||||
|
||||
|
||||
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]
|
||||
@@ -0,0 +1,12 @@
|
||||
package fr.istic.tlc.dao;
|
||||
|
||||
|
||||
|
||||
import fr.istic.tlc.domain.Choice;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
|
||||
@ApplicationScoped
|
||||
public class ChoiceRepository implements PanacheRepository<Choice> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package fr.istic.tlc.dao;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import fr.istic.tlc.domain.Comment;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepository;
|
||||
|
||||
@ApplicationScoped
|
||||
public class CommentRepository implements PanacheRepository<Comment> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package fr.istic.tlc.dao;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import fr.istic.tlc.domain.MealPreference;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepository;
|
||||
|
||||
@ApplicationScoped
|
||||
public class MealPreferenceRepository implements PanacheRepository<MealPreference> {
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package fr.istic.tlc.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import fr.istic.tlc.domain.Poll;
|
||||
import fr.istic.tlc.domain.User;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepository;
|
||||
|
||||
@ApplicationScoped
|
||||
public class PollRepository implements PanacheRepository<Poll> {
|
||||
|
||||
public Poll findBySlug(String slug){
|
||||
return find("slug", slug).firstResult();
|
||||
}
|
||||
public Poll findByAdminSlug(String slug){
|
||||
return find("slugAdmin", slug).firstResult();
|
||||
}
|
||||
public List<User> findAllUser4Poll(long id){
|
||||
return this.getEntityManager().createQuery("select distinct c.users from Poll p join p.pollChoices as c where p.id = ?1").setParameter(1, id).getResultList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package fr.istic.tlc.dao;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import fr.istic.tlc.domain.User;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepository;
|
||||
|
||||
@ApplicationScoped
|
||||
public class UserRepository implements PanacheRepository<User> {
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package fr.istic.tlc.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.JoinTable;
|
||||
import jakarta.persistence.ManyToMany;
|
||||
import jakarta.persistence.Temporal;
|
||||
import jakarta.persistence.TemporalType;
|
||||
|
||||
@Entity
|
||||
public class Choice {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
|
||||
//private String name;
|
||||
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
private Date startDate;
|
||||
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
private Date endDate;
|
||||
|
||||
@ManyToMany
|
||||
@JoinTable(
|
||||
name = "choice_user",
|
||||
joinColumns = @JoinColumn(name = "choice_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "user_id"))
|
||||
private List<User> users;
|
||||
|
||||
|
||||
public Choice(){}
|
||||
|
||||
public Choice(Date startDate, Date endDate, List<User> users) {
|
||||
this.startDate = startDate;
|
||||
this.endDate = endDate;
|
||||
this.users = users;
|
||||
}
|
||||
|
||||
public void addUser(User user){
|
||||
users.add(user);
|
||||
}
|
||||
|
||||
public void removeUser(User user){
|
||||
users.remove(user);
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Date getstartDate() {
|
||||
return startDate;
|
||||
}
|
||||
|
||||
public void setstartDate(Date startDate) {
|
||||
this.startDate = startDate;
|
||||
}
|
||||
|
||||
public Date getendDate() {
|
||||
return endDate;
|
||||
}
|
||||
|
||||
public void setendDate(Date endDate) {
|
||||
this.endDate = endDate;
|
||||
}
|
||||
|
||||
public List<User> getUsers() {
|
||||
return users;
|
||||
}
|
||||
|
||||
public void setUsers(List<User> users) {
|
||||
this.users = users;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Choice{" +
|
||||
"id=" + id +
|
||||
", startDate=" + startDate +
|
||||
", endDate=" + endDate +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((id == null) ? 0 : id.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
Choice other = (Choice) obj;
|
||||
if (id == null) {
|
||||
if (other.id != null)
|
||||
return false;
|
||||
} else if (!id.equals(other.id))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package fr.istic.tlc.domain;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.JoinTable;
|
||||
import jakarta.persistence.ManyToMany;
|
||||
import jakarta.persistence.Temporal;
|
||||
import jakarta.persistence.TemporalType;
|
||||
|
||||
@Entity
|
||||
public class Comment {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private String content;
|
||||
private String auteur;
|
||||
|
||||
public Comment(){}
|
||||
|
||||
public Comment(String content){
|
||||
this.content=content;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getAuteur() {
|
||||
return auteur;
|
||||
}
|
||||
|
||||
public void setAuteur(String auteur) {
|
||||
this.auteur = auteur;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Comment{" +
|
||||
"id=" + id +
|
||||
", content='" + content + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package fr.istic.tlc.domain;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
|
||||
import jakarta.persistence.ManyToOne;
|
||||
|
||||
@Entity
|
||||
public class MealPreference {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private String content;
|
||||
|
||||
@ManyToOne
|
||||
User user;
|
||||
|
||||
public MealPreference(){}
|
||||
|
||||
public MealPreference(String content){
|
||||
this.content=content;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MealPreference{" +
|
||||
"id=" + id +
|
||||
", content='" + content + '\'' +
|
||||
", user=" + user +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package fr.istic.tlc.domain;
|
||||
|
||||
import fr.istic.tlc.services.Utils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.persistence.CascadeType;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.JoinTable;
|
||||
import jakarta.persistence.ManyToMany;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.OneToOne;
|
||||
import jakarta.persistence.OrderBy;
|
||||
import jakarta.persistence.Temporal;
|
||||
import jakarta.persistence.TemporalType;
|
||||
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
@Entity
|
||||
public class Poll {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private String title;
|
||||
private String location;
|
||||
private String description;
|
||||
private boolean has_meal;
|
||||
private String slug = Utils.getInstance().generateSlug(24);
|
||||
private String slugAdmin = Utils.getInstance().generateSlug(24);
|
||||
private String tlkURL = "https://tlk.io/"+Utils.getInstance().generateSlug(12);
|
||||
public boolean clos = false;
|
||||
|
||||
|
||||
@CreationTimestamp
|
||||
private Date createdAt;
|
||||
|
||||
@UpdateTimestamp
|
||||
private Date updatedAt;
|
||||
|
||||
@OneToMany(cascade = CascadeType.ALL)
|
||||
@JoinColumn(name = "pollID")
|
||||
@OrderBy("startDate ASC")
|
||||
List<Choice> pollChoices;
|
||||
|
||||
|
||||
@OneToOne(cascade = {CascadeType.PERSIST,CascadeType.REMOVE,CascadeType.REFRESH})
|
||||
Choice selectedChoice;
|
||||
|
||||
@OneToMany(cascade = {CascadeType.PERSIST,CascadeType.REMOVE,CascadeType.REFRESH})
|
||||
@JoinColumn(name = "pollID")
|
||||
List<Comment> pollComments = new ArrayList<>();
|
||||
|
||||
@OneToMany(cascade = {CascadeType.PERSIST,CascadeType.REMOVE,CascadeType.REFRESH})
|
||||
@JoinColumn(name = "pollID")
|
||||
List<MealPreference> pollMealPreferences = new ArrayList<>();
|
||||
|
||||
private String padURL;
|
||||
|
||||
public Poll(){}
|
||||
|
||||
public Poll(String title, String location, String description, boolean has_meal, List<Choice> pollChoices) {
|
||||
this.title = title;
|
||||
this.location = location;
|
||||
this.description = description;
|
||||
this.has_meal = has_meal;
|
||||
this.pollChoices = pollChoices;
|
||||
}
|
||||
|
||||
public void addChoice(Choice choice){
|
||||
this.pollChoices.add(choice);
|
||||
}
|
||||
|
||||
public void removeChoice(Choice choice){
|
||||
this.pollChoices.remove(choice);
|
||||
}
|
||||
|
||||
public void addComment(Comment comment){ this.pollComments.add(comment);}
|
||||
|
||||
public void removeComment(Comment comment){ this.pollComments.remove(comment);}
|
||||
|
||||
public void addMealPreference(MealPreference mealPreference){ this.pollMealPreferences.add(mealPreference);}
|
||||
|
||||
public void removeComment(MealPreference mealPreference){ this.pollMealPreferences.remove(mealPreference);}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getTlkURL() {
|
||||
return tlkURL;
|
||||
}
|
||||
|
||||
public void setTlkURL(String tlkURL) {
|
||||
this.tlkURL = tlkURL;
|
||||
}
|
||||
|
||||
public List<Comment> getPollComments() {
|
||||
return pollComments;
|
||||
}
|
||||
|
||||
public List<MealPreference> getPollMealPreferences() {
|
||||
return pollMealPreferences;
|
||||
}
|
||||
|
||||
public void setPollMealPreferences(List<MealPreference> pollMealPreferences) {
|
||||
this.pollMealPreferences = pollMealPreferences;
|
||||
}
|
||||
|
||||
public void setPollComments(List<Comment> pollComments) {
|
||||
this.pollComments = pollComments;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public void setLocation(String location) {
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public boolean isHas_meal() {
|
||||
return has_meal;
|
||||
}
|
||||
|
||||
public void setHas_meal(boolean has_meal) {
|
||||
this.has_meal = has_meal;
|
||||
}
|
||||
|
||||
public String getSlug() {
|
||||
return slug;
|
||||
}
|
||||
|
||||
public void setSlug(String slug) {
|
||||
this.slug = slug;
|
||||
}
|
||||
|
||||
public String getSlugAdmin() {
|
||||
return slugAdmin;
|
||||
}
|
||||
|
||||
public void setSlugAdmin(String slugAdmin) {
|
||||
this.slugAdmin = slugAdmin;
|
||||
}
|
||||
|
||||
public Date getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Date createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public List<Choice> getPollChoices() {
|
||||
return pollChoices;
|
||||
}
|
||||
|
||||
public void setPollChoices(List<Choice> pollChoices) {
|
||||
this.pollChoices = pollChoices;
|
||||
}
|
||||
|
||||
public Choice getSelectedChoice() {
|
||||
return selectedChoice;
|
||||
}
|
||||
|
||||
public void setSelectedChoice(Choice selectedChoice) {
|
||||
this.selectedChoice = selectedChoice;
|
||||
}
|
||||
|
||||
public String getPadURL() {
|
||||
return this.padURL;
|
||||
}
|
||||
|
||||
|
||||
public void setPadURL(String padURL) {
|
||||
this.padURL=padURL;
|
||||
}
|
||||
|
||||
public boolean isClos() {
|
||||
return clos;
|
||||
}
|
||||
|
||||
public void setClos(boolean clos) {
|
||||
this.clos = clos;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Poll{" +
|
||||
"id=" + id +
|
||||
", title='" + title + '\'' +
|
||||
", location='" + location + '\'' +
|
||||
", description='" + description + '\'' +
|
||||
", has_meal=" + has_meal +
|
||||
", createdAt=" + createdAt +
|
||||
'}';
|
||||
}
|
||||
|
||||
public Date getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(Date updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package fr.istic.tlc.domain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.persistence.CascadeType;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.ManyToMany;
|
||||
import jakarta.persistence.OneToMany;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
@Entity
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private String username;
|
||||
private String mail;
|
||||
private String icsurl;
|
||||
|
||||
|
||||
@JsonIgnore
|
||||
@ManyToMany(mappedBy = "users")
|
||||
List<Choice> userChoices = new ArrayList<>();
|
||||
|
||||
|
||||
@JsonIgnore
|
||||
@OneToMany(mappedBy="user", cascade = CascadeType.ALL)
|
||||
List<MealPreference> userMealPreferences = new ArrayList<>();
|
||||
|
||||
public User(){}
|
||||
|
||||
public User(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public void addChoice(Choice choice){
|
||||
this.userChoices.add(choice);
|
||||
}
|
||||
public String getMail() {
|
||||
return mail;
|
||||
}
|
||||
|
||||
public void setMail(String mail) {
|
||||
this.mail = mail;
|
||||
}
|
||||
|
||||
|
||||
public String getIcsurl() {
|
||||
return icsurl;
|
||||
}
|
||||
|
||||
public void setIcsurl(String icsurl) {
|
||||
this.icsurl = icsurl;
|
||||
}
|
||||
|
||||
public void removeChoice(Choice choice){
|
||||
this.userChoices.remove(choice);
|
||||
}
|
||||
|
||||
|
||||
public void addMealPreference (MealPreference mealPreference) {this.userMealPreferences.add(mealPreference);}
|
||||
|
||||
public void removeMealPreference (MealPreference mealPreference) {this.userMealPreferences.remove(mealPreference);}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public List<Choice> getUserChoices() {
|
||||
return userChoices;
|
||||
}
|
||||
|
||||
public List<MealPreference> getUserMealPreferences() {
|
||||
return userMealPreferences;
|
||||
}
|
||||
|
||||
public void setUserMealPreferences(List<MealPreference> userMealPreferences) {
|
||||
this.userMealPreferences = userMealPreferences;
|
||||
}
|
||||
|
||||
public void setUserChoices(List<Choice> userChoices) {
|
||||
this.userChoices = userChoices;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "User{" +
|
||||
"id=" + id +
|
||||
", username='" + username + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package fr.istic.tlc.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ChoiceUser {
|
||||
private List<Long> choices;
|
||||
private String mail;
|
||||
private String pref;
|
||||
private String ics;
|
||||
private String username;
|
||||
|
||||
public List<Long> getChoices() {
|
||||
return choices;
|
||||
}
|
||||
|
||||
public void setChoices(List<Long> value) {
|
||||
this.choices = value;
|
||||
}
|
||||
|
||||
public String getMail() {
|
||||
return mail;
|
||||
}
|
||||
|
||||
public void setMail(String value) {
|
||||
this.mail = value;
|
||||
}
|
||||
|
||||
public String getPref() {
|
||||
return pref;
|
||||
}
|
||||
|
||||
public void setPref(String value) {
|
||||
this.pref = value;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String value) {
|
||||
this.username = value;
|
||||
}
|
||||
|
||||
public String getIcs() {
|
||||
return ics;
|
||||
}
|
||||
|
||||
public void setIcs(String ics) {
|
||||
this.ics = ics;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package fr.istic.tlc.dto;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
|
||||
public class EventDTO {
|
||||
|
||||
private Date startDate;
|
||||
|
||||
private Date endDate;
|
||||
|
||||
private String description;
|
||||
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public Date getStartDate() {
|
||||
return startDate;
|
||||
}
|
||||
|
||||
public void setStartDate(Date startDate) {
|
||||
this.startDate = startDate;
|
||||
}
|
||||
|
||||
public Date getEndDate() {
|
||||
return endDate;
|
||||
}
|
||||
|
||||
public void setEndDate(Date endDate) {
|
||||
this.endDate = endDate;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package fr.istic.tlc.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class EventDTOAndSelectedChoice {
|
||||
|
||||
List<EventDTO> eventdtos;
|
||||
List<Long> selectedChoices;
|
||||
public List<EventDTO> getEventdtos() {
|
||||
return eventdtos;
|
||||
}
|
||||
public void setEventdtos(List<EventDTO> eventdtos) {
|
||||
this.eventdtos = eventdtos;
|
||||
}
|
||||
public List<Long> getSelectedChoices() {
|
||||
return selectedChoices;
|
||||
}
|
||||
public void setSelectedChoices(List<Long> selectedChoices) {
|
||||
this.selectedChoices = selectedChoices;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package fr.istic.tlc.resources;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import fr.istic.tlc.dao.ChoiceRepository;
|
||||
import fr.istic.tlc.dao.PollRepository;
|
||||
import fr.istic.tlc.dao.UserRepository;
|
||||
import fr.istic.tlc.domain.Choice;
|
||||
import fr.istic.tlc.domain.Poll;
|
||||
import fr.istic.tlc.domain.User;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class ChoiceResourceEx {
|
||||
|
||||
@Autowired
|
||||
ChoiceRepository choiceRepository;
|
||||
@Autowired
|
||||
PollRepository pollRepository;
|
||||
@Autowired
|
||||
UserRepository userRepository;
|
||||
|
||||
@GetMapping("/polls/{slug}/choices")
|
||||
public ResponseEntity<List<Choice>> retrieveAllChoicesFromPoll(@PathVariable String slug) {
|
||||
// On vérifie que le choix existe
|
||||
Poll poll = pollRepository.findBySlug(slug);
|
||||
if (poll == null) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
List<Choice> choices = poll.getPollChoices();
|
||||
return new ResponseEntity<>(choices, HttpStatus.OK);
|
||||
}
|
||||
|
||||
@GetMapping("/users/{idUser}/choices")
|
||||
public ResponseEntity<List<Choice>> retrieveAllChoicesFromUser(@PathVariable long idUser) {
|
||||
// On vérifie que l'utilisateur existe
|
||||
User user = userRepository.findById(idUser);
|
||||
if (user == null) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return new ResponseEntity<>(user.getUserChoices(), HttpStatus.OK);
|
||||
}
|
||||
|
||||
@GetMapping("/polls/{slug}/choices/{idChoice}")
|
||||
public ResponseEntity<Choice> retrieveChoiceFromPoll(@PathVariable String slug, @PathVariable long idChoice) {
|
||||
// On vérifie que le choix et le poll existent
|
||||
Poll poll = pollRepository.findBySlug(slug);
|
||||
Choice choice = choiceRepository.findById(idChoice);
|
||||
if (poll == null || choice== null) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
// On vérifie que le choix appartienne bien au poll
|
||||
if(!poll.getPollChoices().contains(choice)){
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return new ResponseEntity<>(choice, HttpStatus.OK);
|
||||
}
|
||||
|
||||
@GetMapping("/users/{idUser}/choices/{idChoice}")
|
||||
public ResponseEntity<Choice> retrieveChoiceFromUser(@PathVariable long idUser, @PathVariable long idChoice) {
|
||||
// On vérifie que le choix et l'utilisateur existent
|
||||
User user = userRepository.findById(idUser);
|
||||
Choice choice = choiceRepository.findById(idChoice);
|
||||
if (user == null || choice == null) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
// On vérifie que le choix appartienne bien à l'utilisateur
|
||||
if(!user.getUserChoices().contains(choice)){
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return new ResponseEntity<>(choice, HttpStatus.OK);
|
||||
}
|
||||
|
||||
@DeleteMapping("/polls/{slug}/choices")
|
||||
public ResponseEntity<?> deleteChoiceFromPoll(@RequestBody HashMap<String, List<Long>> choices, @PathVariable String slug, @RequestParam String token) {
|
||||
// On vérifie que le poll existe
|
||||
List<Long> idchoices = choices.get("choices");
|
||||
Poll poll = pollRepository.findBySlug(slug);
|
||||
if (poll == null) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
// On vérifie que le token soit bon
|
||||
if(!poll.getSlugAdmin().equals(token)){
|
||||
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
// On enlève les choix du poll
|
||||
for (Long id: idchoices) {
|
||||
// On vérifie que le choice existe
|
||||
Choice choice = choiceRepository.findById(id);
|
||||
if (choice!= null) {
|
||||
// On remove le choice du poll
|
||||
poll.removeChoice(choice);
|
||||
pollRepository.getEntityManager().merge(poll);
|
||||
// On remove le choices des utilisateurs
|
||||
for (User user:userRepository.findAll().list()) {
|
||||
if(user.getUserChoices().contains(choice)){
|
||||
user.getUserChoices().remove(choice);
|
||||
userRepository.getEntityManager().merge(user);
|
||||
}
|
||||
}
|
||||
// On supprime le choice
|
||||
choiceRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
return new ResponseEntity<>(HttpStatus.OK);
|
||||
}
|
||||
|
||||
@PostMapping("/polls/{slug}/choices")
|
||||
public ResponseEntity<List<Choice>> createChoices(@RequestBody List<Choice> choices, @PathVariable String slug, @RequestParam String token) {
|
||||
// On vérifie que le poll existe
|
||||
Poll poll = pollRepository.findBySlug(slug);
|
||||
if (poll == null){
|
||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
// On vérifie que le token soit bon
|
||||
if(!poll.getSlugAdmin().equals(token)){
|
||||
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
// On ajoute chaque choix au poll et vice versa
|
||||
for (Choice choice:choices) {
|
||||
this.choiceRepository.persist(choice);
|
||||
poll.addChoice(choice);
|
||||
pollRepository.getEntityManager().merge(poll);
|
||||
}
|
||||
return new ResponseEntity<>(choices, HttpStatus.CREATED);
|
||||
}
|
||||
|
||||
@PutMapping("/polls/{slug}/choices/{idChoice}")
|
||||
public ResponseEntity<Choice> updateChoice(@Valid @RequestBody Choice choice1, @PathVariable String slug, @PathVariable long idChoice, @RequestParam String token) {
|
||||
// On vérifie que le poll et le choix existent
|
||||
Poll poll = pollRepository.findBySlug(slug);
|
||||
Choice choice = choiceRepository.findById(idChoice);
|
||||
if (poll == null || choice == null) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
// On vérifie que le choix appartienne bien au poll
|
||||
if(!poll.getPollChoices().contains(choice)){
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
// On vérifie que le token soit bon
|
||||
if(!poll.getSlugAdmin().equals(token)){
|
||||
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
// On met à jour l'ancien choix
|
||||
Choice ancientChoice = choice;
|
||||
if (choice1.getstartDate()!=null){
|
||||
ancientChoice.setstartDate(choice1.getstartDate());
|
||||
}
|
||||
if (choice1.getendDate()!=null){
|
||||
ancientChoice.setendDate(choice1.getendDate());
|
||||
}
|
||||
// On update la bdd
|
||||
Choice updatedChoice = choiceRepository.getEntityManager().merge(ancientChoice);
|
||||
return new ResponseEntity<>(updatedChoice, HttpStatus.OK);
|
||||
}
|
||||
|
||||
@PostMapping("/polls/{slug}/vote/{idUser}")
|
||||
public ResponseEntity<Object> vote(@RequestBody HashMap<String, List<Long>> choices, @PathVariable String slug, @PathVariable long idUser) {
|
||||
// On vérifie que le poll et l'utilisateur existent
|
||||
List<Long> idchoices = choices.get("choices");
|
||||
Poll poll = pollRepository.findBySlug(slug);
|
||||
User user = userRepository.findById(idUser);
|
||||
if (poll == null || user == null){
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
for (Long choice : idchoices) {
|
||||
// On vérifie que le choice existe
|
||||
Choice optchoice = choiceRepository.findById(choice);
|
||||
if (optchoice == null){
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
// On vérifie que le choix appartienne bien au poll
|
||||
if(!poll.getPollChoices().contains(optchoice)){
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
// On vérifie que le user n'ai pas déjà voté pour ce choix
|
||||
if(user.getUserChoices().contains(optchoice)){
|
||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
// On ajoute le choix à la liste de l'utilisateur et vice versa
|
||||
optchoice.addUser(user);
|
||||
choiceRepository.getEntityManager().merge(optchoice);
|
||||
}
|
||||
return new ResponseEntity<>(HttpStatus.OK);
|
||||
}
|
||||
|
||||
@PostMapping("/polls/{slug}/choices/{idChoice}/removevote/{idUser}")
|
||||
public ResponseEntity<Object> removeVote(@PathVariable String slug, @PathVariable long idChoice, @PathVariable long idUser) {
|
||||
// On vérifie que le poll, le choix et l'utilisateur existent
|
||||
Poll poll = pollRepository.findBySlug(slug);
|
||||
Choice choice = choiceRepository.findById(idChoice);
|
||||
User user = userRepository.findById(idUser);
|
||||
if (poll == null || choice == null || user == null){
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
// On vérifie que le choix appartienne bien au poll
|
||||
if(!poll.getPollChoices().contains(choice)){
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
// On vérifie que le user ait bien voté pour ce choix
|
||||
if(!user.getUserChoices().contains(choice)){
|
||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
// On retire le choix à la liste de l'utilisateur et vice versa
|
||||
choice.removeUser(user);
|
||||
choiceRepository.getEntityManager().merge(choice);
|
||||
user.removeChoice(choice);
|
||||
userRepository.getEntityManager().merge(user);
|
||||
|
||||
return new ResponseEntity<>(HttpStatus.OK);
|
||||
}
|
||||
|
||||
@GetMapping("/polls/{slug}/choices/{idChoice}/count")
|
||||
public ResponseEntity<Object> numberOfVoteForChoice(@PathVariable String slug, @PathVariable long idChoice){
|
||||
// On vérifie que le poll et choix existent
|
||||
Poll poll = pollRepository.findBySlug(slug);
|
||||
Choice choice = choiceRepository.findById(idChoice);
|
||||
if (poll == null || choice == null){
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
// On vérifie que le choix appartienne bien au poll
|
||||
if(!poll.getPollChoices().contains(choice)){
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
// On compte le nombre de vote pour le choix
|
||||
return new ResponseEntity<>(choice.getUsers().size(),HttpStatus.OK);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package fr.istic.tlc.resources;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import fr.istic.tlc.dao.CommentRepository;
|
||||
import fr.istic.tlc.dao.PollRepository;
|
||||
import fr.istic.tlc.domain.Comment;
|
||||
import fr.istic.tlc.domain.Poll;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class CommentResourceEx {
|
||||
@Autowired
|
||||
PollRepository pollRepository;
|
||||
@Autowired
|
||||
CommentRepository commentRepository;
|
||||
|
||||
/* @GetMapping("users/{idUser}/comments")
|
||||
public ResponseEntity<List<Comment>> getAllCommentsFromUser(@PathVariable long idUser) {
|
||||
// On vérifie que l'utilisateur existe
|
||||
User optUser = userRepository.findById(idUser);
|
||||
if(optUser== null){
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return new ResponseEntity<>(optUser.getUserComments(), HttpStatus.OK);
|
||||
}*/
|
||||
|
||||
@GetMapping("polls/{slug}/comments")
|
||||
public ResponseEntity<Object> getAllCommentsFromPoll(@PathVariable String slug) {
|
||||
// On vérifie que le poll existe
|
||||
Poll optPoll = pollRepository.findBySlug(slug);
|
||||
if(optPoll== null){
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return new ResponseEntity<>(optPoll.getPollComments(),HttpStatus.OK);
|
||||
}
|
||||
|
||||
@GetMapping("polls/{slug}/comments/{idComment}")
|
||||
public ResponseEntity<Object> getCommentFromPoll(@PathVariable String slug, @PathVariable long idComment){
|
||||
// On vérifie que le poll et le comment existe
|
||||
Poll optPoll = pollRepository.findBySlug(slug);
|
||||
Comment optComment = commentRepository.findById(idComment);
|
||||
if(optPoll== null || optComment== null){
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
// On vérifie que le comment appartienne bien au poll
|
||||
if (!optPoll.getPollComments().contains(optComment)){
|
||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
return new ResponseEntity<>(optComment,HttpStatus.OK);
|
||||
}
|
||||
|
||||
/* @PostMapping("polls/{slug}/comments/{idUser}")
|
||||
public ResponseEntity<Object> createComment(@Valid @RequestBody Comment comment, @PathVariable String slug, @PathVariable long idUser){
|
||||
// On vérifie que le poll et le User existe
|
||||
Poll poll = pollRepository.findBySlug(slug);
|
||||
User user = userRepository.findById(idUser);
|
||||
if (poll== null || user== null){
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
// On set le user dans comment
|
||||
comment.setUser(user);
|
||||
// On ajoute le commentaire dans le poll
|
||||
poll.addComment(comment);
|
||||
pollRepository.save(poll);
|
||||
// On save le commentaire
|
||||
Comment savedComment = commentRepository.save(comment);
|
||||
return new ResponseEntity<>(savedComment, HttpStatus.CREATED);
|
||||
}*/
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
package fr.istic.tlc.resources;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import javax.mail.MessagingException;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
|
||||
import org.apache.http.impl.nio.client.HttpAsyncClients;
|
||||
|
||||
import fr.istic.tlc.dao.ChoiceRepository;
|
||||
import fr.istic.tlc.dao.PollRepository;
|
||||
import fr.istic.tlc.dao.UserRepository;
|
||||
import fr.istic.tlc.domain.Choice;
|
||||
import fr.istic.tlc.domain.Poll;
|
||||
import fr.istic.tlc.dto.EventDTO;
|
||||
import fr.istic.tlc.dto.EventDTOAndSelectedChoice;
|
||||
import fr.istic.tlc.services.Utils;
|
||||
import net.fortuna.ical4j.data.CalendarBuilder;
|
||||
import net.fortuna.ical4j.data.ParserException;
|
||||
import net.fortuna.ical4j.model.Calendar;
|
||||
import net.fortuna.ical4j.model.Component;
|
||||
import net.fortuna.ical4j.model.ComponentList;
|
||||
import net.fortuna.ical4j.model.DateTime;
|
||||
import net.fortuna.ical4j.model.Period;
|
||||
import net.fortuna.ical4j.model.PeriodList;
|
||||
import net.fortuna.ical4j.model.component.CalendarComponent;
|
||||
import net.fortuna.ical4j.model.component.VEvent;
|
||||
import net.fortuna.ical4j.util.MapTimeZoneCache;
|
||||
|
||||
@Path("/api/ics")
|
||||
public class ICSResources {
|
||||
|
||||
@Inject
|
||||
PollRepository pollRep;
|
||||
|
||||
@Inject
|
||||
UserRepository userRep;
|
||||
|
||||
@Inject
|
||||
ChoiceRepository choiceRep;
|
||||
|
||||
@GET
|
||||
@Path("polls/{slug}/{ics}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public EventDTOAndSelectedChoice parseCalendartoAppointments(@PathParam("slug") String slug,
|
||||
@PathParam("ics") String ics)
|
||||
throws IOException, ParserException, InterruptedException, ExecutionException, MessagingException {
|
||||
EventDTOAndSelectedChoice result = new EventDTOAndSelectedChoice();
|
||||
List<EventDTO> appointments = new ArrayList<>();
|
||||
List<Long> selectedChoices = new ArrayList<>();
|
||||
result.setEventdtos(appointments);
|
||||
result.setSelectedChoices(selectedChoices);
|
||||
|
||||
// Get Poll
|
||||
Poll p = this.pollRep.findBySlug(slug);
|
||||
Date minDate = new Date();
|
||||
if (p != null) {
|
||||
// Get minimal date for Poll to filter ics
|
||||
|
||||
if (p.getPollChoices().size() > 0 && minDate.after(p.getPollChoices().get(0).getstartDate()))
|
||||
minDate = p.getPollChoices().get(0).getstartDate();
|
||||
}
|
||||
|
||||
// Get user to get its ICS
|
||||
// User u = this.userRep.find("mail", usermail).firstResult();
|
||||
byte[] decodedBytes = Base64.getDecoder().decode(ics);
|
||||
String decodedString = new String(decodedBytes);
|
||||
|
||||
if (decodedString != null && !"".equals(decodedString)) {
|
||||
// String s =
|
||||
// "http://zimbra.inria.fr/home/olivier.barais@irisa.fr/Calendar.ics";
|
||||
|
||||
// Query the ics url
|
||||
|
||||
System.setProperty("net.fortuna.ical4j.timezone.cache.impl", MapTimeZoneCache.class.getName());
|
||||
CloseableHttpAsyncClient client = HttpAsyncClients.createDefault();
|
||||
client.start();
|
||||
HttpGet request = new HttpGet(decodedString);
|
||||
|
||||
Future<HttpResponse> future = client.execute(request, null);
|
||||
HttpResponse response = future.get();
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
response.getEntity().writeTo(out);
|
||||
String responseString = out.toString();
|
||||
out.close();
|
||||
client.close();
|
||||
|
||||
// Parse result
|
||||
StringReader sin = new StringReader(responseString);
|
||||
CalendarBuilder builder = new CalendarBuilder();
|
||||
Calendar calendar = builder.build(sin);
|
||||
ComponentList<CalendarComponent> events = calendar.getComponents(Component.VEVENT);
|
||||
List<Choice> choices = new ArrayList<Choice>();
|
||||
if (p!= null)
|
||||
choices = p.getPollChoices();
|
||||
// Create Event to draw
|
||||
java.util.Calendar calEnd = java.util.Calendar.getInstance();
|
||||
calEnd.setTime(new Date());
|
||||
calEnd.add(java.util.Calendar.YEAR, 1);
|
||||
DateTime start = new DateTime(minDate);
|
||||
DateTime end = new DateTime(calEnd.getTime());
|
||||
for (CalendarComponent event : events) {
|
||||
|
||||
Period period = new Period(start, end);
|
||||
PeriodList list = event.calculateRecurrenceSet(period);
|
||||
for (Period p1 : list) {
|
||||
if (minDate.before(p1.getStart())) {
|
||||
EventDTO a = new EventDTO();
|
||||
a.setStartDate(p1.getStart());
|
||||
a.setEndDate(p1.getEnd());
|
||||
if (((VEvent) event).getSummary() != null)
|
||||
a.setDescription(((VEvent) event).getSummary().getValue());
|
||||
|
||||
// Si intersection ajoute l'ID du choice comme ID selected
|
||||
// https://stackoverflow.com/questions/325933/determine-whether-two-date-ranges-overlap
|
||||
for (Choice choice : choices) {
|
||||
if (Utils.getInstance().intersect(choice.getstartDate(), choice.getendDate(), p1.getStart(),
|
||||
p1.getEnd())) {
|
||||
if (!selectedChoices.contains(choice.getId())) {
|
||||
selectedChoices.add(choice.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
appointments.add(a);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package fr.istic.tlc.resources;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import fr.istic.tlc.dao.ChoiceRepository;
|
||||
import fr.istic.tlc.dao.MealPreferenceRepository;
|
||||
import fr.istic.tlc.dao.PollRepository;
|
||||
import fr.istic.tlc.dao.UserRepository;
|
||||
import fr.istic.tlc.domain.MealPreference;
|
||||
import fr.istic.tlc.domain.Poll;
|
||||
import fr.istic.tlc.domain.User;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class MealPreferenceResource {
|
||||
@Autowired
|
||||
ChoiceRepository choiceRepository;
|
||||
@Autowired
|
||||
PollRepository pollRepository;
|
||||
@Autowired
|
||||
UserRepository userRepository;
|
||||
@Autowired
|
||||
MealPreferenceRepository mealPreferenceRepository;
|
||||
|
||||
@GetMapping("polls/{slug}/mealpreferences")
|
||||
public ResponseEntity<Object> getAllMealPreferencesFromPoll(@PathVariable String slug) {
|
||||
// On vérifie que le poll existe
|
||||
Poll optPoll = pollRepository.findBySlug(slug);
|
||||
if(optPoll == null){
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return new ResponseEntity<>(optPoll.getPollMealPreferences(),HttpStatus.OK);
|
||||
}
|
||||
|
||||
@GetMapping("polls/{slug}/mealpreference/{idMealPreference}")
|
||||
public ResponseEntity<Object> getMealPreferenceFromPoll(@PathVariable String slug, @PathVariable long idMealPreference){
|
||||
// On vérifie que le poll et la meal preference existe
|
||||
Poll optPoll = pollRepository.findBySlug(slug);
|
||||
MealPreference optMealPreference = mealPreferenceRepository.findById(idMealPreference);
|
||||
if(optPoll == null || optMealPreference == null){
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
// On vérifie que la meal preference appartienne bien au poll
|
||||
if (!optPoll.getPollMealPreferences().contains(optMealPreference)){
|
||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
return new ResponseEntity<>(optMealPreference,HttpStatus.OK);
|
||||
}
|
||||
|
||||
@PostMapping("polls/{slug}/mealpreference/{idUser}")
|
||||
public ResponseEntity<Object> createMealPreference(@Valid @RequestBody MealPreference mealPreference, @PathVariable String slug, @PathVariable long idUser){
|
||||
// On vérifie que le poll et le User existe
|
||||
Poll poll = pollRepository.findBySlug(slug);
|
||||
User user = userRepository.findById(idUser);
|
||||
if (poll == null || user == null){
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
// On set le user dans la meal preference
|
||||
mealPreference.setUser(user);
|
||||
// On ajoute la meal preference dans le poll
|
||||
poll.addMealPreference(mealPreference);
|
||||
// On save la meal preference
|
||||
mealPreferenceRepository.persist(mealPreference);
|
||||
pollRepository.getEntityManager().merge(poll);
|
||||
return new ResponseEntity<>(mealPreference, HttpStatus.CREATED);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
package fr.istic.tlc.resources;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import fr.istic.tlc.dao.ChoiceRepository;
|
||||
import fr.istic.tlc.dao.CommentRepository;
|
||||
import fr.istic.tlc.dao.MealPreferenceRepository;
|
||||
import fr.istic.tlc.dao.PollRepository;
|
||||
import fr.istic.tlc.dao.UserRepository;
|
||||
import fr.istic.tlc.domain.Choice;
|
||||
import fr.istic.tlc.domain.Comment;
|
||||
import fr.istic.tlc.domain.MealPreference;
|
||||
import fr.istic.tlc.domain.Poll;
|
||||
import fr.istic.tlc.domain.User;
|
||||
import fr.istic.tlc.dto.ChoiceUser;
|
||||
import fr.istic.tlc.services.SendEmail;
|
||||
|
||||
@Path("/api/poll")
|
||||
public class NewPollResourceEx {
|
||||
|
||||
@Inject
|
||||
PollRepository pollRep;
|
||||
|
||||
@Inject
|
||||
UserRepository userRep;
|
||||
|
||||
@Inject
|
||||
ChoiceRepository choiceRep;
|
||||
|
||||
@Inject
|
||||
MealPreferenceRepository mealprefRep;
|
||||
|
||||
@Inject
|
||||
CommentRepository commentRep;
|
||||
|
||||
@Inject
|
||||
SendEmail sendmail;
|
||||
|
||||
@Path("/slug/{slug}")
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Poll getPollBySlug(@PathParam("slug") String slug) {
|
||||
Poll p = pollRep.findBySlug(slug);
|
||||
if (p != null)
|
||||
p.getPollComments().clear();
|
||||
p.setSlugAdmin("");
|
||||
return p;
|
||||
}
|
||||
|
||||
@Path("/aslug/{aslug}")
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Poll getPollByASlug(@PathParam("aslug") String aslug) {
|
||||
return pollRep.findByAdminSlug(aslug);
|
||||
}
|
||||
|
||||
@Path("/comment/{slug}")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Transactional
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Comment createComment4Poll(@PathParam("slug") String slug, Comment c) {
|
||||
this.commentRep.persist(c);
|
||||
Poll p = pollRep.findBySlug(slug);
|
||||
p.addComment(c);
|
||||
this.pollRep.persistAndFlush(p);
|
||||
return c;
|
||||
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/update1")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Transactional
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Poll updatePoll(Poll p) {
|
||||
System.err.println( "p " + p);
|
||||
Poll p1 = pollRep.findById(p.getId());
|
||||
List<Choice> choicesToRemove = new ArrayList<Choice>();
|
||||
for (Choice c : p1.getPollChoices()) {
|
||||
if (!p.getPollChoices().contains(c)) {
|
||||
|
||||
choicesToRemove.add(c);
|
||||
System.err.println("toremove " + c.getId());
|
||||
}
|
||||
|
||||
}
|
||||
for (Choice c : p.getPollChoices()) {
|
||||
if (c.getId() != null) {
|
||||
this.choiceRep.getEntityManager().merge(c);
|
||||
} else {
|
||||
this.choiceRep.getEntityManager().persist(c);
|
||||
}
|
||||
|
||||
}
|
||||
for (Choice c : choicesToRemove) {
|
||||
if (c.equals(p1.getSelectedChoice())) {
|
||||
p.setSelectedChoice(null);
|
||||
p1.setSelectedChoice(null);
|
||||
p.setClos(false);
|
||||
}
|
||||
for (User u : c.getUsers()) {
|
||||
u.getUserChoices().remove(c);
|
||||
}
|
||||
c.getUsers().clear();
|
||||
this.choiceRep.delete(c);
|
||||
|
||||
}
|
||||
|
||||
for (Choice c : p.getPollChoices()) {
|
||||
System.err.println("tomerge " + c.getId());
|
||||
}
|
||||
|
||||
Poll p2 = this.pollRep.getEntityManager().merge(p);
|
||||
return p2;
|
||||
|
||||
}
|
||||
|
||||
@Path("/choiceuser")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Transactional
|
||||
public User addChoiceUser(ChoiceUser userChoice) {
|
||||
User u = this.userRep.find("mail", userChoice.getMail()).firstResult();
|
||||
if (u == null) {
|
||||
u = new User();
|
||||
u.setUsername(userChoice.getUsername());
|
||||
u.setIcsurl(userChoice.getIcs());
|
||||
u.setMail(userChoice.getMail());
|
||||
this.userRep.persist(u);
|
||||
}
|
||||
|
||||
|
||||
if (userChoice.getPref() != null && !"".equals(userChoice.getPref())) {
|
||||
MealPreference mp = new MealPreference();
|
||||
mp.setContent(userChoice.getPref());
|
||||
mp.setUser(u);
|
||||
this.mealprefRep.persist(mp);
|
||||
}
|
||||
for (Long choiceId : userChoice.getChoices()) {
|
||||
Choice c = this.choiceRep.findById(choiceId);
|
||||
c.addUser(u);
|
||||
this.choiceRep.persistAndFlush(c);
|
||||
}
|
||||
return u;
|
||||
}
|
||||
|
||||
@Path("/selectedchoice/{choiceid}")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Transactional
|
||||
public void closePoll(@PathParam("choiceid") String choiceid) {
|
||||
Choice c = choiceRep.findById(Long.parseLong(choiceid));
|
||||
Poll p = this.pollRep.find("select p from Poll as p join p.pollChoices as c where c.id= ?1", c.getId())
|
||||
.firstResult();
|
||||
p.setClos(true);
|
||||
p.setSelectedChoice(c);
|
||||
this.pollRep.persist(p);
|
||||
this.sendmail.sendASimpleEmail(p);
|
||||
// TODO Send Email
|
||||
|
||||
}
|
||||
|
||||
@GET()
|
||||
@Path("polls/{slug}/comments")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public List<Comment> getAllCommentsFromPoll(@PathParam("slug") String slug) {
|
||||
Poll p = this.pollRep.findBySlug(slug);
|
||||
if (p!= null)
|
||||
return p.getPollComments();
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package fr.istic.tlc.resources;
|
||||
|
||||
import fr.istic.tlc.services.Utils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import fr.istic.tlc.dao.PollRepository;
|
||||
import fr.istic.tlc.domain.Poll;
|
||||
import io.quarkus.panache.common.Sort;
|
||||
import net.gjerull.etherpad.client.EPLiteClient;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class PollResourceEx {
|
||||
|
||||
@Autowired
|
||||
PollRepository pollRepository;
|
||||
|
||||
@ConfigProperty(name = "doodle.usepad")
|
||||
Boolean usePad = true;
|
||||
@ConfigProperty(name = "doodle.internalPadUrl", defaultValue="http://etherpad:9001/")
|
||||
String padUrl = "";
|
||||
@ConfigProperty(name = "doodle.externalPadUrl", defaultValue="http://etherpad.diverse-team.fr/")
|
||||
String externalPadUrl = "";
|
||||
@ConfigProperty(name = "doodle.padApiKey")
|
||||
String apikey = "";
|
||||
EPLiteClient client;
|
||||
|
||||
@GetMapping("/polls")
|
||||
public ResponseEntity<List<Poll>> retrieveAllpolls() {
|
||||
// On récupère la liste de tous les poll qu'on trie ensuite par titre
|
||||
List<Poll> polls = pollRepository.findAll(Sort.by("title", Sort.Direction.Ascending)).list();
|
||||
return new ResponseEntity<>(polls, HttpStatus.OK);
|
||||
}
|
||||
|
||||
@GetMapping("/polls/{slug}")
|
||||
public ResponseEntity<Poll> retrievePoll(@PathVariable String slug, @RequestParam(required = false) String token) {
|
||||
// On vérifie que le poll existe
|
||||
Poll poll = pollRepository.findBySlug(slug);
|
||||
if (poll == null) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
// Si un token est donné, on vérifie qu'il soit bon
|
||||
if (token != null && !poll.getSlugAdmin().equals(token)) {
|
||||
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
poll.setSlugAdmin("");
|
||||
return new ResponseEntity<>(poll, HttpStatus.OK);
|
||||
}
|
||||
|
||||
@GetMapping("/polls/{slug}/pad")
|
||||
public ResponseEntity<String> retrievePadURL(@PathVariable("slug") String slug) {
|
||||
// On vérifie que le poll existe
|
||||
Poll poll = pollRepository.findBySlug(slug);
|
||||
if (poll == null) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return new ResponseEntity<>(poll.getPadURL(), HttpStatus.OK);
|
||||
}
|
||||
|
||||
@DeleteMapping("/polls/{slug}")
|
||||
@Transactional
|
||||
public ResponseEntity<Poll> deletePoll(@PathVariable("slug") String slug, @RequestParam String token) {
|
||||
// On vérifie que le poll existe
|
||||
Poll poll = pollRepository.findBySlug(slug);
|
||||
if (poll == null) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
// On vérifie que le token soit bon
|
||||
if (!poll.getSlugAdmin().equals(token)) {
|
||||
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
// On supprime tous les choix du poll
|
||||
// Fait automatiquement par le cascade type ALL
|
||||
|
||||
// On supprime tous les commentaires du poll
|
||||
// Fait automatiquement par le cascade type ALL
|
||||
|
||||
// On supprime le pad
|
||||
if (client == null) {
|
||||
client = new EPLiteClient(padUrl, apikey);
|
||||
}
|
||||
|
||||
client.deletePad(getPadId(poll));
|
||||
// On supprime le poll de la bdd
|
||||
pollRepository.deleteById(poll.getId());
|
||||
return new ResponseEntity<>(poll, HttpStatus.OK);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@PostMapping("/polls")
|
||||
@Transactional
|
||||
public ResponseEntity<Poll> createPoll(@Valid @RequestBody Poll poll) {
|
||||
// On enregistre le poll dans la bdd
|
||||
String padId = Utils.getInstance().generateSlug(15);
|
||||
if (this.usePad) {
|
||||
if (client == null) {
|
||||
client = new EPLiteClient(padUrl, apikey);
|
||||
}
|
||||
client.createPad(padId);
|
||||
initPad(poll.getTitle(), poll.getLocation(), poll.getDescription(), client, padId);
|
||||
poll.setPadURL(externalPadUrl + "p/" + padId);
|
||||
}
|
||||
pollRepository.persist(poll);
|
||||
return new ResponseEntity<>(poll, HttpStatus.CREATED);
|
||||
}
|
||||
|
||||
@PutMapping("/polls/{slug}")
|
||||
@Transactional
|
||||
public ResponseEntity<Object> updatePoll(@Valid @RequestBody Poll poll, @PathVariable String slug,
|
||||
@RequestParam String token) {
|
||||
// On vérifie que le poll existe
|
||||
Poll optionalPoll = pollRepository.findBySlug(slug);
|
||||
if (optionalPoll == null)
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
// On vérifie que le token soit bon
|
||||
if (!optionalPoll.getSlugAdmin().equals(token)) {
|
||||
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
// On met au poll le bon id et les bons slugs
|
||||
Poll ancientPoll = optionalPoll;
|
||||
// On se connecte au pad
|
||||
String padId = getPadId(ancientPoll);
|
||||
|
||||
// On sauvegarde les anciennes données pour mettre à jour le pad
|
||||
String title = ancientPoll.getTitle();
|
||||
String location = ancientPoll.getLocation();
|
||||
String description = ancientPoll.getDescription();
|
||||
|
||||
// On met à jour l'ancien poll
|
||||
if (poll.getTitle() != null) {
|
||||
ancientPoll.setTitle(poll.getTitle());
|
||||
}
|
||||
if (poll.getLocation() != null) {
|
||||
ancientPoll.setLocation(poll.getLocation());
|
||||
}
|
||||
if (poll.getDescription() != null) {
|
||||
ancientPoll.setDescription(poll.getDescription());
|
||||
}
|
||||
ancientPoll.setHas_meal(poll.isHas_meal());
|
||||
// On update le pad
|
||||
String ancientPad = (String) client.getText(padId).get("text");
|
||||
ancientPad = ancientPad.replaceFirst(title, ancientPoll.getTitle());
|
||||
ancientPad = ancientPad.replaceFirst(location, ancientPoll.getLocation());
|
||||
ancientPad = ancientPad.replaceFirst(description, ancientPoll.getDescription());
|
||||
client.setText(padId, ancientPad);
|
||||
// On enregistre le poll dans la bdd
|
||||
Poll updatedPoll = pollRepository.getEntityManager().merge(ancientPoll);
|
||||
return new ResponseEntity<>(updatedPoll, HttpStatus.OK);
|
||||
}
|
||||
|
||||
private static void initPad(String pollTitle, String pollLocation, String pollDescription, EPLiteClient client,
|
||||
String padId) {
|
||||
final String str = pollTitle + '\n' + "Localisation : " + pollLocation + '\n' + "Description : "
|
||||
+ pollDescription + '\n';
|
||||
client.setText(padId, str);
|
||||
}
|
||||
|
||||
private static String getPadId(Poll poll) {
|
||||
//return poll.getPadURL().substring(poll.getPadURL().length() - 6);
|
||||
return poll.getPadURL().substring(poll.getPadURL().lastIndexOf('/') + 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package fr.istic.tlc.resources;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import fr.istic.tlc.dao.ChoiceRepository;
|
||||
import fr.istic.tlc.dao.PollRepository;
|
||||
import fr.istic.tlc.dao.UserRepository;
|
||||
import fr.istic.tlc.domain.Choice;
|
||||
import fr.istic.tlc.domain.Poll;
|
||||
import fr.istic.tlc.domain.User;
|
||||
import io.quarkus.panache.common.Sort;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class UserResource {
|
||||
|
||||
@Autowired
|
||||
ChoiceRepository choiceRepository;
|
||||
@Autowired
|
||||
PollRepository pollRepository;
|
||||
@Autowired
|
||||
UserRepository userRepository;
|
||||
|
||||
@GetMapping("/users")
|
||||
public ResponseEntity<List<User>> retrieveAllUsers() {
|
||||
// On récupère tous les utilisateurs qu'on trie ensuite par username
|
||||
List<User> users = userRepository.findAll(Sort.by( "username", Sort.Direction.Ascending)).list();
|
||||
return new ResponseEntity<>(users, HttpStatus.OK);
|
||||
}
|
||||
|
||||
@GetMapping("/users/{idUser}")
|
||||
public ResponseEntity<User> retrieveUser(@PathVariable long idUser) {
|
||||
// On vérifie que l'utilisateur existe
|
||||
User user = userRepository.findById(idUser);
|
||||
if (user== null) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return new ResponseEntity<>(user, HttpStatus.OK);
|
||||
}
|
||||
|
||||
@GetMapping("/polls/{slug}/users")
|
||||
public ResponseEntity<List<User>> getAllUserFromPoll(@PathVariable String slug) {
|
||||
List<User> users = new ArrayList<>();
|
||||
// On vérifie que le poll existe
|
||||
Poll poll = pollRepository.findBySlug(slug);
|
||||
if (poll== null) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
// On parcours les choix du poll pour récupérer les users ayant voté
|
||||
if (!poll.getPollChoices().isEmpty()) {
|
||||
for (Choice choice : poll.getPollChoices()) {
|
||||
if (!choice.getUsers().isEmpty()) {
|
||||
for (User user : choice.getUsers()) {
|
||||
// On vérifie que le user ne soit pas déjà dans la liste
|
||||
if (!users.contains(user)) {
|
||||
users.add(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ResponseEntity<>(users, HttpStatus.OK);
|
||||
}
|
||||
|
||||
@DeleteMapping("/users/{idUser}")
|
||||
public ResponseEntity<User> deleteUser(@PathVariable long idUser) {
|
||||
// On vérifie que l'utilisateur existe
|
||||
User user = userRepository.findById(idUser);
|
||||
if (user== null) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
// On supprime l'utilisateur de la liste d'utilisateur de chaque choix
|
||||
for (Choice choice : user.getUserChoices()) {
|
||||
choice.removeUser(user);
|
||||
choiceRepository.getEntityManager().merge(choice);
|
||||
}
|
||||
// On supprime les commentaires de l'utilisateurs
|
||||
// Fait automatiquement par le cascade type ALL
|
||||
|
||||
// On supprime l'utilisateur de la bdd
|
||||
userRepository.deleteById(idUser);
|
||||
return new ResponseEntity<>(user, HttpStatus.OK);
|
||||
}
|
||||
|
||||
@PostMapping("/users")
|
||||
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
|
||||
// On sauvegarde l'utilisateur dans la bdd
|
||||
userRepository.persist(user);
|
||||
return new ResponseEntity<>(user, HttpStatus.CREATED);
|
||||
}
|
||||
|
||||
@PutMapping("/users/{idUser}")
|
||||
public ResponseEntity<User> updateUser(@PathVariable long idUser, @Valid @RequestBody User user) {
|
||||
// On vérifie que l'utilisateur existe
|
||||
User optionalUser = userRepository.findById(idUser);
|
||||
if (optionalUser== null) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
// On met le bon id sur l'utilisateur
|
||||
User ancientUser = optionalUser;
|
||||
if (user.getUsername() != null) {
|
||||
ancientUser.setUsername(user.getUsername());
|
||||
}
|
||||
// On update l'utilisateur dans la bdd
|
||||
User updatedUser = userRepository.getEntityManager().merge(ancientUser);
|
||||
return new ResponseEntity<>(updatedUser, HttpStatus.OK);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
package fr.istic.tlc.resources.features;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import fr.istic.tlc.dao.ChoiceRepository;
|
||||
import fr.istic.tlc.dao.PollRepository;
|
||||
import fr.istic.tlc.dao.UserRepository;
|
||||
import fr.istic.tlc.domain.Choice;
|
||||
import fr.istic.tlc.domain.Poll;
|
||||
import fr.istic.tlc.domain.User;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jxl.Workbook;
|
||||
import jxl.format.Alignment;
|
||||
import jxl.format.Border;
|
||||
import jxl.format.BorderLineStyle;
|
||||
import jxl.format.Colour;
|
||||
import jxl.format.VerticalAlignment;
|
||||
import jxl.write.Label;
|
||||
import jxl.write.Number;
|
||||
import jxl.write.WritableCellFormat;
|
||||
import jxl.write.WritableFont;
|
||||
import jxl.write.WritableSheet;
|
||||
import jxl.write.WritableWorkbook;
|
||||
|
||||
@Path("/api")
|
||||
public class ExportResource {
|
||||
@Inject
|
||||
ChoiceRepository choiceRepository;
|
||||
@Inject
|
||||
PollRepository pollRepository;
|
||||
@Inject
|
||||
UserRepository userRepository;
|
||||
|
||||
|
||||
@ConfigProperty(name = "doodle.tmpfolder")
|
||||
String EXCEL_FILE_LOCATION = "/tmp/excelFiles";
|
||||
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_OCTET_STREAM)
|
||||
@Path("/polls/{slug}/results")
|
||||
public Response downloadResultsExcel(@PathParam("slug") String slug) throws IOException {
|
||||
Poll poll = pollRepository.findBySlug(slug);
|
||||
if (poll == null) {
|
||||
return null;
|
||||
}
|
||||
String filePath = createExcelFile(poll, slug);
|
||||
return getHttpEntityToDownload(filePath, "vnd.ms-excel");
|
||||
}
|
||||
|
||||
/*
|
||||
* @RequestMapping(value = "/polls/{slug}/print", method = RequestMethod.GET,
|
||||
* produces = APPLICATION_PDF) public @ResponseBody HttpEntity<byte[]>
|
||||
* downloadResultsPdf(@PathVariable String slug) throws IOException { Poll poll
|
||||
* = pollRepository.findBySlug(slug); if (poll == null) { return new
|
||||
* ResponseEntity<>(HttpStatus.NOT_FOUND); } String filePath = "./Test.xls";
|
||||
* //Utils.excel2pdf(); //convertToPdf(filePath); return
|
||||
* getHttpEntityToDownload(filePath,"pdf"); }
|
||||
*/
|
||||
|
||||
/*
|
||||
* private String convertToPdf(String filePath){ return "";
|
||||
*
|
||||
* }
|
||||
*/
|
||||
|
||||
static int beginningColumnCell = 0;
|
||||
static int beginningRowCell = 3;
|
||||
static int fontSize = 9;
|
||||
static Colour borderColour = Colour.WHITE;
|
||||
|
||||
private String createExcelFile(Poll poll, String slug) throws IOException {
|
||||
DateFormat dateFormat = new SimpleDateFormat("dd.MM.yy-HH.mm.ss");
|
||||
Date date = new Date();
|
||||
String fileName = EXCEL_FILE_LOCATION + File.separator + slug + "-" + dateFormat.format(date) + ".xls";
|
||||
|
||||
File folder = new File(EXCEL_FILE_LOCATION);
|
||||
if (!folder.exists()) {
|
||||
folder.mkdir();
|
||||
}
|
||||
|
||||
// Create an Excel file
|
||||
WritableWorkbook Wbook = null;
|
||||
try {
|
||||
System.out.println("Création du fichier");
|
||||
// Create an Excel file in the file location
|
||||
File file = new File(fileName);
|
||||
|
||||
if (!file.createNewFile()) {
|
||||
System.out.println("Erreur lors de la création du fichier");
|
||||
}
|
||||
Wbook = Workbook.createWorkbook(file);
|
||||
|
||||
// Create an Excel sheet
|
||||
WritableSheet mainSheet = Wbook.createSheet("SONDAGE", 0);
|
||||
Wbook.setColourRGB(Colour.BLUE, 53, 37, 230);
|
||||
|
||||
// Format objects
|
||||
WritableCellFormat formatTitle = new WritableCellFormat();
|
||||
WritableFont fontTitle = new WritableFont(WritableFont.TAHOMA, 16, WritableFont.BOLD);
|
||||
fontTitle.setColour(Colour.BLUE);
|
||||
formatTitle.setFont(fontTitle);
|
||||
|
||||
Label label;
|
||||
label = new Label(0, 0, "Sondage \"" + poll.getTitle() + "\"", formatTitle);
|
||||
mainSheet.addCell(label);
|
||||
label = new Label(0, 1, "http://localhost:3000/polls/" + poll.getSlug());
|
||||
mainSheet.addCell(label);
|
||||
|
||||
// On récupere les users qui ont voté dans ce sondage
|
||||
List<User> users = retrieveUsers(poll);
|
||||
|
||||
// On ecrit les users sur la première colonne
|
||||
writeUsers(poll, Wbook, users);
|
||||
|
||||
// On ecrit les choix avec les votes de chaque users
|
||||
writeChoices(poll, Wbook, users);
|
||||
|
||||
System.out.println("Enregistrement du fichier");
|
||||
// On ecrit les donnée du workbook dans un format excel
|
||||
Wbook.write();
|
||||
|
||||
} catch (Exception e) {
|
||||
System.out.println("Erreur lors de la création du fichier :( " + e.toString());
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
|
||||
if (Wbook != null) {
|
||||
try {
|
||||
Wbook.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
|
||||
private List<User> retrieveUsers(Poll poll) {
|
||||
List<User> users = new ArrayList<>();
|
||||
// On parcours les choix du poll pour récupérer les users ayant voté
|
||||
if (!poll.getPollChoices().isEmpty()) {
|
||||
for (Choice choice : poll.getPollChoices()) {
|
||||
if (!choice.getUsers().isEmpty()) {
|
||||
for (User user : choice.getUsers()) {
|
||||
// On vérifie que le user ne soit pas déjà dans la liste
|
||||
if (!users.contains(user)) {
|
||||
users.add(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return users;
|
||||
}
|
||||
|
||||
private void writeChoices(Poll poll, WritableWorkbook Wbook, List<User> users) throws jxl.write.WriteException {
|
||||
Label label;
|
||||
Number number;
|
||||
WritableSheet mainSheet = Wbook.getSheet(0);
|
||||
|
||||
List<fr.istic.tlc.domain.Choice> choices = poll.getPollChoices();
|
||||
// List<Choice> choices =
|
||||
// choiceRepository.findAll(Sort.by(Sort.Direction.ASC,"startDate"));
|
||||
|
||||
// Format objects
|
||||
WritableCellFormat formatVoteYes = new WritableCellFormat();
|
||||
formatVoteYes.setAlignment(Alignment.CENTRE);
|
||||
formatVoteYes.setVerticalAlignment(VerticalAlignment.CENTRE);
|
||||
formatVoteYes.setBorder(Border.ALL, BorderLineStyle.THIN, borderColour);
|
||||
formatVoteYes.setBackground(Colour.LIGHT_GREEN);
|
||||
WritableFont fontVoteYes = new WritableFont(WritableFont.TAHOMA, fontSize, WritableFont.NO_BOLD);
|
||||
fontVoteYes.setColour(Colour.BLACK);
|
||||
formatVoteYes.setFont(fontVoteYes);
|
||||
// Format objects
|
||||
Wbook.setColourRGB(Colour.LIGHT_ORANGE, 255, 195, 195);
|
||||
WritableCellFormat formatVoteNo = new WritableCellFormat();
|
||||
formatVoteNo.setAlignment(Alignment.CENTRE);
|
||||
formatVoteNo.setVerticalAlignment(VerticalAlignment.CENTRE);
|
||||
formatVoteNo.setBorder(Border.ALL, BorderLineStyle.THIN, borderColour);
|
||||
formatVoteNo.setBackground(Colour.LIGHT_ORANGE);
|
||||
WritableFont fontVoteNo = new WritableFont(WritableFont.TAHOMA, fontSize, WritableFont.NO_BOLD);
|
||||
fontVoteNo.setColour(Colour.BLACK);
|
||||
formatVoteNo.setFont(fontVoteNo);
|
||||
|
||||
// On ecrit les colonnes des choix
|
||||
for (int i = 0; i < choices.size(); i++) {
|
||||
mainSheet.setColumnView(1 + beginningColumnCell + i, 14);
|
||||
// On ecrit la date
|
||||
writeChoiceDate(Wbook, choices, i);
|
||||
|
||||
// On ecrit les votes
|
||||
List<User> listUsersVotes = choices.get(i).getUsers();
|
||||
for (int x = 0; x < users.size(); x++) {
|
||||
if (listUsersVotes.contains(users.get(x))) {
|
||||
label = new Label(1 + beginningColumnCell + i, 3 + beginningRowCell + x, "OK", formatVoteYes);
|
||||
} else {
|
||||
label = new Label(1 + beginningColumnCell + i, 3 + beginningRowCell + x, "-", formatVoteNo);
|
||||
}
|
||||
mainSheet.addCell(label);
|
||||
}
|
||||
// on ecrit le nombre total de vote pour le choix
|
||||
number = new Number(1 + beginningColumnCell + i, 3 + beginningRowCell + users.size(),
|
||||
listUsersVotes.size());
|
||||
mainSheet.addCell(number);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void writeChoiceDate(WritableWorkbook Wbook, List<Choice> choices, int i) throws jxl.write.WriteException {
|
||||
Label label;
|
||||
WritableSheet mainSheet = Wbook.getSheet(0);
|
||||
String month[] = { "Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Aout", "Septembre",
|
||||
"Novembre", "Décembre" };
|
||||
String dayOfWeek[] = { "Lun.", "Mar.", "Mer.", "Jeu.", "Ven.", "Sam.", "Dim." };
|
||||
Wbook.setColourRGB(Colour.BLUE, 53, 37, 230);
|
||||
// Format objects
|
||||
WritableCellFormat formatDate = new WritableCellFormat();
|
||||
formatDate.setAlignment(Alignment.CENTRE);
|
||||
formatDate.setVerticalAlignment(VerticalAlignment.CENTRE);
|
||||
formatDate.setBorder(Border.ALL, BorderLineStyle.THIN, borderColour);
|
||||
formatDate.setBackground(Colour.BLUE);
|
||||
WritableFont fontDate = new WritableFont(WritableFont.TAHOMA, fontSize, WritableFont.NO_BOLD);
|
||||
fontDate.setColour(Colour.WHITE);
|
||||
formatDate.setFont(fontDate);
|
||||
|
||||
// On recupère la date de début
|
||||
Choice choice = choices.get(i);
|
||||
Date startDate = choice.getstartDate();
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTime(startDate);
|
||||
int startYear = calendar.get(Calendar.YEAR);
|
||||
String startMonth = month[calendar.get(Calendar.MONTH)];
|
||||
int startDayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
|
||||
String startDayOfWeek = dayOfWeek[calendar.get(Calendar.DAY_OF_WEEK) - 1];
|
||||
int startHourInt = calendar.get(Calendar.HOUR_OF_DAY);
|
||||
String startHour = (startHourInt < 10 ? "0" : "") + startHourInt;
|
||||
int startMinuteInt = calendar.get(Calendar.MINUTE);
|
||||
String startMinute = (startMinuteInt < 10 ? "0" : "") + startMinuteInt;
|
||||
// On recupère la date de fin
|
||||
Date endDate = choice.getendDate();
|
||||
calendar.setTime(endDate);
|
||||
int endHourInt = calendar.get(Calendar.HOUR_OF_DAY);
|
||||
String endHour = (endHourInt < 10 ? "0" : "") + endHourInt;
|
||||
int endMinuteInt = calendar.get(Calendar.MINUTE);
|
||||
String endMinute = (endMinuteInt < 10 ? "0" : "") + endMinuteInt;
|
||||
|
||||
label = new Label(1 + beginningColumnCell + i, beginningRowCell, startMonth + " " + startYear, formatDate);
|
||||
mainSheet.addCell(label);
|
||||
label = new Label(1 + beginningColumnCell + i, 1 + beginningRowCell, startDayOfWeek + " " + startDayOfMonth,
|
||||
formatDate);
|
||||
mainSheet.addCell(label);
|
||||
label = new Label(1 + beginningColumnCell + i, 2 + beginningRowCell,
|
||||
startHour + ":" + startMinute + " - " + endHour + ":" + endMinute, formatDate);
|
||||
mainSheet.addCell(label);
|
||||
}
|
||||
|
||||
private void writeUsers(Poll poll, WritableWorkbook Wbook, List<User> users) throws jxl.write.WriteException {
|
||||
Label label;
|
||||
|
||||
WritableSheet mainSheet = Wbook.getSheet(0);
|
||||
mainSheet.setColumnView(beginningColumnCell, 25);
|
||||
|
||||
// Format objects
|
||||
WritableCellFormat formatUser = new WritableCellFormat();
|
||||
formatUser.setAlignment(Alignment.RIGHT);
|
||||
formatUser.setVerticalAlignment(VerticalAlignment.CENTRE);
|
||||
formatUser.setBorder(Border.ALL, BorderLineStyle.THIN, borderColour);
|
||||
|
||||
formatUser.setBackground(Colour.GRAY_25);
|
||||
WritableFont fontUser = new WritableFont(WritableFont.TAHOMA, fontSize, WritableFont.NO_BOLD);
|
||||
fontUser.setColour(Colour.BLACK);
|
||||
formatUser.setFont(fontUser);
|
||||
|
||||
// On ecrit la premier colonne avec users et label "Nombre"
|
||||
for (int i = 0; i < users.size(); i++) {
|
||||
label = new Label(beginningColumnCell, 3 + beginningRowCell + i, users.get(i).getUsername(), formatUser);
|
||||
mainSheet.addCell(label);
|
||||
}
|
||||
label = new Label(beginningColumnCell, 3 + beginningRowCell + users.size(), "Nombre");
|
||||
mainSheet.addCell(label);
|
||||
}
|
||||
|
||||
private Response getHttpEntityToDownload(String filePath, String fileType) throws IOException {
|
||||
File file = getFile(filePath);
|
||||
|
||||
// header.set("Content-Disposition", "inline; filename=" + file.getName());
|
||||
return Response.ok(((Object) file), MediaType.APPLICATION_OCTET_STREAM)
|
||||
.header("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"").build();
|
||||
}
|
||||
|
||||
private File getFile(String filePath) throws FileNotFoundException {
|
||||
File file = new File(filePath);
|
||||
if (!file.exists()) {
|
||||
throw new FileNotFoundException("file with path: " + filePath + " was not found.");
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
/*
|
||||
* @ControllerAdvice public class GlobalExceptionHandler {
|
||||
*
|
||||
* @ExceptionHandler(value = FileNotFoundException.class) public void
|
||||
* handle(FileNotFoundException ex, HttpServletResponse response) throws
|
||||
* IOException { System.out.println("handling file not found exception");
|
||||
* response.sendError(404, ex.getMessage()); }
|
||||
*
|
||||
* @ExceptionHandler(value = IOException.class) public void handle(IOException
|
||||
* ex, HttpServletResponse response) throws IOException {
|
||||
* System.out.println("handling io exception"); response.sendError(500,
|
||||
* ex.getMessage()); } }
|
||||
*/
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package fr.istic.tlc.resources.itf;
|
||||
|
||||
|
||||
import fr.istic.tlc.dao.ChoiceRepository;
|
||||
import fr.istic.tlc.domain.Choice;
|
||||
import io.quarkus.hibernate.orm.rest.data.panache.PanacheRepositoryResource;
|
||||
import io.quarkus.rest.data.panache.ResourceProperties;
|
||||
|
||||
@ResourceProperties(path = "/api/choice")
|
||||
public interface ChoiceResource extends PanacheRepositoryResource<ChoiceRepository, Choice,Long> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package fr.istic.tlc.resources.itf;
|
||||
|
||||
import fr.istic.tlc.dao.CommentRepository;
|
||||
import fr.istic.tlc.domain.Comment;
|
||||
import io.quarkus.hibernate.orm.rest.data.panache.PanacheRepositoryResource;
|
||||
import io.quarkus.rest.data.panache.ResourceProperties;
|
||||
|
||||
@ResourceProperties(path = "/api/comment")
|
||||
public interface CommentResource extends PanacheRepositoryResource<CommentRepository, Comment,Long> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package fr.istic.tlc.resources.itf;
|
||||
|
||||
import fr.istic.tlc.dao.MealPreferenceRepository;
|
||||
import fr.istic.tlc.domain.MealPreference;
|
||||
import io.quarkus.hibernate.orm.rest.data.panache.PanacheRepositoryResource;
|
||||
import io.quarkus.rest.data.panache.ResourceProperties;
|
||||
|
||||
@ResourceProperties(path = "/api/mealpreference")
|
||||
public interface MealPreferenceResource extends PanacheRepositoryResource<MealPreferenceRepository,MealPreference,Long> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package fr.istic.tlc.resources.itf;
|
||||
|
||||
import fr.istic.tlc.dao.PollRepository;
|
||||
import fr.istic.tlc.domain.Poll;
|
||||
import io.quarkus.hibernate.orm.rest.data.panache.PanacheRepositoryResource;
|
||||
import io.quarkus.rest.data.panache.ResourceProperties;
|
||||
|
||||
@ResourceProperties(path = "/api/poll")
|
||||
public interface PollResource extends PanacheRepositoryResource<PollRepository, Poll,Long> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package fr.istic.tlc.resources.itf;
|
||||
|
||||
import fr.istic.tlc.dao.UserRepository;
|
||||
import fr.istic.tlc.domain.User;
|
||||
import io.quarkus.hibernate.orm.rest.data.panache.PanacheRepositoryResource;
|
||||
import io.quarkus.rest.data.panache.ResourceProperties;
|
||||
|
||||
@ResourceProperties(path = "/api/user")
|
||||
public interface UserResource extends PanacheRepositoryResource<UserRepository, User,Long> {
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package fr.istic.tlc.services;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import fr.istic.tlc.dao.PollRepository;
|
||||
import fr.istic.tlc.domain.Poll;
|
||||
import fr.istic.tlc.domain.User;
|
||||
import io.quarkus.mailer.Mail;
|
||||
import io.quarkus.mailer.Mailer;
|
||||
import net.fortuna.ical4j.model.DateTime;
|
||||
import net.fortuna.ical4j.model.TimeZone;
|
||||
import net.fortuna.ical4j.model.TimeZoneRegistry;
|
||||
import net.fortuna.ical4j.model.TimeZoneRegistryFactory;
|
||||
import net.fortuna.ical4j.model.component.VEvent;
|
||||
import net.fortuna.ical4j.model.component.VTimeZone;
|
||||
import net.fortuna.ical4j.model.parameter.Role;
|
||||
import net.fortuna.ical4j.model.property.Attendee;
|
||||
import net.fortuna.ical4j.model.property.CalScale;
|
||||
import net.fortuna.ical4j.model.property.Method;
|
||||
import net.fortuna.ical4j.model.property.Organizer;
|
||||
import net.fortuna.ical4j.model.property.ProdId;
|
||||
import net.fortuna.ical4j.model.property.Uid;
|
||||
import net.fortuna.ical4j.model.property.Version;
|
||||
import net.fortuna.ical4j.util.MapTimeZoneCache;
|
||||
import net.fortuna.ical4j.util.RandomUidGenerator;
|
||||
import net.fortuna.ical4j.util.UidGenerator;
|
||||
|
||||
@ApplicationScoped
|
||||
public class SendEmail {
|
||||
|
||||
@Inject
|
||||
Mailer mailer;
|
||||
|
||||
@Inject
|
||||
PollRepository pollRep;
|
||||
|
||||
@ConfigProperty(name = "doodle.organizermail")
|
||||
String organizermail= "test@test.fr";
|
||||
public void sendASimpleEmail(Poll p ) {
|
||||
// Create a default MimeMessage object.
|
||||
System.setProperty("net.fortuna.ical4j.timezone.cache.impl", MapTimeZoneCache.class.getName());
|
||||
|
||||
List<User> u = this.pollRep.findAllUser4Poll(p.getId());
|
||||
List<String> attendees = new ArrayList<String>();
|
||||
for (User u1 : u) {
|
||||
attendees.add(u1.getMail());
|
||||
}
|
||||
|
||||
String ics = this.getICS1(p.getSelectedChoice().getstartDate(), p.getSelectedChoice().getendDate(), p.getTitle(), attendees, organizermail);
|
||||
Mail m = new Mail();
|
||||
m.addAttachment("meeting.ics", ics.getBytes(), "text/calendar");
|
||||
|
||||
m.setFrom(organizermail);
|
||||
m.setTo(attendees);
|
||||
m.setCc(Arrays.asList(organizermail));
|
||||
m.setFrom(organizermail);
|
||||
m.setSubject("Réunion c" + p.getTitle() + " [créneau confirmé] ");
|
||||
m.setHtml("La date définitive pour la réunion : \""+ p.getTitle() + "\" a été validée par l\'organisateur. <BR>" +
|
||||
"Un salon a été créé de discussion pour cette réunion est accessible à cette adresse <a [href]=\" " +p.getTlkURL() + "\" target=\"_blank\">" + p.getTlkURL() + "</a>.<BR>\n" +
|
||||
"Un pad a été créé pour cette réunion <a [href]=\""+ p.getPadURL() + "\" target=\"_blank\">\""+ p.getPadURL() + "\"</a>.</span><BR>\n");
|
||||
|
||||
mailer.send(m);
|
||||
|
||||
}
|
||||
|
||||
|
||||
public String getICS1(Date start, Date end, String libelle, List<String> attendees, String organizer) {
|
||||
|
||||
// Create a TimeZone
|
||||
TimeZoneRegistry registry = TimeZoneRegistryFactory.getInstance().createRegistry();
|
||||
TimeZone timezone = registry.getTimeZone("Europe/Paris");
|
||||
VTimeZone tz = timezone.getVTimeZone();
|
||||
|
||||
// Create the event
|
||||
DateTime startd = new DateTime(start);
|
||||
DateTime endd = new DateTime(end);
|
||||
VEvent meeting = new VEvent(startd, endd, libelle);
|
||||
// add timezone info..
|
||||
meeting.getProperties().add(tz.getTimeZoneId());
|
||||
|
||||
// generate unique identifier..
|
||||
UidGenerator ug = new RandomUidGenerator();
|
||||
Uid uid = ug.generateUid();
|
||||
meeting.getProperties().add(uid);
|
||||
|
||||
|
||||
// add attendees..
|
||||
for (String attendee : attendees) {
|
||||
Attendee p1 = new Attendee(URI.create("mailto:"+attendee));
|
||||
p1.getParameters().add(Role.REQ_PARTICIPANT);
|
||||
// dev1.getParameters().add(new Cn("Developer 1"));
|
||||
meeting.getProperties().add(p1);
|
||||
}
|
||||
Organizer p1 = new Organizer(URI.create("mailto:"+organizer));
|
||||
meeting.getProperties().add(p1);
|
||||
|
||||
|
||||
// Create a calendar
|
||||
net.fortuna.ical4j.model.Calendar icsCalendar = new net.fortuna.ical4j.model.Calendar();
|
||||
icsCalendar.getProperties().add(Version.VERSION_2_0);
|
||||
icsCalendar.getProperties().add(new ProdId("Zimbra-Calendar-Provider"));
|
||||
icsCalendar.getProperties().add(CalScale.GREGORIAN);
|
||||
icsCalendar.getProperties().add(Method.REQUEST);
|
||||
icsCalendar.getComponents().add(tz);
|
||||
|
||||
// Add the event and print
|
||||
icsCalendar.getComponents().add(meeting);
|
||||
|
||||
return icsCalendar.toString();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package fr.istic.tlc.services;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Random;
|
||||
|
||||
public class Utils {
|
||||
private Random random = null;// = new Random();
|
||||
private static final String CHARS = "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ234567890";
|
||||
private static Utils instance = null;
|
||||
private Utils(){
|
||||
this.random = new Random();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static Utils getInstance(){
|
||||
if (instance == null)
|
||||
instance = new Utils();
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
public String generateSlug(int length) {
|
||||
if (random == null){
|
||||
random = new Random();
|
||||
}
|
||||
StringBuilder slug = new StringBuilder(length);
|
||||
for (int i = 0; i < length; i++) {
|
||||
slug.append(CHARS.charAt(random.nextInt(CHARS.length())));
|
||||
}
|
||||
return slug.toString();
|
||||
}
|
||||
|
||||
public boolean intersect(Date start1, Date end1, Date start2, Date end2) {
|
||||
if (start1 == null || start2 == null ||end1 == null||end2 == null)
|
||||
return false;
|
||||
return end1.after(start2) && start1.before(end2);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,34 @@
|
||||
doodle:
|
||||
usepad: true
|
||||
internalPadUrl: "http://localhost:9001/"
|
||||
externalPadUrl: "http://localhost:9001/"
|
||||
padApiKey: "19d89ca52bc0fa4f19d6325464d9d7a032649b9fa68c111514627081e2784b4a"
|
||||
organizermail: "olivier.barais@gmail.com"
|
||||
tmpfolder: "/tmp/excelFiles"
|
||||
quarkus:
|
||||
datasource:
|
||||
db-kind: mysql
|
||||
username: tlc
|
||||
password: tlc
|
||||
jdbc:
|
||||
url: jdbc:mysql://localhost:3306/tlc?useUnicode=true&serverTimezone=Europe/Paris
|
||||
driver: com.mysql.cj.jdbc.Driver
|
||||
hibernate-orm:
|
||||
validate-in-dev-mode: false
|
||||
# flyway:
|
||||
# migrate-at-start: true
|
||||
# baseline-on-migrate: true
|
||||
database:
|
||||
generation: update
|
||||
globally-quoted-identifiers: true
|
||||
mailer:
|
||||
from: test@quarkus.io
|
||||
# host: smtp.sendgrid.net
|
||||
host: localhost
|
||||
port: 2525
|
||||
# port: 465
|
||||
# ssl: true
|
||||
|
||||
# username: ""
|
||||
# password: ""
|
||||
mock: false
|
||||
@@ -0,0 +1,294 @@
|
||||
-- phpMyAdmin SQL Dump
|
||||
-- version 5.1.1deb5ubuntu1
|
||||
-- https://www.phpmyadmin.net/
|
||||
--
|
||||
-- Hôte : localhost:3306
|
||||
-- Généré le : mar. 22 août 2023 à 10:08
|
||||
-- Version du serveur : 8.0.33-0ubuntu0.22.04.4
|
||||
-- Version de PHP : 8.1.2-1ubuntu2.13
|
||||
|
||||
|
||||
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||
SET AUTOCOMMIT = 0;
|
||||
START TRANSACTION;
|
||||
SET time_zone = "+00:00";
|
||||
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
|
||||
--
|
||||
-- Base de données : `tlc`
|
||||
--
|
||||
CREATE DATABASE IF NOT EXISTS `tlc` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||||
USE `tlc`;
|
||||
|
||||
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
|
||||
--
|
||||
-- Base de données : `tlc`
|
||||
--
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Structure de la table `Choice`
|
||||
--
|
||||
|
||||
CREATE TABLE `Choice` (
|
||||
`id` bigint NOT NULL,
|
||||
`endDate` datetime(6) DEFAULT NULL,
|
||||
`startDate` datetime(6) DEFAULT NULL,
|
||||
`pollID` bigint DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Structure de la table `Choice_SEQ`
|
||||
--
|
||||
|
||||
CREATE TABLE `Choice_SEQ` (
|
||||
`next_val` bigint DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
--
|
||||
-- Déchargement des données de la table `Choice_SEQ`
|
||||
--
|
||||
|
||||
INSERT INTO `Choice_SEQ` (`next_val`) VALUES
|
||||
(1);
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Structure de la table `choice_user`
|
||||
--
|
||||
|
||||
CREATE TABLE `choice_user` (
|
||||
`choice_id` bigint NOT NULL,
|
||||
`user_id` bigint NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Structure de la table `Comment`
|
||||
--
|
||||
|
||||
CREATE TABLE `Comment` (
|
||||
`id` bigint NOT NULL,
|
||||
`auteur` varchar(255) DEFAULT NULL,
|
||||
`content` varchar(255) DEFAULT NULL,
|
||||
`pollID` bigint DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Structure de la table `Comment_SEQ`
|
||||
--
|
||||
|
||||
CREATE TABLE `Comment_SEQ` (
|
||||
`next_val` bigint DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
--
|
||||
-- Déchargement des données de la table `Comment_SEQ`
|
||||
--
|
||||
|
||||
INSERT INTO `Comment_SEQ` (`next_val`) VALUES
|
||||
(1);
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Structure de la table `MealPreference`
|
||||
--
|
||||
|
||||
CREATE TABLE `MealPreference` (
|
||||
`id` bigint NOT NULL,
|
||||
`content` varchar(255) DEFAULT NULL,
|
||||
`user_id` bigint DEFAULT NULL,
|
||||
`pollID` bigint DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Structure de la table `MealPreference_SEQ`
|
||||
--
|
||||
|
||||
CREATE TABLE `MealPreference_SEQ` (
|
||||
`next_val` bigint DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
--
|
||||
-- Déchargement des données de la table `MealPreference_SEQ`
|
||||
--
|
||||
|
||||
INSERT INTO `MealPreference_SEQ` (`next_val`) VALUES
|
||||
(1);
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Structure de la table `Poll`
|
||||
--
|
||||
|
||||
CREATE TABLE `Poll` (
|
||||
`id` bigint NOT NULL,
|
||||
`clos` bit(1) NOT NULL,
|
||||
`createdAt` datetime(6) DEFAULT NULL,
|
||||
`description` varchar(255) DEFAULT NULL,
|
||||
`has_meal` bit(1) NOT NULL,
|
||||
`location` varchar(255) DEFAULT NULL,
|
||||
`padURL` varchar(255) DEFAULT NULL,
|
||||
`slug` varchar(255) DEFAULT NULL,
|
||||
`slugAdmin` varchar(255) DEFAULT NULL,
|
||||
`title` varchar(255) DEFAULT NULL,
|
||||
`tlkURL` varchar(255) DEFAULT NULL,
|
||||
`updatedAt` datetime(6) DEFAULT NULL,
|
||||
`selectedChoice_id` bigint DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Structure de la table `Poll_SEQ`
|
||||
--
|
||||
|
||||
CREATE TABLE `Poll_SEQ` (
|
||||
`next_val` bigint DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
--
|
||||
-- Déchargement des données de la table `Poll_SEQ`
|
||||
--
|
||||
|
||||
INSERT INTO `Poll_SEQ` (`next_val`) VALUES
|
||||
(1);
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Structure de la table `User`
|
||||
--
|
||||
|
||||
CREATE TABLE `User` (
|
||||
`id` bigint NOT NULL,
|
||||
`icsurl` varchar(255) DEFAULT NULL,
|
||||
`mail` varchar(255) DEFAULT NULL,
|
||||
`username` varchar(255) DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Structure de la table `User_SEQ`
|
||||
--
|
||||
|
||||
CREATE TABLE `User_SEQ` (
|
||||
`next_val` bigint DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
--
|
||||
-- Déchargement des données de la table `User_SEQ`
|
||||
--
|
||||
|
||||
INSERT INTO `User_SEQ` (`next_val`) VALUES
|
||||
(1);
|
||||
|
||||
--
|
||||
-- Index pour les tables déchargées
|
||||
--
|
||||
|
||||
--
|
||||
-- Index pour la table `Choice`
|
||||
--
|
||||
ALTER TABLE `Choice`
|
||||
ADD PRIMARY KEY (`id`),
|
||||
ADD KEY `FKpptbydus718x0n5w5s1hmtvnp` (`pollID`);
|
||||
|
||||
--
|
||||
-- Index pour la table `choice_user`
|
||||
--
|
||||
ALTER TABLE `choice_user`
|
||||
ADD KEY `FK74lqrm3h9f56d6ijnvjobl0wb` (`user_id`),
|
||||
ADD KEY `FKljka9n83yo9s4qpol3wplp1lw` (`choice_id`);
|
||||
|
||||
--
|
||||
-- Index pour la table `Comment`
|
||||
--
|
||||
ALTER TABLE `Comment`
|
||||
ADD PRIMARY KEY (`id`),
|
||||
ADD KEY `FKgw1unu5kgu9s7sdkqaoy0kyyh` (`pollID`);
|
||||
|
||||
--
|
||||
-- Index pour la table `MealPreference`
|
||||
--
|
||||
ALTER TABLE `MealPreference`
|
||||
ADD PRIMARY KEY (`id`),
|
||||
ADD KEY `FK61nykkil19yk0on84o44ykk3p` (`user_id`),
|
||||
ADD KEY `FK9pk3lx8mh8478nxj8lvxvaox0` (`pollID`);
|
||||
|
||||
--
|
||||
-- Index pour la table `Poll`
|
||||
--
|
||||
ALTER TABLE `Poll`
|
||||
ADD PRIMARY KEY (`id`),
|
||||
ADD UNIQUE KEY `UK_n779urxmh62kwbspgd6gp8564` (`selectedChoice_id`);
|
||||
|
||||
--
|
||||
-- Index pour la table `User`
|
||||
--
|
||||
ALTER TABLE `User`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Contraintes pour les tables déchargées
|
||||
--
|
||||
|
||||
--
|
||||
-- Contraintes pour la table `Choice`
|
||||
--
|
||||
ALTER TABLE `Choice`
|
||||
ADD CONSTRAINT `FKpptbydus718x0n5w5s1hmtvnp` FOREIGN KEY (`pollID`) REFERENCES `Poll` (`id`);
|
||||
|
||||
--
|
||||
-- Contraintes pour la table `choice_user`
|
||||
--
|
||||
ALTER TABLE `choice_user`
|
||||
ADD CONSTRAINT `FK74lqrm3h9f56d6ijnvjobl0wb` FOREIGN KEY (`user_id`) REFERENCES `User` (`id`),
|
||||
ADD CONSTRAINT `FKljka9n83yo9s4qpol3wplp1lw` FOREIGN KEY (`choice_id`) REFERENCES `Choice` (`id`);
|
||||
|
||||
--
|
||||
-- Contraintes pour la table `Comment`
|
||||
--
|
||||
ALTER TABLE `Comment`
|
||||
ADD CONSTRAINT `FKgw1unu5kgu9s7sdkqaoy0kyyh` FOREIGN KEY (`pollID`) REFERENCES `Poll` (`id`);
|
||||
|
||||
--
|
||||
-- Contraintes pour la table `MealPreference`
|
||||
--
|
||||
ALTER TABLE `MealPreference`
|
||||
ADD CONSTRAINT `FK61nykkil19yk0on84o44ykk3p` FOREIGN KEY (`user_id`) REFERENCES `User` (`id`),
|
||||
ADD CONSTRAINT `FK9pk3lx8mh8478nxj8lvxvaox0` FOREIGN KEY (`pollID`) REFERENCES `Poll` (`id`);
|
||||
|
||||
--
|
||||
-- Contraintes pour la table `Poll`
|
||||
--
|
||||
ALTER TABLE `Poll`
|
||||
ADD CONSTRAINT `FKfdictafwo8dwab5rjrjkmmxri` FOREIGN KEY (`selectedChoice_id`) REFERENCES `Choice` (`id`);
|
||||
COMMIT;
|
||||
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
@@ -0,0 +1,39 @@
|
||||
#
|
||||
# Licensed to Apereo under one or more contributor license
|
||||
# agreements. See the NOTICE file distributed with this work
|
||||
# for additional information regarding copyright ownership.
|
||||
# Apereo licenses this file to you under the Apache License,
|
||||
# Version 2.0 (the "License"); you may not use this file
|
||||
# except in compliance with the License. You may obtain a
|
||||
# copy of the License at the following location:
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
# Known optional properties for ical4j:
|
||||
#net.fortuna.ical4j.parser=net.fortuna.ical4j.data.HCalendarParserFactory
|
||||
#net.fortuna.ical4j.timezone.registry=net.fortuna.ical4j.model.DefaultTimeZoneRegistryFactory
|
||||
#net.fortuna.ical4j.timezone.update.enabled={true|false}
|
||||
#net.fortuna.ical4j.timezone.date.floating={true|false}
|
||||
#net.fortuna.ical4j.factory.decoder=net.fortuna.ical4j.util.DefaultDecoderFactory
|
||||
#net.fortuna.ical4j.factory.encoder=net.fortuna.ical4j.util.DefaultEncoderFactory
|
||||
#net.fortuna.ical4j.recur.maxincrementcount=1000
|
||||
#ical4j.unfolding.relaxed={true|false}
|
||||
#ical4j.parsing.relaxed={true|false}
|
||||
#ical4j.validation.relaxed={true|false}
|
||||
#ical4j.compatibility.outlook={true|false}
|
||||
#ical4j.compatibility.notes={true|false}
|
||||
|
||||
# Values...
|
||||
ical4j.unfolding.relaxed=true
|
||||
ical4j.compatibility.outlook=true
|
||||
ical4j.compatibility.notes=true
|
||||
ical4j.parsing.relaxed=true
|
||||
ical4j.validation.relaxed=true
|
||||
@@ -0,0 +1,21 @@
|
||||
[
|
||||
{
|
||||
"name" : "net.fortuna.ical4j.util.JCacheTimeZoneCache",
|
||||
"allDeclaredConstructors" : true,
|
||||
"allPublicConstructors" : true,
|
||||
"allDeclaredMethods" : true,
|
||||
"allPublicMethods" : true,
|
||||
"allDeclaredFields" : true,
|
||||
"allPublicFields" : true
|
||||
},
|
||||
{
|
||||
"name" : "net.fortuna.ical4j.util.MapTimeZoneCache",
|
||||
"allDeclaredConstructors" : true,
|
||||
"allPublicConstructors" : true,
|
||||
"allDeclaredMethods" : true,
|
||||
"allPublicMethods" : true,
|
||||
"allDeclaredFields" : true,
|
||||
"allPublicFields" : true
|
||||
}
|
||||
|
||||
]
|
||||
18
ansible/files/doodlestudent/front/.browserslistrc
Normal file
18
ansible/files/doodlestudent/front/.browserslistrc
Normal file
@@ -0,0 +1,18 @@
|
||||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# For the full list of supported browsers by the Angular framework, please see:
|
||||
# https://angular.io/guide/browser-support
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
last 1 Chrome version
|
||||
last 1 Firefox version
|
||||
last 2 Edge major versions
|
||||
last 2 Safari major versions
|
||||
last 2 iOS major versions
|
||||
Firefox ESR
|
||||
not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line.
|
||||
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
|
||||
16
ansible/files/doodlestudent/front/.editorconfig
Normal file
16
ansible/files/doodlestudent/front/.editorconfig
Normal file
@@ -0,0 +1,16 @@
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
46
ansible/files/doodlestudent/front/.gitignore
vendored
Normal file
46
ansible/files/doodlestudent/front/.gitignore
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
# Only exists if Bazel was run
|
||||
/bazel-out
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# profiling files
|
||||
chrome-profiler-events*.json
|
||||
speed-measure-plugin*.json
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
27
ansible/files/doodlestudent/front/README.md
Normal file
27
ansible/files/doodlestudent/front/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Tlcfront
|
||||
|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 10.1.7.
|
||||
|
||||
## Development server
|
||||
|
||||
Run `npm start` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||
|
||||
## Build
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
|
||||
## Further help
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
|
||||
29
ansible/files/doodlestudent/front/TODO.md
Normal file
29
ansible/files/doodlestudent/front/TODO.md
Normal file
@@ -0,0 +1,29 @@
|
||||
## TODO
|
||||
|
||||
[X] nombre de participants dans la vue edit
|
||||
[X] Vue Admin
|
||||
[X] Selection date retenue
|
||||
[X] Sondage clos dans la vue edit et admin
|
||||
[X] Modification par admin d'un doodle en cours
|
||||
[X] Chargement ics externe dans la vue edit calendrier
|
||||
[X] Import ICS coté serveur et complétion automatique des dispos
|
||||
[X] Test Chargement ics externe dans la vue edit calendrier
|
||||
[X] ICS avec évènement à répétition
|
||||
[X] Date sur plusieurs jours
|
||||
[X] Date journée entière
|
||||
[X] Menu dans la vue admin pour edit, export
|
||||
[X] Réintégration etherpad avec prop dans le fichier de conf
|
||||
[X] Réintégration export Excel
|
||||
[X] Affichage TalTo et Pad si not null
|
||||
[X] Affichage commentaire
|
||||
[X] envoie de mail avec ics
|
||||
[X] Test envoi de mail avec ics à la cloture du poll
|
||||
[X] Figé le poll séléctionné quand poll séléction date poll validé
|
||||
[X] Chargement ICS dans la vue createPoll
|
||||
[ ] Test etherpad client
|
||||
|
||||
[ ] Modification par un utilisateur des données déjà rentrées pour un poll
|
||||
[ ] Gestion du choix peut être (relation suppléméntaire entre choice et user)
|
||||
[ ] DSL pour la sélection automatique des plages libres à la création d'un poll
|
||||
[ ] Vue mensuel
|
||||
[ ] Sécurité SlugAdmin envoyé uniquement
|
||||
131
ansible/files/doodlestudent/front/angular.json
Normal file
131
ansible/files/doodlestudent/front/angular.json
Normal file
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"tlcfront": {
|
||||
"projectType": "application",
|
||||
"schematics": {},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/tlcfront",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"aot": true,
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"./node_modules/bootstrap/dist/css/bootstrap.css",
|
||||
"node_modules/primeng/resources/themes/bootstrap4-light-blue/theme.css",
|
||||
"node_modules/primeicons/primeicons.css",
|
||||
"node_modules/primeng/resources/primeng.min.css",
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": [
|
||||
"./node_modules/jquery/dist/jquery.js",
|
||||
"./node_modules/bootstrap/dist/js/bootstrap.js"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "5mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb",
|
||||
"maximumError": "10kb"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "tlcfront:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "tlcfront:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "tlcfront:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.spec.json",
|
||||
"e2e/tsconfig.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
},
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "e2e/protractor.conf.js",
|
||||
"devServerTarget": "tlcfront:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "tlcfront:serve:production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}},
|
||||
"defaultProject": "tlcfront"
|
||||
}
|
||||
36
ansible/files/doodlestudent/front/e2e/protractor.conf.js
Normal file
36
ansible/files/doodlestudent/front/e2e/protractor.conf.js
Normal file
@@ -0,0 +1,36 @@
|
||||
// @ts-check
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
|
||||
|
||||
/**
|
||||
* @type { import("protractor").Config }
|
||||
*/
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./src/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
browserName: 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: require('path').join(__dirname, './tsconfig.json')
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({
|
||||
spec: {
|
||||
displayStacktrace: StacktraceOption.PRETTY
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
23
ansible/files/doodlestudent/front/e2e/src/app.e2e-spec.ts
Normal file
23
ansible/files/doodlestudent/front/e2e/src/app.e2e-spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { AppPage } from './app.po';
|
||||
import { browser, logging } from 'protractor';
|
||||
|
||||
describe('workspace-project App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', () => {
|
||||
page.navigateTo();
|
||||
expect(page.getTitleText()).toEqual('tlcfront app is running!');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Assert that there are no errors emitted from the browser
|
||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||
expect(logs).not.toContain(jasmine.objectContaining({
|
||||
level: logging.Level.SEVERE,
|
||||
} as logging.Entry));
|
||||
});
|
||||
});
|
||||
11
ansible/files/doodlestudent/front/e2e/src/app.po.ts
Normal file
11
ansible/files/doodlestudent/front/e2e/src/app.po.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
navigateTo(): Promise<unknown> {
|
||||
return browser.get(browser.baseUrl) as Promise<unknown>;
|
||||
}
|
||||
|
||||
getTitleText(): Promise<string> {
|
||||
return element(by.css('app-root .content span')).getText() as Promise<string>;
|
||||
}
|
||||
}
|
||||
14
ansible/files/doodlestudent/front/e2e/tsconfig.json
Normal file
14
ansible/files/doodlestudent/front/e2e/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"module": "commonjs",
|
||||
"target": "es2018",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"jasminewd2",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
||||
32
ansible/files/doodlestudent/front/karma.conf.js
Normal file
32
ansible/files/doodlestudent/front/karma.conf.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage-istanbul-reporter'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
client: {
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
coverageIstanbulReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/tlcfront'),
|
||||
reports: ['html', 'lcovonly', 'text-summary'],
|
||||
fixWebpackSourcePaths: true
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false,
|
||||
restartOnFileChange: true
|
||||
});
|
||||
};
|
||||
13960
ansible/files/doodlestudent/front/package-lock.json
generated
Normal file
13960
ansible/files/doodlestudent/front/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
57
ansible/files/doodlestudent/front/package.json
Normal file
57
ansible/files/doodlestudent/front/package.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "tlcfront",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve --proxy-config proxy.conf.json",
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "16.2.1",
|
||||
"@angular/common": "16.2.1",
|
||||
"@angular/compiler": "16.2.1",
|
||||
"@angular/core": "16.2.1",
|
||||
"@angular/forms": "16.2.1",
|
||||
"@angular/localize": "16.2.1",
|
||||
"@angular/platform-browser": "16.2.1",
|
||||
"@angular/platform-browser-dynamic": "16.2.1",
|
||||
"@angular/router": "16.2.1",
|
||||
"@fullcalendar/angular": "6.1.8",
|
||||
"@fullcalendar/core": "6.1.8",
|
||||
"@fullcalendar/daygrid": "6.1.8",
|
||||
"@fullcalendar/interaction": "6.1.8",
|
||||
"@fullcalendar/timegrid": "6.1.8",
|
||||
"@ng-bootstrap/ng-bootstrap": "15.1.1",
|
||||
"bootstrap": "5.3.1",
|
||||
"jquery": "3.7.0",
|
||||
"primeicons": "6.0.1",
|
||||
"primeng": "16.2.0",
|
||||
"rxjs": "7.8.1",
|
||||
"tslib": "2.6.2",
|
||||
"zone.js": "0.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "16.2.0",
|
||||
"@angular/cli": "16.2.0",
|
||||
"@angular/compiler-cli": "16.2.1",
|
||||
"@types/jasmine": "4.3.5",
|
||||
"@types/jasminewd2": "2.0.10",
|
||||
"@types/node": "20.5.1",
|
||||
"codelyzer": "6.0.2",
|
||||
"jasmine-core": "5.1.0",
|
||||
"jasmine-spec-reporter": "7.0.0",
|
||||
"karma": "6.4.2",
|
||||
"karma-chrome-launcher": "3.2.0",
|
||||
"karma-coverage-istanbul-reporter": "3.0.3",
|
||||
"karma-jasmine": "5.1.0",
|
||||
"karma-jasmine-html-reporter": "2.1.0",
|
||||
"protractor": "7.0.0",
|
||||
"ts-node": "10.9.1",
|
||||
"tslint": "6.1.0",
|
||||
"typescript": "5.1.6"
|
||||
}
|
||||
}
|
||||
8
ansible/files/doodlestudent/front/proxy.conf.json
Normal file
8
ansible/files/doodlestudent/front/proxy.conf.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"/api/*": {
|
||||
"target": "http://localhost:8080",
|
||||
"secure": false,
|
||||
"logLevel": "debug",
|
||||
"changeOrigin": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
.Poll_Informations {
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.10);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.Meal_Preferences {
|
||||
padding: 1rem;
|
||||
border-bottom: 2px solid #F0F4F8;
|
||||
}
|
||||
|
||||
.Poll_Has_Meal {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.Poll_Has_Meal {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.Poll_Description_Title {
|
||||
font-size: 0.8rem;
|
||||
color: #243B53;
|
||||
}
|
||||
|
||||
.Poll_Infos {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.Poll_Info {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<div class="Container">
|
||||
|
||||
|
||||
<img src="../../assets/flat_logo.png" alt="Logo Simba" height="50px" [ngStyle]="{ 'marginBottom': '1rem' }" />
|
||||
|
||||
<!-- { isModalOpened &&
|
||||
<div className="modal" onClick={() => setIsModalOpened(false)}>
|
||||
<div className="Export_Modal" >
|
||||
<a className="Export Disabled" target="_blank" rel="noopener noreferrer">
|
||||
<svg aria-hidden="true" width="40px" height="40px" focusable="false" data-prefix="fas" data-icon="file-pdf" className="Export_Icon" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M181.9 256.1c-5-16-4.9-46.9-2-46.9 8.4 0 7.6 36.9 2 46.9zm-1.7 47.2c-7.7 20.2-17.3 43.3-28.4 62.7 18.3-7 39-17.2 62.9-21.9-12.7-9.6-24.9-23.4-34.5-40.8zM86.1 428.1c0 .8 13.2-5.4 34.9-40.2-6.7 6.3-29.1 24.5-34.9 40.2zM248 160h136v328c0 13.3-10.7 24-24 24H24c-13.3 0-24-10.7-24-24V24C0 10.7 10.7 0 24 0h200v136c0 13.2 10.8 24 24 24zm-8 171.8c-20-12.2-33.3-29-42.7-53.8 4.5-18.5 11.6-46.6 6.2-64.2-4.7-29.4-42.4-26.5-47.8-6.8-5 18.3-.4 44.1 8.1 77-11.6 27.6-28.7 64.6-40.8 85.8-.1 0-.1.1-.2.1-27.1 13.9-73.6 44.5-54.5 68 5.6 6.9 16 10 21.5 10 17.9 0 35.7-18 61.1-61.8 25.8-8.5 54.1-19.1 79-23.2 21.7 11.8 47.1 19.5 64 19.5 29.2 0 31.2-32 19.7-43.4-13.9-13.6-54.3-9.7-73.6-7.2zM377 105L279 7c-4.5-4.5-10.6-7-17-7h-6v128h128v-6.1c0-6.3-2.5-12.4-7-16.9zm-74.1 255.3c4.1-2.7-2.5-11.9-42.8-9 37.1 15.8 42.8 9 42.8 9z"></path></svg>
|
||||
<span>PDF (Premium)</span>
|
||||
</a>
|
||||
<a className="Export" target="_blank" rel="noopener noreferrer" href={`${BASE_URL}/polls/${slug}/results`}>
|
||||
<svg aria-hidden="true" width="40px" height="40px" focusable="false" data-prefix="fas" data-icon="file-excel" className="Export_Icon" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M224 136V0H24C10.7 0 0 10.7 0 24v464c0 13.3 10.7 24 24 24h336c13.3 0 24-10.7 24-24V160H248c-13.2 0-24-10.8-24-24zm60.1 106.5L224 336l60.1 93.5c5.1 8-.6 18.5-10.1 18.5h-34.9c-4.4 0-8.5-2.4-10.6-6.3C208.9 405.5 192 373 192 373c-6.4 14.8-10 20-36.6 68.8-2.1 3.9-6.1 6.3-10.5 6.3H110c-9.5 0-15.2-10.5-10.1-18.5l60.3-93.5-60.3-93.5c-5.2-8 .6-18.5 10.1-18.5h34.8c4.4 0 8.5 2.4 10.6 6.3 26.1 48.8 20 33.6 36.6 68.5 0 0 6.1-11.7 36.6-68.5 2.1-3.9 6.2-6.3 10.6-6.3H274c9.5-.1 15.2 10.4 10.1 18.4zM384 121.9v6.1H256V0h6.1c6.4 0 12.5 2.5 17 7l97.9 98c4.5 4.5 7 10.6 7 16.9z"></path></svg>
|
||||
<span>EXCEL</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}-->
|
||||
|
||||
<app-top-bar [adminSlug]="poll?.slugAdmin" [slug]="poll?.slug" [padURL]="poll?.padURL" [talkToURL]="poll?.tlkURL" ></app-top-bar>
|
||||
|
||||
<p-card>
|
||||
<p-toast></p-toast>
|
||||
<ng-template pTemplate="title">
|
||||
<h1>{{poll?.title}}</h1>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="subtitle">
|
||||
<div class="Dates"><span>Créé il y a {{poll?.createdAt | dateago}}</span></div>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="content">
|
||||
<div class="Poll_Infos">
|
||||
<p class="Poll_Location"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="feather feather-map-pin">
|
||||
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
|
||||
<circle cx="12" cy="10" r="3"></circle>
|
||||
</svg>{{poll?.location}}</p>
|
||||
<div *ngIf="poll?.has_meal" class="Poll_Has_Meal"><svg class="feather" aria-hidden="true" width="20" height="20"
|
||||
focusable="false" data-prefix="fas" data-icon="utensils" role="img" xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 416 512">
|
||||
<path fill="currentColor"
|
||||
d="M207.9 15.2c.8 4.7 16.1 94.5 16.1 128.8 0 52.3-27.8 89.6-68.9 104.6L168 486.7c.7 13.7-10.2 25.3-24 25.3H80c-13.7 0-24.7-11.5-24-25.3l12.9-238.1C27.7 233.6 0 196.2 0 144 0 109.6 15.3 19.9 16.1 15.2 19.3-5.1 61.4-5.4 64 16.3v141.2c1.3 3.4 15.1 3.2 16 0 1.4-25.3 7.9-139.2 8-141.8 3.3-20.8 44.7-20.8 47.9 0 .2 2.7 6.6 116.5 8 141.8.9 3.2 14.8 3.4 16 0V16.3c2.6-21.6 44.8-21.4 48-1.1zm119.2 285.7l-15 185.1c-1.2 14 9.9 26 23.9 26h56c13.3 0 24-10.7 24-24V24c0-13.2-10.7-24-24-24-82.5 0-221.4 178.5-64.9 300.9z">
|
||||
</path>
|
||||
</svg>
|
||||
Cet évènement contient un repas
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div >
|
||||
<div class="container-fluid">
|
||||
<div class="table-responsive-sm card">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2"></th>
|
||||
<th *ngFor="let ev of events" class="text-light" style="text-align: center;background-color: #545B62">{{ev.start | date:'EEEE d LLLL': 'CEST':'fr'}}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th *ngFor="let ev of events" style="text-align: center">{{ev.start | date:'H:mm'}} <BR>-<BR> {{ev.end | date:'H:mm'}}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span>{{uniqueUsers.length}} participant</span><span *ngIf="uniqueUsers.length > 1">s</span></th>
|
||||
<th *ngFor="let pc of poll?.pollChoices" style="text-align: center">{{pc.users.length}}</th>
|
||||
</tr>
|
||||
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let u of userChoices | keyvalue">
|
||||
<td><input type="text" [disabled]='true' pInputText [ngModel]="uniqueUsers | usernamePipe:u.key"></td>
|
||||
<td *ngFor="let ev of events" style="text-align: center"><p-checkbox [disabled]='true' [binary]="true" [ngModel]="u.value | selecteddate4userPipe:u.key:ev" ></p-checkbox></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td></td>
|
||||
<td *ngFor="let ev of events" style="text-align: center"><p-button [disabled]="poll?.clos" (onClick)="selectEvent($event,ev )" >sélectionner cette date</p-button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="footer">
|
||||
|
||||
<app-show-comments *ngIf="poll" [comments]="comments"></app-show-comments>
|
||||
|
||||
</ng-template>
|
||||
</p-card>
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminPollComponent } from './admin-poll.component';
|
||||
|
||||
describe('AdminPollComponent', () => {
|
||||
let component: AdminPollComponent;
|
||||
let fixture: ComponentFixture<AdminPollComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ AdminPollComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AdminPollComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { MessageService } from 'primeng/api';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { PollService } from '../poll-service.service';
|
||||
import { Poll, User, PollChoice, PollCommentElement, ChoiceUser } from '../model/model';
|
||||
import { CalendarOptions, EventInput } from '@fullcalendar/core';
|
||||
import { FullCalendarComponent } from '@fullcalendar/angular';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-poll',
|
||||
templateUrl: './admin-poll.component.html',
|
||||
styleUrls: ['./admin-poll.component.css'],
|
||||
providers: [MessageService, PollService, FullCalendarComponent]
|
||||
|
||||
})
|
||||
export class AdminPollComponent implements OnInit {
|
||||
|
||||
constructor(public messageService: MessageService, private actRoute: ActivatedRoute, private pollService: PollService) { }
|
||||
slugid: string;
|
||||
poll: Poll;
|
||||
events: EventInput[] = [];
|
||||
uniqueUsers: User[] = [];
|
||||
userChoices: Map<number, PollChoice[]> = new Map();
|
||||
comments: PollCommentElement[];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.actRoute.paramMap.subscribe(params => {
|
||||
this.slugid = params.get('slugadminid');
|
||||
this.pollService.getPollBySlugAdminId(this.slugid).subscribe(p => {
|
||||
this.poll = p;
|
||||
if (p != null){
|
||||
this.pollService.getComentsBySlugId(this.poll?.slug).subscribe(cs => this.comments = cs);
|
||||
}
|
||||
this.uniqueUsers.splice(0, this.uniqueUsers.length);
|
||||
this.poll.pollChoices.forEach(pc => {
|
||||
pc.users.forEach(user => {
|
||||
if (this.uniqueUsers.filter(us => us.id === user.id).length === 0 ){
|
||||
this.uniqueUsers.push(user);
|
||||
this.userChoices.set(user.id, []);
|
||||
}
|
||||
});
|
||||
|
||||
const evt =
|
||||
{
|
||||
title: '',
|
||||
start: pc.startDate,
|
||||
end: pc.endDate,
|
||||
resourceEditable: false,
|
||||
eventResizableFromStart: false,
|
||||
backgroundColor: 'red',
|
||||
extendedProps: {
|
||||
choiceid: pc.id,
|
||||
selected: false
|
||||
},
|
||||
};
|
||||
this.events.push(evt);
|
||||
});
|
||||
this.poll.pollChoices.forEach(pc => {
|
||||
pc.users.forEach(us => {
|
||||
this.userChoices.get(us.id).push(pc);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
selectEvent($event: any, event: EventInput): void{
|
||||
this.pollService.selectEvent(event.extendedProps.choiceid).subscribe(e => {
|
||||
this.messageService.add({
|
||||
severity: 'success',
|
||||
summary: 'Données enregistrées',
|
||||
detail: 'Le sondage est maintenant close'}
|
||||
);
|
||||
this.poll.clos = true;
|
||||
}, (error) => {
|
||||
this.messageService.add(
|
||||
{
|
||||
severity: 'warn',
|
||||
summary: 'Sélection de cette date impossible',
|
||||
detail: 'Le sondage n\'a pu être clos'}
|
||||
);
|
||||
});
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
:root {
|
||||
--header-height : 180px;
|
||||
--participant-width : 230px;
|
||||
--cell-width : 65px;
|
||||
--cell-height: 40px;
|
||||
--new-participant-height : 65px;
|
||||
--cell-padding : 1rem;
|
||||
|
||||
--color-new-participant : #E6E6FF;
|
||||
--color-vote-yes : #E6E6FF;
|
||||
--color-vote-no :rgb(254,246,246) ;
|
||||
}
|
||||
|
||||
ul{
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
.Poll_Has_Meal {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.Poll_Vote_Wrapper{
|
||||
display: flex;
|
||||
justify-content:center;
|
||||
}
|
||||
|
||||
.Poll_Vote_Content{
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
/*border : 2px solid black; */
|
||||
max-width: 100%;
|
||||
max-height: 390px;
|
||||
overflow-y: scroll;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.Cell_Poll_Header{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.Cell_Option .Cell_Poll_Header, .Cell_Option_Votes{
|
||||
border-left: 1px solid black;
|
||||
}
|
||||
|
||||
|
||||
.Cell_Options{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.Cell_Option{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/*Tailles de cellules*/
|
||||
.Cell_Participants_Header,.Cell_Participant_Count,
|
||||
.Cell_New_Participant,.Cell_Participant{
|
||||
width: var(--participant-width);
|
||||
}
|
||||
.Cell_Option_Name,.Cell_Option_Count,
|
||||
.Cell_Option_New_Participant_Vote,.Cell_Option_Vote_Yes,
|
||||
.Cell_Option_Vote_No{
|
||||
width: var(--cell-width);
|
||||
}
|
||||
/*.Cell_New_Participant,.Cell_Option_New_Participant_Vote{
|
||||
height: var(--new-participant-height);
|
||||
}*/
|
||||
|
||||
.Cell_Option_New_Participant_Vote {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.Checkbox_Btn.LastCheck {
|
||||
border-radius: 0 0 5px 0;
|
||||
}
|
||||
|
||||
|
||||
.Cell_Participants_Header,.Cell_Option_Name, .Cell_Header_Name{
|
||||
height: var(--header-height);
|
||||
}
|
||||
/*.Cell_Participant_Count,.Cell_Option_Count,
|
||||
.Cell_Option_Vote_Yes,.Cell_Option_Vote_No,.Cell_Participant{
|
||||
height: var(--cell-height);
|
||||
}*/
|
||||
|
||||
.Cell_Participant_Count {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
|
||||
/* Disposition dans les cellules */
|
||||
.Cell_Participants_Header,.Cell_Participant_Count,
|
||||
.Cell_Option_Count,
|
||||
.Cell_Option_Vote_Yes,.Cell_Option_Vote_No{
|
||||
padding: var(--cell-padding);
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
/*border: 1px solid black;*/
|
||||
}
|
||||
|
||||
.Poll_Vote_Content {
|
||||
border: 1px solid #E6E6FF;
|
||||
}
|
||||
|
||||
/*.Cell_Participant,.Cell_New_Participant{
|
||||
padding: var(--cell-padding);
|
||||
padding-left: 20%;
|
||||
border: 1px solid black;
|
||||
text-align: left;
|
||||
}*/
|
||||
|
||||
.Cell_Option_Name, .Cell_Participants_Header {
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
|
||||
.Cell_Participant, .Cell_Vote {
|
||||
height: 50px;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
/*Couleurs*/
|
||||
.Cell_Option_New_Participant_Vote{
|
||||
background-color: var(--color-new-participant);
|
||||
}
|
||||
.Cell_Option_Vote_Yes{
|
||||
background-color: var(--color-vote-yes)
|
||||
}
|
||||
.Cell_Option_Vote_No{
|
||||
background-color: var(--color-vote-no)
|
||||
}
|
||||
/* Fixe la premiere colonne et parametrage du scroll*/
|
||||
|
||||
.Cell_Options{
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
|
||||
.Cell_New_Participant_Input {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
border-radius: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.Cell_New_Participant_Input.error {
|
||||
border: 2px solid #EF4E4E;
|
||||
}
|
||||
|
||||
.Poll_View_Btn {
|
||||
display: flex;
|
||||
flex: 1 1;
|
||||
border: 1px solid #1D0EBE;
|
||||
display: flex;
|
||||
color: #4D3DF7;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1.2rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.Poll_View_Btn:first-child {
|
||||
border-radius: 5px 0 0 5px;
|
||||
}
|
||||
|
||||
.Poll_View_Btn:last-child {
|
||||
border-radius: 0 5px 5px 0;
|
||||
}
|
||||
|
||||
.Poll_View_Btn.active {
|
||||
background-color: #4D3DF7;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.Poll_Btns {
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.Meal_Preferences_Toggle {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.Poll_Location {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.feather {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.Cell_Header {
|
||||
width: var(--participant-width);
|
||||
}
|
||||
|
||||
.Poll_Has_Meal {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.Poll_Description_Title {
|
||||
font-size: 0.8rem;
|
||||
color: #243B53;
|
||||
}
|
||||
|
||||
.Poll_Infos {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.Poll_Info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.green {
|
||||
background-color: #199473;
|
||||
}
|
||||
|
||||
.green:hover {
|
||||
background-color: #147D64;
|
||||
}
|
||||
|
||||
.Poll_Vote_Action {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.Cell_Poll_Header.Cell_Option_Name {
|
||||
width: 100%!important;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: #65D6AD;
|
||||
color: #014D40;
|
||||
}
|
||||
|
||||
.Poll_Subtitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
font-size: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.Dates {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 0.8rem;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.Pad_Url {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.Edit_Link {
|
||||
margin-left: 1rem;
|
||||
font-weight: 500;
|
||||
color: #4d3cf7;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.Edit_Link:hover {
|
||||
color: #1D0EBE;
|
||||
}
|
||||
|
||||
.Link {
|
||||
text-decoration: none;
|
||||
color: #4d3cf7;
|
||||
}
|
||||
|
||||
.Link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.Cell_Day {
|
||||
background-color: #4d3cf7;
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.Poll_Start_Date {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.Poll_Date {
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
color: #4d3cf7;
|
||||
}
|
||||
|
||||
.Checkbox_Btn {
|
||||
width: 65px;
|
||||
height: 50px;
|
||||
border: none;
|
||||
outline: none;
|
||||
text-align: center;
|
||||
transition: background-color 0.3s linear;
|
||||
border: 1px solid #4d3cf7;
|
||||
}
|
||||
|
||||
.Checkbox_Btn.Active {
|
||||
background-color: #4d3cf7;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.Cell_New_Participant {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
|
||||
.Links {
|
||||
display: flex;
|
||||
font-weight: 600;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
background-color: #4D3DF7;
|
||||
border-radius: 5px 5px 0 0;
|
||||
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.10);
|
||||
|
||||
}
|
||||
|
||||
.Links_Right {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.Links_Left {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.Feat_Link {
|
||||
padding: 0.7rem 1rem;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.3s linear;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Feat_Link:last-child {
|
||||
border-radius: 0 5px 0 0;
|
||||
}
|
||||
|
||||
.Feat_Link.Unique {
|
||||
border-radius: 5px 5px 0 0!important;
|
||||
}
|
||||
|
||||
.Feat_Link:hover {
|
||||
background-color: #0C008C;
|
||||
}
|
||||
|
||||
.MealPref {
|
||||
padding: 1rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.Author_MealPref {
|
||||
font-weight: bold;
|
||||
color: #4d3cf7;
|
||||
}
|
||||
|
||||
.Author_Comment {
|
||||
font-weight: bold;
|
||||
color: #4d3cf7;
|
||||
}
|
||||
|
||||
|
||||
.orange {
|
||||
background-color: #F7D070;
|
||||
}
|
||||
|
||||
.orange:hover {
|
||||
background-color: #E9B949;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed; /* Stay in place */
|
||||
z-index: 1; /* Sit on top */
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%; /* Full width */
|
||||
height: 100%; /* Full height */
|
||||
overflow: auto; /* Enable scroll if needed */
|
||||
background-color: rgb(0,0,0); /* Fallback color */
|
||||
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
|
||||
}
|
||||
|
||||
.Export_Modal {
|
||||
background-color: #fefefe;
|
||||
margin: 15% auto; /* 15% from the top and centered */
|
||||
width: 500px; /* Could be more or less, depending on screen size */
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 160px;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.10);
|
||||
}
|
||||
|
||||
.Export {
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
color: #4d3cf7;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
transition: background-color 0.3s linear;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.Export:hover {
|
||||
background-color: #4d3cf7;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.Export:first-child {
|
||||
border-right: 4px solid #4d3cf7;
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
|
||||
.Export:last-child {
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
|
||||
.Export_Icon {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.Poll_Informations {
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.10);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.Meal_Preferences {
|
||||
padding: 1rem;
|
||||
border-bottom: 2px solid #F0F4F8;
|
||||
}
|
||||
|
||||
.Disabled {
|
||||
background-color: #cccccc;
|
||||
}
|
||||
|
||||
.Disabled:hover {
|
||||
color: #4d3cf7;
|
||||
background-color: #cccccc;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.Comment {
|
||||
padding: 1rem;
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
<div class="Container">
|
||||
<img src="../../assets/flat_logo.png" alt="Logo Simba" height="50px" [ngStyle]="{ 'marginBottom': '1rem' }" />
|
||||
<app-top-bar [slug]="poll?.slug" [padURL]="poll?.padURL" [talkToURL]="poll?.tlkURL" ></app-top-bar>
|
||||
|
||||
|
||||
<p-card>
|
||||
<p-toast></p-toast>
|
||||
<ng-template pTemplate="title">
|
||||
<h1>{{poll?.title}}</h1>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="subtitle">
|
||||
<div class="Dates"><span>Créé il y a {{poll?.createdAt | dateago}}</span></div>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="content">
|
||||
<div class="Poll_Infos">
|
||||
<p class="Poll_Location"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="feather feather-map-pin">
|
||||
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
|
||||
<circle cx="12" cy="10" r="3"></circle>
|
||||
</svg>{{poll?.location}}</p>
|
||||
<div *ngIf="poll?.has_meal" class="Poll_Has_Meal"><svg class="feather" aria-hidden="true" width="20" height="20"
|
||||
focusable="false" data-prefix="fas" data-icon="utensils" role="img" xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 416 512">
|
||||
<path fill="currentColor"
|
||||
d="M207.9 15.2c.8 4.7 16.1 94.5 16.1 128.8 0 52.3-27.8 89.6-68.9 104.6L168 486.7c.7 13.7-10.2 25.3-24 25.3H80c-13.7 0-24.7-11.5-24-25.3l12.9-238.1C27.7 233.6 0 196.2 0 144 0 109.6 15.3 19.9 16.1 15.2 19.3-5.1 61.4-5.4 64 16.3v141.2c1.3 3.4 15.1 3.2 16 0 1.4-25.3 7.9-139.2 8-141.8 3.3-20.8 44.7-20.8 47.9 0 .2 2.7 6.6 116.5 8 141.8.9 3.2 14.8 3.4 16 0V16.3c2.6-21.6 44.8-21.4 48-1.1zm119.2 285.7l-15 185.1c-1.2 14 9.9 26 23.9 26h56c13.3 0 24-10.7 24-24V24c0-13.2-10.7-24-24-24-82.5 0-221.4 178.5-64.9 300.9z">
|
||||
</path>
|
||||
</svg>
|
||||
Cet évènement contient un repas
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="p-fluid">
|
||||
<div class="p-field">
|
||||
<label for="nom">Nom prénom participant</label>
|
||||
<input #nom="ngModel" id="nom" type="text" required pInputText [(ngModel)]="personalInformation.nom"
|
||||
[ngClass]="{'p-invalid': (nom.invalid && submitted) || (nom.dirty && nom.invalid)}">
|
||||
<small *ngIf="(nom.invalid && submitted) || (nom.dirty && nom.invalid)" class="p-error">Le nom est
|
||||
requis.</small>
|
||||
</div>
|
||||
<div class="p-field">
|
||||
<label for="mail">Email participant</label>
|
||||
<span class="p-input-icon-right">
|
||||
<i *ngIf="loademail" class="pi pi-spin pi-spinner" ></i>
|
||||
|
||||
<input #mail="ngModel" id="mail" type="email" required pInputText (change)="getUserFromMail()" [(ngModel)]="personalInformation.mail"
|
||||
[ngClass]="{'p-invalid': (mail.invalid && submitted) || (mail.dirty && mail.invalid)}">
|
||||
</span>
|
||||
<small class="p-error" *ngIf="(mail.invalid && submitted )|| (mail.dirty && mail.invalid)">Le
|
||||
mail est requis.</small>
|
||||
|
||||
</div>
|
||||
<div class="p-field">
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 1rem;font-weight: normal;">Avez vous un agenda avec un flux ics accessible ?</p>
|
||||
<p-inputSwitch [(ngModel)]="hasics"></p-inputSwitch>
|
||||
</div>
|
||||
<div *ngIf="hasics" class="p-field">
|
||||
<label for="ics">URL ICS du participant</label>
|
||||
<span class="p-input-icon-right">
|
||||
<i *ngIf="loadics" class="pi pi-spin pi-spinner" ></i>
|
||||
<input #mail="ngModel" id="ics" type="text" pInputText (change)="getICS()" [(ngModel)]="personalInformation.ics">
|
||||
</span>
|
||||
</div>
|
||||
<div *ngIf="poll?.has_meal" class="p-field">
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 1rem;font-weight: normal;">Avez vous des préférences alimentaires ?</p>
|
||||
<p-inputSwitch [(ngModel)]="personalInformation.pref"></p-inputSwitch>
|
||||
</div>
|
||||
<div *ngIf="personalInformation?.pref" class="p-field">
|
||||
<label for="desc">Description préférences alimentaires</label>
|
||||
<textarea #desc="ngModel" id="desc" required pInputTextarea [(ngModel)]="personalInformation.desc"
|
||||
[ngClass]="{'p-invalid': (desc.invalid && submitted) || (desc.dirty && desc.invalid)}"></textarea>
|
||||
<small class="p-error" *ngIf="(desc.invalid && submitted) || (desc.dirty && desc.invalid)">La description est
|
||||
requise.</small>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p-selectButton [options]="calendarortableoption" [(ngModel)]="calendarortable">
|
||||
<ng-template let-item>
|
||||
<i [class]="item.icon">Vue {{item.text}}</i>
|
||||
</ng-template>
|
||||
</p-selectButton>
|
||||
</div>
|
||||
|
||||
<div *ngIf="calendarortable ==='calendar'">
|
||||
<full-calendar #calendar [options]="options"></full-calendar>
|
||||
</div>
|
||||
|
||||
<div *ngIf="calendarortable !='calendar'">
|
||||
<div class="container-fluid">
|
||||
<div class="table-responsive-sm card">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2"></th>
|
||||
<th *ngFor="let ev of events" class="text-light" style="text-align: center;background-color: #545B62">{{ev.start | date:'EEEE d LLLL': 'CEST':'fr'}}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th *ngFor="let ev of events" style="text-align: center">{{ev.start | date:'H:mm'}} <BR>-<BR> {{ev.end | date:'H:mm'}}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span>{{uniqueUsers.length + 1}} participant</span><span *ngIf="uniqueUsers.length > 0">s</span></th>
|
||||
<th *ngFor="let pc of poll?.pollChoices" style="text-align: center">{{pc.users.length}}</th>
|
||||
</tr>
|
||||
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let u of userChoices | keyvalue">
|
||||
<td><input type="text" [disabled]='true' pInputText [ngModel]="uniqueUsers | usernamePipe:u.key"></td>
|
||||
<td *ngFor="let ev of events" style="text-align: center"><p-checkbox [disabled]='true' [binary]="true" [ngModel]="u.value | selecteddate4userPipe:u.key:ev" ></p-checkbox></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td><input #nomvotant="" id="nomvotant" type="text" required pInputText [(ngModel)]="personalInformation.nom"></td>
|
||||
<td *ngFor="let ev of events" style="text-align: center"><p-checkbox [binary]="true" (onChange)="updateEvent($event,ev )" [ngModel]="ev.extendedProps.selected" ></p-checkbox></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p-button [disabled]="voeuxsoumis" label="Soumettre voeux" (onClick)="createReponse()"
|
||||
icon="pi pi-angle-left"></p-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</ng-template>
|
||||
<ng-template pTemplate="footer">
|
||||
<app-show-comments *ngIf="poll" [comments]="comments"></app-show-comments>
|
||||
|
||||
<div class="p-fluid">
|
||||
<div class="p-field">
|
||||
<label for="comment">Auteur du commentaire associé à ce sondage</label>
|
||||
<input #comment="ngModel" id="comment" type="text" required pInputText [(ngModel)]="comment1"
|
||||
[ngClass]="{'p-invalid': (comment.invalid && csubmitted) || (comment.dirty && comment.invalid)}">
|
||||
<small *ngIf="(comment.invalid && csubmitted) || (comment.dirty && comment.invalid)" class="p-error">L'auteur
|
||||
du commentaire est requis.</small>
|
||||
</div>
|
||||
|
||||
<div class="p-field">
|
||||
<label for="commentdesc">Commentaire</label>
|
||||
<textarea #commentdesc="ngModel" id="commentdesc" required pInputTextarea [(ngModel)]="commentdesc1"
|
||||
[ngClass]="{'p-invalid': (commentdesc.invalid && csubmitted) || (commentdesc.dirty && commentdesc.invalid)}"></textarea>
|
||||
<small class="p-error"
|
||||
*ngIf="(commentdesc.invalid && csubmitted) || (commentdesc.dirty && commentdesc.invalid)">Le commentaire est
|
||||
requis.</small>
|
||||
</div>
|
||||
<div>
|
||||
<p-button [disabled]="commentsoumis" label="Ajouter commentaire" (onClick)="createComment()"
|
||||
icon="pi pi-angle-left"></p-button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</ng-template>
|
||||
</p-card>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<!--<p-button (onClick)="testEvent()">test</p-button>-->
|
||||
<!-- Modal -->
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AnswerPollComponent } from './answer-poll.component';
|
||||
|
||||
describe('AnswerPollComponent', () => {
|
||||
let component: AnswerPollComponent;
|
||||
let fixture: ComponentFixture<AnswerPollComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ AnswerPollComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AnswerPollComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,360 @@
|
||||
import { Component, OnInit, ViewChild, AfterViewChecked } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { PollService } from '../poll-service.service';
|
||||
import { Poll, ChoiceUser, PollCommentElement, User, PollChoice } from '../model/model';
|
||||
import { CalendarOptions, EventInput } from '@fullcalendar/core';
|
||||
import { FullCalendarComponent } from '@fullcalendar/angular';
|
||||
import frLocale from '@fullcalendar/core/locales/fr';
|
||||
import { MessageService } from 'primeng/api';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ModalPollClosComponent } from '../modal-poll-clos/modal-poll-clos.component';
|
||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||
import interactionPlugin from '@fullcalendar/interaction';
|
||||
import timeGridPlugin from '@fullcalendar/timegrid';
|
||||
|
||||
@Component({
|
||||
selector: 'app-answer-poll',
|
||||
templateUrl: './answer-poll.component.html',
|
||||
styleUrls: ['./answer-poll.component.css'],
|
||||
providers: [MessageService, PollService, FullCalendarComponent, NgbModal]
|
||||
|
||||
})
|
||||
export class AnswerPollComponent implements OnInit {
|
||||
|
||||
constructor(public messageService: MessageService,
|
||||
// tslint:disable-next-line:align
|
||||
private actRoute: ActivatedRoute, private pollService: PollService,
|
||||
// tslint:disable-next-line:align
|
||||
private modalService: NgbModal) { }
|
||||
slugid: string;
|
||||
poll: Poll;
|
||||
calendarortableoption: any[];
|
||||
calendarortable = 'calendar';
|
||||
personalInformation: any = {
|
||||
nom: '',
|
||||
mail: '',
|
||||
desc: '',
|
||||
ics: '',
|
||||
pref: false
|
||||
};
|
||||
hasics: false;
|
||||
options: CalendarOptions;
|
||||
@ViewChild('calendar') calendarComponent: FullCalendarComponent;
|
||||
submitted = false;
|
||||
csubmitted = false;
|
||||
voeuxsoumis = false;
|
||||
commentsoumis = false;
|
||||
events: EventInput[] = [];
|
||||
eventsfromics: EventInput[] = [];
|
||||
allevents: EventInput[] = [];
|
||||
loadics = false;
|
||||
loademail = false;
|
||||
comments: PollCommentElement[];
|
||||
|
||||
comment1 = '';
|
||||
commentdesc1 = '';
|
||||
uniqueUsers: User[] = [];
|
||||
userChoices: Map<number, PollChoice[]> = new Map();
|
||||
ngOnInit(): void {
|
||||
this.calendarortableoption = [
|
||||
{ icon: 'pi pi-calendar', text: 'Calendrier', value: 'calendar' },
|
||||
{ icon: 'pi pi-table', text: 'Tableau', value: 'table' },
|
||||
];
|
||||
|
||||
|
||||
this.actRoute.paramMap.subscribe(params => {
|
||||
this.slugid = params.get('slugid');
|
||||
this.pollService.getPollBySlugId(this.slugid).subscribe(p => {
|
||||
this.poll = p;
|
||||
this.pollService.getComentsBySlugId(this.slugid).subscribe(cs => this.comments = cs);
|
||||
|
||||
|
||||
if (this.poll.clos) {
|
||||
this.openModal();
|
||||
}
|
||||
const calendarApi = this.calendarComponent.getApi();
|
||||
// calendarApi.next();
|
||||
this.uniqueUsers.splice(0, this.uniqueUsers.length);
|
||||
this.poll.pollChoices.forEach(pc => {
|
||||
pc.users.forEach(user => {
|
||||
if (this.uniqueUsers.filter(us => us.id === user.id).length === 0) {
|
||||
this.uniqueUsers.push(user);
|
||||
this.userChoices.set(user.id, []);
|
||||
}
|
||||
});
|
||||
|
||||
const evt =
|
||||
{
|
||||
title: '',
|
||||
start: pc.startDate,
|
||||
end: pc.endDate,
|
||||
resourceEditable: false,
|
||||
eventResizableFromStart: false,
|
||||
backgroundColor: 'red',
|
||||
id: this.getUniqueId(8),
|
||||
extendedProps: {
|
||||
choiceid: pc.id,
|
||||
selected: false,
|
||||
},
|
||||
};
|
||||
calendarApi.addEvent(evt, true);
|
||||
this.events.push(evt);
|
||||
this.allevents.push(evt);
|
||||
|
||||
});
|
||||
this.poll.pollChoices.forEach(pc => {
|
||||
pc.users.forEach(us => {
|
||||
this.userChoices.get(us.id).push(pc);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
this.options = {
|
||||
initialView: 'timeGridWeek',
|
||||
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin],
|
||||
|
||||
// dateClick: this.handleDateClick.bind(this), // bind is important!
|
||||
/*eventDragStart: (timeSheetEntry, jsEvent, ui, activeView) => {
|
||||
this.eventDragStart(
|
||||
timeSheetEntry, jsEvent, ui, activeView
|
||||
);
|
||||
},
|
||||
eventDragStop: (timeSheetEntry, jsEvent, ui, activeView) => {
|
||||
this.eventDragStop(
|
||||
timeSheetEntry, jsEvent, ui, activeView
|
||||
);
|
||||
},*/
|
||||
// events: this.events,
|
||||
events: this.allevents,
|
||||
editable: false,
|
||||
droppable: false,
|
||||
// selectMirror: true,
|
||||
eventResizableFromStart: false,
|
||||
selectable: false,
|
||||
locale: frLocale,
|
||||
themeSystem: 'bootstrap',
|
||||
slotMinTime: '08:00:00',
|
||||
slotMaxTime: '20:00:00',
|
||||
eventMouseEnter: (mouseEnterInfo) => {
|
||||
|
||||
},
|
||||
eventClick: (info) => {
|
||||
if (!info.event.extendedProps.fromics) {
|
||||
if (info.event.extendedProps.selected) {
|
||||
info.event.setExtendedProp('selected', false);
|
||||
const evt = this.events.filter(e => e.extendedProps.choiceid === info.event.extendedProps.choiceid).pop();
|
||||
evt.extendedProps.selected = false;
|
||||
evt.backgroundColor = 'red';
|
||||
info.event.setProp('backgroundColor', 'red');
|
||||
this.poll.pollChoices.filter(pc => pc.id === evt.extendedProps.choiceid)[0].users.splice(-1, 1);
|
||||
|
||||
} else {
|
||||
info.event.setExtendedProp('selected', true);
|
||||
const evt = this.events.filter(e => e.extendedProps.choiceid === info.event.extendedProps.choiceid).pop();
|
||||
evt.extendedProps.selected = true;
|
||||
evt.backgroundColor = 'green';
|
||||
info.event.setProp('backgroundColor', 'green');
|
||||
this.poll.pollChoices.filter(pc => pc.id === evt.extendedProps.choiceid)[0].users.push({ id: -1 });
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// info.event.remove();
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
updateEvent($event: any, event: EventInput): void {
|
||||
|
||||
event.extendedProps.selected = $event.checked;
|
||||
if ($event.checked) {
|
||||
event.backgroundColor = 'green';
|
||||
this.poll.pollChoices.filter(pc => pc.id === event.extendedProps.choiceid)[0].users.push({ id: -1 });
|
||||
|
||||
} else {
|
||||
event.backgroundColor = 'red';
|
||||
this.poll.pollChoices.filter(pc => pc.id === event.extendedProps.choiceid)[0].users.splice(-1, 1);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
createComment(): void {
|
||||
|
||||
|
||||
if (this.comment1 && this.commentdesc1) {
|
||||
const c: PollCommentElement = {
|
||||
content: this.commentdesc1,
|
||||
auteur: this.comment1
|
||||
};
|
||||
this.pollService.addComment4Poll(this.slugid, c).subscribe(e => {
|
||||
this.messageService.add({
|
||||
severity: 'success',
|
||||
summary: 'Données enregistrées',
|
||||
detail: 'Merci pour ce commentaire'
|
||||
}
|
||||
);
|
||||
this.pollService.getComentsBySlugId(this.poll?.slug).subscribe(cs => this.comments = cs);
|
||||
this.commentsoumis = true;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
this.messageService.add(
|
||||
{
|
||||
severity: 'warn',
|
||||
summary: 'Données incomplètes',
|
||||
detail: 'Veuillez remplir les champs requis'
|
||||
}
|
||||
);
|
||||
this.csubmitted = true;
|
||||
}
|
||||
|
||||
createReponse(): void {
|
||||
if (this.personalInformation.nom && this.personalInformation.mail &&
|
||||
this.events.filter(e => e.extendedProps.selected).length > 0 &&
|
||||
(this.personalInformation.desc || !this.personalInformation.pref)) {
|
||||
const cu: ChoiceUser = {
|
||||
username: this.personalInformation.nom,
|
||||
mail: this.personalInformation.mail,
|
||||
pref: this.personalInformation.desc,
|
||||
ics: this.personalInformation.ics,
|
||||
choices: this.events.filter(e => e.extendedProps.selected).map(x => x.extendedProps.choiceid)
|
||||
};
|
||||
this.pollService.updateChoice4user(cu).subscribe(e => {
|
||||
// cu.choices.forEach(c => this.poll.pollChoices.filter( c1 => c1.id === c)[0].users.push(e));
|
||||
// if (this.uniqueUsers.filter(u1 => u1.id === e.id ).length === 0) {
|
||||
// this.uniqueUsers.push(e);
|
||||
// }
|
||||
this.messageService.add({
|
||||
severity: 'success',
|
||||
summary: 'Données enregistrées',
|
||||
detail: 'Merci pour votre participation'
|
||||
}
|
||||
);
|
||||
this.voeuxsoumis = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.messageService.add(
|
||||
{
|
||||
severity: 'warn',
|
||||
summary: 'Données incomplètes',
|
||||
detail: 'Veuillez remplir les champs requis et sélectioner au moins une date'
|
||||
}
|
||||
);
|
||||
this.submitted = true;
|
||||
|
||||
}
|
||||
|
||||
getICS(): void {
|
||||
this.loadics = true;
|
||||
this.pollService.getICS(this.slugid, this.personalInformation.ics).subscribe(res => {
|
||||
this.loadics = false;
|
||||
|
||||
const calendarApi = this.calendarComponent.getApi();
|
||||
if (res.eventdtos.length > 0) {
|
||||
this.eventsfromics.forEach(eid => {
|
||||
const index = this.allevents.indexOf(eid);
|
||||
if (index > -1) {
|
||||
this.allevents.splice(index, 1);
|
||||
}
|
||||
calendarApi.getEventById(eid.id)?.remove();
|
||||
});
|
||||
this.eventsfromics = [];
|
||||
}
|
||||
console.log(res);
|
||||
|
||||
res.eventdtos.forEach(evtdto => { // calendarApi.next();
|
||||
const evt1 =
|
||||
{
|
||||
title: evtdto.description,
|
||||
start: evtdto.startDate,
|
||||
end: evtdto.endDate,
|
||||
resourceEditable: false,
|
||||
eventResizableFromStart: false,
|
||||
id: this.getUniqueId(8),
|
||||
|
||||
backgroundColor: 'blue',
|
||||
extendedProps: {
|
||||
fromics: true
|
||||
},
|
||||
|
||||
|
||||
};
|
||||
const eventAPI = calendarApi.addEvent(evt1, true);
|
||||
this.eventsfromics.push(evt1);
|
||||
this.allevents.push(evt1);
|
||||
|
||||
});
|
||||
|
||||
const unselected = this.events.map(ev => ev.extendedProps.choiceid);
|
||||
res.selectedChoices.forEach(e => {
|
||||
const index = unselected.indexOf(e);
|
||||
if (index > -1) {
|
||||
unselected.splice(index, 1);
|
||||
}
|
||||
const evt1 = this.events.filter(ev => ev.extendedProps.choiceid === e)[0];
|
||||
|
||||
const evt2 = calendarApi.getEventById(evt1.id);
|
||||
evt1.backgroundColor = 'red';
|
||||
evt1.extendedProps.selected = false;
|
||||
evt2.setProp('backgroundColor', 'red');
|
||||
// this.poll.pollChoices.filter(pc => pc.id === evt1.extendedProps.choiceid)[0].users.push({ id: -1 });
|
||||
});
|
||||
unselected.forEach(e => {
|
||||
const evt1 = this.events.filter(ev => ev.extendedProps.choiceid === e)[0];
|
||||
|
||||
const evt2 = calendarApi.getEventById(evt1.id);
|
||||
evt1.backgroundColor = 'green';
|
||||
evt1.extendedProps.selected = true;
|
||||
evt2.setProp('backgroundColor', 'green');
|
||||
this.poll.pollChoices.filter(pc => pc.id === evt1.extendedProps.choiceid)[0].users.push({ id: -1 });
|
||||
});
|
||||
}, (err) => {
|
||||
this.loadics = false;
|
||||
|
||||
this.messageService.add(
|
||||
{
|
||||
severity: 'warn',
|
||||
summary: 'Ne peut récupérer l\'agenda à partir de l\'adresse de l\'ics',
|
||||
detail: 'Une erreur s\'est produite au moment de la récupération de l\'agenda'
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
openModal(): void {
|
||||
const modalRef = this.modalService.open(ModalPollClosComponent, {
|
||||
beforeDismiss: () => false,
|
||||
centered: true,
|
||||
windowClass: 'lgModal',
|
||||
backdrop: 'static'
|
||||
});
|
||||
modalRef.componentInstance.poll = this.poll;
|
||||
}
|
||||
|
||||
getUserFromMail(): void {
|
||||
}
|
||||
|
||||
private getUniqueId(parts: number): string {
|
||||
const stringArr = [];
|
||||
for (let i = 0; i < parts; i++) {
|
||||
// tslint:disable-next-line:no-bitwise
|
||||
const S4 = (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
|
||||
stringArr.push(S4);
|
||||
}
|
||||
return stringArr.join('-');
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { HomeComponentComponent } from './home-component/home-component.component';
|
||||
import { CreatePollComponentComponent } from './create-poll-component/create-poll-component.component';
|
||||
import { AnswerPollComponent } from './answer-poll/answer-poll.component';
|
||||
import { AdminPollComponent } from './admin-poll/admin-poll.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: HomeComponentComponent
|
||||
},
|
||||
{
|
||||
path: 'create',
|
||||
component: CreatePollComponentComponent
|
||||
},
|
||||
{
|
||||
path: 'update/:slugadminid',
|
||||
component: CreatePollComponentComponent
|
||||
},
|
||||
{
|
||||
path: 'answer/:slugid',
|
||||
component: AnswerPollComponent
|
||||
},
|
||||
{
|
||||
path: 'admin/:slugadminid',
|
||||
component: AdminPollComponent
|
||||
}
|
||||
|
||||
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
87
ansible/files/doodlestudent/front/src/app/app.component.css
Normal file
87
ansible/files/doodlestudent/front/src/app/app.component.css
Normal file
@@ -0,0 +1,87 @@
|
||||
.Container {
|
||||
max-width: 800px;
|
||||
margin: 2rem auto;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.SmallCard_Container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.SmallCard {
|
||||
background-color: white;
|
||||
margin-right: 1rem;
|
||||
padding: 2rem;
|
||||
border-radius: 0.5rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.SmallCard_Image {
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.SmallCard_Title {
|
||||
padding: 1rem;
|
||||
font-weight: 800;
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.SmallCard_Subtitle {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.SmallCard:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.Home_Container {
|
||||
width: 1024px;
|
||||
margin: 0 auto;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.Home_Wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.Home_Logo {
|
||||
margin-bottom: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.Home_Button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.Home_CreateLink {
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
background-color: #43dbac;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
font-size: 1.3rem;
|
||||
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.10);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<router-outlet></router-outlet>
|
||||
@@ -0,0 +1,35 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterTestingModule
|
||||
],
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have as title 'tlcfront'`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('tlcfront');
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement;
|
||||
expect(compiled.querySelector('.content span').textContent).toContain('tlcfront app is running!');
|
||||
});
|
||||
});
|
||||
12
ansible/files/doodlestudent/front/src/app/app.component.ts
Normal file
12
ansible/files/doodlestudent/front/src/app/app.component.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { HomeComponentComponent } from './home-component/home-component.component';
|
||||
import { CreatePollComponentComponent } from './create-poll-component/create-poll-component.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css'],
|
||||
providers: [HomeComponentComponent, CreatePollComponentComponent]
|
||||
})
|
||||
export class AppComponent {
|
||||
}
|
||||
86
ansible/files/doodlestudent/front/src/app/app.module.ts
Normal file
86
ansible/files/doodlestudent/front/src/app/app.module.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { CardSmallComponentComponent } from './card-small-component/card-small-component.component';
|
||||
import { HomeComponentComponent } from './home-component/home-component.component';
|
||||
import { CreatePollComponentComponent } from './create-poll-component/create-poll-component.component';
|
||||
import {StepsModule} from 'primeng/steps';
|
||||
import {MenuItem} from 'primeng/api';
|
||||
// import {FullCalendarModule} from 'primeng/fullcalendar';
|
||||
import {ToastModule} from 'primeng/toast';
|
||||
import {MessagesModule} from 'primeng/messages';
|
||||
import {MessageModule} from 'primeng/message';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {InputTextareaModule} from 'primeng/inputtextarea';
|
||||
import {InputSwitchModule} from 'primeng/inputswitch';
|
||||
import {CardModule} from 'primeng/card';
|
||||
import {ButtonModule} from 'primeng/button';
|
||||
import {InputTextModule} from 'primeng/inputtext';
|
||||
import {SelectButtonModule} from 'primeng/selectbutton';
|
||||
import {MenubarModule} from 'primeng/menubar';
|
||||
import {CheckboxModule} from 'primeng/checkbox';
|
||||
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'; // a plugin
|
||||
import interactionPlugin from '@fullcalendar/interaction'; // a plugin
|
||||
import timeGridPlugin from '@fullcalendar/timegrid';
|
||||
|
||||
import { FullCalendarModule } from '@fullcalendar/angular';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { AnswerPollComponent } from './answer-poll/answer-poll.component';
|
||||
import { AdminPollComponent } from './admin-poll/admin-poll.component';
|
||||
import { DateagoPipe } from './dateago.pipe'; // the main connector. must go first
|
||||
|
||||
import { registerLocaleData } from '@angular/common';
|
||||
import localeFr from '@angular/common/locales/fr';
|
||||
import { UsernamePipePipe } from './username-pipe.pipe';
|
||||
import { Selecteddate4userPipePipe } from './selecteddate4user-pipe.pipe';
|
||||
import { ModalPollClosComponent } from './modal-poll-clos/modal-poll-clos.component';
|
||||
import { TopBarComponent } from './top-bar/top-bar.component';
|
||||
import { ShowCommentsComponent } from './show-comments/show-comments.component';
|
||||
|
||||
registerLocaleData(localeFr, 'fr');
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
CardSmallComponentComponent,
|
||||
HomeComponentComponent,
|
||||
CreatePollComponentComponent,
|
||||
AnswerPollComponent,
|
||||
AdminPollComponent,
|
||||
DateagoPipe,
|
||||
UsernamePipePipe,
|
||||
Selecteddate4userPipePipe,
|
||||
ModalPollClosComponent,
|
||||
TopBarComponent,
|
||||
ShowCommentsComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
BrowserAnimationsModule,
|
||||
AppRoutingModule,
|
||||
StepsModule,
|
||||
FullCalendarModule,
|
||||
ToastModule,
|
||||
MessagesModule,
|
||||
MessageModule,
|
||||
InputSwitchModule,
|
||||
CardModule,
|
||||
ButtonModule,
|
||||
InputTextModule,
|
||||
InputTextareaModule,
|
||||
SelectButtonModule,
|
||||
MenubarModule,
|
||||
CheckboxModule,
|
||||
NgbModule
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent],
|
||||
|
||||
|
||||
})
|
||||
export class AppModule { }
|
||||
@@ -0,0 +1,38 @@
|
||||
|
||||
.SmallCard_Container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.SmallCard {
|
||||
background-color: white;
|
||||
margin-right: 1rem;
|
||||
padding: 2rem;
|
||||
border-radius: 0.5rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.SmallCard_Image {
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.SmallCard_Title {
|
||||
padding: 1rem;
|
||||
font-weight: 800;
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.SmallCard_Subtitle {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.SmallCard:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<div class="SmallCard_Container">
|
||||
<div *ngFor="let card of cards" class="SmallCard" [ngStyle]="card.style">
|
||||
<div class="SmallCard_Image">
|
||||
<img [src]="card.image" height="200px"/>
|
||||
</div>
|
||||
<div class="SmallCard_Title">
|
||||
{{ card.title }}
|
||||
</div>
|
||||
<div class="SmallCard_Subtitle">
|
||||
{{ card.subtitle }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CardSmallComponentComponent } from './card-small-component.component';
|
||||
|
||||
describe('CardSmallComponentComponent', () => {
|
||||
let component: CardSmallComponentComponent;
|
||||
let fixture: ComponentFixture<CardSmallComponentComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ CardSmallComponentComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CardSmallComponentComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { Card } from '../home-component/Card';
|
||||
|
||||
@Component({
|
||||
selector: 'app-card-small-component',
|
||||
templateUrl: './card-small-component.component.html',
|
||||
styleUrls: ['./card-small-component.component.css']
|
||||
})
|
||||
export class CardSmallComponentComponent implements OnInit {
|
||||
|
||||
|
||||
@Input()
|
||||
cards: Card[];
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
.CreatePoll_Form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.CreatePoll_Input {
|
||||
width: 100%;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
.CreatePoll_Input:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
font-weight: 400;
|
||||
outline: none;
|
||||
color: #102A43;
|
||||
width: 100%;
|
||||
font-size: 17px;
|
||||
border-radius: .25rem;
|
||||
background-color: #F0F4F8;
|
||||
border: 1px solid white;
|
||||
}
|
||||
|
||||
input:focus, textarea:focus {
|
||||
border-color: #4D3DF7;
|
||||
color: #102A43;
|
||||
}
|
||||
|
||||
.CreatePoll_Input input, textarea {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.CreatePoll_Input textarea {
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
.CreatePoll_Buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.CreatePoll_Button {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.CreatePoll_LabelError {
|
||||
color: #EF4E4E;
|
||||
}
|
||||
|
||||
.rbc-calendar {
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.rbc-event {
|
||||
background-color: #4D3DF7;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.rbc-today {
|
||||
background-color: #E6E6FF;
|
||||
}
|
||||
|
||||
.rbc-current-time-indicator {
|
||||
background-color: #4D3DF7;
|
||||
}
|
||||
|
||||
.rbc-slot-selection {
|
||||
background-color: #C4C6FF;
|
||||
}
|
||||
|
||||
.rbc-time-view {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.switch {
|
||||
display: inline-block;
|
||||
height: 34px;
|
||||
position: relative;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
display:none;
|
||||
}
|
||||
|
||||
.slider {
|
||||
background-color: #ccc;
|
||||
bottom: 0;
|
||||
cursor: pointer;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
background-color: #fff;
|
||||
bottom: 4px;
|
||||
content: "";
|
||||
height: 26px;
|
||||
left: 4px;
|
||||
position: absolute;
|
||||
transition: .4s;
|
||||
width: 26px;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #4D3DF7;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
.slider.round {
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.CreatePoll_Switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.CreatePoll_Switch span {
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
.Poll_Link {
|
||||
font-size: 1.2rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.Recap_Link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.Copy_Link {
|
||||
border: none;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.text-green {
|
||||
color: #199473;
|
||||
}
|
||||
|
||||
.Recap_Links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
<div class="Container">
|
||||
<img src="../../assets/flat_logo.png" alt="Logo Simba" height="50px" [ngStyle]="{ 'marginBottom': '1rem' }"/>
|
||||
|
||||
<div class="card">
|
||||
<p-toast></p-toast>
|
||||
<p-steps [model]="items" [readonly]="false" [(activeIndex)]="step"></p-steps>
|
||||
</div>
|
||||
<div [hidden]="step!=0" class="stepsdemo-content">
|
||||
<p-card>
|
||||
<ng-template pTemplate="title">
|
||||
Informations
|
||||
</ng-template>
|
||||
<ng-template pTemplate="subtitle">
|
||||
Entrez les informations sur le rendez-vous à planifier
|
||||
</ng-template>
|
||||
<ng-template pTemplate="content">
|
||||
<div class="p-fluid">
|
||||
<div class="p-field">
|
||||
<label for="titre">Titre de la réunion</label>
|
||||
<input #titre="ngModel" id="titre" type="text" required pInputText
|
||||
[(ngModel)]="poll.title"
|
||||
[ngClass]="{'p-invalid': (titre.invalid && submitted) || (titre.dirty && titre.invalid)}">
|
||||
<small *ngIf="(titre.invalid && submitted) || (titre.dirty && titre.invalid)"
|
||||
class="p-error">Le titre est requis.</small>
|
||||
</div>
|
||||
<div class="p-field">
|
||||
<label for="lieu">Lieu de la réunion</label>
|
||||
<input #lieu="ngModel" id="lieu" type="text" required pInputText
|
||||
[(ngModel)]="poll.location"
|
||||
[ngClass]="{'p-invalid': (lieu.invalid && submitted) || (lieu.dirty && lieu.invalid)}">
|
||||
<small class="p-error" *ngIf="(lieu.invalid && submitted )|| (lieu.dirty && lieu.invalid)">Le
|
||||
lieu est requis.</small>
|
||||
</div>
|
||||
<div class="p-field">
|
||||
<label for="desc">Description</label>
|
||||
<textarea #desc="ngModel" id="desc" required pInputTextarea
|
||||
[(ngModel)]="poll.description"
|
||||
[ngClass]="{'p-invalid': (desc.invalid && submitted) || (desc.dirty && desc.invalid)}"></textarea>
|
||||
<small class="p-error" *ngIf="(desc.invalid && submitted) || (desc.dirty && desc.invalid)">La description est requise.</small>
|
||||
</div>
|
||||
<div class="p-field">
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 1rem;font-weight: normal;">Repas</p>
|
||||
<p-inputSwitch [ariaLabelledBy]="'repas'" #repas="ngModel" id="repas" [(ngModel)]="poll.has_meal"></p-inputSwitch>
|
||||
</div>
|
||||
<div *ngIf="poll?.id" class="p-field">
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 1rem;font-weight: normal;">Sondage clos</p>
|
||||
<p-inputSwitch #clos="ngModel" id="clos" [(ngModel)]="poll.clos"></p-inputSwitch>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="footer">
|
||||
<div class="p-grid p-nogutter p-justify-end">
|
||||
<p-button [disabled]=true label="Back" icon="pi pi-angle-left"></p-button>
|
||||
|
||||
<p-button class="float-right" label="Next" (onClick)="nextPage()" icon="pi pi-angle-right" iconPos="right"></p-button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</p-card>
|
||||
</div>
|
||||
<div *ngIf="step==1" class="stepsdemo-content">
|
||||
<p-card>
|
||||
<ng-template pTemplate="content">
|
||||
<div class="p-fluid">
|
||||
|
||||
<div class="p-field">
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 1rem;font-weight: normal;">Avez vous un agenda avec un flux ics accessible ?</p>
|
||||
<p-inputSwitch [(ngModel)]="hasics"></p-inputSwitch>
|
||||
</div>
|
||||
<div *ngIf="hasics" class="p-field">
|
||||
<label for="ics">URL ICS du participant</label>
|
||||
<span class="p-input-icon-right">
|
||||
<i *ngIf="loadics" class="pi pi-spin pi-spinner" ></i>
|
||||
<input #mail="ngModel" id="ics" type="text" pInputText (change)="getICS()" [(ngModel)]="ics">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<full-calendar #calendar [options]="options"></full-calendar>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="footer">
|
||||
<div>
|
||||
<p-button label="Back" (onClick)="prevPage1()" icon="pi pi-angle-left"></p-button>
|
||||
<p-button class="float-right" label="Next" (onClick)="nextPage1()" icon="pi pi-angle-right" iconPos="right"></p-button>
|
||||
</div>
|
||||
|
||||
|
||||
</ng-template>
|
||||
</p-card>
|
||||
</div>
|
||||
<div *ngIf="step==2" class="stepsdemo-content">
|
||||
<p-card>
|
||||
<ng-template pTemplate="content">
|
||||
Le sondage est créé. <BR>
|
||||
Le lien pour participer est <a [href]="urlsondage" target="_blank">{{urlsondage}} </a>. <BR>
|
||||
Le lien d'administration est <a [href]="urlsondageadmin" target="_blank">{{urlsondageadmin}}</a>.<BR>
|
||||
Un salon a été créé de discussion pour cette réunion est accessible à cette adresse <a [href]="urlsalon" target="_blank">{{urlsalon}}</a>.<BR>
|
||||
<span *ngIf="urlpad">Un pad a été créé pour cette réunion <a [href]="urlpad" target="_blank">{{urlpad}}</a>.</span><BR>
|
||||
<BR>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="footer">
|
||||
<div>
|
||||
<p-button label="Back" (onClick)="prevPage1()" icon="pi pi-angle-left"></p-button>
|
||||
</div>
|
||||
|
||||
|
||||
</ng-template>
|
||||
</p-card>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CreatePollComponentComponent } from './create-poll-component.component';
|
||||
|
||||
describe('CreatePollComponentComponent', () => {
|
||||
let component: CreatePollComponentComponent;
|
||||
let fixture: ComponentFixture<CreatePollComponentComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ CreatePollComponentComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CreatePollComponentComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,387 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { MenuItem, MessageService } from 'primeng/api';
|
||||
import { PollService } from '../poll-service.service';
|
||||
import { FullCalendarComponent } from '@fullcalendar/angular';
|
||||
import frLocale from '@fullcalendar/core/locales/fr';
|
||||
import { PollChoice, Poll, User } from '../model/model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { CalendarOptions, EventInput } from '@fullcalendar/core';
|
||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||
import interactionPlugin from '@fullcalendar/interaction';
|
||||
import timeGridPlugin from '@fullcalendar/timegrid';
|
||||
|
||||
/*FullCalendarModule.registerPlugins([ // register FullCalendar plugins
|
||||
dayGridPlugin,
|
||||
interactionPlugin,
|
||||
timeGridPlugin
|
||||
]);*/
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-poll-component',
|
||||
templateUrl: './create-poll-component.component.html',
|
||||
styleUrls: ['./create-poll-component.component.css'],
|
||||
providers: [MessageService, PollService, FullCalendarComponent]
|
||||
})
|
||||
export class CreatePollComponentComponent implements OnInit {
|
||||
urlsondage = '';
|
||||
urlsondageadmin = '';
|
||||
urlsalon = '';
|
||||
urlpad = '';
|
||||
|
||||
items: MenuItem[];
|
||||
options: CalendarOptions;
|
||||
|
||||
step = 0;
|
||||
|
||||
slugid: string;
|
||||
poll: Poll = {};
|
||||
|
||||
events: EventInput[] = [];
|
||||
eventsfromics: EventInput[] = [];
|
||||
allevents: EventInput[] = [];
|
||||
|
||||
|
||||
calendarComponent: FullCalendarComponent;
|
||||
hasics = false;
|
||||
loadics = false;
|
||||
ics: string;
|
||||
|
||||
@ViewChild('calendar') set content(content: FullCalendarComponent) {
|
||||
if (content) { // initially setter gets called with undefined
|
||||
this.calendarComponent = content;
|
||||
const calendarApi = this.calendarComponent.getApi();
|
||||
|
||||
this.poll.pollChoices.forEach(pc => {
|
||||
|
||||
const evt =
|
||||
{
|
||||
title: '',
|
||||
start: pc.startDate,
|
||||
end: pc.endDate,
|
||||
resourceEditable: false,
|
||||
eventResizableFromStart: false,
|
||||
extendedProps: {
|
||||
choiceid: pc.id,
|
||||
tmpId: this.getUniqueId(8)
|
||||
},
|
||||
};
|
||||
this.events.push(evt);
|
||||
calendarApi.addEvent(evt, true);
|
||||
|
||||
});
|
||||
calendarApi.setOption('validRange', {
|
||||
start: this.getValidDate(),
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
submitted = false;
|
||||
|
||||
|
||||
constructor(public messageService: MessageService, public pollService: PollService, private actRoute: ActivatedRoute) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.poll.pollChoices = [];
|
||||
this.items = [{
|
||||
label: 'Informations pour le rendez vous',
|
||||
command: () => {
|
||||
this.step = 0;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Choix de la date',
|
||||
command: () => {
|
||||
this.step = 1;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Résumé',
|
||||
command: () => {
|
||||
this.step = 2;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
|
||||
this.options = {
|
||||
initialView: 'timeGridWeek',
|
||||
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin],
|
||||
|
||||
// dateClick: this.handleDateClick.bind(this), // bind is important!
|
||||
select: (selectionInfo) => {
|
||||
console.log(selectionInfo);
|
||||
const calendarApi = this.calendarComponent.getApi();
|
||||
console.log(this.getUniqueId(8));
|
||||
const evt = {
|
||||
title: '',
|
||||
start: selectionInfo.start,
|
||||
end: selectionInfo.end,
|
||||
resourceEditable: true,
|
||||
eventResizableFromStart: true,
|
||||
id: this.getUniqueId(8),
|
||||
|
||||
extendedProps: {
|
||||
// tmpId: this.getUniqueId(8)
|
||||
},
|
||||
};
|
||||
calendarApi.addEvent(evt, true);
|
||||
this.events.push(evt);
|
||||
this.allevents.push(evt);
|
||||
},
|
||||
|
||||
events: this.allevents,
|
||||
editable: true,
|
||||
droppable: true,
|
||||
// selectMirror: true,
|
||||
eventResizableFromStart: true,
|
||||
selectable: true,
|
||||
locale: frLocale,
|
||||
themeSystem: 'bootstrap',
|
||||
slotMinTime: '08:00:00',
|
||||
slotMaxTime: '20:00:00',
|
||||
eventMouseEnter: (mouseEnterInfo) => {
|
||||
|
||||
},
|
||||
eventDrop: (info) => {
|
||||
const evt = this.events.filter(e => e.id === info.event.id).pop();
|
||||
evt.start = info.event.start;
|
||||
evt.end = info.event.end;
|
||||
},
|
||||
eventResize: (info) => {
|
||||
const evt = this.events.filter(e => e.id === info.event.id).pop();
|
||||
const index = this.events.indexOf(evt);
|
||||
evt.start = info.event.start;
|
||||
evt.end = info.event.end;
|
||||
},
|
||||
eventClick: (info) => {
|
||||
const evt = this.events.filter(e => e.id === info.event.id).pop();
|
||||
if (evt != null){
|
||||
const index = this.events.indexOf(evt);
|
||||
if (index > -1) {
|
||||
this.events.splice(index, 1);
|
||||
}
|
||||
const index1 = this.allevents.indexOf(evt);
|
||||
if (index1 > -1) {
|
||||
this.allevents.splice(index1, 1);
|
||||
}
|
||||
info.event.remove();
|
||||
}
|
||||
|
||||
},
|
||||
validRange: {
|
||||
start: Date.now()
|
||||
}
|
||||
};
|
||||
|
||||
this.actRoute.paramMap.subscribe(params => {
|
||||
this.slugid = params.get('slugadminid');
|
||||
console.log(this.slugid);
|
||||
|
||||
if (this.slugid != null) {
|
||||
|
||||
this.pollService.getPollBySlugAdminId(this.slugid).subscribe(p => {
|
||||
if (p != null) {
|
||||
this.poll = p;
|
||||
} else {
|
||||
this.messageService.add(
|
||||
{
|
||||
severity: 'warn',
|
||||
summary: 'Un sondage avec cet identifiant n\'existe pas',
|
||||
detail: 'Le sondage n\'a pas été récupéré'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
nextPage(): void {
|
||||
|
||||
if (this.poll.title && this.poll.location && this.poll.description) {
|
||||
this.step = 1;
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
this.messageService.add(
|
||||
{
|
||||
severity: 'warn',
|
||||
summary: 'Données incomplètes',
|
||||
detail: 'Veuillez remplir les champs requis'
|
||||
}
|
||||
);
|
||||
|
||||
this.submitted = true;
|
||||
}
|
||||
|
||||
nextPage1(): void {
|
||||
console.log(this.poll.id);
|
||||
if (this.poll.id == null) {
|
||||
this.events.forEach(e => {
|
||||
this.poll.pollChoices.push({
|
||||
startDate: e.start as any,
|
||||
endDate: e.end as any,
|
||||
});
|
||||
});
|
||||
this.pollService.createPoll(this.poll).subscribe(p1 => {
|
||||
this.poll = p1;
|
||||
this.urlsondage = window.location.protocol + '//' + window.location.host + '/answer/' + p1.slug;
|
||||
this.urlsondageadmin = window.location.protocol + '//' + window.location.host + '/admin/' + p1.slugAdmin;
|
||||
this.urlsalon = p1.tlkURL;
|
||||
this.urlpad = p1.padURL;
|
||||
this.step = 2;
|
||||
});
|
||||
} else {
|
||||
|
||||
const toKeep: PollChoice[] = [];
|
||||
this.events.filter(c => c.extendedProps != null && c.extendedProps.choiceid != null).forEach(e => {
|
||||
toKeep.push(this.poll.pollChoices.filter(c1 => c1.id === e.extendedProps.choiceid)[0]);
|
||||
});
|
||||
this.poll.pollChoices = toKeep;
|
||||
this.poll.pollChoices.forEach(c => {
|
||||
const res = this.events.filter(c1 => c1.extendedProps != null &&
|
||||
c1.extendedProps.choiceid != null && c1.extendedProps.choiceid === c.id)[0];
|
||||
c.startDate = res.start as any;
|
||||
c.endDate = res.end as any;
|
||||
});
|
||||
|
||||
this.events.filter(c => c.extendedProps == null || c.extendedProps.choiceid == null).forEach(e => {
|
||||
this.poll.pollChoices.push({
|
||||
startDate: e.start as any,
|
||||
endDate: e.end as any,
|
||||
});
|
||||
});
|
||||
console.log(this.events);
|
||||
console.log(this.poll.pollChoices);
|
||||
|
||||
this.pollService.updtatePoll(this.poll).subscribe(p1 => {
|
||||
this.poll = p1;
|
||||
this.urlsondage = 'http://localhost:4200/answer/' + p1.slug;
|
||||
this.urlsondageadmin = 'http://localhost:4200/admin/' + p1.slugAdmin;
|
||||
this.urlsalon = p1.tlkURL;
|
||||
this.urlpad = p1.padURL;
|
||||
this.step = 2;
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
prevPage1(): void {
|
||||
|
||||
this.step = this.step - 1;
|
||||
}
|
||||
|
||||
|
||||
private getUniqueId(parts: number): string {
|
||||
const stringArr = [];
|
||||
for (let i = 0; i < parts; i++) {
|
||||
// tslint:disable-next-line:no-bitwise
|
||||
const S4 = (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
|
||||
stringArr.push(S4);
|
||||
}
|
||||
return stringArr.join('-');
|
||||
}
|
||||
|
||||
private getValidDate(): number {
|
||||
if (this.poll.id != null) {
|
||||
if ((this.poll.pollChoices[0].startDate as any - Date.now()) < 0) {
|
||||
return this.poll.pollChoices[0].startDate as any;
|
||||
}
|
||||
}
|
||||
return Date.now();
|
||||
|
||||
}
|
||||
|
||||
|
||||
getICS(): void {
|
||||
this.loadics = true;
|
||||
this.pollService.getICS(this.slugid, this.ics).subscribe(res => {
|
||||
this.loadics = false;
|
||||
|
||||
const calendarApi = this.calendarComponent.getApi();
|
||||
if (res.eventdtos.length > 0) {
|
||||
this.eventsfromics.forEach(eid => {
|
||||
const index = this.allevents.indexOf(eid);
|
||||
if (index > -1) {
|
||||
this.allevents.splice(index, 1);
|
||||
}
|
||||
calendarApi.getEventById(eid.id)?.remove();
|
||||
});
|
||||
this.eventsfromics = [];
|
||||
}
|
||||
console.log(res);
|
||||
|
||||
res.eventdtos.forEach(evtdto => { // calendarApi.next();
|
||||
const evt1 =
|
||||
{
|
||||
title: evtdto.description,
|
||||
start: evtdto.startDate,
|
||||
end: evtdto.endDate,
|
||||
resourceEditable: false,
|
||||
editable: false,
|
||||
droppable: false,
|
||||
selectable: false,
|
||||
eventResizableFromStart: false,
|
||||
id: this.getUniqueId(8),
|
||||
|
||||
backgroundColor: 'red',
|
||||
extendedProps: {
|
||||
fromics: true
|
||||
},
|
||||
|
||||
|
||||
};
|
||||
const eventAPI = calendarApi.addEvent(evt1, true);
|
||||
this.eventsfromics.push(evt1);
|
||||
this.allevents.push(evt1);
|
||||
|
||||
});
|
||||
|
||||
const unselected = this.events.map(ev => ev.extendedProps.choiceid);
|
||||
res.selectedChoices.forEach(e => {
|
||||
const index = unselected.indexOf(e);
|
||||
if (index > -1) {
|
||||
unselected.splice(index, 1);
|
||||
}
|
||||
const evt1 = this.events.filter(ev => ev.extendedProps.choiceid === e)[0];
|
||||
|
||||
const evt2 = calendarApi.getEventById(evt1.id);
|
||||
evt1.backgroundColor = 'red';
|
||||
evt1.extendedProps.selected = false;
|
||||
evt2.setProp('backgroundColor', 'red');
|
||||
// this.poll.pollChoices.filter(pc => pc.id === evt1.extendedProps.choiceid)[0].users.push({ id: -1 });
|
||||
});
|
||||
unselected.forEach(e => {
|
||||
const evt1 = this.events.filter(ev => ev.extendedProps.choiceid === e)[0];
|
||||
|
||||
const evt2 = calendarApi.getEventById(evt1.id);
|
||||
evt1.backgroundColor = 'green';
|
||||
evt1.extendedProps.selected = true;
|
||||
evt2.setProp('backgroundColor', 'green');
|
||||
this.poll.pollChoices.filter(pc => pc.id === evt1.extendedProps.choiceid)[0].users.push({ id: -1 });
|
||||
});
|
||||
}, (err) => {
|
||||
this.loadics = false;
|
||||
|
||||
this.messageService.add(
|
||||
{
|
||||
severity: 'warn',
|
||||
summary: 'Ne peut récupérer l\'agenda à partir de l\'adresse de l\'ics',
|
||||
detail: 'Une erreur s\'est produite au moment de la récupération de l\'agenda'
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { DateagoPipe } from './dateago.pipe';
|
||||
|
||||
describe('DateagoPipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new DateagoPipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
});
|
||||
41
ansible/files/doodlestudent/front/src/app/dateago.pipe.ts
Normal file
41
ansible/files/doodlestudent/front/src/app/dateago.pipe.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'dateago',
|
||||
pure: true
|
||||
|
||||
})
|
||||
export class DateagoPipe implements PipeTransform {
|
||||
|
||||
transform(value: any, args?: any): any {
|
||||
if (value) {
|
||||
const seconds = Math.floor((+new Date() - +new Date(value)) / 1000);
|
||||
if (seconds < 29) { // less than 30 seconds ago will show as 'Just now'
|
||||
return 'quelques secondes';
|
||||
}
|
||||
const intervals = {
|
||||
année: 31536000,
|
||||
mois: 2592000,
|
||||
semaine: 604800,
|
||||
jour: 86400,
|
||||
heure: 3600,
|
||||
minute: 60,
|
||||
seconde: 1
|
||||
};
|
||||
let counter;
|
||||
// tslint:disable-next-line:forin
|
||||
for (const i in intervals) {
|
||||
counter = Math.floor(seconds / intervals[i]);
|
||||
if (counter > 0){
|
||||
if (counter === 1 || i === 'mois') {
|
||||
return counter + ' ' + i + ''; // singular (1 day ago)
|
||||
} else {
|
||||
return counter + ' ' + i + 's'; // plural (2 days ago)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
|
||||
export class Card {
|
||||
constructor(public image: string, public style: any, public title: string, public subtitle: string){
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
.Container {
|
||||
max-width: 800px;
|
||||
margin: 2rem auto;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.Home_Container {
|
||||
width: 1024px;
|
||||
margin: 0 auto;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.Home_Wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.Home_Logo {
|
||||
margin-bottom: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.Home_Button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.Home_CreateLink {
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
background-color: #43dbac;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
font-size: 1.3rem;
|
||||
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.10);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="Home_Container">
|
||||
<div class="Home_Wrapper">
|
||||
<div class="Home_Logo">
|
||||
<img src="../../assets/Logo.png" alt="Logo Simba" height="130px"/>
|
||||
</div>
|
||||
<app-card-small-component [cards]="cards"></app-card-small-component>
|
||||
<div class="Home_Button">
|
||||
<a routerLink="/create" class="Home_CreateLink">Créer votre poll !</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HomeComponentComponent } from './home-component.component';
|
||||
|
||||
describe('HomeComponentComponent', () => {
|
||||
let component: HomeComponentComponent;
|
||||
let fixture: ComponentFixture<HomeComponentComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ HomeComponentComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(HomeComponentComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CardSmallComponentComponent } from '../card-small-component/card-small-component.component';
|
||||
import {Card} from './Card';
|
||||
@Component({
|
||||
selector: 'app-home-component',
|
||||
templateUrl: './home-component.component.html',
|
||||
styleUrls: ['./home-component.component.css'],
|
||||
providers: [CardSmallComponentComponent]
|
||||
|
||||
})
|
||||
export class HomeComponentComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
|
||||
cards: Card[] = [];
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.cards.push(new Card('assets/1.png', {backgroundColor: '#44baf2', color: 'white'}, 'Créez un sondage', 'Définissez plusieurs créneaux pour votre réunion.'));
|
||||
this.cards.push(new Card('assets/2.png', {backgroundColor: '#fc506d', color: 'white'}, 'Envoyez vos invitations', 'Les participants aux sondages pourront voter pour les dates qui leur conviennent le mieux !'));
|
||||
this.cards.push(new Card('assets/3.png', {backgroundColor: '#8f3ee8', color: 'white'}, 'Faites votre choix', 'Vous pourrez obtenir en direct les résultats du sondage afin de choisir au mieux la meilleure proposition.'));
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header text-center justify-content-center">
|
||||
<h4 class="modal-title" id="modal-basic-title">Le sondage est maintenant clos</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="d-flex justify-content-center">
|
||||
La date retenue pour le soundage est le:
|
||||
</div>
|
||||
<BR>
|
||||
<div class="d-flex justify-content-center">
|
||||
<b>{{poll?.selectedChoice?.startDate | date:'EEEE d LLLL': 'CEST':'fr'}}</b>
|
||||
</div>
|
||||
<div class="d-flex justify-content-center">
|
||||
<b> de {{poll?.selectedChoice?.startDate | date:'H:mm'}}</b>
|
||||
</div>
|
||||
<div class="d-flex justify-content-center">
|
||||
<b> - </b>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-center">
|
||||
<b> {{poll?.selectedChoice?.endDate | date:'H:mm'}}</b>
|
||||
</div>
|
||||
<BR>
|
||||
Le lieu sera {{poll?.location}}
|
||||
<BR>
|
||||
<div *ngIf="poll?.padURL">
|
||||
Un pad est ouvert <a [href]="poll?.padURL" target="_blank">ici</a>
|
||||
</div>
|
||||
<BR>
|
||||
<div *ngIf="poll?.tlkURL">
|
||||
Un salon de discussion est ouvert <a [href]="poll?.tlkURL" target="_blank">ici</a>
|
||||
</div>
|
||||
<BR>
|
||||
<div *ngIf="poll?.has_meal">Un repas est prévu pour ce meeting.
|
||||
</div>
|
||||
<BR>
|
||||
<div *ngIf="poll?.description">
|
||||
L'ordre du jour est le suivant:
|
||||
{{poll.description}}
|
||||
</div>
|
||||
<BR>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-dark" (click)="dismissModalAndNavigate()">Retour</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ModalPollClosComponent } from './modal-poll-clos.component';
|
||||
|
||||
describe('ModalPollClosComponent', () => {
|
||||
let component: ModalPollClosComponent;
|
||||
let fixture: ComponentFixture<ModalPollClosComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ModalPollClosComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ModalPollClosComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Poll } from '../model/model';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-modal-poll-clos',
|
||||
templateUrl: './modal-poll-clos.component.html',
|
||||
styleUrls: ['./modal-poll-clos.component.css'],
|
||||
providers: [NgbActiveModal]
|
||||
})
|
||||
export class ModalPollClosComponent implements OnInit {
|
||||
|
||||
@Input() poll: Poll;
|
||||
constructor(public activeModal: NgbActiveModal, public router: Router) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
dismissModalAndNavigate(): void{
|
||||
this.activeModal.close();
|
||||
window.location.href = '/';
|
||||
|
||||
}
|
||||
}
|
||||
57
ansible/files/doodlestudent/front/src/app/model/model.ts
Normal file
57
ansible/files/doodlestudent/front/src/app/model/model.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
export interface Poll {
|
||||
createdAt?: Date;
|
||||
description?: string;
|
||||
has_meal?: boolean;
|
||||
id?: number;
|
||||
location?: string;
|
||||
padURL?: string;
|
||||
pollChoices?: PollChoice[];
|
||||
selectedChoice ?: PollChoice;
|
||||
pollComments?: PollCommentElement[];
|
||||
pollMealPreferences?: PollCommentElement[];
|
||||
slug?: string;
|
||||
slugAdmin?: string;
|
||||
title?: string;
|
||||
tlkURL?: string;
|
||||
updatedAt?: Date;
|
||||
clos ?: boolean;
|
||||
}
|
||||
|
||||
export interface PollChoice {
|
||||
endDate?: Date;
|
||||
id?: number;
|
||||
startDate?: Date;
|
||||
users?: User[];
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id?: number;
|
||||
username?: string;
|
||||
mail?: string;
|
||||
}
|
||||
|
||||
export interface ChoiceUser {
|
||||
username?: string;
|
||||
mail?: string;
|
||||
pref?: string;
|
||||
ics?: string;
|
||||
choices?: number[];
|
||||
}
|
||||
|
||||
export interface PollCommentElement {
|
||||
content?: string;
|
||||
id?: number;
|
||||
auteur?: string;
|
||||
}
|
||||
|
||||
export interface EventDTO{
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
|
||||
export interface EventDTOAndSelectedChoice {
|
||||
eventdtos?: EventDTO[];
|
||||
selectedChoices?: number[];
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user