parent
0a9efb7684
commit
c73c5b400a
|
@ -10,6 +10,7 @@ X-Python-Version: >= 2.6
|
|||
Package: python-authentic2-cut
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends},
|
||||
python-authentic2 (>= 2.1.20.1054)
|
||||
python-authentic2 (>= 2.1.20.1054),
|
||||
python-pil
|
||||
Description: CUT plugin for Authentic2
|
||||
|
||||
|
|
2
setup.py
2
setup.py
|
@ -96,6 +96,8 @@ setup(
|
|||
},
|
||||
install_requires=[
|
||||
'authentic2',
|
||||
'Pillow',
|
||||
'python-magic',
|
||||
],
|
||||
entry_points={
|
||||
'authentic2.plugin': [
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
class AttachmentsInline(admin.TabularInline):
|
||||
model = models.ValidationRequestAttachment
|
||||
|
||||
|
||||
class ValidationRequestAdmin(admin.ModelAdmin):
|
||||
list_display = ('created', 'user', 'origin', 'status', 'validated')
|
||||
readonly_fields = ['created']
|
||||
inlines = [AttachmentsInline]
|
||||
|
||||
|
||||
admin.site.register(models.ValidationRequest, ValidationRequestAdmin)
|
|
@ -0,0 +1,195 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# authentic2_cut - Authentic2 plugin for CUT
|
||||
# Copyright (C) 2017 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import base64
|
||||
import magic
|
||||
import uuid
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db.transaction import atomic
|
||||
from django.conf import settings
|
||||
from django.db import IntegrityError
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from rest_framework.generics import ListAPIView
|
||||
from rest_framework import serializers, pagination, permissions
|
||||
from rest_framework.decorators import list_route, detail_route
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
from django_filters.rest_framework import FilterSet
|
||||
|
||||
from authentic2_idp_oidc.utils import make_pairwise_reversible_sub
|
||||
from authentic2 import api_views
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
class Base64ImageField(serializers.FileField):
|
||||
def to_internal_value(self, data):
|
||||
# base64 encoded image - decode
|
||||
if not isinstance(data, unicode):
|
||||
raise ValidationError('must be base64 string')
|
||||
id = uuid.uuid4()
|
||||
try:
|
||||
content = base64.b64decode(data)
|
||||
except:
|
||||
raise ValidationError('invalid base64')
|
||||
data = ContentFile(content, name = id.urn[9:] + '.jpg')
|
||||
return super(Base64ImageField, self).to_internal_value(data)
|
||||
|
||||
|
||||
class CUTValidateAttachmentSerializer(serializers.Serializer):
|
||||
b64_content = Base64ImageField()
|
||||
|
||||
|
||||
class CUTCreateValidateSerializer(serializers.Serializer):
|
||||
justificatifs = serializers.ListField(
|
||||
child=CUTValidateAttachmentSerializer(),
|
||||
allow_empty=False)
|
||||
external_id = serializers.CharField()
|
||||
|
||||
|
||||
def get_origin(request):
|
||||
origin = request.user
|
||||
if hasattr(origin, 'oidc_client'):
|
||||
origin = origin.oidc_client
|
||||
return origin
|
||||
|
||||
|
||||
class CUTValidateSerializer(serializers.ModelSerializer):
|
||||
sub = serializers.SerializerMethodField()
|
||||
|
||||
def get_sub(self, instance):
|
||||
request = self.context['request']
|
||||
origin = get_origin(request)
|
||||
if hasattr(origin, 'identifier_policy'):
|
||||
oidc_client = origin
|
||||
if oidc_client.identifier_policy == oidc_client.POLICY_PAIRWISE_REVERSIBLE:
|
||||
return make_pairwise_reversible_sub(oidc_client, instance.user)
|
||||
else:
|
||||
return ''
|
||||
return instance.user.uuid
|
||||
|
||||
class Meta:
|
||||
model = models.ValidationRequest
|
||||
fields = ('id', 'created', 'external_id', 'status', 'reason', 'validated', 'sub')
|
||||
|
||||
|
||||
@detail_route(methods=['get', 'post'], url_path='validate', permission_classes=(api_views.DjangoPermission('custom_user.cut_validate_user'),))
|
||||
def validate_cut(self, request, uuid):
|
||||
user = self.get_object()
|
||||
origin = get_origin(request)
|
||||
|
||||
if request.method == 'GET':
|
||||
qs = models.ValidationRequest.objects.for_origin(origin).filter(user=user).select_related('user')
|
||||
serializer = CUTValidateSerializer(qs, many=True, context={'request': request})
|
||||
return Response({
|
||||
'result': 1,
|
||||
'next': None,
|
||||
'previous': None,
|
||||
'results': serializer.data,
|
||||
})
|
||||
serializer = CUTCreateValidateSerializer(data=request.data)
|
||||
max_size = getattr(settings, 'CUT_VALIDATION_ATTACHMENT_MAX_SIZE', 300 * 1024 * 1024)
|
||||
if serializer.is_valid():
|
||||
errors = []
|
||||
attachments = serializer.validated_data['justificatifs']
|
||||
external_id = serializer.validated_data['external_id']
|
||||
for i, attachment in enumerate(attachments):
|
||||
content = attachment['b64_content']
|
||||
if content.size > max_size:
|
||||
errors.append({
|
||||
'code': 'justificatifs-too-big',
|
||||
'page': i,
|
||||
'max-size': max_size,
|
||||
})
|
||||
if magic.from_buffer(content.read(10000), mime=True) != 'image/jpeg':
|
||||
errors.append({
|
||||
'code': 'justificatifs-bad-format',
|
||||
'page': i,
|
||||
'accepted': ['image/jpeg'],
|
||||
})
|
||||
# rewind cursor
|
||||
content.seek(0)
|
||||
if models.ValidationRequest.objects.filter(
|
||||
user=user, external_id=external_id).exists():
|
||||
errors.append({
|
||||
'code': 'already-exists',
|
||||
})
|
||||
if errors:
|
||||
response = {'result': 0, 'errors': errors}
|
||||
return Response(response, status.HTTP_400_BAD_REQUEST)
|
||||
with atomic():
|
||||
try:
|
||||
validation_request = models.ValidationRequest.objects.create(
|
||||
user=user, external_id=external_id, origin=origin)
|
||||
except IntegrityError as e:
|
||||
response = {'result': 0, 'errors': [{'code': 'already-exists'}]}
|
||||
return Response(response, status.HTTP_400_BAD_REQUEST)
|
||||
for attachment in attachments:
|
||||
models.ValidationRequestAttachment.objects.create(
|
||||
validation_request=validation_request,
|
||||
image=attachment['b64_content'])
|
||||
return Response({
|
||||
'result': 1,
|
||||
'status': 'received',
|
||||
'sub': uuid,
|
||||
'id': validation_request.id,
|
||||
'external_id': external_id,
|
||||
})
|
||||
else:
|
||||
errors = []
|
||||
for key in serializer.errors:
|
||||
errors.append({'code': 'schema-error', 'field': key, 'sub-errors': serializer.errors[key]})
|
||||
response = {'result': 0, 'errors': errors}
|
||||
return Response(response, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# attach new API to users' api
|
||||
api_views.UsersAPI.validate_cut = validate_cut
|
||||
|
||||
# re-register UsersAPI to get validate_cut
|
||||
#api_views.router.register(r'users', api_views.UsersAPI, base_name='a2-api-users')
|
||||
|
||||
|
||||
class ValidateFilter(FilterSet):
|
||||
class Meta:
|
||||
model = models.ValidationRequest
|
||||
fields = {
|
||||
'validated': {
|
||||
'gt',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ValidateAPI(api_views.ExceptionHandlerMixin, ListAPIView):
|
||||
methods = ['GET']
|
||||
ordering_fields = ['id']
|
||||
serializer_class = CUTValidateSerializer
|
||||
pagination_class = pagination.CursorPagination
|
||||
ordering = ['id']
|
||||
filter_class = ValidateFilter
|
||||
permission_classes = (api_views.DjangoPermission('custom_user.cut_validate_user'),)
|
||||
|
||||
def get_queryset(self):
|
||||
qs = models.ValidationRequest.objects
|
||||
origin = get_origin(self.request)
|
||||
qs = qs.for_origin(origin).select_related('user')
|
||||
return qs
|
||||
|
||||
|
||||
validate = ValidateAPI.as_view()
|
|
@ -43,6 +43,7 @@ class AppConfig(django.apps.AppConfig):
|
|||
def ready(self):
|
||||
from django.db.models.signals import post_migrate, post_save
|
||||
from django_rbac.utils import get_ou_model
|
||||
from . import api_views
|
||||
|
||||
post_migrate.connect(
|
||||
self.post_migrate,
|
||||
|
@ -72,7 +73,7 @@ class AppConfig(django.apps.AppConfig):
|
|||
return user_datas
|
||||
|
||||
def a2_hook_manager_modify_form(self, view, form):
|
||||
from django.forms.widgets import DateTimeInput, HiddenInput
|
||||
from django.forms.widgets import DateTimeInput, HiddenInput, Textarea
|
||||
from authentic2.passwords import generate_password
|
||||
|
||||
from . import models
|
||||
|
@ -106,6 +107,7 @@ class AppConfig(django.apps.AppConfig):
|
|||
form.fields['creation_partner'].initial = (
|
||||
view.request.user.ou.name if view.request.user.ou else 'super-utilisateur')
|
||||
form.fields['creation_partner'].widget = HiddenInput()
|
||||
form.fields['comment'].widget = Textarea(attrs={'rows': 4})
|
||||
del form.fields['creation_domain']
|
||||
else:
|
||||
del form.fields['creation_domain']
|
||||
|
@ -134,6 +136,7 @@ class AppConfig(django.apps.AppConfig):
|
|||
'validation_context', 'validation_date', 'ou']:
|
||||
if field_name in form.fields:
|
||||
del form.fields[field_name]
|
||||
form.fields['comment'].widget = Textarea(attrs={'rows': 4})
|
||||
else:
|
||||
for field_name in form.fields:
|
||||
if field_name not in ['username',
|
||||
|
@ -197,6 +200,7 @@ class AppConfig(django.apps.AppConfig):
|
|||
'creation_partner', 'creation_domain']:
|
||||
if field_name in form.fields:
|
||||
del form.fields[field_name]
|
||||
form.fields['comment'].widget = Textarea(attrs={'readonly': '', 'rows': 4})
|
||||
else:
|
||||
for field_name in form.fields:
|
||||
if field_name not in ['username', 'first_name', 'last_name', 'email']:
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# authentic2_cut - Authentic2 plugin for CUT
|
||||
# Copyright (C) 2017 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django import forms
|
||||
|
||||
from . import models
|
||||
|
||||
class ValidationForm(forms.Form):
|
||||
reason = forms.ChoiceField(
|
||||
choices=models.ValidationRequest.REASON_CHOICES,
|
||||
label=u'Raison du refus')
|
|
@ -0,0 +1,39 @@
|
|||
import os
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.transaction import atomic
|
||||
|
||||
from authentic2_idp_oidc.models import OIDCClient
|
||||
from authentic2_cut.models import ValidationRequest, ValidationRequestAttachment
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Create validation requests'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--ou-slug')
|
||||
parser.add_argument('--oidc-client-slug')
|
||||
parser.add_argument('--user-pk', type=int)
|
||||
parser.add_argument('paths', nargs='+')
|
||||
|
||||
def handle(self, ou_slug, oidc_client_slug, user_pk, paths, **options):
|
||||
oidc_client = OIDCClient.objects.get(ou__slug=ou_slug, slug=oidc_client_slug)
|
||||
User = get_user_model()
|
||||
user = User.objects.get(pk=user_pk)
|
||||
|
||||
with atomic():
|
||||
validation_request = ValidationRequest.objects.create(
|
||||
user=user,
|
||||
origin=oidc_client)
|
||||
for path in paths:
|
||||
|
||||
with open(path) as file_object:
|
||||
filename = os.path.basename(path)
|
||||
f = ContentFile(file_object.read(), name=filename)
|
||||
ValidationRequestAttachment.objects.create(
|
||||
validation_request=validation_request,
|
||||
image=f)
|
||||
self.stdout.write('New validation request %s' % validation_request.pk)
|
|
@ -0,0 +1,56 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('authentic2_cut', '0002_auto_20171116_1052'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ValidationRequest',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Date de cr\xe9ation', db_index=True)),
|
||||
('origin_id', models.PositiveIntegerField(verbose_name=b'origin id')),
|
||||
('status', models.CharField(default=b'received', max_length=16, verbose_name='Statut', choices=[(b'received', 're\xe7u'), (b'accepted', 'accept\xe9'), (b'refused', 'refus\xe9')])),
|
||||
('reason', models.TextField(blank=True, verbose_name='Raison du refus', choices=[(b'unreadable', 'pi\xe8ce(s) illisible(s)'), (b'invalid', 'pi\xe8ce(s) invalides(s)'), (b'underaged', 'invividu mineur')])),
|
||||
('validated', models.DateTimeField(null=True, verbose_name='Date de validation')),
|
||||
('external_id', models.TextField(null=True, verbose_name='Identifiant externe', blank=True)),
|
||||
('taken', models.DateTimeField(null=True, verbose_name='En cours')),
|
||||
('origin_ct', models.ForeignKey(verbose_name=b'origin ct', to='contenttypes.ContentType')),
|
||||
('taken_by', models.ForeignKey(related_name='validation_requests_taken', verbose_name='Trait\xe9 par', to=settings.AUTH_USER_MODEL, null=True)),
|
||||
('user', models.ForeignKey(related_name='validation_requests', verbose_name='Utilisateur', to=settings.AUTH_USER_MODEL)),
|
||||
('validated_by', models.ForeignKey(related_name='validation_requests_validated', verbose_name='Valid\xe9/refus\xe9 par', to=settings.AUTH_USER_MODEL, null=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-created',),
|
||||
'verbose_name': 'Requ\xeate de validation',
|
||||
'verbose_name_plural': 'Requ\xeates de validation',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ValidationRequestAttachment',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('image', models.ImageField(upload_to=b'', verbose_name=b'contenu')),
|
||||
('validation_request', models.ForeignKey(related_name='attachments', verbose_name='requ\xeate de validation', to='authentic2_cut.ValidationRequest')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('pk',),
|
||||
'verbose_name': 'Pi\xe8ce jointe',
|
||||
'verbose_name_plural': 'Pi\xe8ces jointes',
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='validationrequest',
|
||||
unique_together=set([('origin_ct', 'origin_id', 'user', 'external_id')]),
|
||||
),
|
||||
]
|
|
@ -1,5 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.db.models.query import Q
|
||||
from django.utils.timezone import now
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
|
||||
|
||||
class Journal(models.Model):
|
||||
|
@ -23,3 +32,127 @@ class Journal(models.Model):
|
|||
verbose_name = 'historique'
|
||||
verbose_name_plural = 'historiques'
|
||||
ordering = ('-timestamp', '-id')
|
||||
|
||||
|
||||
class ValidationRequestManager(models.Manager):
|
||||
def for_origin(self, origin):
|
||||
ct = ContentType.objects.get_for_model(origin)
|
||||
pk = origin.pk
|
||||
return self.filter(origin_ct=ct, origin_id=pk)
|
||||
|
||||
def next_request(self, user, after=None):
|
||||
qs = self.select_for_update()
|
||||
qs = qs.order_by('pk')
|
||||
# la demande est nouvelle
|
||||
# la demande est en cours de traitement mais
|
||||
# aucune date de prise en main n'est posée
|
||||
# la demande est prise depuis plus d'une heure
|
||||
# la demande est prise par <user>
|
||||
qs = qs.filter(
|
||||
Q(status=ValidationRequest.STATUS_RECEIVED)
|
||||
& (Q(taken__isnull=True)
|
||||
| Q(taken__lt=now() - timedelta(seconds=3600))
|
||||
| Q(taken_by=user)))
|
||||
# on veut la prochaine demande à traiter
|
||||
if after:
|
||||
qs = qs.filter(pk__gt=after.pk)
|
||||
validation_request = qs[:1].first()
|
||||
if validation_request:
|
||||
# la demande est associée à l'utilisateur en cours
|
||||
validation_request.taken_by = user
|
||||
validation_request.taken = now()
|
||||
validation_request.save()
|
||||
return validation_request
|
||||
|
||||
def received(self):
|
||||
return self.filter(status=ValidationRequest.STATUS_RECEIVED)
|
||||
|
||||
|
||||
class ValidationRequest(models.Model):
|
||||
STATUS_RECEIVED = 'received'
|
||||
STATUS_ACCEPTED = 'accepted'
|
||||
STATUS_REFUSED = 'refused'
|
||||
|
||||
STATUS_CHOICES = (
|
||||
(STATUS_RECEIVED, u'reçu'),
|
||||
(STATUS_ACCEPTED, u'accepté'),
|
||||
(STATUS_REFUSED, u'refusé'),
|
||||
)
|
||||
|
||||
REASON_UNREADABLE = 'unreadable'
|
||||
REASON_INVALID = 'invalid'
|
||||
REASON_UNDERAGED = 'underaged'
|
||||
REASON_CHOICES = (
|
||||
(REASON_UNREADABLE, u'pièce(s) illisible(s)'),
|
||||
(REASON_INVALID, u'pièce(s) invalides(s)'),
|
||||
(REASON_UNDERAGED, u'invividu mineur'),
|
||||
)
|
||||
|
||||
created = models.DateTimeField(
|
||||
verbose_name=u'Date de création',
|
||||
db_index=True,
|
||||
auto_now_add=True)
|
||||
origin_ct = models.ForeignKey(
|
||||
'contenttypes.ContentType',
|
||||
verbose_name='origin ct')
|
||||
origin_id = models.PositiveIntegerField(
|
||||
verbose_name='origin id')
|
||||
origin = GenericForeignKey('origin_ct', 'origin_id')
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
verbose_name=u'Utilisateur',
|
||||
related_name='validation_requests')
|
||||
status = models.CharField(
|
||||
max_length=16,
|
||||
choices=STATUS_CHOICES,
|
||||
verbose_name=u'Statut',
|
||||
default=STATUS_RECEIVED)
|
||||
reason = models.TextField(
|
||||
choices=REASON_CHOICES,
|
||||
blank=True,
|
||||
verbose_name=u'Raison du refus')
|
||||
validated = models.DateTimeField(
|
||||
null=True,
|
||||
verbose_name=u'Date de validation')
|
||||
validated_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
verbose_name=u'Validé/refusé par',
|
||||
null=True,
|
||||
related_name='validation_requests_validated')
|
||||
external_id = models.TextField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=u'Identifiant externe')
|
||||
taken = models.DateTimeField(
|
||||
verbose_name=u'En cours',
|
||||
null=True)
|
||||
taken_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
verbose_name=u'Traité par',
|
||||
null=True,
|
||||
related_name='validation_requests_taken')
|
||||
|
||||
objects = ValidationRequestManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = u'Requête de validation'
|
||||
verbose_name_plural = u'Requêtes de validation'
|
||||
ordering = ('-created',)
|
||||
unique_together = (
|
||||
('origin_ct', 'origin_id', 'user', 'external_id'),
|
||||
)
|
||||
|
||||
|
||||
class ValidationRequestAttachment(models.Model):
|
||||
validation_request = models.ForeignKey(
|
||||
ValidationRequest,
|
||||
verbose_name=u'requête de validation',
|
||||
related_name='attachments')
|
||||
|
||||
image = models.ImageField(
|
||||
verbose_name='contenu')
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
verbose_name = u'Pièce jointe'
|
||||
verbose_name_plural = u'Pièces jointes'
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
{% extends "authentic2/manager/form.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>Validation d'identité</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'a2-manager-users' %}{% if multiple_ou and object.ou %}?search-ou={{ object.ou.pk }}{% endif %}">{% trans 'Users' %}{% if multiple_ou and object.ou %} : {{ object.ou }}{% endif %}</a>
|
||||
<a href="{% url 'a2-manager-user-detail' pk=object.pk %}">{{ object.get_full_name }}</a>
|
||||
<a href="#">Validation d'identité</a>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<p>Validation demandée par {{ validation_request.origin }} le {{ validation_request.created }}, identifiant externe de la demande <em>{{ validation_request.external_id }}</em>.</p>
|
||||
|
||||
{% if validation_request.taken_by != user %}
|
||||
<p>Demande en cours de traitement par {{ validation_request.taken_by }} depuis le {{ validation_request.taken }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
{% if attachment_urls %}
|
||||
<h4>Pièces jointes</h4>
|
||||
<ul>
|
||||
{% for attachment_url in attachment_urls %}
|
||||
<li><a target="_blank" href="{{ attachment_url }}">Pièce jointe {{ forloop.counter }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h4>Identité</h4>
|
||||
<div>
|
||||
{{ block.super }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block buttons %}
|
||||
<div class="buttons">
|
||||
<button name="validate">Valider</button>
|
||||
</div>
|
||||
<h4>Refus</h4>
|
||||
{{ validation_form.as_p }}
|
||||
<div class="buttons">
|
||||
<button name="refuse">Refuser</button>
|
||||
<button style="float: right; margin-right: 30px" name="next">Suivant</button>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -17,7 +17,7 @@
|
|||
from django.conf.urls import patterns, url
|
||||
from authentic2.manager.urls import manager_login_required
|
||||
|
||||
from . import views
|
||||
from . import views, api_views
|
||||
from authentic2.decorators import required
|
||||
|
||||
urlpatterns = required(
|
||||
|
@ -35,6 +35,12 @@ urlpatterns = required(
|
|||
name='cut-manager-user-actions-journal'),
|
||||
url('^manage/users/(?P<pk>\d+)/modifications-journal/$', views.user_modifications_journal,
|
||||
name='cut-manager-user-modifications-journal'),
|
||||
url('^manage/validation/next/$', views.next_validation,
|
||||
name='cut-manager-user-next-validation'),
|
||||
url('^manage/validation/(?P<pk>\d+)/$', views.validation,
|
||||
name='cut-manager-user-validation'),
|
||||
url('^manage/validation/attachment/(?P<pk>\d*)/$', views.validation_attachment,
|
||||
name='cut-manager-user-validation-attachment'),
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -42,4 +48,5 @@ urlpatterns += patterns(
|
|||
'',
|
||||
url('^cgu/$', views.cgu,
|
||||
name='cut-cgu'),
|
||||
url('^api/validate/$', api_views.validate, name='api-cut-validate'),
|
||||
)
|
||||
|
|
|
@ -15,19 +15,24 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from django.utils.timezone import now
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.views.generic.base import TemplateView
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.http import HttpResponse, HttpResponseRedirect, Http404
|
||||
from django.db.transaction import atomic
|
||||
|
||||
from authentic2.views import EditProfile
|
||||
from authentic2.manager.views import SimpleSubTableView
|
||||
from authentic2.manager.user_views import UserEditView, UserDetailView
|
||||
from authentic2 import hooks
|
||||
from authentic2.utils import redirect
|
||||
|
||||
from .custom_settings import CORE_ATTRIBUTES
|
||||
from . import tables
|
||||
from . import tables, models, forms
|
||||
|
||||
|
||||
class EditCoreView(EditProfile):
|
||||
|
@ -166,3 +171,81 @@ class CGU(TemplateView):
|
|||
return super(CGU, self).get_context_data(**kwargs)
|
||||
|
||||
cgu = CGU.as_view()
|
||||
|
||||
|
||||
def next_validation(request):
|
||||
if not request.user.is_authenticated() or not request.user.has_perm_any('custom_user.cut_validate_user'):
|
||||
raise PermissionDenied
|
||||
with atomic():
|
||||
validation_request = models.ValidationRequest.objects.next_request(request.user)
|
||||
if not validation_request:
|
||||
messages.info(request, u'Il n\'y aucune demande de validation en ce moment.')
|
||||
return redirect(request, 'a2-manager-homepage')
|
||||
return redirect(request, 'cut-manager-user-validation',
|
||||
kwargs={'pk': validation_request.pk})
|
||||
|
||||
|
||||
class Validation(UserEditCoreView):
|
||||
template_name = 'authentic2/cut_manager_user_validation.html'
|
||||
permissions = ['custom_user.cut_validate_user']
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
qs = models.ValidationRequest.objects.received()
|
||||
self.validation_request = super(Validation, self).get_object(queryset=qs)
|
||||
return self.validation_request.user
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
# si la demande en cours a déjà été traitée, on passe à la suivante
|
||||
try:
|
||||
return super(Validation, self).dispatch(request, *args, **kwargs)
|
||||
except Http404:
|
||||
return redirect(request, 'cut-manager-user-next-validation')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super(Validation, self).get_context_data(**kwargs)
|
||||
ctx['validation_request'] = self.validation_request
|
||||
ctx['attachment_urls'] = [reverse('cut-manager-user-validation-attachment', kwargs={'pk': attachment.pk}) for attachment in self.validation_request.attachments.all()]
|
||||
ctx['action'] = u'Valider'
|
||||
ctx['validation_form'] = forms.ValidationForm()
|
||||
return ctx
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if 'refuse' in request.POST:
|
||||
reason = request.POST.get('reason')
|
||||
if not reason:
|
||||
messages.error(request, u'Vous devez préciser une raison pour le refus')
|
||||
return HttpResponseRedirect('')
|
||||
self.validation_request.reason = reason
|
||||
self.validation_request.status = models.ValidationRequest.STATUS_REFUSED
|
||||
self.validation_request.validated = now()
|
||||
self.validation_request.validated_by = request.user
|
||||
self.validation_request.save()
|
||||
elif 'validate' in request.POST:
|
||||
super(Validation, self).post(request, *args, **kwargs)
|
||||
with atomic():
|
||||
validation_request = models.ValidationRequest.objects.next_request(
|
||||
request.user, after=self.validation_request)
|
||||
# on ne trouve plus de requête de validation après celle-ci
|
||||
# recommençons du début
|
||||
if not validation_request:
|
||||
return redirect(request, 'cut-manager-user-next-validation')
|
||||
return redirect(request, 'cut-manager-user-validation',
|
||||
kwargs={'pk': validation_request.pk})
|
||||
|
||||
def form_valid(self, form):
|
||||
self.validation_request.status = models.ValidationRequest.STATUS_ACCEPTED
|
||||
self.validation_request.validated = now()
|
||||
self.validation_request.validated_by = self.request.user
|
||||
self.validation_request.save()
|
||||
if hasattr(self.validation_request.origin, 'ou') and self.validation_request.origin.ou:
|
||||
forn.instance.attributes.validation_partner = self.validation_request.origin.ou.slug
|
||||
return super(Validation, self).form_valid(form)
|
||||
|
||||
validation = Validation.as_view()
|
||||
|
||||
|
||||
def validation_attachment(request, pk):
|
||||
if not request.user.is_authenticated() or not request.user.has_perm_any('custom_user.cut_validate_user'):
|
||||
raise PermissionDenied
|
||||
attachment = models.ValidationRequestAttachment.objects.get(pk=pk)
|
||||
return HttpResponse(attachment.image, content_type='image/jpeg')
|
||||
|
|
Loading…
Reference in New Issue