Commit e324d683 authored by Giorgos Kazelidis's avatar Giorgos Kazelidis

- Implemented the user profile edit, search and recovery tasks

- Created and used a library of helper functions that refer (mainly) to views
- Created and used a library of validators that refer (mainly) to forms
- Corrected/enhanced the existing views - used TemplateResponse objects instead of calling the render() shortcut function, inserted post-validation error codes in template contexts when needed, etc.
- Deleted the logout template and used redirection to the login template on logout
- Corrected the generic URL format
- Enhanced the documentation of templates and modules
parent e6f4aa1b
......@@ -136,7 +136,7 @@
(C\) 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/usermerge/login/default/ via web browser to run the project
(D) navigate to http://127.0.0.1:8000/ via web browser to run the project
## STOPPING THE PROJECT (AND EXITING THE VIRTUAL ENVIRONMENT)
(A) stop the Django development server (running at http://127.0.0.1:8000/):
......
......@@ -91,7 +91,7 @@ LOGIN_URL = 'default_login'
# - The module provides an optional multi-threaded queue logging feature to perform logging in the background. The feature requires Python 3 to run and uses the standard QueueHandler and QueueListener logging classes to create background threads (one for each logger with configured handlers) that handle the logging, thus letting the foreground threads, i.e. those who initiate the logging, handle the requests quickly without blocking for write locks on the log files. It should be used after the logging configuration is set up. The latter is set up during the (usermerge) application setup, the application is set up each time it is loaded and, in production, it is loaded each time a child process is created by the main Apache process (see wsgi.py, https://github.com/django/django/blob/master/django/core/wsgi.py , https://github.com/django/django/blob/master/django/__init__.py and https://modwsgi.readthedocs.io/en/develop/user-guides/processes-and-threading.html). Since the logging configuration is set up for each child process created by the main Apache process, the feature would create too many background threads that could potentially interfere with the operation of Apache and mod_wsgi, which are responsible for managing processes and threads in production, in unexpected ways (https://groups.google.com/forum/#!topic/modwsgi/NMgrti4o9Bw). Therefore, it is recommended that the feature is NOT used, especially in production.
# * Brief summary of the below defined configuration:
# - During development (the DEBUG setting is True), ALL log records/messages are output to the console/terminal (sys.stderr stream) where the Django development server has been run via the runserver command.
# - In production (the DEBUG setting is False), ALL log records/messages, EXCEPT FOR the DEBUG ones, are output to log files that exist in the production_logs directory of the usermerge application (directory). General INFO and WARNING messages are output to general_info_and_warnings.log, ERROR and CRITICAL messages are output to errors.log, while DEBUG messages are NOT output at all. Since the Django development server is inactive, NO messages are logged on the django.server logger for output to the sys.stderr stream. It is worth mentioning that mod_wsgi intercepts the sys.stdout and sys.stderr streams and redirects the output to the Apache error log (https://modwsgi.readthedocs.io/en/develop/user-guides/debugging-techniques.html).
# - In production (the DEBUG setting is False), ALL log records/messages, EXCEPT FOR the DEBUG ones, are output to log files that exist in the production_logs directory of the usermerge application (directory). General INFO and WARNING messages are output to general_info_and_warnings.log, ERROR and CRITICAL messages are output to errors.log and INFO messages logged on the usermerge.views.recover_user_profile logger are output to changes_of_credentials_during_profile_recovery.log, while DEBUG messages are NOT output at all. Since the Django development server is inactive, NO messages are logged on the django.server logger for output to the sys.stderr stream. It is worth mentioning that mod_wsgi intercepts the sys.stdout and sys.stderr streams and redirects the output to the Apache error log (https://modwsgi.readthedocs.io/en/develop/user-guides/debugging-techniques.html).
# * Detailed configuration and usage examples: https://www.webforefront.com/django/setupdjangologging.html
def LOG_LEVEL_IS_LOWER_THAN_ERROR(log_record):
......@@ -169,6 +169,15 @@ LOGGING = {
'backupCount': 5,
'formatter': 'verbose',
},
'file_for_changes_of_credentials_during_profile_recovery': {
'level': 'INFO',
'filters': ['require_debug_false'],
'class': 'logging.handlers.ConcurrentRotatingFileHandler',
'filename': os.path.join(BASE_DIR, 'usermerge/production_logs/changes_of_credentials_during_profile_recovery.log'),
'maxBytes': 1024 * 20, # 20 KB
'backupCount': 5,
'formatter': 'laconic',
},
},
'root': {
'level': 'DEBUG' if DEBUG else 'INFO',
......@@ -190,6 +199,11 @@ LOGGING = {
'handlers': ['console', 'file_for_general_info_and_warnings', 'file_for_errors'],
'propagate': False,
},
'usermerge.views.recover_user_profile': {
'level': 'INFO',
'handlers': ['console', 'file_for_changes_of_credentials_during_profile_recovery'],
'propagate': False,
},
},
}
......
......@@ -13,10 +13,11 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('admin/', admin.site.urls),
path('usermerge/', include('usermerge.urls')),
path('', include('usermerge.urls')),
]
......@@ -10,8 +10,9 @@ from django.middleware.csrf import rotate_token
from django.utils.crypto import constant_time_compare
from usermerge.models import Registry
# For more information on default/customizing user authentication and password management, see https://docs.djangoproject.com/en/2.0/topics/auth/ .
# 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.
SESSION_KEY = '_auth_user_id'
PLATFORM_SESSION_KEY = '_auth_platform'
......@@ -50,7 +51,7 @@ def _get_user_session_key(request):
def login(request, user, backend = None):
"""
Persist a user id and a backend in the request. This way a user doesn't have to reauthenticate on every request.
Persist a user id and a backend in the request. This way a user does not have to reauthenticate on every request.
Note that data set during the anonymous session is retained when the user logs in.
"""
platform_name = request.POST['platform']
......
......@@ -4,13 +4,12 @@ from usermerge.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/ .
# For more information on the platform names that are provided in the drop-down list of the login form, see login.html and get_names_of_SoftLab_provided_platforms_from_DB() function of views.py .
# For more information on default/customizing user authentication and password management, see https://docs.djangoproject.com/en/2.0/topics/auth/ and auth.py .
class UserBackend:
def authenticate(self, request, platform = None, username = None, password = None):
"""
Authenticate the credentials POSTed via the login form.
Authenticate the provided credentials, i.e. username and password, for the selected platform.
If the selected platform is SLUB, the user should be authenticated as Admin with the happy path being:
* Try to find an Admin instance of usermergeDB that corresponds to the provided username.
......@@ -24,15 +23,15 @@ class UserBackend:
* If the provided password matches the one of the instance, the user is authenticated successfully as User and
the User instance that is associated with the corresponding Registry instance is returned.
If any of the checks (finding the corresponding instance and comparing the passwords) fails, the authentication
fails and an empty instance (None) is implicitly returned (if the first check fails, the second is never performed).
If any of the checks (finding the corresponding instance and comparing the passwords) fails, the authentication fails
and an empty instance (None) is implicitly returned (if the first check fails, the second one is never performed).
"""
if platform == 'SLUB':
# Admin authentication
try:
admin = Admin.objects.get(username = username)
except Admin.DoesNotExist:
# Run the default password hasher once to reduce the timing difference between an existent and
# Run the default password hasher once to reduce the timing difference between an existing and
# a non-existent Admin instance (#20760), as done in ModelBackend's authenticate() method
# (https://github.com/django/django/blob/master/django/contrib/auth/backends.py).
dummy_password = make_password(password)
......@@ -44,8 +43,8 @@ class UserBackend:
try:
registry = Registry.objects.get(platform__name = platform, username = username)
except Registry.DoesNotExist:
# Run the default password hasher once to reduce the timing difference between an existent and
# a non-existent User instance (#20760), as done in ModelBackend's authenticate() method
# Run the default password hasher once to reduce the timing difference between an existing and
# a non-existent Registry instance (#20760), as done in ModelBackend's authenticate() method
# (https://github.com/django/django/blob/master/django/contrib/auth/backends.py).
dummy_password = make_password(password)
else:
......@@ -54,8 +53,8 @@ class UserBackend:
def get_user(self, user_model_label, user_id):
"""
Return the user instance associated with the given model and id (if model is neither User nor Admin, raise a ValueError exception).
If no user is retrieved, return an empty instance (None).
Return the user instance associated with the given model and id (if model is neither User nor Admin, raise a ValueError exception
with the appropriate error message). If no user is retrieved, return an empty instance (None).
"""
if user_model_label == 'usermerge.User':
try:
......@@ -69,5 +68,5 @@ class UserBackend:
return None
else:
raise ValueError('The user_model_label argument of get_user() backend method '
'should either be "usermerge.User" or "usermerge.Admin"!')
'should be the "usermerge.User" or "usermerge.Admin" string!')
import re
from django import forms
from usermerge.models import Platform
from usermerge.validators import platform_is_selected_from_provided_list, ece_id_is_not_031YY000
# Create your forms here.
# https://docs.djangoproject.com/en/2.0/ref/forms/
# The below-defined forms are based on the models defined in models.py .
# For more information on clean_<fieldname>() and clean() methods, see https://docs.djangoproject.com/en/2.0/ref/forms/validation/ .
# For more information on the platform names that are provided in the drop-down list of the login form, see login.html and get_names_of_SoftLab_provided_platforms_from_DB() function of views.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': 'Το δηλωθέν όνομα πλατφόρμας δεν ανταποκρίνεται σε καμία από τις '
'υποστηριζόμενες πλατφόρμες της βάσης!'})
platform = forms.CharField(error_messages = {'required': 'Το όνομα πλατφόρμας πρέπει απαραίτητα να επιλεχθεί και να είναι μη-κενό!'},
validators = [platform_is_selected_from_provided_list])
username = forms.CharField(max_length = 50,
error_messages = {'required': 'Το όνομα χρήστη πρέπει απαραίτητα να συμπληρωθεί!',
'max_length': 'Το όνομα χρήστη δεν πρέπει να υπερβαίνει τους 50 χαρακτήρες!'})
......@@ -19,27 +17,47 @@ class LoginForm(forms.Form):
error_messages = {'required': 'Ο κωδικός πρόσβασης πρέπει απαραίτητα να συμπληρωθεί!',
'max_length': 'Ο κωδικός πρόσβασης δεν πρέπει να υπερβαίνει τους 50 χαρακτήρες!'})
def clean_platform(self):
platform = self.cleaned_data['platform']
# Ensure that the platform (name) is selected among the provided ones in the drop-down list of the login form.
# If it is not (e.g. it is misedited via JavaScript), raise a ValidationError that will be printed under the form.
if platform == 'SLUB' or (platform != 'ECE-NTUA' and platform in Platform.objects.values_list('name', flat = True)):
return platform
else:
raise forms.ValidationError('Το δηλωθέν όνομα πλατφόρμας δεν ανταποκρίνεται σε καμία '
'από τις υποστηριζόμενες πλατφόρμες της βάσης!')
def clean(self):
cleaned_data = super().clean()
# If platform and username have survived the initial individual field checks (by the time the form’s clean() 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 piYYbRRR (if it is not, raise a
# ValidationError that will be printed under the login form), where YY refers to year and RRR refers to username serial number.
# 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 not re.match('pi[0-9]{2}b[0-9]{3}', username):
raise forms.ValidationError('Το όνομα χρήστη στις πλατφόρμες Novice και Grader πρέπει να είναι '
'της μορφής piYYbRRR (ΥΥ: έτος, RRR: σειριακός αριθμός ονόματος)!')
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': 'Το όνομα πρέπει απαραίτητα να συμπληρωθεί!',
'max_length': 'Το όνομα δεν πρέπει να υπερβαίνει τους 50 χαρακτήρες!'})
last_name = forms.CharField(max_length = 50,
error_messages = {'required': 'Το επώνυμο πρέπει απαραίτητα να συμπληρωθεί!',
'max_length': 'Το επώνυμο δεν πρέπει να υπερβαίνει τους 50 χαρακτήρες!'})
ece_id = forms.RegexField(required = False, max_length = 8, regex = '031[0-9]{5}',
# The empty value '' (empty string) becomes None in order to be interpreted as NULL in usermergeDB.
empty_value = None, error_messages = {'max_length': 'Ο αριθμός μητρώου πρέπει να είναι της μορφής '
'031YYSSS (YY: έτος, SSS: αύξων αριθμός)!',
'invalid': 'Ο αριθμός μητρώου πρέπει να είναι της μορφής '
'031YYSSS (YY: έτος, SSS: αύξων αριθμός)!'},
validators = [ece_id_is_not_031YY000])
email = forms.EmailField(max_length = 254,
error_messages = {'required': 'Το ηλεκτρονικό ταχυδρομείο πρέπει απαραίτητα να συμπληρωθεί!',
'max_length': 'Το ηλεκτρονικό ταχυδρομείο δεν πρέπει να υπερβαίνει τους 254 χαρακτήρες!',
'invalid': 'Το ηλεκτρονικό ταχυδρομείο πρέπει να είναι της μορφής local-part@domain που\n'
'περιγράφεται στην ιστοσελίδα https://en.wikipedia.org/wiki/Email_address !'})
class UserProfileRecoveryForm(UserProfileEditForm):
first_name = None
last_name = None
"""
Set of helper functions that contribute to the correct execution and enhanced structure/modularity of major module functions (e.g. views).
"""
from django import forms
from django.template.response import TemplateResponse
from usermerge.auth import PLATFORM_SESSION_KEY
from usermerge.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():
"""
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.
"""
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
def get_form_error_messages(form):
"""
Check if the given form is an instance of "django.forms.forms.Form" class. If it is, return the list of all the ValidationError
exception messages that are associated with its fields - if the form is not bound to any set of data, i.e. form.is_bound returns
False, or the (bound) form field validation succeeds, i.e. form.is_valid() returns True, the list will be empty and vice versa.
Otherwise, raise a TypeError exception with the appropriate error message.
"""
if isinstance(form, forms.Form):
form_error_values = list(form.errors.values())
return [form_error_values[x][y] for x in range(len(form_error_values)) for y in range(len(form_error_values[x]))]
else:
raise TypeError('The form argument of get_form_error_messages() helper function '
'should be an instance of "django.forms.forms.Form" class!')
def _update_user_profile(request, changed_data, cleaned_data):
"""
[Acts as inner function of the edit_user_profile() view] Check which fields of the user profile edit form have been changed, update
the session user's profile in usermergeDB with the corresponding validated (cleaned) and adequate values and display the updated
profile along with the appropriate success message in the user profile edit page.
"""
session_user = request.user
if 'first_name' in changed_data:
session_user.first_name = cleaned_data['first_name']
if 'last_name' in changed_data:
session_user.last_name = cleaned_data['last_name']
if 'ece_id' in changed_data:
session_user.ece_id = cleaned_data['ece_id']
if 'email' in changed_data:
session_user.email = cleaned_data['email']
session_user.save(update_fields = changed_data)
request.user = session_user # Update the profile in the user profile edit page via the associated request.
return TemplateResponse(request, 'user_profile_edit.html',
{'success_message': 'Το προφίλ σας ενημερώθηκε επιτυχώς με χρήση των τροποποιηθέντων στοιχείων!'})
def _display_user_profile_edit_error_messages(request, error_data):
"""
[Acts as inner function of the edit_user_profile() view] Create the appropriate post-validation error messages by using the
validated (cleaned), yet inadequate, ece_id or/and email values of the user profile edit form along with the corresponding
error codes (in the registered_ece_id_was_cleared code case, the validated ece_id is None and the non-None session user's
ece_id is used instead), display them in the user profile edit page and include the error codes in the page context.
"""
error_messages = []
error_codes = []
# The duplicate_ece_id and registered_ece_id_was_cleared errors can never occur both at the same time.
for code in error_data:
if code == 'duplicate_ece_id':
error_messages.append('O δηλωθείς αριθμός μητρώου <' + error_data[code] + '> '
'έχει ήδη καταχωρηθεί από άλλο χρήστη της βάσης!\n'
'Παρακαλούμε ελέγξτε την ορθότητα του αριθμού μητρώου και δοκιμάστε ξανά!')
error_codes.append(code)
elif code == 'duplicate_email':
error_messages.append('Το δηλωθέν ηλεκτρονικό ταχυδρομείο <' + error_data[code] + '> '
'έχει ήδη καταχωρηθεί από άλλο χρήστη της βάσης!\n'
'Παρακαλούμε ελέγξτε την ορθότητα του ηλεκτρονικού ταχυδρομείου και δοκιμάστε ξανά!')
error_codes.append(code)
else: # code == 'registered_ece_id_was_cleared'
error_messages.append('Στο προφίλ σας είναι ήδη καταχωρημένος o αριθμός μητρώου <' + error_data[code] + '>, '
'ο οποίος επιτρέπεται\n'
'μεν να τροποποιηθεί έγκυρα αλλά απαγορεύεται να διαγραφεί οριστικά!')
error_codes.append(code)
return TemplateResponse(request, 'user_profile_edit.html', {'error_messages': error_messages,
'post_validation_error_codes': error_codes})
def _display_user_profile_recovery_success_message(request, recov_user):
"""
[Acts as inner function of the search_for_recovery_user_profile() view] Display the appropriate success message in the user profile
recovery page by taking the recovery user profile (non-empty User instance that corresponds to both the validated - cleaned - and
adequate ece_id and email values of the corresponding form) into account. This message presents the (empty) session user with all the
field values of the aforementioned profile and informs him/her that if he/she proceeds with the profile recovery, he/she will get
logged out after his/her credentials for the login platform have first been associated with this profile (they will replace any
previously associated ones). Finally, include the recovery profile in the page context to assist the recovery procedure - see the
recover_user_profile() view.
"""
success_message = ('Τα δηλωθέντα στοιχεία εντοπίστηκαν επιτυχώς στη βάση και παραπέμπουν στο εξής καταχωρημένο προφίλ:\n\n'
'Όνομα: %s\n'
'Επώνυμο: %s\n'
'Αριθμός μητρώου: %s\n'
'Ηλεκτρονικό ταχυδρομείο: %s\n\n'
'Αν επιθυμείτε τη συσχέτιση του προαναφερθέντος προφίλ με τα διαπιστευτήρια εισόδου σας ως χρήστης\n'
'του %s (σε περίπτωση αλλαγμένων διαπιστευτηρίων, τα προϋπάρχοντα θα διαγραφούν από τη βάση),\n'
'παρακαλούμε ανακτήστε τα παραπάνω στοιχεία και εξέλθετε από το σύστημα!'
) % (recov_user.first_name, recov_user.last_name,
'[Δεν έχει καταχωρηθεί]' if recov_user.ece_id is None else recov_user.ece_id,
recov_user.email, request.session[PLATFORM_SESSION_KEY])
return TemplateResponse(request, 'user_profile_recovery.html', {'success_message': success_message, 'recov_user': recov_user})
def _display_user_profile_recovery_error_messages(request, error_data):
"""
[Acts as inner function of the search_for_recovery_user_profile() view] Create the appropriate post-validation error messages
by using the validated (cleaned), yet inadequate, ece_id or/and email values of the user profile recovery form along with the
corresponding error codes, display them in the user profile recovery page and include the error codes in the page context.
"""
error_messages = []
error_codes = []
# The ece_id_and_email_exist_in_different_profiles error can only occur by itself. This means that if it occurs,
# neither of the non_existent_ece_id and non_existent_email errors can occur at the same time.
for code in error_data:
if code == 'non_existent_ece_id':
if error_data[code] is None:
error_messages.append('Στη βάση δεν εντοπίστηκε κανένα προφίλ χρήστη που να συνδυάζει "κενό" αριθμό μητρώου\n'
'με καταχωρημένο ηλεκτρονικό ταχυδρομείο! Παρακαλούμε δοκιμάστε ξανά με έναν\n'
'έγκυρο οκταψήφιο αριθμό μητρώου!')
else:
error_messages.append('Ο δηλωθείς αριθμός μητρώου <' + error_data[code] + '> δεν είναι καταχωρημένος στη βάση!\n'
'Παρακαλούμε ελέγξτε την ορθότητα του αριθμού μητρώου και δοκιμάστε ξανά!')
error_codes.append(code)
elif code == 'non_existent_email':
error_messages.append('Το δηλωθέν ηλεκτρονικό ταχυδρομείο <' + error_data[code] + '> δεν είναι καταχωρημένο στη βάση!\n'
'Παρακαλούμε ελέγξτε την ορθότητα του ηλεκτρονικού ταχυδρομείου και δοκιμάστε ξανά!')
error_codes.append(code)
else: # code == 'ece_id_and_email_exist_in_different_profiles'
if error_data[code]['ece_id'] is None:
error_messages.append('Το δηλωθέν ηλεκτρονικό ταχυδρομείο <' + error_data[code]['email'] + '> '
'βρέθηκε καταχωρημένο στη βάση,\n'
'ενώ σε αυτήν εντοπίστηκε και (τουλάχιστον ένα) προφίλ χρήστη που συνδυάζει '
'"κενό" αριθμό μητρώου με\n'
'καταχωρημένο ηλεκτρονικό ταχυδρομείο! Η αναζήτηση, όμως, απέτυχε καθώς δεν εντοπίστηκε κανένα\n'
'προφίλ χρήστη που να συνδυάζει το δηλωθέν ηλεκτρονικό ταχυδρομείο με "κενό" αριθμό μητρώου!')
else:
error_messages.append('Ο αριθμός μητρώου <' + error_data[code]['ece_id'] + '> και '
'το ηλεκτρονικό ταχυδρομείο <' + error_data[code]['email'] + '> που δηλώθηκαν\n'
'είναι μεν καταχωρημένα στη βάση, αλλά η αναζήτηση απέτυχε καθώς δεν εντοπίστηκε '
'κανένα προφίλ χρήστη\n'
'που να τα συνδυάζει!')
error_codes.append(code)
return TemplateResponse(request, 'user_profile_recovery.html', {'error_messages': error_messages,
'post_validation_error_codes': error_codes})
......@@ -6,7 +6,7 @@ from usermerge import auth
# Create your middleware here.
# https://docs.djangoproject.com/en/2.0/topics/http/middleware/
# AuthenticationMiddleware is a middleware component that associates the current session 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).
# 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).
def get_user(request):
if not hasattr(request, '_cached_user'):
......
......@@ -4,18 +4,19 @@ from django.utils.crypto import salted_hmac
# 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, see forms.py and views.py .
# 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 .
# 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 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 but it should NEVER become empty.
# * ALL the fields of a Registry instance should be NON-empty. The user (user_id), username and password values can be changed validly (the platform - platform_id - value should NEVER be changed) but they should NEVER become empty.
# * ALL the fields of an Admin instance should be NON-empty. Their values can be changed validly but they should NEVER become empty.
# * 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, 3-->2 and 3-->3 (empty field values can be changed to valid non-empty ones, non-empty field values can be changed validly but they should NEVER become empty).
# * 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).
# * 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).
# * 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/ .
# Given that support for time zones is enabled (USE_TZ setting is True), DateTimeField values are stored in UTC format in usermergeDB and are rendered 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 render format, see https://docs.djangoproject.com/en/2.0/topics/i18n/timezones/ and the 'Internationalization' part of settings.py .
# 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/).
class User(models.Model):
......
......@@ -6,7 +6,7 @@ from django.contrib.auth.hashers import make_password
from usermerge.models import Admin, Platform, Registry, User
# Populate Admin table with test admins.
admin_hashed_password = make_password('sesame')
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',
......@@ -20,6 +20,7 @@ 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.
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')
......
{% extends 'base.html' %}
{% block title %}
SLUB - Είσοδος Χρήστη/Διαχειριστή
SLUB - Είσοδος
{% endblock %}
{% block content %}
<h4>Είσοδος Χρήστη/Διαχειριστή</h4>
<h4>Είσοδος</h4>
<form id="login_form" accept-charset="utf-8" action="{% url 'submit_login' %}" method="post">
{% csrf_token %}
......@@ -28,13 +28,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" title="" required="required" />
<input type="text" name="username" id="username" 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" title="" required="required" />
<input type="password" name="password" id="password" maxlength="50" required="required" />
</td>
</tr>
</tbody>
......
{% extends 'base.html' %}
{% block title %}
SLUB - Έξοδος Χρήστη/Διαχειριστή
{% endblock %}
{% block content %}
<h4>Έξοδος Χρήστη/Διαχειριστή</h4>
<p>Εξήλθατε με επιτυχία!</p>
<p><a href="{% url 'default_login' %}">Θα θέλατε μήπως να εισέλθετε ξανά;</a></p>
{% endblock %}
* W3Schools - HTML5 Tutorial: https://www.w3schools.com/Html/default.asp
* Django References - Templates: https://docs.djangoproject.com/en/2.0/ref/templates/
* HTML5, CSS, JavaScript and XML tutorials:
- https://www.w3schools.com/
- https://developer.mozilla.org/en-US/docs/Web
* XHTML5 (polyglot markup) usage:
- https://wiki.whatwg.org/wiki/HTML_vs._XHTML
- http://xmlplease.com/xhtml/xhtml5polyglot/
* Django API reference for templates:
- https://docs.djangoproject.com/en/2.0/ref/templates/
* Unicode data support:
- https://stackoverflow.com/questions/38363566/trouble-with-utf-8-characters-what-i-see-is-not-what-i-stored
* <meta> element usage:
- https://moz.com/blog/seo-meta-tags
* Linkage to external style sheets or script files via the jsDelivr CDN:
- https://www.jsdelivr.com/
* <label> element usage:
- https://stackoverflow.com/questions/7636502/why-use-label
......@@ -17,7 +17,32 @@
<h4>Αρχική Σελίδα Χρήστη</h4>
<nav id="user_home_nav">
{% if user.ece_id is None %}
<p>
<strong>
{% if user.first_name == '' and user.last_name == '' and user.email is None %}
&#x272A; Παρακαλούμε συμπληρώστε άμεσα τα στοιχεία του προφίλ σας επιλέγοντας "Επεξεργασία Προφίλ"!
{% else %}
&#x272A; Αν διαθέτετε αριθμό μητρώου, παρακαλούμε συμπληρώστε άμεσα το <br />
αντίστοιχο πεδίο του προφίλ επιλέγοντας "Επεξεργασία Προφίλ"!
{% endif %}
</strong>
</p>
{% endif %}
<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.
{% 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 %}
{# Insert ALL the OTHER navigation options HERE! #}
{# ... #}
{# ... #}
{# ... #}
{% endif %}
<p><a href="{% url 'logout' %}">Έξοδος</a></p>
</nav>
{% endblock %}
{% extends 'base.html' %}
{% block title %}
SLUB - Επεξεργασία Προφίλ Χρήστη
{% endblock %}
{% block content %}
<nav id="session_nav">
{% if user.first_name and user.last_name %}
Έχετε εισέλθει ως <strong>{{ user.first_name }} {{ user.last_name }}</strong>!
{% else %}
Έχετε εισέλθει επιτυχώς!
{% endif %}
| <a href="{% url 'user_home' user.id %}">Αρχική Σελίδα</a>
| <a href="{% url 'logout' %}">Έξοδος</a>
</nav>
<h4>Επεξεργασία Προφίλ Χρήστη</h4>
{% if user.first_name == '' and user.last_name == '' and user.email is None %}
<p>
<strong>
&#x272A; Αν καταχωρήσατε τα στοιχεία του προφίλ σας κατά τη διάρκεια παλιότερης εισόδου σας στο σύστημα και <br />
αυτά δεν εμφανίζονται τώρα στην παρακάτω φόρμα (είτε γιατί έχετε αλλάξει τα διαπιστευτήρια της <br />
πλατφόρμας εισόδου είτε γιατί έχετε εισέλθει για πρώτη φορά με διαπιστευτήρια αυτής της πλατφόρμας), <br />
παρακαλούμε ανακτήστε τα από τη βάση χωρίς να συμπληρώσετε τα πεδία της παρακάτω φόρμας! <br />
<a href="{% url 'default_user_profile_recovery' user.id %}">&#x2192; Ανάκτηση χωρίς Συμπλήρωση</a>
</strong>
</p>
<br />
{% endif %}
<form id="user_profile_edit_form" accept-charset="utf-8" action="{% url 'edit_user_profile' user.id %}" method="post">
{% csrf_token %}
<table>
<tbody>
<tr>
<td style="text-align:right;"><label for="first_name">Όνομα:</label></td>
<td style="text-align:left;">
<input type="text" name="first_name" id="first_name" maxlength="50"
value="{{ user.first_name }}" required="required" />
</td>
</tr>
<tr>
<td style="text-align:right;"><label for="last_name">Επώνυμο:</label></td>
<td style="text-align:left;">
<input type="text" name="last_name" id="last_name" maxlength="50"
value="{{ user.last_name }}" required="required" />
</td>
</tr>
<tr>
<td style="text-align:right;"><label for="ece_id">Αριθμός μητρώου<span style="color:darkorange;">*</span>:</label></td>
<td style="text-align:left;">
<input type="text" name="ece_id" id="ece_id" maxlength="8" pattern="031[0-9]{5}"
{% if user.ece_id is not None %}value="{{ user.ece_id }}"{% endif %} />
</td>
</tr>
<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"
{% if user.email is not None %}value="{{ user.email }}"{% endif %} required="required" />
</td>
</tr>
</tbody>
</table>
<p style="color:darkorange;">
<strong>
* Αν δεν διαθέτετε αριθμό μητρώου ακόμα, μπορείτε προσωρινά να παραλείψετε το αντίστοιχο πεδίο! <br />
Σε αντίθετη περίπτωση, παρακαλούμε συμπληρώστε το άμεσα!
</strong>
</p>
<input type="submit" value="Αποθήκευση" />
</form>
{% if success_message %}
<p style="color:green;">
<img src="images/tick.png" alt="Επιβεβαίωση:" />
<strong>{{ success_message | linebreaksbr }}</strong>
</p>
{% elif error_messages %}
{% for message in error_messages %}
<p style="color:red;">
<img src="images/warning.svg" alt="Σφάλμα:" />
<strong>{{ message | linebreaksbr }}</strong>
</p>
{% endfor %}
{% endif %}
<p><a href="{% url 'user_home' user.id %}">&#x21B5; Επιστροφή στην Αρχική Σελίδα</a></p>
{% endblock %}
{% extends 'base.html' %}
{% block title %}
SLUB - Ανάκτηση Προφίλ Χρήστη
{% endblock %}
{% block content %}
<nav id="session_nav">
Έχετε εισέλθει επιτυχώς!
| <a href="{% url 'user_home' user.id %}">Αρχική Σελίδα</a>
| <a href="{% url 'logout' %}">Έξοδος</a>
</nav>
<h4>Ανάκτηση Προφίλ Χρήστη</h4>
<p>
<strong>
&#x272A; Για τον εντοπισμό του επιθυμητού προφίλ προς ανάκτηση, παρακαλούμε συμπληρώστε <br />
τον αριθμό μητρώου (αν υπάρχει καταχωρημένος) και το ηλεκτρονικό ταχυδρομείο <br />
όπως ακριβώς τα έχετε καταχωρήσει στη βάση!
</strong>
</p>
<form id="user_profile_recovery_form" accept-charset="utf-8" action="{% url 'search_for_recovery_user_profile' user.id %}" method="post">
{% csrf_token %}
<table>
<tbody>
<tr>
<td style="text-align:right;"><label for="ece_id">Αριθμός μητρώου:</label></td>
<td style="text-align:left;">
<input type="text" name="ece_id" id="ece_id" maxlength="8" pattern="031[0-9]{5}" />
</td>
</tr>
<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" />
</td>
</tr>
</tbody>
</table>
<br />
<input type="submit" value="Αναζήτηση" />
</form>
{% if success_message %}
<p style="color:green;">
<img src="images/tick.png" alt="Επιβεβαίωση:" />
<strong>
{{ success_message | linebreaksbr }}
<br />
<a href="{% url 'recover_user_profile' user.id recov_user.id %}">&#x2192; Ανάκτηση και Έξοδος</a>
</strong>
</p>
{% elif error_messages %}
{% for message in error_messages %}
<p style="color:red;">
<img src="images/warning.svg" alt="Σφάλμα:" />
<strong>{{ message | linebreaksbr }}</strong>
</p>
{% endfor %}
{% endif %}
<p><a href="{% url 'default_user_profile_edit' user.id %}">&#x21B5; Επιστροφή στην Επεξεργασία Προφίλ</a></p>
{% endblock %}
......@@ -13,13 +13,24 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.urls import re_path
from usermerge import views
urlpatterns = [
re_path(r'^login/default/', views.show_default_login_page, name = 'default_login'),
re_path(r'^login/submit/', views.log_in, name = 'submit_login'),
re_path(r'^home/user/', views.show_user_home_page, name = 'user_home'),
re_path(r'^home/admin/', views.show_admin_home_page, name = 'admin_home'),
re_path(r'^logout/', views.log_out, name = 'logout'),
# The default login page serves as index page. Therefore, its regular route becomes r'^$' instead of r'^login/default$'.
re_path(r'^$', views.display_default_login_page, name = 'default_login'),
re_path(r'^login/submit$', views.log_in, name = 'submit_login'),
re_path(r'^user/home/id=(\d{1,10})$', views.display_user_home_page, name = 'user_home'),
re_path(r'^user/profile/edit/default/id=(\d{1,10})$', views.display_default_user_profile_edit_page, name = 'default_user_profile_edit'),
re_path(r'^user/profile/edit/submit/id=(\d{1,10})$', views.edit_user_profile, name = 'edit_user_profile'),
re_path(r'^user/profile/recovery/default/id=(\d{1,10})$', views.display_default_user_profile_recovery_page,
name = 'default_user_profile_recovery'),
re_path(r'^user/profile/recovery/search/id=(\d{1,10})$', views.search_for_recovery_user_profile,
name = 'search_for_recovery_user_profile'),
re_path(r'^user/profile/recovery/submit/id=(\d{1,10})/recover/id=(\d{1,10})$', views.recover_user_profile,
name = 'recover_user_profile'),
re_path(r'^admin/home/id=(\d{1,10})$', views.display_admin_home_page, name = 'admin_home'),
re_path(r'^logout$', views.log_out, name = 'logout'),
]
from django.core.exceptions import ValidationError
from usermerge.models import Platform
# Create your validators here.
# https://docs.djangoproject.com/en/2.0/ref/validators/
# 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):
"""
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.
"""
if platform != 'SLUB' and not (platform != 'ECE-NTUA' and platform in Platform.objects.values_list('name', flat = True)):
raise ValidationError('Το όνομα πλατφόρμας πρέπει να επιλέγεται μεταξύ εκείνων\n'
'που παρέχονται στην αντίστοιχη αναπτυσσόμενη λίστα!', code = 'platform_is_not_selected_from_provided_list')
def ece_id_is_not_031YY000(ece_id):
"""
Ensure that the ece_id (format) is not 031YY000.
If it is, raise a ValidationError exception with the appropriate error message and code.
"""
if ece_id[5:] == '000':
raise ValidationError('Ο αριθμός μητρώου απαγορεύεται να είναι της μορφής 031YY000 (YY: έτος)!', code = 'ece_id_is_031YY000')
import logging
from django.contrib.auth import authenticate, logout
from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect, render
from usermerge.auth import login
from usermerge.forms import LoginForm
from usermerge.models import Platform
from django.contrib.auth.models import AnonymousUser
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,
_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
# Create your views here.
# https://docs.djangoproject.com/en/2.0/topics/http/views/
# For more information on authenticate() and logout() functions and login_required() decorator, see https://docs.djangoproject.com/en/2.0/topics/auth/default/ . For more information on login() function, see auth.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 and backends.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.
def get_names_of_SoftLab_provided_platforms_from_DB():
def display_default_login_page(request):
"""
Return the list of all the platform names, except for ECE-NTUA (this 'platform' is not provided by SoftLab),
that exist in usermergeDB (see populateDB.py). This list can be used as part of the context
while rendering our application pages, e.g. in the platform drop-down list of the login form.
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.
"""
platform_names = list(Platform.objects.order_by('id').values_list('name', flat = True))
platform_names.remove('ECE-NTUA')
return platform_names
def show_default_login_page(request):
"""
Show login page without any validation/authentication error messages (that is, before submitting any invalid credentials).
"""
return render(request, 'login.html', {'platform_list': get_names_of_SoftLab_provided_platforms_from_DB()})
return TemplateResponse(request, 'login.html', {'platform_names': get_names_of_SoftLab_provided_platforms_from_DB()})
def log_in(request):
"""
Collect the credentials POSTed via the login form and validate them. If they are not valid, redirect the user to
the login page and display the appropriate validation error messages. Otherwise, authenticate them. If they do
not correspond to any User or Admin instance of usermergeDB (see models.py), redirect the user to the login page and
display the appropriate authentication error message. Otherwise, log the user in and redirect him/her to the
appropriate home page depending on his/her representative model/class (User/Admin from usermerge.models).
Collect the data, i.e. platform, username and password, POSTed via the login form and validate them. If they are not valid, display
the appropriate validation error messages in the login page. Otherwise, authenticate the credentials (username and password) for
the selected platform. If they do not correspond to any user instance of usermergeDB, display the appropriate post-validation error
message in the login page and include the corresponding error code in the page context. Otherwise, log the user in and redirect
him/her to the appropriate home page depending on his/her model (User/Admin).
"""
form = LoginForm(request.POST)
if form.is_valid():
......@@ -43,36 +47,379 @@ def log_in(request):
user = authenticate(request, platform = platform, username = username, password = password)
if user:
login(request, user)
# User instances have an ece_id attribute, whereas Admin instances do not.
# Therefore, if user has an ece_id attribute, he/she is a User. Otherwise, he/she is an Admin.
if hasattr(user, 'ece_id'):
return redirect('user_home')
# If the selected platform is SLUB, the user model is Admin. Otherwise, it is User.
if platform == 'SLUB':
return redirect('admin_home', user.id)
else:
return redirect('admin_home')
return redirect('user_home', user.id)
else:
return render(request, 'login.html',
{'platform_list': get_names_of_SoftLab_provided_platforms_from_DB(),
'error_messages': ['Τα δηλωθέντα διαπιστευτήρια δεν ανταποκρίνονται σε κανένα χρήστη της βάσης!\n'
'Παρακαλούμε ελέγξτε την ορθότητα των διαπιστευτηρίων και δοκιμάστε ξανά!']})
return TemplateResponse(request, 'login.html',
{'platform_names': get_names_of_SoftLab_provided_platforms_from_DB(),
'error_messages': ['Τα δηλωθέντα στοιχεία εισόδου δεν ανταποκρίνονται σε κανέναν χρήστη της βάσης!\n'
'Παρακαλούμε ελέγξτε την ορθότητα των στοιχείων εισόδου και δοκιμάστε ξανά!'],
'post_validation_error_codes': ['non_existent_credentials_pair_for_selected_login_platform']})
else:
return render(request, 'login.html',
{'platform_list': get_names_of_SoftLab_provided_platforms_from_DB(),
# https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions
'error_messages': [list(form.errors.values())[k-1][0] for k in range(len(form.errors.values()))]})
return TemplateResponse(request, 'login.html', {'platform_names': get_names_of_SoftLab_provided_platforms_from_DB(),
'error_messages': get_form_error_messages(form)})
@login_required(redirect_field_name = None) # The session user's model should be User.
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.
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)
def show_user_home_page(request):
return render(request, 'user_home.html', {})
@login_required(redirect_field_name = None) # The session user's model should be User.
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
are not valid, display the appropriate validation error messages in the user profile edit page. Otherwise, check if any form fields
have been changed in comparison to the respective fields of the session user's profile. If no changes have been made, display the
appropriate post-validation error message in the user profile edit page and include the corresponding error code in the page context.
Otherwise, check if the validated (cleaned) ece_id and email are adequate to modify the session user's profile consistently in
usermergeDB by taking the respective field values of the latter into account. If they are not adequate, create a dictionary with
them as values and the corresponding (post-validation) error codes as keys and call the _display_user_profile_edit_error_messages()
helper function with the aforementioned dictionary as the error_data argument. Otherwise, call the _update_user_profile() helper
function with the (name) list of all the changed form fields as the changed_data argument and the dictionary of all the validated
form data (the field names are used as keys and the corresponding validated values as values) as the cleaned_data argument.
"""
session_user = User.objects.get(pk = user_id)
form = UserProfileEditForm(request.POST, initial = {'first_name': session_user.first_name, 'last_name': session_user.last_name,
'ece_id': session_user.ece_id, 'email': session_user.email})
if form.is_valid():
cleaned_data = form.cleaned_data
# first_name = cleaned_data['first_name']
# last_name = cleaned_data['last_name']
ece_id = cleaned_data['ece_id']
email = cleaned_data['email']
if not form.has_changed():
return TemplateResponse(request, 'user_profile_edit.html',
{'error_messages': ['Η βάση δεν χρειάστηκε να ενημερωθεί καθώς κανένα από τα δηλωθέντα στοιχεία του '
'προφίλ δεν τροποποιήθηκε!\n'
'Παρακαλούμε, προτού πατήσετε "Αποθήκευση", βεβαιωθείτε πρώτα ότι έχετε '
'τροποποιήσει ένα ή περισσότερα\n'
'στοιχεία του προφίλ!'],
'post_validation_error_codes': ['unchanged_profile_form_fields']})
else:
changed_data = form.changed_data
# The session user's profile in usermergeDB is empty (session_user.ece_id is None and session_user.email is None).
if session_user.first_name == '' and session_user.last_name == '' and session_user.email is None:
try:
User.objects.get(email = email)
except User.DoesNotExist:
try:
User.objects.get(ece_id = ece_id)
# The validated ece_id either exists multiple times (ece_id is None) or
# does not exist at all (ece_id is not None) in usermergeDB.
except (User.DoesNotExist, User.MultipleObjectsReturned):
return _update_user_profile(request, changed_data, cleaned_data)
else:
if ece_id is None:
return _update_user_profile(request, changed_data, cleaned_data)
else:
return _display_user_profile_edit_error_messages(request, {'duplicate_ece_id': ece_id})
else:
try:
User.objects.get(ece_id = ece_id)
# The validated ece_id either exists multiple times (ece_id is None) or
# does not exist at all (ece_id is not None) in usermergeDB.
except (User.DoesNotExist, User.MultipleObjectsReturned):
return _display_user_profile_edit_error_messages(request, {'duplicate_email': email})
else:
if ece_id is None:
return _display_user_profile_edit_error_messages(request, {'duplicate_email': email})
else:
return _display_user_profile_edit_error_messages(request, {'duplicate_ece_id': ece_id,
'duplicate_email': email})
else: # The session user's profile in usermergeDB is non-empty (session_user.email is not None).
if session_user.ece_id is None:
if ece_id is None:
if 'email' in changed_data:
try:
User.objects.get(email = email)
except User.DoesNotExist:
return _update_user_profile(request, changed_data, cleaned_data)
else:
return _display_user_profile_edit_error_messages(request, {'duplicate_email': email})
else:
return _update_user_profile(request, changed_data, cleaned_data)
else: # ece_id is not None
try:
User.objects.get(ece_id = ece_id)
except User.DoesNotExist:
if 'email' in changed_data:
try:
User.objects.get(email = email)
except User.DoesNotExist:
return _update_user_profile(request, changed_data, cleaned_data)
else:
return _display_user_profile_edit_error_messages(request, {'duplicate_email': email})
else:
return _update_user_profile(request, changed_data, cleaned_data)
else:
if 'email' in changed_data:
try:
User.objects.get(email = email)
except User.DoesNotExist:
return _display_user_profile_edit_error_messages(request, {'duplicate_ece_id': ece_id})
else:
return _display_user_profile_edit_error_messages(request, {'duplicate_ece_id': ece_id,
'duplicate_email': email})
else:
return _display_user_profile_edit_error_messages(request, {'duplicate_ece_id': ece_id})
else: # session_user.ece_id is not None
if ece_id is None:
if 'email' in changed_data:
try:
User.objects.get(email = email)
except User.DoesNotExist:
return _display_user_profile_edit_error_messages(request,
{'registered_ece_id_was_cleared': session_user.ece_id})
else:
return _display_user_profile_edit_error_messages(request,
{'registered_ece_id_was_cleared': session_user.ece_id,
'duplicate_email': email})
else:
return _display_user_profile_edit_error_messages(request,
{'registered_ece_id_was_cleared': session_user.ece_id})
else: # ece_id is not None
if 'ece_id' in changed_data:
try:
User.objects.get(ece_id = ece_id)
except User.DoesNotExist:
if 'email' in changed_data:
try:
User.objects.get(email = email)
except User.DoesNotExist:
return _update_user_profile(request, changed_data, cleaned_data)
else:
return _display_user_profile_edit_error_messages(request, {'duplicate_email': email})
else:
return _update_user_profile(request, changed_data, cleaned_data)
else:
if 'email' in changed_data:
try:
User.objects.get(email = email)
except User.DoesNotExist:
return _display_user_profile_edit_error_messages(request, {'duplicate_ece_id': ece_id})
else:
return _display_user_profile_edit_error_messages(request, {'duplicate_ece_id': ece_id,
'duplicate_email': email})
else:
return _display_user_profile_edit_error_messages(request, {'duplicate_ece_id': ece_id})
else:
if 'email' in changed_data:
try:
User.objects.get(email = email)
except User.DoesNotExist:
return _update_user_profile(request, changed_data, cleaned_data)
else:
return _display_user_profile_edit_error_messages(request, {'duplicate_email': email})
else:
return _update_user_profile(request, changed_data, cleaned_data)
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)
def show_admin_home_page(request):
return render(request, 'admin_home.html', {})
@login_required(redirect_field_name = None) # The session user's model should be User.
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
PermissionDenied exception (there is no point in specifying and recovering a non-empty user profile from usermergeDB for the
session user if the latter's profile is already non-empty - see the search_for_recovery_user_profile() and recover_user_profile()
views below). Otherwise, display the user profile recovery page in default mode, i.e. without any form-submission-related messages.
"""
session_user = User.objects.get(pk = user_id)
# The session user's profile in usermergeDB is non-empty.
if session_user.first_name != '' and session_user.last_name != '' and session_user.email is not None:
raise PermissionDenied
else:
return TemplateResponse(request, 'user_profile_recovery.html', {})
@login_required(redirect_field_name = None) # The session user's model should be User.
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
PermissionDenied exception (there is no point in searching usermergeDB to recover a non-empty profile for the session user if
the latter's profile is already non-empty). Otherwise, collect the data, i.e. ece_id and email, POSTed via the user profile
recovery form and validate them. If they are not valid, display the appropriate validation error messages in the user profile
recovery page. Otherwise, check if they are adequate to specify a non-empty user profile (User instance) for recovery in
usermergeDB (the specified profile would correspond to both the validated - cleaned - ece_id and email). If they are not
adequate (the validated ece_id or/and email do not exist in usermergeDB or they both exist in the latter but not in the same
profile), create a dictionary with them as values and the corresponding (post-validation) error codes as keys and call the
_display_user_profile_recovery_error_messages() helper function with the aforementioned dictionary as the error_data argument.
Otherwise, call the _display_user_profile_recovery_success_message() helper function with the specified profile as the
recov_user argument.
"""
# This view serves as the preparatory step of the user profile recovery procedure, i.e. it is used to set up the corresponding
# page context appropriately before the actual procedure takes place - see the recover_user_profile() view below. The context
# is set up by the called helper function - either _display_user_profile_recovery_error_messages() or
# _display_user_profile_recovery_success_message().
session_user = User.objects.get(pk = user_id)
# The session user's profile in usermergeDB is non-empty (session_user.email is not None).
if session_user.first_name != '' and session_user.last_name != '' and session_user.email is not None:
raise PermissionDenied
else: # The session user's profile in usermergeDB is empty (session_user.ece_id is None and session_user.email is None).
form = UserProfileRecoveryForm(request.POST)
if form.is_valid():
ece_id = form.cleaned_data['ece_id']
email = form.cleaned_data['email']
# Taking into account that the validated email is never None (the validated ece_id can be None), the happy path of the view
# is the following:
# * Search usermergeDB for a non-empty user profile that corresponds to the validated email (in other words, check if the
# validated email exists in usermergeDB).
# * If a corresponding profile is found, check if the validated ece_id matches the one of the profile (in other words, check
# if the validated ece_id exists in usermergeDB and specifically in the same profile as the validated email).
# * If the validated ece_id matches the one of the profile (the profile corresponds to both the validated ece_id and email),
# the search succeeds, the profile is deemed the appropriate one for recovery and the
# _display_user_profile_recovery_success_message() function is called.
# If any of the checks (searching for the corresponding profile and comparing the ece_id values) fails, the search fails,
# the exact search errors (error codes) are determined by the existence statuses of both the validated ece_id and email
# in usermergeDB and the _display_user_profile_recovery_error_messages() function is called (if the first check fails,
# the second one is never performed).
try:
recov_user = User.objects.get(email = email)
except User.DoesNotExist:
try:
User.objects.get(ece_id = ece_id)
except User.DoesNotExist: # The validated ece_id does not exist in usermergeDB (ece_id is not None).
return _display_user_profile_recovery_error_messages(request, {'non_existent_ece_id': ece_id,
'non_existent_email': email})
except User.MultipleObjectsReturned: # The validated ece_id exists multiple times in usermergeDB (ece_id is None).
try:
# Taking into account that empty user profiles (email is None) are indifferent to the search procedure and that
# the validated ece_id is None, search usermergeDB for non-empty user profiles (email is not None) whose ece_id
# is None (None is interpreted as NULL in usermergeDB) and specify the exact error_data argument of the called
# _display_user_profile_recovery_error_messages() function based on the search result.
User.objects.get(ece_id__isnull = True, email__isnull = False)
except User.DoesNotExist:
return _display_user_profile_recovery_error_messages(request, {'non_existent_ece_id': ece_id,
'non_existent_email': email})
except User.MultipleObjectsReturned:
return _display_user_profile_recovery_error_messages(request, {'non_existent_email': email})
else:
return _display_user_profile_recovery_error_messages(request, {'non_existent_email': email})
else:
if ece_id is None:
return _display_user_profile_recovery_error_messages(request, {'non_existent_ece_id': ece_id,
'non_existent_email': email})
else:
return _display_user_profile_recovery_error_messages(request, {'non_existent_email': email})
else: # The validated email exists exactly once in usermergeDB (recov_user.email == email).
if recov_user.ece_id is None:
if ece_id is None:
return _display_user_profile_recovery_success_message(request, recov_user)
else:
try:
User.objects.get(ece_id = ece_id)
except User.DoesNotExist:
return _display_user_profile_recovery_error_messages(request, {'non_existent_ece_id': ece_id})
else:
return _display_user_profile_recovery_error_messages(request, {'ece_id_and_email_exist_in_different_profiles':
{'ece_id': ece_id, 'email': email}})
else: # recov_user.ece_id is not None
if ece_id is None:
try:
# Taking into account that empty user profiles (email is None) are indifferent to the search procedure and
# that the validated ece_id is None, search usermergeDB for non-empty user profiles (email is not None) whose
# ece_id is None (None is interpreted as NULL in usermergeDB) and specify the exact error_data argument of
# the called _display_user_profile_recovery_error_messages() function based on the search result.
User.objects.get(ece_id__isnull = True, email__isnull = False)
except User.DoesNotExist:
return _display_user_profile_recovery_error_messages(request, {'non_existent_ece_id': ece_id})
except User.MultipleObjectsReturned:
return _display_user_profile_recovery_error_messages(request, {'ece_id_and_email_exist_in_different_profiles':
{'ece_id': ece_id, 'email': email}})
else:
return _display_user_profile_recovery_error_messages(request, {'ece_id_and_email_exist_in_different_profiles':
{'ece_id': ece_id, 'email': email}})
else:
if recov_user.ece_id == ece_id:
return _display_user_profile_recovery_success_message(request, recov_user)
else:
try:
User.objects.get(ece_id = ece_id)
except User.DoesNotExist:
return _display_user_profile_recovery_error_messages(request, {'non_existent_ece_id': ece_id})
else:
return _display_user_profile_recovery_error_messages(request,
{'ece_id_and_email_exist_in_different_profiles':
{'ece_id': ece_id, 'email': email}})
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.
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
PermissionDenied exception (there is no point in recovering a non-empty profile from usermergeDB for the session user if the latter's
profile is already non-empty). Otherwise, delete any previous credentials that the recovery user may have retained for the login
platform in usermergeDB and associate him/her with the session user's respective ones. More specifically, delete the recovery
registry, i.e. the Registry instance that corresponds to both the recovery user and the login platform, if it exists in usermergeDB
and update the existing session registry, i.e. the Registry instance that corresponds to both the session user and the login platform,
to reference the recovery user instead. Then delete the session user's profile to complete his/her merge with the recovery user and if
the latter's credentials for the login platform have changed, i.e. he/she retained previous credentials before receiving the session
user's respective ones, use the view logger (usermerge.views.recover_user_profile) to log the appropriate INFO message that mentions
the recovery user's previous and current username (key credential) for the login platform. Finally, log the session user out (flush
the session as it is based on a no longer existing user - profile - and has thus become inconsistent) and redirect him/her to the
default login page.
"""
view_logger = logging.getLogger('usermerge.views.recover_user_profile')
session_user = User.objects.get(pk = session_user_id)
# The session user's profile in usermergeDB is non-empty.
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
# 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)
except Registry.DoesNotExist:
pass
else:
prev_recov_username = recov_registry.username
recov_registry.delete()
session_registry.user = recov_user
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)
# 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()
# view that utilizes the logout() function, log the session user out (flush the session - delete the session data and
# cookie - and set an AnonymousUser instance as the current user in the associated request) and redirect him/her to the
# default login page manually.
request.session.flush()
request.user = AnonymousUser()
return redirect('default_login')
@login_required(redirect_field_name = None) # The session user's model should be Admin.
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)
@login_required(redirect_field_name = None) # The session user's model can be either User or Admin.
def log_out(request):
"""
Log the authenticated user out and redirect him/her to the logout page.
Log the session user out and redirect him/her to the default login page.
"""
logout(request)
return render(request, 'logout.html', {})
return redirect('default_login')
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