#Models ## `Permission` class ``` GENERAL = 0x01 ADMINISTER = 0xff ``` Okay so here is a seemingly simple piece of code I really think is really cool! First of all we are setting up two enums here. But they are set to weird hexadecimal numbers 0x01 and 0xff. If you stick these into a hexadecimal -> decimal converter you'll find that they represent 1 and 255 respectively. But in binary they come out to 00000001 and 11111111 (8 ones). If we do a binary and (&) on these two numbers, we can actually get some unique properties from these. So if we do GENERAL & ADMINSTER, it will come out to the following ``` 00000001 & 11111111 ---------- 00000001 ``` We get back the exact same value as GENERAL! Similarly if we do ADMINSTER & GENERAL we get back GENERAL. This is useful for checking user roles and who is exactly who in this system. So we can create a method 'check(input, checker)' that will take an input hex to test and one to text against. We only need to do '(input & checker) == checker'. But there are some more interesting applications for this. Let us define, for example, a set of enums CAN_LIKE = 0x01, CAN_POST = 0x02, CAN_EDIT = 0x04 and CAN_REMOVE = 0x08. These are respectively in binary 00000001, 00000010, 00000100, 00001000. We can use binary OR (|) to create composite user permissions e.g. CAN_LIKE | CAN_POST | CAN_EDIT = 0x07 = 00000111 -> NEW_ROLE. We can run 'check(NEW_ROLE, CAN_LIKE)' or 'check(NEW_ROLE, CAN_POST)' or 'check(NEW_ROLE, CAN_EDIT)' and all of these will return True. For example NEW_ROLE & CAN_EDIT ``` 00000111 & 00000001 ---------- 00000001 <- equivalent to CAN_EDIT enum ``` A function similar to the check described above can be found in as the 'can' method below in the User class. Moving on! ## `Role` class The Role class instatiates Role model. This is used for the creation of users such as a general user and an administrator ### COLUMN DEFINITIONS: `id` serves as the primary key (expects int). `name` is the name of the role itself (expects unique String len 64) `index` is the name of the index route for the route `default` is a T/F value that determines whether a new user created has that permission or note (ref insert_roles()). This is indexed meaning that a separate table has been created with default as the first column and id as the second column. Default in this table is sorted and a query for default performs a binary search rather than a linear search (reduces search time complexity from `O(N)` to `O(log n)` `permissions` contains the permissions enum (see Permissions class) `users` is not a column but it sets up a database relation. This case is a one-to-many relationship in that for ONE Role record, there are MANY associated User objects. The `backref` param specifies a bi-directional relationship between the two tables in that there is a new property on both a given Role and User object. E.g. Role.users will refer to the User object (i.e. the user table). and User.role (role being the string specified with backref) will refer to the Role object. Lazy = dynamic specifies to return a Query object instead of actually asking the relationship to load all of its child elements upon creating the relationship. It is best practice to include lazy=dynamic upon the establishment of a relationship. ###Sub-note on lazy-dynamic and backref: Currently, lazy-dynamic will make the User collection to be loaded in as a Query object (so not everything is loaded at once). Simiarly (as mentioned above), the User object can reference the Role object by doing User.role however, this uses the default collection loading behavior (i.e. load the entire collection at once). It is fine in this case since the amount of Roles in the Role collection will be *much* less than the amount of entries in the User collection. However, we can specify that User.role uses the lazy-dynamic loading scheme. Simply redefine users here to ``` users = db.relationship('User', backref=db.backref('role', lazy='dynamic'), lazy='dynamic') ``` ### insert_roles() and SQLAlchemy Sessions The staticmethod decorator specifies that insert_roles() must be be called with a instance of the Role class. E.g. role_obj.insert_roles() This method is fairly self-explanatory. It specifies a 'roles' dict This is then iterated through and foreach role in the 'roles' dict we check to see if it already exists (by name) in the Role object i.e. the Roles table. If not, then a new Role object is instantiated After that, the perms, index, default props are set and the the role object is now added to the db session and then committed. A note about sqlalchemy if you haven't noticed already: All changes are added to a Session object (handled by SQLAlchemy). Unless specified otherwise, the session object has a merge operation that finds the difs between the new object (that was created and added to the session object) and the currently existing (corresponding) object existing in the table right now. Then a commit() propegates these changes into the database making as little changes as possible (i.e. every time we update a record, the record's attribute is changed 'in place' rather than being deleted and then replaced. Neat :) ### `__repr__` ```python def __repr__(self): return '' % self.name ``` this __repr__ method is pretty much optional, but it is helpful in that it will allow the program to pretty print the user object when you come across an error ## `User` Model The class User represents users... it extends db.Model and UserMixin. Per the flask-login documentation, the User class needs to implement is_authenticated (returns True if the user is authenticated and in turn fulfill login_required), is_active (returns True if the user has been activated i.e. confirmed by email in our case), is_anonymous (returns if a user is Anonymous i.e. is_active = is_authenticated = False, is_anonymous = True, and get_id() = None), get_id() (returns a UNICODE that has the id of the user NOT an int). ### Column Descriptions: `id` - primary key for the table. Id of the user. i.e. the unique identifier for the collection `confirmed` - boolean val (default value = False) that is an indication of whether the user has confirmed their account via email. `first_name` - ... string self explanatory `last_name` - ... string self explanatory `email` - string self explanatory. But we impose the uniqueness constraint on this column. It is necessary to check for this on the backend before entering an email into the table, else there will be some nasty errors produced when the user tries to add an existing email into the table. Note: first_name, last_name, email form an index table for easy lookup. See Role for more info `password_hash` is a 128 char long string containing the hashed password. As always, it is best practice to never include the plaintext password on the server. This hashed password is checked against when authenticating users. `role_id` is the id of the role the user is. It is a foreign key and relates to the id's in the Role collection. By default the general user is role.id = 1, and role.id = 2 is the admin. Also note that we refer to the Role collection with 'roles' rather than the assigned backref 'role' since we are referring to an individual column. ## Other User Class Variables and Methods Note that the following methods are actually available in your Jinja templates since they are attached to the user instance. `full_name` provides the full name of the user given a first and last name `can` provides a really cool way of determining whether a user has given permissions. See the Permissions class for more info. `is_admin` is an implementation of `can` to test a user against admin permissions. `password` This does not give a password if a user just calls the method and throws an AttributeError. However if someone chooses to set a password e.g. u = User( password = `test` ) the second definition of password method is run, taking the keyword arg (kwarg) as the password to then call the generate_password_hash method and set the password_hash property of the user to the generated password. `verify_password` well...verifies a provided user plaintext password against the password_hash in the user record. Uses the check_password_hash method. `generate_confirmation_token` returns a cryptographically signed string with encrypted user id under key `confirm`. This will expire in 7 days. Note that Serializer is actually TimedJSONWebSerializer when looking for documentation. `generate_changed_email_token` also returns a cryptographically signed string with encrypted user id under key `change_email` and a encrypted new_email parameter password into the method containing the desired new email the user wants to replace the old email with. `generate_password_reset_token` operates similarly to `generate_ confirmation_token`. Generates token for password reset NOTE: For context, the generate_..._token methods are used to create a random string that will be later added to an email (usually) to the requesting user. ### `confirm_account` The confirm_account method will take in a token (which was presumably generated from the generate_confirmation_token method) and then return True if the provided token is valid (and can be decrypted with the SECRET_KEY and has not expired) AND the decrypted token has the key 'confirm' with the id of the requesting user. If so, it flips the 'confirmed' attribute of the requesting user to True. Will throw BadSignature of the token is invalid, will throw SignatureExpired if the token is past the expiration time. ### `change_email` The change_email method will take in a token (which was presumably generated from the generate_email_token method) and then return True True if the token is valid (see above method for explanation of 'valid') and contains the key 'change_email' with value = user id in addition to the key 'new_email' with the new email address the user wants to change their email to. Before the new_email is committed to the session, a query is performed on the User collection on all the emails to maintain the unique constraint on the email columns. Then the user's 'email' attribute is set to the 'new_email' specified in the decrypted token. will throw BadSignature if invalid token and SignatureExpired if the token is expired. ## AnonymousUser We define a custom AnonymousUser class that represents a non-logged user. It extends the AnonymousUserMixing provided by flask-loginmanager we deny all permissions and affirm that this user is not an admin ``` class AnonymousUser(AnonymousUserMixin): def can(self, _): return False def is_admin(self): return False ``` ``` login_manager.anonymous_user = AnonymousUser ``` We then register our custom AnonymousUser class as the default login_manager anonymous user class ``` @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) ``` This is the default user_loader method for login_manager. This method defines how to query for a user given a user_id from the user SESSION object. It is pretty straightforward, it will query the User table and find the user with ID equal to the user_id provided in the user SESSION