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 %}
+                    &#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 %}
+
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>
+                &#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 %}
+
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>
+            &#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 %}
+
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')