Commit 5c12d3c5 authored by BERJON Matthieu's avatar BERJON Matthieu Committed by BAIRE Anthony
Browse files

Adding a mixin and view for the ToS validation

I added a mixin that checks if the user has accepted the latest ToS
version. If not the user is redirected to the ToS validation view. Once
accepted the user is redirected to the page he asked first.

I updated all the `login required` views by adding this new mixin.
One major issue of this code that the redirection argument passed to the
ToS validation view is the url name which is not a good practice I
think. A better case would to use the path but I wasn't able to write
the right regex in the url dispatcher.

Another issue is that the user won't be redirected at login or sign up
to the ToS validation view. This should be handled in the ``

Signed-off-by: BERJON Matthieu's avatarMatthieu Berjon <>
parent f1425cc9
......@@ -16,6 +16,8 @@ from .models import (
from .validators import docker_name_validator
......@@ -233,3 +235,17 @@ class WebappImportForm(forms.Form):
docker_name = forms.CharField(label="Short name", required=False,
class TosForm(forms.ModelForm):
valid = forms.BooleanField(
label=mark_safe('Check here to indicate that you have read and agree to the terms of the <a href="{% url \'main:tos_detail\' %}">A||Go policy</a>.'))
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['tos'].initial = Tos.objects.order_by('-version').first()
class Meta:
model = UserAgreement
fields = ('tos',)
from django.conf import settings
from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse, resolve
from django.http import HttpResponse, JsonResponse
from django.shortcuts import redirect
from .models import Job
from .models import Job, Tos, UserAgreement
from .helpers import get_request_user
......@@ -97,3 +98,22 @@ class JobAuthMixin(AllgoValidAccountMixin, UserPassesTestMixin):
return JsonResponse({"error": "401 Unauthorized"}, status=401)
return super().handle_no_permission()
class HasSignedTosMixin(object):
"""Check if the user has signed the latest ToS"""
def dispatch(self, request, *args, **kwargs):
"""Check if the user has signed the latest ToS
Redirects to the ToS form to sign or let the user reach the page he
asked for
redirect_to = request.path
url_name = resolve(redirect_to)
last_tos = Tos.objects.order_by('-version').first()
user_agreement = UserAgreement.objects.filter(user=request.user, tos=last_tos)
if user_agreement:
return super().dispatch(request, *args, **kwargs)
return redirect(reverse('main:tos_validation', args=[url_name.url_name]))
......@@ -78,6 +78,7 @@ urlpatterns = [
# Terms of service urls
url(r'^tos$', views.TosDetail.as_view(), name='tos_detail'),
url(r'^tos_validation/(?P<url_name>[-\w]+)$', views.TosValidation.as_view(), name='tos_validation'),
# url(r'^runners/$', views.RunnerList.as_view(), name='runner_list'),
# url(r'^runners/_add$', views.RunnerCreate.as_view(), name='runner_create'),
......@@ -67,11 +67,12 @@ from .forms import (
# Local imports
import config
from .helpers import get_base_url, get_ssh_data, upload_data, notify_controller, lookup_job_file, get_request_user, query_webapps_for_user
from .mixins import UserAccessMixin, ProviderAccessMixin, AllAccessMixin, JobAuthMixin
from .mixins import UserAccessMixin, ProviderAccessMixin, AllAccessMixin, JobAuthMixin, HasSignedTosMixin
from .models import (
......@@ -83,6 +84,7 @@ from .models import (
from .signals import job_post_save
from .templatetags.converters import status_icon
......@@ -205,7 +207,7 @@ class UserWebappList(AllAccessMixin, ListView):
return queryset
class WebappUpdate(UserAccessMixin, SuccessMessageMixin, UpdateView):
class WebappUpdate(UserAccessMixin, HasSignedTosMixin, SuccessMessageMixin, UpdateView):
"""Form to update the webapp data
......@@ -256,7 +258,7 @@ class WebappUpdate(UserAccessMixin, SuccessMessageMixin, UpdateView):
return super(WebappUpdate, self).form_invalid(form)
class WebappCreate(ProviderAccessMixin, SuccessMessageMixin, CreateView):
class WebappCreate(ProviderAccessMixin, HasSignedTosMixin, SuccessMessageMixin, CreateView):
"""Create a new webapp
......@@ -340,7 +342,7 @@ def get_rails_webapp_metadata(*, webapp_id=None, docker_name=None):
log.error("webapp import error: failed to get %s (%s)", url, e)
class WebappImport(ProviderAccessMixin, SuccessMessageMixin, FormView):
class WebappImport(ProviderAccessMixin, HasSignedTosMixin, SuccessMessageMixin, FormView):
"""Import a new webapp
This only creates the Webapp entry (along with the tags and webapp
......@@ -451,7 +453,7 @@ class WebappImport(ProviderAccessMixin, SuccessMessageMixin, FormView):
self.object = webapp
return super().form_valid(form)
class WebappVersionImport(UserAccessMixin, DetailView):
class WebappVersionImport(UserAccessMixin, HasSignedTosMixin, DetailView):
"""Import version
This view is enabled only for webapps created with imported=True
......@@ -546,7 +548,7 @@ class WebappVersionImport(UserAccessMixin, DetailView):
return HttpResponseRedirect(request.path_info)
class WebappJson(UserAccessMixin, DetailView):
class WebappJson(UserAccessMixin, HasSignedTosMixin, DetailView):
"""json variant of the application details
(used by the /aio/apps/<DOCKER_NAME>/events endpoint)
......@@ -565,7 +567,7 @@ class WebappJson(UserAccessMixin, DetailView):
"sandbox_state": webapp.get_sandbox_state_display(),
class WebappSandboxPanel(UserAccessMixin, TemplateView):
class WebappSandboxPanel(UserAccessMixin, HasSignedTosMixin, TemplateView):
"""Create a new sandbox for a given application
......@@ -783,7 +785,7 @@ class TagWebappList(AllAccessMixin, ListView):
# -----------------------------------------------------------------------------
class UserUpdate(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
class UserUpdate(LoginRequiredMixin, HasSignedTosMixin, SuccessMessageMixin, UpdateView):
"""Update the user profile
......@@ -824,7 +826,7 @@ class UserUpdate(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
return super(UserUpdate, self).get_context_data(**kwargs)
class UserToken(LoginRequiredMixin, RedirectView):
class UserToken(LoginRequiredMixin, HasSignedTosMixin, RedirectView):
"""Regenerate the user token"""
success_message = 'Token generated successfully.'
......@@ -842,7 +844,7 @@ class UserToken(LoginRequiredMixin, RedirectView):
return reverse('main:user_detail')
class UserSSHAdd(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
class UserSSHAdd(LoginRequiredMixin, HasSignedTosMixin, SuccessMessageMixin, UpdateView):
"""Save a SSH key to the database.
......@@ -863,7 +865,7 @@ class UserSSHAdd(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
return AllgoUser.objects.get(
class UserSSHDelete(LoginRequiredMixin, RedirectView):
class UserSSHDelete(LoginRequiredMixin, HasSignedTosMixin, RedirectView):
"""Delete the user SSH key"""
success_message = 'The SSH key has been successfully deleted.'
......@@ -881,7 +883,7 @@ class UserSSHDelete(LoginRequiredMixin, RedirectView):
return reverse('main:user_detail')
class UserPasswordUpdate(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
class UserPasswordUpdate(LoginRequiredMixin, HasSignedTosMixin, SuccessMessageMixin, UpdateView):
"""Update the user's password.
We reuse the Django password form system in order to keep something robust
......@@ -959,7 +961,7 @@ class UserNeedValidation(LoginRequiredMixin, DetailView):
# -----------------------------------------------------------------------------
class JobList(UserAccessMixin, ListView):
class JobList(UserAccessMixin, HasSignedTosMixin, ListView):
"""Display the list of jobs for a given identified user
......@@ -1301,7 +1303,7 @@ class JobFileDownloadAll(JobAuthMixin, View):
# -----------------------------------------------------------------------------
class RunnerList(UserAccessMixin, ListView):
class RunnerList(UserAccessMixin, HasSignedTosMixin, ListView):
"""List all runners of a given user
......@@ -1339,7 +1341,7 @@ class RunnerList(UserAccessMixin, ListView):
return super().get_context_data(**kwargs)
class RunnerCreate(ProviderAccessMixin, SuccessMessageMixin, CreateView):
class RunnerCreate(ProviderAccessMixin, HasSignedTosMixin, SuccessMessageMixin, CreateView):
"""Create a runner and save it into the database
......@@ -1386,7 +1388,7 @@ class RunnerCreate(ProviderAccessMixin, SuccessMessageMixin, CreateView):
return reverse_lazy('main:runner_update', args=(,))
class RunnerUpdate(UserAccessMixin, SuccessMessageMixin, UpdateView):
class RunnerUpdate(UserAccessMixin, HasSignedTosMixin, SuccessMessageMixin, UpdateView):
"""Update a runner and save it into the database
......@@ -1415,7 +1417,7 @@ class RunnerUpdate(UserAccessMixin, SuccessMessageMixin, UpdateView):
"allgo/runner", "-", self.object.token, get_base_url(self.request)]
return super().get_context_data(**kwargs)
class RunnerDelete(UserAccessMixin, DeleteView):
class RunnerDelete(UserAccessMixin, HasSignedTosMixin, DeleteView):
"""Delete a runner
......@@ -1461,6 +1463,38 @@ def auth(request):
return HttpResponse(status=403)
class TosValidation(SuccessMessageMixin, FormView):
model = UserAgreement
form_class = TosForm
success_message = 'You validated the Terms of Service.'
success_url = reverse_lazy('main:home')
template_name = 'tos_validation.html'
def form_valid(self, form):
""" Validate some fields before saving them
Ensure that the validation button has been checked (in case the javascript
system fails. If not an error message is displayed to the user and he
is redirected to this view.
obj =
if not form.cleaned_data.get('valid'):
messages.add_message(self.request, messages.ERROR, 'You must accept the Terms of Service to use this service.')
log.warning("Someone tried to use the service without accepting the Terms of Service.")
return redirect('main:tos_validation', webapp.docker_name)
obj.user = self.request.user
url_name = 'main:' + self.kwargs['url_name']
return redirect(url_name)
def get_context_data(self, **kwargs):
""" Send to the template the lastest ToS data. """
kwargs['tos_data'] = Tos.objects.order_by('-version').first()
return super().get_context_data(**kwargs)
class TosDetail(DetailView):
template_name = 'tos_detail.html'
context_object_name = 'tos'
{% extends "base.html" %}
{% load converters htmlattrs %}
{% block title %}Validate the Terms of Service{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item active">Terms of Service</li>
{% endblock %}
{% block content %}
<div class="container">
<div class="allgo-page">
<form method="post">
{% csrf_token %}
{{ tos_data.content | markdown | safe }}
<p>You can get the Terms of Service in <a href="{{ tos_data.url }}">PDF format</a>.</p>
<div class="form-group form-check">
{{ form.valid | add_class:"form-check-input"}}
{{ form.valid.label_tag }}
{{ form.tos.as_hidden }}
<input class="btn btn-primary" type="submit" value="Continue">
{% endblock %}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment