Commit 95a43695 authored by Giorgos Kazelidis's avatar Giorgos Kazelidis

- Defined __str__() methods for all the application models

- Removed the last_login field from the User model, inserted it to the Registry model (a User instance retains now one last_login value for each Platform instance that is associated with it via the corresponding Registry instance) and modified the emission arguments of the user_logged_in signal for logged-in User instances
- Constrained the email fields of the User/Admin models and user profile edit/recovery forms to a maximum length of 100 characters
- Constrained the username and password fields of the login form to a minimum length of 8 characters
- Omitted the redundant ECE-NTUA platform from the test Platform instances that are used to populate the usermerge_platform table of usermergeDB
- Omitted the username format check {piYYbSSS with SSS <> 000} of the login form for the Novice and Grader platforms as there can exist valid usernames for them that are not in the aforementioned format
- Enabled the Django admin site, registered all the application models to it and provided instructions on accessing it
- Defined the User_login_required() and Admin_login_required() decorators and used them for enhanced access control in the majority of views
- Reduced the database queries made in recover_user_profile() view to enhance its performance
- Added the production_logs folder to the .gitignore file
parent e324d683
......@@ -5,5 +5,8 @@
# Python compiled bytecode files
*.pyc
# virtual environment folder
# virtual environment folder - see README.md
venv/
# folder that contains the log files created in production - see the "Logging" part of settings.py
myprj/usermerge/production_logs/
......@@ -86,7 +86,7 @@
sudo apt-get install mysql-server
mysql_secure_installation
&nbsp; &nbsp; &nbsp; (7) use the password that was set for the root user during MySQL server installation as the DATABASES['default']['PASSWORD'] value in **~/Desktop/userbase/myprj/myprj/settings.py**
&nbsp; &nbsp; &nbsp; (8) copy the time zone definitions of the system's zoneinfo database, which is located in the /usr/share/zoneinfo/ directory of Ubuntu and most other Unix-like systems, to the "mysql" database without worrying about some possible "Unable to load/Skipping" warnings (you will be asked to provide the password that was set for the MySQL root user) and restart MySQL server (you can skip this substep if you have already executed it once):
&nbsp; &nbsp; &nbsp; (8) copy the time zone definitions of the system's zoneinfo database, which is located in the /usr/share/zoneinfo/ directory of Ubuntu and most other Unix-like systems, to the "mysql" database without worrying about some possible "Unable to load/Skipping" warnings (you will be asked to provide the password of the MySQL root user) and restart MySQL server (you can skip this substep if you have already executed it once):
mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root -p mysql
sudo service mysql restart
......@@ -96,7 +96,7 @@
&nbsp; &nbsp; &nbsp; (10) open MySQL console with root privileges:
python manage.py dbshell
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ALTERNATIVELY, you could run the following command by providing the password that was set for the MySQL root user:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ALTERNATIVELY, you could run the following command by providing the password of the MySQL root user:
mysql -u root -p
&nbsp; &nbsp; &nbsp; (11) if the project database (usermergeDB) has already been created (`SHOW DATABASES;` to check), delete it:
......@@ -114,18 +114,14 @@
python manage.py migrate
&nbsp; &nbsp; &nbsp; (17) if you have previously set SET_DEFAULT_STORAGE_ENGINE_TO_INNODB to "True", set it back to "False"
&nbsp; &nbsp; &nbsp; (18) open Python interpreter in interactive mode:
&nbsp; &nbsp; &nbsp; (18) populate the project database with test data by executing the populateDB.py script of the application directory (usermerge) via the Python interpreter:
python manage.py shell
&nbsp; &nbsp; &nbsp; (19) populate the project database with test data, i.e. execute the populateDB.py script of the application directory (usermerge), by typing the following line and pressing `Enter` twice:
with open('./usermerge/populateDB.py') as popDB: exec(popDB.read())
&nbsp; &nbsp; &nbsp; (20) close Python interpreter by pressing `Ctrl-D`
&nbsp; &nbsp; &nbsp; (21) deactivate the virtual environment:
python manage.py shell --command="with open('./usermerge/populateDB.py') as popDB: exec(popDB.read())"
&nbsp; &nbsp; &nbsp; (19) deactivate the virtual environment:
deactivate
## RUNNING THE PROJECT (INSIDE THE VIRTUAL ENVIRONMENT)
## ACCESSING THE DJANGO ADMIN SITE OR RUNNING THE PROJECT (AFTER STARTING THE DJANGO DEVELOPMENT SERVER INSIDE THE VIRTUAL ENVIRONMENT)
(A) activate the virtual environment:
cd ~/Desktop/userbase
......@@ -133,12 +129,18 @@
(B) navigate to the project directory:
cd myprj
(C\) start the Django development server (at http://127.0.0.1:8000/):
(C\) create a superuser account in the project database (usermergeDB) if you have not already done so (this step is necessary for accessing the Django admin site) [sources:
&nbsp; &nbsp; &nbsp; -- https://docs.djangoproject.com/en/2.0/ref/contrib/admin/
&nbsp; &nbsp; &nbsp; ]:
&nbsp; &nbsp; &nbsp; * ATTENTION: while creating the superuser account, you will be asked to set the corresponding credentials (username and password) and (optional) email.
python manage.py createsuperuser
(D) start the Django development server (at http://127.0.0.1:8000/):
python manage.py runserver
(D) navigate to http://127.0.0.1:8000/ via web browser to run the project
(E) access the Django admin site by navigating to http://127.0.0.1:8000/admin/ (via web browser) and filling out the emerging login form with the credentials of the aforementioned superuser account OR run the project by navigating to http://127.0.0.1:8000/
## STOPPING THE PROJECT (AND EXITING THE VIRTUAL ENVIRONMENT)
## STOPPING THE DJANGO DEVELOPMENT SERVER (AND EXITING THE VIRTUAL ENVIRONMENT)
(A) stop the Django development server (running at http://127.0.0.1:8000/):
&nbsp; &nbsp; &nbsp; -- return to the terminal where you have run `python manage.py runserver` and press `Ctrl-C`
(B) deactivate the virtual environment:
......
......@@ -37,7 +37,7 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'usermerge',
'usermerge.apps.UsermergeConfig',
]
MIDDLEWARE = [
......@@ -74,9 +74,7 @@ WSGI_APPLICATION = 'myprj.wsgi.application'
# Authentication
# https://docs.djangoproject.com/en/2.0/topics/auth/
AUTHENTICATION_BACKENDS = ['usermerge.backends.UserBackend']
LOGIN_URL = 'default_login'
AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend', 'usermerge.backends.AppUserModelBackend']
# Logging
......
from django.contrib import admin
from .models import Admin, Platform, Registry, User
# Register your models here.
# https://docs.djangoproject.com/en/2.0/ref/contrib/admin/
admin.site.register(User)
admin.site.register(Platform)
admin.site.register(Registry)
admin.site.register(Admin)
from django.apps import AppConfig
# Create your project-specific configuration classes (AppConfig subclasses) for the usermerge application here.
# https://docs.djangoproject.com/en/2.0/ref/applications/
class UsermergeConfig(AppConfig):
name = 'usermerge'
......@@ -8,11 +8,11 @@ from django.contrib.auth import _get_backends, load_backend
from django.contrib.auth.signals import user_logged_in
from django.middleware.csrf import rotate_token
from django.utils.crypto import constant_time_compare
from usermerge.models import Registry
from .models import Registry
# For more information on default/customizing user authentication and password management, see https://docs.djangoproject.com/en/2.0/topics/auth/ and backends.py .
# The user_logged_in signal is defined (alongside the other authentication signals, user_login_failed and user_logged_out) in the django.contrib.auth.signals module (https://github.com/django/django/blob/master/django/contrib/auth/signals.py). When a user logs in successfully, an appropriate user_logged_in signal is sent by the corresponding user model (User/Admin) in the context of the below defined login() function and is instantaneously received by the update_last_login() function (https://github.com/django/django/blob/master/django/contrib/auth/apps.py). update_last_login() then updates the last_login field of the corresponding user instance to the date-time of the signal reception (https://github.com/django/django/blob/master/django/contrib/auth/models.py). For more information on the user models and their last_login fields, see models.py . For more information on signals, see https://docs.djangoproject.com/en/2.0/topics/signals/ .
# For more information on the platform names that are provided in the drop-down list of the login form, see log_in() view.
# For more information on default/customizing user authentication and password management, see https://docs.djangoproject.com/en/2.0/topics/auth/, backends.py and AuthenticationMiddleware of middleware.py .
# For more information on the utilization of the login form input to perform user authentication and login, see the log_in() view.
# The user_logged_in signal is defined (alongside the other authentication signals, user_login_failed and user_logged_out) in the django.contrib.auth.signals module (https://github.com/django/django/blob/master/django/contrib/auth/signals.py). When a user (User/Admin instance) logs in successfully, an appropriate user_logged_in signal is sent by the corresponding user model (Registry model in case of a User instance) in the context of the below defined login() function and is instantaneously received by the update_last_login() function (https://github.com/django/django/blob/master/django/contrib/auth/apps.py). update_last_login() then updates the last_login field of the corresponding user instance for the login platform (SLUB in case of an Admin instance) to the date-time of the signal reception (https://github.com/django/django/blob/master/django/contrib/auth/models.py). For more information on the user models and their last_login fields, see models.py . For more information on signals, see https://docs.djangoproject.com/en/2.0/topics/signals/ .
SESSION_KEY = '_auth_user_id'
PLATFORM_SESSION_KEY = '_auth_platform'
......@@ -28,9 +28,9 @@ def _get_user_model_label(request):
# and user_model_name is the name of some user model (User/Admin) defined in the application models (see models.py).
# WARNING: The model officially used for user representation is stored as label in the AUTH_USER_MODEL setting and is generally based
# either on the AbstractBaseUser or on the AbstractUser model (see https://docs.djangoproject.com/en/2.0/topics/auth/customizing/).
# This means that AUTH_USER_MODEL should, among others, have a password field and possibly a username field, which is NOT true for
# the primary user model of the usermerge application, 'usermerge.User'. To bypass this issue during authentication, AUTH_USER_MODEL
# is omitted and 'usermerge.User' is used directly alongside usermerge's secondary user model, 'usermerge.Admin'. Although the latter
# This means that AUTH_USER_MODEL should, among others, have a password and a (unique) username field, which is NOT true for the
# primary user model of the usermerge application, 'usermerge.User'. To bypass this issue during authentication, AUTH_USER_MODEL is
# omitted and 'usermerge.User' is used directly alongside usermerge's secondary user model, 'usermerge.Admin'. Although the latter
# has both a username and a password field, it does NOT inherit from the AbstractBaseUser or AbstractUser model for simplicity as
# well as peer-conformance reasons. AUTH_USER_MODEL is undeclared in settings.py, i.e. 'auth.User' (AbstractUser-inherited model) by
# default, and should remain that way as it is practically replaced by 'usermerge.User' and 'usermerge.Admin'.
......@@ -41,7 +41,7 @@ def _get_user_model_label(request):
def get_user_model(request):
"""
Return the user model that is active in the session after specifying its label - see _get_user_model_label() function above.
Return the user model that is active in the session after specifying its label - see the _get_user_model_label() function above.
"""
return django_apps.get_model(_get_user_model_label(request), require_ready = False)
......@@ -55,6 +55,7 @@ def login(request, user, backend = None):
Note that data set during the anonymous session is retained when the user logs in.
"""
platform_name = request.POST['platform']
registry = None
session_auth_hash = ''
if user is None:
user = request.user
......@@ -99,7 +100,21 @@ def login(request, user, backend = None):
if hasattr(request, 'user'):
request.user = user
rotate_token(request)
user_logged_in.send(sender = user.__class__, request = request, user = user)
# If the just logged-in user is an Admin instance, its last_login value for the login platform (SLUB) is kept by the user instance
# itself; therefore, send the user_logged_in signal with the Admin model as sender argument, the associated request as request
# argument and the aforementioned Admin instance as user argument. On the other hand, if the just logged-in user is a User instance,
# its last_login value for the login platform is kept by the corresponding Registry instance; therefore, send the user_logged_in
# signal with the Registry model as sender argument, the associated request as request argument and the aforementioned Registry
# instance as user argument (if the logged-in user or/and login platform instances are ever needed in any receiver function of the
# signal, they can be easily specified either via the session data of the request or via the Registry instance). For more information
# on sending the user_logged_in signal, see the relative comment at the top.
if registry:
# User login signal emission
user_logged_in.send(sender = registry.__class__, request = request, user = registry)
else:
# Admin login signal emission
user_logged_in.send(sender = user.__class__, request = request, user = user)
def get_user(request):
"""
......
from django.contrib.auth.hashers import check_password, make_password
from usermerge.models import Admin, Registry, User
from .models import Admin, Registry, User
# Create your backends here.
# https://docs.djangoproject.com/en/2.0/topics/auth/customizing/#authentication-backends
# For more information on default/customizing user authentication and password management, see https://docs.djangoproject.com/en/2.0/topics/auth/ and auth.py .
# For more information on default/customizing user authentication and password management, see https://docs.djangoproject.com/en/2.0/topics/auth/, auth.py and AuthenticationMiddleware of middleware.py .
class AppUserModelBackend:
"""
Authenticates against the application user models, i.e. usermerge.User and usermerge.Admin models.
"""
class UserBackend:
def authenticate(self, request, platform = None, username = None, password = None):
"""
Authenticate the provided credentials, i.e. username and password, for the selected platform.
......
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from functools import wraps
# Create your decorators here.
# There are many different types of decorators in Django, such as view decorators (https://docs.djangoproject.com/en/2.0/topics/http/decorators/), authentication decorators (https://docs.djangoproject.com/en/2.0/topics/auth/default/), utility decorators (https://docs.djangoproject.com/en/2.0/ref/utils/), etc. For more information, search in https://docs.djangoproject.com/en/2.0/search/?q=decorator .
# For more information on decorator definition and usage, see https://realpython.com/primer-on-python-decorators/ .
# For more information on view-related subjects, e.g. request/response objects, exceptions that trigger built-in error views, default and application user models (auth.User and usermerge.{User/Admin}), named URL patterns, etc., see views.py .
def User_login_required(view):
"""
Ensure that the current user has logged in and that his/her model is usermerge.User in order to execute the wrapped view. If he/she
has not logged in (his/her model is AnonymousUser), redirect him/her to the login page. If he/she has logged in but his/her model is
either auth.User or usermerge.Admin, show him/her an HTTP 403 (Forbidden) page by raising a PermissionDenied exception.
"""
def User_model_required(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
if str(request.user.__class__) == "<class 'usermerge.models.User'>":
return func(request, *args, **kwargs)
else: # either auth.User or usermerge.Admin model
raise PermissionDenied
return wrapper
# The login and user model checks are performed by the login_required() and User_model_required() decorators respectively.
return login_required(User_model_required(view), None, 'default_login')
def Admin_login_required(view):
"""
Ensure that the current user has logged in and that his/her model is usermerge.Admin in order to execute the wrapped view. If he/she
has not logged in (his/her model is AnonymousUser), redirect him/her to the login page. If he/she has logged in but his/her model is
either auth.User or usermerge.User, show him/her an HTTP 403 (Forbidden) page by raising a PermissionDenied exception.
"""
def Admin_model_required(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
if str(request.user.__class__) == "<class 'usermerge.models.Admin'>":
return func(request, *args, **kwargs)
else: # either auth.User or usermerge.User model
raise PermissionDenied
return wrapper
# The login and user model checks are performed by the login_required() and Admin_model_required() decorators respectively.
return login_required(Admin_model_required(view), None, 'default_login')
import re
from django import forms
from usermerge.validators import platform_is_selected_from_provided_list, ece_id_is_not_031YY000
from .validators import ece_id_is_not_031YY000, platform_is_SLUB_or_exists_in_DB
# Create your forms here.
# https://docs.djangoproject.com/en/2.0/ref/forms/
# Each programmatic form from the ones defined below is based format-wise on the similarly named template form (e.g. LoginForm is based on the login_form of login.html, UserProfileEditForm is based on the user_profile_edit_form of user_profile_edit.html, etc.) and constraint-wise on the application models (see models.py).
# Each (programmatic) form from the ones defined below is based format-wise on the similarly named template form (e.g. LoginForm is based on the login_form of login.html, UserProfileEditForm is based on the user_profile_edit_form of user_profile_edit.html, etc.) and constraint-wise on the application models (see models.py).
class LoginForm(forms.Form):
platform = forms.CharField(error_messages = {'required': 'Το όνομα πλατφόρμας πρέπει απαραίτητα να επιλεχθεί και να είναι μη-κενό!'},
validators = [platform_is_selected_from_provided_list])
username = forms.CharField(max_length = 50,
validators = [platform_is_SLUB_or_exists_in_DB])
username = forms.CharField(min_length = 8, max_length = 50,
error_messages = {'required': 'Το όνομα χρήστη πρέπει απαραίτητα να συμπληρωθεί!',
'min_length': 'Το όνομα χρήστη πρέπει να έχει τουλάχιστον 8 χαρακτήρες!',
'max_length': 'Το όνομα χρήστη δεν πρέπει να υπερβαίνει τους 50 χαρακτήρες!'})
password = forms.CharField(max_length = 50,
password = forms.CharField(min_length = 8, max_length = 50,
error_messages = {'required': 'Ο κωδικός πρόσβασης πρέπει απαραίτητα να συμπληρωθεί!',
'min_length': 'Ο κωδικός πρόσβασης πρέπει να έχει τουλάχιστον 8 χαρακτήρες!',
'max_length': 'Ο κωδικός πρόσβασης δεν πρέπει να υπερβαίνει τους 50 χαρακτήρες!'})
def clean(self):
cleaned_data = super().clean()
# If platform and username have survived the initial individual field checks (by the time this method is called, all the
# individual field clean methods will have been run, so cleaned_data will be populated with any data that has survived so far)
# and platform refers either to Novice or to Grader, ensure that the username format is piYYbSSS (if it is not, raise a
# ValidationError exception with the appropriate error message and code), where YY refers to year and SSS refers to non-000
# serial number.
if set(('platform', 'username')).issubset(cleaned_data):
platform = cleaned_data['platform']
username = cleaned_data['username']
if platform == 'Novice' or platform == 'Grader':
if re.fullmatch('pi[0-9]{2}b[0-9]{3}', username):
if username[5:] == '000':
raise forms.ValidationError('Το όνομα χρήστη στις πλατφόρμες Novice και Grader απαγορεύεται να\n'
'είναι της μορφής piYYb000 (YY: έτος)!', code = 'novice_or_grader_username_is_piYYb000')
else:
raise forms.ValidationError('Το όνομα χρήστη στις πλατφόρμες Novice και Grader πρέπει να\n'
'είναι της μορφής piYYbSSS (ΥΥ: έτος, SSS: αύξων αριθμός)!',
code = 'novice_or_grader_username_format_is_not_piYYbSSS')
class UserProfileEditForm(forms.Form):
first_name = forms.CharField(max_length = 50,
error_messages = {'required': 'Το όνομα πρέπει απαραίτητα να συμπληρωθεί!',
......@@ -51,9 +33,9 @@ class UserProfileEditForm(forms.Form):
'invalid': 'Ο αριθμός μητρώου πρέπει να είναι της μορφής '
'031YYSSS (YY: έτος, SSS: αύξων αριθμός)!'},
validators = [ece_id_is_not_031YY000])
email = forms.EmailField(max_length = 254,
email = forms.EmailField(max_length = 100,
error_messages = {'required': 'Το ηλεκτρονικό ταχυδρομείο πρέπει απαραίτητα να συμπληρωθεί!',
'max_length': 'Το ηλεκτρονικό ταχυδρομείο δεν πρέπει να υπερβαίνει τους 254 χαρακτήρες!',
'max_length': 'Το ηλεκτρονικό ταχυδρομείο δεν πρέπει να υπερβαίνει τους 100 χαρακτήρες!',
'invalid': 'Το ηλεκτρονικό ταχυδρομείο πρέπει να είναι της μορφής local-part@domain που\n'
'περιγράφεται στην ιστοσελίδα https://en.wikipedia.org/wiki/Email_address !'})
......
......@@ -4,22 +4,19 @@ Set of helper functions that contribute to the correct execution and enhanced st
from django import forms
from django.template.response import TemplateResponse
from usermerge.auth import PLATFORM_SESSION_KEY
from usermerge.models import Platform
from .auth import PLATFORM_SESSION_KEY
from .models import Platform
# Create your helper functions here.
# For more information on view-related subjects, e.g. using request/response objects, making database queries, working with forms, etc., see views.py .
def get_names_of_SoftLab_provided_platforms_from_DB():
def get_all_platform_names_from_DB():
"""
Return the id-ordered list of all the platform names, except for ECE-NTUA (this "platform" is not provided by SoftLab), that exist
in usermergeDB. This list can, among others, be used in the context of application pages/templates to display the name options of
the platform field in forms.
Return the list (QuerySet) of all the platform names that exist in usermergeDB. This list can, among others, be used in the context
of application pages/templates to display the name options of the platform field in forms.
"""
platform_names = list(Platform.objects.order_by('id').values_list('name', flat = True))
platform_names.remove('ECE-NTUA') # For more information on platforms that exist in usermergeDB, see populateDB.py .
return platform_names
return Platform.objects.values_list('name', flat = True)
def get_form_error_messages(form):
"""
......
from django.conf import settings
from django.contrib import auth as django_contrib_auth
from django.utils.deprecation import MiddlewareMixin
from django.utils.functional import SimpleLazyObject
from usermerge import auth
from . import auth as usermerge_auth
# Create your middleware here.
# https://docs.djangoproject.com/en/2.0/topics/http/middleware/
# AuthenticationMiddleware is a middleware component that associates the current user with every incoming web request. Original source code for the get_user() function and the aforementioned component can be found in https://github.com/django/django/blob/master/django/contrib/auth/middleware.py (for more information, see https://docs.djangoproject.com/en/2.0/ref/middleware/#django.contrib.auth.middleware.AuthenticationMiddleware).
# For more information on default/customizing user authentication and password management, see https://docs.djangoproject.com/en/2.0/topics/auth/, auth.py and backends.py .
# The login() function of the application's authentication module (usermerge.auth) is used to log a user (usermerge.{User/Admin} instance) in via the login page of the application - see the log_in() view - and inserts the PLATFORM_SESSION_KEY value into the (dictionary-like) session data based on the platform selected in the corresponding form. On the other hand, the login() function of the default authentication module (django.contrib.auth) is used to log a user (auth.User instance) in via the login page of the admin site (see https://docs.djangoproject.com/en/2.0/ref/contrib/admin/) and does not insert any PLATFORM_SESSION_KEY value into the session data as the corresponding form does not include any platform field.
def get_user(request):
if not hasattr(request, '_cached_user'):
request._cached_user = auth.get_user(request)
# If the PLATFORM_SESSION_KEY value exists in the session data, retrieve the user instance associated with the given request
# session by using the get_user() function of the application's authentication module. Otherwise, retrieve it by using the
# get_user() function of the default authentication module. For more information, see the relative comment at the top.
if usermerge_auth.PLATFORM_SESSION_KEY in request.session:
request._cached_user = usermerge_auth.get_user(request)
else:
request._cached_user = django_contrib_auth.get_user(request)
return request._cached_user
class AuthenticationMiddleware(MiddlewareMixin):
......@@ -20,3 +29,4 @@ class AuthenticationMiddleware(MiddlewareMixin):
'insert "django.contrib.sessions.middleware.SessionMiddleware" before "usermerge.middleware.AuthenticationMiddleware".'
) % ('_CLASSES' if settings.MIDDLEWARE is None else '')
request.user = SimpleLazyObject(lambda: get_user(request))
# Generated by Django 2.0.4 on 2019-01-24 11:33
# Generated by Django 2.0.4 on 2019-05-07 19:10
from django.db import migrations, models
import django.db.models.deletion
......@@ -18,7 +18,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=50)),
('last_name', models.CharField(max_length=50)),
('email', models.EmailField(max_length=254, unique=True)),
('email', models.EmailField(max_length=100, unique=True)),
('username', models.CharField(max_length=50, unique=True)),
('password', models.CharField(max_length=100)),
('last_login', models.DateTimeField(null=True)),
......@@ -37,6 +37,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('username', models.CharField(max_length=50)),
('password', models.CharField(max_length=100)),
('last_login', models.DateTimeField(null=True)),
('platform', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='usermerge.Platform')),
],
options={
......@@ -50,8 +51,7 @@ class Migration(migrations.Migration):
('first_name', models.CharField(max_length=50)),
('last_name', models.CharField(max_length=50)),
('ece_id', models.CharField(max_length=8, null=True, unique=True)),
('email', models.EmailField(max_length=254, null=True, unique=True)),
('last_login', models.DateTimeField(null=True)),
('email', models.EmailField(max_length=100, null=True, unique=True)),
],
),
migrations.AddField(
......
from django.db import models
from django.utils.crypto import salted_hmac
from django.utils.timezone import localtime
# Create your models here.
# https://docs.djangoproject.com/en/2.0/ref/models/
# The below defined models impose on fields only the absolutely necessary DB-related constraints. For more information on elaborate field constraints imposed outside the scope of models, e.g. validation of ece_id and email formats, prohibited clearance of registered ece_id values, etc., see forms.py and views.py .
# The character set (encoding) of usermergeDB is set to utf8mb4 for proper Unicode support and the storage engine of its tables is set to InnoDB for proper transaction support. For more information on the database (table) options, e.g. character set, collation, SQL mode and storage engine, see README.md and the "Database" part of settings.py . It is worth mentioning that if MySQL is used as the database backend, the maximum length of CharFields and EmailFields with (unique) indexes in utf8mb4 encoding should be at most 191 characters (in utf8mb3 encoding, it should be at most 255 characters).
# The below defined models impose on fields only the absolutely necessary DB-related constraints. For more information on elaborate field constraints imposed outside the scope of models, e.g. minimum length constraint for username and password values, pattern constraints for ece_id and email values, prohibited clearance of registered ece_id values, etc., see forms.py and views.py .
# Regarding the fields whose null option is set to True (any unique fields whose null option is undeclared, i.e. False by default, should NEVER be allowed to be empty): The empty DB value for DateTimeFields is NULL (interpreted as None in Python), thus designating the absence of value. The empty DB value for CharFields and EmailFields is the empty/zero-length string (''), which is a valid string value. For unique CharFields and EmailFields, such as ece_id and email of the User model, whose value may NOT always be declared when creating/updating the respective DB entries, this could lead in integrity errors ("Duplicate entry '' for key 'ece_id'/'email'"). To avoid such errors, NULL should be used as the empty DB value of these fields (the empty string should NOT be used in this case). For more information, see https://docs.djangoproject.com/en/2.0/ref/models/fields/ .
# Each time a user logs in, an appropriate user_logged_in signal is emitted (see login() function in auth.py) causing the last_login field of the corresponding User/Admin instance to be updated independently of the other fields. Generally, the last_login field of a user instance is non-empty when the other fields are non-empty and it is empty when the others are empty. It is also possible, though, for the last_login field of a User instance to be non-empty when all the other fields are empty (the user logs in and, although his/her profile is completely empty, he/she opts to log out without filling it out) or for the last_login field of an Admin instance to be empty when all the other fields are non-empty (the user has never logged in but his/her profile has already been filled out in a non-session context, e.g. fixture - see https://docs.djangoproject.com/en/2.0/howto/initial-data/, interactive shell - see https://docs.djangoproject.com/en/2.0/ref/django-admin/, file such as populateDB.py, etc.).
# The password field of a user instance should NEVER contain the user's raw password. It should, instead, contain a hashed version of the latter created by the make_password() function for enhanced security (see examples in populateDB.py). The aforementioned function creates the hashed password using the PBKDF2 algorithm with a SHA256 hash by default. The length of the hashed password is 78 characters. For more information on password management, see https://docs.djangoproject.com/en/2.0/topics/auth/passwords/ .
# Raw passwords can be reported/logged/printed (see views.py) ONLY after they have first been filtered/hidden (see, for example, https://docs.djangoproject.com/en/2.0/howto/error-reporting/).
# The id and last_login fields are populated/updated automatically (each model is implicitly given an id AutoField that acts as the corresponding primary key, each last_login field is initially empty and is updated whenever the corresponding user_logged_in signal is emitted) and should NOT be declared during the creation/update of model instances. The REMAINING fields should satisfy specific requirements (see examples in populateDB.py):
# * The name field of a Platform instance should NEVER be empty. Its value can be changed validly/consistently but it should NEVER become empty. A Platform instance can be created/changed in usermergeDB ONLY in a non-session context, e.g. fixture, interactive shell, etc. (see the relative references above).
# The id and last_login fields are populated/updated automatically (each model is implicitly given an id AutoField that acts as the corresponding primary key, each last_login field is initially empty and is updated whenever the corresponding user_logged_in signal is emitted - see the relative explanation on last_login fields below) and should NOT be declared during the creation/update of model instances. The REMAINING fields should satisfy specific requirements (see examples in populateDB.py):
# * The name field of a Platform instance should NEVER be empty. Its value can be changed validly/consistently but it should NEVER become empty. A Platform instance can be created/changed in usermergeDB ONLY in a NON-session context, e.g. fixture - see https://docs.djangoproject.com/en/2.0/howto/initial-data/, interactive shell - see https://docs.djangoproject.com/en/2.0/ref/django-admin/, file such as populateDB.py, etc.
# * ALL the fields of a Registry instance should be NON-empty. The user (user_id), username and password values can be changed validly/consistently (the platform - platform_id - value should NEVER be changed) but they should NEVER become empty. When a Registry instance is initially created, an empty User instance should also be created and associated with that Registry instance - see the below explanation about the User instance fields.
# * ALL the fields of an Admin instance should be NON-empty. Their values can be changed validly/consistently but they should NEVER become empty. An Admin instance can be created/changed in usermergeDB ONLY in a non-session context, e.g. fixture, interactive shell, etc. (see the relative references above).
# * ALL the fields of an Admin instance should be NON-empty. Their values can be changed validly/consistently but they should NEVER become empty. An Admin instance can be created/changed in usermergeDB ONLY in a NON-session context, e.g. fixture, interactive shell, file such as populateDB.py, etc. (see the relative references above).
# * The fields of a User instance should ALWAYS be in ONE of the following states: 1)ALL filled out, 2)ALL filled out EXCEPT FOR ece_id, 3)NONE filled out. The allowed transitions between field states are: 1-->1, 2-->1, 2-->2, 3-->1 and 3-->2 (empty field values can be changed to valid/consistent non-empty ones, non-empty field values can be changed validly/consistently but they should NEVER become empty). When a User instance is initially created, ALL its fields should be empty (state 3) and EXACTLY ONE Registry instance should be created to reference it - see the above explanation about the Registry instance fields. The fields can then be changed/filled out (state 1/2) ONLY by the corresponding user via the user profile edit form after he/she has logged in first - see the edit_user_profile() view (any other way of changing/filling out the fields is by NO means accepted).
# The password field of a user instance should NEVER contain the user's raw password. It should, instead, contain a hashed version of the latter created by make_password() function for enhanced security (see examples in populateDB.py). The aforementioned function creates the hashed password using the PBKDF2 algorithm with a SHA256 hash by default. The length of the hashed password is 78 characters. For more information on password management, see https://docs.djangoproject.com/en/2.0/topics/auth/passwords/ .
# Raw passwords can be reported/logged/printed (see views.py) ONLY after they have first been filtered/hidden (see, for example, https://docs.djangoproject.com/en/2.0/howto/error-reporting/).
# Given that support for time zones is enabled (USE_TZ setting is True), DateTimeField values are stored in UTC format in usermergeDB and are displayed in the current time zone, that is 'Europe/Athens' by default (specified by the TIME_ZONE setting), in templates. For more information on the time zone selection logic and display format, see https://docs.djangoproject.com/en/2.0/topics/i18n/timezones/ and the 'Internationalization' part of settings.py .
# The get_session_auth_hash() methods and is_authenticated attributes are needed for user authentication, e.g. in login() and get_user() functions of auth.py and login_required() decorator of views.py . Original source code for these methods/attributes can be found in https://github.com/django/django/blob/master/django/contrib/auth/base_user.py (for more information, see https://docs.djangoproject.com/en/2.0/topics/auth/customizing/).
# A User instance retains ONE last_login value FOR EACH Platform instance that is associated with it via the corresponding Registry instance, while an Admin instance is associated ONLY with the SLUB "platform", i.e. it is NOT associated with any Platform instances, and thus retains EXACTLY ONE last_login value. Each time a user logs in, an appropriate user_logged_in signal is emitted - see the login() function in auth.py - causing the last_login field of the corresponding User/Admin instance for the login platform (SLUB in case of an Admin instance) to be updated INDEPENDENTLY of its other fields. A NON-empty User instance should ALWAYS retain a NON-empty last_login value FOR EACH of its associated Platform instances - see the edit_user_profile() and recover_user_profile() views. On the other hand, an empty User instance generally retains an empty last_login value for its ONLY associated Platform instance but can also retain a NON-empty one for it (the user logs in and, although his/her profile is empty, he/she opts to log out - or is logged out automatically due to session expiration - WITHOUT filling it out). An Admin instance generally retains a NON-empty last_login value for the SLUB "platform" as it should be logged in to access any Admin-targeted functionality/view, but can also retain an empty one for it (the user has NEVER logged in but his/her profile has ALREADY been filled out in a NON-session context, e.g. fixture, interactive shell, file such as populateDB.py, etc. - see the relative references above).
# Given that support for time zones is enabled (USE_TZ setting is True), DateTimeField values (datetime objects) are stored in UTC format in usermergeDB and are generally displayed in the current time zone, that is 'Europe/Athens' by default (specified by the TIME_ZONE setting), in templates. It is possible to manually convert these values to a different time zone or/and change their display format if needed, e.g. convert them to local time (default time zone, i.e. 'Europe/Athens') or/and display them in 'DD/MM/YYYY HH:MM:SS' format. For more information on time zone support and datetime objects, see https://docs.djangoproject.com/en/2.0/topics/i18n/timezones/ and the "Internationalization" part of settings.py .
# The get_session_auth_hash() methods and is_authenticated attributes are needed for user authentication, e.g. in the login() and get_user() functions defined in auth.py and login_required() decorator used in views.py and decorators.py . Original source code for these methods/attributes can be found in https://github.com/django/django/blob/master/django/contrib/auth/base_user.py (for more information, see https://docs.djangoproject.com/en/2.0/topics/auth/customizing/).
class User(models.Model):
first_name = models.CharField(max_length = 50)
last_name = models.CharField(max_length = 50)
ece_id = models.CharField(max_length = 8, unique = True, null = True)
email = models.EmailField(unique = True, null = True) # max_length = 254 by default
last_login = models.DateTimeField(null = True)
email = models.EmailField(max_length = 100, unique = True, null = True)
def __str__(self):
return ('[id: %d | first_name: %s | last_name: %s | ece_id: %s | email: %s]'
) % (self.id, self.first_name, self.last_name, self.ece_id, self.email)
@property
def is_authenticated(self):
......@@ -37,11 +42,24 @@ class User(models.Model):
class Platform(models.Model):
name = models.CharField(max_length = 50, unique = True)
def __str__(self):
return '[id: %d | name: %s]' % (self.id, self.name)
class Registry(models.Model):
user = models.ForeignKey('User', on_delete = models.PROTECT)
platform = models.ForeignKey('Platform', on_delete = models.PROTECT)
username = models.CharField(max_length = 50)
password = models.CharField(max_length = 100)
last_login = models.DateTimeField(null = True)
class Meta:
unique_together = (('user', 'platform',), ('platform', 'username',),)
verbose_name_plural = 'registries'
def __str__(self):
return ('[id: %d | user: <User: %s> | platform: <Platform: %s> | username: %s | password: %s | last_login: %s]'
) % (self.id, self.user, self.platform, self.username, self.password,
None if self.last_login is None else localtime(self.last_login))
def get_session_auth_hash(self):
"""
......@@ -50,24 +68,18 @@ class Registry(models.Model):
key_salt = 'usermerge.models.Registry.get_session_auth_hash'
return salted_hmac(key_salt, self.password + '@' + self.platform.name).hexdigest()
class Meta:
unique_together = (('user', 'platform',), ('platform', 'username',),)
verbose_name_plural = 'registries'
class Admin(models.Model):
first_name = models.CharField(max_length = 50)
last_name = models.CharField(max_length = 50)
email = models.EmailField(unique = True) # max_length = 254 by default
email = models.EmailField(max_length = 100, unique = True)
username = models.CharField(max_length = 50, unique = True)
password = models.CharField(max_length = 100)
last_login = models.DateTimeField(null = True)
def get_session_auth_hash(self):
"""
Return an HMAC of the password field.
"""
key_salt = 'usermerge.models.Admin.get_session_auth_hash'
return salted_hmac(key_salt, self.password).hexdigest()
def __str__(self):
return ('[id: %d | first_name: %s | last_name: %s | email: %s | username: %s | password: %s | last_login: %s]'
) % (self.id, self.first_name, self.last_name, self.email, self.username, self.password,
None if self.last_login is None else localtime(self.last_login))
@property
def is_authenticated(self):
......@@ -77,3 +89,10 @@ class Admin(models.Model):
"""
return True
def get_session_auth_hash(self):
"""
Return an HMAC of the password field.
"""
key_salt = 'usermerge.models.Admin.get_session_auth_hash'
return salted_hmac(key_salt, self.password).hexdigest()
......@@ -3,31 +3,37 @@ Populate usermergeDB (see models.py) with test data to check the correctness of
"""
from django.contrib.auth.hashers import make_password
# Use "usermerge.models" instead of ".models" as the import source module to avoid raising a
# "No module named 'django.core.management.commands.models'" ImportError exception when this
# script is executed via the interactive shell (see README.md).
from usermerge.models import Admin, Platform, Registry, User
# Populate Admin table with test admins.
# Populate the usermerge_admin table with test Admin instances.
admin_hashed_password = make_password('7wonders')
admin1 = Admin.objects.create(first_name = 'Νικόλαος', last_name = 'Παπασπύρου', email = 'nikolaos@softlab.ntua.gr',
username = 'nikolaos', password = admin_hashed_password)
admin2 = Admin.objects.create(first_name = 'Ευστάθιος', last_name = 'Ζάχος', email = 'eustathios@corelab.ntua.gr',
username = 'eustathios', password = admin_hashed_password)
# Populate Platform table with test platforms.
ece_ntua = Platform.objects.create(name = 'ECE-NTUA')
# Populate the usermerge_platform table with test Platform instances.
novice = Platform.objects.create(name = 'Novice')
grader = Platform.objects.create(name = 'Grader')
moodle = Platform.objects.create(name = 'Moodle')
plgrader = Platform.objects.create(name = 'PLgrader')
# Populate User table with test users.
# The user1 and user2 instances are created with filled out fields. This creation method can SOMETIMES be used during development to assist testing/debugging, but it is NOT generally accepted and should ALWAYS be avoided in production. On the other hand, creating User instances with empty fields, such as user3, is the accepted method and should ALWAYS be preferred both during development and in production.
# Populate the usermerge_user table with test User instances.
# The user1 and user2 instances are created with filled out fields. This creation method can SOMETIMES be used during development to
# assist testing/debugging, but it is NOT generally accepted and should ALWAYS be avoided in production. On the other hand, creating
# User instances with empty fields, such as user3, is the accepted method and should ALWAYS be preferred both during development and
# in production.
user1 = User.objects.create(first_name = 'Γεώργιος', last_name = 'Καζελίδης', ece_id = '03199999',
email = 'gkazelid@undergraduate.ece.ntua.gr')
user2 = User.objects.create(first_name = 'Ζαχαρίας', last_name = 'Δόγκανος', email = 'zdogkanos@undergraduate.ece.ntua.gr')
user3 = User.objects.create()
# Populate Registry table with test registries.
# Populate the usermerge_registry table with test Registry instances.
user_hashed_password = make_password('password')
registry1 = Registry.objects.create(user = user1, platform = novice, username = 'pi99b999', password = user_hashed_password)
registry2 = Registry.objects.create(user = user2, platform = moodle, username = 'moodler', password = user_hashed_password)
registry3 = Registry.objects.create(user = user3, platform = plgrader, username = 'plgrad', password = user_hashed_password)
registry2 = Registry.objects.create(user = user2, platform = moodle, username = 'moodlemoot', password = user_hashed_password)
registry3 = Registry.objects.create(user = user3, platform = plgrader, username = 'proglang', password = user_hashed_password)
......@@ -9,5 +9,5 @@
{% block content %}
<h4>Σφάλμα 404 (Not Found)</h4>
<p>Η ζητηθείσα ιστοσελίδα "{{ request_path }}" δεν βρέθηκε στον παρόντα διακομιστή!</p>
<p>Η ζητηθείσα ιστοσελίδα δεν βρέθηκε στον παρόντα διακομιστή!</p>
{% endblock %}
......@@ -15,12 +15,11 @@
<tr>
<td style="text-align:right;"><label for="platform">Είσοδος ως:</label></td>
<td style="text-align:left;">
<select name="platform" id="platform">
<select name="platform" id="platform" required="required">
<option value=""></option>
<option value="SLUB">διαχειριστής του SLUB</option>
{% for name in platform_names %}
<option value="{{ name }}" {% if name == 'Novice' %}selected="selected"{% endif %}>
χρήστης του {{ name }}
</option>
<option value="{{ name }}">χρήστης του {{ name }}</option>
{% endfor %}
</select>
</td>
......@@ -28,13 +27,13 @@
<tr>
<td style="text-align:right;"><label for="username">Όνομα χρήστη:</label></td>
<td style="text-align:left;">
<input type="text" name="username" id="username" maxlength="50" required="required" />
<input type="text" name="username" id="username" pattern=".{8,}" maxlength="50" required="required" />
</td>
</tr>
<tr>
<td style="text-align:right;"><label for="password">Κωδικός πρόσβασης:</label></td>
<td style="text-align:left;">
<input type="password" name="password" id="password" maxlength="50" required="required" />
<input type="password" name="password" id="password" pattern=".{8,}" maxlength="50" required="required" />
</td>
</tr>
</tbody>
......
......@@ -14,3 +14,5 @@
- https://www.jsdelivr.com/
* <label> element usage:
- https://stackoverflow.com/questions/7636502/why-use-label
* Minimum length constraint for <input> elements:
- https://stackoverflow.com/questions/10281962/is-there-a-minlength-validation-attribute-in-html5
......@@ -33,7 +33,7 @@
<nav id="home_nav">
{% comment %}
The "Edit Profile" and "Log out" options are ALWAYS available to the user. ALL the OTHER
navigation options are available to him/her ONLY if he/she has filled out his/her profile.
navigation options are available to him/her ONLY if his/her profile is NON-empty.
{% endcomment %}
<p><a href="{% url 'default_user_profile_edit' user.id %}">Επεξεργασία Προφίλ</a></p>
{% if user.first_name != '' and user.last_name != '' and user.email is not None %}
......
......@@ -59,7 +59,7 @@
<tr>
<td style="text-align:right;"><label for="email">Ηλεκτρονικό ταχυδρομείο:</label></td>
<td style="text-align:left;">
<input type="email" name="email" id="email" maxlength="254"
<input type="email" name="email" id="email" maxlength="100"
{% if user.email is not None %}value="{{ user.email }}"{% endif %} required="required" />
</td>
</tr>
......
......@@ -35,7 +35,7 @@
<tr>
<td style="text-align:right;"><label for="email">Ηλεκτρονικό ταχυδρομείο:</label></td>
<td style="text-align:left;">
<input type="email" name="email" id="email" maxlength="254" required="required" />
<input type="email" name="email" id="email" maxlength="100" required="required" />
</td>
</tr>
</tbody>
......
......@@ -15,7 +15,7 @@ Including another URLconf
"""
from django.urls import re_path
from usermerge import views
from . import views
urlpatterns = [
# The default login page serves as index page. Therefore, its regular route becomes r'^$' instead of r'^login/default$'.
......
from django.core.exceptions import ValidationError
from usermerge.models import Platform
from .helper import get_all_platform_names_from_DB
from .models import Platform
# Create your validators here.
# https://docs.djangoproject.com/en/2.0/ref/validators/
# The below defined validators are mainly utilized by the application (programmatic) forms (see forms.py).
# The validators of a form field are run in the context of run_validators() method when the clean() method of the field is called. If the field value is empty, i.e. None, '', [], () or {}, run_validators() will NOT run the validators (see https://docs.djangoproject.com/en/2.0/_modules/django/forms/fields/). Therefore, it can be safely assumed that the single arguments of the following validators are NEVER empty.
# For more information on the platform names that are available to select from in forms, see get_names_of_SoftLab_provided_platforms_from_DB() function of helper.py . In the case of login form, see also login_form of login.html .
def platform_is_selected_from_provided_list(platform):
def platform_is_SLUB_or_exists_in_DB(platform):
"""
Ensure that the platform (name) is selected among the ones provided in the corresponding form drop-down list.
If it is not (e.g. it is misedited via JavaScript), raise a ValidationError exception with the appropriate error message and code.
Ensure that the platform (name) is selected among the ones provided in the corresponding form drop-down list (this list includes SLUB
as well as the platforms that exist in usermergeDB). If it is not (e.g. it is misedited via JavaScript), raise a ValidationError
exception with the appropriate error message and code.
"""
if platform != 'SLUB' and not (platform != 'ECE-NTUA' and platform in Platform.objects.values_list('name', flat = True)):
if platform != 'SLUB' and not platform in get_all_platform_names_from_DB():
raise ValidationError('Το όνομα πλατφόρμας πρέπει να επιλέγεται μεταξύ εκείνων\n'
'που παρέχονται στην αντίστοιχη αναπτυσσόμενη λίστα!', code = 'platform_is_not_selected_from_provided_list')
'που παρέχονται στην αντίστοιχη αναπτυσσόμενη λίστα!', code = 'platform_is_not_SLUB_and_does_not_exist_in_DB')
def ece_id_is_not_031YY000(ece_id):
"""
......
......@@ -6,30 +6,33 @@ from django.core.exceptions import PermissionDenied
# from django.db.models import Q
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from usermerge.auth import login, PLATFORM_SESSION_KEY
from usermerge.helper import (
get_form_error_messages, get_names_of_SoftLab_provided_platforms_from_DB, _display_user_profile_edit_error_messages,
from .auth import login, PLATFORM_SESSION_KEY
from .decorators import Admin_login_required, User_login_required
from .forms import LoginForm, UserProfileEditForm, UserProfileRecoveryForm
from .helper import (
get_all_platform_names_from_DB, get_form_error_messages, _display_user_profile_edit_error_messages,
_display_user_profile_recovery_error_messages, _display_user_profile_recovery_success_message, _update_user_profile
)
from usermerge.forms import LoginForm, UserProfileEditForm, UserProfileRecoveryForm
from usermerge.models import Platform, Registry, User
from .models import Registry, User
# Create your views here.
# https://docs.djangoproject.com/en/2.0/topics/http/views/
# For more information on configuring and using the logging system, see https://docs.djangoproject.com/en/2.0/topics/logging/ and the 'Logging' part of settings.py .
# For more information on default/customizing user authentication and password management, see https://docs.djangoproject.com/en/2.0/topics/auth/, auth.py and backends.py .
# For more information on configuring and using the logging system, see https://docs.djangoproject.com/en/2.0/topics/logging/ and the "Logging" part of settings.py .
# For more information on default/customizing user authentication and password management, see https://docs.djangoproject.com/en/2.0/topics/auth/, auth.py, backends.py and AuthenticationMiddleware of middleware.py .
# For more information on making database queries, i.e. creating, retrieving, updating and deleting model instances, see https://docs.djangoproject.com/en/2.0/topics/db/queries/ .
# For more information on working with forms, see https://docs.djangoproject.com/en/2.0/topics/forms/ .
# For more information on using TemplateResponse objects - instead of standard HttpResponse objects and calls to the render() shortcut function - to display application pages/templates, see https://docs.djangoproject.com/en/2.0/ref/template-response/ .
# For more information on the possible/valid (field) states of a user profile (User instance) in usermergeDB and the allowed/consistent transitions between them, see models.py . It is worth mentioning that a user profile is empty if its email is None and vice versa.
# A user whose model is User and profile is non-empty should have access to all the User-targeted, i.e. User_login_required()-decorated, views except for the user profile recovery ones. On the other hand, a user whose model is User and profile is empty should have access only to the User-targeted views that relate to the user home page and the user profile edit/recovery. Therefore, he/she should fill out his/her profile or recover a non-empty one from usermergeDB first to gain access to all the other User-targeted views (these views should ensure that only non-empty user profiles are allowed access) - if this was done, he/she would have access to all the User-targeted views except for the user profile recovery ones. Finally, a user whose model is Admin should have access to all the Admin-targeted, i.e. Admin_login_required()-decorated, views.
# The below defined views are not targeted at the default user model but at the application user models (see the _get_user_model_label() function of auth.py), i.e. the views can be accessed only by usermerge.{User/Admin} instances (the "usermerge." prefix is usually omitted in the application context) and not by auth.User ones - see the User_login_required() and Admin_login_required() decorators. Furthermore, the user instances (including the session one) that are created, retrieved, updated or deleted in the context of each view are usermerge.{User/Admin} instances. On the other hand, the admin site (see https://docs.djangoproject.com/en/2.0/ref/contrib/admin/) is not targeted at the application user models but at the default user model as only auth.User instances can be active staff members - each one has the is_active and is_staff attributes and the latter are set to True - and also have permissions - each one has the is_superuser, groups and user_permissions attributes as well as the relative methods, e.g. get_group_permissions(), get_all_permissions(), etc. - to access the site and utilize some or all of its capabilities. The admin site should generally be used for retrieving/reviewing model instances and not for creating, updating or deleting them - the latter actions should better be performed by appropriately defined views. When trying to create, update or delete instances via the admin site, extra care should be given to retain consistency in usermergeDB, e.g. a Platform instance could be created without any problem, while the updated password of an Admin instance would wrongly be stored in raw format (without being hashed first), etc. (for more information, see models.py).
def display_default_login_page(request):
"""
Display the login page in default mode, i.e. without any form-submission-related messages.
The default login page is the index page of the usermerge application.
"""
return TemplateResponse(request, 'login.html', {'platform_names': get_names_of_SoftLab_provided_platforms_from_DB()})
return TemplateResponse(request, 'login.html', {'platform_names': get_all_platform_names_from_DB()})
def log_in(request):
"""
......@@ -54,29 +57,29 @@ def log_in(request):
return redirect('user_home', user.id)
else:
return TemplateResponse(request, 'login.html',
{'platform_names': get_names_of_SoftLab_provided_platforms_from_DB(),
{'platform_names': get_all_platform_names_from_DB(),
'error_messages': ['Τα δηλωθέντα στοιχεία εισόδου δεν ανταποκρίνονται σε κανέναν χρήστη της βάσης!\n'
'Παρακαλούμε ελέγξτε την ορθότητα των στοιχείων εισόδου και δοκιμάστε ξανά!'],
'post_validation_error_codes': ['non_existent_credentials_pair_for_selected_login_platform']})
else:
return TemplateResponse(request, 'login.html', {'platform_names': get_names_of_SoftLab_provided_platforms_from_DB(),
return TemplateResponse(request, 'login.html', {'platform_names': get_all_platform_names_from_DB(),
'error_messages': get_form_error_messages(form)})
@login_required(redirect_field_name = None) # The session user's model should be User.
@User_login_required
def display_user_home_page(request, user_id):
"""
Display the user home page.
"""
return TemplateResponse(request, 'user_home.html', {})
@login_required(redirect_field_name = None) # The session user's model should be User.
@User_login_required
def display_default_user_profile_edit_page(request, user_id):
"""
Display the user profile edit page in default mode, i.e. without any form-submission-related messages.
"""
return TemplateResponse(request, 'user_profile_edit.html', {})
@login_required(redirect_field_name = None) # The session user's model should be User.
@User_login_required
def edit_user_profile(request, user_id):
"""
Collect the data, i.e. first name, last name, ece_id and email, POSTed via the user profile edit form and validate them. If they
......@@ -227,7 +230,7 @@ def edit_user_profile(request, user_id):
else: # not form.is_valid()
return TemplateResponse(request, 'user_profile_edit.html', {'error_messages': get_form_error_messages(form)})
@login_required(redirect_field_name = None) # The session user's model should be User.
@User_login_required
def display_default_user_profile_recovery_page(request, user_id):
"""
Check if the session user's profile in usermergeDB is empty. If it is not, display an HTTP 403 (Forbidden) page by raising a
......@@ -242,7 +245,7 @@ def display_default_user_profile_recovery_page(request, user_id):
else:
return TemplateResponse(request, 'user_profile_recovery.html', {})
@login_required(redirect_field_name = None) # The session user's model should be User.
@User_login_required
def search_for_recovery_user_profile(request, user_id):
"""
Check if the session user's profile in usermergeDB is empty. If it is not, display an HTTP 403 (Forbidden) page by raising a
......@@ -354,7 +357,7 @@ def search_for_recovery_user_profile(request, user_id):
else: # not form.is_valid()
return TemplateResponse(request, 'user_profile_recovery.html', {'error_messages': get_form_error_messages(form)})
@login_required(redirect_field_name = None) # The session user's model should be User.
@User_login_required
def recover_user_profile(request, session_user_id, recov_user_id):
"""
Check if the session user's profile in usermergeDB is empty. If it is not, display an HTTP 403 (Forbidden) page by raising a
......@@ -376,28 +379,28 @@ def recover_user_profile(request, session_user_id, recov_user_id):
if session_user.first_name != '' and session_user.last_name != '' and session_user.email is not None:
raise PermissionDenied
else:
recov_user = User.objects.get(pk = recov_user_id)
login_platform = Platform.objects.get(name = request.session[PLATFORM_SESSION_KEY])
session_registry = Registry.objects.get(user = session_user, platform = login_platform)
prev_recov_username = None
login_platform_name = request.session[PLATFORM_SESSION_KEY]
session_registry = Registry.objects.get(user = session_user, platform__name = login_platform_name)
prev_recov_username = ''
# The changes in usermergeDB should be made in the following order:
# * Due to the user-platform unique key constraint of the Registry model, the recovery registry should be deleted if it exists
# before the session registry is updated to reference the recovery user.
# * Due to the user foreign key constraint of the Registry model, the session registry should be updated to reference the
# recovery user (profile) before the session user's profile is deleted.
try:
recov_registry = Registry.objects.get(user = recov_user, platform = login_platform)
recov_registry = Registry.objects.get(user_id = recov_user_id, platform__name = login_platform_name)
except Registry.DoesNotExist:
pass
else:
prev_recov_username = recov_registry.username
recov_registry.delete()
session_registry.user = recov_user
session_registry.user_id = recov_user_id
session_registry.save(update_fields = ['user'])
session_user.delete()
if prev_recov_username:
view_logger.info('user_id: %d | platform: %s (id: %d) | old_username: %s | new_username: %s',
recov_user.id, login_platform.name, login_platform.id, prev_recov_username, session_registry.username)
recov_user_id, login_platform_name, session_registry.platform_id,
prev_recov_username, session_registry.username)
# On the one hand, the logout() authentication function emits a user_logged_out signal that, among others, provides
# the corresponding handler functions with the logged-out user's profile. On the other hand, the session user's profile
# has been deleted from usermergeDB and cannot be accessed by any function. Therefore, instead of calling the log_out()
......@@ -408,14 +411,14 @@ def recover_user_profile(request, session_user_id, recov_user_id):
request.user = AnonymousUser()
return redirect('default_login')
@login_required(redirect_field_name = None) # The session user's model should be Admin.
@Admin_login_required
def display_admin_home_page(request, user_id):
"""
Display the admin home page.
"""
return TemplateResponse(request, 'admin_home.html', {})
@login_required(redirect_field_name = None) # The session user's model can be either User or Admin.
@login_required(redirect_field_name = None, login_url = 'default_login') # The session user's model can be either User or Admin.
def log_out(request):
"""
Log the session user out and redirect him/her to the default login page.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment