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
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])
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()
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'}
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'}
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' }
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. """
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]
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
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()