Example #1
0
class User(db.Model, Searchable):
    """
    Base class for all users in the system.
    Should not be directly instantiated. (`hexagonal.auth` currently does that, but i'm working on it)
    """

    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    """ Integer primary key. """

    login = db.Column(db.String(80), unique=True, index=True, nullable=False)
    """ Login of the user. Must be unique. """

    password = db.Column(db.String(128), nullable=False)
    """ Password hash of the user. """

    reset_password = db.Column(db.Boolean, default=False)
    """ Reset password flag. If true, on next login user would be prompted to reset the password. """

    role = db.Column(db.String(20), index=True, nullable=False)
    """ Polymorphic identity of the user. Used to implement inheritance. """

    name = db.Column(db.String(80), index=True, nullable=False)
    """ User's full name. """


    address = db.Column(db.String(80), nullable=False)
    """ User's full address. """

    phone = db.Column(db.String(80), nullable=False)
    """ User's phone number. """

    card_number = db.Column(db.String(80), nullable=False)
    """ User's library card number. """

    queued_requests = db.relationship('QueuedRequest', back_populates='patron')
    queued_documents = association_proxy('queued_requests', 'document')

    __mapper_args__ = {
        'polymorphic_on': role,
        'polymorphic_identity': 'user'
    }

    permissions = []

    fuzzy_search_fields = ['name', 'address', 'phone']

    def has_permission(self, permission):
        """
        Whether this user has the required permission.

        By default returns whether permission is present in the class static field `permissions`.

        :param permission: permission to be checked.
        :return: whether the current user has the required permission.
        """

        return permission in self.permissions
Example #2
0
class LogEntry(db.Model):
    __tablename__ = 'log_entries'

    id = db.Column(db.Integer, primary_key=True)

    who = db.Column(db.String(256))

    what = db.Column(db.String(256))

    obj = db.Column(db.String(256))

    when = db.Column(db.DateTime(), default=text('NOW()'))

    def __repr__(self):
        return ' '.join([self.who, self.what, self.obj])
Example #3
0
class QueuedRequest(db.Model):
    __tablename__ = 'queued_requests'

    patron = db.relationship('Patron', back_populates='queued_requests')
    patron_id = db.Column(db.ForeignKey('users.id'), primary_key=True)

    document = db.relationship('Document', back_populates='queued_requests')
    document_id = db.Column(db.ForeignKey('documents.id'), index=True)

    created_at = db.Column(db.DateTime, default=text('NOW()'))
    resolved_at = db.Column(db.DateTime, default=None, nullable=True)

    priority = association_proxy('patron', 'queuing_priority')

    notified = db.Column(db.Boolean, nullable=False, default=False)

    def resolve(self):
        self.resolved_at = datetime.datetime.now()
Example #4
0
class JournalArticle(Document):
    """
    Journal article type of document.
    These could be checked out for two weeks by anyone.
    """

    __tablename__ = 'journal_articles'

    id = db.Column(db.Integer, db.ForeignKey('documents.id'), primary_key=True)
    """ Integer primary key. """

    issue_editor = db.Column(db.String(80))
    """ Editor of the issue. """

    issue_publication_date = db.Column(db.Date)
    """ Publication date of the issue. """

    journal = db.Column(db.String(80))
    """ Journal. """

    __mapper_args__ = {'polymorphic_identity': 'journal_article'}
Example #5
0
class Book(Document):
    """
    Book document type.
    These could be checked out for 2 weeks, if they are bestsellers.
    Otherwise, students check out these for 3 weeks, and faculty members check out these for 4 weeks.
    """

    __tablename__ = 'books'

    id = db.Column(db.Integer, db.ForeignKey('documents.id'), primary_key=True)
    """ Integer primary foreign key to documents. """

    edition = db.Column(db.Integer)
    """ Book edition. """

    publishment_year = db.Column(db.Integer)
    """ Publishment year. """

    bestseller = db.Column(db.Boolean, default=False)
    """ Whether this book is a bestseller. """

    publisher = db.Column(db.String(80))
    """ Relation with publisher. """

    reference = db.Column(db.Boolean, default=False)
    """ Whether this book is a reference book"""

    __mapper_args__ = {'polymorphic_identity': 'book'}
Example #6
0
class AVMaterial(Document):
    """
    AVMaterial document type.
    These could be checked out for 2 weeks by anyone.
    """

    __tablename__ = 'av_materials'

    id = db.Column(db.Integer, db.ForeignKey('documents.id'), primary_key=True)
    """ Primary foreign key to documents. """

    __mapper_args__ = {
        'polymorphic_identity': 'av_material'
    }
Example #7
0
class DocumentCopy(db.Model):
    """
    Copy of a document.
    References a specific document and document type by foreign key.
    """

    __tablename__ = 'document_copies'

    id = db.Column(db.Integer, primary_key=True)
    """ Integer primary key. """

    document_id = db.Column(db.Integer, db.ForeignKey('documents.id'))
    """ Foreign key to documents. """

    document = db.relationship('Document', back_populates='copies')
    """ Associated document. """

    location = db.Column(db.String(200))
    """ Location of the copy in the physical library. """

    loan = db.relationship('Loan',
                           back_populates='document_copy',
                           uselist=False)
    """ Relation to current loan. May be None when document is available. """
Example #8
0
class Librarian(User):
    """
    Librarian type of user.
    """

    __tablename__ = 'librarians'

    __mapper_args__ = {'polymorphic_identity': 'librarian'}

    access_level = db.Column(db.Integer, default=1, nullable=False)

    def has_permission(self, permission):
        """
        Whether this user has the required permission.

        :param permission: permission to be checked.
        :return: whether the current user has the required permission.
        """

        if self.access_level <= 0 or self.access_level >= 4:
            raise ValueError('Librarian has wrong access level')

        permission_map = [
            [],
            [
                Permission.manage, Permission.modify_document,
                Permission.modify_patron
            ],
            [
                Permission.manage, Permission.modify_document,
                Permission.modify_patron, Permission.create_document,
                Permission.create_patron, Permission.outstanding_request
            ],
            [
                Permission.manage, Permission.modify_document,
                Permission.modify_patron, Permission.create_document,
                Permission.create_patron, Permission.delete_document,
                Permission.delete_patron, Permission.outstanding_request
            ]
        ]

        return permission in permission_map[self.access_level]
Example #9
0
class Loan(db.Model):
    """
    Model for one loan of a specific document by a specific user.
    Internal model, gets squashed in the api.
    """
    class Status(enum.Enum):
        """
        Loan status.
        Each loan can be:

         * `requested` - which means that it has been requested by a patron.

         * `approved` - which means that a librarian has approved the request, and the document is now in patron's possession

         * `returned` - which means that the patron has supposedly brought the document into the library, and it is now waiting for approval from a librarian
        """
        approved = 1
        requested = 2
        returned = 3

    __tablename__ = 'loans'

    id = db.Column(db.Integer, primary_key=True)
    """ Integer primary key. """

    user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    """ Foreign key to user. """

    user = db.relationship('User')
    """ Borrowing user id. """

    document_copy_id = db.Column(db.Integer,
                                 db.ForeignKey('document_copies.id'))
    """ Foreign key to document_copy. """

    document_copy = db.relationship('DocumentCopy', back_populates='loan')
    """ Loaned document_copy. """

    due_date = db.Column(db.Date)
    """ Date when the document_copy must be returned. """

    renewed = db.Column(db.Boolean, default=False)
    """ Whether this loan was renewed. """

    status = db.Column(db.Enum(Status), default=Status.requested)
    """ Current loan status. """

    document = association_proxy('document_copy', 'document')

    def get_overdue_fine(self):
        """
        Get total overdue fine for this loan.
        Returns 0 if it is not overdue.

        :return: the overdue fine, in rubles.
        """
        days = (datetime.date.today() - self.due_date).days
        return max(
            0,
            min(days * app.config.get('OVERDUE_FINE_PER_DAY', 100),
                self.document.price))

    @staticmethod
    def overdue_loan_query():
        """
        Get the query for overdue loans.

        :return: query for overdue loans.
        """

        return Loan.query.filter(Loan.status == Loan.Status.approved,
                                 Loan.due_date < datetime.date.today())

    @staticmethod
    def get_overdue_loans():
        """
        Get overdue loans.

        :return: list.
        """

        return Loan.overdue_loan_query().all()

    @staticmethod
    def get_overdue_loan_count():
        """
        Get the amount of overdue loans.

        :return: amount.
        """

        return Loan.overdue_loan_query().count()

    @staticmethod
    def requested_loan_query():
        """
        Get the query for requested loans.

        :return: query for requested loans.
        """

        return Loan.query.filter(Loan.status == Loan.Status.requested)

    @staticmethod
    def get_requested_loans():
        """
        Get requested loans.

        :return: list.
        """

        return Loan.requested_loan_query().all()

    @staticmethod
    def get_requested_loan_count():
        """
        Get the amount of requested loans.

        :return: amount.
        """

        return Loan.requested_loan_query().count()

    @staticmethod
    def returned_loan_query():
        """
        Get the query for returned loans.

        :return: query for returned loans.
        """

        return Loan.query.filter(Loan.status == Loan.Status.returned)

    @staticmethod
    def get_returned_loans():
        """
        Get returned loans.

        :return: list.
        """

        return Loan.returned_loan_query().all()

    @staticmethod
    def get_returned_loan_count():
        """
        Get the amount of returned loans.

        :return: amount.
        """

        return Loan.returned_loan_query().count()

    def overdue(self):
        """
        Check whether this loan is overdue.

        :return: whether this loan is overdue.
        """

        return datetime.date.today() >= self.due_date

    def overdue_days(self):
        """
        Gives number of overdued days of loan.

        Overdued or overdue? English is my second language.
        But Leonid Lyigin says that my eNgLiSh is finish <3

        :return: number of days
        """

        if self.overdue():
            delta = datetime.date.today() - self.due_date
            return ((delta.total_seconds() / 60) / 60) / 24

    def renew_document(self):
        """

        Allows user to renew his period of book checkout for one more period,
        without overduing the renewable document by old date

        :return: new date, when book will become overdued
        """

        if self.document.outstanding:
            raise ValueError

        if self.can_be_renewed():
            self.renewed = True
            delta = self.user.get_checkout_period_for(self.document)
            self.due_date = datetime.date.today() + delta
            return self.due_date
        else:
            raise ValueError

    def can_be_renewed(self):
        """
        Flag for renew_document function.
        Gives information is renew option is it available to renew loan or not.

        :return: whether the loan can be renewed.
        """

        from hexagonal.model.visiting_professor_patron import VisitingProfessorPatron

        if self.due_date > datetime.date.today():
            if isinstance(self.user,
                          VisitingProfessorPatron) or not self.renewed:
                return True
        return False
Example #10
0
class Document(db.Model, Searchable):
    """
    Base class for all documents.
    Should not be instantiated directly.

    Contains common fields for all documents, and inherits from :py:class:`hexagonal.model.searchable.Searchable`,
    adding search capability.

    Fuzzy search fields are `title` and `type`.
    Fuzzy array search fields are `keywords` and `authors`.
    """

    __tablename__ = 'documents'

    id = db.Column(db.Integer, primary_key=True)
    """ Integer primary key (referenced from subclasses) """

    title = db.Column(db.String(80), unique=True, index=True, nullable=False)
    """ Document title (exists for all types) """

    price = db.Column(db.Integer, nullable=False, default=0)
    """ Document price (used in calculating overdue fine) """

    copies = db.relationship('DocumentCopy', back_populates='document', cascade='all, delete-orphan')
    """ Relation with copies of this document. """

    keywords = db.Column(db.ARRAY(db.String(80)))
    """ Relation with keywords. """

    authors = db.Column(db.ARRAY(db.String(80)))
    """ Authors of this document. """

    type = db.Column(db.String(20), nullable=False)
    """ Polymorphic identity column for inheritance support. """

    queued_requests = db.relationship('QueuedRequest', back_populates='document')
    awaiting_patrons = association_proxy('queued_requests', 'patron')

    outstanding = db.Column(db.Boolean, default=False)

    __mapper_args__ = {
        'polymorphic_on': type,
        'polymorphic_identity': 'document'
    }

    fuzzy_search_fields = ['title', 'type']
    fuzzy_array_search_fields = ['keywords', 'authors']

    @hybrid_property
    def available_copies(self):
        """
        Hybrid property for currently available copies of this document.
        Available copies are copies which don't have an associated loan.
        """

        return DocumentCopy.query.filter(
            DocumentCopy.document == self, DocumentCopy.loan == None
        ).all()

    def outstanding_request(self):
        from hexagonal.model.loan import Loan
        import datetime
        self.outstanding = True
        db.session.add(self)
        for qr in self.queued_requests:
            db.session.delete(qr)
        loans = Loan.query.filter(Loan.document == self).all()
        for loan in loans:
            db.session.add(loan)
            loan.due_date = datetime.date.today()
        db.session.commit()