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
...@@ -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.
Toutefois, vous autorisez le traitement de ce contenu (tant qu'il est accessible) par les administrateurs du site à des fins statistiques.
### Cadre d'utilisation