from django.db import models
from django.utils.crypto import salted_hmac
from django.utils.timezone import localtime

# Create your models here.
# https://docs.djangoproject.com/en/2.0/ref/models/

# The character set (encoding) of usermergeDB is set to utf8mb4 for proper Unicode support and the storage engine of its tables is set to InnoDB for proper transaction support. For more information on the database (table) options, e.g. character set, collation, SQL mode and storage engine, see README.md and the "Database" section of settings.py . It is worth mentioning that if MySQL is used as the database backend, the maximum length of CharFields and EmailFields with (unique) indexes in utf8mb4 encoding should be at most 191 characters (in utf8mb3 encoding, it should be at most 255 characters).
# 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. minimum length constraint for username and password values, pattern constraints for ece_id and email values, 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/ .
# 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 the 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/ .
# 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/).

# 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 - see the relative explanation on last_login fields below) 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/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 - 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.
#   * 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 import_user_credentials() view.
#   * 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, file such as populateDB.py, 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 import_user_credentials() view. 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).

# A User instance retains ONE last_login value FOR EACH Platform instance that is associated with it via the corresponding Registry instance, while an Admin instance is associated ONLY with the SLUB "platform", i.e. it is NOT associated with any Platform instances, and thus retains EXACTLY ONE last_login value. Each time a user logs in, an appropriate user_logged_in signal is emitted - see the login() function in auth.py - causing the last_login field of the corresponding User/Admin instance for the login platform (SLUB in case of an Admin instance) to be updated INDEPENDENTLY of its other fields. A NON-empty User instance should ALWAYS retain a NON-empty last_login value FOR EACH of its associated Platform instances - see the edit_user_profile() and recover_user_profile() views. On the other hand, an empty User instance generally retains an empty last_login value for its ONLY associated Platform instance - see the import_user_credentials() view - but can also retain a NON-empty one for it (the user logs in and, although his/her profile is empty, he/she opts to log out - or is logged out automatically due to session expiration - WITHOUT filling it out). An Admin instance generally retains a NON-empty last_login value for the SLUB "platform" as it should be logged in to access any Admin-targeted functionality/view, but can also retain an empty one for it (the user has NEVER logged in but his/her profile has ALREADY been filled out in a NON-session context, e.g. fixture, interactive shell, file such as populateDB.py, etc. - see the relative references above).
# Given that support for time zones is enabled (USE_TZ setting is True), DateTimeField values (datetime objects) are stored in UTC format in usermergeDB and are generally displayed in the current time zone, that is 'Europe/Athens' by default (specified by the TIME_ZONE setting), in templates. It is possible to manually convert these values to a different time zone or/and change their display format if needed, e.g. convert them to local time (default time zone, i.e. 'Europe/Athens') or/and display them in 'DD/MM/YYYY HH:MM:SS' format. For more information on time zone support and datetime objects, see https://docs.djangoproject.com/en/2.0/topics/i18n/timezones/ and the "Internationalization" section of settings.py .
# The get_session_auth_hash() methods and is_authenticated attributes are needed for user authentication, e.g. in the login() and get_user() functions defined in auth.py and login_required() decorator used in views.py and decorators.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 = 50)
    last_name = models.CharField(max_length = 50)
    ece_id = models.CharField(max_length = 8, unique = True, null = True)
    email = models.EmailField(max_length = 100, unique = True, null = True)

    def __str__(self):
        return ('[id: %d | first_name: %s | last_name: %s | ece_id: %s | email: %s]'
               ) % (self.id, self.first_name, self.last_name, self.ece_id, self.email)

    @property
    def is_authenticated(self):
        """
        Always return True. This is a way to tell if the user has been authenticated in templates, views, etc.
        """
        return True

    def has_empty_profile(self):
        """
        Return the opposite boolean value of the email field, i.e. True if the email is None and False otherwise. This is a way to tell
        whether the user has an empty profile in templates, views, etc. as the user profile is empty if the email is None and vice versa.
        """
        # The email value can be used to determine whether the user profile (User instance) is empty or not due to the latter's
        # possible/valid (field) states in usermergeDB and the allowed/consistent transitions between them (see the relative
        # comment at the top).
        return not bool(self.email)

class Platform(models.Model):
    name = models.CharField(max_length = 50, unique = True)

    def __str__(self):
        return '[id: %d | name: %s]' % (self.id, self.name)

class Registry(models.Model):
    user = models.ForeignKey('User', on_delete = models.PROTECT)
    platform = models.ForeignKey('Platform', on_delete = models.PROTECT)
    username = models.CharField(max_length = 50)
    password = models.CharField(max_length = 100)
    last_login = models.DateTimeField(null = True)

    class Meta:
        unique_together = (('user', 'platform',), ('platform', 'username',),)
        verbose_name_plural = 'registries'

    def __str__(self):
        return ('[id: %d | user: <User: %s> | platform: <Platform: %s> | username: %s | password: %s | last_login: %s]'
               ) % (self.id, self.user, self.platform, self.username, self.password,
                    None if self.last_login is None else localtime(self.last_login))

    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 Admin(models.Model):
    first_name = models.CharField(max_length = 50)
    last_name = models.CharField(max_length = 50)
    email = models.EmailField(max_length = 100, unique = True)
    username = models.CharField(max_length = 50, unique = True)
    password = models.CharField(max_length = 100)
    last_login = models.DateTimeField(null = True)

    def __str__(self):
        return ('[id: %d | first_name: %s | last_name: %s | email: %s | username: %s | password: %s | last_login: %s]'
               ) % (self.id, self.first_name, self.last_name, self.email, self.username, self.password,
                    None if self.last_login is None else localtime(self.last_login))

    @property
    def is_authenticated(self):
        """
        Always return True. This is a way to tell whether the user has been authenticated in templates, views, etc.
        """
        return True

    def get_session_auth_hash(self):
        """
        Return an HMAC of the password field.
        """
        key_salt = 'usermerge.models.Admin.get_session_auth_hash'
        return salted_hmac(key_salt, self.password).hexdigest()