68 Commits
Etape2 ... main

Author SHA1 Message Date
tuanvu
6335b425f7 install docker without sudo 2026-02-16 18:57:38 +01:00
tuanvu
c198419f58 install docker 2026-02-16 18:55:18 +01:00
tuanvu
39ae214f78 3rd can use shell? 2026-02-16 18:51:50 +01:00
tuanvu
28d90e66dd rebuild 2026-02-16 18:15:14 +01:00
tuanvu
d1fa4044e1 change docker build 2026-02-16 17:45:39 +01:00
tuanvu
181eb1ae4a test4 2026-02-16 17:38:10 +01:00
tuanvu
9d15c7dfa3 retest 3rd one 2026-02-16 17:11:27 +01:00
tuanvu
2d1fb8759a add tag 2026-02-16 17:04:43 +01:00
tuanvu
df45862a27 3rd ones 2026-02-16 17:01:30 +01:00
tuanvu
3670516270 3rd ones 2026-02-16 16:58:11 +01:00
tuanvu
16a0145a4f add git 2026-02-16 16:54:54 +01:00
tuanvu
62385a9bf6 git recursive? 2026-02-16 16:53:55 +01:00
tuanvu
320bd0974e update before? 2026-02-16 16:51:32 +01:00
tuanvu
882efd42bf install git 2026-02-16 16:50:40 +01:00
tuanvu
3eee533f66 install git 2026-02-16 16:49:30 +01:00
tuanvu
51251e2390 remove ssh 2026-02-16 16:48:44 +01:00
tuanvu
4485ce3f04 test runner 2026-02-16 16:06:49 +01:00
Alexis Leboeuf
f0b852d5cd Re-added gitmodules as it was deleted 2026-02-16 15:56:30 +01:00
tuanvu
466b82d174 use heodic maybe? 2026-02-16 15:54:46 +01:00
tuanvu
247f2eae1c maybe 2nd will pass? 2026-02-16 15:53:18 +01:00
tuanvu
8680677903 maybe 2nd will pass? 2026-02-16 15:51:20 +01:00
tuanvu
24eae0d590 TLS encore 2026-02-16 15:50:15 +01:00
tuanvu
ef6a11dd35 test tache2 2026-02-16 15:47:33 +01:00
tuanvu
12aeb5573e debian for check_app 2026-02-16 15:46:53 +01:00
tuanvu
65db761a81 debian for check_app 2026-02-16 15:46:34 +01:00
tuanvu
ecc6bcb233 git for check_app 2026-02-16 15:46:04 +01:00
tuanvu
c55d5e51d7 alpine again for check_app 2026-02-16 15:44:47 +01:00
tuanvu
85964ad5e7 change to 3.23 2026-02-16 15:43:33 +01:00
tuanvu
14839ad79b add node alpine 2026-02-16 15:41:14 +01:00
tuanvu
ae97e3317b add nodejs 2026-02-16 15:40:07 +01:00
tuanvu
539d3499a8 test 2026-02-16 15:37:57 +01:00
tuanvu
c4f8bc3a0c test 2026-02-16 15:36:32 +01:00
tuanvu
fce18a4da1 remove subfolder 2026-02-16 15:35:01 +01:00
tuanvu
29faddbe2b retry debian for first task 2026-02-16 15:31:27 +01:00
Alexis Leboeuf
110033155f test2 2026-02-16 15:26:18 +01:00
Alexis Leboeuf
3f809eacfc test 2026-02-16 15:23:09 +01:00
Alexis Leboeuf
b9cbb24bf1 Added debug 2026-02-16 15:18:36 +01:00
Alexis Leboeuf
a57233ba31 Added update before to test 2026-02-16 15:16:26 +01:00
Alexis Leboeuf
27b8229b99 Test 2026-02-16 15:10:39 +01:00
tuanvu
f4704af0ec add env 2026-02-16 15:00:01 +01:00
tuanvu
3725de1d31 add job build-frontend 2026-02-16 14:57:22 +01:00
tuanvu
56da48d1b0 return to alpine 2026-02-16 14:55:01 +01:00
tuanvu
4ddd2cfb37 test sudo 2026-02-16 14:53:32 +01:00
tuanvu
2034192e2f change alpine to debian 2026-02-16 14:39:36 +01:00
tuanvu
a168cd1d88 add gitlab-ci 2026-02-16 14:37:23 +01:00
Alexandre Chevalier
781b9972f3 ajout readme 2026-02-15 14:14:32 +01:00
Alexandre Chevalier
0acb6ee271 grafana fonctionne dans le compose 2026-02-15 13:59:48 +01:00
Alexandre Chevalier
bf241435be prometheus et munin fonctionnent dans le compose 2026-02-14 20:20:17 +01:00
Alexandre Chevalier
d6f422ea17 set version des images 2026-02-14 19:03:45 +01:00
Alexandre Chevalier
204c890eeb image du service back recup depuis dockerhub 2026-02-14 16:59:52 +01:00
Alexandre Chevalier
51aeb014f4 deplacement fichiers grafana et prometheus 2026-02-14 11:59:16 +01:00
Alexandre Chevalier
1d8792b6e0 fix suppr creation dossier docker sur ansible 2026-02-13 23:41:01 +01:00
Alexandre Chevalier
6c08c063e1 Merge remote-tracking branch 'origin/Tache3' into Tache3
# Conflicts:
#	ansible/playbook.yml
2026-02-13 23:38:17 +01:00
Alexandre Chevalier
2291a85b79 deploiement sur VM 2026-02-13 23:37:08 +01:00
tuanvu
0976663ea6 add 127.0.0.1 to munin node to see it works or not 2026-02-13 21:26:13 +01:00
tuanvu
d7ac18bb62 Merge branch 'aventure2' into Tache3 2026-02-13 20:06:07 +01:00
tuanvu
eb4d3caa8d change grafana to dockerfile to add munin after 2026-02-13 20:04:15 +01:00
tuanvu
94c9a56a6a add prometheus and grafana but still have to find templates dashboard for prometheus and munin 2026-02-13 18:07:33 +01:00
Alexandre Chevalier
e37f99144b suppr fichiers inutiles 2026-02-13 16:55:57 +01:00
Alexandre Chevalier
fda23a778e deplacement dans ansible des fichiers docker 2026-02-13 16:48:19 +01:00
tuanvu
7fe633c476 add certbot but have to re-see because need to specify domain and email 2026-02-13 16:04:25 +01:00
trochas
180fd75947 remove nginxFront 2026-02-12 18:08:15 +01:00
Alexandre Chevalier
a0e07036c1 Revert "suppr nginx.conf"
This reverts commit 8141dcee8c.
2026-02-12 18:05:25 +01:00
Alexandre Chevalier
8141dcee8c suppr nginx.conf 2026-02-12 17:35:31 +01:00
Alexandre Chevalier
4490d222c7 front depend des autres conteneurs 2026-02-12 17:32:09 +01:00
trochas
004e414802 ajout gestion erreur nginx 2026-02-12 17:28:38 +01:00
trochas
9f820449af fix double serveur nginx -> tout tourne sur le nginx du front 2026-02-12 17:22:11 +01:00
tuanvu
8bc54e787b fix the nginx and stmp but re-see listen :80 because it's repeated 2026-02-12 16:27:43 +01:00
152 changed files with 322881 additions and 695 deletions

113
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,113 @@
stages:
- check
- build
- package
- deploy
variables:
DOCKER_REGISTRY: "docker.io"
IMAGE_BACKEND: "docker.io/benarbause/doodleback-with-quarkus:latest"
IMAGE_FRONTEND: "docker.io/vuminh224/doodle-frontend:latest"
APP_REPO: "https://github.com/barais/doodlestudent.git"
SUBMODULE_PATH: "doodlestudent"
check_app_update:
stage: check
image: debian:latest
tags:
- docker
script:
- apt-get update
- apt-get install -y git
# Récupère le dernier commit distant
- LATEST_REMOTE=$(git ls-remote $APP_REPO HEAD | awk '{print $1}')
# Récupère le commit actuel du submodule
- CURRENT_SUBMODULE=$(git rev-parse HEAD:$SUBMODULE_PATH || echo "none")
# Compare
- |
if [ "$LATEST_REMOTE" != "$CURRENT_SUBMODULE" ]; then
echo "NEW_COMMIT=true" >> variables.env
else
echo "NEW_COMMIT=false" >> variables.env
fi
artifacts:
paths:
- variables.env
# Build Frontend (Angular)
build_frontend:
stage: build
image: node:current-alpine3.23
tags:
- docker
dependencies:
- check_app_update
script:
- apk add --no-cache git
- git submodule update --init --recursive
- source variables.env || true
- if [ "$NEW_COMMIT" != "true" ]; then
echo "No new commit, skipping build";
exit 0;
fi
- ls -la doodlestudent/front #test
- cd doodlestudent/front
- npm ci
- npm run build --prod
artifacts:
paths:
- doodlestudent/front/dist/
# Build & Push Docker frontend
build_docker_frontend:
stage: package
tags:
- shell
dependencies:
- check_app_update
- build_frontend
script:
- apt update
- apt install -y docker.io
- source variables.env || true
- if [ "$NEW_COMMIT" != "true" ]; then
echo "No new commit, skipping build";
exit 0;
fi
- echo "$DOCKERHUB_TOKEN" | docker login -u "$DOCKERHUB_USER" --password-stdin
- docker build -t "$IMAGE_FRONTEND" doodlestudent/front
- docker push "$IMAGE_FRONTEND"
- docker logout
# Deploy to Production
deploy_production:
stage: deploy
image: alpine:latest
dependencies:
- build_docker_frontend
- check_app_update
tags:
- docker
before_script:
- apk add --no-cache openssh-client docker-cli
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan -H "$DEPLOY_SERVER" >> ~/.ssh/known_hosts 2>/dev/null || true
script:
- source variables.env || true
- if [ "$NEW_COMMIT" != "true" ]; then
echo "No new commit, skipping build";
exit 0;
fi
- |
ssh "$DEPLOY_USER@$DEPLOY_SERVER" << 'EOF'
cd /app
docker compose pull
docker compose up -d
EOF
environment:
name: production
url: http://148.60.11.130
when: manual

376
Consignes.md Normal file
View 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 &#40;https://vm.istic.univ-rennes1.fr/ séléctionner **ubuntu20** comme image de base&#41; et demandez l'accès externe vers le port *80* &#40;http&#41; et *443* &#40;https&#41; de votre machine virtuelle par le [helpdesk, catégorie ISTIC-ESIR - Tous problèmes informatiques]&#40;https://assistance.univ-rennes1.fr/&#41;, l'accès au port 22 se fera au travers du [VPN]&#40;https://istic.univ-rennes1.fr/intranet/services#section-4&#41;. Partager moi l'adresse IP de la machine et le sous domaine souhaité [ici]&#40;https://forms.gle/a7EfKraasGpTYAXa6&#41; &#40;sous domaine de *diverse-team.fr*&#41;. &#40;Je mets jour quand c'est fait le [google doc joint]&#40;https://docs.google.com/spreadsheets/d/14Yv-3ujq9OL0HuYkj4Wlp9Piw5075b6iEKSf-yJWytc/edit?usp=sharing&#41;&#41;)
- **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
View File

@@ -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 &#40;https://vm.istic.univ-rennes1.fr/ séléctionner **ubuntu20** comme image de base&#41; et demandez l'accès externe vers le port *80* &#40;http&#41; et *443* &#40;https&#41; de votre machine virtuelle par le [helpdesk, catégorie ISTIC-ESIR - Tous problèmes informatiques]&#40;https://assistance.univ-rennes1.fr/&#41;, l'accès au port 22 se fera au travers du [VPN]&#40;https://istic.univ-rennes1.fr/intranet/services#section-4&#41;. Partager moi l'adresse IP de la machine et le sous domaine souhaité [ici]&#40;https://forms.gle/a7EfKraasGpTYAXa6&#41; &#40;sous domaine de *diverse-team.fr*&#41;. &#40;Je mets jour quand c'est fait le [google doc joint]&#40;https://docs.google.com/spreadsheets/d/14Yv-3ujq9OL0HuYkj4Wlp9Piw5075b6iEKSf-yJWytc/edit?usp=sharing&#41;&#41;)
- **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.

3
ansible/files/.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "doodlestudent"]
path = doodlestudent
url = https://github.com/barais/doodlestudent.git

View 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

View File

@@ -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

View File

@@ -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

View 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)

View 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).

View File

@@ -0,0 +1 @@

View 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

View 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

View File

@@ -0,0 +1 @@
19d89ca52bc0fa4f19d6325464d9d7a032649b9fa68c111514627081e2784b4a

View 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 its 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

View 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
View 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
View 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%

View 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>

View File

@@ -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" ]

View File

@@ -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"]

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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();
}
}

View File

@@ -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> {
}

View File

@@ -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;
}
}

View File

@@ -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 + '\'' +
'}';
}
}

View File

@@ -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 +
'}';
}
}

View File

@@ -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;
}
}

View File

@@ -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 + '\'' +
'}';
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}*/
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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()); } }
*/
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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 */;

View File

@@ -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

View File

@@ -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
}
]

View 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.

View 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

View 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

View 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).

View 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

View 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"
}

View 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
}
}));
}
};

View 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));
});
});

View 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>;
}
}

View 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"
]
}
}

View 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
});
};

File diff suppressed because it is too large Load Diff

View 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"
}
}

View File

@@ -0,0 +1,8 @@
{
"/api/*": {
"target": "http://localhost:8080",
"secure": false,
"logLevel": "debug",
"changeOrigin": true
}
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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();
});
});

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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 -->

View File

@@ -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();
});
});

View File

@@ -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('-');
}
}

View File

@@ -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 { }

View 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);
}

View File

@@ -0,0 +1 @@
<router-outlet></router-outlet>

View File

@@ -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!');
});
});

View 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 {
}

View 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 { }

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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();
});
});

View File

@@ -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 {
}
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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();
});
});

View File

@@ -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'
}
);
}
);
}
}

View File

@@ -0,0 +1,8 @@
import { DateagoPipe } from './dateago.pipe';
describe('DateagoPipe', () => {
it('create an instance', () => {
const pipe = new DateagoPipe();
expect(pipe).toBeTruthy();
});
});

View 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;
}
}

View File

@@ -0,0 +1,7 @@
export class Card {
constructor(public image: string, public style: any, public title: string, public subtitle: string){
}
}

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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();
});
});

View File

@@ -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.'));
}
}

View File

@@ -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>

View File

@@ -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();
});
});

View File

@@ -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 = '/';
}
}

Some files were not shown because too many files have changed in this diff Show More