Commit e324d683 authored by Giorgos Kazelidis's avatar Giorgos Kazelidis

- Implemented the user profile edit, search and recovery tasks

- Created and used a library of helper functions that refer (mainly) to views
- Created and used a library of validators that refer (mainly) to forms
- Corrected/enhanced the existing views - used TemplateResponse objects instead of calling the render() shortcut function, inserted post-validation error codes in template contexts when needed, etc.
- Deleted the logout template and used redirection to the login template on logout
- Corrected the generic URL format
- Enhanced the documentation of templates and modules
parent e6f4aa1b
......@@ -136,7 +136,7 @@
(C\) start the Django development server (at http://127.0.0.1:8000/):
python manage.py runserver
(D) navigate to http://127.0.0.1:8000/usermerge/login/default/ via web browser to run the project
(D) navigate to http://127.0.0.1:8000/ via web browser to run the project
## STOPPING THE PROJECT (AND EXITING THE VIRTUAL ENVIRONMENT)
(A) stop the Django development server (running at http://127.0.0.1:8000/):
......
......@@ -91,7 +91,7 @@ LOGIN_URL = 'default_login'
# - The module provides an optional multi-threaded queue logging feature to perform logging in the background. The feature requires Python 3 to run and uses the standard QueueHandler and QueueListener logging classes to create background threads (one for each logger with configured handlers) that handle the logging, thus letting the foreground threads, i.e. those who initiate the logging, handle the requests quickly without blocking for write locks on the log files. It should be used after the logging configuration is set up. The latter is set up during the (usermerge) application setup, the application is set up each time it is loaded and, in production, it is loaded each time a child process is created by the main Apache process (see wsgi.py, https://github.com/django/django/blob/master/django/core/wsgi.py , https://github.com/django/django/blob/master/django/__init__.py and https://modwsgi.readthedocs.io/en/develop/user-guides/processes-and-threading.html). Since the logging configuration is set up for each child process created by the main Apache process, the feature would create too many background threads that could potentially interfere with the operation of Apache and mod_wsgi, which are responsible for managing processes and threads in production, in unexpected ways (https://groups.google.com/forum/#!topic/modwsgi/NMgrti4o9Bw). Therefore, it is recommended that the feature is NOT used, especially in production.
# * Brief summary of the below defined configuration:
# - During development (the DEBUG setting is True), ALL log records/messages are output to the console/terminal (sys.stderr stream) where the Django development server has been run via the runserver command.
# - In production (the DEBUG setting is False), ALL log records/messages, EXCEPT FOR the DEBUG ones, are output to log files that exist in the production_logs directory of the usermerge application (directory). General INFO and WARNING messages are output to general_info_and_warnings.log, ERROR and CRITICAL messages are output to errors.log, while DEBUG messages are NOT output at all. Since the Django development server is inactive, NO messages are logged on the django.server logger for output to the sys.stderr stream. It is worth mentioning that mod_wsgi intercepts the sys.stdout and sys.stderr streams and redirects the output to the Apache error log (https://modwsgi.readthedocs.io/en/develop/user-guides/debugging-techniques.html).
# - In production (the DEBUG setting is False), ALL log records/messages, EXCEPT FOR the DEBUG ones, are output to log files that exist in the production_logs directory of the usermerge application (directory). General INFO and WARNING messages are output to general_info_and_warnings.log, ERROR and CRITICAL messages are output to errors.log and INFO messages logged on the usermerge.views.recover_user_profile logger are output to changes_of_credentials_during_profile_recovery.log, while DEBUG messages are NOT output at all. Since the Django development server is inactive, NO messages are logged on the django.server logger for output to the sys.stderr stream. It is worth mentioning that mod_wsgi intercepts the sys.stdout and sys.stderr streams and redirects the output to the Apache error log (https://modwsgi.readthedocs.io/en/develop/user-guides/debugging-techniques.html).
# * Detailed configuration and usage examples: https://www.webforefront.com/django/setupdjangologging.html
def LOG_LEVEL_IS_LOWER_THAN_ERROR(log_record):
......@@ -169,6 +169,15 @@ LOGGING = {
'backupCount': 5,
'formatter': 'verbose',
},
'file_for_changes_of_credentials_during_profile_recovery': {
'level': 'INFO',
'filters': ['require_debug_false'],
'class': 'logging.handlers.ConcurrentRotatingFileHandler',
'filename': os.path.join(BASE_DIR, 'usermerge/production_logs/changes_of_credentials_during_profile_recovery.log'),
'maxBytes': 1024 * 20, # 20 KB
'backupCount': 5,
'formatter': 'laconic',
},
},
'root': {
'level': 'DEBUG' if DEBUG else 'INFO',
......@@ -190,6 +199,11 @@ LOGGING = {
'handlers': ['console', 'file_for_general_info_and_warnings', 'file_for_errors'],
'propagate': False,
},
'usermerge.views.recover_user_profile': {
'level': 'INFO',
'handlers': ['console', 'file_for_changes_of_credentials_during_profile_recovery'],
'propagate': False,
},
},
}
......
......@@ -13,10 +13,11 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('admin/', admin.site.urls),
path('usermerge/', include('usermerge.urls')),
path('', include('usermerge.urls')),
]
......@@ -10,8 +10,9 @@ from django.middleware.csrf import rotate_token
from django.utils.crypto import constant_time_compare
from usermerge.models import Registry
# For more information on default/customizing user authentication and password management, see https://docs.djangoproject.com/en/2.0/topics/auth/ .
# For more information on default/customizing user authentication and password management, see https://docs.djangoproject.com/en/2.0/topics/auth/ and backends.py .
# The user_logged_in signal is defined (alongside the other authentication signals, user_login_failed and user_logged_out) in the django.contrib.auth.signals module (https://github.com/django/django/blob/master/django/contrib/auth/signals.py). When a user logs in successfully, an appropriate user_logged_in signal is sent by the corresponding user model (User/Admin) in the context of the below defined login() function and is instantaneously received by the update_last_login() function (https://github.com/django/django/blob/master/django/contrib/auth/apps.py). update_last_login() then updates the last_login field of the corresponding user instance to the date-time of the signal reception (https://github.com/django/django/blob/master/django/contrib/auth/models.py). For more information on the user models and their last_login fields, see models.py . For more information on signals, see https://docs.djangoproject.com/en/2.0/topics/signals/ .
# For more information on the platform names that are provided in the drop-down list of the login form, see log_in() view.
SESSION_KEY = '_auth_user_id'
PLATFORM_SESSION_KEY = '_auth_platform'
......@@ -50,7 +51,7 @@ def _get_user_session_key(request):
def login(request, user, backend = None):
"""
Persist a user id and a backend in the request. This way a user doesn't have to reauthenticate on every request.
Persist a user id and a backend in the request. This way a user does not have to reauthenticate on every request.
Note that data set during the anonymous session is retained when the user logs in.
"""
platform_name = request.POST['platform']
......
......@@ -4,13 +4,12 @@ from usermerge.models import Admin, Registry, User
# Create your backends here.
# https://docs.djangoproject.com/en/2.0/topics/auth/customizing/#authentication-backends
# For more information on default/customizing user authentication and password management, see https://docs.djangoproject.com/en/2.0/topics/auth/ .
# For more information on the platform names that are provided in the drop-down list of the login form, see login.html and get_names_of_SoftLab_provided_platforms_from_DB() function of views.py .
# For more information on default/customizing user authentication and password management, see https://docs.djangoproject.com/en/2.0/topics/auth/ and auth.py .
class UserBackend:
def authenticate(self, request, platform = None, username = None, password = None):
"""
Authenticate the credentials POSTed via the login form.
Authenticate the provided credentials, i.e. username and password, for the selected platform.
If the selected platform is SLUB, the user should be authenticated as Admin with the happy path being:
* Try to find an Admin instance of usermergeDB that corresponds to the provided username.
......@@ -24,15 +23,15 @@ class UserBackend:
* If the provided password matches the one of the instance, the user is authenticated successfully as User and
the User instance that is associated with the corresponding Registry instance is returned.
If any of the checks (finding the corresponding instance and comparing the passwords) fails, the authentication
fails and an empty instance (None) is implicitly returned (if the first check fails, the second is never performed).
If any of the checks (finding the corresponding instance and comparing the passwords) fails, the authentication fails
and an empty instance (None) is implicitly returned (if the first check fails, the second one is never performed).
"""
if platform == 'SLUB':
# Admin authentication
try:
admin = Admin.objects.get(username = username)
except Admin.DoesNotExist:
# Run the default password hasher once to reduce the timing difference between an existent and
# Run the default password hasher once to reduce the timing difference between an existing and
# a non-existent Admin instance (#20760), as done in ModelBackend's authenticate() method
# (https://github.com/django/django/blob/master/django/contrib/auth/backends.py).
dummy_password = make_password(password)
......@@ -44,8 +43,8 @@ class UserBackend:
try:
registry = Registry.objects.get(platform__name = platform, username = username)
except Registry.DoesNotExist:
# Run the default password hasher once to reduce the timing difference between an existent and
# a non-existent User instance (#20760), as done in ModelBackend's authenticate() method
# Run the default password hasher once to reduce the timing difference between an existing and
# a non-existent Registry instance (#20760), as done in ModelBackend's authenticate() method
# (https://github.com/django/django/blob/master/django/contrib/auth/backends.py).
dummy_password = make_password(password)
else:
......@@ -54,8 +53,8 @@ class UserBackend:
def get_user(self, user_model_label, user_id):
"""
Return the user instance associated with the given model and id (if model is neither User nor Admin, raise a ValueError exception).
If no user is retrieved, return an empty instance (None).
Return the user instance associated with the given model and id (if model is neither User nor Admin, raise a ValueError exception
with the appropriate error message). If no user is retrieved, return an empty instance (None).
"""
if user_model_label == 'usermerge.User':
try:
......@@ -69,5 +68,5 @@ class UserBackend:
return None
else:
raise ValueError('The user_model_label argument of get_user() backend method '
'should either be "usermerge.User" or "usermerge.Admin"!')
'should be the "usermerge.User" or "usermerge.Admin" string!')
import re
from django import forms
from usermerge.models import Platform
from usermerge.validators import platform_is_selected_from_provided_list, ece_id_is_not_031YY000
# Create your forms here.
# https://docs.djangoproject.com/en/2.0/ref/forms/
# The below-defined forms are based on the models defined in models.py .
# For more information on clean_<fieldname>() and clean() methods, see https://docs.djangoproject.com/en/2.0/ref/forms/validation/ .
# For more information on the platform names that are provided in the drop-down list of the login form, see login.html and get_names_of_SoftLab_provided_platforms_from_DB() function of views.py .
# Each programmatic form from the ones defined below is based format-wise on the similarly named template form (e.g. LoginForm is based on the login_form of login.html, UserProfileEditForm is based on the user_profile_edit_form of user_profile_edit.html, etc.) and constraint-wise on the application models (see models.py).
class LoginForm(forms.Form):
platform = forms.CharField(error_messages = {'required': 'Το δηλωθέν όνομα πλατφόρμας δεν ανταποκρίνεται σε καμία από τις '
'υποστηριζόμενες πλατφόρμες της βάσης!'})
platform = forms.CharField(error_messages = {'required': 'Το όνομα πλατφόρμας πρέπει απαραίτητα να επιλεχθεί και να είναι μη-κενό!'},
validators = [platform_is_selected_from_provided_list])
username = forms.CharField(max_length = 50,
error_messages = {'required': 'Το όνομα χρήστη πρέπει απαραίτητα να συμπληρωθεί!',
'max_length': 'Το όνομα χρήστη δεν πρέπει να υπερβαίνει τους 50 χαρακτήρες!'})
......@@ -19,27 +17,47 @@ class LoginForm(forms.Form):
error_messages = {'required': 'Ο κωδικός πρόσβασης πρέπει απαραίτητα να συμπληρωθεί!',
'max_length': 'Ο κωδικός πρόσβασης δεν πρέπει να υπερβαίνει τους 50 χαρακτήρες!'})
def clean_platform(self):
platform = self.cleaned_data['platform']
# Ensure that the platform (name) is selected among the provided ones in the drop-down list of the login form.
# If it is not (e.g. it is misedited via JavaScript), raise a ValidationError that will be printed under the form.
if platform == 'SLUB' or (platform != 'ECE-NTUA' and platform in Platform.objects.values_list('name', flat = True)):
return platform
else:
raise forms.ValidationError('Το δηλωθέν όνομα πλατφόρμας δεν ανταποκρίνεται σε καμία '
'από τις υποστηριζόμενες πλατφόρμες της βάσης!')
def clean(self):
cleaned_data = super().clean()
# If platform and username have survived the initial individual field checks (by the time the form’s clean() method is called,
# all the individual field clean methods will have been run, so cleaned_data will be populated with any data that has survived
# so far) and platform refers either to Novice or to Grader, ensure that the username format is piYYbRRR (if it is not, raise a
# ValidationError that will be printed under the login form), where YY refers to year and RRR refers to username serial number.
# If platform and username have survived the initial individual field checks (by the time this method is called, all the
# individual field clean methods will have been run, so cleaned_data will be populated with any data that has survived so far)
# and platform refers either to Novice or to Grader, ensure that the username format is piYYbSSS (if it is not, raise a
# ValidationError exception with the appropriate error message and code), where YY refers to year and SSS refers to non-000
# serial number.
if set(('platform', 'username')).issubset(cleaned_data):
platform = cleaned_data['platform']
username = cleaned_data['username']
if platform == 'Novice' or platform == 'Grader':
if not re.match('pi[0-9]{2}b[0-9]{3}', username):
raise forms.ValidationError('Το όνομα χρήστη στις πλατφόρμες Novice και Grader πρέπει να είναι '
'της μορφής piYYbRRR (ΥΥ: έτος, RRR: σειριακός αριθμός ονόματος)!')
if re.fullmatch('pi[0-9]{2}b[0-9]{3}', username):
if username[5:] == '000':
raise forms.ValidationError('Το όνομα χρήστη στις πλατφόρμες Novice και Grader απαγορεύεται να\n'
'είναι της μορφής piYYb000 (YY: έτος)!', code = 'novice_or_grader_username_is_piYYb000')
else:
raise forms.ValidationError('Το όνομα χρήστη στις πλατφόρμες Novice και Grader πρέπει να\n'
'είναι της μορφής piYYbSSS (ΥΥ: έτος, SSS: αύξων αριθμός)!',
code = 'novice_or_grader_username_format_is_not_piYYbSSS')
class UserProfileEditForm(forms.Form):
first_name = forms.CharField(max_length = 50,
error_messages = {'required': 'Το όνομα πρέπει απαραίτητα να συμπληρωθεί!',
'max_length': 'Το όνομα δεν πρέπει να υπερβαίνει τους 50 χαρακτήρες!'})
last_name = forms.CharField(max_length = 50,
error_messages = {'required': 'Το επώνυμο πρέπει απαραίτητα να συμπληρωθεί!',
'max_length': 'Το επώνυμο δεν πρέπει να υπερβαίνει τους 50 χαρακτήρες!'})
ece_id = forms.RegexField(required = False, max_length = 8, regex = '031[0-9]{5}',
# The empty value '' (empty string) becomes None in order to be interpreted as NULL in usermergeDB.
empty_value = None, error_messages = {'max_length': 'Ο αριθμός μητρώου πρέπει να είναι της μορφής '
'031YYSSS (YY: έτος, SSS: αύξων αριθμός)!',
'invalid': 'Ο αριθμός μητρώου πρέπει να είναι της μορφής '
'031YYSSS (YY: έτος, SSS: αύξων αριθμός)!'},
validators = [ece_id_is_not_031YY000])
email = forms.EmailField(max_length = 254,
error_messages = {'required': 'Το ηλεκτρονικό ταχυδρομείο πρέπει απαραίτητα να συμπληρωθεί!',
'max_length': 'Το ηλεκτρονικό ταχυδρομείο δεν πρέπει να υπερβαίνει τους 254 χαρακτήρες!',
'invalid': 'Το ηλεκτρονικό ταχυδρομείο πρέπει να είναι της μορφής local-part@domain που\n'
'περιγράφεται στην ιστοσελίδα https://en.wikipedia.org/wiki/Email_address !'})
class UserProfileRecoveryForm(UserProfileEditForm):
first_name = None
last_name = None
This diff is collapsed.
......@@ -6,7 +6,7 @@ from usermerge import auth
# Create your middleware here.
# https://docs.djangoproject.com/en/2.0/topics/http/middleware/
# AuthenticationMiddleware is a middleware component that associates the current session user with every incoming web request. Original source code for the get_user() function and the aforementioned component can be found in https://github.com/django/django/blob/master/django/contrib/auth/middleware.py (for more information, see https://docs.djangoproject.com/en/2.0/ref/middleware/#django.contrib.auth.middleware.AuthenticationMiddleware).
# AuthenticationMiddleware is a middleware component that associates the current user with every incoming web request. Original source code for the get_user() function and the aforementioned component can be found in https://github.com/django/django/blob/master/django/contrib/auth/middleware.py (for more information, see https://docs.djangoproject.com/en/2.0/ref/middleware/#django.contrib.auth.middleware.AuthenticationMiddleware).
def get_user(request):
if not hasattr(request, '_cached_user'):
......
......@@ -4,18 +4,19 @@ from django.utils.crypto import salted_hmac
# Create your models here.
# https://docs.djangoproject.com/en/2.0/ref/models/
# The below defined models impose on fields only the absolutely necessary DB-related constraints. For more information on elaborate field constraints imposed outside the scope of models, e.g. validation of ece_id and email formats, see forms.py and views.py .
# The below defined models impose on fields only the absolutely necessary DB-related constraints. For more information on elaborate field constraints imposed outside the scope of models, e.g. validation of ece_id and email formats, prohibited clearance of registered ece_id values, etc., see forms.py and views.py .
# Regarding the fields whose null option is set to True (any unique fields whose null option is undeclared, i.e. False by default, should NEVER be allowed to be empty): The empty DB value for DateTimeFields is NULL (interpreted as None in Python), thus designating the absence of value. The empty DB value for CharFields and EmailFields is the empty/zero-length string (''), which is a valid string value. For unique CharFields and EmailFields, such as ece_id and email of the User model, whose value may NOT always be declared when creating/updating the respective DB entries, this could lead in integrity errors ("Duplicate entry '' for key 'ece_id'/'email'"). To avoid such errors, NULL should be used as the empty DB value of these fields (the empty string should NOT be used in this case). For more information, see https://docs.djangoproject.com/en/2.0/ref/models/fields/ .
# Each time a user logs in, an appropriate user_logged_in signal is emitted (see login() function in auth.py) causing the last_login field of the corresponding User/Admin instance to be updated independently of the other fields. Generally, the last_login field of a user instance is non-empty when the other fields are non-empty and it is empty when the others are empty. It is also possible, though, for the last_login field of a User instance to be non-empty when all the other fields are empty (the user logs in and, although his/her profile is completely empty, he/she opts to log out without filling it out) or for the last_login field of an Admin instance to be empty when all the other fields are non-empty (the user has never logged in but his/her profile has already been filled out in a non-session context, e.g. fixture - see https://docs.djangoproject.com/en/2.0/howto/initial-data/, interactive shell - see https://docs.djangoproject.com/en/2.0/ref/django-admin/, file such as populateDB.py, etc.).
# The id and last_login fields are populated/updated automatically (each model is implicitly given an id AutoField that acts as the corresponding primary key, each last_login field is initially empty and is updated whenever the corresponding user_logged_in signal is emitted) and should NOT be declared during the creation/update of model instances. The REMAINING fields should satisfy specific requirements (see examples in populateDB.py):
# * The name field of a Platform instance should NEVER be empty. Its value can be changed validly but it should NEVER become empty.
# * ALL the fields of a Registry instance should be NON-empty. The user (user_id), username and password values can be changed validly (the platform - platform_id - value should NEVER be changed) but they should NEVER become empty.
# * ALL the fields of an Admin instance should be NON-empty. Their values can be changed validly but they should NEVER become empty.
# * The fields of a User instance should ALWAYS be in ONE of the following states: 1)ALL filled out, 2)ALL filled out EXCEPT FOR ece_id, 3)NONE filled out. The allowed transitions between field states are: 1-->1, 2-->1, 2-->2, 3-->1, 3-->2 and 3-->3 (empty field values can be changed to valid non-empty ones, non-empty field values can be changed validly but they should NEVER become empty).
# * The name field of a Platform instance should NEVER be empty. Its value can be changed validly/consistently but it should NEVER become empty. A Platform instance can be created/changed in usermergeDB ONLY in a non-session context, e.g. fixture, interactive shell, etc. (see the relative references above).
# * ALL the fields of a Registry instance should be NON-empty. The user (user_id), username and password values can be changed validly/consistently (the platform - platform_id - value should NEVER be changed) but they should NEVER become empty. When a Registry instance is initially created, an empty User instance should also be created and associated with that Registry instance - see the below explanation about the User instance fields.
# * ALL the fields of an Admin instance should be NON-empty. Their values can be changed validly/consistently but they should NEVER become empty. An Admin instance can be created/changed in usermergeDB ONLY in a non-session context, e.g. fixture, interactive shell, etc. (see the relative references above).
# * The fields of a User instance should ALWAYS be in ONE of the following states: 1)ALL filled out, 2)ALL filled out EXCEPT FOR ece_id, 3)NONE filled out. The allowed transitions between field states are: 1-->1, 2-->1, 2-->2, 3-->1 and 3-->2 (empty field values can be changed to valid/consistent non-empty ones, non-empty field values can be changed validly/consistently but they should NEVER become empty). When a User instance is initially created, ALL its fields should be empty (state 3) and EXACTLY ONE Registry instance should be created to reference it - see the above explanation about the Registry instance fields. The fields can then be changed/filled out (state 1/2) ONLY by the corresponding user via the user profile edit form after he/she has logged in first - see the edit_user_profile() view (any other way of changing/filling out the fields is by NO means accepted).
# The password field of a user instance should NEVER contain the user's raw password. It should, instead, contain a hashed version of the latter created by make_password() function for enhanced security (see examples in populateDB.py). The aforementioned function creates the hashed password using the PBKDF2 algorithm with a SHA256 hash by default. The length of the hashed password is 78 characters. For more information on password management, see https://docs.djangoproject.com/en/2.0/topics/auth/passwords/ .
# Given that support for time zones is enabled (USE_TZ setting is True), DateTimeField values are stored in UTC format in usermergeDB and are rendered in the current time zone, that is 'Europe/Athens' by default (specified by the TIME_ZONE setting), in templates. For more information on the time zone selection logic and render format, see https://docs.djangoproject.com/en/2.0/topics/i18n/timezones/ and the 'Internationalization' part of settings.py .
# Raw passwords can be reported/logged/printed (see views.py) ONLY after they have first been filtered/hidden (see, for example, https://docs.djangoproject.com/en/2.0/howto/error-reporting/).
# Given that support for time zones is enabled (USE_TZ setting is True), DateTimeField values are stored in UTC format in usermergeDB and are displayed in the current time zone, that is 'Europe/Athens' by default (specified by the TIME_ZONE setting), in templates. For more information on the time zone selection logic and display format, see https://docs.djangoproject.com/en/2.0/topics/i18n/timezones/ and the 'Internationalization' part of settings.py .
# The get_session_auth_hash() methods and is_authenticated attributes are needed for user authentication, e.g. in login() and get_user() functions of auth.py and login_required() decorator of views.py . Original source code for these methods/attributes can be found in https://github.com/django/django/blob/master/django/contrib/auth/base_user.py (for more information, see https://docs.djangoproject.com/en/2.0/topics/auth/customizing/).
class User(models.Model):
......
......@@ -6,7 +6,7 @@ from django.contrib.auth.hashers import make_password
from usermerge.models import Admin, Platform, Registry, User
# Populate Admin table with test admins.
admin_hashed_password = make_password('sesame')
admin_hashed_password = make_password('7wonders')
admin1 = Admin.objects.create(first_name = 'Νικόλαος', last_name = 'Παπασπύρου', email = 'nikolaos@softlab.ntua.gr',
username = 'nikolaos', password = admin_hashed_password)
admin2 = Admin.objects.create(first_name = 'Ευστάθιος', last_name = 'Ζάχος', email = 'eustathios@corelab.ntua.gr',
......@@ -20,6 +20,7 @@ moodle = Platform.objects.create(name = 'Moodle')
plgrader = Platform.objects.create(name = 'PLgrader')
# Populate User table with test users.
# The user1 and user2 instances are created with filled out fields. This creation method can SOMETIMES be used during development to assist testing/debugging, but it is NOT generally accepted and should ALWAYS be avoided in production. On the other hand, creating User instances with empty fields, such as user3, is the accepted method and should ALWAYS be preferred both during development and in production.
user1 = User.objects.create(first_name = 'Γεώργιος', last_name = 'Καζελίδης', ece_id = '03199999',
email = 'gkazelid@undergraduate.ece.ntua.gr')
user2 = User.objects.create(first_name = 'Ζαχαρίας', last_name = 'Δόγκανος', email = 'zdogkanos@undergraduate.ece.ntua.gr')
......
{% extends 'base.html' %}
{% block title %}
SLUB - Είσοδος Χρήστη/Διαχειριστή
SLUB - Είσοδος
{% endblock %}
{% block content %}
<h4>Είσοδος Χρήστη/Διαχειριστή</h4>
<h4>Είσοδος</h4>
<form id="login_form" accept-charset="utf-8" action="{% url 'submit_login' %}" method="post">
{% csrf_token %}
......@@ -28,13 +28,13 @@
<tr>
<td style="text-align:right;"><label for="username">Όνομα χρήστη:</label></td>
<td style="text-align:left;">
<input type="text" name="username" id="username" maxlength="50" title="" required="required" />
<input type="text" name="username" id="username" maxlength="50" required="required" />
</td>
</tr>
<tr>
<td style="text-align:right;"><label for="password">Κωδικός πρόσβασης:</label></td>
<td style="text-align:left;">
<input type="password" name="password" id="password" maxlength="50" title="" required="required" />
<input type="password" name="password" id="password" maxlength="50" required="required" />
</td>
</tr>
</tbody>
......
{% extends 'base.html' %}
{% block title %}
SLUB - Έξοδος Χρήστη/Διαχειριστή
{% endblock %}
{% block content %}
<h4>Έξοδος Χρήστη/Διαχειριστή</h4>
<p>Εξήλθατε με επιτυχία!</p>
<p><a href="{% url 'default_login' %}">Θα θέλατε μήπως να εισέλθετε ξανά;</a></p>
{% endblock %}
* W3Schools - HTML5 Tutorial: https://www.w3schools.com/Html/default.asp
* Django References - Templates: https://docs.djangoproject.com/en/2.0/ref/templates/
* HTML5, CSS, JavaScript and XML tutorials:
- https://www.w3schools.com/
- https://developer.mozilla.org/en-US/docs/Web
* XHTML5 (polyglot markup) usage:
- https://wiki.whatwg.org/wiki/HTML_vs._XHTML
- http://xmlplease.com/xhtml/xhtml5polyglot/
* Django API reference for templates:
- https://docs.djangoproject.com/en/2.0/ref/templates/
* Unicode data support:
- https://stackoverflow.com/questions/38363566/trouble-with-utf-8-characters-what-i-see-is-not-what-i-stored
* <meta> element usage:
- https://moz.com/blog/seo-meta-tags
* Linkage to external style sheets or script files via the jsDelivr CDN:
- https://www.jsdelivr.com/
* <label> element usage:
- https://stackoverflow.com/questions/7636502/why-use-label
......@@ -17,7 +17,32 @@
<h4>Αρχική Σελίδα Χρήστη</h4>
<nav id="user_home_nav">
{% if user.ece_id is None %}
<p>
<strong>
{% if user.first_name == '' and user.last_name == '' and user.email is None %}
&#x272A; Παρακαλούμε συμπληρώστε άμεσα τα στοιχεία του προφίλ σας επιλέγοντας "Επεξεργασία Προφίλ"!
{% else %}
&#x272A; Αν διαθέτετε αριθμό μητρώου, παρακαλούμε συμπληρώστε άμεσα το <br />
αντίστοιχο πεδίο του προφίλ επιλέγοντας "Επεξεργασία Προφίλ"!
{% endif %}
</strong>
</p>
{% endif %}
<nav id="home_nav">
{% comment %}
The "Edit Profile" and "Log out" options are ALWAYS available to the user. ALL the OTHER
navigation options are available to him/her ONLY if he/she has filled out his/her profile.
{% endcomment %}
<p><a href="{% url 'default_user_profile_edit' user.id %}">Επεξεργασία Προφίλ</a></p>
{% if user.first_name != '' and user.last_name != '' and user.email is not None %}
{# Insert ALL the OTHER navigation options HERE! #}
{# ... #}
{# ... #}
{# ... #}
{% endif %}
<p><a href="{% url 'logout' %}">Έξοδος</a></p>
</nav>
{% endblock %}
{% extends 'base.html' %}
{% block title %}
SLUB - Επεξεργασία Προφίλ Χρήστη
{% endblock %}
{% block content %}
<nav id="session_nav">
{% if user.first_name and user.last_name %}
Έχετε εισέλθει ως <strong>{{ user.first_name }} {{ user.last_name }}</strong>!
{% else %}
Έχετε εισέλθει επιτυχώς!
{% endif %}
| <a href="{% url 'user_home' user.id %}">Αρχική Σελίδα</a>
| <a href="{% url 'logout' %}">Έξοδος</a>
</nav>
<h4>Επεξεργασία Προφίλ Χρήστη</h4>
{% if user.first_name == '' and user.last_name == '' and user.email is None %}
<p>
<strong>
&#x272A; Αν καταχωρήσατε τα στοιχεία του προφίλ σας κατά τη διάρκεια παλιότερης εισόδου σας στο σύστημα και <br />
αυτά δεν εμφανίζονται τώρα στην παρακάτω φόρμα (είτε γιατί έχετε αλλάξει τα διαπιστευτήρια της <br />
πλατφόρμας εισόδου είτε γιατί έχετε εισέλθει για πρώτη φορά με διαπιστευτήρια αυτής της πλατφόρμας), <br />
παρακαλούμε ανακτήστε τα από τη βάση χωρίς να συμπληρώσετε τα πεδία της παρακάτω φόρμας! <br />
<a href="{% url 'default_user_profile_recovery' user.id %}">&#x2192; Ανάκτηση χωρίς Συμπλήρωση</a>
</strong>
</p>
<br />
{% endif %}
<form id="user_profile_edit_form" accept-charset="utf-8" action="{% url 'edit_user_profile' user.id %}" method="post">
{% csrf_token %}
<table>
<tbody>
<tr>
<td style="text-align:right;"><label for="first_name">Όνομα:</label></td>
<td style="text-align:left;">
<input type="text" name="first_name" id="first_name" maxlength="50"
value="{{ user.first_name }}" required="required" />
</td>
</tr>
<tr>
<td style="text-align:right;"><label for="last_name">Επώνυμο:</label></td>
<td style="text-align:left;">
<input type="text" name="last_name" id="last_name" maxlength="50"
value="{{ user.last_name }}" required="required" />
</td>
</tr>
<tr>
<td style="text-align:right;"><label for="ece_id">Αριθμός μητρώου<span style="color:darkorange;">*</span>:</label></td>
<td style="text-align:left;">
<input type="text" name="ece_id" id="ece_id" maxlength="8" pattern="031[0-9]{5}"
{% if user.ece_id is not None %}value="{{ user.ece_id }}"{% endif %} />
</td>
</tr>
<tr>
<td style="text-align:right;"><label for="email">Ηλεκτρονικό ταχυδρομείο:</label></td>
<td style="text-align:left;">
<input type="email" name="email" id="email" maxlength="254"
{% if user.email is not None %}value="{{ user.email }}"{% endif %} required="required" />
</td>
</tr>
</tbody>
</table>
<p style="color:darkorange;">
<strong>
* Αν δεν διαθέτετε αριθμό μητρώου ακόμα, μπορείτε προσωρινά να παραλείψετε το αντίστοιχο πεδίο! <br />
Σε αντίθετη περίπτωση, παρακαλούμε συμπληρώστε το άμεσα!
</strong>
</p>
<input type="submit" value="Αποθήκευση" />
</form>
{% if success_message %}
<p style="color:green;">
<img src="images/tick.png" alt="Επιβεβαίωση:" />
<strong>{{ success_message | linebreaksbr }}</strong>
</p>
{% elif error_messages %}
{% for message in error_messages %}
<p style="color:red;">
<img src="images/warning.svg" alt="Σφάλμα:" />
<strong>{{ message | linebreaksbr }}</strong>
</p>
{% endfor %}
{% endif %}
<p><a href="{% url 'user_home' user.id %}">&#x21B5; Επιστροφή στην Αρχική Σελίδα</a></p>
{% endblock %}
{% extends 'base.html' %}
{% block title %}
SLUB - Ανάκτηση Προφίλ Χρήστη
{% endblock %}
{% block content %}
<nav id="session_nav">
Έχετε εισέλθει επιτυχώς!
| <a href="{% url 'user_home' user.id %}">Αρχική Σελίδα</a>
| <a href="{% url 'logout' %}">Έξοδος</a>
</nav>
<h4>Ανάκτηση Προφίλ Χρήστη</h4>
<p>
<strong>
&#x272A; Για τον εντοπισμό του επιθυμητού προφίλ προς ανάκτηση, παρακαλούμε συμπληρώστε <br />
τον αριθμό μητρώου (αν υπάρχει καταχωρημένος) και το ηλεκτρονικό ταχυδρομείο <br />
όπως ακριβώς τα έχετε καταχωρήσει στη βάση!
</strong>
</p>
<form id="user_profile_recovery_form" accept-charset="utf-8" action="{% url 'search_for_recovery_user_profile' user.id %}" method="post">
{% csrf_token %}
<table>
<tbody>
<tr>
<td style="text-align:right;"><label for="ece_id">Αριθμός μητρώου:</label></td>
<td style="text-align:left;">
<input type="text" name="ece_id" id="ece_id" maxlength="8" pattern="031[0-9]{5}" />
</td>
</tr>
<tr>
<td style="text-align:right;"><label for="email">Ηλεκτρονικό ταχυδρομείο:</label></td>
<td style="text-align:left;">
<input type="email" name="email" id="email" maxlength="254" required="required" />
</td>
</tr>
</tbody>
</table>
<br />
<input type="submit" value="Αναζήτηση" />
</form>
{% if success_message %}
<p style="color:green;">
<img src="images/tick.png" alt="Επιβεβαίωση:" />
<strong>
{{ success_message | linebreaksbr }}
<br />
<a href="{% url 'recover_user_profile' user.id recov_user.id %}">&#x2192; Ανάκτηση και Έξοδος</a>
</strong>
</p>
{% elif error_messages %}
{% for message in error_messages %}
<p style="color:red;">
<img src="images/warning.svg" alt="Σφάλμα:" />
<strong>{{ message | linebreaksbr }}</strong>
</p>
{% endfor %}
{% endif %}
<p><a href="{% url 'default_user_profile_edit' user.id %}">&#x21B5; Επιστροφή στην Επεξεργασία Προφίλ</a></p>
{% endblock %}
......@@ -13,13 +13,24 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.urls import re_path
from usermerge import views
urlpatterns = [
re_path(r'^login/default/', views.show_default_login_page, name = 'default_login'),
re_path(r'^login/submit/', views.log_in, name = 'submit_login'),
re_path(r'^home/user/', views.show_user_home_page, name = 'user_home'),
re_path(r'^home/admin/', views.show_admin_home_page, name = 'admin_home'),
re_path(r'^logout/', views.log_out, name = 'logout'),
# The default login page serves as index page. Therefore, its regular route becomes r'^$' instead of r'^login/default$'.
re_path(r'^$', views.display_default_login_page, name = 'default_login'),
re_path(r'^login/submit$', views.log_in, name = 'submit_login'),
re_path(r'^user/home/id=(\d{1,10})$', views.display_user_home_page, name = 'user_home'),
re_path(r'^user/profile/edit/default/id=(\d{1,10})$', views.display_default_user_profile_edit_page, name = 'default_user_profile_edit'),
re_path(r'^user/profile/edit/submit/id=(\d{1,10})$', views.edit_user_profile, name = 'edit_user_profile'),
re_path(r'^user/profile/recovery/default/id=(\d{1,10})$', views.display_default_user_profile_recovery_page,
name = 'default_user_profile_recovery'),
re_path(r'^user/profile/recovery/search/id=(\d{1,10})$', views.search_for_recovery_user_profile,
name = 'search_for_recovery_user_profile'),
re_path(r'^user/profile/recovery/submit/id=(\d{1,10})/recover/id=(\d{1,10})$', views.recover_user_profile,
name = 'recover_user_profile'),
re_path(r'^admin/home/id=(\d{1,10})$', views.display_admin_home_page, name = 'admin_home'),
re_path(r'^logout$', views.log_out, name = 'logout'),
]
from django.core.exceptions import ValidationError
from usermerge.models import Platform
# Create your validators here.
# https://docs.djangoproject.com/en/2.0/ref/validators/
# The validators of a form field are run in the context of run_validators() method when the clean() method of the field is called. If the field value is empty, i.e. None, '', [], () or {}, run_validators() will NOT run the validators (see https://docs.djangoproject.com/en/2.0/_modules/django/forms/fields/). Therefore, it can be safely assumed that the single arguments of the following validators are NEVER empty.
# For more information on the platform names that are available to select from in forms, see get_names_of_SoftLab_provided_platforms_from_DB() function of helper.py . In the case of login form, see also login_form of login.html .
def platform_is_selected_from_provided_list(platform):
"""
Ensure that the platform (name) is selected among the ones provided in the corresponding form drop-down list.
If it is not (e.g. it is misedited via JavaScript), raise a ValidationError exception with the appropriate error message and code.
"""
if platform != 'SLUB' and not (platform != 'ECE-NTUA' and platform in Platform.objects.values_list('name', flat = True)):
raise ValidationError('Το όνομα πλατφόρμας πρέπει να επιλέγεται μεταξύ εκείνων\n'
'που παρέχονται στην αντίστοιχη αναπτυσσόμενη λίστα!', code = 'platform_is_not_selected_from_provided_list')
def ece_id_is_not_031YY000(ece_id):
"""
Ensure that the ece_id (format) is not 031YY000.
If it is, raise a ValidationError exception with the appropriate error message and code.
"""
if ece_id[5:] == '000':
raise ValidationError('Ο αριθμός μητρώου απαγορεύεται να είναι της μορφής 031YY000 (YY: έτος)!', code = 'ece_id_is_031YY000')
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment