diff --git a/README.md b/README.md index 96404eabe00417db8541281cc1842280829da2bd..e9168cae07858640fa3195ec847ef631fab43f10 100644 --- a/README.md +++ b/README.md @@ -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/): diff --git a/myprj/myprj/settings.py b/myprj/myprj/settings.py index 041b1d88f0fe4b86948bb08667984394acfe961e..de0da5a0a5de10aa9af23dce0781e29235c33822 100644 --- a/myprj/myprj/settings.py +++ b/myprj/myprj/settings.py @@ -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, + }, }, } diff --git a/myprj/myprj/urls.py b/myprj/myprj/urls.py index 71a0adfd81464e61cea12fb77f1519fa8194ffe6..bdf9cd4404154f8a9e32c9bb45c16b558553131d 100644 --- a/myprj/myprj/urls.py +++ b/myprj/myprj/urls.py @@ -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')), ] diff --git a/myprj/usermerge/auth.py b/myprj/usermerge/auth.py index 2d5e76dbc83e57890fb58383923effc65d41e712..5ac444ab56f411ffb78bd647f4be6f0a9ff5d4eb 100644 --- a/myprj/usermerge/auth.py +++ b/myprj/usermerge/auth.py @@ -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'] diff --git a/myprj/usermerge/backends.py b/myprj/usermerge/backends.py index 5679937fe128929259fffe1b0d8e6b47c32f67cf..48a22b9f62e833ed1bb2a0fa1384d3f300733283 100644 --- a/myprj/usermerge/backends.py +++ b/myprj/usermerge/backends.py @@ -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!') diff --git a/myprj/usermerge/forms.py b/myprj/usermerge/forms.py index 9cd3ac52dbf3cd43dc1ffc237d8f9a0c88dac6f9..a78f294f6a2c2ad113f8d7e5fbbb2cdea2325e01 100644 --- a/myprj/usermerge/forms.py +++ b/myprj/usermerge/forms.py @@ -1,17 +1,15 @@ 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 diff --git a/myprj/usermerge/helper.py b/myprj/usermerge/helper.py new file mode 100644 index 0000000000000000000000000000000000000000..899cf6d8e6324c70418541a7b5fb90d7e71a9c0d --- /dev/null +++ b/myprj/usermerge/helper.py @@ -0,0 +1,151 @@ +""" +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}) + diff --git a/myprj/usermerge/middleware.py b/myprj/usermerge/middleware.py index 1bdf641926fae62e0cb88283acc33ec2c4341135..be2f54c817cf820457753d8ad9cd3228470b0850 100644 --- a/myprj/usermerge/middleware.py +++ b/myprj/usermerge/middleware.py @@ -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'): diff --git a/myprj/usermerge/models.py b/myprj/usermerge/models.py index d366b3596344ff72aba9e89e4c472ef6394e1f1b..f86cd8e9e827aaf612bbbca81275eb46db3ad89b 100755 --- a/myprj/usermerge/models.py +++ b/myprj/usermerge/models.py @@ -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): diff --git a/myprj/usermerge/populateDB.py b/myprj/usermerge/populateDB.py index c449f8193e6f6abf7539a1ea47f15e199b00c6ce..4e36ff64b4c483709b47afeeffa67916e0168b92 100644 --- a/myprj/usermerge/populateDB.py +++ b/myprj/usermerge/populateDB.py @@ -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') diff --git a/myprj/usermerge/static/images/tick.png b/myprj/usermerge/static/images/tick.png new file mode 100755 index 0000000000000000000000000000000000000000..bd6efc0e1f7ba9422c675426db91a8f98e73ede9 Binary files /dev/null and b/myprj/usermerge/static/images/tick.png differ diff --git a/myprj/usermerge/templates/login.html b/myprj/usermerge/templates/login.html index b5a112accb0ee50385d7d87b75e29a90e07e9c4e..e1eef7d6fac0f50f0edb93bbde73529b4db7fac9 100644 --- a/myprj/usermerge/templates/login.html +++ b/myprj/usermerge/templates/login.html @@ -1,11 +1,11 @@ {% 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> diff --git a/myprj/usermerge/templates/logout.html b/myprj/usermerge/templates/logout.html deleted file mode 100644 index 3895ce81b563bee98178bdc86c3b3c44eca2876f..0000000000000000000000000000000000000000 --- a/myprj/usermerge/templates/logout.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends 'base.html' %} - -{% block title %} - SLUB - ΞΞΎΞΏΞ΄ΞΏΟ Ξ§ΟΞ�ΟΟΞ·/ΞΞΉΞ±ΟΡιΟΞΉΟΟΞ� -{% endblock %} - -{% block content %} - <h4>ΞΞΎΞΏΞ΄ΞΏΟ Ξ§ΟΞ�ΟΟΞ·/ΞΞΉΞ±ΟΡιΟΞΉΟΟΞ�</h4> - <p>ΞΞΎΞ�λθαΟΞ΅ ΞΌΞ΅ Ξ΅ΟΞΉΟΟ ΟΞ―Ξ±!</p> - <p><a href="{% url 'default_login' %}">ΞΞ± ΞΈΞλαΟΞ΅ ΞΌΞ�ΟΟΟ Ξ½Ξ± ΡιΟΞλθΡΟΞ΅ ΞΎΞ±Ξ½Ξ¬;</a></p> -{% endblock %} diff --git a/myprj/usermerge/templates/template_refs.txt b/myprj/usermerge/templates/template_refs.txt index 0688deb0f167f14a0583e60ffb6b99fe70c64022..0ced49550f4733cd785238e06309850e8953ecea 100644 --- a/myprj/usermerge/templates/template_refs.txt +++ b/myprj/usermerge/templates/template_refs.txt @@ -1,2 +1,16 @@ -* 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 diff --git a/myprj/usermerge/templates/user_home.html b/myprj/usermerge/templates/user_home.html index a2c0a5ff61674eb0f5325482cb8b567bf0fd73f2..48ef4660cd1a277453488c6ea0ae0af84e3e9ad0 100644 --- a/myprj/usermerge/templates/user_home.html +++ b/myprj/usermerge/templates/user_home.html @@ -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 %} + ✪ Ξ Ξ±ΟακαλοΟΞΌΞ΅ ΟΟ ΞΌΟληΟΟΟΟΞ΅ άμΡΟΞ± ΟΞ± ΟΟΞΏΞΉΟΡία ΟΞΏΟ ΟΟΞΏΟΞ―Ξ» ΟΞ±Ο Ξ΅ΟΞΉΞ»ΞΞ³ΞΏΞ½ΟΞ±Ο "ΞΟΡξΡΟΞ³Ξ±ΟΞ―Ξ± Ξ ΟΞΏΟΞ―Ξ»"! + {% else %} + ✪ ΞΞ½ διαθΞΟΞ΅ΟΞ΅ Ξ±ΟΞΉΞΈΞΌΟ ΞΌΞ·ΟΟΟΞΏΟ , ΟΞ±ΟακαλοΟΞΌΞ΅ ΟΟ ΞΌΟληΟΟΟΟΞ΅ άμΡΟΞ± ΟΞΏ <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 %} + diff --git a/myprj/usermerge/templates/user_profile_edit.html b/myprj/usermerge/templates/user_profile_edit.html new file mode 100644 index 0000000000000000000000000000000000000000..c6794715c43b2100137b33dfde99708e3b57f619 --- /dev/null +++ b/myprj/usermerge/templates/user_profile_edit.html @@ -0,0 +1,95 @@ +{% 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> + ✪ ΞΞ½ ΞΊΞ±ΟΞ±ΟΟΟΞ�ΟΞ±ΟΞ΅ ΟΞ± ΟΟΞΏΞΉΟΡία ΟΞΏΟ ΟΟΞΏΟΞ―Ξ» ΟΞ±Ο ΞΊΞ±ΟΞ¬ ΟΞ· διάΟκΡια ΟαλιΟΟΞ΅ΟΞ·Ο Ξ΅ΞΉΟΟΞ΄ΞΏΟ ΟΞ±Ο ΟΟΞΏ ΟΟΟΟΞ·ΞΌΞ± ΞΊΞ±ΞΉ <br /> + Ξ±Ο ΟΞ¬ δΡν ΡμΟΞ±Ξ½Ξ―ΞΆΞΏΞ½ΟΞ±ΞΉ ΟΟΟΞ± ΟΟΞ·Ξ½ ΟΞ±ΟΞ±ΞΊΞ¬ΟΟ ΟΟΟΞΌΞ± (ΡίΟΞ΅ Ξ³ΞΉΞ±ΟΞ― ΞΟΞ΅ΟΞ΅ αλλάξΡι ΟΞ± διαΟΞΉΟΟΞ΅Ο ΟΞ�ΟΞΉΞ± ΟΞ·Ο <br /> + ΟλαΟΟΟΟΞΌΞ±Ο Ξ΅ΞΉΟΟΞ΄ΞΏΟ Ξ΅Ξ―ΟΞ΅ Ξ³ΞΉΞ±ΟΞ― ΞΟΞ΅ΟΞ΅ ΡιΟΞλθΡι Ξ³ΞΉΞ± ΟΟΟΟΞ· ΟΞΏΟΞ¬ ΞΌΞ΅ διαΟΞΉΟΟΞ΅Ο ΟΞ�ΟΞΉΞ± Ξ±Ο ΟΞ�Ο ΟΞ·Ο ΟλαΟΟΟΟΞΌΞ±Ο), <br /> + ΟΞ±ΟακαλοΟΞΌΞ΅ Ξ±Ξ½Ξ±ΞΊΟΞ�ΟΟΞ΅ ΟΞ± Ξ±ΟΟ ΟΞ· Ξ²Ξ¬ΟΞ· ΟΟΟΞ―Ο Ξ½Ξ± ΟΟ ΞΌΟληΟΟΟΞ΅ΟΞ΅ ΟΞ± ΟΡδία ΟΞ·Ο ΟΞ±ΟΞ±ΞΊΞ¬ΟΟ ΟΟΟΞΌΞ±Ο! <br /> + <a href="{% url 'default_user_profile_recovery' user.id %}">→ ΞνάκΟΞ·ΟΞ· ΟΟΟΞ―Ο Ξ£Ο ΞΌΟΞ»Ξ�ΟΟΟΞ·</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 %}">↵ ΞΟΞΉΟΟΟΞΏΟΞ� ΟΟΞ·Ξ½ ΞΟΟΞΉΞΊΞ� ΣΡλίδα</a></p> +{% endblock %} + diff --git a/myprj/usermerge/templates/user_profile_recovery.html b/myprj/usermerge/templates/user_profile_recovery.html new file mode 100644 index 0000000000000000000000000000000000000000..86077e72b41018f79e90eb00f2d04650e2fad18c --- /dev/null +++ b/myprj/usermerge/templates/user_profile_recovery.html @@ -0,0 +1,68 @@ +{% 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> + ✪ ΞΞΉΞ± ΟΞΏΞ½ ΡνΟΞΏΟΞΉΟΞΌΟ ΟΞΏΟ Ξ΅ΟΞΉΞΈΟ ΞΌΞ·ΟΞΏΟ ΟΟΞΏΟΞ―Ξ» ΟΟΞΏΟ Ξ±Ξ½Ξ¬ΞΊΟΞ·ΟΞ·, ΟΞ±ΟακαλοΟΞΌΞ΅ ΟΟ ΞΌΟληΟΟΟΟΞ΅ <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 %}">→ ΞνάκΟΞ·ΟΞ· ΞΊΞ±ΞΉ ΞξοδοΟ</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 %}">↵ ΞΟΞΉΟΟΟΞΏΟΞ� ΟΟΞ·Ξ½ ΞΟΡξΡΟΞ³Ξ±ΟΞ―Ξ± Ξ ΟΞΏΟΞ―Ξ»</a></p> +{% endblock %} + diff --git a/myprj/usermerge/urls.py b/myprj/usermerge/urls.py index 85107f88037831a96a20771ad5885d0b98fca6a6..f0f544d92bd1ce92be8dccec8a75dc3b44582ddc 100644 --- a/myprj/usermerge/urls.py +++ b/myprj/usermerge/urls.py @@ -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'), ] + diff --git a/myprj/usermerge/validators.py b/myprj/usermerge/validators.py new file mode 100644 index 0000000000000000000000000000000000000000..d1442ca5e84957ae2b23abf68333a460d2eda97c --- /dev/null +++ b/myprj/usermerge/validators.py @@ -0,0 +1,25 @@ +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') diff --git a/myprj/usermerge/views.py b/myprj/usermerge/views.py index 16dbda6f1fbe0d0748912bcdf1895b9b984fbfd5..595a3f2ec0391cc589e05a9fef8ee6e1229e2c35 100644 --- a/myprj/usermerge/views.py +++ b/myprj/usermerge/views.py @@ -1,39 +1,43 @@ +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')