diff --git a/beat/web/accounts/api.py b/beat/web/accounts/api.py new file mode 100644 index 0000000000000000000000000000000000000000..a56b2e4d956a76dbefe294713508460fbe12ad5d --- /dev/null +++ b/beat/web/accounts/api.py @@ -0,0 +1,523 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : + +############################################################################### +# # +# Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ # +# Contact: beat.support@idiap.ch # +# # +# This file is part of the beat.web module of the BEAT platform. # +# # +# Commercial License Usage # +# Licensees holding valid commercial BEAT licenses may use this file in # +# accordance with the terms contained in a written agreement between you # +# and Idiap. For further information contact tto@idiap.ch # +# # +# Alternatively, this file may be used under the terms of the GNU Affero # +# Public License version 3 as published by the Free Software and appearing # +# in the file LICENSE.AGPL included in the packaging of this file. # +# The BEAT platform 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. # +# # +# You should have received a copy of the GNU Affero Public License along # +# with the BEAT platform. If not, see http://www.gnu.org/licenses/. # +# # +############################################################################### + +from django.conf import settings +from django.contrib.auth.models import User +from django.shortcuts import get_object_or_404 +from django.http import Http404 +from django.db import models +from django.db.models import Q +from django.core.urlresolvers import reverse +from django.template import loader +from django.template import Context + +from rest_framework import generics +from rest_framework import views +from rest_framework import permissions +from rest_framework.response import Response +from rest_framework import status + +from .serializers import FullSupervisionTrackSerializer +from .serializers import SupervisionTrackUpdateSerializer + +from ..common.utils import validate_restructuredtext +from ..ui.templatetags.markup import restructuredtext + + +from .models import SupervisionTrack, Profile +from ..common.models import Shareable +from ..common.exceptions import ShareError +from ..common.mixins import CommonContextMixin +from ..ui.registration.models import RegistrationProfile + +from itertools import chain +from datetime import datetime, timedelta + + +from .permissions import IsGodfatherAndAuthor, IsAuthorAndNotGodfather + +from ..common.responses import BadRequestResponse, ForbiddenResponse + +import datetime +import re +from urlparse import urlparse + +import simplejson as json + + +#---------------------------------------------------------- + + +class GodfatherListView(generics.ListAPIView): + model = SupervisionTrack + serializer_class = FullSupervisionTrackSerializer + + def get_permissions(self): + permission_classes = [permissions.IsAuthenticated, IsGodfatherAndAuthor] + + self.permission_classes = permission_classes + + return super(GodfatherListView, self).get_permissions() + + + def get_serializer(self, *args, **kwargs): + + return super(GodfatherListView, self).get_serializer(*args, **kwargs) + + + def list(self, request): + #A godfather can validate an account of: + #1) a new user requesting validation + #2) an existing validated user rejected by a previous supervisor + #3) an existing validated user requesting a change of supervisor + #4) a blocked user requesting a supervision + #On all cases check the current key in supervisee profile match the supervisiontrack key as this is the current supervision request/track from the supervisee + queryset = SupervisionTrack.objects.filter(godfather=request.user).filter(Q(supervisee__profile__status=Profile.WAITINGVALIDATION)|Q(supervisee__profile__status=Profile.REJECTED)|Q(supervisee__profile__status=Profile.YEARREVALIDATION)|Q(supervisee__profile__status=Profile.ACCEPTED)|Q(supervisee__profile__status=Profile.BLOCKED)).filter(Q(supervisee__profile__supervision_key=models.F('supervision_key'))) + serializer = FullSupervisionTrackSerializer(queryset, many=True, context ={'request': request}) + + return Response(serializer.data) + + +#---------------------------------------------------------- + + +class BaseUpdateSupervisionTrackView(generics.UpdateAPIView): + model = SupervisionTrack + serializer_class = SupervisionTrackUpdateSerializer + + def get_permissions(self): + permission_classes = [permissions.IsAuthenticated, IsGodfatherAndAuthor] + + self.permission_classes = permission_classes + + return super(BaseUpdateSupervisionTrackView, self).get_permissions() + + +#---------------------------------------------------------- + + +class GodfatherAddSuperviseeView(BaseUpdateSupervisionTrackView): + permission_classes = BaseUpdateSupervisionTrackView.permission_classes + + def put(self, request, supervisee_name): + supervisee = User.objects.get(username=supervisee_name) + profile_supervisee = Profile.objects.get(user=supervisee) + if supervisee.profile.status != Profile.ACCEPTED: + supervisiontrack = SupervisionTrack.objects.get(godfather=request.user, supervisee=supervisee, supervisee__profile__supervision_key=models.F('supervision_key')) + + supervisiontrack.is_valid = True + + now = datetime.datetime.now() + expiration_date_delta = datetime.timedelta(days=settings.ACCOUNT_EXPIRATION_DAYS) + supervisiontrack.expiration_date = now + expiration_date_delta + + supervisiontrack.start_date = now + supervisiontrack.last_validation_date = now + supervisee.profile.status = Profile.ACCEPTED + supervisee.profile.rejection_date = None + + supervisiontrack.save() + supervisee.profile.save() + supervisee.is_active = True + supervisee.save() + + from django.core.mail import send_mail + + parsed_url = urlparse(settings.URL_PREFIX) + server_address = '%s://%s' % (parsed_url.scheme, parsed_url.hostname) + + c = Context({ 'supervisor': supervisiontrack.godfather, + 'supervisee': supervisee, + 'prefix': server_address, + }) + + try: + t = loader.get_template('registration/mail.godfather_validated.subject.txt') + subject = t.render(c) + + # Note: e-mail subject *must not* contain newlines + subject = settings.EMAIL_SUBJECT_PREFIX + ''.join(subject.splitlines()) + + t = loader.get_template('registration/mail.godfather_validated.message.txt') + message = t.render(c) + + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [supervisee.email]) + except: + pass + + return Response(status=status.HTTP_204_NO_CONTENT) + else: + #Track already valid + reason = "You can't validate this supervisee as his supervision is still valid" + result = { + 'error': reason, + } + return BadRequestResponse(result) + + +#---------------------------------------------------------- + + +class GodfatherRemoveSuperviseeView(BaseUpdateSupervisionTrackView): + permission_classes = BaseUpdateSupervisionTrackView.permission_classes + + def put(self, request, supervisee_name): + supervisee = User.objects.get(username=supervisee_name) + supervisiontrack = SupervisionTrack.objects.get(godfather=request.user, supervisee=supervisee, supervisee__profile__supervision_key=models.F('supervision_key')) + + from django.core.mail import send_mail + + parsed_url = urlparse(settings.URL_PREFIX) + server_address = '%s://%s' % (parsed_url.scheme, parsed_url.hostname) + + c = Context({ 'supervisor': supervisiontrack.godfather, + 'supervisee': supervisee, + 'prefix': server_address, + }) + + if supervisee.profile.status == Profile.WAITINGVALIDATION: + #New user account waiting validation, so delete this account and inform by email the user + try: + t = loader.get_template('registration/mail.godfather_rejected.subject.txt') + subject = t.render(c) + + # Note: e-mail subject *must not* contain newlines + subject = settings.EMAIL_SUBJECT_PREFIX + ''.join(subject.splitlines()) + + t = loader.get_template('registration/mail.godfather_rejected_delete_account.message.txt') + message = t.render(c) + + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [supervisee.email]) + except: + pass + + registration_profile = RegistrationProfile.objects.get(user=supervisee) + supervisee.profile.delete() + supervisee.delete() + supervisiontrack.delete() + registration_profile.delete() + else: + #Reject this account and inform by email the user + now = datetime.datetime.now() + expiration_date_delta = datetime.timedelta(days=settings.ACCOUNT_BLOCKAGE_AFTER_FIRST_REJECTION_DAYS) + + supervisiontrack.expiration_date = now + supervisiontrack.is_valid = False + + if supervisee.profile.status != Profile.BLOCKED: + supervisee.profile.status = Profile.REJECTED + if supervisee.profile.rejection_date == None: + supervisee.profile.rejection_date = now + expiration_date_delta + else: + supervisee.profile.rejection_date = None + + supervisee.profile.supervision_key = None + + supervisiontrack.save() + supervisee.profile.save() + supervisee.save() + + try: + t = loader.get_template('registration/mail.godfather_rejected.subject.txt') + subject = t.render(c) + + # Note: e-mail subject *must not* contain newlines + subject = settings.EMAIL_SUBJECT_PREFIX + ''.join(subject.splitlines()) + + t = loader.get_template('registration/mail.godfather_rejected.message.txt') + message = t.render(c) + + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [supervisee.email]) + except: + pass + + #New supervision request refused + if supervisiontrack.start_date is None: + supervisiontrack.delete() + + return Response(status=status.HTTP_204_NO_CONTENT) + + +#---------------------------------------------------------- + + +class BaseCreateSupervisionTrackViewSupervisee(generics.CreateAPIView): + model = SupervisionTrack + serializer_class = SupervisionTrackUpdateSerializer + + def get_permissions(self): + permission_classes = [permissions.IsAuthenticated, IsAuthorAndNotGodfather] + + self.permission_classes = permission_classes + + return super(BaseCreateSupervisionTrackViewSupervisee, self).get_permissions() + + +#---------------------------------------------------------- + + +class SuperviseeAddGodfatherView(BaseCreateSupervisionTrackViewSupervisee): + permission_classes = BaseCreateSupervisionTrackViewSupervisee.permission_classes + + def post(self, request, supervisor_name): + #check if user exists and its validity and if it's a godfather account + try: + godfather = User.objects.get(username=supervisor_name) + if not godfather.profile.is_godfather: + #Not a valid godfather + reason = "Not a valid supervisor request" + result = { + 'error': reason, + } + return BadRequestResponse(result) + else: + if godfather.profile.status == Profile.BLOCKED: + #Not a valid godfather + reason = "Not a valid supervisor request" + result = { + 'error': reason, + } + return BadRequestResponse(result) + except: + #This username does not exist but don't give too much information + reason = "Not a valid supervisor request" + result = { + 'error': reason, + } + return BadRequestResponse(result) + + supervisee = request.user + if supervisee.profile.supervision_key is not None: + #There's a key check if there's a valid track + supervisiontrack = SupervisionTrack.objects.get(supervisee=supervisee, supervisee__profile__supervision_key=models.F('supervision_key')) + if supervisiontrack.is_valid: + if supervisee.profile.status != Profile.WAITINGVALIDATION and supervisee.profile.status != Profile.NEWUSER and supervisee.profile.status != Profile.BLOCKED: + #Stop the current supervision + now = datetime.datetime.now() + expiration_date_delta = datetime.timedelta(days=settings.ACCOUNT_BLOCKAGE_AFTER_FIRST_REJECTION_DAYS) + + + supervisiontrack.expiration_date = now + supervisiontrack.is_valid = False + supervisiontrack.save() + + #Inform by email the revoked supervisor + from django.core.mail import send_mail + + parsed_url = urlparse(settings.URL_PREFIX) + server_address = '%s://%s' % (parsed_url.scheme, parsed_url.hostname) + + c = Context({ 'supervisor': supervisiontrack.godfather, + 'supervisee': supervisiontrack.supervisee, + 'prefix': server_address, + }) + + try: + t = loader.get_template('registration/mail.godfather_rejection.subject.txt') + subject = t.render(c) + + # Note: e-mail subject *must not* contain newlines + subject = settings.EMAIL_SUBJECT_PREFIX + ''.join(subject.splitlines()) + + t = loader.get_template('registration/mail.godfather_rejection.message.txt') + message = t.render(c) + + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [supervisiontrack.godfather.email]) + except: + pass + + #Reject this account and inform by email the supervisor and the user + + supervisee.profile.status = Profile.REJECTED + supervisee.profile.supervision_key = None + if supervisee.profile.rejection_date == None: + supervisee.profile.rejection_date = now + expiration_date_delta + + supervisee.profile.save() + supervisee.save() + else: + #Not allowed to do this (unproper profile.status) + reason = "You are not able to perform this action, your profile is %s"%(supervisee.profile.status) + result = { + 'error': reason, + } + return BadRequestResponse(result) + + else: + #A pending request already exist + reason = "You are not able to perform this action as you already have a pending request" + result = { + 'error': reason, + } + return BadRequestResponse(result) + else: + #No key is present in supervisee + + #Make sure all tracks are invalid + supervisiontracks = SupervisionTrack.objects.filter(supervisee=supervisee, is_valid=True) + # This should never be the case but if it happens invalidate all tracks + if supervisiontracks.count() > 0: + now = datetime.datetime.now() + for track in supervisiontracks: + track.is_valid = False + track.expiration_date = now + track.save() + + #Create and assign key + supervisee.profile.supervision_key = supervisee.profile._generate_current_supervision_key() + supervisiontrack = SupervisionTrack.objects.create( + supervisee = supervisee, + godfather = godfather, + is_valid = False, + ) + + #Assign key to supervision track + supervisiontrack.supervision_key = supervisee.profile.supervision_key + supervisiontrack.save() + supervisee.profile.supervision.add(supervisiontrack) + supervisee.save() + + #Inform by email the supervisor that he has a new supervisee request + from django.core.mail import send_mail + + parsed_url = urlparse(settings.URL_PREFIX) + server_address = '%s://%s' % (parsed_url.scheme, parsed_url.hostname) + + c = Context({ 'supervisor': godfather, + 'supervisee': supervisee, + 'prefix': server_address, + }) + + try: + t = loader.get_template('registration/mail.godfather_validation.subject.txt') + subject = t.render(c) + + # Note: e-mail subject *must not* contain newlines + subject = settings.EMAIL_SUBJECT_PREFIX + ''.join(subject.splitlines()) + + t = loader.get_template('registration/mail.godfather_validation_supervisee_add_request.message.txt') + message = t.render(c) + + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [godfather.email]) + except: + pass + + return Response(status=status.HTTP_204_NO_CONTENT) + + +#---------------------------------------------------------- + + +class BaseUpdateSupervisionTrackFromSuperviseeView(generics.UpdateAPIView): + model = SupervisionTrack + serializer_class = SupervisionTrackUpdateSerializer + + def get_permissions(self): + permission_classes = [permissions.IsAuthenticated, IsAuthorAndNotGodfather] + + self.permission_classes = permission_classes + + return super(BaseUpdateSupervisionTrackFromSuperviseeView, self).get_permissions() + + +#---------------------------------------------------------- + + +class SuperviseeReValidationView(BaseUpdateSupervisionTrackFromSuperviseeView): + permission_classes = BaseUpdateSupervisionTrackFromSuperviseeView.permission_classes + + def put(self, request): + supervisee = request.user + now = datetime.datetime.now() + + if supervisee.profile.supervision_key is not None: + #There's a key check if there's a valid track + supervisiontrack = SupervisionTrack.objects.get(supervisee=supervisee, supervisee__profile__supervision_key=models.F('supervision_key')) + if supervisiontrack.is_valid: + if supervisee.profile.status == Profile.YEARREVALIDATION: + #Check Godfather validity + godfather = supervisiontrack.godfather + #If Godfather account is not valid. Reject the account (though this should already be done during godfather rejection) + if godfather.profile.status != Profile.BLOCKED: + #Change status + supervisee.profile.status = Profile.ACCEPTED + #Extend supervisiontrack validity for another 12 months + expiration_date_delta = datetime.timedelta(days=settings.ACCOUNT_EXPIRATION_DAYS) + new_expiration_date = supervisiontrack.expiration_date + expiration_date_delta + supervisiontrack.expiration_date = new_expiration_date + supervisiontrack.last_validation_date = now + else: + #Change status + expiration_date_delta = datetime.timedelta(days=settings.ACCOUNT_BLOCKAGE_AFTER_FIRST_REJECTION_DAYS) + + supervisiontrack.expiration_date = now + supervisiontrack.is_valid = False + + supervisee.profile.status = Profile.REJECTED + supervisee.profile.rejection_date = now + expiration_date_delta + supervisee.profile.supervision_key = None + #save + supervisiontrack.save() + supervisee.profile.save() + supervisee.save() + else: + #Track already valid + reason = "You don't need to revalidate at the moment, your supervision is still valid" + result = { + 'error': reason, + } + return BadRequestResponse(result) + else: + #A pending request already exist + reason = "You are not able to perform this action as you already have a pending supervision request" + result = { + 'error': reason, + } + return BadRequestResponse(result) + else: + #No key is present in supervisee + #Make sure all tracks are invalid + supervisiontracks = SupervisionTrack.objects.filter(supervisee=supervisee, is_valid=True) + # This should never be the case but if it happens invalidate all tracks + if supervisiontracks.count() > 0: + now = datetime.datetime.now() + for track in supervisiontracks: + track.is_valid = False + track.expiration_date = now + track.save() + + #Not allowed to do this (unproper profile.status) + reason = "You are not allowed to perform this action, you first need to get a valid supervision" + result = { + 'error': reason, + } + return BadRequestResponse(result) + + return Response(status=status.HTTP_204_NO_CONTENT) + + +#---------------------------------------------------------- diff --git a/beat/web/accounts/api_urls.py b/beat/web/accounts/api_urls.py new file mode 100644 index 0000000000000000000000000000000000000000..c6b3d1659cb1a0fd026afb47ee6e17d907e7a1a6 --- /dev/null +++ b/beat/web/accounts/api_urls.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : + +############################################################################### +# # +# Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ # +# Contact: beat.support@idiap.ch # +# # +# This file is part of the beat.web module of the BEAT platform. # +# # +# Commercial License Usage # +# Licensees holding valid commercial BEAT licenses may use this file in # +# accordance with the terms contained in a written agreement between you # +# and Idiap. For further information contact tto@idiap.ch # +# # +# Alternatively, this file may be used under the terms of the GNU Affero # +# Public License version 3 as published by the Free Software and appearing # +# in the file LICENSE.AGPL included in the packaging of this file. # +# The BEAT platform 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. # +# # +# You should have received a copy of the GNU Affero Public License along # +# with the BEAT platform. If not, see http://www.gnu.org/licenses/. # +# # +############################################################################### + +from django.conf.urls import * +from . import api + +urlpatterns = [ + url( + r'^$', + api.GodfatherListView.as_view(), + name='list_supervisee' + ), + + url( + r'^(?P<supervisee_name>[\w\W]+)/validate/$', + api.GodfatherAddSuperviseeView.as_view(), + name='validate_supervisee' + ), + + url( + r'^(?P<supervisee_name>[\w\W]+)/remove/$', + api.GodfatherRemoveSuperviseeView.as_view(), + name='remove_supervisee' + ), + + url( + r'^(?P<supervisor_name>[\w\W]+)/add/$', + api.SuperviseeAddGodfatherView.as_view(), + name='add_supervisor' + ), + + url( + r'^revalidate/$', + api.SuperviseeReValidationView.as_view(), + name='revalidate_account' + ), + +] diff --git a/beat/web/accounts/management/__init__.py b/beat/web/accounts/management/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/beat/web/accounts/management/commands/__init__.py b/beat/web/accounts/management/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/beat/web/accounts/management/commands/block_rejected_users.py b/beat/web/accounts/management/commands/block_rejected_users.py new file mode 100644 index 0000000000000000000000000000000000000000..a00ffddf793a0974328efa71a4216e627018df0b --- /dev/null +++ b/beat/web/accounts/management/commands/block_rejected_users.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : +# encoding: utf-8 + +############################################################################### +# # +# Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ # +# Contact: beat.support@idiap.ch # +# # +# This file is part of the beat.web module of the BEAT platform. # +# # +# Commercial License Usage # +# Licensees holding valid commercial BEAT licenses may use this file in # +# accordance with the terms contained in a written agreement between you # +# and Idiap. For further information contact tto@idiap.ch # +# # +# Alternatively, this file may be used under the terms of the GNU Affero # +# Public License version 3 as published by the Free Software and appearing # +# in the file LICENSE.AGPL included in the packaging of this file. # +# The BEAT platform 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. # +# # +# You should have received a copy of the GNU Affero Public License along # +# with the BEAT platform. If not, see http://www.gnu.org/licenses/. # +# # +############################################################################### + + +from django.core.management.base import BaseCommand, CommandError + +from datetime import date +import datetime + +from django.contrib.auth.models import User +from django.conf import settings + +from ...models import SupervisionTrack +from ...models import Profile +from ....ui.registration.models import RegistrationProfile + +import sys +import random + +class Command(BaseCommand): + + help = 'Block rejected users after rejection date with no valid supervisor' + + def add_arguments(self, parser): + parser.add_argument('--noinput', action='store_false', dest='interactive', default=False, + help=('Tells Django to NOT prompt the user for input of any kind.')) + + + def handle(self, *args, **options): + block = True + + if options['interactive']: + try: + answer = self.get_input_data('Block rejected user(s) that have not been validated by a supervisor? (y/n)? ', 'y').lower() + except KeyboardInterrupt: + self.stderr.write("\nOperation canceled.") + sys.exit(1) + + if answer != 'y': + self.stdout.write('Block users operation canceled') + sys.exit(1) + + if block: + rejected_profiles = Profile.objects.filter(status=Profile.REJECTED) + count = 0 + for rejected_profile in rejected_profiles: + user = rejected_profile.user + if user.profile.rejection_date < datetime.datetime.now(): + count +=1 + user.profile.status = Profile.BLOCKED + user.profile.rejection_date = None + user.is_active = False + + if user.profile.supervision_key != None: + supervisiontrack = SupervisionTrack.objects.get(supervision_key=rejected_profile.supervision_key) + if supervisiontrack.is_valid == False and supervisiontrack.start_date is None: + user.profile.supervision_key = None + supervisiontrack.delete() + + user.profile.save() + user.save() + + self.stdout.write('{} Rejected user(s) successfully blocked/'.format(count) + '{} Total user(s) checked'.format(rejected_profiles.count())) + + + def get_input_data(self, message, default=None): + """ + Override this method if you want to customize data inputs or + validation exceptions. + """ + raw_value = raw_input(message) + + if default and raw_value == '': + raw_value = default + + return raw_value diff --git a/beat/web/accounts/management/commands/clean_blocked_users_expired_requests.py b/beat/web/accounts/management/commands/clean_blocked_users_expired_requests.py new file mode 100644 index 0000000000000000000000000000000000000000..98cdeb8fcaa255bbf3fe51803bfbef25748ce459 --- /dev/null +++ b/beat/web/accounts/management/commands/clean_blocked_users_expired_requests.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : +# encoding: utf-8 + +############################################################################### +# # +# Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ # +# Contact: beat.support@idiap.ch # +# # +# This file is part of the beat.web module of the BEAT platform. # +# # +# Commercial License Usage # +# Licensees holding valid commercial BEAT licenses may use this file in # +# accordance with the terms contained in a written agreement between you # +# and Idiap. For further information contact tto@idiap.ch # +# # +# Alternatively, this file may be used under the terms of the GNU Affero # +# Public License version 3 as published by the Free Software and appearing # +# in the file LICENSE.AGPL included in the packaging of this file. # +# The BEAT platform 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. # +# # +# You should have received a copy of the GNU Affero Public License along # +# with the BEAT platform. If not, see http://www.gnu.org/licenses/. # +# # +############################################################################### + + +from django.core.management.base import BaseCommand, CommandError + +from datetime import date +import datetime + +from django.contrib.auth.models import User +from django.conf import settings + +from ...models import SupervisionTrack +from ...models import Profile +from ....ui.registration.models import RegistrationProfile + +import sys +import random + +class Command(BaseCommand): + + help = 'Clean blocked users expired requests for supervisor validation' + + def add_arguments(self, parser): + parser.add_argument('--noinput', action='store_false', dest='interactive', default=False, + help=('Tells Django to NOT prompt the user for input of any kind.')) + + + def handle(self, *args, **options): + block = True + + if options['interactive']: + try: + answer = self.get_input_data('Clean blocked user(s) that have not been validated by a supervisor? (y/n)? ', 'y').lower() + except KeyboardInterrupt: + self.stderr.write("\nOperation canceled.") + sys.exit(1) + + if answer != 'y': + self.stdout.write('Clean blocked users operation canceled') + sys.exit(1) + + if block: + blocked_profiles = Profile.objects.filter(status=Profile.BLOCKED) + count = 0 + for blocked_profile in blocked_profiles: + user = blocked_profile.user + if user.profile.rejection_date is not None: + if user.profile.rejection_date < datetime.datetime.now(): + count +=1 + user.profile.status = Profile.BLOCKED + user.profile.rejection_date = None + user.is_active = False + + if user.profile.supervision_key != None: + supervisiontrack = SupervisionTrack.objects.get(supervision_key=blocked_profile.supervision_key) + if supervisiontrack.is_valid == False and supervisiontrack.start_date is None: + user.profile.supervision_key = None + supervisiontrack.delete() + + user.profile.save() + user.save() + + self.stdout.write('{} Blocked user(s) successfully cleaned from expired supervision/'.format(count) + '{} Total user(s) checked'.format(blocked_profiles.count())) + + + def get_input_data(self, message, default=None): + """ + Override this method if you want to customize data inputs or + validation exceptions. + """ + raw_value = raw_input(message) + + if default and raw_value == '': + raw_value = default + + return raw_value diff --git a/beat/web/accounts/management/commands/clean_invalid_users.py b/beat/web/accounts/management/commands/clean_invalid_users.py new file mode 100644 index 0000000000000000000000000000000000000000..36c3059e240a1321b09708e4f87a335003fd834f --- /dev/null +++ b/beat/web/accounts/management/commands/clean_invalid_users.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : +# encoding: utf-8 + +############################################################################### +# # +# Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ # +# Contact: beat.support@idiap.ch # +# # +# This file is part of the beat.web module of the BEAT platform. # +# # +# Commercial License Usage # +# Licensees holding valid commercial BEAT licenses may use this file in # +# accordance with the terms contained in a written agreement between you # +# and Idiap. For further information contact tto@idiap.ch # +# # +# Alternatively, this file may be used under the terms of the GNU Affero # +# Public License version 3 as published by the Free Software and appearing # +# in the file LICENSE.AGPL included in the packaging of this file. # +# The BEAT platform 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. # +# # +# You should have received a copy of the GNU Affero Public License along # +# with the BEAT platform. If not, see http://www.gnu.org/licenses/. # +# # +############################################################################### + + +from django.core.management.base import BaseCommand, CommandError + +from datetime import date +import datetime + +from django.contrib.auth.models import User +from django.conf import settings + +from ...models import SupervisionTrack +from ...models import Profile +from ....ui.registration.models import RegistrationProfile + +import sys +import random + +class Command(BaseCommand): + + help = 'Cleanup outdated invalid users' + + def add_arguments(self, parser): + parser.add_argument('--noinput', action='store_false', dest='interactive', default=False, + help=('Tells Django to NOT prompt the user for input of any kind.')) + + + def handle(self, *args, **options): + clean = True + + if options['interactive']: + try: + answer = self.get_input_data('Delete user(s) that have not been validated by a supervisor? (y/n)? ', 'y').lower() + except KeyboardInterrupt: + self.stderr.write("\nOperation canceled.") + sys.exit(1) + + if answer != 'y': + self.stdout.write('Cleanup canceled') + sys.exit(1) + + if clean: + invalid_userprofiles_new_users = Profile.objects.filter(status=Profile.NEWUSER) + invalid_userprofiles_waiting_validation = Profile.objects.filter(status=Profile.WAITINGVALIDATION) + count = 0 + for invalid_profile in invalid_userprofiles_new_users: + user = invalid_profile.user + supervisiontrack = SupervisionTrack.objects.get(supervision_key=invalid_profile.supervision_key) + registration_profile = RegistrationProfile.objects.get(user=invalid_profile.user) + + expiration_date = datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS) + if user.profile.registration_date + expiration_date <= datetime.datetime.now(): + count += 1 + user.delete() + invalid_profile.delete() + supervisiontrack.delete() + registration_profile.delete() + + for invalid_profile in invalid_userprofiles_waiting_validation: + user = invalid_profile.user + supervisiontrack = SupervisionTrack.objects.get(supervision_key=invalid_profile.supervision_key) + registration_profile = RegistrationProfile.objects.get(user=invalid_profile.user) + + expiration_date = datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS_FROM_GODFATHER) + if user.profile.registration_date + expiration_date <= datetime.datetime.now(): + count += 1 + user.delete() + invalid_profile.delete() + supervisiontrack.delete() + registration_profile.delete() + + self.stdout.write('{} Invalid user(s) successfully cleaned/'.format(count) + '{} Total user(s) checked'.format(invalid_userprofiles_new_users.count()+invalid_userprofiles_waiting_validation.count())) + + + def get_input_data(self, message, default=None): + """ + Override this method if you want to customize data inputs or + validation exceptions. + """ + raw_value = raw_input(message) + + if default and raw_value == '': + raw_value = default + + return raw_value diff --git a/beat/web/accounts/management/commands/year_revalidation_users.py b/beat/web/accounts/management/commands/year_revalidation_users.py new file mode 100644 index 0000000000000000000000000000000000000000..b00a3669cea53b8cc3f62f814925bbdec04a8848 --- /dev/null +++ b/beat/web/accounts/management/commands/year_revalidation_users.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : +# encoding: utf-8 + +############################################################################### +# # +# Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ # +# Contact: beat.support@idiap.ch # +# # +# This file is part of the beat.web module of the BEAT platform. # +# # +# Commercial License Usage # +# Licensees holding valid commercial BEAT licenses may use this file in # +# accordance with the terms contained in a written agreement between you # +# and Idiap. For further information contact tto@idiap.ch # +# # +# Alternatively, this file may be used under the terms of the GNU Affero # +# Public License version 3 as published by the Free Software and appearing # +# in the file LICENSE.AGPL included in the packaging of this file. # +# The BEAT platform 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. # +# # +# You should have received a copy of the GNU Affero Public License along # +# with the BEAT platform. If not, see http://www.gnu.org/licenses/. # +# # +############################################################################### + + +from django.core.management.base import BaseCommand, CommandError + +from datetime import date +import datetime + +from django.db.models import Q +from django.contrib.auth.models import User +from django.conf import settings +from django.core.mail import send_mail +from django.template import loader +from django.template import Context + +from ...models import SupervisionTrack +from ...models import Profile +from ....ui.registration.models import RegistrationProfile + +import sys +import random +from urlparse import urlparse + +class Command(BaseCommand): + + help = 'Check users for yearly revalidation' + + def add_arguments(self, parser): + parser.add_argument('--noinput', action='store_false', dest='interactive', default=False, + help=('Tells Django to NOT prompt the user for input of any kind.')) + + + def handle(self, *args, **options): + yearrevalidate = True + + if options['interactive']: + try: + answer = self.get_input_data('Check user(s) for yearly revalidation by a supervisor? (y/n)? ', 'y').lower() + except KeyboardInterrupt: + self.stderr.write("\nOperation canceled.") + sys.exit(1) + + if answer != 'y': + self.stdout.write('Check users yearly revalidation operation canceled') + sys.exit(1) + + if yearrevalidate: + torevalidate_profiles = Profile.objects.filter(Q(status=Profile.ACCEPTED)|Q(status=Profile.YEARREVALIDATION)) + + now = datetime.datetime.now() + blocked_count = 0 + warned_count = 0 + + for torevalidate_profile in torevalidate_profiles: + user = torevalidate_profile.user + if user.profile.is_godfather == False: + #Not godfather + if user.profile.supervision_key != None: + supervisiontrack = SupervisionTrack.objects.get(supervision_key=torevalidate_profile.supervision_key) + if supervisiontrack.is_valid: + #Check expiration date + expiration_date_delta = datetime.timedelta(days=settings.ACCOUNT_BLOCKAGE_AFTER_FIRST_REJECTION_DAYS) + if supervisiontrack.expiration_date < now + expiration_date_delta: + #check if need to block account + if now > supervisiontrack.expiration_date: + blocked_count += 1 + #terminate supervision + supervisiontrack.expiration_date = now + supervisiontrack.is_valid = False + + #block user + user.profile.status = Profile.BLOCKED + user.profile.rejection_date = None + user.profile.supervision_key = None + user.is_active = False + + #save all changes + supervisiontrack.save() + user.profile.save() + user.save() + else: + #send the warning information + #set user to YEARREVALIDATION + if user.profile.status == Profile.ACCEPTED: + user.profile.status = Profile.YEARREVALIDATION + user.profile.save() + #send email to user + for expiration_reminder in settings.EXPIRATION_REMINDERS_REVALIDATION: + if supervisiontrack.expiration_date.date() - now.date() == datetime.timedelta(days=expiration_reminder): + warned_count += 1 + + parsed_url = urlparse(settings.URL_PREFIX) + server_address = '%s://%s' % (parsed_url.scheme, parsed_url.hostname) + + c = Context({ 'user': user, + 'expiration_date': supervisiontrack.expiration_date.date(), + }) + + try: + t = loader.get_template('registration/mail.account_revalidation.subject.txt') + subject = t.render(c) + + # Note: e-mail subject *must not* contain newlines + subject = settings.EMAIL_SUBJECT_PREFIX + ''.join(subject.splitlines()) + + t = loader.get_template('registration/mail.account_revalidation.message.txt') + message = t.render(c) + + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [supervisiontrack.supervisee.email]) + except: + pass + + + else: + #Is godfather + #TODO implement this procedure + pass + + self.stdout.write('{} blocked user(s) and '.format(blocked_count)+'{} warned user(s)/'.format(warned_count) + '{} Total user(s) that need revalidation'.format(torevalidate_profiles.count())) + + + def get_input_data(self, message, default=None): + """ + Override this method if you want to customize data inputs or + validation exceptions. + """ + raw_value = raw_input(message) + + if default and raw_value == '': + raw_value = default + + return raw_value diff --git a/beat/web/accounts/migrations/0002_profile.py b/beat/web/accounts/migrations/0002_profile.py new file mode 100644 index 0000000000000000000000000000000000000000..d20bbcfb9c4b69511ca77f4e43166d827bf1c1c9 --- /dev/null +++ b/beat/web/accounts/migrations/0002_profile.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.5 on 2017-05-08 17:00 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[(b'A', b'Accepted'), (b'R', b'Rejected'), (b'W', b'Waiting Validation'), (b'B', b'Blocked')], default=b'B', max_length=1)), + ('is_godfather', models.BooleanField(default=False)), + ('supervisees', models.ManyToManyField(blank=True, related_name='users', to=settings.AUTH_USER_MODEL)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/beat/web/accounts/migrations/0003_auto_20170518_1232.py b/beat/web/accounts/migrations/0003_auto_20170518_1232.py new file mode 100644 index 0000000000000000000000000000000000000000..f14663979235a4738808109856b055b994e611a1 --- /dev/null +++ b/beat/web/accounts/migrations/0003_auto_20170518_1232.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.5 on 2017-05-18 12:32 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('accounts', '0002_profile'), + ] + + operations = [ + migrations.CreateModel( + name='SupervisionTrack', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_valid', models.BooleanField(default=False)), + ('start_date', models.DateTimeField(blank=True, null=True)), + ('expiration_date', models.DateTimeField(blank=True, null=True)), + ('last_validation_date', models.DateTimeField(blank=True, null=True)), + ('godfather', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='supervisees', to=settings.AUTH_USER_MODEL)), + ('supervisee', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='supervisors', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.RemoveField( + model_name='profile', + name='supervisees', + ), + migrations.AddField( + model_name='profile', + name='supervision', + field=models.ManyToManyField(blank=True, related_name='profiles', to='accounts.SupervisionTrack'), + ), + ] diff --git a/beat/web/accounts/migrations/0004_auto_20170523_1736.py b/beat/web/accounts/migrations/0004_auto_20170523_1736.py new file mode 100644 index 0000000000000000000000000000000000000000..bfe5e0b238a1bd465a7d22391db1a93ca0c552d7 --- /dev/null +++ b/beat/web/accounts/migrations/0004_auto_20170523_1736.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.5 on 2017-05-23 17:36 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0003_auto_20170518_1232'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='supervision_key', + field=models.CharField(blank=True, max_length=40, null=True), + ), + migrations.AddField( + model_name='supervisiontrack', + name='supervision_key', + field=models.CharField(blank=True, max_length=40, null=True), + ), + ] diff --git a/beat/web/accounts/migrations/0005_auto_20170608_0935.py b/beat/web/accounts/migrations/0005_auto_20170608_0935.py new file mode 100644 index 0000000000000000000000000000000000000000..60270a7b442a6e638d721252c11c18aeb325c3f6 --- /dev/null +++ b/beat/web/accounts/migrations/0005_auto_20170608_0935.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.5 on 2017-06-08 09:35 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0004_auto_20170523_1736'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='registration_date', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='profile', + name='status', + field=models.CharField(choices=[(b'N', b'New User'), (b'W', b'Waiting Validation'), (b'A', b'Accepted'), (b'R', b'Rejected'), (b'Y', b'Yearly revalidation'), (b'B', b'Blocked no supervisor')], default=b'B', max_length=1), + ), + ] diff --git a/beat/web/accounts/migrations/0006_auto_20170608_1451.py b/beat/web/accounts/migrations/0006_auto_20170608_1451.py new file mode 100644 index 0000000000000000000000000000000000000000..a0e80203a504f995905542446ee1186823e12505 --- /dev/null +++ b/beat/web/accounts/migrations/0006_auto_20170608_1451.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.5 on 2017-06-08 14:51 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0005_auto_20170608_0935'), + ] + + operations = [ + migrations.AlterField( + model_name='supervisiontrack', + name='godfather', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='supervisees', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/beat/web/accounts/migrations/0007_profile_first_rejection_date.py b/beat/web/accounts/migrations/0007_profile_first_rejection_date.py new file mode 100644 index 0000000000000000000000000000000000000000..ff5cac2066bca90f4a195a5f5936c1e0c86780d7 --- /dev/null +++ b/beat/web/accounts/migrations/0007_profile_first_rejection_date.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.5 on 2017-06-26 14:06 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0006_auto_20170608_1451'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='first_rejection_date', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/beat/web/accounts/migrations/0008_auto_20170626_1516.py b/beat/web/accounts/migrations/0008_auto_20170626_1516.py new file mode 100644 index 0000000000000000000000000000000000000000..f1ac0352304f8d6924686edc1f12ddf0397abe4c --- /dev/null +++ b/beat/web/accounts/migrations/0008_auto_20170626_1516.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.5 on 2017-06-26 15:16 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0007_profile_first_rejection_date'), + ] + + operations = [ + migrations.RenameField( + model_name='profile', + old_name='first_rejection_date', + new_name='rejection_date', + ), + ] diff --git a/beat/web/accounts/migrations/0009_auto_20170627_0956.py b/beat/web/accounts/migrations/0009_auto_20170627_0956.py new file mode 100644 index 0000000000000000000000000000000000000000..00e134aecaa5d92635068f42feab0b5102feeec5 --- /dev/null +++ b/beat/web/accounts/migrations/0009_auto_20170627_0956.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.5 on 2017-06-27 09:56 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0008_auto_20170626_1516'), + ] + + operations = [ + migrations.AlterField( + model_name='supervisiontrack', + name='supervisee', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='supervisors', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/beat/web/accounts/migrations/0010_check_all_accounts.py b/beat/web/accounts/migrations/0010_check_all_accounts.py new file mode 100644 index 0000000000000000000000000000000000000000..c1dc9a2f8f647fc5b70a2a9a8277064072682b73 --- /dev/null +++ b/beat/web/accounts/migrations/0010_check_all_accounts.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : + +############################################################################### +# # +# Copyright (c) 2017 Idiap Research Institute, http://www.idiap.ch/ # +# Contact: beat.support@idiap.ch # +# # +# This file is part of the beat.web module of the BEAT platform. # +# # +# Commercial License Usage # +# Licensees holding valid commercial BEAT licenses may use this file in # +# accordance with the terms contained in a written agreement between you # +# and Idiap. For further information contact tto@idiap.ch # +# # +# Alternatively, this file may be used under the terms of the GNU Affero # +# Public License version 3 as published by the Free Software and appearing # +# in the file LICENSE.AGPL included in the packaging of this file. # +# The BEAT platform 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. # +# # +# You should have received a copy of the GNU Affero Public License along # +# with the BEAT platform. If not, see http://www.gnu.org/licenses/. # +# # +############################################################################### + +from __future__ import unicode_literals + +from django.db.models import Q +from django.core.urlresolvers import reverse +from django.template import loader +from django.template import Context + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +from django.contrib.auth.models import User +from datetime import datetime, timedelta +import datetime +import re +from urlparse import urlparse + +import simplejson as json + +def set_profile_state(apps, schema_editor): + '''Set profile status''' + + profiles = apps.get_model("accounts", "Profile") + supervisiontracks = apps.get_model("accounts", "SupervisionTrack") + + users = User.objects.all() + now = datetime.datetime.now() + expiration_date_delta = datetime.timedelta(days=settings.ACCOUNT_BLOCKAGE_AFTER_FIRST_REJECTION_DAYS) + + specialusers = ["AnonymousUser", "plot", "system", "scheduler"] + for user in users: + user.save() + if user.is_staff or user.username in specialusers: + user.profile.status = 'A' + user.profile.rejection_date = None + else: + #reject this account and inform by email the user + user.profile.status = 'R' + user.profile.rejection_date = now + expiration_date_delta + from django.core.mail import send_mail + + parsed_url = urlparse(settings.URL_PREFIX) + server_address = '%s://%s' % (parsed_url.scheme, parsed_url.hostname) + + c = Context({ 'user': user, + 'prefix': server_address, + }) + + try: + t = loader.get_template('registration/mail.migration_10_accounts.subject.txt') + subject = t.render(c) + + # Note: e-mail subject *must not* contain newlines + subject = settings.EMAIL_SUBJECT_PREFIX + ''.join(subject.splitlines()) + + t = loader.get_template('registration/mail.migration_10_accounts.message.txt') + message = t.render(c) + + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [user.email]) + except: + pass + + user.profile.supervision_key = None + + user.profile.save() + user.save() + +def backward_dummy(apps, schema_editor): + pass + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0009_auto_20170627_0956'), + ] + + operations = [ + migrations.RunPython(set_profile_state, backward_dummy) + ] diff --git a/beat/web/accounts/models.py b/beat/web/accounts/models.py index 79b0629bda6e06f62d3e3f012418680ea968f4e7..ae35b2a0556be1daac5fb5e2a0f0b9fcc18bd2b8 100644 --- a/beat/web/accounts/models.py +++ b/beat/web/accounts/models.py @@ -27,6 +27,11 @@ from django.db import models from django.contrib.auth.models import User +from django.db.models.signals import post_save +from django.dispatch import receiver + +import random +import string class AccountSettings(models.Model): @@ -39,3 +44,77 @@ class AccountSettings(models.Model): experiment_mail_notifications_enabled = models.BooleanField(default=True) database_notifications_enabled = models.BooleanField(default=True) environment_notifications_enabled = models.BooleanField(default=True) + + +class SupervisionTrack(models.Model): + + #_____ Fields __________ + + supervisee = models.ForeignKey(User, on_delete=models.CASCADE, + related_name='supervisors') + godfather = models.ForeignKey(User, on_delete=models.CASCADE, + related_name='supervisees') + is_valid = models.BooleanField(default=False) + start_date = models.DateTimeField(null=True, blank=True) + expiration_date = models.DateTimeField(null=True, blank=True) + last_validation_date = models.DateTimeField(null=True, blank=True) + supervision_key = models.CharField(max_length=40, null=True, blank=True) + + def __unicode__(self): + return u'Godfather: %s, Supervisee, %s, Validity: %s' % (self.godfather.username, self.supervisee.username, self.is_valid) + + +class Profile(models.Model): + #_____ Constants __________ + + #New account creation 'N'/'W' + #Godfather acceptance/rejection 'A'/'R' + #Yearly revalidation/blockage 'Y'/'B' + NEWUSER = 'N' + WAITINGVALIDATION = 'W' + ACCEPTED = 'A' + REJECTED = 'R' + YEARREVALIDATION = 'Y' + BLOCKED = 'B' + + PROFILE_STATUS = ( + (NEWUSER, 'New User'), + (WAITINGVALIDATION, 'Waiting Validation'), + (ACCEPTED, 'Accepted'), + (REJECTED, 'Rejected'), + (YEARREVALIDATION, 'Yearly revalidation'), + (BLOCKED, 'Blocked no supervisor'), + ) + + #_____ Fields __________ + + # This field is required. + user = models.OneToOneField(User, on_delete=models.CASCADE) + + # Other fields here + status = models.CharField(max_length=1, choices=PROFILE_STATUS, default=BLOCKED) + is_godfather = models.BooleanField(default=False) + supervision = models.ManyToManyField(SupervisionTrack, related_name='profiles', blank=True) + supervision_key = models.CharField(max_length=40, null=True, blank=True) + registration_date = models.DateTimeField(null=True, blank=True) + rejection_date = models.DateTimeField(null=True, blank=True) + + def __unicode__(self): + return u'User: %s' % self.user.username + + def _generate_current_supervision_key(self): + length = 40 + return ''.join(random.choice(string.ascii_letters + string.digits) for _ + in range(length)) + +@receiver(post_save, sender=User) +def create_user_profile(sender, instance, created, **kwargs): + if created: + Profile.objects.create(user=instance) + else: + return + +@receiver(post_save, sender=User) +def save_user_profile(sender, instance, **kwargs): + player, created = Profile.objects.get_or_create(user=instance) + instance.profile.save() diff --git a/beat/web/accounts/permissions.py b/beat/web/accounts/permissions.py new file mode 100644 index 0000000000000000000000000000000000000000..18f8c351a8ba901c8068d2ca826d25a0ac815948 --- /dev/null +++ b/beat/web/accounts/permissions.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : + +############################################################################### +# # +# Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ # +# Contact: beat.support@idiap.ch # +# # +# This file is part of the beat.web module of the BEAT platform. # +# # +# Commercial License Usage # +# Licensees holding valid commercial BEAT licenses may use this file in # +# accordance with the terms contained in a written agreement between you # +# and Idiap. For further information contact tto@idiap.ch # +# # +# Alternatively, this file may be used under the terms of the GNU Affero # +# Public License version 3 as published by the Free Software and appearing # +# in the file LICENSE.AGPL included in the packaging of this file. # +# The BEAT platform 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. # +# # +# You should have received a copy of the GNU Affero Public License along # +# with the BEAT platform. If not, see http://www.gnu.org/licenses/. # +# # +############################################################################### + +from rest_framework import permissions + + +#---------------------------------------------------------- + + +class IsGodfatherAndAuthor(permissions.BasePermission): + """ + The logged in user should also be the author + """ + message = 'Not a supervisor account' + + def has_permission(self, request, view): + return request.user.profile.is_godfather + + +#---------------------------------------------------------- + + +class IsAuthorAndNotGodfather(permissions.BasePermission): + """ + The logged in user should also be the author + """ + message = 'Not a supervisee account' + + def has_permission(self, request, view): + return not request.user.profile.is_godfather diff --git a/beat/web/accounts/serializers.py b/beat/web/accounts/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..491af9bda3c9e613818c0765f42d0b228e87c395 --- /dev/null +++ b/beat/web/accounts/serializers.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : + +############################################################################### +# # +# Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ # +# Contact: beat.support@idiap.ch # +# # +# This file is part of the beat.web module of the BEAT platform. # +# # +# Commercial License Usage # +# Licensees holding valid commercial BEAT licenses may use this file in # +# accordance with the terms contained in a written agreement between you # +# and Idiap. For further information contact tto@idiap.ch # +# # +# Alternatively, this file may be used under the terms of the GNU Affero # +# Public License version 3 as published by the Free Software and appearing # +# in the file LICENSE.AGPL included in the packaging of this file. # +# The BEAT platform 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. # +# # +# You should have received a copy of the GNU Affero Public License along # +# with the BEAT platform. If not, see http://www.gnu.org/licenses/. # +# # +############################################################################### + +from django.contrib.auth.models import User, AnonymousUser + +from rest_framework import serializers + +from .models import Profile, SupervisionTrack +from ..common.models import Contribution +from ..common.fields import JSONSerializerField +from ..ui.templatetags.markup import restructuredtext +from ..common.utils import validate_restructuredtext + +import simplejson as json + + +#---------------------------------------------------------- + + +class UserSerializer(serializers.ModelSerializer): + username = serializers.SerializerMethodField() + email = serializers.SerializerMethodField() + + class Meta: + model = User + fields = ['username', 'email'] + + def get_username(self, obj): + return obj.username + + def get_email(self, obj): + return obj.email + + +#---------------------------------------------------------- + + +class BasicSupervisionTrackSerializer(serializers.ModelSerializer): + supervisee = UserSerializer() + godfather = UserSerializer() + is_valid = serializers.SerializerMethodField() + start_date = serializers.SerializerMethodField() + expiration_date = serializers.SerializerMethodField() + last_validation_date = serializers.SerializerMethodField() + supervision_key = serializers.SerializerMethodField() + + class Meta: + model = SupervisionTrack + fields = ['is_valid'] + + #def get_supervisee(self, obj): + # return obj.supervisee + + #def get_godfather(self, obj): + # return obj.godfather + + def get_is_valid(self, obj): + return obj.is_valid + + def get_start_date(self, obj): + return obj.start_date + + def get_expiration_date(self, obj): + return obj.expiration_date + + def get_last_validation_date(self, obj): + return obj.last_validation_date + + def get_supervision_key(self, obj): + return obj.supervision_key + + +#---------------------------------------------------------- + + +class FullSupervisionTrackSerializer(BasicSupervisionTrackSerializer): + + class Meta(BasicSupervisionTrackSerializer.Meta): + fields = ['supervisee', 'godfather', 'is_valid', 'start_date', 'expiration_date','last_validation_date', 'supervision_key'] + + +#---------------------------------------------------------- + + +class SupervisionTrackUpdateSerializer(BasicSupervisionTrackSerializer): + pass + + +#---------------------------------------------------------- diff --git a/beat/web/accounts/static/accounts/css/dialogs.css b/beat/web/accounts/static/accounts/css/dialogs.css new file mode 100644 index 0000000000000000000000000000000000000000..004a329b992a0615901c2e7e2c606f69256f9f23 --- /dev/null +++ b/beat/web/accounts/static/accounts/css/dialogs.css @@ -0,0 +1,96 @@ +/****************** ACCOUNT DIALOG ******************************/ + +div.renew_account .progress +{ + margin: 40px auto; +} + + +div.renew_account #infos +{ + text-align: center; + display: block; + margin-top: 10px; + margin-bottom: 10px; + color: #777777; +} + + +div.renew_account span.label +{ + display: inline-block; + width: 15%; + color: black; + font-size: 1em; +} + + +div.renew_account input#prefix, +div.renew_account input#suffix +{ + display: inline-block; + width: 79%; +} + + +div.renew_account input#suffix +{ + margin-top: 10px; +} + + +div.renew_account span.documentation +{ + color: #AAAAAA; + font-size: 0.8em; +} + +div.change_supervisor.progress +{ + margin: 40px auto; +} + + +div.change_supervisor #infos +{ + text-align: center; + display: block; + margin-top: 10px; + margin-bottom: 10px; + color: #777777; +} + + +div.change_supervisor span.label +{ + display: inline-block; + width: 50%; + color: black; + font-size: 1em; +} + + +div.change_supervisor input#new_godfather_username +{ + display: inline-block; + width: 50%; +} + + +div.change_supervisor input#new_godfather_username +{ + margin-top: 10px; +} + + +div.change_supervisor span.documentation +{ + color: #AAAAAA; + font-size: 0.8em; +} + +.btn-supervisee +{ + padding-top: 0em; + padding-bottom: 0em; +} diff --git a/beat/web/accounts/static/accounts/js/dialogs.js b/beat/web/accounts/static/accounts/js/dialogs.js new file mode 100644 index 0000000000000000000000000000000000000000..2e65ddcafc38b49acc4dedc80fe87c2a38c57b35 --- /dev/null +++ b/beat/web/accounts/static/accounts/js/dialogs.js @@ -0,0 +1,434 @@ +/* + * Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ + * Contact: beat.support@idiap.ch + * + * This file is part of the beat.web module of the BEAT platform. + * + * Commercial License Usage + * Licensees holding valid commercial BEAT licenses may use this file in + * accordance with the terms contained in a written agreement between you + * and Idiap. For further information contact tto@idiap.ch + * + * Alternatively, this file may be used under the terms of the GNU Affero + * Public License version 3 as published by the Free Software and appearing + * in the file LICENSE.AGPL included in the packaging of this file. + * The BEAT platform 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. + * + * You should have received a copy of the GNU Affero Public License along + * with the BEAT platform. If not, see http://www.gnu.org/licenses/. +*/ +/** + * beat.accounts.dialogs.js + * Implementation of experiment-related dialogs + */ +if (beat === undefined) var beat = {}; +if (beat.accounts === undefined) beat.accounts = {}; +if (beat.accounts.dialogs === undefined) beat.accounts.dialogs = {}; + +/** + * Implements a modal dialog to renew a user account. + * The user is presented with a small modal window that will allow + * him to certify the wish to renew his account. It is possible to cancel + * the action at any moment by pressing "Cancel" or hitting the ESC key. + * + * Parameters: + * + * dialog_id: ID of the DOM element to use + * api_url (str): The URL towards the API for renewing the account + * redirect_url (str): The URL where to redirect the user if the operation is + * successful + */ +beat.accounts.dialogs.modal_renew_account = function(dialog_id, api_url, redirect_url) { + + // Create the dialog + $('#' + dialog_id).dialog({ + autoOpen: false, + resizable: false, + width: Math.min($(window).width() * 0.6, 700), + position: { my: "center", at: "center", of: window }, + modal: true, + closeOnEscape: true, + buttons: [ + { + id: 'button-' + dialog_id + '-renameit', + text: 'Revalidate', + click: function() { + + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + var csrftoken = $.cookie('csrftoken'); + xhr.setRequestHeader('X-CSRFToken', csrftoken); + } + }); + + $.ajax({ + type: "PUT", + url: api_url, + data: JSON.stringify({ + name: name, + }), + contentType: "application/json; charset=utf-8", + dataType: "json", + + success: function(data) { + $('#' + dialog_id).dialog("close"); + window.location.href = redirect_url; + }, + + error: function(jqXHR, textStatus, errorThrown) { + if ((jqXHR.status == 400) && (jqXHR.responseText.length > 0)) + alert(jqXHR.responseText); + else + alert('Error: ' + errorThrown); + + $(this).dialog("close"); + } + }); + } + }, + { + id: 'button-' + dialog_id + '-cancel', + text: 'Cancel', + click: function() { + $(this).dialog("close"); + } + } + ] + }); + + // Display the dialog + $('#' + dialog_id).dialog("open"); +} + +/** + * Implements a modal dialog to change supervisor. + * The user is presented with a small modal window that will allow + * him to add the new supervisor username. It is possible to cancel the action at + * any moment by pressing "Cancel" or hitting the ESC key. + * + * Parameters: + * + * dialog_id: ID of the DOM element to use + * old_godfather_username: previous supervisor username + * api_url (str): The URL towards the API for changing the supervisor + * redirect_url (str): The URL where to redirect the user if the operation is + * successful + */ +beat.accounts.dialogs.modal_change_supervisor = function(dialog_id, old_godfather_username, api_url, redirect_url) { + + var split_api_url = api_url.split(old_godfather_username); + + // Create the dialog + $('#' + dialog_id).dialog({ + autoOpen: false, + resizable: false, + width: Math.min($(window).width() * 0.6, 700), + position: { my: "center", at: "center", of: window }, + modal: true, + closeOnEscape: true, + buttons: [ + { + id: 'button-' + dialog_id + '-renameit', + text: 'Request', + click: function() { + + var new_godfather_username = $('#' + dialog_id + ' #new_godfather_username')[0].value; + + var name = $('#' + dialog_id + ' input')[0].value.trim(); + + if (name.length == 0) + { + alert("You can't enter an empty supervisor username!"); + return; + } + + if (new_godfather_username == old_godfather_username) + { + alert("The new supervisor username is the same as the previous one!"); + return; + } + + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + var csrftoken = $.cookie('csrftoken'); + xhr.setRequestHeader('X-CSRFToken', csrftoken); + } + }); + + $.ajax({ + type: "POST", + url: split_api_url[0] + new_godfather_username + split_api_url[1], + data: JSON.stringify({ + name: name, + }), + contentType: "application/json; charset=utf-8", + dataType: "json", + + success: function(data) { + $('#' + dialog_id).dialog("close"); + window.location.href = redirect_url; + }, + + error: function(jqXHR, textStatus, errorThrown) { + if ((jqXHR.status == 400) && (jqXHR.responseText.length > 0)) + alert(jqXHR.responseText); + else + alert('Error: ' + errorThrown); + } + }); + } + }, + { + id: 'button-' + dialog_id + '-cancel', + text: 'Cancel', + click: function() { + $(this).dialog("close"); + } + } + ] + }); + + // Initialise the dialog content + $('#' + dialog_id + ' input')[0].value = old_godfather_username; + + // Display the dialog + $('#' + dialog_id).dialog("open"); +} + +/** + * Implements a modal dialog to add a supervisor. + * The user is presented with a small modal window that will allow + * him to add to set the supervisor username. It is possible to cancel the action at + * any moment by pressing "Cancel" or hitting the ESC key. + * + * Parameters: + * + * dialog_id: ID of the DOM element to use + * api_url (str): The URL towards the API for adding a supervisor + * redirect_url (str): The URL where to redirect the user if the operation is + * successful + */ +beat.accounts.dialogs.modal_add_supervisor = function(dialog_id, api_url, redirect_url) { + + var split_api_url = api_url.split("nogodfather"); + + // Create the dialog + $('#' + dialog_id).dialog({ + autoOpen: false, + resizable: false, + width: Math.min($(window).width() * 0.6, 700), + position: { my: "center", at: "center", of: window }, + modal: true, + closeOnEscape: true, + buttons: [ + { + id: 'button-' + dialog_id + '-renameit', + text: 'Request', + click: function() { + + var new_godfather_username = $('#' + dialog_id + ' #new_godfather_username')[0].value; + + var name = $('#' + dialog_id + ' input')[0].value.trim(); + + if (name.length == 0) + { + alert("You can't enter an empty supervisor username!"); + return; + } + + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + var csrftoken = $.cookie('csrftoken'); + xhr.setRequestHeader('X-CSRFToken', csrftoken); + } + }); + + $.ajax({ + type: "POST", + url: split_api_url[0] + new_godfather_username + split_api_url[1], + data: JSON.stringify({ + name: name, + }), + contentType: "application/json; charset=utf-8", + dataType: "json", + + success: function(data) { + $('#' + dialog_id).dialog("close"); + window.location.href = redirect_url; + }, + + error: function(jqXHR, textStatus, errorThrown) { + if ((jqXHR.status == 400) && (jqXHR.responseText.length > 0)) + alert(jqXHR.responseText); + else + alert('Error: ' + errorThrown); + } + }); + } + }, + { + id: 'button-' + dialog_id + '-cancel', + text: 'Cancel', + click: function() { + $(this).dialog("close"); + } + } + ] + }); + + // Initialise the dialog content + $('#' + dialog_id + ' input')[0].value = ''; + + // Display the dialog + $('#' + dialog_id).dialog("open"); +} + +/** + * Implements a modal dialog for the supervisor to validate a supervisee + * account. It is possible to cancel the action at any moment by pressing + * "Cancel" or hitting the ESC key. + * + * Parameters: + * + * dialog_id: ID of the DOM element to use + * api_url (str): The URL towards the API to validate a supervisee + * redirect_url (str): The URL where to redirect the user if the operation is + * successful + */ +beat.accounts.dialogs.modal_validate_supervisee = function(dialog_id, api_url, redirect_url) { + + // Create the dialog + $('#' + dialog_id).dialog({ + autoOpen: false, + resizable: false, + width: Math.min($(window).width() * 0.6, 700), + position: { my: "center", at: "center", of: window }, + modal: true, + closeOnEscape: true, + buttons: [ + { + id: 'button-' + dialog_id + '-renameit', + text: 'Accept', + click: function() { + + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + var csrftoken = $.cookie('csrftoken'); + xhr.setRequestHeader('X-CSRFToken', csrftoken); + } + }); + + $.ajax({ + type: "PUT", + url: api_url, + data: JSON.stringify({ + name: name, + }), + contentType: "application/json; charset=utf-8", + dataType: "json", + + success: function(data) { + $('#' + dialog_id).dialog("close"); + window.location.href = redirect_url; + }, + + error: function(jqXHR, textStatus, errorThrown) { + if ((jqXHR.status == 400) && (jqXHR.responseText.length > 0)) + alert(jqXHR.responseText); + else + alert('Error: ' + errorThrown); + + $(this).dialog("close"); + } + }); + } + }, + { + id: 'button-' + dialog_id + '-cancel', + text: 'Cancel', + click: function() { + $(this).dialog("close"); + } + } + ] + }); + + // Display the dialog + $('#' + dialog_id).dialog("open"); +} + +/** + * Implements a modal dialog for the supervisor to remove a supervisee + * account. It is possible to cancel the action at any moment by pressing + * "Cancel" or hitting the ESC key. + * + * Parameters: + * + * dialog_id: ID of the DOM element to use + * api_url (str): The URL towards the API to remove a supervisee + * redirect_url (str): The URL where to redirect the user if the operation is + * successful + */ +beat.accounts.dialogs.modal_remove_supervisee = function(dialog_id, api_url, redirect_url) { + + // Create the dialog + $('#' + dialog_id).dialog({ + autoOpen: false, + resizable: false, + width: Math.min($(window).width() * 0.6, 700), + position: { my: "center", at: "center", of: window }, + modal: true, + closeOnEscape: true, + buttons: [ + { + id: 'button-' + dialog_id + '-renameit', + text: 'Reject', + click: function() { + + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + var csrftoken = $.cookie('csrftoken'); + xhr.setRequestHeader('X-CSRFToken', csrftoken); + } + }); + + $.ajax({ + type: "PUT", + url: api_url, + data: JSON.stringify({ + name: name, + }), + contentType: "application/json; charset=utf-8", + dataType: "json", + + success: function(data) { + $('#' + dialog_id).dialog("close"); + window.location.href = redirect_url; + }, + + error: function(jqXHR, textStatus, errorThrown) { + if ((jqXHR.status == 400) && (jqXHR.responseText.length > 0)) + alert(jqXHR.responseText); + else + alert('Error: ' + errorThrown); + + $(this).dialog("close"); + } + }); + } + }, + { + id: 'button-' + dialog_id + '-cancel', + text: 'Cancel', + click: function() { + $(this).dialog("close"); + } + } + ] + }); + + // Display the dialog + $('#' + dialog_id).dialog("open"); +} diff --git a/beat/web/accounts/templates/accounts/dialogs/change_supervisor.html b/beat/web/accounts/templates/accounts/dialogs/change_supervisor.html new file mode 100644 index 0000000000000000000000000000000000000000..b4156162b534b29c739d3d7ecce14c92675da92d --- /dev/null +++ b/beat/web/accounts/templates/accounts/dialogs/change_supervisor.html @@ -0,0 +1,32 @@ +{% comment %} + * Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ + * Contact: beat.support@idiap.ch + * + * This file is part of the beat.web module of the BEAT platform. + * + * Commercial License Usage + * Licensees holding valid commercial BEAT licenses may use this file in + * accordance with the terms contained in a written agreement between you + * and Idiap. For further information contact tto@idiap.ch + * + * Alternatively, this file may be used under the terms of the GNU Affero + * Public License version 3 as published by the Free Software and appearing + * in the file LICENSE.AGPL included in the packaging of this file. + * The BEAT platform 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. + * + * You should have received a copy of the GNU Affero Public License along + * with the BEAT platform. If not, see http://www.gnu.org/licenses/. +{% endcomment %} + +<div id="{{ dialog_id }}" class="change_supervisor" style="display:none;" title="Supervision update"> + <div class="settings"> + <div><span id="infos"></span></div> + Take into account that requesting a new supervision will temporarely set your account to an inactive state until the request is validated by your new supervisor.<br> + If your new supervisor rejects your supervision request, you need to find another supervisor.<br> + If no supervisor is found after a specific date, your account will be blocked and you will need to proceed with an account reactivation request<br> + <div><span class="label">New supervisor username: </span><input id="new_godfather_username" type="text" /></div> + <div><span class="documentation">A request for supervision will be sent to the new supervisor</span></div> + </div> +</div> diff --git a/beat/web/accounts/templates/accounts/dialogs/remove_supervisee.html b/beat/web/accounts/templates/accounts/dialogs/remove_supervisee.html new file mode 100644 index 0000000000000000000000000000000000000000..a4743fbe2a34563d33aa71b2cc6fdccca05ab6cd --- /dev/null +++ b/beat/web/accounts/templates/accounts/dialogs/remove_supervisee.html @@ -0,0 +1,28 @@ +{% comment %} + * Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ + * Contact: beat.support@idiap.ch + * + * This file is part of the beat.web module of the BEAT platform. + * + * Commercial License Usage + * Licensees holding valid commercial BEAT licenses may use this file in + * accordance with the terms contained in a written agreement between you + * and Idiap. For further information contact tto@idiap.ch + * + * Alternatively, this file may be used under the terms of the GNU Affero + * Public License version 3 as published by the Free Software and appearing + * in the file LICENSE.AGPL included in the packaging of this file. + * The BEAT platform 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. + * + * You should have received a copy of the GNU Affero Public License along + * with the BEAT platform. If not, see http://www.gnu.org/licenses/. +{% endcomment %} + +<div id="{{ dialog_id }}" class="renew_account" style="display:none;" title="Remove supervisee"> + <div class="settings"> + <div><span id="infos"></span></div> + This action will remove the supervisee from your supervision list. + </div> +</div> diff --git a/beat/web/accounts/templates/accounts/dialogs/renew_account.html b/beat/web/accounts/templates/accounts/dialogs/renew_account.html new file mode 100644 index 0000000000000000000000000000000000000000..e94afdb87174c7054c4a3c8db55a3cf643492431 --- /dev/null +++ b/beat/web/accounts/templates/accounts/dialogs/renew_account.html @@ -0,0 +1,29 @@ +{% comment %} + * Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ + * Contact: beat.support@idiap.ch + * + * This file is part of the beat.web module of the BEAT platform. + * + * Commercial License Usage + * Licensees holding valid commercial BEAT licenses may use this file in + * accordance with the terms contained in a written agreement between you + * and Idiap. For further information contact tto@idiap.ch + * + * Alternatively, this file may be used under the terms of the GNU Affero + * Public License version 3 as published by the Free Software and appearing + * in the file LICENSE.AGPL included in the packaging of this file. + * The BEAT platform 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. + * + * You should have received a copy of the GNU Affero Public License along + * with the BEAT platform. If not, see http://www.gnu.org/licenses/. +{% endcomment %} + +<div id="{{ dialog_id }}" class="renew_account" style="display:none;" title="Account revalidation"> + <div class="settings"> + <div><span id="infos"></span></div> + By revalidating your account you acknowledge that you wish to continue using the platform and your + supervision is still valid. + </div> +</div> diff --git a/beat/web/accounts/templates/accounts/dialogs/validate_supervisee.html b/beat/web/accounts/templates/accounts/dialogs/validate_supervisee.html new file mode 100644 index 0000000000000000000000000000000000000000..e90a8ed36cb54c4c6fedb0a9524dae437c179ee6 --- /dev/null +++ b/beat/web/accounts/templates/accounts/dialogs/validate_supervisee.html @@ -0,0 +1,28 @@ +{% comment %} + * Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ + * Contact: beat.support@idiap.ch + * + * This file is part of the beat.web module of the BEAT platform. + * + * Commercial License Usage + * Licensees holding valid commercial BEAT licenses may use this file in + * accordance with the terms contained in a written agreement between you + * and Idiap. For further information contact tto@idiap.ch + * + * Alternatively, this file may be used under the terms of the GNU Affero + * Public License version 3 as published by the Free Software and appearing + * in the file LICENSE.AGPL included in the packaging of this file. + * The BEAT platform 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. + * + * You should have received a copy of the GNU Affero Public License along + * with the BEAT platform. If not, see http://www.gnu.org/licenses/. +{% endcomment %} + +<div id="{{ dialog_id }}" class="renew_account alert-warning" style="display:none;" title="Validate supervisee"> + <div class="settings"> + <div><span id="infos"></span></div> + This action will add the supervisee to your supervision list. You therefore acknowledge that this user works under your supervision and follows the platform terms of service. + </div> +</div> diff --git a/beat/web/accounts/templates/accounts/settings.html b/beat/web/accounts/templates/accounts/settings.html index 6b00af35064bde03c0d6229d80a6e50cf790a5c8..075999f8cb4f68eba5714bb9d8f1ef0b341f2274 100644 --- a/beat/web/accounts/templates/accounts/settings.html +++ b/beat/web/accounts/templates/accounts/settings.html @@ -25,9 +25,27 @@ {% load ui_tags %} {% load gravatar %} {% load registration_tags %} +{% load account_tags %} {% block title %}Settings for {{ request.user.first_name }} ({{ request.user.username }}){% endblock %} +{% block stylesheets %} +{{ block.super }} +<link rel="stylesheet" href="{% fingerprint "accounts/css/dialogs.css" %}" type="text/css" media="screen" /> +<link rel="stylesheet" href="{% fingerprint "jquery-ui/themes/base/minified/jquery-ui.min.css" %}" type="text/css" media="screen" /> +{% endblock %} + +{% block scripts %} +{{ block.super }} +{% csrf_token %} +<script src="{% fingerprint "accounts/js/dialogs.js" %}" type="text/javascript" charset="utf-8"></script> +<script src="{% fingerprint "jquery-ui/ui/minified/jquery.ui.core.min.js" %}" type="text/javascript" charset="utf-8"></script> +<script src="{% fingerprint "jquery-ui/ui/minified/jquery.ui.position.min.js" %}" type="text/javascript" charset="utf-8"></script> +<script src="{% fingerprint "jquery-ui/ui/minified/jquery.ui.widget.min.js" %}" type="text/javascript" charset="utf-8"></script> +<script src="{% fingerprint "jquery-ui/ui/minified/jquery.ui.button.min.js" %}" type="text/javascript" charset="utf-8"></script> +<script src="{% fingerprint "jquery-ui/ui/minified/jquery.ui.dialog.min.js" %}" type="text/javascript" charset="utf-8"></script> +{% endblock %} + {% block content %} <div class="row"> <div class="col-sm-6 col-sm-offset-3"> @@ -178,6 +196,196 @@ </div> </div> +{% if not user.profile.is_godfather and not user.is_superuser %} + +<div class="row"> + + <div class="col-sm-4 col-sm-offset-1"> + + <div class="panel panel-default"> + <div class="panel-heading"><i class="fa fa-credit-card-alt"></i> Account management + {% if user.profile.status == 'A' %} + <span class="label label-success" style="float:right">Status Accepted</span> + {% elif user.profile.status == 'R' %} + {% if user.profile.supervision_key == None %} + <span class="label label-danger" style="float:right">Status Rejected</span> + {% else %} + <span class="label label-warning" style="float:right">Status Pending</span> + {% endif %} + {% elif user.profile.status == 'Y' %} + <span class="label label-warning" style="float:right">Status Revalidation Required</span> + {% endif %} + </div> + <div class="panel-body"> + <form id="token" method="post" action="" class="form"> + {% csrf_token %} + <div class="form-group" style="text-align: justify;"> + {% if user.profile.status == 'A' %} + <li class="list-group-item list-group-item-success" style="text-align:center;">Status is currently accepted: No action is directly required</li> + <br> + <h4>Supervision Information</h4> + <ul class="list-group"> + <li class="list-group-item"><b>Supervisor:</b> <a href="{% url "activity-stream" supervisiontrack.godfather.username %}">{{ supervisiontrack.godfather.username }}</a></li> + <li class="list-group-item"><b>Supervision start date:</b> {{ supervisiontrack.start_date }}</li> + <li class="list-group-item"><b>Supervision last validation date:</b> {{ supervisiontrack.last_validation_date }}</li> + <li class="list-group-item"><b>Supervision expiration date:</b> {{ supervisiontrack.expiration_date }}</li> + </ul> + <div class="form-group" style="text-align: center;"> + <input type="hidden" name="token" value="true"> + <button type="button" class="btn btn-warning" onclick="beat.accounts.dialogs.modal_change_supervisor('change_supervisor', '{{ supervisiontrack.godfather.username }}', '{% url 'api_accounts:add_supervisor' supervisiontrack.godfather.username %}', '{% url 'accounts:settings' %}');"><i class="fa fa-refresh fa-fw fa-lg"></i> Change supervisor?</button> + </div> + {% elif user.profile.status == 'R' %} + {% if user.profile.supervision_key == None %} + <li class="list-group-item list-group-item-danger" style="text-align:center;">Status is rejected: You need to find a new supervisor</li> + <br> + <h4>General Information</h4> + <ul class="list-group"> + <li class="list-group-item"><b>You need to find a new supervisor to continue using your account</b></li> + <li class="list-group-item"><b>Deadline account blockage date (if no supervisor):</b> {{ user.profile.rejection_date }}</li> + <li class="list-group-item"><i>If your account gets blocked you can still follow the account reactivation procedure to re-activate it.</i></li> + </ul> + <div class="form-group" style="text-align: center;"> + <input type="hidden" name="token" value="true"> + <button type="button" class="btn btn-danger" onclick="beat.accounts.dialogs.modal_add_supervisor('change_supervisor', '{% url 'api_accounts:add_supervisor' 'nogodfather' %}', '{% url 'accounts:settings' %}');"><i class="fa fa-plus fa-fw fa-lg"></i> Add a supervisor</button> + </div> + {% else %} + <li class="list-group-item list-group-item-warning" style="text-align:center;">Status is pending: You need to wait approval from supervisor</li> + <br> + <h4>General Information about a pending request</h4> + <ul class="list-group"> + <li class="list-group-item"><b>Pending request made to supervisor:</b> {{ supervisiontrack.godfather.username }}</li> + <li class="list-group-item"><b>Deadline account blockage date (if no supervisor):</b> {{ user.profile.rejection_date }}</li> + <li class="list-group-item"><i>You need to be patient until this supervisor accepts your request. An email was sent about your request, but in the meantime you can remind him to accept your supervision.<br> + If this supervisor rejects your supervision request, you will be informed and you will get the opportunity to make a new one.<br> + If your account gets blocked you can still follow the account reactivation procedure to re-activate it.</i></li> + </ul> + {% endif %} + <br> + {% elif user.profile.status == 'Y' %} + <li class="list-group-item list-group-item-warning" style="text-align:center;">Revalidation required: You need to confirm that you wish to revalidate your account (year basis check)</li> + <br> + <h4>General Information</h4> + <ul class="list-group"> + <li class="list-group-item"><b>Supervisor:</b> {{ supervisiontrack.godfather.username }}</li> + <li class="list-group-item"><b>Supervision Track started on:</b> {{ supervisiontrack.start_date }}</li> + <li class="list-group-item"><b>Supervision Track last validation date:</b> {{ supervisiontrack.last_validation_date }}</li> + <li class="list-group-item"><b>Supervision Track expiration date:</b> {{ supervisiontrack.expiration_date }}</li> + </ul> + <div class="form-group" style="text-align: center;"> + <input type="hidden" name="token" value="true"> + <button type="button" class="btn btn-warning" onclick="beat.accounts.dialogs.modal_renew_account('account_renew', '{% url 'api_accounts:revalidate_account' %}', '{% url 'accounts:settings' %}');"><i class="fa fa-refresh fa-fw fa-lg"></i> Revalidate account for 12 months</button> + </div> + {% endif %} + </div> + </form> + </div> + + </div> + + </div> +</div> + +{% else %} +{% if supervisiontracks_pending|length > 0 or supervisiontracks_valid|length > 0 %} +<div class="row"> + + <div class="col-sm-6 col-sm-offset-1"> + + <div class="panel panel-default"> + <div class="panel-heading"><i class="fa fa-users"></i> Supervision management + </div> + <div class="panel-body"> + <form id="token" method="post" action="" class="form"> + {% csrf_token %} + <div class="form-group" style="text-align: justify;"> + {% if supervisiontracks_pending|length > 0 %} + <li class="list-group-item list-group-item-danger" style="text-align:center;">Pending requests: You have a few days to accept or reject these supervision requests</li> + <br> + <table class="table"> + <thead> + <tr> + <th>Firstname</th> + <th>Lastname</th> + <th>Username</th> + <th>Email</th> + <th>Actions</th> + </tr> + </thead> + <tbody> + {% for supervisiontrack_pending in supervisiontracks_pending%} + <tr class="danger"> + <td>{{supervisiontrack_pending.supervisee.first_name}}</td> + <td>{{supervisiontrack_pending.supervisee.last_name}}</td> + <td>{{supervisiontrack_pending.supervisee.username}}</td> + <td>{{supervisiontrack_pending.supervisee.email}}</td> + <td> + + <button type="button" class="btn btn-success btn-supervisee" onclick="beat.accounts.dialogs.modal_validate_supervisee('validate_supervisee', '{% url 'api_accounts:validate_supervisee' supervisiontrack_pending.supervisee.username %}', '{% url 'accounts:settings' %}');"> Accept</button> + <button type="button" class="btn btn-danger btn-supervisee" onclick="beat.accounts.dialogs.modal_remove_supervisee('remove_supervisee', '{% url 'api_accounts:remove_supervisee' supervisiontrack_pending.supervisee.username %}', '{% url 'accounts:settings' %}');"> Reject</button> + </td> + </tr> + {% endfor %} + </tbody> + </table> + <hr /> + {% endif %} + + {% if supervisiontracks_valid|length > 0 %} + <li class="list-group-item" style="text-align:center;"><b>Supervision list</b></li> + <table class="table"> + <thead> + <tr> + <th>Firstname</th> + <th>Lastname</th> + <th>Username</th> + <th>Email</th> + <th>Start Date</th> + <th>Expiration Date</th> + <th>Actions</th> + </tr> + </thead> + <tbody> + {% for supervisiontrack_valid in supervisiontracks_valid%} + {% if supervisiontrack_valid.supervisee.profile.status == 'A' %} + <tr class="success"> + {% else %} + <tr class="warning"> + {% endif %} + <td>{{supervisiontrack_valid.supervisee.first_name}}</td> + <td>{{supervisiontrack_valid.supervisee.last_name}}</td> + <td>{{supervisiontrack_valid.supervisee.username}}</td> + <td>{{supervisiontrack_valid.supervisee.email}}</td> + <td>{{supervisiontrack_valid.start_date}}</td> + <td>{{supervisiontrack_valid.expiration_date}}</td> + <td> + <button type="button" class="btn btn-danger btn-supervisee" onclick="beat.accounts.dialogs.modal_remove_supervisee('remove_supervisee', '{% url 'api_accounts:remove_supervisee' supervisiontrack_valid.supervisee.username %}', '{% url 'accounts:settings' %}');"> Reject</button> + + </td> + </tr> + {% endfor %} + </tbody> + </table> + + <li class="list-group-item"><i>Valid supervisees are marked in green and supervisees that are under a revalidation process (yearly basis) are marked in yellow. +You can remind them to revalidate their account before the expiration date or else their account will get blocked (and need to pass through an account reactivation procedure)</i><br> + {% endif %} + </div> + </form> + </div> + + </div> + + </div> +</div> + +{% endif %} +{% endif %} + +{% change_supervisor "change_supervisor" %} +{% account_renew "account_renew" %} +{% validate_supervisee "validate_supervisee" %} +{% remove_supervisee "remove_supervisee" %} + <script type="text/javascript"> jQuery(document).ready(function() { $('form#token').submit(function(event) { diff --git a/beat/web/accounts/templatetags/__init__.py b/beat/web/accounts/templatetags/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/beat/web/accounts/templatetags/account_tags.py b/beat/web/accounts/templatetags/account_tags.py new file mode 100644 index 0000000000000000000000000000000000000000..fbfe7d4921b8d44108829444988f669e436bca4a --- /dev/null +++ b/beat/web/accounts/templatetags/account_tags.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : + +############################################################################### +# # +# Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ # +# Contact: beat.support@idiap.ch # +# # +# This file is part of the beat.web module of the BEAT platform. # +# # +# Commercial License Usage # +# Licensees holding valid commercial BEAT licenses may use this file in # +# accordance with the terms contained in a written agreement between you # +# and Idiap. For further information contact tto@idiap.ch # +# # +# Alternatively, this file may be used under the terms of the GNU Affero # +# Public License version 3 as published by the Free Software and appearing # +# in the file LICENSE.AGPL included in the packaging of this file. # +# The BEAT platform 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. # +# # +# You should have received a copy of the GNU Affero Public License along # +# with the BEAT platform. If not, see http://www.gnu.org/licenses/. # +# # +############################################################################### + + +import random + +from django import template +from django.conf import settings + +register = template.Library() + + +@register.inclusion_tag('accounts/dialogs/renew_account.html') +def account_renew(id): + return { 'dialog_id': id, + } + +@register.inclusion_tag('accounts/dialogs/change_supervisor.html') +def change_supervisor(id): + return { 'dialog_id': id, + } + +@register.inclusion_tag('accounts/dialogs/validate_supervisee.html') +def validate_supervisee(id): + return { 'dialog_id': id, + } + +@register.inclusion_tag('accounts/dialogs/remove_supervisee.html') +def remove_supervisee(id): + return { 'dialog_id': id, + } diff --git a/beat/web/accounts/tests.py b/beat/web/accounts/tests.py index 3d08ce2865cc3b14e5b4b0a366d198d64c01ce7c..81a61ce82a147cf52a3f11e0f887e7a68780de7b 100644 --- a/beat/web/accounts/tests.py +++ b/beat/web/accounts/tests.py @@ -3,7 +3,7 @@ ############################################################################### # # -# Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ # +# Copyright (c) 2017 Idiap Research Institute, http://www.idiap.ch/ # # Contact: beat.support@idiap.ch # # # # This file is part of the beat.web module of the BEAT platform. # @@ -25,6 +25,1087 @@ # # ############################################################################### +from django.core.urlresolvers import reverse from django.test import TestCase -# Create your tests here. + +from rest_framework import status +from rest_framework.test import APITestCase + +from django.contrib.auth.models import User +from django.conf import settings + +from .models import SupervisionTrack, Profile + +from datetime import datetime, timedelta +import datetime +import simplejson as json + +#---------------------------------------------------------- + + +class AccountTestCase(APITestCase): + + def setUp(self): + self.tearDown() + + + now = datetime.datetime.now() + expiration_date_delta = datetime.timedelta(days=settings.ACCOUNT_BLOCKAGE_AFTER_FIRST_REJECTION_DAYS) + + # Create the users + self.password = '1234' + + self.firstgodfather = User.objects.create_user('firstgodfather', 'firstgodfather@test.org', self.password) + self.secondgodfather = User.objects.create_user('secondgodfather', 'secondgodfather@test.org', self.password) + self.invalidgodfather = User.objects.create_user('invalidgodfather', 'invalidgodfather@test.org', self.password) + + self.accepteduser = User.objects.create_user('accepteduser', 'accepteduser@test.org', self.password) + self.rejecteduser = User.objects.create_user('rejecteduser', 'rejecteduser@test.org', self.password) + self.yearrevalidationuser = User.objects.create_user('yearrevalidationuser', 'yearrevalidationuser@test.org', self.password) + self.blockeduser = User.objects.create_user('blockeduser', 'blockeduser@test.org', self.password) + + # Update the profiles + #godfathers + self.firstgodfather.profile.status = Profile.ACCEPTED + self.secondgodfather.profile.status = Profile.ACCEPTED + self.invalidgodfather.profile.status = Profile.BLOCKED + self.firstgodfather.profile.is_godfather = True + self.secondgodfather.profile.is_godfather = True + self.invalidgodfather.profile.is_godfather = True + self.invalidgodfather.is_active = False + + self.firstgodfather.save() + self.secondgodfather.save() + self.invalidgodfather.save() + self.firstgodfather.profile.save() + self.secondgodfather.profile.save() + self.invalidgodfather.profile.save() + + #users + self.accepteduser.profile.status = Profile.ACCEPTED + self.rejecteduser.profile.status = Profile.REJECTED + self.yearrevalidationuser.profile.status = Profile.YEARREVALIDATION + self.blockeduser.profile.status = Profile.BLOCKED + + # Create the supervision tracks + #Accepted user + #Create and assign key + self.accepteduser.profile.supervision_key = self.accepteduser.profile._generate_current_supervision_key() + supervisiontrack = SupervisionTrack.objects.create( + supervisee = self.accepteduser, + godfather = self.firstgodfather, + is_valid = True, + start_date = now, + expiration_date = now + expiration_date_delta, + last_validation_date = now + ) + + #Assign key to supervision track + supervisiontrack.supervision_key = self.accepteduser.profile.supervision_key + supervisiontrack.save() + self.accepteduser.profile.supervision.add(supervisiontrack) + self.accepteduser.profile.save() + self.accepteduser.save() + + #Rejected user + #No supervision key but a rejection date + self.rejecteduser.profile.supervision_key = None + self.rejecteduser.profile.rejection_date = now + expiration_date_delta + self.rejecteduser.profile.save() + self.rejecteduser.save() + + #YearRevalidation user + #Create and assign key + self.yearrevalidationuser.profile.supervision_key = self.yearrevalidationuser.profile._generate_current_supervision_key() + supervisiontrack = SupervisionTrack.objects.create( + supervisee = self.yearrevalidationuser, + godfather = self.firstgodfather, + is_valid = True, + start_date = now, + expiration_date = now + expiration_date_delta, + last_validation_date = now + ) + + #Assign key to supervision track + supervisiontrack.supervision_key = self.yearrevalidationuser.profile.supervision_key + supervisiontrack.save() + self.yearrevalidationuser.profile.supervision.add(supervisiontrack) + self.yearrevalidationuser.profile.save() + self.yearrevalidationuser.save() + + #Blocked user + self.blockeduser.profile.supervision_key = None + self.blockeduser.profile.rejection_date = None + self.blockeduser.is_active = False + self.blockeduser.profile.save() + self.blockeduser.save() + + + def tearDown(self): + user = None + pass + + def test_check_access_and_setup(self): + + #Godfathers + user = self.firstgodfather + logged_in = self.client.login(username=user.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(user.username, 'firstgodfather') + logged_out = self.client.logout() + + user = self.secondgodfather + logged_in = self.client.login(username=user.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(user.username, 'secondgodfather') + logged_out = self.client.logout() + + user = self.invalidgodfather + logged_in = self.client.login(username=user.username, password=self.password) + self.assertFalse(logged_in) + self.assertEqual(user.username, 'invalidgodfather') + logged_out = self.client.logout() + + #Users + user = self.accepteduser + logged_in = self.client.login(username=user.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(user.username, 'accepteduser') + logged_out = self.client.logout() + + user = self.rejecteduser + logged_in = self.client.login(username=user.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(user.username, 'rejecteduser') + logged_out = self.client.logout() + + user = self.yearrevalidationuser + logged_in = self.client.login(username=user.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(user.username, 'yearrevalidationuser') + logged_out = self.client.logout() + + user = self.blockeduser + logged_in = self.client.login(username=user.username, password=self.password) + self.assertFalse(logged_in) + self.assertEqual(user.username, 'blockeduser') + logged_out = self.client.logout() + + +#---------------------------------------------------------- + + +class AccountListTestCase(AccountTestCase): + + def setUp(self): + super(AccountListTestCase, self).setUp() + + self.url = reverse('api_accounts:list_supervisee') + + def test_anonymous_user(self): + response = self.client.get(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['detail'] + self.assertEqual(errormsg, 'Authentication credentials were not provided.') + + def test_logged_in_user_not_godfather(self): + + client = self.accepteduser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'accepteduser') + + response = self.client.get(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['detail'] + self.assertEqual(errormsg, 'Not a supervisor account') + + logged_out = self.client.logout() + + def test_logged_in_user_godfather(self): + + client = self.firstgodfather + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'firstgodfather') + + response = self.client.get(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 2) + self.assertEqual(content[0]['supervisee']['username'], 'accepteduser') + self.assertEqual(content[1]['supervisee']['username'], 'yearrevalidationuser') + + logged_out = self.client.logout() + + +#---------------------------------------------------------- + + +class AccountRevalidationTestCase(AccountTestCase): + + def setUp(self): + super(AccountRevalidationTestCase, self).setUp() + + self.url = reverse('api_accounts:revalidate_account') + + def test_anonymous_user(self): + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['detail'] + self.assertEqual(errormsg, 'Authentication credentials were not provided.') + + def test_logged_in_accepted_user(self): + client = self.accepteduser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'accepteduser') + + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['error'] + self.assertEqual(errormsg, "You don't need to revalidate at the moment, your supervision is still valid") + + logged_out = self.client.logout() + + def test_logged_in_rejected_user(self): + client = self.rejecteduser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'rejecteduser') + + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['error'] + self.assertEqual(errormsg, 'You are not allowed to perform this action, you first need to get a valid supervision') + + logged_out = self.client.logout() + + def test_logged_in_yearrevalidation_user(self): + + client = self.yearrevalidationuser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'yearrevalidationuser') + + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + logged_out = self.client.logout() + + def test_logged_in_yearrevalidation_user_twice_validation(self): + + client = self.yearrevalidationuser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'yearrevalidationuser') + + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['error'] + self.assertEqual(errormsg, "You don't need to revalidate at the moment, your supervision is still valid") + + logged_out = self.client.logout() + + def test_logged_in_blocked_user(self): + + client = self.blockeduser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertFalse(logged_in) + self.assertEqual(client.username, 'blockeduser') + + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['detail'] + self.assertEqual(errormsg, 'Authentication credentials were not provided.') + + logged_out = self.client.logout() + + def test_logged_in_user_godfather(self): + + client = self.firstgodfather + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'firstgodfather') + + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['detail'] + self.assertEqual(errormsg, 'Not a supervisee account') + + logged_out = self.client.logout() + + def test_logged_in_invalid_godfather(self): + + client = self.invalidgodfather + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertFalse(logged_in) + self.assertEqual(client.username, 'invalidgodfather') + + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['detail'] + self.assertEqual(errormsg, 'Authentication credentials were not provided.') + + logged_out = self.client.logout() + + +#---------------------------------------------------------- + + +class AccountAddSupervisorTestCase(AccountTestCase): + + def setUp(self): + super(AccountAddSupervisorTestCase, self).setUp() + + self.url = reverse('api_accounts:add_supervisor', kwargs={'supervisor_name': self.secondgodfather.username}) + + def test_anonymous_user(self): + response = self.client.post(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['detail'] + self.assertEqual(errormsg, 'Authentication credentials were not provided.') + + def test_logged_in_accepted_user_supervision_request_to_invalid_godfather(self): + client = self.accepteduser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'accepteduser') + + previous_supervision_key = client.profile.supervision_key + previous_supervision_track = SupervisionTrack.objects.get(supervision_key = previous_supervision_key) + + self.assertEqual(previous_supervision_track.is_valid, True) + + self.url = reverse('api_accounts:add_supervisor', kwargs={'supervisor_name': self.invalidgodfather.username}) + #change supervisor + response = self.client.post(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['error'] + self.assertEqual(errormsg, 'Not a valid supervisor request') + + client = User.objects.get(username=client.username) + new_supervision_key = client.profile.supervision_key + new_supervision_track = SupervisionTrack.objects.get(supervision_key = new_supervision_key) + + #check track change and supervisor + previous_supervision_track = SupervisionTrack.objects.get(supervision_key = previous_supervision_key) + self.assertEqual(previous_supervision_track, new_supervision_track) + self.assertEqual(previous_supervision_track.is_valid, True) + self.assertEqual(previous_supervision_key, new_supervision_key) + self.assertEqual(client.profile.status, Profile.ACCEPTED) + logged_out = self.client.logout() + + def test_logged_in_accepted_user(self): + + client = self.accepteduser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'accepteduser') + + previous_supervision_key = client.profile.supervision_key + previous_supervision_track = SupervisionTrack.objects.get(supervision_key = previous_supervision_key) + + self.assertEqual(previous_supervision_track.is_valid, True) + + #change supervisor + response = self.client.post(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + client = User.objects.get(username=client.username) + new_supervision_key = client.profile.supervision_key + new_supervision_track = SupervisionTrack.objects.get(supervision_key = new_supervision_key) + + #check track change and supervisor + previous_supervision_track = SupervisionTrack.objects.get(supervision_key = previous_supervision_key) + self.assertEqual(previous_supervision_track.is_valid, False) + self.assertNotEqual(previous_supervision_key, new_supervision_key) + self.assertNotEqual(previous_supervision_track.godfather, new_supervision_track.godfather) + self.assertEqual(client.profile.status, Profile.REJECTED) + logged_out = self.client.logout() + + def test_logged_in_rejected_user(self): + + client = self.rejecteduser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'rejecteduser') + + previous_supervision_key = client.profile.supervision_key + self.assertEqual(previous_supervision_key, None) + + #change supervisor + response = self.client.post(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + client = User.objects.get(username=client.username) + new_supervision_key = client.profile.supervision_key + new_supervision_track = SupervisionTrack.objects.get(supervision_key = new_supervision_key) + + #check track change and supervisor + self.assertNotEqual(previous_supervision_key, new_supervision_key) + self.assertEqual(new_supervision_track.godfather.username, "secondgodfather") + self.assertEqual(client.profile.status, Profile.REJECTED) + logged_out = self.client.logout() + + def test_logged_in_rejected_user_twice(self): + + client = self.rejecteduser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'rejecteduser') + + previous_supervision_key = client.profile.supervision_key + self.assertEqual(previous_supervision_key, None) + + #change supervisor + response = self.client.post(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + client = User.objects.get(username=client.username) + new_supervision_key = client.profile.supervision_key + new_supervision_track = SupervisionTrack.objects.get(supervision_key = new_supervision_key) + + #check track change and supervisor + self.assertNotEqual(previous_supervision_key, new_supervision_key) + self.assertEqual(new_supervision_track.godfather.username, "secondgodfather") + self.assertEqual(client.profile.status, Profile.REJECTED) + #change supervisor second request + response = self.client.post(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['error'] + self.assertEqual(errormsg, "You are not able to perform this action as you already have a pending request") + + client = User.objects.get(username=client.username) + new_supervision_key = client.profile.supervision_key + new_supervision_track = SupervisionTrack.objects.get(supervision_key = new_supervision_key) + + #check track change and supervisor + self.assertNotEqual(previous_supervision_key, new_supervision_key) + self.assertEqual(new_supervision_track.godfather.username, "secondgodfather") + self.assertEqual(client.profile.status, Profile.REJECTED) + logged_out = self.client.logout() + + def test_logged_in_yearrevalidation_user(self): + + client = self.yearrevalidationuser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'yearrevalidationuser') + + previous_supervision_key = client.profile.supervision_key + previous_supervision_track = SupervisionTrack.objects.get(supervision_key = previous_supervision_key) + + self.assertEqual(previous_supervision_track.is_valid, True) + + #change supervisor + response = self.client.post(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + client = User.objects.get(username=client.username) + new_supervision_key = client.profile.supervision_key + new_supervision_track = SupervisionTrack.objects.get(supervision_key = new_supervision_key) + + #check track change and supervisor + previous_supervision_track = SupervisionTrack.objects.get(supervision_key = previous_supervision_key) + self.assertEqual(previous_supervision_track.is_valid, False) + self.assertNotEqual(previous_supervision_key, new_supervision_key) + self.assertNotEqual(previous_supervision_track.godfather, new_supervision_track.godfather) + self.assertEqual(client.profile.status, Profile.REJECTED) + + logged_out = self.client.logout() + + def test_logged_in_blocked_user(self): + + client = self.blockeduser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertFalse(logged_in) + self.assertEqual(client.username, 'blockeduser') + + response = self.client.post(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['detail'] + self.assertEqual(errormsg, 'Authentication credentials were not provided.') + + #This reactivation can be done through online request ONLY + + logged_out = self.client.logout() + + def test_logged_in_user_godfather(self): + + client = self.firstgodfather + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'firstgodfather') + + response = self.client.post(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['detail'] + self.assertEqual(errormsg, 'Not a supervisee account') + + logged_out = self.client.logout() + + def test_logged_in_invalid_godfather(self): + + client = self.invalidgodfather + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertFalse(logged_in) + self.assertEqual(client.username, 'invalidgodfather') + + response = self.client.post(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['detail'] + self.assertEqual(errormsg, 'Authentication credentials were not provided.') + + logged_out = self.client.logout() + + +#---------------------------------------------------------- + + +class AccountValidateSuperviseeTestCase(AccountTestCase): + + def setUp(self): + super(AccountValidateSuperviseeTestCase, self).setUp() + + self.url = reverse('api_accounts:validate_supervisee', kwargs={'supervisee_name': self.accepteduser.username}) + + def test_anonymous_user(self): + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['detail'] + self.assertEqual(errormsg, 'Authentication credentials were not provided.') + + def test_logged_in_accepted_user(self): + client = self.accepteduser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'accepteduser') + + previous_supervision_key = client.profile.supervision_key + previous_supervision_track = SupervisionTrack.objects.get(supervision_key = previous_supervision_key) + + self.assertEqual(previous_supervision_track.is_valid, True) + + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['detail'] + self.assertEqual(errormsg, 'Not a supervisor account') + + logged_out = self.client.logout() + + def test_logged_in_rejected_user(self): + client = self.rejecteduser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'rejecteduser') + + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['detail'] + self.assertEqual(errormsg, 'Not a supervisor account') + + logged_out = self.client.logout() + + def test_logged_in_yearrevalidation_user(self): + + client = self.yearrevalidationuser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'yearrevalidationuser') + + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['detail'] + self.assertEqual(errormsg, 'Not a supervisor account') + + logged_out = self.client.logout() + + def test_logged_in_blocked_user(self): + + client = self.blockeduser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertFalse(logged_in) + self.assertEqual(client.username, 'blockeduser') + + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['detail'] + self.assertEqual(errormsg, 'Authentication credentials were not provided.') + + logged_out = self.client.logout() + + def test_logged_in_user_godfather_validate_already_accepted_user(self): + + client = self.firstgodfather + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'firstgodfather') + + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['error'] + self.assertEqual(errormsg, "You can't validate this supervisee as his supervision is still valid") + + logged_out = self.client.logout() + + def test_logged_in_user_secondgodfather_validates_previously_accepted_user(self): + + self.url = reverse('api_accounts:add_supervisor', kwargs={'supervisor_name': self.secondgodfather.username}) + + client = self.accepteduser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'accepteduser') + + previous_supervision_key = client.profile.supervision_key + previous_supervision_track = SupervisionTrack.objects.get(supervision_key = previous_supervision_key) + + self.assertEqual(previous_supervision_track.is_valid, True) + + #change supervisor + response = self.client.post(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + client = User.objects.get(username=client.username) + new_supervision_key = client.profile.supervision_key + new_supervision_track = SupervisionTrack.objects.get(supervision_key = new_supervision_key) + + #check track change and supervisor + previous_supervision_track = SupervisionTrack.objects.get(supervision_key = previous_supervision_key) + self.assertEqual(previous_supervision_track.is_valid, False) + self.assertEqual(new_supervision_track.is_valid, False) + self.assertNotEqual(previous_supervision_key, new_supervision_key) + self.assertNotEqual(previous_supervision_track.godfather, new_supervision_track.godfather) + self.assertEqual(client.profile.status, Profile.REJECTED) + logged_out = self.client.logout() + + self.url = reverse('api_accounts:validate_supervisee', kwargs={'supervisee_name': self.accepteduser.username}) + client = self.secondgodfather + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'secondgodfather') + + response = self.client.put(self.url, format='json') + + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + logged_out = self.client.logout() + + client = User.objects.get(username=self.accepteduser.username) + new_supervision_key = client.profile.supervision_key + new_supervision_track = SupervisionTrack.objects.get(supervision_key = new_supervision_key) + + #check track change and supervisor + self.assertEqual(previous_supervision_track.is_valid, False) + self.assertEqual(new_supervision_track.is_valid, True) + self.assertNotEqual(previous_supervision_key, new_supervision_key) + self.assertNotEqual(previous_supervision_track.godfather, new_supervision_track.godfather) + self.assertEqual(client.profile.status, Profile.ACCEPTED) + + + def test_logged_in_user_secondgodfather_validates_rejected_user(self): + + self.url = reverse('api_accounts:add_supervisor', kwargs={'supervisor_name': self.secondgodfather.username}) + + client = self.rejecteduser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'rejecteduser') + + previous_supervision_key = client.profile.supervision_key + self.assertEqual(previous_supervision_key, None) + + #change supervisor + response = self.client.post(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + client = User.objects.get(username=client.username) + new_supervision_key = client.profile.supervision_key + new_supervision_track = SupervisionTrack.objects.get(supervision_key = new_supervision_key) + + #check track change and supervisor + self.assertNotEqual(previous_supervision_key, new_supervision_key) + self.assertEqual(new_supervision_track.godfather.username, "secondgodfather") + self.assertEqual(client.profile.status, Profile.REJECTED) + logged_out = self.client.logout() + + self.url = reverse('api_accounts:validate_supervisee', kwargs={'supervisee_name': self.rejecteduser.username}) + client = self.secondgodfather + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'secondgodfather') + + response = self.client.put(self.url, format='json') + + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + logged_out = self.client.logout() + + client = User.objects.get(username=self.rejecteduser.username) + new_supervision_key = client.profile.supervision_key + new_supervision_track = SupervisionTrack.objects.get(supervision_key = new_supervision_key) + + #check track change and supervisor + self.assertEqual(new_supervision_track.is_valid, True) + self.assertNotEqual(new_supervision_track.godfather, "secondgodfather") + self.assertEqual(client.profile.status, Profile.ACCEPTED) + + def test_logged_in_user_secondgodfather_validates_yearrevalidation_user(self): + + self.url = reverse('api_accounts:add_supervisor', kwargs={'supervisor_name': self.secondgodfather.username}) + + client = self.yearrevalidationuser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'yearrevalidationuser') + + previous_supervision_key = client.profile.supervision_key + previous_supervision_track = SupervisionTrack.objects.get(supervision_key = previous_supervision_key) + + self.assertEqual(previous_supervision_track.is_valid, True) + + #change supervisor + response = self.client.post(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + client = User.objects.get(username=client.username) + new_supervision_key = client.profile.supervision_key + new_supervision_track = SupervisionTrack.objects.get(supervision_key = new_supervision_key) + + #check track change and supervisor + previous_supervision_track = SupervisionTrack.objects.get(supervision_key = previous_supervision_key) + self.assertEqual(previous_supervision_track.is_valid, False) + self.assertNotEqual(previous_supervision_key, new_supervision_key) + self.assertNotEqual(previous_supervision_track.godfather, new_supervision_track.godfather) + self.assertEqual(client.profile.status, Profile.REJECTED) + + logged_out = self.client.logout() + + self.url = reverse('api_accounts:validate_supervisee', kwargs={'supervisee_name': self.yearrevalidationuser.username}) + client = self.secondgodfather + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'secondgodfather') + + response = self.client.put(self.url, format='json') + + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + logged_out = self.client.logout() + + client = User.objects.get(username=self.yearrevalidationuser.username) + new_supervision_key = client.profile.supervision_key + new_supervision_track = SupervisionTrack.objects.get(supervision_key = new_supervision_key) + + #check track change and supervisor + self.assertEqual(previous_supervision_track.is_valid, False) + self.assertEqual(new_supervision_track.is_valid, True) + self.assertNotEqual(previous_supervision_key, new_supervision_key) + self.assertNotEqual(previous_supervision_track.godfather, new_supervision_track.godfather) + self.assertEqual(client.profile.status, Profile.ACCEPTED) + + def test_logged_in_user_godfather_validation_try(self): + + client = self.firstgodfather + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'firstgodfather') + + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['error'] + self.assertEqual(errormsg, "You can't validate this supervisee as his supervision is still valid") + + logged_out = self.client.logout() + + def test_logged_in_invalid_godfather(self): + + client = self.invalidgodfather + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertFalse(logged_in) + self.assertEqual(client.username, 'invalidgodfather') + + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['detail'] + self.assertEqual(errormsg, 'Authentication credentials were not provided.') + + logged_out = self.client.logout() + + +#---------------------------------------------------------- + + +class AccountRemoveSuperviseeTestCase(AccountTestCase): + + def setUp(self): + super(AccountRemoveSuperviseeTestCase, self).setUp() + + self.url = reverse('api_accounts:remove_supervisee', kwargs={'supervisee_name': self.accepteduser.username}) + + def test_anonymous_user(self): + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['detail'] + self.assertEqual(errormsg, 'Authentication credentials were not provided.') + + def test_logged_in_accepted_user(self): + client = self.accepteduser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'accepteduser') + + previous_supervision_key = client.profile.supervision_key + previous_supervision_track = SupervisionTrack.objects.get(supervision_key = previous_supervision_key) + + self.assertEqual(previous_supervision_track.is_valid, True) + + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['detail'] + self.assertEqual(errormsg, 'Not a supervisor account') + + logged_out = self.client.logout() + + def test_logged_in_rejected_user(self): + client = self.rejecteduser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'rejecteduser') + + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['detail'] + self.assertEqual(errormsg, 'Not a supervisor account') + + logged_out = self.client.logout() + + def test_logged_in_yearrevalidation_user(self): + + client = self.yearrevalidationuser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'yearrevalidationuser') + + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['detail'] + self.assertEqual(errormsg, 'Not a supervisor account') + + logged_out = self.client.logout() + + def test_logged_in_blocked_user(self): + + client = self.blockeduser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertFalse(logged_in) + self.assertEqual(client.username, 'blockeduser') + + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['detail'] + self.assertEqual(errormsg, 'Authentication credentials were not provided.') + + logged_out = self.client.logout() + + def test_logged_in_godfather_remove_accepted_user(self): + #accepted user before + client = self.accepteduser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'accepteduser') + + previous_supervision_key = client.profile.supervision_key + previous_supervision_track = SupervisionTrack.objects.get(supervision_key = previous_supervision_key) + + self.assertEqual(previous_supervision_track.is_valid, True) + self.assertEqual(client.profile.status, Profile.ACCEPTED) + logged_out = self.client.logout() + + #accepted user removed + client = self.firstgodfather + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'firstgodfather') + + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + logged_out = self.client.logout() + + #accepted user after + client = User.objects.get(username=self.accepteduser.username) + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'accepteduser') + + new_supervision_key = client.profile.supervision_key + self.assertEqual(new_supervision_key, None) + previous_supervision_track = SupervisionTrack.objects.get(supervision_key = previous_supervision_key) + self.assertEqual(previous_supervision_track.is_valid, False) + self.assertEqual(client.profile.status, Profile.REJECTED) + logged_out = self.client.logout() + + def test_logged_in_godfather_remove_yearrevalidation_user(self): + self.url = reverse('api_accounts:remove_supervisee', kwargs={'supervisee_name': self.yearrevalidationuser.username}) + + #yearrevalidationuser user before + client = self.yearrevalidationuser + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'yearrevalidationuser') + + previous_supervision_key = client.profile.supervision_key + previous_supervision_track = SupervisionTrack.objects.get(supervision_key = previous_supervision_key) + + self.assertEqual(previous_supervision_track.is_valid, True) + self.assertEqual(client.profile.status, Profile.YEARREVALIDATION) + logged_out = self.client.logout() + + #yearrevalidationuser user removed + client = self.firstgodfather + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'firstgodfather') + + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + logged_out = self.client.logout() + + #yearrevalidationuser user after + client = User.objects.get(username=self.yearrevalidationuser.username) + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertTrue(logged_in) + self.assertEqual(client.username, 'yearrevalidationuser') + + new_supervision_key = client.profile.supervision_key + self.assertEqual(new_supervision_key, None) + previous_supervision_track = SupervisionTrack.objects.get(supervision_key = previous_supervision_key) + self.assertEqual(previous_supervision_track.is_valid, False) + self.assertEqual(client.profile.status, Profile.REJECTED) + logged_out = self.client.logout() + + def test_logged_in_invalid_godfather(self): + + client = self.invalidgodfather + + logged_in = self.client.login(username=client.username, password=self.password) + self.assertFalse(logged_in) + self.assertEqual(client.username, 'invalidgodfather') + + response = self.client.put(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + content = json.loads(response.content) + self.assertEqual(len(json.loads(response.content)), 1) + errormsg = content['detail'] + self.assertEqual(errormsg, 'Authentication credentials were not provided.') + + logged_out = self.client.logout() diff --git a/beat/web/accounts/views.py b/beat/web/accounts/views.py index 4e4917de05c589a049e80206cf27a07590b7535d..823516c60c547728c2c9f5d710e9cfeb55ff0539 100644 --- a/beat/web/accounts/views.py +++ b/beat/web/accounts/views.py @@ -29,11 +29,14 @@ from django.shortcuts import render from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import PasswordChangeForm from django.contrib import messages +from django.db import models +from django.db.models import Q from rest_framework.authtoken.models import Token from .forms import AccountSettingsForm from .models import AccountSettings +from .models import SupervisionTrack, Profile @login_required def account_settings(request): @@ -65,8 +68,28 @@ def account_settings(request): account_settings_form = AccountSettingsForm(instance=account_settings) password_change_form = PasswordChangeForm(user=user) + supervisiontrack = None + supervisiontracks_valid = None + supervisiontracks_pending = None + if user.profile.is_godfather == False: + supervisee = user + if supervisee.profile.supervision_key is not None: + #There's a key check if there's a valid track + supervisiontrack = SupervisionTrack.objects.get(supervisee=supervisee, supervisee__profile__supervision_key=models.F('supervision_key')) + else: + supervisiontrack = None + else: + godfather = user + #Filter and get all supervision tracks valid and + supervisiontracks_valid = SupervisionTrack.objects.filter(godfather=godfather, is_valid=True) + supervisiontracks_pending = SupervisionTrack.objects.filter(godfather=godfather, is_valid=False, start_date=None) + return render(request, 'accounts/settings.html', {'account_settings_form': account_settings_form, 'password_change_form': password_change_form, + 'user': user, + 'supervisiontrack': supervisiontrack, + 'supervisiontracks_valid': supervisiontracks_valid, + 'supervisiontracks_pending': supervisiontracks_pending, 'token' : user.auth_token}) diff --git a/beat/web/navigation/admin.py b/beat/web/navigation/admin.py index 924827c59d2a52f2ec274662e9a9ca94eb5e3e9b..85d82d7ca1322e2ac4b2d0e3ae4f1daec1c96d8a 100644 --- a/beat/web/navigation/admin.py +++ b/beat/web/navigation/admin.py @@ -31,6 +31,8 @@ from django.contrib.auth.models import User from .models import Agreement from ..accounts.models import AccountSettings +from ..accounts.models import SupervisionTrack +from ..accounts.models import Profile #---------------------------------------------------------- @@ -50,6 +52,21 @@ class AccountSettingsInline(admin.StackedInline): #---------------------------------------------------------- +class SupervisionTrackInline(admin.StackedInline): + model = SupervisionTrack + fk_name = 'godfather' + + +#---------------------------------------------------------- + + +class ProfileInline(admin.StackedInline): + model = Profile + + +#---------------------------------------------------------- + + class UserAdmin(UserAdmin): def agreement_number(self, obj): @@ -62,6 +79,30 @@ class UserAdmin(UserAdmin): int(obj.accountsettings.database_notifications_enabled) + \ int(obj.accountsettings.environment_notifications_enabled) + def godfather(self, obj): + return obj.profile.is_godfather + + def status(self, obj): + return obj.profile.status + + def supervision(self, obj): + if obj.is_staff: + return "Staff account" + if obj.profile.is_godfather: + return "Supervisor account" + else: + supervisiontrack = SupervisionTrack.objects.get(supervisee=obj, + is_valid=True) + godfather = supervisiontrack.godfather + return godfather + + def supervision_key(self, obj): + return obj.profile.supervision_key + + def rejection_date(self, obj): + return obj.profile.rejection_date + + list_display = ( 'username', 'is_staff', @@ -69,6 +110,11 @@ class UserAdmin(UserAdmin): 'notifications', 'agreement_number', 'last_login', + 'godfather', + 'status', + 'supervision', + 'supervision_key', + 'rejection_date', ) ordering = ( @@ -80,7 +126,54 @@ class UserAdmin(UserAdmin): inlines = ( AgreementInline, AccountSettingsInline, + SupervisionTrackInline, + ProfileInline, ) admin.site.unregister(User) admin.site.register(User, UserAdmin) + + +#---------------------------------------------------------- + + +class SupervisionTrackAdmin(admin.ModelAdmin): + + + list_display = ( + 'godfather', + 'supervisee', + 'is_valid', + 'start_date', + 'expiration_date', + 'last_validation_date', + 'supervision_key', + ) + + ordering = ( + 'godfather', + ) + +admin.site.register(SupervisionTrack, SupervisionTrackAdmin) + + +#---------------------------------------------------------- + + +class ProfileAdmin(admin.ModelAdmin): + + + list_display = ( + 'user', + 'status', + 'registration_date', + 'is_godfather', + 'supervision_key', + 'rejection_date', + ) + + ordering = ( + 'user', + ) + +admin.site.register(Profile, ProfileAdmin) diff --git a/beat/web/settings/settings.py b/beat/web/settings/settings.py index 17a7b1d25b1d807ac7ea186406fd9b2e03126c96..61ec9923e3be878a19d47649852d11bc6190fe0d 100755 --- a/beat/web/settings/settings.py +++ b/beat/web/settings/settings.py @@ -216,6 +216,10 @@ DATASETS_ROOT_PATH = None ############################################################################## ACCOUNT_ACTIVATION_DAYS = 2 +ACCOUNT_ACTIVATION_DAYS_FROM_GODFATHER = 7 +ACCOUNT_EXPIRATION_DAYS = 365 +ACCOUNT_BLOCKAGE_AFTER_FIRST_REJECTION_DAYS = 56 + LOGIN_REDIRECT_URL = '/' LOGIN_URL = '/login/' SYSTEM_ACCOUNT = 'system' @@ -429,6 +433,8 @@ AUTHENTICATION_BACKENDS = ( ANONYMOUS_USER_ID = -1 +AUTH_PROFILE_MODULE = 'accounts.Profile' + ############################################################################## # # REST FRAMEWORK PERMISSION CLASSES: http://www.django-rest-framework.org/api-guide/permissions/ @@ -465,6 +471,7 @@ REST_FRAMEWORK = { #In days EXPIRATION_DELTA = 180 EXPIRATION_REMINDERS = [1, 7, 30] +EXPIRATION_REMINDERS_REVALIDATION = [1, 7, 30, 50] ############################################################################## # diff --git a/beat/web/statistics/templates/statistics/statistics.html b/beat/web/statistics/templates/statistics/statistics.html index ebd752a7164dd0d46532e4fa5b417b1130f1d95f..14431d83d3e7ceceb5c84f0164fbfeeda278fc19 100644 --- a/beat/web/statistics/templates/statistics/statistics.html +++ b/beat/web/statistics/templates/statistics/statistics.html @@ -46,7 +46,7 @@ <!-- summary statistics --> <div class="row"> - <div class="col-sm-3"> + <div class="col-sm-offset-1 col-sm-2"> <h3>Totals <span class="badge">{{ totals.counter }} entries</span></h3> <ul class="list-group"> <li class="list-group-item">CPU Time <span class="badge">{{ totals.cpu_time_hours }} h</span></li> @@ -58,7 +58,7 @@ </ul> <p class="help">Instantaneous (not averaged over wallclock time)</p> </div> - <div class="col-sm-3"> + <div class="col-sm-2"> <h3>User Objects</h3> <ul class="list-group"> <li class="list-group-item">Experiments <span class="badge">{{ totals.experiments }}</span></li> @@ -70,7 +70,7 @@ </ul> <p class="help">Items contributed by users</p> </div> - <div class="col-sm-3"> + <div class="col-sm-2"> <h3>User Queries</h3> <ul class="list-group"> <li class="list-group-item">Attestations <span class="badge">{{ totals.attestations }}</span></li> @@ -79,7 +79,7 @@ </ul> <p class="help">User analysis and certification</p> </div> - <div class="col-sm-3"> + <div class="col-sm-2"> <h3>System Properties</h3> <ul class="list-group"> <li class="list-group-item">Users <span class="badge">{{ totals.users }}</span></li> @@ -90,9 +90,21 @@ </ul> <p class="help">Items available and managed by administrators</p> </div> + <div class="col-sm-2"> + <h3>User Profiles</h3> + <ul class="list-group"> + <li class="list-group-item">Total <span class="badge">{{ totals.users }}</span></li> + <li class="list-group-item">Awaiting Activation <span class="badge">{{ totals.newusers }}</span></li> + <li class="list-group-item">Awaiting Supervisor Validation <span class="badge">{{ totals.waitingvalidationusers}}</span></li> + <li class="list-group-item">Active <span class="badge">{{ totals.acceptedusers }}</span></li> + <li class="list-group-item">Year Revalidation <span class="badge">{{ totals.yearrevalidationusers }}</span></li> + <li class="list-group-item">Without Supervisor <span class="badge">{{ totals.rejectedusers }}</span></li> + <li class="list-group-item">Inactive <span class="badge">{{ totals.blockedusers }}</span></li> + </ul> + <p class="help">Profile status of users on the platform</p> + </div> </div> - <!-- tabs --> <div class="row"> <div class="col-sm-12"> diff --git a/beat/web/statistics/views.py b/beat/web/statistics/views.py index d13a7e073a0087ec5d5c852117f528288bc2dbcb..2b1664ffc68c8a9ee7d3f9ed14b344f25953a28b 100644 --- a/beat/web/statistics/views.py +++ b/beat/web/statistics/views.py @@ -56,6 +56,7 @@ def calculate_totals(): from ..plotters.models import Plotter from ..plotters.models import PlotterParameter from ..search.models import Search + from ..accounts.models import Profile # for calculating the total cpu time, we use the HourlyStatistics and # accumulate over the whole history @@ -78,6 +79,13 @@ def calculate_totals(): output_size = details['data_written_size'] output_time = details['data_written_time'] + new_users = User.objects.filter(profile__status = Profile.NEWUSER) + waiting_validation_users = User.objects.filter(profile__status = Profile.WAITINGVALIDATION) + accepted_users = User.objects.filter(profile__status = Profile.ACCEPTED) + rejected_users = User.objects.filter(profile__status = Profile.REJECTED) + yearrevalidation_users = User.objects.filter(profile__status = Profile.YEARREVALIDATION) + blocked_users = User.objects.filter(profile__status = Profile.BLOCKED) + return dict( counter=counter, @@ -89,6 +97,12 @@ def calculate_totals(): output_bw_mb_s = int((output_size/float(2**20))/output_time) if output_size else 0, users=User.objects.count(), + newusers=new_users.count(), + waitingvalidationusers=waiting_validation_users.count(), + acceptedusers=accepted_users.count(), + rejectedusers=rejected_users.count(), + yearrevalidationusers=yearrevalidation_users.count(), + blockedusers=blocked_users.count(), databases=Database.objects.count(), environments=Environment.objects.count(), queues=Queue.objects.count(), diff --git a/beat/web/ui/registration/docs/forms.txt b/beat/web/ui/registration/docs/forms.txt index e779d433ebcb76a3e27a219ec90eaae61e252d59..5435413ae83c17ae9a65c4657708fb5c267db8f6 100644 --- a/beat/web/ui/registration/docs/forms.txt +++ b/beat/web/ui/registration/docs/forms.txt @@ -23,7 +23,7 @@ requires the password to be entered twice to catch typos. Subclasses should feel free to add any additional validation they need, but should either preserve the base ``save()`` or implement a ``save()`` method which returns a ``User``. - + Fields: ``username`` @@ -40,6 +40,10 @@ Fields: ``password2`` The password, again, to catch typos. +``godfather`` + The godfather that will validate this account. + + Non-validation methods: @@ -71,6 +75,6 @@ implementations of useful customizations: Subclass of ``RegistrationForm`` which disallows registration with email addresses from popular free webmail services; moderately useful for preventing automated spam registrations. - + To change the list of banned domains, subclass this form and override the attribute ``bad_domains``. diff --git a/beat/web/ui/registration/forms.py b/beat/web/ui/registration/forms.py index b41f5b997be29376f31a78fc9ed0339c6966eaac..355dcc2d01d9d1013285ef66ff435348380186b8 100644 --- a/beat/web/ui/registration/forms.py +++ b/beat/web/ui/registration/forms.py @@ -30,6 +30,7 @@ Forms and validation code for user registration. """ +import datetime from django.contrib.auth.models import User from django import forms @@ -39,6 +40,8 @@ from django.utils.translation import ugettext_lazy as _ from .models import RegistrationProfile from .models import PreregistrationProfile +from ...accounts.models import SupervisionTrack +from ...accounts.models import Profile # I put this on all required fields, because it's easier to pick up # on them with CSS or JavaScript if they have a class of "required" @@ -79,6 +82,11 @@ class RegistrationForm(forms.Form): label=_(u'Password')) password2 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict, render_value=False), label=_(u'Password (again)')) + godfather = forms.RegexField(regex=r'^\w+$', + max_length=30, + widget=forms.TextInput(attrs=attrs_dict), + label=_(u'Supervisor Username')) + def clean_username(self): """ @@ -92,6 +100,24 @@ class RegistrationForm(forms.Form): return self.cleaned_data['username'] raise forms.ValidationError(_(u'This username is already taken. Please choose another.')) + def clean_godfather(self): + """ + Validate that the username is alphanumeric and exists. + + """ + try: + user = User.objects.get(username__iexact=self.cleaned_data['godfather']) + if user.profile.status == Profile.BLOCKED: + raise forms.ValidationError(_(u'This user is not a valid supervisor. Please choose another.')) + except User.DoesNotExist: + raise forms.ValidationError(_(u'This supervisor username does not exist. Please choose another.')) + + if not user.profile.is_godfather: + raise forms.ValidationError(_(u'This user is not a recognized supervisor. Please choose another.')) + + return self.cleaned_data['godfather'] + + def clean(self): """ Verifiy that the values entered into the two password fields @@ -123,6 +149,26 @@ class RegistrationForm(forms.Form): password=self.cleaned_data['password1'], email=self.cleaned_data['email'], ) + + #Create and assign key + new_user.profile.supervision_key = new_user.profile._generate_current_supervision_key() + godfather = User.objects.get(username = self.cleaned_data['godfather']) + supervisiontrack = SupervisionTrack.objects.create( + supervisee = new_user, + godfather = godfather, + is_valid = False, + ) + + #Assign key to supervision track + supervisiontrack.supervision_key = new_user.profile.supervision_key + supervisiontrack.save() + new_user.profile.is_godfather = False + new_user.profile.status = Profile.NEWUSER + new_user.profile.registration_date = datetime.datetime.now() + new_user.profile.supervision.add(supervisiontrack) + new_user.save() + + return new_user @@ -206,3 +252,72 @@ class RegistrationFormNoFreeEmail(RegistrationForm): if email_domain in self.bad_domains: raise forms.ValidationError(_(u'Registration using free email addresses is prohibited. Please supply a different email address.')) return self.cleaned_data['email'] + + +class BlockedUserRevalidationForm(forms.Form): + """ + Form for registering a new user account. + + Validates that the requested username is not already in use, and + requires the password to be entered twice to catch typos. + + Subclasses should feel free to add any additional validation they + need, but should either preserve the base ``save()`` or implement + a ``save()`` method which returns a ``User``. + + """ + + username = forms.RegexField(regex=r'^\w+$', + max_length=30, + widget=forms.TextInput(attrs=attrs_dict), + label=_(u'Username')) + password = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict, render_value=False), + label=_(u'Password')) + godfather = forms.RegexField(regex=r'^\w+$', + max_length=30, + widget=forms.TextInput(attrs=attrs_dict), + label=_(u'Supervisor Username (Your account needs to be re-validated by a recognized person)')) + + + def clean_username(self): + """ + Validate that the username is alphanumeric and user exists + + """ + try: + user = User.objects.get(username__iexact=self.cleaned_data['username']) + return self.cleaned_data['username'] + except User.DoesNotExist: + raise forms.ValidationError(_(u'This username has not been recognized')) + + def clean_godfather(self): + """ + Validate that the username is alphanumeric and exists. + + """ + try: + user = User.objects.get(username__iexact=self.cleaned_data['godfather']) + except User.DoesNotExist: + raise forms.ValidationError(_(u'This supervisor username does not exist. Please choose another.')) + + if not user.profile.is_godfather: + raise forms.ValidationError(_(u'This user is not a recognized supervisor. Please choose another.')) + + return self.cleaned_data['godfather'] + + + def clean(self): + """ + Verifiy that the values entered into the two password fields + match. Note that an error here will end up in + ``non_field_errors()`` because it doesn't apply to a single + field. + + """ + if 'password1' in self.cleaned_data and 'password2' in self.cleaned_data: + if self.cleaned_data['password1'] != self.cleaned_data['password2']: + self._errors["password2"] = self.error_class([_(u'You must type the same password each time')]) + del self.cleaned_data["password1"] + del self.cleaned_data["password2"] + + return self.cleaned_data diff --git a/beat/web/ui/registration/models.py b/beat/web/ui/registration/models.py index 546669521a234c86ac4f1ec1672fd0a0aa2dbe24..405c31daf6734d0b7fea08c3103962ff1113d666 100644 --- a/beat/web/ui/registration/models.py +++ b/beat/web/ui/registration/models.py @@ -39,6 +39,9 @@ from django.template import loader from django.template import Context from django.utils.translation import ugettext_lazy as _ +from ...accounts.models import SupervisionTrack +from ...accounts.models import Profile + SHA1_RE = re.compile('^[a-f0-9]{40}$') @@ -93,6 +96,43 @@ class RegistrationManager(models.Manager): profile.activation_key = self.model.ACTIVATED profile.save() user_activated.send(sender=self.model, user=user) + # The user activated via the link but activation from godfather or admin + # is requested now to have access to the platform (so set it + # inactive again) + user.is_active = False + user.profile.status = Profile.WAITINGVALIDATION + user.profile.registration_date = datetime.datetime.now() + user.save() + + # Send email to godfather + # Fetch Godfather user from supervision track + supervisiontrack = SupervisionTrack.objects.get(supervision_key=user.profile.supervision_key) + godfather_user = supervisiontrack.godfather + + from django.core.mail import send_mail + + parsed_url = urlparse(settings.URL_PREFIX) + server_address = '%s://%s' % (parsed_url.scheme, parsed_url.hostname) + + c = Context({ 'supervisor': godfather_user, + 'supervisee': user, + 'prefix': server_address, + }) + + try: + t = loader.get_template('registration/mail.godfather_validation.subject.txt') + subject = t.render(c) + + # Note: e-mail subject *must not* contain newlines + subject = settings.EMAIL_SUBJECT_PREFIX + ''.join(subject.splitlines()) + + t = loader.get_template('registration/mail.godfather_validation.message.txt') + message = t.render(c) + + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [godfather_user.email]) + except: + pass + return user return False diff --git a/beat/web/ui/registration/templates/registration/activate.html b/beat/web/ui/registration/templates/registration/activate.html index 54c1d444cab38651557336e686e0b3de16bc6b72..d434cebb12d5ed98d0ecb6d1c676370953873a4a 100644 --- a/beat/web/ui/registration/templates/registration/activate.html +++ b/beat/web/ui/registration/templates/registration/activate.html @@ -2,21 +2,21 @@ {% comment %} * Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ * Contact: beat.support@idiap.ch - * + * * This file is part of the beat.web module of the BEAT platform. - * + * * Commercial License Usage * Licensees holding valid commercial BEAT licenses may use this file in * accordance with the terms contained in a written agreement between you * and Idiap. For further information contact tto@idiap.ch - * + * * Alternatively, this file may be used under the terms of the GNU Affero * Public License version 3 as published by the Free Software and appearing * in the file LICENSE.AGPL included in the packaging of this file. * The BEAT platform 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. - * + * * You should have received a copy of the GNU Affero Public License along * with the BEAT platform. If not, see http://www.gnu.org/licenses/. {% endcomment %} @@ -26,13 +26,19 @@ <div class="row"> <div class="col-sm-4 col-sm-offset-4"> - <h4>{% if account %}Account activated{% else %}Error{% endif %}</h4> + <h4>{% if account %}Account almost activated{% else %}Error{% endif %}</h4> {% if account %} - <p class="text">Your account was successfully activated. Welcome {{ account.username }}. - You can now <a href="{% url 'login' %}">sign in</a> and start your own experiments!</p> + <p class="text">Your account is almost activated. Welcome {{ account.username }}. + We have asked your "supervisor" to validate your account and accept you as a + supervisee. You will be informed by email once this is done. + In the meantime you can remind your "supervisor" to validate + your account. You will then be able to <a href="{% url 'login' %}">sign in</a> and start your own experiments!</p> {% else %} - <p class="text">Your account wasn't activated. Check that the URL you entered is correct. If you still have problems to activate your account, <a href="{% url 'contact' %}">contact a member of our support staff</a>.</p> + <p class="text">Your account wasn't activated. Check that the URL you entered is correct. + If you already clicked on the link sent to you by email, you need to wait + for your "supervisor" to accept your account activation request. + If you still have problems to activate your account, <a href="{% url 'contact' %}">contact a member of our support staff</a>.</p> {% endif %} </div> diff --git a/beat/web/ui/registration/templates/registration/blocked_user_reactivate.html b/beat/web/ui/registration/templates/registration/blocked_user_reactivate.html new file mode 100644 index 0000000000000000000000000000000000000000..91e7f88c44ac15816a883b668cc3aeacbc5be2b9 --- /dev/null +++ b/beat/web/ui/registration/templates/registration/blocked_user_reactivate.html @@ -0,0 +1,82 @@ +{% extends "base.html" %} +{% comment %} + * Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ + * Contact: beat.support@idiap.ch + * + * This file is part of the beat.web module of the BEAT platform. + * + * Commercial License Usage + * Licensees holding valid commercial BEAT licenses may use this file in + * accordance with the terms contained in a written agreement between you + * and Idiap. For further information contact tto@idiap.ch + * + * Alternatively, this file may be used under the terms of the GNU Affero + * Public License version 3 as published by the Free Software and appearing + * in the file LICENSE.AGPL included in the packaging of this file. + * The BEAT platform 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. + * + * You should have received a copy of the GNU Affero Public License along + * with the BEAT platform. If not, see http://www.gnu.org/licenses/. +{% endcomment %} + +{% load registration_tags %} + +{% block content %} + +<div class="row"> + <div class="col-sm-4 col-sm-offset-4"> + + <div class="panel panel-default login-panel"> + + <div class="panel-heading">Account Reactivation</div> + + <div class="panel-body"> + + <form method="post" action="" class="form"> + {% csrf_token %} + + {% for error in form.non_field_errors %} + <div class="alert alert-danger" role="alert">{{ error }}</div> + {% endfor %} + + <div class="form-group{% if form.username.errors %} has-error{% endif %}"> + {{ form.username.label_tag }} + {{ form.username|tabindex:1|addclass:"form-control" }} + {% for error in form.username.errors %} + <div class="alert alert-danger" role="alert">{{ error }}</div> + {% endfor %} + </div> + + <div class="form-group{% if form.password.errors %} has-error{% endif %}"> + {{ form.password.label_tag }} + {{ form.password|tabindex:2|addclass:"form-control" }} + {% for error in form.password.errors %} + <div class="alert alert-danger" role="alert">{{ error }}</div> + {% endfor %} + </div> + + <div class="form-group{% if form.godfather.errors %} has-error{% endif %}"> + {{ form.godfather.label_tag }} + {{ form.godfather|tabindex:2|addclass:"form-control" }} + {% for error in form.godfather.errors %} + <div class="alert alert-danger" role="alert">{{ error }}</div> + {% endfor %} + </div> + + <div class="form-group"> + <button type="submit" class="btn btn-success" tabindex="3"><i class="fa fa-plug fa-fw fa-lg"></i> Reactivation request</button> + </div> + + </form> + + <p class="comment">In order to sign in, you must be <a href="{% url 'registration' %}">registered</a></p> + <p class="comment">Already registered and not blocked? <a href="{% url 'login' %}">login</a></p> + </div> + </div> + + </div> +</div> + +{% endblock content %} diff --git a/beat/web/ui/registration/templates/registration/login.html b/beat/web/ui/registration/templates/registration/login.html index bf265cdad5926f95a4255a42fdc86f520dfcbb16..a08be74b799e4ab9f7eae5ee969268623f9dd189 100644 --- a/beat/web/ui/registration/templates/registration/login.html +++ b/beat/web/ui/registration/templates/registration/login.html @@ -64,7 +64,7 @@ </form> <p class="comment">In order to sign in, you must be <a href="{% url 'registration' %}">registered</a></p> - + <p class="comment">Inactive/Blocked user need to get their account reactivated <a href="{% url 'blocked_user_reactivation' %}">reactivation</a></p> </div> </div> diff --git a/beat/web/ui/registration/templates/registration/mail.account_revalidation.message.txt b/beat/web/ui/registration/templates/registration/mail.account_revalidation.message.txt new file mode 100644 index 0000000000000000000000000000000000000000..2603383696072abc0f910d2b44df157aecba8ec8 --- /dev/null +++ b/beat/web/ui/registration/templates/registration/mail.account_revalidation.message.txt @@ -0,0 +1,13 @@ +Dear {{ user.first_name }}, + +Your need to re-confirm that you still use your account on the BEAT Platform. +This check is done on a yearly basis. + +This can be done by going to your profile and making a revalidation request by +clicking on the corresponding button. + +If you haven't done this by this expiration date: {{ expiration_date }}, your account +will be blocked and you will then need to go trough an account revalidation +procedure to re-activate it. + +BEAT Administrators at the Idiap Research Institute diff --git a/beat/web/ui/registration/templates/registration/mail.account_revalidation.subject.txt b/beat/web/ui/registration/templates/registration/mail.account_revalidation.subject.txt new file mode 100644 index 0000000000000000000000000000000000000000..1de7fbb2d93421d8508fa45aeb420ab044bb6c40 --- /dev/null +++ b/beat/web/ui/registration/templates/registration/mail.account_revalidation.subject.txt @@ -0,0 +1 @@ +Account re-validation - Required Confirmation diff --git a/beat/web/ui/registration/templates/registration/mail.godfather_rejected.message.txt b/beat/web/ui/registration/templates/registration/mail.godfather_rejected.message.txt new file mode 100644 index 0000000000000000000000000000000000000000..f52e65923653bc83d7a12b6ecbd10575da8f32dd --- /dev/null +++ b/beat/web/ui/registration/templates/registration/mail.godfather_rejected.message.txt @@ -0,0 +1,17 @@ +Dear {{ supervisee.first_name }}, + +Your supervision request was not accepted and has been rejected by the supervisor {{ supervisor.username }}. + +Either you made a mistake or you are not a supervisee anymore for {{ supervisor.username }} + +Your account {{ supervisee.username }} is currently not fully validated. + +You can request to validate your account through a new supervision request +either through an existing validated supervisor from the platform or by asking your current supervisor +to register on the platform as a supervisor and when his account is validated, +you can request a supervision from him and get a re-activation of your +account. + +Your account will be blocked on {{ supervisee.profile.rejection_date }}. You have until this date to find a +supervisor. Passed this date your account will be blocked and you will need to +proceed with an account reactivation request. diff --git a/beat/web/ui/registration/templates/registration/mail.godfather_rejected.subject.txt b/beat/web/ui/registration/templates/registration/mail.godfather_rejected.subject.txt new file mode 100644 index 0000000000000000000000000000000000000000..09ffd3d8ce91f60e543a60ebbe2736f7a706119d --- /dev/null +++ b/beat/web/ui/registration/templates/registration/mail.godfather_rejected.subject.txt @@ -0,0 +1 @@ +Account not confirmed - Supervision Rejected diff --git a/beat/web/ui/registration/templates/registration/mail.godfather_rejected_delete_account.message.txt b/beat/web/ui/registration/templates/registration/mail.godfather_rejected_delete_account.message.txt new file mode 100644 index 0000000000000000000000000000000000000000..ca0d45a1ab8cc8b9aa93489458b690540d650945 --- /dev/null +++ b/beat/web/ui/registration/templates/registration/mail.godfather_rejected_delete_account.message.txt @@ -0,0 +1,9 @@ +Dear {{ supervisee.first_name }}, + +Your supervision request was not accepted and has been rejected by the supervisor {{ supervisor.username }}. + +Either you made a mistake or you are not a supervisee of {{ supervisor.username }} + +Your newly created account {{ supervisee.username }} has therefore currently been deleted. + +You can re-create a new account and make a proper supervision request. diff --git a/beat/web/ui/registration/templates/registration/mail.godfather_rejection.message.txt b/beat/web/ui/registration/templates/registration/mail.godfather_rejection.message.txt new file mode 100644 index 0000000000000000000000000000000000000000..f3be84a068a3ff804ff3a92a4f909f7dd663ca8c --- /dev/null +++ b/beat/web/ui/registration/templates/registration/mail.godfather_rejection.message.txt @@ -0,0 +1,12 @@ +Dear {{ supervisor.first_name }}, + +One of your supervisees has made a request to end your current supervision and request for a new one towards another supervisor related to his current situation. + +First Name: {{ supervisee.first_name }} +Last Name: {{ supervisee.last_name }} +Email: {{ supervisee.email }} +Username: {{ supervisee.username }} + +This message is to inform you that you are no longer responsible towards this supervisee in the current future. + +BEAT Administrators at the Idiap Research Institute diff --git a/beat/web/ui/registration/templates/registration/mail.godfather_rejection.subject.txt b/beat/web/ui/registration/templates/registration/mail.godfather_rejection.subject.txt new file mode 100644 index 0000000000000000000000000000000000000000..bab40169e7e2493347274d54f5b7fa894e048d14 --- /dev/null +++ b/beat/web/ui/registration/templates/registration/mail.godfather_rejection.subject.txt @@ -0,0 +1 @@ +Account validation - Revoked Supervision diff --git a/beat/web/ui/registration/templates/registration/mail.godfather_validated.message.txt b/beat/web/ui/registration/templates/registration/mail.godfather_validated.message.txt new file mode 100644 index 0000000000000000000000000000000000000000..aac566f8f1fa0286795b1cf0c25c857ea347fe15 --- /dev/null +++ b/beat/web/ui/registration/templates/registration/mail.godfather_validated.message.txt @@ -0,0 +1,6 @@ +Dear {{ supervisee.first_name }}, + +Your account was successfully activated by your supervisor {{ supervisor.username }}. + +You can now <a href="{% url 'login' %}">sign in</a> and start your own +experiments!</p> diff --git a/beat/web/ui/registration/templates/registration/mail.godfather_validated.subject.txt b/beat/web/ui/registration/templates/registration/mail.godfather_validated.subject.txt new file mode 100644 index 0000000000000000000000000000000000000000..af97067a67b3cb9a6a2731e828ffe90391c3558c --- /dev/null +++ b/beat/web/ui/registration/templates/registration/mail.godfather_validated.subject.txt @@ -0,0 +1 @@ +Account successfully activated - Supervision Confirmed diff --git a/beat/web/ui/registration/templates/registration/mail.godfather_validation.message.txt b/beat/web/ui/registration/templates/registration/mail.godfather_validation.message.txt new file mode 100644 index 0000000000000000000000000000000000000000..47fac57756aa39b614136bf5bb10c32c25e61ee7 --- /dev/null +++ b/beat/web/ui/registration/templates/registration/mail.godfather_validation.message.txt @@ -0,0 +1,18 @@ +Dear {{ supervisor.first_name }}, + +Thank you in advance for validating the registration of one your supervisees at the Idiap Research Institute's Biometric +Evaluation and Testing (BEAT) platform. Before we can activate his account +you must login to your account and under supervision tab validate the following supervisee: + +First Name: {{ supervisee.first_name }} +Last Name: {{ supervisee.last_name }} +Email: {{ supervisee.email }} +Username: {{ supervisee.username }} + +If you don't do this the supervisee will not be able to use his account and +will be deleted after 7 days. + +If you are having problems to activate your supervisee account, contact a member of our +support staff at {{ prefix }}{% url 'contact' %}. + +BEAT Administrators at the Idiap Research Institute diff --git a/beat/web/ui/registration/templates/registration/mail.godfather_validation.subject.txt b/beat/web/ui/registration/templates/registration/mail.godfather_validation.subject.txt new file mode 100644 index 0000000000000000000000000000000000000000..00bae0b5ed20b2984ffb59c3b98098c9aefc7faa --- /dev/null +++ b/beat/web/ui/registration/templates/registration/mail.godfather_validation.subject.txt @@ -0,0 +1 @@ +Account validation - Supervision Confirmation required diff --git a/beat/web/ui/registration/templates/registration/mail.godfather_validation_supervisee_add_request.message.txt b/beat/web/ui/registration/templates/registration/mail.godfather_validation_supervisee_add_request.message.txt new file mode 100644 index 0000000000000000000000000000000000000000..813b7ebe96c75181b8d1682e717ea754f7783656 --- /dev/null +++ b/beat/web/ui/registration/templates/registration/mail.godfather_validation_supervisee_add_request.message.txt @@ -0,0 +1,18 @@ +Dear {{ supervisor.first_name }}, + +Thank you in advance for validating the request of one your supervisees at the Idiap Research Institute's Biometric +Evaluation and Testing (BEAT) platform. Before we can activate this +supervision request, you must login to your account and under supervision tab validate the following supervisee: + +First Name: {{ supervisee.first_name }} +Last Name: {{ supervisee.last_name }} +Email: {{ supervisee.email }} +Username: {{ supervisee.username }} + +If you don't do this the supervisee will not be able to use his account in +future and will not be recognized as your supervisee. + +If you are having problems to activate your supervisee account, contact a member of our +support staff at {{ prefix }}{% url 'contact' %}. + +BEAT Administrators at the Idiap Research Institute diff --git a/beat/web/ui/registration/templates/registration/mail.migration_10_accounts.message.txt b/beat/web/ui/registration/templates/registration/mail.migration_10_accounts.message.txt new file mode 100644 index 0000000000000000000000000000000000000000..cb0958238029a6e40799d9ad3a4b72459c619d5e --- /dev/null +++ b/beat/web/ui/registration/templates/registration/mail.migration_10_accounts.message.txt @@ -0,0 +1,18 @@ +Dear {{ user.first_name }}, + +The admin of the BEAT platform would like to inform you of a security update +to your account used under username {{ user.username}} + +From now on you will need to be supervised by a known supervisor. + +To comply with this new rule, please go to your account settings and under +Account Management click on "Add a supervisor" and validate it with a known +supervisor. + +Failing to do so, your account will be blocked after a few weeks! (you +can however still re-activate it through an account reactivation procedure) + +If you are having problems to revalidate your account, contact a member of our +support staff at {{ prefix }}{% url 'contact' %}. + +BEAT Administrators at the Idiap Research Institute diff --git a/beat/web/ui/registration/templates/registration/mail.migration_10_accounts.subject.txt b/beat/web/ui/registration/templates/registration/mail.migration_10_accounts.subject.txt new file mode 100644 index 0000000000000000000000000000000000000000..4d87013ca9ff2b727c1404229184e22c140cbcb6 --- /dev/null +++ b/beat/web/ui/registration/templates/registration/mail.migration_10_accounts.subject.txt @@ -0,0 +1 @@ +Major Account Update - Supervision Required (BEAT platform) diff --git a/beat/web/ui/registration/templates/registration/mail.supervisee_blocked_state_wait_for_activation.message.txt b/beat/web/ui/registration/templates/registration/mail.supervisee_blocked_state_wait_for_activation.message.txt new file mode 100644 index 0000000000000000000000000000000000000000..0473236bdb2393d170df4cf92b4a22cf379fce5b --- /dev/null +++ b/beat/web/ui/registration/templates/registration/mail.supervisee_blocked_state_wait_for_activation.message.txt @@ -0,0 +1,14 @@ +Dear {{ supervisee.first_name }}, + +Your account is currently blocked due to a lack of supervisor. + +We acknowledge your request made to re-activate your account via a supervision request to {{ supervisor.username }}. + +You will be informed if your supervisor has accepted or rejected your request. + +If no information is given by your supervisor, you will be allowed to make a new supervision request after a week. + +If you are having problems to activate your account, contact a member of our +support staff at {{ prefix }}{% url 'contact' %}. + +BEAT Administrators at the Idiap Research Institute diff --git a/beat/web/ui/registration/templates/registration/mail.supervisee_blocked_validation_wait.subject.txt b/beat/web/ui/registration/templates/registration/mail.supervisee_blocked_validation_wait.subject.txt new file mode 100644 index 0000000000000000000000000000000000000000..1c53371c0bb8c748a3baee534fdd9c1b71995b5d --- /dev/null +++ b/beat/web/ui/registration/templates/registration/mail.supervisee_blocked_validation_wait.subject.txt @@ -0,0 +1 @@ +Blocked Account re-validation - Supervision request confirmation diff --git a/beat/web/ui/registration/templates/registration/registration_form.html b/beat/web/ui/registration/templates/registration/registration_form.html index eb3cc436cd9ce29188dfa7afdf8b5a6f3fe2abf8..3dc5dd1fc78079147141b8e7c62003d9e1cca108 100644 --- a/beat/web/ui/registration/templates/registration/registration_form.html +++ b/beat/web/ui/registration/templates/registration/registration_form.html @@ -2,21 +2,21 @@ {% comment %} * Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ * Contact: beat.support@idiap.ch - * + * * This file is part of the beat.web module of the BEAT platform. - * + * * Commercial License Usage * Licensees holding valid commercial BEAT licenses may use this file in * accordance with the terms contained in a written agreement between you * and Idiap. For further information contact tto@idiap.ch - * + * * Alternatively, this file may be used under the terms of the GNU Affero * Public License version 3 as published by the Free Software and appearing * in the file LICENSE.AGPL included in the packaging of this file. * The BEAT platform 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. - * + * * You should have received a copy of the GNU Affero Public License along * with the BEAT platform. If not, see http://www.gnu.org/licenses/. {% endcomment %} @@ -91,6 +91,19 @@ {% endfor %} </div> + <div class="form-group{% if form.godfather.errors %} has-error{% endif %}"> + {{ form.godfather.label_tag }} + {{ form.godfather|tabindex:7|addclass:"form-control" }} + {% for error in form.godfather.errors %} + <div class="alert alert-danger" role="alert">{{ error }}</div> + {% endfor %} + <div class="alert alert-warning" role="alert">Your account needs to + be validated by a recognized person on the platform. This recognized + person is known as a supervisor. + If you wish to become a supervisor, please follow the <a href="{% url 'contact' %}">contact information</a> + </div> + </div> + <div class="form-group{% if form.tos.errors %} has-error{% endif %}"> {{ form.tos }} I have carefully read the <a href="{% url 'terms-of-service' %}">Terms of Service</a>, which include the Privacy and Data Protection Terms of Use, and fully agree and undertake to comply with all provisions therein by checking this box. {% for error in form.tos.errors %} diff --git a/beat/web/ui/urls.py b/beat/web/ui/urls.py index 75dcf8a9eb76845c119d4c78beeb67020ee13468..960b0c3ada55cf26ade4f71b31e5d8dc4e0479bb 100644 --- a/beat/web/ui/urls.py +++ b/beat/web/ui/urls.py @@ -48,6 +48,11 @@ urlpatterns = [ name='login', ), + url(r'^blocked_user_reactivation/$', + views.blocked_user_reactivation, + name='blocked_user_reactivation', + ), + url(r'^events/(?P<author_name>\w+)/$', views.activity_stream, name='activity-stream', diff --git a/beat/web/ui/views.py b/beat/web/ui/views.py index 07bbef7f5e0a16b299afbfeadc100b3ae19d8f34..b6183a915274f732ba8c010bdadcff91aef12099 100644 --- a/beat/web/ui/views.py +++ b/beat/web/ui/views.py @@ -29,6 +29,8 @@ from django.shortcuts import get_object_or_404 from django.shortcuts import render_to_response from django.template import RequestContext +from django.template import loader +from django.template import Context from django.contrib.auth.views import login as django_login from django.contrib.auth.forms import PasswordChangeForm from django.contrib.auth.models import User @@ -45,6 +47,11 @@ from rest_framework.authtoken.models import Token from ..import __version__ from ..common.models import Shareable +from registration.forms import BlockedUserRevalidationForm +from ..accounts.models import Profile, SupervisionTrack + +import datetime +from urlparse import urlparse import logging logger = logging.getLogger(__name__) @@ -71,6 +78,113 @@ def login(request): return response +def blocked_user_reactivation(request): + '''Reactivation page''' + + if request.method == "POST": + form = BlockedUserRevalidationForm(request.POST) + if form.is_valid(): + try: + user = User.objects.get(username=request.POST["username"]) + if user.check_password(request.POST["password"]): + # Check if user is a blocked user + if user.profile.status == Profile.BLOCKED: + godfather = User.objects.get(username=request.POST["godfather"]) + # Check the godfather + if godfather.profile.status == Profile.ACCEPTED: + # Check if supervision track already exists + if user.profile.supervision_key is None: + # Create and assign key + supervisee = user + supervisee.profile.supervision_key = supervisee.profile._generate_current_supervision_key() + supervisiontrack = SupervisionTrack.objects.create( + supervisee = supervisee, + godfather = godfather, + is_valid = False, + ) + + # Assign key to supervision track + supervisiontrack.supervision_key = supervisee.profile.supervision_key + supervisiontrack.save() + supervisee.profile.supervision.add(supervisiontrack) + # Add a rejection date to the supervisee profile + now = datetime.datetime.now() + expiration_date_delta = datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS_FROM_GODFATHER) + if supervisee.profile.rejection_date == None: + supervisee.profile.rejection_date = now + expiration_date_delta + + supervisee.profile.save() + supervisee.save() + + #Inform by email the supervisor that he has a new supervisee request + from django.core.mail import send_mail + + parsed_url = urlparse(settings.URL_PREFIX) + server_address = '%s://%s' % (parsed_url.scheme, parsed_url.hostname) + + c = Context({ 'supervisor': godfather, + 'supervisee': supervisee, + 'prefix': server_address, + }) + + try: + t = loader.get_template('registration/mail.godfather_validation.subject.txt') + subject = t.render(c) + + # Note: e-mail subject *must not* contain newlines + subject = settings.EMAIL_SUBJECT_PREFIX + ''.join(subject.splitlines()) + + t = loader.get_template('registration/mail.godfather_validation_supervisee_add_request.message.txt') + message = t.render(c) + + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [godfather.email]) + except: + pass + + # inform the supervisee of his request + c = Context({ 'supervisor': godfather, + 'supervisee': supervisee, + 'prefix': server_address, + }) + + try: + t = loader.get_template('registration/mail.supervisee_blocked_validation_wait.subject.txt') + subject = t.render(c) + + # Note: e-mail subject *must not* contain newlines + subject = settings.EMAIL_SUBJECT_PREFIX + ''.join(subject.splitlines()) + + t = loader.get_template('registration/mail.supervisee_blocked_state_wait_for_activation.message.txt') + message = t.render(c) + + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [supervisee.email]) + except: + pass + + messages.success(request, "Your supervision request has been successfully processed.") + else: + messages.error(request, "A supervision request already exists for this account, you need to wait for your supervisor's decision.") + else: + messages.error(request, "The selected supervisor is not valid.") + else: + path = request.GET.get('next', '/') + messages.error(request, "Your profile is not blocked, you can go to the login page instead.") + else: + # Don't give too much details though we know the problem is the password only at this step + messages.error(request, "Wrong user or password combination!") + except User.DoesNotExist: + # Don't give too much details though we know the problem is the password only at this step + messages.error(request, "Wrong user or password combination!") + pass + + #return redirect('blocked_user_reactivation', pk=post.pk) + else: + form = BlockedUserRevalidationForm() + + return render_to_response('registration/blocked_user_reactivate.html', {'form': form}, + context_instance=RequestContext(request)) + + def gather_contributions(requestor, author): '''Gather contributions that are accessible to a certain requestor''' diff --git a/beat/web/urls.py b/beat/web/urls.py index b0b91118c37c8cec212927313415f824806064ad..081492e4b8ae69284430ba754999dde87eab4a07 100644 --- a/beat/web/urls.py +++ b/beat/web/urls.py @@ -164,6 +164,10 @@ unprefixed_patterns += [ include('beat.web.reports.api_urls', namespace='api_reports'), ), + url(r'^api/v1/accounts/', + include('beat.web.accounts.api_urls', namespace='api_accounts'), + ), + ] diff --git a/beat/web/utils/management/commands/daily_cron_actions.py b/beat/web/utils/management/commands/daily_cron_actions.py index d92e736856a9b31dd9ed3f8b5775e80172cab478..88d08e1d02d394fa5375779154846e5a71215a6d 100644 --- a/beat/web/utils/management/commands/daily_cron_actions.py +++ b/beat/web/utils/management/commands/daily_cron_actions.py @@ -52,3 +52,15 @@ class Command(BaseCommand): # Send report cleanup warnings and cleanup reports call_command('send_report_cleanup_warning_and_cleanup') + + # Clean and remove invalid users + call_command('clean_invalid_users') + + # Block rejected users with no supervision after specific rejection date + call_command('block_rejected_users') + + # Clean blocked users with no supervision after specific rejection date + call_command('clean_blocked_users_expired_requests') + + # Yearly revalidation process with warnings, status change and blockage + call_command('year_revalidation_users') diff --git a/beat/web/utils/management/commands/install.py b/beat/web/utils/management/commands/install.py index 7201b86eddc7a1b13c4339a5ce0a8bab86f817ca..a735c8b5f8a15bc4fe0237d71800dc3afe9393d4 100755 --- a/beat/web/utils/management/commands/install.py +++ b/beat/web/utils/management/commands/install.py @@ -98,6 +98,12 @@ def add_user(name, passwd, token_key): user.is_staff = True user.is_superuser = True user.save() + #set profile + user.profile.status = 'A' + user.profile.rejection_date = None + user.profile.supervision_key = None + user.profile.save() + user.save() if name == passwd: logger.info("Created user `%s' with password `%s'", name, passwd)