Skip to content
Snippets Groups Projects
models.py 47.6 KiB
Newer Older
Benjamin Bertrand's avatar
Benjamin Bertrand committed
# -*- coding: utf-8 -*-
"""
app.models
~~~~~~~~~~

This module implements the models used in the app.

:copyright: (c) 2017 European Spallation Source ERIC
:license: BSD 2-Clause, see LICENSE for more details.

"""
import ipaddress
Benjamin Bertrand's avatar
Benjamin Bertrand committed
import qrcode
import urllib.parse
import elasticsearch
import sqlalchemy as sa
Benjamin Bertrand's avatar
Benjamin Bertrand committed
from enum import Enum
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.dialects import postgresql
from sqlalchemy.orm import validates
from sqlalchemy_continuum import make_versioned, version_class
from citext import CIText
from flask import current_app
from flask_login import UserMixin, current_user
from wtforms import ValidationError
Benjamin Bertrand's avatar
Benjamin Bertrand committed
from rq import Queue
from .extensions import db, login_manager, ldap_manager, cache
from .plugins import FlaskUserPlugin
Benjamin Bertrand's avatar
Benjamin Bertrand committed
from .validators import (
    ICS_ID_RE,
    HOST_NAME_RE,
    VLAN_NAME_RE,
    MAC_ADDRESS_RE,
    DEVICE_TYPE_RE,
    TAG_RE,
)
from . import utils, search
make_versioned(plugins=[FlaskUserPlugin()])
# See http://docs.sqlalchemy.org/en/latest/core/compiler.html#utc-timestamp-function
class utcnow(sa.sql.expression.FunctionElement):
    type = sa.types.DateTime()


Benjamin Bertrand's avatar
Benjamin Bertrand committed
@sa.ext.compiler.compiles(utcnow, "postgresql")
def pg_utcnow(element, compiler, **kw):
Benjamin Bertrand's avatar
Benjamin Bertrand committed
    return "TIMEZONE('utc', CURRENT_TIMESTAMP)"
def temporary_ics_ids():
    """Generator that returns the full list of temporary ICS ids"""
Benjamin Bertrand's avatar
Benjamin Bertrand committed
    return (
        f'{current_app.config["TEMPORARY_ICS_ID"]}{letter}{number:0=3d}'
        for letter in string.ascii_uppercase
        for number in range(0, 1000)
    )
    """Return a set with the temporary ICS ids used"""
    temporary_items = Item.query.filter(
Benjamin Bertrand's avatar
Benjamin Bertrand committed
        Item.ics_id.startswith(current_app.config["TEMPORARY_ICS_ID"])
    ).all()
    return {item.ics_id for item in temporary_items}


def get_temporary_ics_id():
    """Return a temporary ICS id that is available"""
    used_temp_ics_ids = used_temporary_ics_ids()
    for ics_id in temporary_ics_ids():
        if ics_id not in used_temp_ics_ids:
Benjamin Bertrand's avatar
Benjamin Bertrand committed
        raise ValueError("No temporary ICS id available")
Benjamin Bertrand's avatar
Benjamin Bertrand committed
@login_manager.user_loader
Benjamin Bertrand's avatar
Benjamin Bertrand committed
@cache.memoize(timeout=1800)
Benjamin Bertrand's avatar
Benjamin Bertrand committed
def load_user(user_id):
    """User loader callback for flask-login

    :param str user_id: unicode ID of a user
    :returns: corresponding user object or None
Benjamin Bertrand's avatar
Benjamin Bertrand committed
    """
    return User.query.get(int(user_id))


@ldap_manager.save_user
def save_user(dn, username, data, memberships):
    """User saver for flask-ldap3-login

    This method is called whenever a LDAPLoginForm()
    successfully validates.
    """
    user = User.query.filter_by(username=username).first()
    if user is None:
Benjamin Bertrand's avatar
Benjamin Bertrand committed
        user = User(
            username=username,
            display_name=utils.attribute_to_string(data["cn"]),
            email=utils.attribute_to_string(data["mail"]),
        )
    # Always update the user groups to keep them up-to-date
Benjamin Bertrand's avatar
Benjamin Bertrand committed
    user.groups = sorted(
        [utils.attribute_to_string(group["cn"]) for group in memberships]
    )
    db.session.add(user)
    db.session.commit()
Benjamin Bertrand's avatar
Benjamin Bertrand committed
    return user


# Tables required for Many-to-Many relationships between users and favorites attributes
favorite_manufacturers_table = db.Table(
Benjamin Bertrand's avatar
Benjamin Bertrand committed
    "favorite_manufacturers",
    db.Column(
        "user_id", db.Integer, db.ForeignKey("user_account.id"), primary_key=True
    ),
    db.Column(
        "manufacturer_id",
        db.Integer,
        db.ForeignKey("manufacturer.id"),
        primary_key=True,
    ),
)
favorite_models_table = db.Table(
Benjamin Bertrand's avatar
Benjamin Bertrand committed
    "favorite_models",
    db.Column(
        "user_id", db.Integer, db.ForeignKey("user_account.id"), primary_key=True
    ),
    db.Column("model_id", db.Integer, db.ForeignKey("model.id"), primary_key=True),
)
favorite_locations_table = db.Table(
Benjamin Bertrand's avatar
Benjamin Bertrand committed
    "favorite_locations",
    db.Column(
        "user_id", db.Integer, db.ForeignKey("user_account.id"), primary_key=True
    ),
    db.Column(
        "location_id", db.Integer, db.ForeignKey("location.id"), primary_key=True
    ),
)
favorite_statuses_table = db.Table(
Benjamin Bertrand's avatar
Benjamin Bertrand committed
    "favorite_statuses",
    db.Column(
        "user_id", db.Integer, db.ForeignKey("user_account.id"), primary_key=True
    ),
    db.Column("status_id", db.Integer, db.ForeignKey("status.id"), primary_key=True),
)
favorite_actions_table = db.Table(
Benjamin Bertrand's avatar
Benjamin Bertrand committed
    "favorite_actions",
    db.Column(
        "user_id", db.Integer, db.ForeignKey("user_account.id"), primary_key=True
    ),
    db.Column("action_id", db.Integer, db.ForeignKey("action.id"), primary_key=True),
Benjamin Bertrand's avatar
Benjamin Bertrand committed
class User(db.Model, UserMixin):
    # "user" is a reserved word in postgresql
    # so let's use another name
Benjamin Bertrand's avatar
Benjamin Bertrand committed
    __tablename__ = "user_account"
Benjamin Bertrand's avatar
Benjamin Bertrand committed

    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.Text, nullable=False, unique=True)
    display_name = db.Column(db.Text, nullable=False)
    email = db.Column(db.Text)
    groups = db.Column(postgresql.ARRAY(db.Text), default=[])
Benjamin Bertrand's avatar
Benjamin Bertrand committed
    tokens = db.relationship("Token", backref="user")
Benjamin Bertrand's avatar
Benjamin Bertrand committed
    tasks = db.relationship("Task", backref="user")
    # The favorites won't be accessed very often so we load them
    # only when necessary (lazy=True)
    favorite_manufacturers = db.relationship(
Benjamin Bertrand's avatar
Benjamin Bertrand committed
        "Manufacturer",
        secondary=favorite_manufacturers_table,
        lazy=True,
Benjamin Bertrand's avatar
Benjamin Bertrand committed
        backref=db.backref("favorite_users", lazy=True),
    )
    favorite_models = db.relationship(
Benjamin Bertrand's avatar
Benjamin Bertrand committed
        "Model",
        secondary=favorite_models_table,
        lazy=True,
Benjamin Bertrand's avatar
Benjamin Bertrand committed
        backref=db.backref("favorite_users", lazy=True),
    )
    favorite_locations = db.relationship(
Benjamin Bertrand's avatar
Benjamin Bertrand committed
        "Location",
        secondary=favorite_locations_table,
        lazy=True,
Benjamin Bertrand's avatar
Benjamin Bertrand committed
        backref=db.backref("favorite_users", lazy=True),
    )
    favorite_statuses = db.relationship(
Benjamin Bertrand's avatar
Benjamin Bertrand committed
        "Status",
        secondary=favorite_statuses_table,
        lazy=True,
Benjamin Bertrand's avatar
Benjamin Bertrand committed
        backref=db.backref("favorite_users", lazy=True),
    )
    favorite_actions = db.relationship(
Loading
Loading full blame...