Commit e6f4aa1b authored by Giorgos Kazelidis's avatar Giorgos Kazelidis

- Installed concurrent-log-handler and set up the logging system

- Loaded the time zone tables in mysql database
- Added Unicode/UTF-8 data support in usermergeDB and templates
- Replaced CLASS_SESSION_KEY with PLATFORM_SESSION_KEY in the session data as the latter indicates both user model and platform
- Corrected the session auth hash calculation method of both user models to take password (and platform, in case of User model) into account
- Enhanced template structure and functionality by using XHTML5 syntax, new HTML5 semantic elements, html5shiv.js, normalize.css, field labels, appropriate images/icons, etc. and moving internal CSS to base.css
- Made some minor changes in models (changed max_length of first_name and last_name fields) and database connection options (introduced SET_DEFAULT_STORAGE_ENGINE_TO_INNODB variable and changed SQL mode)
parent 219ab1a8
......@@ -2,10 +2,13 @@
* Working OS: Ubuntu 16.04 LTS
* Python version: 3.5.2 (pre-installed)
* Git version: 2.7.4
* pip version: 8.1.1
* pip3 version: 10.0.1
* virtualenv version: 15.2.0
* Django version: 2.0.4
* MySQL server version: 5.7.22
* pytz version: 2018.4
* concurrent-log-handler version: 0.9.12
* mysqlclient version: 1.3.12
* MySQL server version: 5.7.24
## DOWNLOADING THE PROJECT & SETTING UP THE ENVIRONMENT
(A) install pip3 (`pip3 --version` to see if it is already installed):
......@@ -37,13 +40,20 @@
      (3) navigate to the local instance:
cd userbase
(E) install Django and MySQL inside the local instance using virtualenv with Python 3 [sources:
(E) install and configure Django and MySQL inside the local instance using virtualenv with Python 3 [sources:
      -- steps 2 and 3 of https://help.dreamhost.com/hc/en-us/articles/215317948-How-to-install-Django-using-virtualenv
      -- step 3 of https://www.digitalocean.com/community/tutorials/how-to-create-a-django-app-and-connect-it-to-a-database
      -- end of section http://docs.python-guide.org/en/latest/dev/virtualenvs/#lower-level-virtualenv
      -- https://docs.djangoproject.com/en/2.0/topics/install/
      -- https://pypi.org/project/mysqlclient/
      -- https://pypi.org/project/concurrent-log-handler/
      -- https://www.digitalocean.com/community/tutorials/how-to-install-mysql-on-ubuntu-16-04
      -- https://dev.mysql.com/doc/refman/5.7/en/mysql-tzinfo-to-sql.html
      -- https://datawookie.netlify.com/blog/2017/08/setting-up-time-zones-in-mysql/
      -- "Create a MySQL Database" section of http://www.marinamele.com/taskbuster-django-tutorial/install-and-configure-mysql-for-django
      -- https://gist.github.com/hofmannsven/9164408
      -- https://dev.mysql.com/doc/refman/5.7/en/sql-syntax.html
      -- https://stackoverflow.com/questions/43644218/why-is-table-charset-set-to-utf8mb4-and-collation-to-utf8mb4-unicode-520-ci?rq=1
      -- https://www.tutorialspoint.com/django/django_admin_interface.htm
      -- https://docs.djangoproject.com/en/2.0/ref/django-admin/
      -- https://docs.python.org/3/tutorial/interpreter.html
......@@ -65,42 +75,53 @@
      (5) install appropriate versions of virtual-environment-specific packages:
pip install -r requirements.txt
           ALTERNATIVELY, you could install the packages (Django and MySQL Database Connector - mysqlclient) individually by typing the following:
            ALTERNATIVELY, you could install the packages (Django, mysqlclient, concurrent-log-handler and pytz - the last one is installed alongside Django) individually by typing the following:
pip install Django==2.0.4
pip install Django
pip install mysqlclient
       (6) install MySQL server (`mysql --version` to see if it is already installed):
             * WARNING: while installing MySQL server, you will be asked to set the password for the root user.
pip install concurrent-log-handler
      (6) install MySQL server (`mysqld --version` to see if it is already installed):
            * ATTENTION: while installing MySQL server, you will be asked to set the password for the root user.
sudo apt-get install mysql-server
mysql_secure_installation
       (7) use the password that was set for the root user during MySQL server installation as the 'PASSWORD' value of the 'default' dictionary of the DATABASES dictionary in **~/Desktop/userbase/myprj/myprj/settings.py**
       (8) open MySQL console with root privileges:
      (7) use the password that was set for the root user during MySQL server installation as the DATABASES['default']['PASSWORD'] value in **~/Desktop/userbase/myprj/myprj/settings.py**
      (8) copy the time zone definitions of the system's zoneinfo database, which is located in the /usr/share/zoneinfo/ directory of Ubuntu and most other Unix-like systems, to the "mysql" database without worrying about some possible "Unable to load/Skipping" warnings (you will be asked to provide the password that was set for the MySQL root user) and restart MySQL server (you can skip this substep if you have already executed it once):
mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root -p mysql
sudo service mysql restart
      (9) navigate to the project directory (myprj):
cd myprj
      (10) open MySQL console with root privileges:
python manage.py dbshell
           ALTERNATIVELY, you could run the following command by providing the password that was set for the root user during MySQL server installation:
            ALTERNATIVELY, you could run the following command by providing the password that was set for the MySQL root user:
mysql -u root -p
       (9) if the project database (usermergeDB) has already been created (`SHOW DATABASES;` to check), delete it:
      (11) if the project database (usermergeDB) has already been created (`SHOW DATABASES;` to check), delete it:
DROP DATABASE usermergeDB;
       (10) create the project database:
      (12) create the project database (no tables are created during this substep) with "utf8mb4" as the character set and X as the collation, where X is "utf8mb4_0900_ci_ai" for MySQL (server version) 8.0, "utf8mb4_unicode_520_ci" for MySQL 5.6/5.7 or "utf8mb4_unicode_ci" for MySQL 5.5:
CREATE DATABASE usermergeDB;
       (11) exit MySQL console:
CREATE DATABASE usermergeDB CHARACTER SET utf8mb4 COLLATE X;
      (13) exit MySQL console:
EXIT;
       (12) initiate the database:
      (14) use the collation that was selected during the project database creation as the DATABASES['default']['OPTIONS']['init_command']#collation_connection value in **~/Desktop/userbase/myprj/myprj/settings.py**
      (15) if MySQL server version is 5.5.4 and earlier, set SET_DEFAULT_STORAGE_ENGINE_TO_INNODB to "True" in **~/Desktop/userbase/myprj/myprj/settings.py**
      (16) initiate the project database (the tables are created during this substep):
python manage.py migrate
       (13) open Python interpreter in interactive mode:
      (17) if you have previously set SET_DEFAULT_STORAGE_ENGINE_TO_INNODB to "True", set it back to "False"
      (18) open Python interpreter in interactive mode:
python manage.py shell
       (14) populate the database with test data, i.e. execute populateDB.py script, by typing the following line and pressing `Enter` TWICE:
      (19) populate the project database with test data, i.e. execute the populateDB.py script of the application directory (usermerge), by typing the following line and pressing `Enter` twice:
with open('./populateDB.py') as popDB: exec(popDB.read())
       (15) close Python interpreter by pressing `Ctrl-D`
       (16) deactivate the virtual environment:
with open('./usermerge/populateDB.py') as popDB: exec(popDB.read())
      (20) close Python interpreter by pressing `Ctrl-D`
      (21) deactivate the virtual environment:
deactivate
......@@ -109,7 +130,7 @@
cd ~/Desktop/userbase
source venv/bin/activate
(B) navigate to the project directory (myprj):
(B) navigate to the project directory:
cd myprj
(C\) start the Django development server (at http://127.0.0.1:8000/):
......@@ -119,14 +140,14 @@
## STOPPING THE PROJECT (AND EXITING THE VIRTUAL ENVIRONMENT)
(A) stop the Django development server (running at http://127.0.0.1:8000/):
       -- return to the terminal where you have run `python manage.py runserver` and press `Ctrl-C`
      -- return to the terminal where you have run `python manage.py runserver` and press `Ctrl-C`
(B) deactivate the virtual environment:
deactivate
## UPDATING THE PROJECT
Provided you have already downloaded the (source code of the) project (that is, you have followed the steps of the "DOWNLOADING THE PROJECT & SETTING UP THE ENVIRONMENT" section above), the recommended steps to update the (source code of the) project are:
       (1) delete the local instance of the remote repository (userbase) as well as the virtual environment:
      (1) delete the local instance of the remote repository (userbase) as well as the virtual environment:
rm -rf ~/Desktop/userbase
       (2) follow steps (D) and (E) of the "DOWNLOADING THE PROJECT & SETTING UP THE ENVIRONMENT" section above
      (2) follow steps (D) and (E) of the "DOWNLOADING THE PROJECT & SETTING UP THE ENVIRONMENT" section above
......@@ -10,7 +10,7 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.0/ref/settings/
"""
import os
import concurrent_log_handler, logging.config, os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
......@@ -70,32 +70,168 @@ TEMPLATES = [
WSGI_APPLICATION = 'myprj.wsgi.application'
# Authentication
# https://docs.djangoproject.com/en/2.0/topics/auth/
AUTHENTICATION_BACKENDS = ['usermerge.backends.UserBackend']
LOGIN_URL = '/usermerge/login/default/'
LOGIN_URL = 'default_login'
# Logging
# https://docs.djangoproject.com/en/2.0/topics/logging/
# * django.utils.log module (default configuration dictionary, setup process and extensions): https://docs.djangoproject.com/en/2.0/_modules/django/utils/log/
# * Replacement of the default configuration dictionary and setup process with appropriate custom ones:
# - https://www.caktusgroup.com/blog/2015/01/27/Django-Logging-Configuration-logging_config-default-settings-logger/
# - https://lincolnloop.com/blog/django-logging-right-way/
# * Concurrent/safe logging to files from multiple processes via the concurrent-log-handler module (supports both Windows and POSIX systems):
# - https://pypi.org/project/concurrent-log-handler/
# - https://bugs.python.org/issue4749
# - 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).
# * Detailed configuration and usage examples: https://www.webforefront.com/django/setupdjangologging.html
def LOG_LEVEL_IS_LOWER_THAN_ERROR(log_record):
# This is the callback function of the exclude_errors log filter.
# It is used to reject the ERROR and CRITICAL log records/messages that pass through the filter.
if log_record.levelname in ['ERROR', 'CRITICAL']:
return False
else:
return True
LOGGING_CONFIG = None
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse',
},
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
'exclude_errors': {
'()': 'django.utils.log.CallbackFilter',
'callback': LOG_LEVEL_IS_LOWER_THAN_ERROR,
},
},
'formatters': {
'django.server': {
'()': 'django.utils.log.ServerFormatter',
'format': '[%(server_time)s] %(message)s',
},
'verbose': {
'format': '[%(asctime)s,%(msecs)d <%(levelname)s> - path: %(pathname)s, line: %(lineno)d,'
' func: %(funcName)s, logger: %(name)s, process/thread: %(process)d/%(thread)d] %(message)s',
'datefmt': '%d/%b/%Y %H:%M:%S',
},
'standard': {
'format': '[%(asctime)s,%(msecs)d <%(levelname)s> - file: %(filename)s, line: %(lineno)d,'
' func: %(funcName)s, logger: %(name)s] %(message)s',
'datefmt': '%d/%b/%Y %H:%M:%S',
},
'laconic': {
'format': '[%(asctime)s,%(msecs)d <%(levelname)s> - file: %(filename)s, line: %(lineno)d] %(message)s',
'datefmt': '%d/%b/%Y %H:%M:%S',
},
},
'handlers': {
'console': {
'level': 'DEBUG',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
'django.server': {
'level': 'INFO',
'class': 'logging.StreamHandler',
'formatter': 'django.server',
},
'file_for_general_info_and_warnings': {
'level': 'INFO',
'filters': ['require_debug_false', 'exclude_errors'],
'class': 'logging.handlers.ConcurrentRotatingFileHandler',
'filename': os.path.join(BASE_DIR, 'usermerge/production_logs/general_info_and_warnings.log'),
'maxBytes': 1024 * 100, # 100 KB
'backupCount': 5,
'formatter': 'verbose',
},
'file_for_errors': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'logging.handlers.ConcurrentRotatingFileHandler',
'filename': os.path.join(BASE_DIR, 'usermerge/production_logs/errors.log'),
'maxBytes': 1024 * 200, # 200 KB
'backupCount': 5,
'formatter': 'verbose',
},
},
'root': {
'level': 'DEBUG' if DEBUG else 'INFO',
'handlers': ['console', 'file_for_general_info_and_warnings', 'file_for_errors'],
},
'loggers': {
'django': {
'level': 'DEBUG' if DEBUG else 'INFO',
'handlers': ['console', 'file_for_general_info_and_warnings', 'file_for_errors'],
'propagate': False,
},
'django.server': {
'level': 'INFO',
'handlers': ['django.server'],
'propagate': False,
},
'usermerge': {
'level': 'DEBUG' if DEBUG else 'INFO',
'handlers': ['console', 'file_for_general_info_and_warnings', 'file_for_errors'],
'propagate': False,
},
},
}
logging.config.dictConfig(LOGGING)
# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
# https://docs.djangoproject.com/en/2.0/ref/databases
# * MySQL notes: https://docs.djangoproject.com/en/2.0/ref/databases/#mysql-notes
# If the database tables have NOT been created yet and the database server's default storage engine is MyISAM (MySQL 5.5.4 and earlier),
# SET_DEFAULT_STORAGE_ENGINE_TO_INNODB should be set to True. The default storage engine is thus set to InnoDB upon connecting to the
# database via the init_command option (it is reverted back to MyISAM when the established connection is closed). The corresponding query
# is needed ONLY during table creation. After the tables have been created, it should be omitted from future connections by setting
# SET_DEFAULT_STORAGE_ENGINE_TO_INNODB back to False.
SET_DEFAULT_STORAGE_ENGINE_TO_INNODB = False # see README.md
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'usermergeDB',
'USER': 'root',
'PASSWORD': 'SET_ROOT_PASSWORD_HERE',
'PASSWORD': 'SET_ROOT_PASSWORD_HERE', # see README.md
'HOST': '',
'PORT': '',
'OPTIONS': {
'init_command': 'SET default_storage_engine = INNODB;' \
"SET sql_mode = 'STRICT_TRANS_TABLES';" \
# greek language support
# https://stackoverflow.com/questions/37307146/difference-between-utf8mb4-unicode-ci-and-utf8mb4-unicode-520-ci-collations-in-m
# https://stackoverflow.com/questions/43644218/why-is-table-charset-set-to-utf8mb4-and-collation-to-utf8mb4-unicode-520-ci?rq=1
'ALTER DATABASE usermergeDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci;',
},
}
# The charset option is set to utf8mb4 and the collation_connection variable in the init_command option is set to
# utf8mb4_0900_ci_ai for MySQL 8.0, utf8mb4_unicode_520_ci for MySQL 5.6/5.7 or utf8mb4_unicode_ci for MySQL 5.5
# in order to better support the representation and sorting/comparison of Unicode data in the database.
# For more information, see
# https://stackoverflow.com/questions/43644218/why-is-table-charset-set-to-utf8mb4-and-collation-to-utf8mb4-unicode-520-ci?rq=1 ,
# https://stackoverflow.com/questions/38363566/trouble-with-utf-8-characters-what-i-see-is-not-what-i-stored and
# https://mathiasbynens.be/notes/mysql-utf8mb4 .
'charset': 'utf8mb4',
# The sql_mode option includes the same modes as the default SQL mode in MySQL 5.7 (https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html).
# Even if it is changed at some later point, it should ALWAYS include the STRICT_TRANS_TABLES mode.
'sql_mode': 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,'
'ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION',
'init_command': 'SET default_storage_engine = InnoDB;' if SET_DEFAULT_STORAGE_ENGINE_TO_INNODB else ''
'SET collation_connection = SET_COLLATION_CONNECTION_HERE;', # see README.md
},
},
}
......@@ -136,3 +272,4 @@ USE_TZ = True
# https://docs.djangoproject.com/en/2.0/howto/static-files/
STATIC_URL = '/static/'
"""
Modified version of django.contrib.auth module (https://docs.djangoproject.com/en/2.0/_modules/django/contrib/auth/).
"""
from django.apps import apps as django_apps
from django.conf import settings
from django.contrib.auth import _get_backends, load_backend
from django.contrib.auth.signals import user_logged_in
from django.middleware.csrf import rotate_token
from django.utils.crypto import constant_time_compare
from usermerge.models import Registry
# For more information on default/customizing user authentication and password management, see https://docs.djangoproject.com/en/2.0/topics/auth/ .
# 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/ .
SESSION_KEY = '_auth_user_id'
PLATFORM_SESSION_KEY = '_auth_platform'
BACKEND_SESSION_KEY = '_auth_user_backend'
CLASS_SESSION_KEY = '_auth_user_class'
HASH_SESSION_KEY = '_auth_user_hash'
SESSION_KEY = '_auth_user_id'
def get_user_model(user_class):
def _get_user_model_label(request):
"""
Return the user model (User/Admin from usermerge.models - see models.py) that is active in the session.
Return the label of the user model that is active in the session based on the platform selected during login.
If the selected platform was SLUB, return the label of the Admin model. Otherwise, return the label of the User model.
"""
# user_class is either 'usermerge.User' or 'usermerge.Admin' (the format should necessarily be 'app_label.model_name').
return django_apps.get_model(user_class, require_ready = False)
# The label of the user model should be formatted as 'app_label.user_model_name', where app_label is the application label (usermerge)
# and user_model_name is the name of some user model (User/Admin) defined in the application models (see models.py).
# WARNING: The model officially used for user representation is stored as label in the AUTH_USER_MODEL setting and is generally based
# either on the AbstractBaseUser or on the AbstractUser model (see https://docs.djangoproject.com/en/2.0/topics/auth/customizing/).
# This means that AUTH_USER_MODEL should, among others, have a password field and possibly a username field, which is NOT true for
# the primary user model of the usermerge application, 'usermerge.User'. To bypass this issue during authentication, AUTH_USER_MODEL
# is omitted and 'usermerge.User' is used directly alongside usermerge's secondary user model, 'usermerge.Admin'. Although the latter
# has both a username and a password field, it does NOT inherit from the AbstractBaseUser or AbstractUser model for simplicity as
# well as peer-conformance reasons. AUTH_USER_MODEL is undeclared in settings.py, i.e. 'auth.User' (AbstractUser-inherited model) by
# default, and should remain that way as it is practically replaced by 'usermerge.User' and 'usermerge.Admin'.
if request.session[PLATFORM_SESSION_KEY] == 'SLUB':
return 'usermerge.Admin'
else:
return 'usermerge.User'
def get_user_model(request):
"""
Return the user model that is active in the session after specifying its label - see _get_user_model_label() function above.
"""
return django_apps.get_model(_get_user_model_label(request), require_ready = False)
def _get_user_session_key(request):
# This value in the session is always serialized to a string, so we need
# to convert it back to Python whenever we access it.
return get_user_model(request.session[CLASS_SESSION_KEY])._meta.pk.to_python(request.session[SESSION_KEY])
# This value in the session is always serialized to a string, so we need to convert it back to Python whenever we access it.
return get_user_model(request)._meta.pk.to_python(request.session[SESSION_KEY])
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. Note that data set during
the anonymous session is retained when the user logs in.
Persist a user id and a backend in the request. This way a user doesn't 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']
session_auth_hash = ''
if user is None:
user = request.user
if hasattr(user, 'get_session_auth_hash'):
# Calculate the session authentication hash for the authenticated user that attempts to log in based on the platform selected in the
# login form. The selected platform is representative of the user model (if the selected platform was SLUB, the user model is Admin,
# otherwise it is User) and the calculation of the hash differs between the user models (see the different get_session_auth_hash()
# methods in models.py). If the user logs in successfully, the hash is stored in the session data and can then be used to verify the
# user's authenticity during the session (see, for example, the get_user() function below).
if platform_name == 'SLUB':
# Admin hash calculation
session_auth_hash = user.get_session_auth_hash()
else:
# User hash calculation
registry = Registry.objects.get(user = user, platform__name = platform_name)
session_auth_hash = registry.get_session_auth_hash()
if SESSION_KEY in request.session:
if _get_user_session_key(request) != user.pk or (
session_auth_hash and
not constant_time_compare(request.session.get(HASH_SESSION_KEY, ''), session_auth_hash)):
# To avoid reusing another user's session, create a new, empty
# session if the existing session corresponds to a different
# authenticated user.
if _get_user_session_key(request) != user.pk or not constant_time_compare(request.session[HASH_SESSION_KEY], session_auth_hash):
# To avoid reusing another user's session, create a new, empty session if the existing session corresponds to
# a different authenticated user. Do the same if the user's session authentication hash differs from the one
# stored in the existing session.
request.session.flush()
else:
request.session.cycle_key()
try:
backend = backend or user.backend
except AttributeError:
......@@ -53,15 +87,12 @@ def login(request, user, backend = None):
if len(backends) == 1:
_, backend = backends[0]
else:
raise ValueError(
'You have multiple authentication backends configured and '
'therefore must provide the `backend` argument or set the '
'`backend` attribute on the user.'
)
raise ValueError('You have multiple authentication backends configured and therefore must '
'provide the "backend" argument or set the "backend" attribute on the user.')
request.session[SESSION_KEY] = user._meta.pk.value_to_string(user)
# The value of CLASS_SESSION_KEY is needed to clarify if the authenticated user
# is a User ('usermerge.User') or an Admin ('usermerge.Admin').
request.session[CLASS_SESSION_KEY] = str(user.__class__).replace('.models', '').split("'")[1]
# The platform (name) selected in the login form is stored in the session data.
request.session[PLATFORM_SESSION_KEY] = platform_name
request.session[BACKEND_SESSION_KEY] = backend
request.session[HASH_SESSION_KEY] = session_auth_hash
if hasattr(request, 'user'):
......@@ -72,27 +103,33 @@ def login(request, user, backend = None):
def get_user(request):
"""
Return the user model instance associated with the given request session.
If no user is retrieved, return an instance of `AnonymousUser`.
If no user is retrieved, return an instance of 'AnonymousUser'.
"""
from django.contrib.auth.models import AnonymousUser
user = None
try:
user_id = _get_user_session_key(request)
user_model_label = _get_user_model_label(request)
backend_path = request.session[BACKEND_SESSION_KEY]
except KeyError:
pass
else:
if backend_path in settings.AUTHENTICATION_BACKENDS:
backend = load_backend(backend_path)
user = backend.get_user(request.session[CLASS_SESSION_KEY], user_id)
# Verify the session.
if hasattr(user, 'get_session_auth_hash'):
session_hash = request.session.get(HASH_SESSION_KEY)
session_hash_verified = session_hash and constant_time_compare(
session_hash,
user.get_session_auth_hash()
)
if not session_hash_verified:
user = backend.get_user(user_model_label, user_id)
# Verify the session if the associated user is retrieved.
if user:
# Calculate the session authentication hash for the associated user based on his/her model (see the different
# get_session_auth_hash() methods in models.py). If the calculated hash coincides with the one stored in the
# session, return the associated user instance. Otherwise, flush the session (delete the session data and
# cookie) and return an 'AnonymousUser' instance.
session_auth_hash = ''
if user_model_label == 'usermerge.Admin':
session_auth_hash = user.get_session_auth_hash()
else: # user_model_label == 'usermerge.User'
registry = Registry.objects.get(user = user, platform__name = request.session[PLATFORM_SESSION_KEY])
session_auth_hash = registry.get_session_auth_hash()
if not constant_time_compare(request.session[HASH_SESSION_KEY], session_auth_hash):
request.session.flush()
user = None
return user or AnonymousUser()
......
from django.contrib.auth.hashers import check_password
from usermerge.models import Admin, Platform, Registry, User
from django.contrib.auth.hashers import check_password, make_password
from usermerge.models import Admin, Registry, User
# Create your backends here.
# https://docs.djangoproject.com/en/2.0/topics/auth/customizing/#other-authentication-sources
# https://docs.djangoproject.com/en/2.0/topics/auth/customizing/#authentication-backends
# For more information on Admin, Platform, Registry and User models, see models.py .
# 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 password management functions (e.g. check_password()), see https://docs.djangoproject.com/en/2.0/topics/auth/passwords/ .
class UserBackend:
def authenticate(self, request, platform, username, password):
def authenticate(self, request, platform = None, username = None, password = None):
"""
Authenticate the credentials POSTed via the login form.
......@@ -26,45 +25,49 @@ class UserBackend:
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 returned (if the first check fails, the second is never performed).
fails and an empty instance (None) is implicitly returned (if the first check fails, the second is never performed).
"""
if platform != 'SLUB':
# User authentication
platform_instance = Platform.objects.get(name = platform)
try:
registry = Registry.objects.get(platform = platform_instance, username = username)
except Registry.DoesNotExist:
return None
else:
if check_password(password, registry.password):
return registry.user
else:
return None
else:
if platform == 'SLUB':
# Admin authentication
try:
admin = Admin.objects.get(username = username)
except Admin.DoesNotExist:
return None
# Run the default password hasher once to reduce the timing difference between an existent 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)
else:
if check_password(password, admin.password):
return admin
else:
return None
else:
# User authentication
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
# (https://github.com/django/django/blob/master/django/contrib/auth/backends.py).
dummy_password = make_password(password)
else:
if check_password(password, registry.password):
return registry.user
def get_user(self, user_class, user_id):
def get_user(self, user_model_label, user_id):
"""
Return the user instance associated with the given class/model (User/Admin) and id.
If no associated user instance is found, 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).
If no user is retrieved, return an empty instance (None).
"""
if user_class == 'usermerge.User':
if user_model_label == 'usermerge.User':
try:
return User.objects.get(pk = user_id)
except User.DoesNotExist:
return None
else:
elif user_model_label == 'usermerge.Admin':
try:
return Admin.objects.get(pk = user_id)
except Admin.DoesNotExist:
return None
else:
raise ValueError('The user_model_label argument of get_user() backend method '
'should either be "usermerge.User" or "usermerge.Admin"!')
"""
Modified version of django.contrib.auth.middleware module (https://github.com/django/django/blob/master/django/contrib/auth/middleware.py).
"""
from django.conf import settings
from django.utils.deprecation import MiddlewareMixin
from django.utils.functional import SimpleLazyObject
......@@ -9,6 +6,8 @@ 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).
def get_user(request):
if not hasattr(request, '_cached_user'):
request._cached_user = auth.get_user(request)
......@@ -17,9 +16,7 @@ def get_user(request):
class AuthenticationMiddleware(MiddlewareMixin):
def process_request(self, request):
assert hasattr(request, 'session'), (
"The Django authentication middleware requires session middleware "
"to be installed. Edit your MIDDLEWARE%s setting to insert "
"'django.contrib.sessions.middleware.SessionMiddleware' before "
"'usermerge.middleware.AuthenticationMiddleware'."
) % ("_CLASSES" if settings.MIDDLEWARE is None else "")
'The Django authentication middleware requires session middleware to be installed. Edit your MIDDLEWARE%s setting to '
'insert "django.contrib.sessions.middleware.SessionMiddleware" before "usermerge.middleware.AuthenticationMiddleware".'
) % ('_CLASSES' if settings.MIDDLEWARE is None else '')
request.user = SimpleLazyObject(lambda: get_user(request))
# Generated by Django 2.0.4 on 2018-06-28 11:10
# Generated by Django 2.0.4 on 2019-01-24 11:33
from django.db import migrations, models
import django.db.models.deletion
......@@ -16,8 +16,8 @@ class Migration(migrations.Migration):
name='Admin',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=100)),
('last_name', models.CharField(max_length=100)),
('first_name', models.CharField(max_length=50)),
('last_name', models.CharField(max_length=50)),
('email', models.EmailField(max_length=254, unique=True)),
('username', models.CharField(max_length=50, unique=True)),
('password', models.CharField(max_length=100)),
......@@ -39,13 +39,16 @@ class Migration(migrations.Migration):
('password', models.CharField(max_length=100)),
('platform', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='usermerge.Platform')),
],
options={
'verbose_name_plural': 'registries',
},
),
migrations.CreateModel(
name='User',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=100)),
('last_name', models.CharField(max_length=100)),
('first_name', models.CharField(max_length=50)),
('last_name', models.CharField(max_length=50)),
('ece_id', models.CharField(max_length=8, null=True, unique=True)),
('email', models.EmailField(max_length=254, null=True, unique=True)),
('last_login', models.DateTimeField(null=True)),
......
......@@ -4,32 +4,32 @@ from django.utils.crypto import salted_hmac
# Create your models here.
# https://docs.djangoproject.com/en/2.0/ref/models/
# Regarding the fields whose null attribute/option is set to True (WARNING: Any unique fields whose null attribute/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 inserting/updating the respective DB entries/records, 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/ .
# first_name and last_name fields can contain greek characters. The length of each greek character is 2 bytes, while the length of each english/punctuation character is 1 byte.
# Django creates hashed passwords using the PBKDF2 algorithm with a SHA256 hash by default (https://docs.djangoproject.com/en/2.0/topics/auth/passwords/). The length of each hashed password is 78 bytes.
# The ECE-ID format is 031YYRRR (YY: year, RRR: registration number).
# The user_logged_in signal (see login() function in auth.py) triggers the update of the last_login fields, so the latter are necessary.
# Original source code for get_session_auth_hash() methods and is_authenticated 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/). get_session_auth_hash() methods are needed in login() and get_user() functions of auth.py, while is_authenticated attributes are needed for login_required() decorator of 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, 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 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 .
# 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):
first_name = models.CharField(max_length = 100)
last_name = models.CharField(max_length = 100)
first_name = models.CharField(max_length = 50)
last_name = models.CharField(max_length = 50)
ece_id = models.CharField(max_length = 8, unique = True, null = True)
email = models.EmailField(unique = True, null = True)
email = models.EmailField(unique = True, null = True) # max_length = 254 by default
last_login = models.DateTimeField(null = True)
def get_session_auth_hash(self):
"""
Return an HMAC of the email field.
"""
key_salt = 'usermerge.models.User.get_session_auth_hash'
return salted_hmac(key_salt, self.email).hexdigest()
@property
def is_authenticated(self):
"""
Always return True. This is a way to tell if the user has been
authenticated in views.
authenticated in templates, views, etc.
"""
return True
......@@ -42,29 +42,37 @@ class Registry(models.Model):
username = models.CharField(max_length = 50)
password = models.CharField(max_length = 100)
def get_session_auth_hash(self):
"""
Return an HMAC of the platform (name) and password fields.
"""
key_salt = 'usermerge.models.Registry.get_session_auth_hash'
return salted_hmac(key_salt, self.password + '@' + self.platform.name).hexdigest()
class Meta:
unique_together = (('user', 'platform',), ('platform', 'username',),)
verbose_name_plural = 'registries'
class Admin(models.Model):
first_name = models.CharField(max_length = 100)
last_name = models.CharField(max_length = 100)
email = models.EmailField(unique = True)
first_name = models.CharField(max_length = 50)
last_name = models.CharField(max_length = 50)
email = models.EmailField(unique = True) # max_length = 254 by default
username = models.CharField(max_length = 50, unique = True)
password = models.CharField(max_length = 100)
last_login = models.DateTimeField(null = True)
def get_session_auth_hash(self):
"""
Return an HMAC of the email field.
Return an HMAC of the password field.
"""
key_salt = 'usermerge.models.Admin.get_session_auth_hash'
return salted_hmac(key_salt, self.email).hexdigest()
return salted_hmac(key_salt, self.password).hexdigest()
@property
def is_authenticated(self):
"""
Always return True. This is a way to tell if the user has been
authenticated in views.
authenticated in templates, views, etc.
"""
return True
"""
Populate usermergeDB (see models.py) with test data to check the correctness of the usermerge application features.
"""
from django.contrib.auth.hashers import make_password
from usermerge.models import Admin, Platform, Registry, User
......@@ -21,9 +22,11 @@ plgrader = Platform.objects.create(name = 'PLgrader')
# Populate User table with test users.
user1 = User.objects.create(first_name = 'Γεώργιος', last_name = 'Καζελίδης', ece_id = '03199999',
email = 'gkazelid@undergraduate.ece.ntua.gr')
user2 = User.objects.create()
user2 = User.objects.create(first_name = 'Ζαχαρίας', last_name = 'Δόγκανος', email = 'zdogkanos@undergraduate.ece.ntua.gr')
user3 = User.objects.create()
# Populate Registry table with test registries.
user_hashed_password = make_password('password')
registry1 = Registry.objects.create(user = user1, platform = novice, username = 'pi99b999', password = user_hashed_password)
registry2 = Registry.objects.create(user = user2, platform = moodle, username = 'moodler', password = user_hashed_password)
registry3 = Registry.objects.create(user = user3, platform = plgrader, username = 'plgrad', password = user_hashed_password)
* The favicon.ico was created at https://www.favicon.cc/ .
* The apple_touch_icon.png was created by converting favicon.ico to favicon.png and resizing the latter from 16x16 to 256x256 pixels at http://icoconvert.com/ .
This source diff could not be displayed because it is too large. You can view the blob instead.
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
]>
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
x="0px" y="0px" width="16px" height="16px" viewBox="0 0 16 16" style="overflow:visible;enable-background:new 0 0 16 16;"
xml:space="preserve" preserveAspectRatio="xMinYMid meet">
<defs>
</defs>
<path style="fill:#999999;" d="M7,0.7L0.2,14.1c-0.5,1,0,1.8,1.1,1.8h13.2c1.1,0,1.6-0.8,1.1-1.8L8.8,0.7C8.3-0.2,7.5-0.2,7,0.7z
M9.4,13.4c0,0.8-0.7,1.5-1.5,1.5c-0.8,0-1.5-0.7-1.5-1.5c0-0.8,0.7-1.5,1.5-1.5C8.7,11.9,9.4,12.5,9.4,13.4z M9.2,5.9v4
c0,0.5-0.5,1-1,1H7.6c-0.5,0-1-0.5-1-1v-4c0-0.6,0.5-1,1-1h0.6C8.8,4.9,9.2,5.3,9.2,5.9z"/>
</svg>
article, aside, figure, footer, header, main, nav, section {
display: block; /* https://www.w3schools.com/html/html5_browsers.asp */
}
html {
/* https://stackoverflow.com/questions/4565942/should-global-css-styles-be-set-on-the-html-element-or-the-body-element */
background-color: #F1F1FA;
}
body {
text-align: center;
}
footer, header {
font-size: 16px;
padding: 10px 0px;
}
main {
font-size: 14px;
}
h4 {
font-size: 16px;
text-decoration: underline;
}
table {
margin: 0px auto; /* https://stackoverflow.com/questions/14073188/center-align-a-html-table */
}
#copyright_and_contact_footer, #ece_ntua_header {
background-color: #82B7EF;
}
#slub_header {
background-color: #D3D6E6;
}
#session_nav {
background-color: #82B7EF;
font-size: 16px;
padding: 10px 0px;
}
* CSS tutorials:
- https://www.w3schools.com/css/default.asp
- https://developer.mozilla.org/en-US/docs/Web/CSS
- created favicon.ico at https://www.favicon.cc/
- converted favicon.ico to favicon.png at http://icoconvert.com/
{# https://docs.djangoproject.com/en/2.0/ref/views/#the-400-bad-request-view #}
{# https://docs.djangoproject.com/en/2.0/_modules/django/views/defaults/ #}
{% extends "base.html" %}
{% extends 'base.html' %}
{% block title %}
SLUB - Σφάλμα 400 (Bad Request)
......
{# https://docs.djangoproject.com/en/2.0/ref/views/#the-403-http-forbidden-view #}
{# https://docs.djangoproject.com/en/2.0/_modules/django/views/defaults/ #}
{% extends "base.html" %}
{% extends 'base.html' %}
{% block title %}
SLUB - Σφάλμα 403 (Forbidden)
......
{# https://docs.djangoproject.com/en/2.0/ref/views/#the-404-page-not-found-view #}
{# https://docs.djangoproject.com/en/2.0/_modules/django/views/defaults/ #}
{% extends "base.html" %}
{% extends 'base.html' %}
{% block title %}
SLUB - Σφάλμα 404 (Not Found)
......@@ -9,5 +9,5 @@
{% block content %}
<h4>Σφάλμα 404 (Not Found)</h4>
<p>Η ζητηθείσα ιστοσελίδα "{{ request_path }}" δεν βρέθηκε στον παρόντα διακομιστή! :(</p>
<p>Η ζητηθείσα ιστοσελίδα "{{ request_path }}" δεν βρέθηκε στον παρόντα διακομιστή!</p>
{% endblock %}
{# https://docs.djangoproject.com/en/2.0/ref/views/#the-500-server-error-view #}
{# https://docs.djangoproject.com/en/2.0/_modules/django/views/defaults/ #}
{% extends "base.html" %}
{% extends 'base.html' %}
{% block title %}
SLUB - Σφάλμα 500 (Server Error)
......
{% extends "base.html" %}
{% extends 'base.html' %}
{% block title %}
SLUB - Αρχική Σελίδα Διαχειριστή
{% endblock %}
{% block content %}
<div class="bar" id="in-session-bar" style="padding-top:10px; padding-bottom:10px;">Έχετε εισέλθει επιτυχώς! :)</div>
<nav id="session_nav">
Έχετε εισέλθει ως <strong>{{ user.first_name }} {{ user.last_name }}</strong>!
| <a href="{% url 'admin_home' user.id %}">Αρχική Σελίδα</a>
| <a href="{% url 'logout' %}">Έξοδος</a>
</nav>
<h4>Αρχική Σελίδα Διαχειριστή</h4>
<p><a href="{% url 'logout' %}">Έξοδος</a></p>
{% endblock %}
<!DOCTYPE html>
<html>
<!DOCTYPE html> {# HTML5 DOCTYPE declaration #}
{# https://stackoverflow.com/questions/14649798/what-is-lang-attribute-of-the-html-tag-used-for #}
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="el" lang="el">
<head>
<meta charset="utf-8" />
{# https://stackoverflow.com/questions/6771258/what-does-meta-http-equiv-x-ua-compatible-content-ie-edge-do #}
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>
{% block title %}
SLUB
SLUB - SoftLab UserBase
{% endblock %}
</title>
{# https://docs.djangoproject.com/en/2.0/howto/static-files/ #}
{% load static %}
<link rel="icon" href="{% static 'usermerge/favicon.png' %}" type="image/png"/>
<base href="{% static '' %}" />
{# https://en.wikipedia.org/wiki/Favicon #}
{# https://stackoverflow.com/questions/44939439/can-one-large-size-favicon-serve-all-devices-and-browsers #}
<link rel="shortcut icon" href="images/favicon.ico" />
<link rel="apple-touch-icon" href="images/apple_touch_icon.png" />
{# https://stackoverflow.com/questions/6887336/what-is-the-difference-between-normalize-css-and-reset-css #}
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/normalize.css/normalize.css" />
<link rel="stylesheet" type="text/css" href="stylesheets/base.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<style>
body {
background-color: #F1F1FA;
}
h4 {
text-align: center;
text-decoration: underline;
}
p {
text-align: center;
}
table {
margin: 0px auto; {# https://stackoverflow.com/questions/14073188/center-align-a-html-table #}
}
.bar, .footer, .header {
text-align: center;
width: 100%;
}
#ece-ntua-header, #footer, #in-session-bar {
background-color: #82B7EF;
}
#slub-header {
background-color: #D3D6E6;
}
</style>
{# https://www.w3schools.com/html/html5_browsers.asp #}
<!--[if lt IE 9]>
<script src="https://cdn.jsdelivr.net/npm/html5shiv/dist/html5shiv.min.js"></script>
<![endif]-->
</head>
<body>
<div class="header" id="ece-ntua-header">
<header id="ece_ntua_header" style="padding:0px;">
<table>
<tr>
<td style="width:240px;">
<img src="https://courses.softlab.ntua.gr/Images/Various/pyrforos.svg" alt="Προμηθέας Πυρφόρος" style="height:120px; width:120px;"/>
</td>
<td>
<p><a href="https://www.ntua.gr/" target="_blank">Εθνικό Μετσόβιο Πολυτεχνείο</a></p>
<p><a href="https://www.ece.ntua.gr/" target="_blank">Σχολή Ηλεκτρολόγων Μηχανικών και Μηχανικών Υπολογιστών</a></p>
<p><a href="http://www.cs.ntua.gr/" target="_blank">Τομέας Τεχνολογίας Πληροφορικής και Υπολογιστών</a></p>
<p><a href="http://www.softlab.ntua.gr/" target="_blank">Εργαστήριο Τεχνολογίας Λογισμικού</a></p>
</td>
</tr>
<tbody>
<tr>
<td style="width:250px;">
<img src="images/pyrforos.svg" alt="Προμηθέας Πυρφόρος" style="height:120px; width:120px;" />
</td>
<td>
<p><a href="https://www.ntua.gr/" target="_blank">Εθνικό Μετσόβιο Πολυτεχνείο</a></p>
<p>
<a href="https://www.ece.ntua.gr/" target="_blank">Σχολή Ηλεκτρολόγων Μηχανικών και Μηχανικών Υπολογιστών</a>
</p>
<p><a href="http://www.cs.ntua.gr/" target="_blank">Τομέας Τεχνολογίας Πληροφορικής και Υπολογιστών</a></p>
<p><a href="http://www.softlab.ntua.gr/" target="_blank">Εργαστήριο Τεχνολογίας Λογισμικού</a></p>
</td>
</tr>
</tbody>
</table>
</div>
</header>
<div class="header" id="slub-header">
<header id="slub_header">
<table>
<tr>
<td style="padding-left:10px; width:240px;">
<p>
<tbody>
<tr>
<td style="width:250px;">
<strong>
<span style="color:red;">S</span>oft<span style="color:red;">L</span>ab
<span style="color:red;">U</span>ser<span style="color:red;">B</span>ase
</strong>
</p>
</td>
<td>
<p>
Το <strong><span style="color:red;">SLUB</span></strong> είναι το σύστημα διαχείρισης και συσχέτισης <br/>
διαπιστευτηρίων για τους χρήστες των πλατφορμών του SoftLab. <br/>
Οι πλατφόρμες που διατίθενται μέσω του SoftLab είναι <br/>
ο <a href="http://courses.softlab.ntua.gr/progintro/" target="_blank">Novice</a>,
</td>
<td style="padding:0px 10px;">
Το <strong><span style="color:red;">SLUB</span></strong> είναι το σύστημα διαχείρισης και συσχέτισης <br />
λογαριασμών για τους χρήστες των πλατφορμών του SoftLab. <br />
Οι πλατφόρμες που διατίθενται μέσω του SoftLab είναι <br />
ο <a href="http://courses.softlab.ntua.gr/" target="_blank">Novice</a>,
o <a href="http://grader.softlab.ntua.gr/" target="_blank">Grader</a>,
το <a href="https://moodle.softlab.ntua.gr/" target="_blank">Moodle</a> και
ο <a href="http://plgrader.softlab.ntua.gr/" target="_blank">PLgrader</a>.
</p>
</td>
</tr>
</td>
</tr>
</tbody>
</table>
</div>
</header>
{% block content %}
<p>Αυτή η ιστοσελίδα δεν έχει ακόμα περιεχόμενο... :(</p>
{% endblock %}
<main>
{% block content %}
<p>Αυτή η ιστοσελίδα δεν έχει ακόμα περιεχόμενο!</p>
{% endblock %}
</main>
<div class="footer" id="footer">
<footer id="copyright_and_contact_footer">
<table>
<tr>
<td style="padding-right:30px; width:240px;">
<p>&copy; {% now "Y" %} SoftLab</p>
</td>
<td style="width:240px;">
<p><a href="mailto:webmaster@courses.softlab.ntua.gr">Επικοινωνία με Webmaster</a></p>
</td>
<td style="padding-left:30px; width:240px;">
<p>
Αναπτύxθηκε σε <br/>
<a href="https://www.python.org/" target="_blank">Python</a>,
<a href="https://www.djangoproject.com/" target="_blank">Django</a> και
<a href="https://www.mysql.com/" target="_blank">MySQL</a>
</p>
</td>
</tr>
<tbody>
<tr>
<td style="width:250px;">&#xA9; {% now 'Y' %} SoftLab</td>
<td style="width:250px;"><a href="mailto:webmaster@courses.softlab.ntua.gr">Επικοινωνία με Webmaster</a></td>
<td style="width:250px; padding-left:30px;">
Θερμές Ευχαριστίες σε <br />
<a href="https://www.djangoproject.com/" target="_blank">Django</a>,
<a href="https://www.mysql.com/" target="_blank">MySQL</a> και
<a href="https://httpd.apache.org/" target="_blank">Apache</a>
</td>
</tr>
</tbody>
</table>
</div>
</footer>
</body>
</html>
{% extends "base.html" %}
{% extends 'base.html' %}
{% block title %}
SLUB - Είσοδος Χρήστη/Διαχειριστή
......@@ -7,54 +7,52 @@
{% block content %}
<h4>Είσοδος Χρήστη/Διαχειριστή</h4>
<form name="login" style="text-align:center;" action="{% url 'submit_login' %}" method="POST">
<form id="login_form" accept-charset="utf-8" action="{% url 'submit_login' %}" method="post">
{% csrf_token %}
<table>
<tr>
<td style="text-align:right;">Είσοδος ως:</td>
<td style="text-align:left;">
<select name="platform">
<option value="SLUB">διαχειριστής του SLUB</option>
{% for platform in platform_list %}
<option value="{{ platform }}" {% if platform == "Novice" %}selected{% endif %}>
χρήστης του {{ platform }}
</option>
{% endfor %}
</select>
</td>
</tr>
<tr>
<td style="text-align:right;">Όνομα χρήστη:</td>
<td style="text-align:left;">
{# https://stackoverflow.com/questions/5272433/html5-form-required-attribute-set-custom-validation-message #}
{# https://stackoverflow.com/questions/8078388/hiding-title-tags-on-hover #}
<input type="text" name="username" maxlength="50" onmouseover="this.title='';"
oninput="setCustomValidity('');" oninvalid="setCustomValidity('Παρακαλούμε συμπληρώστε το πεδίο!');" required/>
</td>
</tr>
<tr>
<td style="text-align:right;">Κωδικός πρόσβασης:</td>
<td style="text-align:left;">
{# https://stackoverflow.com/questions/5272433/html5-form-required-attribute-set-custom-validation-message #}
{# https://stackoverflow.com/questions/8078388/hiding-title-tags-on-hover #}
<input type="password" name="password" maxlength="50" onmouseover="this.title='';"
oninput="setCustomValidity('');" oninvalid="setCustomValidity('Παρακαλούμε συμπληρώστε το πεδίο!');" required/>
</td>
</tr>
<tbody>
<tr>
<td style="text-align:right;"><label for="platform">Είσοδος ως:</label></td>
<td style="text-align:left;">
<select name="platform" id="platform">
<option value="SLUB">διαχειριστής του SLUB</option>
{% for name in platform_names %}
<option value="{{ name }}" {% if name == 'Novice' %}selected="selected"{% endif %}>
χρήστης του {{ name }}
</option>
{% endfor %}
</select>
</td>
</tr>
<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" />
</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" />
</td>
</tr>
</tbody>
</table>
<br/>
<button type="submit" value="login" name="login"><strong>Είσοδος</strong></button>
<br />
<input type="submit" value="Είσοδος" />
</form>
{% if error_messages %}
{% for message in error_messages %}
<p style="color:red; font-size:14px;">
<img src="https://moodle.softlab.ntua.gr/theme/image.php/softlab/core/1522849333/i/warning" alt="Σφάλμα"/>
<p style="color:red;">
<img src="images/warning.svg" alt="Σφάλμα:" />
<strong>{{ message | linebreaksbr }}</strong>
</p>
{% endfor %}
{% endif %}
<br/>
<br />
{% endblock %}
{% extends "base.html" %}
{% extends 'base.html' %}
{% block title %}
SLUB - Έξοδος Χρήστη/Διαχειριστή
......@@ -6,6 +6,6 @@
{% block content %}
<h4>Έξοδος Χρήστη/Διαχειριστή</h4>
<p>Εξήλθατε με επιτυχία! :)</p>
<p>Εξήλθατε με επιτυχία!</p>
<p><a href="{% url 'default_login' %}">Θα θέλατε μήπως να εισέλθετε ξανά;</a></p>
{% endblock %}
{% extends "base.html" %}
{% extends 'base.html' %}
{% block title %}
SLUB - Αρχική Σελίδα Χρήστη
{% endblock %}
{% block content %}
<div class="bar" id="in-session-bar" style="padding-top:10px; padding-bottom:10px;">Έχετε εισέλθει επιτυχώς! :)</div>
<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>
<p><a href="{% url 'logout' %}">Έξοδος</a></p>
<nav id="user_home_nav">
<p><a href="{% url 'logout' %}">Έξοδος</a></p>
</nav>
{% endblock %}
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