Commit b89d8b63 authored by Florent Chehab's avatar Florent Chehab

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
parent 2f3e6bb6
Pipeline #42031 passed with stages
in 8 minutes and 37 seconds
...@@ -34,11 +34,15 @@ check_back: ...@@ -34,11 +34,15 @@ check_back:
check_front: check_front:
<<: *only-default <<: *only-default
stage: check 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: before_script:
- cd frontend && cp -R /usr/src/deps/node_modules . - cd frontend && cp -R /usr/src/deps/node_modules .
script: script:
- npm run build - npm run build
artifacts:
paths:
- frontend/webpack-stats.json
expire_in: 1 hour
tags: tags:
- docker - docker
...@@ -70,7 +74,7 @@ test_back: ...@@ -70,7 +74,7 @@ test_back:
test_frontend: test_frontend:
<<: *only-default <<: *only-default
stage: test 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: before_script:
- cd frontend && cp -R /usr/src/deps/node_modules . - cd frontend && cp -R /usr/src/deps/node_modules .
script: script:
...@@ -90,7 +94,7 @@ flake8: ...@@ -90,7 +94,7 @@ flake8:
eslint: eslint:
<<: *only-default <<: *only-default
stage: lint 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: before_script:
- cd frontend && cp -R /usr/src/deps/node_modules . - cd frontend && cp -R /usr/src/deps/node_modules .
script: script:
......
...@@ -72,8 +72,7 @@ class WithUserTestCase(TestCase): ...@@ -72,8 +72,7 @@ class WithUserTestCase(TestCase):
cls.authenticated_user, cls.authenticated_user,
cls.authenticated_user_2, cls.authenticated_user_2,
]: ]:
user.has_validated_cgu = True user.has_validated_cgu_rgpd = True
user.has_validated_rgpd = True
user.save() user.save()
cls.unauthenticated_client = APIClient() cls.unauthenticated_client = APIClient()
......
...@@ -167,6 +167,37 @@ class BannedUserViewSet(ViewSet): ...@@ -167,6 +167,37 @@ class BannedUserViewSet(ViewSet):
return Response(status=200) 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): class RecommendationListChangeFollowerViewSet(ViewSet):
""" """
Viewset to be able to add or delete followers on Viewset to be able to add or delete followers on
...@@ -184,7 +215,7 @@ class RecommendationListChangeFollowerViewSet(ViewSet): ...@@ -184,7 +215,7 @@ class RecommendationListChangeFollowerViewSet(ViewSet):
recommendation = RecommendationList.objects.get(pk=pk) recommendation = RecommendationList.objects.get(pk=pk)
if recommendation.is_public: if recommendation.is_public:
recommendation.followers.add(self.request.user) recommendation.followers.add(request.user)
recommendation.save() recommendation.save()
return Response(status=200) return Response(status=200)
else: else:
...@@ -195,7 +226,7 @@ class RecommendationListChangeFollowerViewSet(ViewSet): ...@@ -195,7 +226,7 @@ class RecommendationListChangeFollowerViewSet(ViewSet):
return Response(status=403) return Response(status=403)
# can delete folower even if list not public # can delete folower even if list not public
recommendation = RecommendationList.objects.get(pk=pk) recommendation = RecommendationList.objects.get(pk=pk)
recommendation.followers.remove(self.request.user) recommendation.followers.remove(request.user)
recommendation.save() recommendation.save()
return Response(status=200) return Response(status=200)
...@@ -205,6 +236,7 @@ ALL_API_VIEW_VIEWSETS = [ ...@@ -205,6 +236,7 @@ ALL_API_VIEW_VIEWSETS = [
LogFrontendErrorsViewSet, LogFrontendErrorsViewSet,
BannedUserViewSet, BannedUserViewSet,
RecommendationListChangeFollowerViewSet, RecommendationListChangeFollowerViewSet,
DeleteUserViewSet,
] ]
ALL_VIEWSETS = ALL_API_VIEWSETS + ALL_API_VIEW_VIEWSETS ALL_VIEWSETS = ALL_API_VIEWSETS + ALL_API_VIEW_VIEWSETS
......
...@@ -5,4 +5,4 @@ from base_app.models import User ...@@ -5,4 +5,4 @@ from base_app.models import User
class UserForm(forms.ModelForm): class UserForm(forms.ModelForm):
class Meta: class Meta:
model = User model = User
fields = ("has_validated_rgpd", "has_validated_cgu") fields = ("has_validated_cgu_rgpd",)
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
...@@ -53,9 +53,9 @@ class RexDriRequestMiddleware(MiddlewareMixin): ...@@ -53,9 +53,9 @@ class RexDriRequestMiddleware(MiddlewareMixin):
else: else:
# User is authenticated # User is authenticated
# We check if he / she has validated the CGU and the full RGPD # 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 request.user.has_validated_cgu_rgpd:
if not path.startswith("cgu"): if not path.startswith("cgu-rgpd"):
return HttpResponseRedirect("/cgu/?next={}".format(full_path)) return HttpResponseRedirect("/cgu-rgpd/?next={}".format(full_path))
# Handling of banned users # Handling of banned users
elif request.user.is_banned and full_path != "/banned_note/": elif request.user.is_banned and full_path != "/banned_note/":
return HttpResponseRedirect("/banned_note/") return HttpResponseRedirect("/banned_note/")
# 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),
),
]
...@@ -28,6 +28,10 @@ def validate(user, allow_sharing_personal_info): ...@@ -28,6 +28,10 @@ def validate(user, allow_sharing_personal_info):
class User(AbstractUser): class User(AbstractUser):
"""
If you modify this model, don't forget to modify the command that handles its emptying for RGPD
"""
@cached_property @cached_property
def cached_groups(self) -> List[str]: def cached_groups(self) -> List[str]:
out = ["authenticated_user"] out = ["authenticated_user"]
...@@ -40,10 +44,15 @@ class User(AbstractUser): ...@@ -40,10 +44,15 @@ class User(AbstractUser):
pseudo = models.CharField( pseudo = models.CharField(
blank=False, null=False, max_length=12, default="Anonymous42" blank=False, null=False, max_length=12, default="Anonymous42"
) )
has_validated_cgu = models.BooleanField(default=False, null=False) has_validated_cgu_rgpd = models.BooleanField(default=False, null=False)
has_validated_rgpd = models.BooleanField(default=False, null=False)
is_banned = 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): def save(self, *args, **kwargs):
""" """
Custom save function to ensure consistency. Custom save function to ensure consistency.
...@@ -75,6 +84,7 @@ class UserSerializer(BaseModelSerializer): ...@@ -75,6 +84,7 @@ class UserSerializer(BaseModelSerializer):
"allow_sharing_personal_info", "allow_sharing_personal_info",
"secondary_email", "secondary_email",
"pseudo", "pseudo",
"delete_next_time",
"is_staff", "is_staff",
) )
read_only_fields = ("username", "first_name", "last_name", "email") read_only_fields = ("username", "first_name", "last_name", "email")
......
...@@ -179,7 +179,8 @@ AUTH_PASSWORD_VALIDATORS = [ ...@@ -179,7 +179,8 @@ AUTH_PASSWORD_VALIDATORS = [
] ]
LOGIN_URL = "/user/login" LOGIN_URL = "/user/login"
LOGIN_EXEMPT_URLS = [LOGIN_URL] RGPD_URL = "/rgpd-raw"
LOGIN_EXEMPT_URLS = [LOGIN_URL, RGPD_URL]
if TESTING: if TESTING:
# In a testing environment we may want to by pass the loginRequiredMiddleware # In a testing environment we may want to by pass the loginRequiredMiddleware
......
{% load static %}
<!DOCTYPE html>
<html style="font-size:14">
<head>
<meta
charset="utf-8"
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
/>
<!-- Custom colors for phone status bar -->
<meta name="theme-color" content="#9c27b0">
<meta name="apple-mobile-web-app-status-bar-style" content="#9c27b0">
<!-- Favicon hell -->
<link rel="apple-touch-icon-precomposed" sizes="57x57" href="{% static '/base_app/favicon/apple-touch-icon-57x57.png' %}" />
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="{% static '/base_app/favicon/apple-touch-icon-114x114.png' %}" />
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="{% static '/base_app/favicon/apple-touch-icon-72x72.png' %}" />
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="{% static '/base_app/favicon/apple-touch-icon-144x144.png' %}" />
<link rel="apple-touch-icon-precomposed" sizes="60x60" href="{% static '/base_app/favicon/apple-touch-icon-60x60.png' %}" />
<link rel="apple-touch-icon-precomposed" sizes="120x120" href="{% static '/base_app/favicon/apple-touch-icon-120x120.png' %}" />
<link rel="apple-touch-icon-precomposed" sizes="76x76" href="{% static '/base_app/favicon/apple-touch-icon-76x76.png' %}" />
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="{% static '/base_app/favicon/apple-touch-icon-152x152.png' %}" />
<link rel="icon" type="image/png" href="{% static '/base_app/favicon/favicon-196x196.png' %}" sizes="196x196" />
<link rel="icon" type="image/png" href="{% static '/base_app/favicon/favicon-96x96.png' %}" sizes="96x96" />
<link rel="icon" type="image/png" href="{% static '/base_app/favicon/favicon-32x32.png' %}" sizes="32x32" />
<link rel="icon" type="image/png" href="{% static '/base_app/favicon/favicon-16x16.png' %}" sizes="16x16" />
<link rel="icon" type="image/png" href="{% static '/base_app/favicon/favicon-128.png' %}" sizes="128x128" />
<!-- End of favicon hell -->
<title>REX-DRI</title>
</head>
<body>
<div id="app" class="columns">
<!-- React -->
</div>
</body>
{% block content %}{% endblock %}
<noscript>Cette application nécessite Javascript... Merci de l'activer ou d'utiliser un navigateur approprié.</noscript>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<br>
<br>
<form method="post">
{% csrf_token %}
Valider le CGU
{{ form.has_validated_cgu }}
<br>
<br>
Valider la charte RGPD
{{ form.has_validated_rgpd }}
<br>
<br>
<input type="submit" value="Valider">
</form>
</body>
</html>
\ No newline at end of file
{% extends "base.html" %}
{% load render_bundle from webpack_loader %}
{% block content %}
<script>
var __hasValidatedCguRgpd = {% if user.has_validated_cgu_rgpd %} true {% else %} false {% endif %};
</script>
{% render_bundle 'rgpdCgu' %}
{% render_bundle 'vendor' %}
{% if user.has_validated_cgu_rgpd %}
{% else %}
<div style="display: none;">
<!-- Hidden form to be used by react to submit the values -->
<form method="post">
{% csrf_token %}
{{ form.has_validated_cgu_rgpd }}
<br>
<input type="submit" value="Valider" id="formSubmitButton">
</form>
</div>
{% endif %}
{% endblock %}
{% extends "base.html" %}
{% load render_bundle from webpack_loader %} {% load render_bundle from webpack_loader %}
{% load static %}
<!DOCTYPE html>
<html style="font-size:14">
<head> {% block content %}
<meta
charset="utf-8"
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
/>
<!-- Custom colors for phone status bar -->
<meta name="theme-color" content="#9c27b0">
<meta name="apple-mobile-web-app-status-bar-style" content="#9c27b0">
<!-- Favicon hell -->
<link rel="apple-touch-icon-precomposed" sizes="57x57" href="{% static '/base_app/favicon/apple-touch-icon-57x57.png' %}" />
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="{% static '/base_app/favicon/apple-touch-icon-114x114.png' %}" />
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="{% static '/base_app/favicon/apple-touch-icon-72x72.png' %}" />
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="{% static '/base_app/favicon/apple-touch-icon-144x144.png' %}" />
<link rel="apple-touch-icon-precomposed" sizes="60x60" href="{% static '/base_app/favicon/apple-touch-icon-60x60.png' %}" />
<link rel="apple-touch-icon-precomposed" sizes="120x120" href="{% static '/base_app/favicon/apple-touch-icon-120x120.png' %}" />
<link rel="apple-touch-icon-precomposed" sizes="76x76" href="{% static '/base_app/favicon/apple-touch-icon-76x76.png' %}" />
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="{% static '/base_app/favicon/apple-touch-icon-152x152.png' %}" />
<link rel="icon" type="image/png" href="{% static '/base_app/favicon/favicon-196x196.png' %}" sizes="196x196" />
<link rel="icon" type="image/png" href="{% static '/base_app/favicon/favicon-96x96.png' %}" sizes="96x96" />
<link rel="icon" type="image/png" href="{% static '/base_app/favicon/favicon-32x32.png' %}" sizes="32x32" />
<link rel="icon" type="image/png" href="{% static '/base_app/favicon/favicon-16x16.png' %}" sizes="16x16" />
<link rel="icon" type="image/png" href="{% static '/base_app/favicon/favicon-128.png' %}" sizes="128x128" />
<!-- End of favicon hell -->
<title>REX-DRI</title>
</head>
<body>
<div id="app" class="columns">
<!-- React -->
</div>
</body>
<script> <script>
var __AppUserId = {{ user.pk }}; var __AppUserId = {{ user.pk }};
var __AllBackendEndPointsRoutes = {{ endpoints|safe }}; var __AllBackendEndPointsRoutes = {{ endpoints|safe }};
</script> </script>
{% render_bundle 'main' %} {% render_bundle 'main' %}
{% render_bundle 'vendor' %} {% render_bundle 'vendor' %}
<link rel="stylesheet" href="{{ front_bundle_loc }}mapbox-gl-dist/mapbox-gl.css"/> <link rel="stylesheet" href="{{ front_bundle_loc }}mapbox-gl-dist/mapbox-gl.css"/>
<noscript>Cette application nécessite Javascript... Merci de l'activer ou d'utiliser un navigateur approprié.</noscript>
</html> {% endblock %}
{% extends "base.html" %}
{% load render_bundle from webpack_loader %}
{% block content %}
{% render_bundle 'rgpdRaw' %}
{% render_bundle 'vendor' %}
{% endblock %}
...@@ -16,15 +16,13 @@ class HasAccessTestCase(TestCase): ...@@ -16,15 +16,13 @@ class HasAccessTestCase(TestCase):
cls.has_validated_user = User.objects.create_user( cls.has_validated_user = User.objects.create_user(
username="yolo", email="osef@master.fr", password=password username="yolo", email="osef@master.fr", password=password
) )
cls.has_validated_user.has_validated_cgu = True cls.has_validated_user.has_validated_cgu_rgpd = True
cls.has_validated_user.has_validated_rgpd = True
cls.has_validated_user.save() cls.has_validated_user.save()
cls.has_not_validated_user = User.objects.create_user( cls.has_not_validated_user = User.objects.create_user(
username="yolo2", email="osef@master.fr", password=password username="yolo2", email="osef@master.fr", password=password
) )
cls.has_not_validated_user.has_validated_cgu = False cls.has_not_validated_user.has_validated_cgu_rgpd = False
cls.has_not_validated_user.has_validated_rgpd = False
cls.has_not_validated_user.save() cls.has_not_validated_user.save()
cls.has_validated_client = APIClient() cls.has_validated_client = APIClient()
...@@ -44,4 +42,4 @@ class HasAccessTestCase(TestCase): ...@@ -44,4 +42,4 @@ class HasAccessTestCase(TestCase):
def test_not_accepted_refused(self): def test_not_accepted_refused(self):
response = self.has_not_validated_client.get("/api/") response = self.has_not_validated_client.get("/api/")
self.assertEqual(response.status_code, 302) # We have a redirect 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/")
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)
...@@ -26,7 +26,8 @@ urlpatterns += [ ...@@ -26,7 +26,8 @@ urlpatterns += [
name="cas_ng_proxy_callback", name="cas_ng_proxy_callback",
), ),
url(r"^app/.*", views.index), 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"^banned_note/", views.banned),
url(r"^$", RedirectView.as_view(url="./app/"), name="go to real home"), url(r"^$", RedirectView.as_view(url="./app/"), name="go to real home"),
url(r"", include("backend_app.urls")), url(r"", include("backend_app.urls")),
......
...@@ -12,6 +12,10 @@ from base_app.models import User ...@@ -12,6 +12,10 @@ from base_app.models import User
logger = logging.getLogger("django") logger = logging.getLogger("django")
def get_bundle_loc(name):
return "/".join(get_files(name)[0]["url"].split("/")[:-1]) + "/"
def index(request): def index(request):
""" """
View to to display the index app that contains the JS / CSS View to to display the index app that contains the JS / CSS
...@@ -23,7 +27,7 @@ def index(request): ...@@ -23,7 +27,7 @@ def index(request):
user = request.user user = request.user
# small hack to get the correct location of the frontend bundled files # 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 # We also retrieve the list of all routes endpoints
endpoints = list(map(lambda v: clean_route(v.end_point_route), ALL_VIEWSETS)) endpoints = list(map(lambda v: clean_route(v.end_point_route), ALL_VIEWSETS))
...@@ -34,7 +38,17 @@ def index(request): ...@@ -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": if request.method == "POST":
user = User.objects.get(pk=request.user.pk) user = User.objects.get(pk=request.user.pk)
form = UserForm(request.POST, instance=user) form = UserForm(request.POST, instance=user)
...@@ -45,11 +59,11 @@ def cgu(request): ...@@ -45,11 +59,11 @@ def cgu(request):
user = User.objects.get(pk=request.user.pk) user = User.objects.get(pk=request.user.pk)
form = UserForm(instance=user) 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 # if the user has just validated everything, we redirect him to the location he requested
return HttpResponseRedirect(request.GET["next"]) return HttpResponseRedirect(request.GET["next"])
else: 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): def banned(request):
......
...@@ -63,7 +63,7 @@ services: ...@@ -63,7 +63,7 @@ services:
# Service to handle frontend live developpments and building # Service to handle frontend live developpments and building
frontend: frontend:
# Get the image from the registry # 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. # To use a locally built one, comment above, uncomment bellow.
# build: ./frontend # build: ./frontend
# On startup, we retrieve the dependencies from the image and start the developpement server # On startup, we retrieve the dependencies from the image and start the developpement server
......
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.