From b89d8b63a207a51843f06de107b9c8a982603977 Mon Sep 17 00:00:00 2001 From: Florent Chehab Date: Sun, 16 Jun 2019 18:10:15 +0200 Subject: [PATCH] feat(rgpd/cgu): everything is ready * Full handling of RGPD/CGU validation * management command to clean user personnal data created * Added RGPD and CGU as markdown in the documentation (also used in the front) Front: * Multiple entrypoints for the different needs * Added raw file loader * bumped frontend image version * More generic front template (for the multiple entries) * Added NotFound Component * More generic Markdown component (in regards to handling off-the-api situations and heading size) Closes #67 --- .gitlab-ci.yml | 10 +- backend/backend_app/tests/utils.py | 3 +- backend/backend_app/viewsets.py | 36 +++- backend/base_app/forms.py | 2 +- backend/base_app/management/__init__.py | 0 .../base_app/management/commands/__init__.py | 0 .../commands/clean_user_accounts.py | 54 +++++ backend/base_app/middleware.py | 6 +- .../migrations/0003_auto_20190616_1754.py | 27 +++ backend/base_app/models.py | 14 +- backend/base_app/settings/main.py | 3 +- backend/base_app/templates/base.html | 41 ++++ backend/base_app/templates/cgu.html | 27 --- backend/base_app/templates/cgu_rgpd.html | 25 +++ backend/base_app/templates/index.html | 43 +--- backend/base_app/templates/rgpd_raw.html | 9 + .../tests/test_need_validate_cgu_rgpd.py | 8 +- .../base_app/tests/test_rgpd_raw_public.py | 13 ++ backend/base_app/urls.py | 3 +- backend/base_app/views.py | 22 +- docker-compose.yml | 2 +- documentation/Other/cgu.md | 87 ++++++++ documentation/Other/rgpd.md | 57 +++++ frontend/package-lock.json | 10 + frontend/package.json | 1 + frontend/src/components/app/App.js | 25 ++- frontend/src/components/app/BaseTemplate.js | 61 ++++++ frontend/src/components/app/Logo.js | 86 ++++++++ .../app/{AppFrame.js => MainAppFrame.js} | 148 ++++--------- frontend/src/components/app/menuItems.js | 3 +- .../src/components/common/ThemeProvider.js | 106 --------- .../{Markdown.js => markdown/BaseMarkdown.js} | 201 ++++++++---------- .../components/common/markdown/Markdown.js | 47 ++++ .../{ => markdown}/TruncatedMarkdown.js | 0 .../common/theme/OfflineThemeProvider.js | 24 +++ .../components/common/theme/ThemeProvider.js | 56 +++++ frontend/src/components/common/theme/utils.js | 55 +++++ .../components/form/fields/MarkdownField.js | 2 +- frontend/src/components/pages/FormRgpdCgu.js | 148 +++++++++++++ .../components/pages/PageAboutConditions.js | 35 --- .../src/components/pages/PageAboutProject.js | 2 +- frontend/src/components/pages/PageHome.js | 6 +- frontend/src/components/pages/PageNotFound.js | 24 +++ frontend/src/components/pages/PageUser.js | 8 + frontend/src/components/pages/PagesRgpdCgu.js | 23 ++ .../recommendation/SelectListSubPage.js | 2 +- .../recommendation/view/TextBlock.js | 2 +- .../components/settings/theme/ColorDemo.js | 8 +- .../components/settings/theme/ColorTools.js | 2 +- .../university/modules/CountryDri.js | 2 +- .../university/modules/UniversityDri.js | 2 +- .../modules/UniversitySemestersDates.js | 2 +- .../university/modules/common/Scholarship.js | 2 +- .../CoursesComments.js | 2 +- .../PreviousDeparture.js | 2 +- frontend/src/components/user/DeleteAccount.js | 98 +++++++++ frontend/src/components/user/UserInfo.js | 38 +++- frontend/src/config/appRoutes.js | 6 +- frontend/src/config/other.js | 6 + frontend/src/{index.js => entry/mainApp.js} | 6 +- frontend/src/entry/rgpdCguForm.js | 39 ++++ frontend/src/entry/rgpdRaw.js | 39 ++++ frontend/webpack.config.base.js | 14 +- frontend/webpack.config.dev.js | 10 +- server/docker-compose.prod.yml | 2 +- 65 files changed, 1350 insertions(+), 497 deletions(-) create mode 100644 backend/base_app/management/__init__.py create mode 100644 backend/base_app/management/commands/__init__.py create mode 100644 backend/base_app/management/commands/clean_user_accounts.py create mode 100644 backend/base_app/migrations/0003_auto_20190616_1754.py create mode 100644 backend/base_app/templates/base.html delete mode 100644 backend/base_app/templates/cgu.html create mode 100644 backend/base_app/templates/cgu_rgpd.html create mode 100644 backend/base_app/templates/rgpd_raw.html create mode 100644 backend/base_app/tests/test_rgpd_raw_public.py create mode 100644 documentation/Other/cgu.md create mode 100644 documentation/Other/rgpd.md create mode 100644 frontend/src/components/app/BaseTemplate.js create mode 100644 frontend/src/components/app/Logo.js rename frontend/src/components/app/{AppFrame.js => MainAppFrame.js} (60%) delete mode 100644 frontend/src/components/common/ThemeProvider.js rename frontend/src/components/common/{Markdown.js => markdown/BaseMarkdown.js} (50%) create mode 100644 frontend/src/components/common/markdown/Markdown.js rename frontend/src/components/common/{ => markdown}/TruncatedMarkdown.js (100%) create mode 100644 frontend/src/components/common/theme/OfflineThemeProvider.js create mode 100644 frontend/src/components/common/theme/ThemeProvider.js create mode 100644 frontend/src/components/common/theme/utils.js create mode 100644 frontend/src/components/pages/FormRgpdCgu.js delete mode 100644 frontend/src/components/pages/PageAboutConditions.js create mode 100644 frontend/src/components/pages/PageNotFound.js create mode 100644 frontend/src/components/pages/PagesRgpdCgu.js create mode 100644 frontend/src/components/user/DeleteAccount.js create mode 100644 frontend/src/config/other.js rename frontend/src/{index.js => entry/mainApp.js} (89%) create mode 100644 frontend/src/entry/rgpdCguForm.js create mode 100644 frontend/src/entry/rgpdRaw.js diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2d6e3347..f3d25469 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -34,11 +34,15 @@ check_back: check_front: <<: *only-default stage: check - image: registry.gitlab.utc.fr/rex-dri/rex-dri/frontend:v0.4.0 + image: registry.gitlab.utc.fr/rex-dri/rex-dri/frontend:v0.5.0 before_script: - cd frontend && cp -R /usr/src/deps/node_modules . script: - npm run build + artifacts: + paths: + - frontend/webpack-stats.json + expire_in: 1 hour tags: - docker @@ -70,7 +74,7 @@ test_back: test_frontend: <<: *only-default stage: test - image: registry.gitlab.utc.fr/rex-dri/rex-dri/frontend:v0.4.0 + image: registry.gitlab.utc.fr/rex-dri/rex-dri/frontend:v0.5.0 before_script: - cd frontend && cp -R /usr/src/deps/node_modules . script: @@ -90,7 +94,7 @@ flake8: eslint: <<: *only-default stage: lint - image: registry.gitlab.utc.fr/rex-dri/rex-dri/frontend:v0.4.0 + image: registry.gitlab.utc.fr/rex-dri/rex-dri/frontend:v0.5.0 before_script: - cd frontend && cp -R /usr/src/deps/node_modules . script: diff --git a/backend/backend_app/tests/utils.py b/backend/backend_app/tests/utils.py index e129a592..c39f0d59 100644 --- a/backend/backend_app/tests/utils.py +++ b/backend/backend_app/tests/utils.py @@ -72,8 +72,7 @@ class WithUserTestCase(TestCase): cls.authenticated_user, cls.authenticated_user_2, ]: - user.has_validated_cgu = True - user.has_validated_rgpd = True + user.has_validated_cgu_rgpd = True user.save() cls.unauthenticated_client = APIClient() diff --git a/backend/backend_app/viewsets.py b/backend/backend_app/viewsets.py index 3f192dda..507adc1e 100644 --- a/backend/backend_app/viewsets.py +++ b/backend/backend_app/viewsets.py @@ -167,6 +167,37 @@ class BannedUserViewSet(ViewSet): return Response(status=200) +class DeleteUserViewSet(ViewSet): + """ + Viewset to handle account deletion + """ + + end_point_route = "emptyUserAccount" + permission_classes = tuple() + + def create(self, request): + """ + Line up the user from the request for deletion + """ + user = request.user + user.delete_next_time = True + user.save() + return Response(status=201) + + def update(self, request): + # Here only to have the correct routes automatically generated, not to be used. + return Response(status=403) + + def delete(self, request, pk="osef"): # don't delete this unused argument! + """ + Un-Line up the user from the request for deletion + """ + user = request.user + user.delete_next_time = False + user.save() + return Response(status=200) + + class RecommendationListChangeFollowerViewSet(ViewSet): """ Viewset to be able to add or delete followers on @@ -184,7 +215,7 @@ class RecommendationListChangeFollowerViewSet(ViewSet): recommendation = RecommendationList.objects.get(pk=pk) if recommendation.is_public: - recommendation.followers.add(self.request.user) + recommendation.followers.add(request.user) recommendation.save() return Response(status=200) else: @@ -195,7 +226,7 @@ class RecommendationListChangeFollowerViewSet(ViewSet): return Response(status=403) # can delete folower even if list not public recommendation = RecommendationList.objects.get(pk=pk) - recommendation.followers.remove(self.request.user) + recommendation.followers.remove(request.user) recommendation.save() return Response(status=200) @@ -205,6 +236,7 @@ ALL_API_VIEW_VIEWSETS = [ LogFrontendErrorsViewSet, BannedUserViewSet, RecommendationListChangeFollowerViewSet, + DeleteUserViewSet, ] ALL_VIEWSETS = ALL_API_VIEWSETS + ALL_API_VIEW_VIEWSETS diff --git a/backend/base_app/forms.py b/backend/base_app/forms.py index 3fe3261b..e795db2a 100644 --- a/backend/base_app/forms.py +++ b/backend/base_app/forms.py @@ -5,4 +5,4 @@ from base_app.models import User class UserForm(forms.ModelForm): class Meta: model = User - fields = ("has_validated_rgpd", "has_validated_cgu") + fields = ("has_validated_cgu_rgpd",) diff --git a/backend/base_app/management/__init__.py b/backend/base_app/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/base_app/management/commands/__init__.py b/backend/base_app/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/base_app/management/commands/clean_user_accounts.py b/backend/base_app/management/commands/clean_user_accounts.py new file mode 100644 index 00000000..527f3b51 --- /dev/null +++ b/backend/base_app/management/commands/clean_user_accounts.py @@ -0,0 +1,54 @@ +import logging + +from django.contrib.sessions.management.commands.clearsessions import ( + Command as ClearSessionCommand, +) +from django.core.management.base import BaseCommand +from django_cas_ng.models import ProxyGrantingTicket, SessionTicket + +from backend_app.models.recommendationList import RecommendationList +from backend_app.models.userData import UserData +from backend_app.utils import get_default_theme_settings +from base_app.models import User + +logger = logging.getLogger("django") + + +class Command(BaseCommand): + help = "Command to handle user accounts emptying" + + def handle(self, *args, **options): + logger.info("here") + for user in User.objects.filter(delete_next_time=True): + logger.info("Emptying account of user {}".format(user.pk)) + + # for user Data we don't delete it but restore it to default + # We do this to be consistent with the assumption that each user has some userData + user_data = UserData.objects.get(pk=user.pk) + user_data.theme = get_default_theme_settings() + user_data.save() + + # deleting all owned private lists + RecommendationList.objects.filter(owner=user, is_public=False).delete() + + # Emptying user model + user.allow_sharing_personal_info = False + user.secondary_email = "" + user.pseudo = "Del." + user.has_validated_cgu_rgpd = False + user.is_banned = False + + user.username = "__Deleted__{}".format(user.pk) + user.email = "" + user.is_active = False + + user.delete_next_time = False + user.is_deleted = True + + user.save() + + # Finally we clear the sessions + ClearSessionCommand().handle() + ProxyGrantingTicket.clean_deleted_sessions() + SessionTicket.clean_deleted_sessions() + # Still error 500 on delete then reconnect... Unknown diff --git a/backend/base_app/middleware.py b/backend/base_app/middleware.py index acd0816f..2f986dc1 100644 --- a/backend/base_app/middleware.py +++ b/backend/base_app/middleware.py @@ -53,9 +53,9 @@ class RexDriRequestMiddleware(MiddlewareMixin): else: # User is authenticated # We check if he / she has validated the CGU and the full RGPD - if not (request.user.has_validated_cgu and request.user.has_validated_rgpd): - if not path.startswith("cgu"): - return HttpResponseRedirect("/cgu/?next={}".format(full_path)) + if not request.user.has_validated_cgu_rgpd: + if not path.startswith("cgu-rgpd"): + return HttpResponseRedirect("/cgu-rgpd/?next={}".format(full_path)) # Handling of banned users elif request.user.is_banned and full_path != "/banned_note/": return HttpResponseRedirect("/banned_note/") diff --git a/backend/base_app/migrations/0003_auto_20190616_1754.py b/backend/base_app/migrations/0003_auto_20190616_1754.py new file mode 100644 index 00000000..e5c5ed1c --- /dev/null +++ b/backend/base_app/migrations/0003_auto_20190616_1754.py @@ -0,0 +1,27 @@ +# Generated by Django 2.1.7 on 2019-06-16 15:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("base_app", "0002_auto_20190531_2007")] + + operations = [ + migrations.RenameField( + model_name="user", + old_name="has_validated_cgu", + new_name="has_validated_cgu_rgpd", + ), + migrations.RemoveField(model_name="user", name="has_validated_rgpd"), + migrations.AddField( + model_name="user", + name="delete_next_time", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="user", + name="is_deleted", + field=models.BooleanField(default=False), + ), + ] diff --git a/backend/base_app/models.py b/backend/base_app/models.py index 6492a78e..c2feb1a0 100644 --- a/backend/base_app/models.py +++ b/backend/base_app/models.py @@ -28,6 +28,10 @@ def validate(user, allow_sharing_personal_info): class User(AbstractUser): + """ + If you modify this model, don't forget to modify the command that handles its emptying for RGPD + """ + @cached_property def cached_groups(self) -> List[str]: out = ["authenticated_user"] @@ -40,10 +44,15 @@ class User(AbstractUser): pseudo = models.CharField( blank=False, null=False, max_length=12, default="Anonymous42" ) - has_validated_cgu = models.BooleanField(default=False, null=False) - has_validated_rgpd = models.BooleanField(default=False, null=False) + has_validated_cgu_rgpd = models.BooleanField(default=False, null=False) is_banned = models.BooleanField(default=False, null=False) + # Handling of account deletion + delete_next_time = models.BooleanField( + default=False, null=False + ) # if true, the account will be deleted at midnight + is_deleted = models.BooleanField(default=False, null=False) + def save(self, *args, **kwargs): """ Custom save function to ensure consistency. @@ -75,6 +84,7 @@ class UserSerializer(BaseModelSerializer): "allow_sharing_personal_info", "secondary_email", "pseudo", + "delete_next_time", "is_staff", ) read_only_fields = ("username", "first_name", "last_name", "email") diff --git a/backend/base_app/settings/main.py b/backend/base_app/settings/main.py index 16c4a91d..50e69a5a 100644 --- a/backend/base_app/settings/main.py +++ b/backend/base_app/settings/main.py @@ -179,7 +179,8 @@ AUTH_PASSWORD_VALIDATORS = [ ] LOGIN_URL = "/user/login" -LOGIN_EXEMPT_URLS = [LOGIN_URL] +RGPD_URL = "/rgpd-raw" +LOGIN_EXEMPT_URLS = [LOGIN_URL, RGPD_URL] if TESTING: # In a testing environment we may want to by pass the loginRequiredMiddleware diff --git a/backend/base_app/templates/base.html b/backend/base_app/templates/base.html new file mode 100644 index 00000000..134bd82e --- /dev/null +++ b/backend/base_app/templates/base.html @@ -0,0 +1,41 @@ +{% load static %} + + + + + + + + + + + + + + + + + + + + + + + + REX-DRI + + + +
+ +
+ + + {% block content %}{% endblock %} + + + diff --git a/backend/base_app/templates/cgu.html b/backend/base_app/templates/cgu.html deleted file mode 100644 index 22dda3e5..00000000 --- a/backend/base_app/templates/cgu.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - Title - - - -
-
-
-{% csrf_token %} - Valider le CGU - {{ form.has_validated_cgu }} -
-
- Valider la charte RGPD - {{ form.has_validated_rgpd }} -
-
- - -
- - - - \ No newline at end of file diff --git a/backend/base_app/templates/cgu_rgpd.html b/backend/base_app/templates/cgu_rgpd.html new file mode 100644 index 00000000..e5c9f519 --- /dev/null +++ b/backend/base_app/templates/cgu_rgpd.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% load render_bundle from webpack_loader %} + +{% block content %} + + + + {% render_bundle 'rgpdCgu' %} + {% render_bundle 'vendor' %} + + {% if user.has_validated_cgu_rgpd %} + {% else %} +
+ +
+ {% csrf_token %} + {{ form.has_validated_cgu_rgpd }} +
+ +
+
+ {% endif %} +{% endblock %} diff --git a/backend/base_app/templates/index.html b/backend/base_app/templates/index.html index 8aaa33fa..14ddf952 100644 --- a/backend/base_app/templates/index.html +++ b/backend/base_app/templates/index.html @@ -1,49 +1,14 @@ +{% extends "base.html" %} {% load render_bundle from webpack_loader %} -{% load static %} - - - - - - - - - - - - - - - - - - - - - - - REX-DRI - - - -
- -
- +{% block content %} - - {% render_bundle 'main' %} {% render_bundle 'vendor' %} - - + +{% endblock %} diff --git a/backend/base_app/templates/rgpd_raw.html b/backend/base_app/templates/rgpd_raw.html new file mode 100644 index 00000000..ae7e4ec8 --- /dev/null +++ b/backend/base_app/templates/rgpd_raw.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% load render_bundle from webpack_loader %} + +{% block content %} + + {% render_bundle 'rgpdRaw' %} + {% render_bundle 'vendor' %} + +{% endblock %} diff --git a/backend/base_app/tests/test_need_validate_cgu_rgpd.py b/backend/base_app/tests/test_need_validate_cgu_rgpd.py index 733ef246..0a9e1a30 100644 --- a/backend/base_app/tests/test_need_validate_cgu_rgpd.py +++ b/backend/base_app/tests/test_need_validate_cgu_rgpd.py @@ -16,15 +16,13 @@ class HasAccessTestCase(TestCase): cls.has_validated_user = User.objects.create_user( username="yolo", email="osef@master.fr", password=password ) - cls.has_validated_user.has_validated_cgu = True - cls.has_validated_user.has_validated_rgpd = True + cls.has_validated_user.has_validated_cgu_rgpd = True cls.has_validated_user.save() cls.has_not_validated_user = User.objects.create_user( username="yolo2", email="osef@master.fr", password=password ) - cls.has_not_validated_user.has_validated_cgu = False - cls.has_not_validated_user.has_validated_rgpd = False + cls.has_not_validated_user.has_validated_cgu_rgpd = False cls.has_not_validated_user.save() cls.has_validated_client = APIClient() @@ -44,4 +42,4 @@ class HasAccessTestCase(TestCase): def test_not_accepted_refused(self): response = self.has_not_validated_client.get("/api/") self.assertEqual(response.status_code, 302) # We have a redirect - self.assertEqual(response.url, "/cgu/?next=/api/") + self.assertEqual(response.url, "/cgu-rgpd/?next=/api/") diff --git a/backend/base_app/tests/test_rgpd_raw_public.py b/backend/base_app/tests/test_rgpd_raw_public.py new file mode 100644 index 00000000..2eef99f8 --- /dev/null +++ b/backend/base_app/tests/test_rgpd_raw_public.py @@ -0,0 +1,13 @@ +from django.test import TestCase +from rest_framework.test import APIClient + + +class HasAccessTestCase(TestCase): + """ + Test to check that the RGPD doc is publicly accessible + """ + + def test_unauthentificated_can_access(self): + client = APIClient() + response = client.get("/rgpd-raw/") + self.assertEqual(response.status_code, 200) diff --git a/backend/base_app/urls.py b/backend/base_app/urls.py index efc944a7..16e61b92 100644 --- a/backend/base_app/urls.py +++ b/backend/base_app/urls.py @@ -26,7 +26,8 @@ urlpatterns += [ name="cas_ng_proxy_callback", ), url(r"^app/.*", views.index), - url(r"^cgu/.*", views.cgu), + url(r"^cgu-rgpd/.*", views.cgu_rgpd), + url(r"^rgpd-raw/.*", views.rgpd_raw), url(r"^banned_note/", views.banned), url(r"^$", RedirectView.as_view(url="./app/"), name="go to real home"), url(r"", include("backend_app.urls")), diff --git a/backend/base_app/views.py b/backend/base_app/views.py index 581ed0ba..54d2e9cf 100644 --- a/backend/base_app/views.py +++ b/backend/base_app/views.py @@ -12,6 +12,10 @@ from base_app.models import User logger = logging.getLogger("django") +def get_bundle_loc(name): + return "/".join(get_files(name)[0]["url"].split("/")[:-1]) + "/" + + def index(request): """ View to to display the index app that contains the JS / CSS @@ -23,7 +27,7 @@ def index(request): user = request.user # small hack to get the correct location of the frontend bundled files - front_bundle_loc = "/".join(get_files("main")[0]["url"].split("/")[:-1]) + "/" + front_bundle_loc = get_bundle_loc("main") # We also retrieve the list of all routes endpoints endpoints = list(map(lambda v: clean_route(v.end_point_route), ALL_VIEWSETS)) @@ -34,7 +38,17 @@ def index(request): ) -def cgu(request): +def rgpd_raw(request): + """ + Render the view that displays only the RGPD conditions + """ + return render(request, "rgpd_raw.html") + + +def cgu_rgpd(request): + """ + Render the view that handles user accepting CGU and RGPD conditions + """ if request.method == "POST": user = User.objects.get(pk=request.user.pk) form = UserForm(request.POST, instance=user) @@ -45,11 +59,11 @@ def cgu(request): user = User.objects.get(pk=request.user.pk) form = UserForm(instance=user) - if "next" in request.GET and user.has_validated_cgu and user.has_validated_rgpd: + if "next" in request.GET and user.has_validated_cgu_rgpd: # if the user has just validated everything, we redirect him to the location he requested return HttpResponseRedirect(request.GET["next"]) else: - return render(request, "cgu.html", dict(user=user, form=form)) + return render(request, "cgu_rgpd.html", dict(user=user, form=form)) def banned(request): diff --git a/docker-compose.yml b/docker-compose.yml index a5cf6d66..d7c28be7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -63,7 +63,7 @@ services: # Service to handle frontend live developpments and building frontend: # Get the image from the registry - image: registry.gitlab.utc.fr/rex-dri/rex-dri/frontend:v0.4.0 + image: registry.gitlab.utc.fr/rex-dri/rex-dri/frontend:v0.5.0 # To use a locally built one, comment above, uncomment bellow. # build: ./frontend # On startup, we retrieve the dependencies from the image and start the developpement server diff --git a/documentation/Other/cgu.md b/documentation/Other/cgu.md new file mode 100644 index 00000000..e47b3604 --- /dev/null +++ b/documentation/Other/cgu.md @@ -0,0 +1,87 @@ +Conditions générales d'utilisation de la plateforme REX-DRI +=========================================================== + + +Les présentes « Conditions générales d'utilisation » (CGU) ont pour objet l'encadrement juridique des modalités de mise à disposition des services de la plateforme **REX-DRI** à ses utilisateurs. + + +*Le terme **« utilisateur »** désigne toute personne accédant à la plateforme et qui a accepté les présentes CGU.* + + +La plateforme REX-DRI a pour finalité le partage de retours d'expérience et d'informations générales sur les échanges universitaires. Ce afin d'aider les étudiants à choisir leurs destinations et vivre au mieux leurs expériences d'échanges (de la candidature au retour en France). + + +## Valeurs + + +Les valeurs de la plateforme REX-DRI sont : **bienveillance**, **partage**, **contribution** et **collaboration**. Aidez-nous à les diffuser ! + + +## Responsabilité + + +Toute contribution sur la plateforme est associée à son contributeur. La responsabilité d'éditeur incombe au contributeur. + + +L'utilisateur s'engage à ne pas mettre en ligne de contenu volontairement inexact, trompeur, ou pouvant porter atteinte à autrui. Aucun contenu irrespectueux ne sera toléré. *La législation française s'applique à toute contribution.* + + +En raison du format de la plateforme, le degré de fiabilité des informations mises à disposition est laissé à l'appréciation des utilisateurs ; chacun étant invité à l'améliorer. + + +## Licences associées aux données + + +### Définitions des licences + + +La licence « **REX-DRI-BY** » se définit comme étant une licence CC-BY ([https://creativecommons.org/licenses/by/2.0/fr/](https://creativecommons.org/licenses/by/2.0/fr/)) restreinte dont : + + +- Le droit à « l'adaptation » est restreint à toute personne physique pouvant accéder à la plateforme REX-DRI ([https://rex.dri.utc.fr](https://rex.dri.utc.fr)) +- Le droit au « partage » est restreint au fait que le contenu partagé (adapté ou non) n'est accessible qu'aux utilisateurs de la plateforme REX-DRI ou derrière le système d'authentification central de l'UTC ([https://cas.utc.fr/cas/login](https://cas.utc.fr/cas/login)) + + +La licence « **REX-DRI-PRIVATE** » se définit comme suit : + + +Sauf mention contraire de votre part, vous détenez tous les droits sur le contenu concerné par cette licence. + + +Toutefois, vous autorisez le traitement de ce contenu (tant qu'il est accessible) par les administrateurs du site à des fins statistiques. + + +### Cadre d'utilisation + + +Sauf mention contraire toute donnée transférée sur la plateforme REX-DRI est sous licence **REX-DRI-BY**. + + +Pour information, sont notamment concernées par la licence REX-DRI-PRIVATE les listes privées des utilisateurs, et la configuration utilisateur. + + +## Données personnelles + + +Se référer à la mention d'information ([https://rex.dri.utc.fr/app/about/rgpd/](https://rex.dri.utc.fr/app/about/rgpd/)). + + +## Condition de mise à disposition + + +REX-DRI est une ressource partagée. Afin de garantir le meilleur service possible à tous, aucun ne doit en abuser ou tenter de nuire à toute ou partie de la plateforme. + + +Tout manquement aux présentes CGU pourra donner lieu à des sanctions proportionnées. + + +## Mentions légales + + +Le **SIMDE** (Service informatique de la Maison Des Étudiants -- commission du Bureau Des Étudiants de l'Université de Technologie de Compiègne) met à disposition la plateforme REX-DRI ([https://rex.dri.utc.fr](https://rex.dri.utc.fr)) pour une durée indéterminée. + + +contact : [simde@assos.utc.fr](mailto:simde@assos.utc.fr) + + +Le site est hébergé par l'**UTC** (Université de Technologie de Compiègne). diff --git a/documentation/Other/rgpd.md b/documentation/Other/rgpd.md new file mode 100644 index 00000000..41a0cd7a --- /dev/null +++ b/documentation/Other/rgpd.md @@ -0,0 +1,57 @@ +Mention d'information RGPD plateforme REX-DRI +============================================= + + +Le SIMDE (Service Informatique de la Maison Des Étudiants – commission du Bureau Des Étudiants de l'Université de Technologie de Compiègne) met à disposition la plateforme REX-DRI ([https://rex.dri.utc.fr](https://rex.dri.utc.fr)). Elle a pour finalité le partage de retours d'expérience et d'informations générales sur les échanges universitaires. Ce afin d'aider les étudiants à choisir leurs destinations et vivre au mieux leurs expériences d'échanges (de la candidature au retour en France). + + +La base légale du traitement est le **consentement** (article 6 du règlement européen 2016/679, dit RGPD - Règlement général sur la protection des données personnelles). + +--------------------------------- + + +**Les données traitées sur tout utilisateur de la plateforme sont :** + + +1. Vos nom **\***, prénom **\***, login UTC **\***, identifiant unique **\*** et adresse mail UTC **\*** fournis par le système d'authentification centralisé du SIMDE ([https://assos.utc.fr/login](https://assos.utc.fr/login)), +2. Si vous les renseignez dans l'application, une adresse mail secondaire et votre pseudo, +3. Si vous en donnez l'autorisation sur l'ENT, les cours que vous avez suivis à l'étranger, associés à votre login UTC, +4. La date de votre dernière connexion, +5. Les données sous licence REX-DRI private qui contiennent des informations à caractère personnel. +6. Les traces de toutes les requêtes effectuées sur la plateforme. + + +**Les destinataires de ces données sont :** + + +- Les administrateurs de la plateforme: toutes ces données, +- Les utilisateurs de la plateforme peuvent accéder aux catégories de données 1, 2 et 3 (données d'identification et cours suivis à l'étranger) uniquement après votre consentement explicite. + + +**Durée de conservation des données** + + +- 1, 2, 3, 4 et 5 : au plus 5 ans après la dernière connexion, +- 6 : au plus 31 jours. + + +**Gestion des traces informatiques (6)** + + +La gestion des traces informatiques est obligatoire et a notamment pour finalité l'analyse de l'utilisation de la plateforme et la détection de défaillances. Passer le délai de 31 jours précédemment mentionné, les traces informatiques sont susceptibles d'être conservées dans un format garantissant l'anonymat des données. + +--------------------------------- + +Conformément au règlement européen 2016/679 dit RGPD, vous pouvez retirer votre consentement à tout moment et demander l'effacement des données vous concernant. Pour cela, rendez vous simplement sur la page prévue à cet effet ([https://rex.dri.utc.fr/app/user/me](https://rex.dri.utc.fr/app/user/me)) et cliquez sur le bouton “Supprimer mon compte”. Vous pouvez à tout moment vous rendre sur l'ENT et supprimer l'autorisation de partage des cours que vous avez suivis lors de votre échange. + + +Vous disposez également d'un droit d'accès, de rectification et d'opposition aux informations qui vous concernent ainsi qu'un droit à la limitation du traitement et à la portabilité de ces données, droits que vous pouvez exercer en vous adressant au SIMDE ([simde@assos.utc.fr](mailto:simde@assos.utc.fr)). + + +Pour votre droit à la portabilité de vos données, nous vous invitons à vous rendre dans un premier temps sur l'API ([https://rex.dri.utc.fr/api/](https://rex.dri.utc.fr/api/)) de la plateforme, et à télécharger vos données. + + +Si vous estimez, après nous avoir contactés, que vos droits sur vos données ne sont pas respectés, vous pouvez adresser une réclamation à la CNIL. + + +**\***: les données marquées d'un astérisque sont fournies par le système d'authentification centralisé du SIMDE et ne sont pas modifiables via la plateforme REX-DRI. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 629a03bd..66cd3e4a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12204,6 +12204,16 @@ } } }, + "raw-loader": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-3.0.0.tgz", + "integrity": "sha512-FsELYliOpX5HdPdxa7PzTmEc5OTchmLUs/r4f8oLDGCYE+xC2FjVbDXzdyLcBrdlDnvkx1x5wzphixcWpxJG5w==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "schema-utils": "^1.0.0" + } + }, "react": { "version": "16.8.6", "resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz", diff --git a/frontend/package.json b/frontend/package.json index fdcb6a21..d08131ec 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -73,6 +73,7 @@ "node-sass": "^4.12.0", "postcss-loader": "^3.0.0", "prop-types": "^15.7.2", + "raw-loader": "^3.0.0", "react-hot-loader": "^4.8.3", "react-lorem-component": "^0.13.0", "react-script": "^2.0.5", diff --git a/frontend/src/components/app/App.js b/frontend/src/components/app/App.js index 9fadebd8..a0923f24 100644 --- a/frontend/src/components/app/App.js +++ b/frontend/src/components/app/App.js @@ -8,7 +8,7 @@ import {connect} from "react-redux"; import CustomComponentForAPI from "../common/CustomComponentForAPI"; import {compose} from "recompose"; import {withErrorBoundary} from "../common/ErrorBoundary"; -import {Route} from "react-router-dom"; +import {Route, Switch} from "react-router-dom"; import getActions from "../../redux/api/getActions"; @@ -19,10 +19,11 @@ import PageSearch from "../pages/PageSearch"; import PageSettings from "../pages/PageThemeSettings"; import PageUser from "../pages/PageUser"; import PageLists from "../pages/PageLists"; -import AppFrame from "./AppFrame"; +import MainAppFrame from "./MainAppFrame"; import {APP_ROUTES} from "../../config/appRoutes"; import PageAboutProject from "../pages/PageAboutProject"; -import PageAboutConditions from "../pages/PageAboutConditions"; +import {PageCgu, PageRgpd} from "../pages/PagesRgpdCgu"; +import PageNotFound from "../pages/PageNotFound"; /** * @class App @@ -32,10 +33,10 @@ import PageAboutConditions from "../pages/PageAboutConditions"; class App extends CustomComponentForAPI { customRender() { return ( - <> - - -
+ + +
+ @@ -44,10 +45,12 @@ class App extends CustomComponentForAPI { - -
- - + + + + +
+ ); } } diff --git a/frontend/src/components/app/BaseTemplate.js b/frontend/src/components/app/BaseTemplate.js new file mode 100644 index 00000000..cfc29ef9 --- /dev/null +++ b/frontend/src/components/app/BaseTemplate.js @@ -0,0 +1,61 @@ +import React from "react"; +import AppBar from "@material-ui/core/AppBar"; +import Toolbar from "@material-ui/core/Toolbar"; +import PropTypes from "prop-types"; +import {classNames} from "../../utils/classNames"; +import {appBarHeight, siteMaxWidth} from "../../config/sharedStyles"; +import {makeStyles} from "@material-ui/styles"; + +const useStyle = makeStyles(theme => ({ + root: { + flexGrow: 1, + }, + appBar: { + width: "100%", + height: appBarHeight(theme), + }, + toolBar: { + width: "100%", + maxWidth: siteMaxWidth(), + display: "flex", + }, + content: { + maxWidth: siteMaxWidth(), + }, + centered: { + margin: "0 auto", + }, + middleBlock: { + flex: 1 + }, +})); + +export default function BaseTemplate(props) { + const classes = useStyle(); + return ( +
+ + + {props.toolbarContent} + + + + {props.inBetween} + +
+ {props.children} +
+
+ ); + +} + +BaseTemplate.propTypes = { + toolbarContent: PropTypes.node.isRequired, + children: PropTypes.node.isRequired, + inBetween: PropTypes.node.isRequired, +}; + +BaseTemplate.defaultProps = { + inBetween: <> +}; diff --git a/frontend/src/components/app/Logo.js b/frontend/src/components/app/Logo.js new file mode 100644 index 00000000..38d461a7 --- /dev/null +++ b/frontend/src/components/app/Logo.js @@ -0,0 +1,86 @@ +import {classNames} from "../../utils/classNames"; +import CustomNavLink from "../common/CustomNavLink"; +import Chip from "@material-ui/core/Chip"; +import Avatar from "@material-ui/core/Avatar"; +import Typography from "@material-ui/core/Typography"; +import React from "react"; + +import {makeStyles} from "@material-ui/styles"; +import PropTypes from "prop-types"; + + +const useStyle = makeStyles(theme => ({ + siteName: { + fontWeight: 900, + }, + logoContainer: { + position: "relative", + width: "fit-content", + }, + iconChip: { + backgroundColor: "transparent", + borderRadius: 0, + paddingLeft: 1, + paddingRight: 1, + "&:hover": { + cursor: "pointer" + } + }, + centered: { + margin: "0 auto", + }, + logoUnderline: { + position: "absolute", + left: 0, + bottom: 2, + height: 9, + backgroundColor: theme.palette.secondary.main, + width: "100%", + zIndex: -1, + }, + iconAvatar: { + width: "2.5em", + height: "2.5em", + overflow: "initial", + backgroundColor: "transparent", + right: -4 + }, +})); + +function Core() { + const classes = useStyle(); + + return ( + <> + } + label={REX-DRI} + className={classes.iconChip} + color="primary"/> +
+ + ); +} + +export default function Logo(props) { + const classes = useStyle(); + return ( +
+ { + props.linkTo ? + + : + + } +
+ ); +} + +Logo.defaultProps = { + linkTo: "" +}; + +Logo.propTypes = { + linkTo: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/frontend/src/components/app/AppFrame.js b/frontend/src/components/app/MainAppFrame.js similarity index 60% rename from frontend/src/components/app/AppFrame.js rename to frontend/src/components/app/MainAppFrame.js index 5a3a4295..f12149bc 100644 --- a/frontend/src/components/app/AppFrame.js +++ b/frontend/src/components/app/MainAppFrame.js @@ -1,13 +1,8 @@ import React from "react"; -import AppBar from "@material-ui/core/AppBar"; -import Toolbar from "@material-ui/core/Toolbar"; import IconButton from "@material-ui/core/IconButton"; import MenuIcon from "@material-ui/icons/Menu"; import SettingsIcon from "@material-ui/icons/Settings"; import InfoIcon from "@material-ui/icons/Info"; -import Chip from "@material-ui/core/Chip"; -import Avatar from "@material-ui/core/Avatar"; -import Typography from "@material-ui/core/Typography"; import Button from "@material-ui/core/Button"; import {infoMenuItems, mainMenuItems, secondaryMenuItems, settingsMenuItems} from "./menuItems"; import withStyles from "@material-ui/core/styles/withStyles"; @@ -18,56 +13,10 @@ import DrawerMenu from "./DrawerMenu"; import {APP_ROUTES} from "../../config/appRoutes"; import CustomNavLink from "../common/CustomNavLink"; import {classNames} from "../../utils/classNames"; -import {appBarHeight, siteMaxWidth} from "../../config/sharedStyles"; +import Logo from "./Logo"; +import BaseTemplate from "./BaseTemplate"; const styles = theme => ({ - root: { - flexGrow: 1, - }, - appBar: { - width: "100%", - height: appBarHeight(theme), - }, - toolBar: { - width: "100%", - maxWidth: siteMaxWidth(), - display: "flex", - }, - content: { - maxWidth: siteMaxWidth(), - }, - siteName: { - fontWeight: 900, - }, - logoContainer: { - position: "relative", - width: "fit-content", - }, - iconChip: { - backgroundColor: "transparent", - borderRadius: 0, - paddingLeft: 1, - paddingRight: 1, - "&:hover": { - cursor: "pointer" - } - }, - logoUnderline: { - position: "absolute", - left: 0, - bottom: 2, - height: 9, - backgroundColor: theme.palette.secondary.main, - width: "100%", - zIndex: -1, - }, - iconAvatar: { - width: "2.5em", - height: "2.5em", - overflow: "initial", - backgroundColor: "transparent", - right: -4 - }, menuButton: { [theme.breakpoints.up("lg")]: { display: "none", @@ -151,7 +100,7 @@ const styles = theme => ({ }, }); -class AppFrame extends React.Component { +class MainAppFrame extends React.Component { state = { drawerOpened: false, }; @@ -233,80 +182,71 @@ class AppFrame extends React.Component { ); } - renderLogo() { - const {classes} = this.props; - return ( -
- - } - label={REX-DRI} - className={classes.iconChip} - color="primary"/> -
- -
- ); - } - renderSecondaryIcons() { const {classes} = this.props; return (
+ fabProps={{ + color: "primary", + className: classes.mobileIconContainer, + classes: {primary: classes.primaryDark} + }} + menuItems={infoMenuItems}/> + fabProps={{ + color: "primary", + className: classes.mobileIconContainer, + classes: {primary: classes.primaryDark} + }} + menuItems={settingsMenuItems}/>
); } render() { const {classes} = this.props; - return ( -
- - -
- {this.renderMenuItems(mainMenuItems)} - {this.renderSimplifiedLeft()} -
+ const inBetween = this.closeDrawer()}/>; -
- {this.renderLogo()} -
+ const toolbarContent = ( + <> +
+ {this.renderMenuItems(mainMenuItems)} + {this.renderSimplifiedLeft()} +
-
-
-
- {this.renderMenuItems(secondaryMenuItems)} - {this.renderSimplifiedRight()} -
-
- {this.renderSecondaryIcons()} -
-
- - - this.closeDrawer()}/> -
- {this.props.children} +
+
-
+ +
+
+
+ {this.renderMenuItems(secondaryMenuItems)} + {this.renderSimplifiedRight()} +
+
+ {this.renderSecondaryIcons()} +
+
+ + ); + + return ( + + {this.props.children} + ); } } -AppFrame.propTypes = { +MainAppFrame.propTypes = { classes: PropTypes.object.isRequired, children: PropTypes.node.isRequired, }; -export default withStyles(styles, {withTheme: true})(AppFrame); \ No newline at end of file +export default withStyles(styles, {withTheme: true})(MainAppFrame); \ No newline at end of file diff --git a/frontend/src/components/app/menuItems.js b/frontend/src/components/app/menuItems.js index 010d9cfb..07bc9a9c 100644 --- a/frontend/src/components/app/menuItems.js +++ b/frontend/src/components/app/menuItems.js @@ -23,8 +23,9 @@ export const secondaryMenuItems = [ export const infoMenuItems = [ item("Mes informations", APP_ROUTES.userCurrent, null), - item("Conditions d'utilisations", APP_ROUTES.aboutConditions, null), item("Le projet REX-DRI", APP_ROUTES.aboutProject, null), + item("Conditions d'utilisations", APP_ROUTES.aboutCgu, null), + item("Information RGPD", APP_ROUTES.aboutRgpd, null), ]; export const settingsMenuItems = [ diff --git a/frontend/src/components/common/ThemeProvider.js b/frontend/src/components/common/ThemeProvider.js deleted file mode 100644 index fa591b1a..00000000 --- a/frontend/src/components/common/ThemeProvider.js +++ /dev/null @@ -1,106 +0,0 @@ -import React from "react"; -import PropTypes from "prop-types"; - -import MuiThemeProvider from "@material-ui/core/styles/MuiThemeProvider"; -import {connect} from "react-redux"; -import CustomComponentForAPI from "./CustomComponentForAPI"; -import "typeface-roboto"; - -import getActions from "../../redux/api/getActions"; -import {RequestParams} from "../../redux/api/RequestParams"; -import {responsiveFontSizes, rgbToHex} from "@material-ui/core/styles"; -import {createMuiTheme} from "@material-ui/core"; - -const siteSettings = { - typography: { - fontSize: 14, - htmlFontSize: 14, - }, - myPaper: { - padding: 16 - } -}; - -/** - * method to set the correct meta tags on the HTML page so that - * the status bar on phone is of a matching color as the theme. - * - * @param mainColor - */ -function updatePhoneStatusBarColor(mainColor) { - let color = rgbToHex(mainColor); - - try { - for (let el of document.getElementsByTagName("meta")) { - if (el.name === "theme-color" || el.name === "apple-mobile-web-app-status-bar-style") { - el.content = color; - } - } - } catch (e) { - // nothing, yes. - } -} - -/** - * Method to generate a full site theme based on an object themeData - * @param themeData Should be an object like: src/config/defaultTheme.json - * @returns {Theme} - */ -export function getTheme(themeData) { - const type = themeData.mode, - palette = { - type, - primary: {main: themeData[type].primary}, - secondary: {main: themeData[type].secondary} - }; - - const themeObj = Object.assign({}, siteSettings, {palette}); - const theme = createMuiTheme(themeObj); - return responsiveFontSizes(theme); -} - -/** - * Component that handles theme customization and saving at the scale of the entire app - * - * @class ThemeProvider - * @extends {CustomComponentForAPI} - * @extends React.Component - * - */ -class ThemeProvider extends CustomComponentForAPI { - customRender() { - const {userData} = this.getAllLatestReadData("userData"), - themeData = userData.theme, - theme = getTheme(themeData); - - updatePhoneStatusBarColor(theme.palette.primary.main); - - return ( - - {this.props.children} - - ); - } -} - -ThemeProvider.propTypes = { - children: PropTypes.node.isRequired, -}; - -const mapStateToProps = (state) => { - return { - userData: state.api.userDataOne - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - api: { - // __AppUserId is defined in the html rendered by django - // eslint-disable-next-line no-undef - userData: () => dispatch(getActions("userData").readOne(RequestParams.Builder.withId(__AppUserId).build())), - }, - }; -}; - -export default connect(mapStateToProps, mapDispatchToProps)(ThemeProvider); diff --git a/frontend/src/components/common/Markdown.js b/frontend/src/components/common/markdown/BaseMarkdown.js similarity index 50% rename from frontend/src/components/common/Markdown.js rename to frontend/src/components/common/markdown/BaseMarkdown.js index aae92f4c..f79334d6 100644 --- a/frontend/src/components/common/Markdown.js +++ b/frontend/src/components/common/markdown/BaseMarkdown.js @@ -2,12 +2,10 @@ /* eslint-disable react/display-name */ // Inspired by : https://github.com/mui-org/material-ui/blob/master/docs/src/pages/page-layout-examples/blog/Markdown.js -import React, {Component} from "react"; +import React from "react"; import PropTypes from "prop-types"; import ReactMarkdown from "react-markdown"; -import parseMoney from "../../utils/parseMoney"; -import convertAmountToEur from "../../utils/convertAmountToEur"; import Typography from "@material-ui/core/Typography"; import Table from "@material-ui/core/Table"; import TableBody from "@material-ui/core/TableBody"; @@ -18,7 +16,7 @@ import Paper from "@material-ui/core/Paper"; import {darken, lighten} from "@material-ui/core/styles/colorManipulator"; import Divider from "@material-ui/core/Divider"; -import TextLink, {getLinkColor} from "../common/TextLink"; +import TextLink, {getLinkColor} from "../TextLink"; import {makeStyles} from "@material-ui/styles"; // Custom styling for the rendered markdown @@ -31,12 +29,13 @@ const useStyles = makeStyles(theme => { return { list: { color: palette.text.primary, + marginBottom: theme.spacing(2), }, code: { fontFamily: "monospace" }, listItem: { - marginTop: theme.spacing(1), + marginTop: theme.spacing(0.75), }, blockquote: { color: palette.text.secondary, @@ -62,6 +61,10 @@ const useStyles = makeStyles(theme => { }, bold: { fontWeight: 700 + }, + divider: { + paddingBottom: theme.spacing(1), + marginBottom: theme.spacing(1) } }; }); @@ -70,44 +73,46 @@ const useStyles = makeStyles(theme => { /// Renderers definition //////////////////////////////////////// -const HeadingRenderer = (props) => { - const {level} = props; - - let variant = "body2", - paragraph = false; - - switch (level) { - case 1: - variant = "h5"; - break; - case 2: - variant = "h6"; - break; - case 3: - variant = "subtitle1"; - break; - case 4: - variant = "caption"; - paragraph = true; - break; - default: - variant = "body2"; - break; - } +const headingScale = ["h1", "h2", "h3", "h4", "h5", "h6", "subtitle1", "body2"]; - return ( - - {props.children} - - ); -}; +function getHeadingRender(offset) { + return (props) => { + const {level} = props; -const ListRenderer = (props) => ( -
    - {props.children} -
-); + let variant, + idx = level + offset - 1; + + if (idx < headingScale.length) { + variant = headingScale[idx]; + } else { + variant = headingScale[headingScale.length - 1]; + } + + return ( + + {props.children} + + ); + }; +} + + +const ListRenderer = (props) => { + if (props.ordered) { + return ( +
    + {props.children} +
+ ); + } else { + return ( +
    + {props.children} +
+ ); + } +}; const ListItemRenderer = (props) => (
  • @@ -156,88 +161,62 @@ const TableHeadRenderer = (props) => ( //// End of renderers definition -const renderers = { - heading: HeadingRenderer, - list: ListRenderer, - listItem: ListItemRenderer, - paragraph: (props) => {props.children}, - link: props => , - code: CodeRenderer, - inlineCode: InlineCodeRenderer, - blockquote: BlockquoteRenderer, - table: TableRenderer, - tableHead: TableHeadRenderer, - tableBody: props => ({props.children}), - tableRow: props => ({props.children}), - tableCell: props => ({props.children}), - thematicBreak: () => (), -}; +function getRenderers(headingOffset) { + return ({ + heading: getHeadingRender(headingOffset), + list: ListRenderer, + listItem: ListItemRenderer, + paragraph: (props) => {props.children}, + link: props => , + code: CodeRenderer, + inlineCode: InlineCodeRenderer, + blockquote: BlockquoteRenderer, + table: TableRenderer, + tableHead: TableHeadRenderer, + tableBody: props => ({props.children}), + tableRow: props => ({props.children}), + tableCell: props => ({props.children}), + thematicBreak: () => (), + }); +} /** * Custom Markdown component renderer to make use of material UI * - * A custom tag for currency was adde: `:100.2CHF:` would be understood as `100 CHF (~88€)` for instance. - * - * We don't need to fetch them here. - * - * @class Markdown + * @class BaseMarkdown * @extends {Component} */ -class Markdown extends Component { +function BaseMarkdown(props) { - /** - * Function to "compile" the markdown source - * It adds the money conversion information if the custom tag is present. - * - * @param {string} source - * @returns {string} - * @memberof Markdown - */ - compileSource(source) { - let compiled = ""; - - parseMoney(source).forEach(el => { - if (!el.isMoney) { - compiled += el.text; - } else { - const {amount, currency} = el; - - if (currency === "EUR") { - compiled += `${amount}€`; - } else { - const converted = convertAmountToEur(amount, currency); - compiled += `${amount}${currency} `; - if (converted === null) { - compiled += `*(\`${currency}\` n'a pas été reconnue comme le code d'une monnaie ; nous n'avons pas pu procéder à une conversion automatique)*`; - } else { - compiled += `[*(≈ ${converted}€)*](https://www.xe.com/currencyconverter/convert/?Amount=${amount}&From=${currency}&To=EUR)`; // add money converted information in markdown format - } - } - } - }); - - return compiled; - } + const compiledSource = props.compileSource(props.source); + const renderers = getRenderers(props.headingOffset); - render() { - const compiledSource = this.compileSource(this.props.source); - return ( -
    - -
    - ); - } + return ( +
    + +
    + ); } -Markdown.propTypes = { - source: PropTypes.string +BaseMarkdown.propTypes = { + source: PropTypes.string.isRequired, + compileSource: PropTypes.func.isRequired, + /** + * Shift heading so that they are not too big + */ + headingOffset: PropTypes.number.isRequired, +}; + +BaseMarkdown.defaultProps = { + headingOffset: 4, + compileSource: source => source }; -export default React.memo(Markdown); +export default React.memo(BaseMarkdown); diff --git a/frontend/src/components/common/markdown/Markdown.js b/frontend/src/components/common/markdown/Markdown.js new file mode 100644 index 00000000..b8f9833e --- /dev/null +++ b/frontend/src/components/common/markdown/Markdown.js @@ -0,0 +1,47 @@ +/* eslint-disable react/prop-types */ +/* eslint-disable react/display-name */ + +import React from "react"; +import parseMoney from "../../../utils/parseMoney"; +import convertAmountToEur from "../../../utils/convertAmountToEur"; +import BaseMarkdown from "./BaseMarkdown"; + + +function compileSource(source) { + let compiled = ""; + + parseMoney(source).forEach(el => { + if (!el.isMoney) { + compiled += el.text; + } else { + const {amount, currency} = el; + + if (currency === "EUR") { + compiled += `${amount}€`; + } else { + const converted = convertAmountToEur(amount, currency); + compiled += `${amount}${currency} `; + if (converted === null) { + compiled += `*(\`${currency}\` n'a pas été reconnue comme le code d'une monnaie ; nous n'avons pas pu procéder à une conversion automatique)*`; + } else { + compiled += `[*(≈ ${converted}€)*](https://www.xe.com/currencyconverter/convert/?Amount=${amount}&From=${currency}&To=EUR)`; // add money converted information in markdown format + } + } + } + }); + + return compiled; +} + +/** + * Custom Markdown component renderer to make use of material UI + * + * A custom tag for currency was adde: `:100.2CHF:` would be understood as `100 CHF (~88€)` for instance. + * + * We don't need to fetch them here. + * + * @class Markdown + * @extends {Component} + * @return {string} + */ +export default React.memo((props => compileSource(s)}/>)); diff --git a/frontend/src/components/common/TruncatedMarkdown.js b/frontend/src/components/common/markdown/TruncatedMarkdown.js similarity index 100% rename from frontend/src/components/common/TruncatedMarkdown.js rename to frontend/src/components/common/markdown/TruncatedMarkdown.js diff --git a/frontend/src/components/common/theme/OfflineThemeProvider.js b/frontend/src/components/common/theme/OfflineThemeProvider.js new file mode 100644 index 00000000..13db4e17 --- /dev/null +++ b/frontend/src/components/common/theme/OfflineThemeProvider.js @@ -0,0 +1,24 @@ +import defaultTheme from "../../../config/defaultTheme"; +import MuiThemeProvider from "@material-ui/core/styles/MuiThemeProvider"; +import PropTypes from "prop-types"; +import React from "react"; +import {getTheme, updatePhoneStatusBarColor} from "./utils"; + +/** + * Base Theme provider without connecting to the API + * @constructor + */ +export default function OfflineThemeProvider(props) { + const theme = getTheme(defaultTheme); + updatePhoneStatusBarColor(theme.palette.primary.main); + + return ( + + {props.children} + + ); +} + +OfflineThemeProvider.propTypes = { + children: PropTypes.node.isRequired, +}; \ No newline at end of file diff --git a/frontend/src/components/common/theme/ThemeProvider.js b/frontend/src/components/common/theme/ThemeProvider.js new file mode 100644 index 00000000..9c6d72f4 --- /dev/null +++ b/frontend/src/components/common/theme/ThemeProvider.js @@ -0,0 +1,56 @@ +import React from "react"; +import PropTypes from "prop-types"; + +import MuiThemeProvider from "@material-ui/core/styles/MuiThemeProvider"; +import {connect} from "react-redux"; +import CustomComponentForAPI from "../CustomComponentForAPI"; +import {getTheme, updatePhoneStatusBarColor} from "./utils"; +import getActions from "../../../redux/api/getActions"; +import {RequestParams} from "../../../redux/api/RequestParams"; + + +/** + * Component that handles theme customization and saving at the scale of the entire app + * + * @class ThemeProvider + * @extends {CustomComponentForAPI} + * @extends React.Component + * + */ +class ThemeProvider extends CustomComponentForAPI { + customRender() { + const {userData} = this.getAllLatestReadData("userData"), + themeData = userData.theme, + theme = getTheme(themeData); + + updatePhoneStatusBarColor(theme.palette.primary.main); + + return ( + + {this.props.children} + + ); + } +} + +ThemeProvider.propTypes = { + children: PropTypes.node.isRequired, +}; + +const mapStateToProps = (state) => { + return { + userData: state.api.userDataOne + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + api: { + // __AppUserId is defined in the html rendered by django + // eslint-disable-next-line no-undef + userData: () => dispatch(getActions("userData").readOne(RequestParams.Builder.withId(__AppUserId).build())), + }, + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(ThemeProvider); diff --git a/frontend/src/components/common/theme/utils.js b/frontend/src/components/common/theme/utils.js new file mode 100644 index 00000000..da5dac76 --- /dev/null +++ b/frontend/src/components/common/theme/utils.js @@ -0,0 +1,55 @@ +/** + * We put some stuff in a separate file to prevent useless imports for CGU + */ + +import {createMuiTheme, responsiveFontSizes, rgbToHex} from "@material-ui/core/styles"; +import "typeface-roboto"; + +const siteSettings = { + typography: { + fontSize: 14, + htmlFontSize: 14, + }, + myPaper: { + padding: 16 + } +}; + + +/** + * method to set the correct meta tags on the HTML page so that + * the status bar on phone is of a matching color as the theme. + * + * @param mainColor + */ +export function updatePhoneStatusBarColor(mainColor) { + let color = rgbToHex(mainColor); + + try { + for (let el of document.getElementsByTagName("meta")) { + if (el.name === "theme-color" || el.name === "apple-mobile-web-app-status-bar-style") { + el.content = color; + } + } + } catch (e) { + // nothing, yes. + } +} + +/** + * Method to generate a full site theme based on an object themeData + * @param themeData Should be an object like: src/config/defaultTheme.json + * @returns {Theme} + */ +export function getTheme(themeData) { + const type = themeData.mode, + palette = { + type, + primary: {main: themeData[type].primary}, + secondary: {main: themeData[type].secondary} + }; + + const themeObj = Object.assign({}, siteSettings, {palette}); + const theme = createMuiTheme(themeObj); + return responsiveFontSizes(theme); +} \ No newline at end of file diff --git a/frontend/src/components/form/fields/MarkdownField.js b/frontend/src/components/form/fields/MarkdownField.js index 320f0afb..dbc432b3 100644 --- a/frontend/src/components/form/fields/MarkdownField.js +++ b/frontend/src/components/form/fields/MarkdownField.js @@ -6,7 +6,7 @@ import Grid from "@material-ui/core/Grid"; import TextField from "@material-ui/core/TextField"; import Typography from "@material-ui/core/Typography"; -import Markdown from "../../common/Markdown"; +import Markdown from "../../common/markdown/Markdown"; import Field from "./Field"; import TextLink from "../../common/TextLink"; diff --git a/frontend/src/components/pages/FormRgpdCgu.js b/frontend/src/components/pages/FormRgpdCgu.js new file mode 100644 index 00000000..e7c7e9d8 --- /dev/null +++ b/frontend/src/components/pages/FormRgpdCgu.js @@ -0,0 +1,148 @@ +import React from "react"; +import {PaddedPageDiv} from "./shared"; +import {makeStyles} from "@material-ui/core/styles"; +import ExpansionPanel from "@material-ui/core/ExpansionPanel"; +import ExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary"; +import ExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails"; +import Typography from "@material-ui/core/Typography"; +import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; +import BaseMarkdown from "../common/markdown/BaseMarkdown"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import Switch from "@material-ui/core/Switch"; +import Paper from "@material-ui/core/Paper"; +import Button from "@material-ui/core/Button"; +import {CGU_MARKDOWN_SOURCE, RGPD_MARKDOWN_SOURCE} from "../../config/other"; + + +const useStyles = makeStyles(theme => ({ + detailsRoot: { + display: "block", + }, + paperSwitch: { + backgroundColor: theme.palette.background.default, + padding: theme.spacing(2) + }, + + centered: { + margin: "0 auto", + display: "block", + }, + submitButton: { + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2), + marginLeft: "auto", + marginRight: "auto", + } +})); + + +// eslint-disable-next-line no-undef +const validatedOnServer = !!__hasValidatedCguRgpd; + +function submit(formIsValid) { + if (formIsValid && !validatedOnServer) { + document.getElementById("formSubmitButton").click(); + } +} + +function FormRgpdCgu() { + const [expanded, setExpanded] = React.useState(""), + [cguValidated, setCguValidated] = React.useState(validatedOnServer), + [rgpdValidated, setRgpdValidated] = React.useState(validatedOnServer), + classes = useStyles(); + + const handleChange = panel => (event, isExpanded) => { + setExpanded(isExpanded ? panel : ""); + }; + + + const formIsValid = cguValidated && rgpdValidated; + + // updated the real form generated by django silently + const checkBox = document.getElementById("id_has_validated_cgu_rgpd"); + if (checkBox !== null) { + checkBox.checked = formIsValid; + } + + const data = [ + { + panel: "cgu", + title: "Conditions d'utilisation", + expanded: expanded === "cgu", + source: CGU_MARKDOWN_SOURCE, + validated: cguValidated, + setValidated: setCguValidated, + }, + { + panel: "rgpd", + title: "Mention d'information RGPD", + expanded: expanded === "rgpd", + source: RGPD_MARKDOWN_SOURCE, + validated: rgpdValidated, + setValidated: setRgpdValidated, + } + ]; + + return ( + + { + data.map(el => + + + }> + {el.title}  + ({el.validated ? "acceptée" : "non acceptée"}) + + + + + { + !validatedOnServer ? + <> +
    + +
    + el.setValidated(e.target.checked)} + color="primary" + /> + } + label={`Acceptée ? (${el.validated ? "oui" : "non"})`} + /> +
    +
    + + : <> + } + +
    +
    + ) + } + { + !validatedOnServer ? +
    + +
    + : + <> + } + +
    + ); +} + +export default FormRgpdCgu; \ No newline at end of file diff --git a/frontend/src/components/pages/PageAboutConditions.js b/frontend/src/components/pages/PageAboutConditions.js deleted file mode 100644 index c5fa1365..00000000 --- a/frontend/src/components/pages/PageAboutConditions.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from "react"; -import Typography from "@material-ui/core/Typography"; -import Markdown from "../common/Markdown"; -import {compose} from "recompose"; -import {withErrorBoundary} from "../common/ErrorBoundary"; -import {withPaddedPaper} from "./shared"; - - -const source = ` -# Utilisations des données - -# RGPD - -... -`; - - -/** - * Component corresponding to page about use conditions. - */ -function PageAboutConditions() { - return ( - <> - - Le projet REX-DRI - - - - ); -} - -export default compose( - withPaddedPaper(), - withErrorBoundary() -)(PageAboutConditions); diff --git a/frontend/src/components/pages/PageAboutProject.js b/frontend/src/components/pages/PageAboutProject.js index 8b4a7beb..9895b97a 100644 --- a/frontend/src/components/pages/PageAboutProject.js +++ b/frontend/src/components/pages/PageAboutProject.js @@ -1,6 +1,6 @@ import React from "react"; import Typography from "@material-ui/core/Typography"; -import Markdown from "../common/Markdown"; +import Markdown from "../common/markdown/Markdown"; import {compose} from "recompose"; import {withErrorBoundary} from "../common/ErrorBoundary"; import {withPaddedPaper} from "./shared"; diff --git a/frontend/src/components/pages/PageHome.js b/frontend/src/components/pages/PageHome.js index e7ffebad..3da2760d 100644 --- a/frontend/src/components/pages/PageHome.js +++ b/frontend/src/components/pages/PageHome.js @@ -2,13 +2,11 @@ import React from "react"; import {compose} from "recompose"; import {withErrorBoundary} from "../common/ErrorBoundary"; import Typography from "@material-ui/core/Typography"; -import Markdown from "../common/Markdown"; +import Markdown from "../common/markdown/Markdown"; import {withPaddedPaper} from "./shared"; const source = ` -# Attention - **Ce service est à l'heure actuelle au stade de version _alpha_ afin de montrer certaines fonctionnalités. Les données seront vraisemblablement remises à zéro lors du passage à la phase _beta_ (quand toutes les fonctionnalités seront en place).** Si vous trouvez des bugs ou si vous avez des suggestions, merci de les signaler [ici](https://gitlab.utc.fr/chehabfl/outgoing_rex/issues) ou par mail à l'adresse [florent.chehab@etu.utc.fr](mailto:florent.chehab@etu.utc.fr). @@ -67,7 +65,7 @@ function PageHome() { Bienvenue sur REX-DRI ! - + ); } diff --git a/frontend/src/components/pages/PageNotFound.js b/frontend/src/components/pages/PageNotFound.js new file mode 100644 index 00000000..9203f61e --- /dev/null +++ b/frontend/src/components/pages/PageNotFound.js @@ -0,0 +1,24 @@ +import React from "react"; +import {compose} from "recompose"; +import Typography from "@material-ui/core/Typography"; +import {withPaddedPaper} from "./shared"; + + +/** + * Component corresponding to the landing page of the site + */ +function PageHome() { + return ( + <> + + Cet URL ne mène à aucune page 😢 + + + ); +} + +PageHome.propTypes = {}; + +export default compose( + withPaddedPaper(), +)(PageHome); diff --git a/frontend/src/components/pages/PageUser.js b/frontend/src/components/pages/PageUser.js index 8d0dd069..2edefeba 100644 --- a/frontend/src/components/pages/PageUser.js +++ b/frontend/src/components/pages/PageUser.js @@ -11,6 +11,7 @@ import {compose} from "recompose"; import {withErrorBoundary} from "../common/ErrorBoundary"; import {withPaddedPaper} from "./shared"; import {makeStyles} from "@material-ui/styles"; +import {APP_ROUTES} from "../../config/appRoutes"; const useStyle = makeStyles(theme => ({ @@ -40,6 +41,12 @@ function PageUser(props) { const classes = useStyle(), requestedUserId = getUserIdFromUrl(); + if (requestedUserId === "me") { + // eslint-disable-next-line no-undef + props.history.push(APP_ROUTES.forUser(__AppUserId)); + return <>; + } + return ( <> + + + ); +} + +export function PageCgu() { + return ( + + + + ); +} diff --git a/frontend/src/components/recommendation/SelectListSubPage.js b/frontend/src/components/recommendation/SelectListSubPage.js index 3827c8a0..689f79f7 100644 --- a/frontend/src/components/recommendation/SelectListSubPage.js +++ b/frontend/src/components/recommendation/SelectListSubPage.js @@ -82,7 +82,7 @@ class SelectListSubPage extends CustomComponentForAPI { displayedLists.length === 0 ? <> : -
    +
    diff --git a/frontend/src/components/recommendation/view/TextBlock.js b/frontend/src/components/recommendation/view/TextBlock.js index c86c0f6b..651420db 100644 --- a/frontend/src/components/recommendation/view/TextBlock.js +++ b/frontend/src/components/recommendation/view/TextBlock.js @@ -1,7 +1,7 @@ import React from "react"; import TextField from "@material-ui/core/TextField"; import PropTypes from "prop-types"; -import Markdown from "../../common/Markdown"; +import Markdown from "../../common/markdown/Markdown"; import keycode from "keycode"; import useBlock from "./useBlock"; import truncateString from "../../../utils/truncateString"; diff --git a/frontend/src/components/settings/theme/ColorDemo.js b/frontend/src/components/settings/theme/ColorDemo.js index e0f63950..7dcfeb40 100644 --- a/frontend/src/components/settings/theme/ColorDemo.js +++ b/frontend/src/components/settings/theme/ColorDemo.js @@ -1,8 +1,8 @@ import React from "react"; import PropTypes from "prop-types"; import withStyles from "@material-ui/core/styles/withStyles"; -import AppFrame from "../../app/AppFrame"; -import Markdown from "../../common/Markdown"; +import MainAppFrame from "../../app/MainAppFrame"; +import Markdown from "../../common/markdown/Markdown"; import Typography from "@material-ui/core/Typography"; import Paper from "@material-ui/core/Paper"; @@ -23,14 +23,14 @@ function ColorDemo(props) { return (
    - + Bienvenue sur REX-DRI ! - +
    ); } diff --git a/frontend/src/components/settings/theme/ColorTools.js b/frontend/src/components/settings/theme/ColorTools.js index d616b358..e1169aaf 100644 --- a/frontend/src/components/settings/theme/ColorTools.js +++ b/frontend/src/components/settings/theme/ColorTools.js @@ -20,7 +20,6 @@ import getActions from "../../../redux/api/getActions"; import {compose} from "recompose"; import Input from "@material-ui/core/Input"; import {getLatestRead} from "../../../redux/api/utils"; -import {getTheme} from "../../common/ThemeProvider"; import SaveButton from "../../common/SaveButton"; import ExpansionPanel from "@material-ui/core/ExpansionPanel"; import ExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary"; @@ -32,6 +31,7 @@ import Divider from "@material-ui/core/Divider"; import TextLink from "../../common/TextLink"; import {deepCopy} from "../../../utils/deepCopy"; import {RequestParams} from "../../../redux/api/RequestParams"; +import {getTheme} from "../../common/theme/utils"; const isRgb = string => /#?([0-9a-f]{6})/i.test(string); diff --git a/frontend/src/components/university/modules/CountryDri.js b/frontend/src/components/university/modules/CountryDri.js index b6365db6..fe928a34 100644 --- a/frontend/src/components/university/modules/CountryDri.js +++ b/frontend/src/components/university/modules/CountryDri.js @@ -4,7 +4,7 @@ import withStyles from "@material-ui/core/styles/withStyles"; import compose from "recompose/compose"; import { connect } from "react-redux"; -import Markdown from "../../common/Markdown"; +import Markdown from "../../common/markdown/Markdown"; import Module from "./common/Module"; diff --git a/frontend/src/components/university/modules/UniversityDri.js b/frontend/src/components/university/modules/UniversityDri.js index 6ffbbc6f..502f4933 100644 --- a/frontend/src/components/university/modules/UniversityDri.js +++ b/frontend/src/components/university/modules/UniversityDri.js @@ -4,7 +4,7 @@ import withStyles from "@material-ui/core/styles/withStyles"; import compose from "recompose/compose"; import {connect} from "react-redux"; -import Markdown from "../../common/Markdown"; +import Markdown from "../../common/markdown/Markdown"; import Module from "./common/Module"; diff --git a/frontend/src/components/university/modules/UniversitySemestersDates.js b/frontend/src/components/university/modules/UniversitySemestersDates.js index f713d35f..a21b07c5 100644 --- a/frontend/src/components/university/modules/UniversitySemestersDates.js +++ b/frontend/src/components/university/modules/UniversitySemestersDates.js @@ -9,7 +9,7 @@ import TableCell from "@material-ui/core/TableCell"; import TableHead from "@material-ui/core/TableHead"; import TableRow from "@material-ui/core/TableRow"; -import Markdown from "../../common/Markdown"; +import Markdown from "../../common/markdown/Markdown"; import CloudQueueIcon from "@material-ui/icons/CloudQueue"; import LocalFloristIcon from "@material-ui/icons/LocalFlorist"; diff --git a/frontend/src/components/university/modules/common/Scholarship.js b/frontend/src/components/university/modules/common/Scholarship.js index 364963f9..88be0648 100644 --- a/frontend/src/components/university/modules/common/Scholarship.js +++ b/frontend/src/components/university/modules/common/Scholarship.js @@ -2,7 +2,7 @@ import React from "react"; import PropTypes from "prop-types"; import withStyles from "@material-ui/core/styles/withStyles"; -import Markdown from "../../../common/Markdown"; +import Markdown from "../../../common/markdown/Markdown"; import moneyConversion from "../../../../utils/convertAmountToEur"; import getCurrencySymbol from "../../../../utils/getCurrencySymbol"; import Typography from "@material-ui/core/Typography"; diff --git a/frontend/src/components/university/modules/previousExchangeFeedback/CoursesComments.js b/frontend/src/components/university/modules/previousExchangeFeedback/CoursesComments.js index c1da9c45..fd356cfc 100644 --- a/frontend/src/components/university/modules/previousExchangeFeedback/CoursesComments.js +++ b/frontend/src/components/university/modules/previousExchangeFeedback/CoursesComments.js @@ -5,7 +5,7 @@ import MuiExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary"; import MuiExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails"; import Typography from "@material-ui/core/Typography"; import PropTypes from "prop-types"; -import Markdown from "../../../common/Markdown"; +import Markdown from "../../../common/markdown/Markdown"; const ExpansionPanel = withStyles({ root: { diff --git a/frontend/src/components/university/modules/previousExchangeFeedback/PreviousDeparture.js b/frontend/src/components/university/modules/previousExchangeFeedback/PreviousDeparture.js index 083a7467..37595d84 100644 --- a/frontend/src/components/university/modules/previousExchangeFeedback/PreviousDeparture.js +++ b/frontend/src/components/university/modules/previousExchangeFeedback/PreviousDeparture.js @@ -1,6 +1,6 @@ import React from "react"; import Grid from "@material-ui/core/Grid"; -import TruncatedMarkdown from "../../../common/TruncatedMarkdown"; +import TruncatedMarkdown from "../../../common/markdown/TruncatedMarkdown"; import PropTypes from "prop-types"; import MetricFeedback from "../../../common/MetricFeedback"; import Paper from "@material-ui/core/Paper"; diff --git a/frontend/src/components/user/DeleteAccount.js b/frontend/src/components/user/DeleteAccount.js new file mode 100644 index 00000000..1ffc9d05 --- /dev/null +++ b/frontend/src/components/user/DeleteAccount.js @@ -0,0 +1,98 @@ +import React from "react"; +import PropTypes from "prop-types"; +import ExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary"; +import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; +import Typography from "@material-ui/core/Typography"; +import ExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails"; +import ExpansionPanel from "@material-ui/core/ExpansionPanel"; +import BaseMarkdown from "../common/markdown/BaseMarkdown"; +import Button from "@material-ui/core/Button"; +import getActions from "../../redux/api/getActions"; +import {compose} from "recompose"; +import {connect} from "react-redux"; +import {RequestParams} from "../../redux/api/RequestParams"; + + +const deleteMessage = ` +# Attention + +* Cette action entrainera la suppression de toute vos données sous licence « REX-DRI Private » +(login UTC, adresses mails, pseudos, listes privée, réglages personnels, etc.), +* Afin de satisfaire les exigences de la licence « REX-DRI BY », votre nom et prénom seront conservés, +* Pour toute demande supplémentaire, merci de contacter le SIMDE. +* La suppression de votre compte sera effective **sous 48h**. + +*La suppression d'un compte consiste donc au vidage des informations personnelles associées.* + +# Rappel + +* Cette action sera de toute manière automatiquement réalisée passer 5 ans d'inactivité de votre compte. + +*Si vous souhaitez définitivement supprimer votre compte, cliquez sur le bouton ci-dessous.* +`; + + +function DeleteAccount(props) { + const {deleteProgrammed} = props; + + if (deleteProgrammed) { + return ( + <> + + + ); + + } else { + return ( + <> + + }> + Supprimer mon compte + + + + + + + + ); + } +} + +DeleteAccount.propTypes = { + deleteProgrammed: PropTypes.bool.isRequired, + deleteAccount: PropTypes.func.isRequired, + undoDeleteAccount: PropTypes.func.isRequired, +}; + + +const mapDispatchToProps = (dispatch) => { + return { + deleteAccount: () => { + const params = RequestParams.Builder + .withOnSuccessCallback(() => dispatch(getActions("users").invalidateOne())) + .build(); + + return dispatch(getActions("emptyUserAccount").create(params)); + }, + undoDeleteAccount: () => { + const params = RequestParams.Builder + .withOnSuccessCallback(() => dispatch(getActions("users").invalidateOne())) + .withId("oserf") + .build(); + return dispatch(getActions("emptyUserAccount").delete(params)); + } + }; +}; + +export default compose( + connect(() => ({}), mapDispatchToProps), +)(DeleteAccount); + diff --git a/frontend/src/components/user/UserInfo.js b/frontend/src/components/user/UserInfo.js index 44def4d9..31223b26 100644 --- a/frontend/src/components/user/UserInfo.js +++ b/frontend/src/components/user/UserInfo.js @@ -15,6 +15,7 @@ import UserInfoEditor from "./UserInfoEditor"; import CreateIcon from "@material-ui/icons/Create"; import {classNames} from "../../utils/classNames"; import {RequestParams} from "../../redux/api/RequestParams"; +import DeleteAccount from "./DeleteAccount"; function TypographyWithIcon(props) { return ( @@ -47,6 +48,11 @@ TypographyWithIcon.propTypes = { * @extends {CustomComponentForAPI} */ class UserInfo extends CustomComponentForAPI { + /** + * @override + * @type {boolean} + */ + enableSmartDataRefreshOnComponentDidUpdate = true; apiParams = { "user": ({props}) => @@ -77,6 +83,7 @@ class UserInfo extends CustomComponentForAPI { secondary_email: secondaryEmail, username: login, id, + delete_next_time: deleteProgrammed, } = userModelData, // eslint-disable-next-line no-undef isOwner = parseInt(__AppUserId) === parseInt(id), @@ -167,17 +174,25 @@ class UserInfo extends CustomComponentForAPI { { isOwner ? <> -
    - + { + !deleteProgrammed ? + <> +
    + +
    + this.closeEditorPanel()} + rawModelData={userModelData}/> + : <> + } +
    +
    - this.closeEditorPanel()} - rawModelData={userModelData}/> : <> @@ -225,6 +240,9 @@ const styles = theme => ({ login: { fontFamily: "monospace", }, + deleteContainer: { + margin: theme.spacing(2) + }, spacer: { marginTop: theme.spacing(2), marginBottom: theme.spacing(1), diff --git a/frontend/src/config/appRoutes.js b/frontend/src/config/appRoutes.js index 017ccc6e..8159b2f0 100644 --- a/frontend/src/config/appRoutes.js +++ b/frontend/src/config/appRoutes.js @@ -17,7 +17,8 @@ const base = "/app/", themeSettings = settings + "theme/", about = base + "about/", aboutProject = about + "project/", - aboutConditions = about + "conditions/", + aboutRgpd = about + "rgpd/", + aboutCgu = about + "cgu/", logout = "/user/logout"; export const APP_ROUTES = { @@ -39,6 +40,7 @@ export const APP_ROUTES = { themeSettings, about, aboutProject, - aboutConditions, + aboutCgu, + aboutRgpd, logout, }; diff --git a/frontend/src/config/other.js b/frontend/src/config/other.js new file mode 100644 index 00000000..cd5ed0dd --- /dev/null +++ b/frontend/src/config/other.js @@ -0,0 +1,6 @@ +import cgu from "../../../documentation/Other/cgu.md"; +import rgpd from "../../../documentation/Other/rgpd.md"; + + +export const RGPD_MARKDOWN_SOURCE = rgpd; +export const CGU_MARKDOWN_SOURCE = cgu; \ No newline at end of file diff --git a/frontend/src/index.js b/frontend/src/entry/mainApp.js similarity index 89% rename from frontend/src/index.js rename to frontend/src/entry/mainApp.js index bde55e54..40c83dfe 100644 --- a/frontend/src/index.js +++ b/frontend/src/entry/mainApp.js @@ -5,9 +5,9 @@ import {BrowserRouter as Router} from "react-router-dom"; import {SnackbarProvider} from "notistack"; // provider to easily handle notifications across the app import Button from "@material-ui/core/Button"; -import store from "./redux/store"; -import App from "./components/app/App"; -import ThemeProvider from "./components/common/ThemeProvider"; +import store from "../redux/store"; +import App from "../components/app/App"; +import ThemeProvider from "../components/common/theme/ThemeProvider"; import CssBaseline from "@material-ui/core/CssBaseline"; const MainReactEntry = () => ( diff --git a/frontend/src/entry/rgpdCguForm.js b/frontend/src/entry/rgpdCguForm.js new file mode 100644 index 00000000..bef2c530 --- /dev/null +++ b/frontend/src/entry/rgpdCguForm.js @@ -0,0 +1,39 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import OfflineThemeProvider from "../components/common/theme/OfflineThemeProvider"; +import CssBaseline from "@material-ui/core/CssBaseline"; +import Logo from "../components/app/Logo"; +import BaseTemplate from "../components/app/BaseTemplate"; +import FormRgpdCgu from "../components/pages/FormRgpdCgu"; + +function MainReactEntry() { + + const toolbarContent = ( +
    + +
    + ); + + return ( + + <> + +
    + + + +
    + +
    + ); +} + +const wrapper = document.getElementById("app"); + +wrapper ? ReactDOM.render(, wrapper) : null; + +// eslint-disable-next-line no-undef +if (module.hot) { + // eslint-disable-next-line no-undef + module.hot.accept(); +} diff --git a/frontend/src/entry/rgpdRaw.js b/frontend/src/entry/rgpdRaw.js new file mode 100644 index 00000000..33cda8ce --- /dev/null +++ b/frontend/src/entry/rgpdRaw.js @@ -0,0 +1,39 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import OfflineThemeProvider from "../components/common/theme/OfflineThemeProvider"; +import CssBaseline from "@material-ui/core/CssBaseline"; +import Logo from "../components/app/Logo"; +import BaseTemplate from "../components/app/BaseTemplate"; +import {PageRgpd} from "../components/pages/PagesRgpdCgu"; + +function MainReactEntry() { + + const toolbarContent = ( +
    + +
    + ); + + return ( + + <> + +
    + + + +
    + +
    + ); +} + +const wrapper = document.getElementById("app"); + +wrapper ? ReactDOM.render(, wrapper) : null; + +// eslint-disable-next-line no-undef +if (module.hot) { + // eslint-disable-next-line no-undef + module.hot.accept(); +} diff --git a/frontend/webpack.config.base.js b/frontend/webpack.config.base.js index f07208b4..f7eb20ef 100644 --- a/frontend/webpack.config.base.js +++ b/frontend/webpack.config.base.js @@ -5,7 +5,13 @@ const CopyPlugin = require("copy-webpack-plugin"); const config = { entry: { main: [ - "./src/index", + "./src/entry/mainApp", + ], + rgpdCgu: [ + "./src/entry/rgpdCguForm", + ], + rgpdRaw: [ + "./src/entry/rgpdRaw", ], vendor: [ "@babel/polyfill", @@ -55,7 +61,11 @@ const config = { outputPath: "fonts/" } }] - } + }, + { + test: /\.(md)$/, + use: "raw-loader", + }, ], }, plugins: [ diff --git a/frontend/webpack.config.dev.js b/frontend/webpack.config.dev.js index bdff04d8..d908b99d 100644 --- a/frontend/webpack.config.dev.js +++ b/frontend/webpack.config.dev.js @@ -9,7 +9,15 @@ const devConfig = merge(webpackConfig, { main: [ "webpack-dev-server/client?http://localhost:3000", "webpack/hot/only-dev-server", - ] + ], + rgpdCgu: [ + "webpack-dev-server/client?http://localhost:3000", + "webpack/hot/only-dev-server", + ], + rgpdRaw: [ + "webpack-dev-server/client?http://localhost:3000", + "webpack/hot/only-dev-server", + ], }, optimization: { minimize: false, diff --git a/server/docker-compose.prod.yml b/server/docker-compose.prod.yml index 545751c3..e91a1894 100644 --- a/server/docker-compose.prod.yml +++ b/server/docker-compose.prod.yml @@ -36,7 +36,7 @@ services: volumes: ["postgres_data_prod:/var/lib/postgresql/data/"] frontend: # Will be killed as soon as the front is generated - image: registry.gitlab.utc.fr/rex-dri/rex-dri/frontend:v0.4.0 + image: registry.gitlab.utc.fr/rex-dri/rex-dri/frontend:v0.5.0 command: /bin/sh -c "cd frontend && cp -R /usr/src/deps/node_modules . && npm run build" volumes: - ../:/usr/src/app/ -- GitLab