Source code for railgun.website.models

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# @file: railgun/website/models.py
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# This file is released under BSD 2-clause license.

"""Database models are defined with `SQLAlchemy <http://www.sqlalchemy.org>`_.
You may refer to `SQLAlchemy Object Relational Tutorial
<http://docs.sqlalchemy.org/en/latest/orm/tutorial.html>`_ to have a glance
at the usage of this library.
"""

import os
from datetime import datetime

from babel.dates import UTC
from flask.ext.babel import gettext, get_locale
from werkzeug.security import generate_password_hash, check_password_hash

from railgun.common.dateutil import from_plain_date, to_plain_date
from .context import db, app

# define the states of all handins
_ = lambda s: s

#: The tuple of possible submission states.
HANDIN_STATES = (_('Pending'), _('Running'), _('Rejected'), _('Accepted'))


if app.config['SQLALCHEMY_DATABASE_URI'].startswith('mysql'):
    # Special patch: SQLAlchemy will use BLOB as the default backend for
    # PickleType in MySQL database.  However, `partials` may exceeds the
    # limit of 64K.  So Use LongBlob instead for LongPickleType.
    class LongPickleType(db.PickleType):
        from sqlalchemy.databases import mysql
        impl = mysql.MSLongBlob
else:
    LongPickleType = db.PickleType


[docs]class User(db.Model): """A user represents a registered account in Railgun.""" __tablename__ = 'users' # Table arguments. Inrecognized arguments will be ignored by certain # database engine. __table_args__ = {'mysql_engine': 'InnoDB'} #: Primary key of the table id = db.Column(db.Integer, db.Sequence('user_id_seq'), primary_key=True) #: Which authentication provider does this user come from? #: #: An authentication provider is a third-party service that validates #: the login name and password, and provide account information if the #: user passes validation. #: #: :class:`railgun.website.thuauth.TsinghuaAuthProvider` is the #: authentication provider to connect to Tsinghua account. Other #: providers may connect to csv account files, or ldap account servers. #: #: :data:`None` or empty indicates that this user does not come from #: any external provider. provider = db.Column(db.String(32), default='') #: The user's login name, maximum 50 characters, unique among users. name = db.Column(db.String(50), unique=True) #: The user's email address, maximum 70 characters, unique among users. #: #: Note that some authentication providers may not give the user's email #: address. Railgun will create a dummy email address like #: "[name]@not-a-email.secoder.net" for these users, and guide the user #: to fill in their actual email address at their first login. #: #: You may modify the suffix of dummy email addresses in #: ``config.EXAMPLE_USER_EMAIL_SUFFIX``. email = db.Column(db.String(80), unique=True) #: The hashed user password, maximum 255 characters. #: Users from third-party authentication providers will have :data:`None` #: in `password` field. password = db.Column(db.String(255)) #: Is this user an administrator? is_admin = db.Column(db.Boolean, default=False) #: Is this user active? If not active, the user will not be allowed to #: sign in the system. is_active = db.Column(db.Boolean, default=True) #: The user's given name, maximum 64 characters. given_name = db.Column(db.String(64), default='') #: The user's family name, maximum 64 characters. family_name = db.Column(db.String(64), default='') #: Locale name of this user. It will be set to the browser locale #: name when a user is created. #: If browser locale is not known, it will be set to #: ``config.DEFAULT_LOCALE``. locale = db.Column(db.String(16), default=app.config['DEFAULT_LOCALE']) #: Timezone of this user. It will be set to ``config.DEFAULT_TIMEZONE`` #: when a user is created. timezone = db.Column(db.String(32), default=app.config['DEFAULT_TIMEZONE']) #: Refer to the final scores of this user on each homework assignment. scores = db.relationship('FinalScore') def __repr__(self): return "<User(%s)>" % (self.name)
[docs] def set_password(self, password): """Update the hashed :attr:`password` by given plain text `password`. :param password: The plain text password. :type password: :class:`str` """ self.password = generate_password_hash(password)
[docs] def check_password(self, password): """Validate the plain text `password`. Since all users from third-party authentication providers will store :data:`None` in this attribute, you may call :func:`railgun.website.userauth.authenticate` if you just want to validate a user login at a very high-level stage. This method, however, is called mainly by the utilities in :mod:`~railgun.website.userauth`. :param password: The plain text password. :type password: :class:`str` :return: True if `password` passes validation, False otherwise. """ return check_password_hash(self.password, password)
[docs] def gather_scores(self): """Collect the final scores for each homework belong to this user. :return: :class:`dict` of ("homework uuid" -> "final score"). """ return {sc.hwid: sc.score for sc in self.scores}
[docs] def fill_i18n_from_request(self): """Fill :attr:`locale` and :attr:`timezone` according to current browser request. This method only works if it is called in a request context. """ self.locale = str(get_locale()) # TODO: detect timezone from request
[docs]class FinalScore(db.Model): """A final score stores the highest score among all submissions for a particular homework assignment belong to a given user. """ __tablename__ = 'finalscore' # Table arguments. Inrecognized arguments will be ignored by certain # database engine. __table_args__ = {'mysql_engine': 'InnoDB'} id = db.Column(db.Integer, db.Sequence('finalscore_id_seq'), primary_key=True) #: Link with the associated user, usually mapped to a foreign key. user_id = db.Column(db.Integer, db.ForeignKey('users.id')) #: Link with the associated homework. #: #: Since we do not have a homework table, we just store the uuid #: of the homework assignment. Maximum 32 characters. hwid = db.Column(db.String(32), index=True) #: The final score value. score = db.Column(db.Float, default=0.0) def __repr__(self): return ("<FinalScore(uid=%d,hwid=%s,score=%f)>" % (self.user_id, self.hwid, self.score))
[docs]class Handin(db.Model): """A handin stores the information of a submission from user.""" __tablename__ = 'handins' # Table arguments. Inrecognized arguments will be ignored by certain # database engine. __table_args__ = {'mysql_engine': 'InnoDB'} #: We use uuid to seek locate submissions, but we maintain an integral id, #: in that some databases do not support uuids, and most databases handle #: integral primary keys much faster than string uuids. id = db.Column(db.Integer, db.Sequence('handin_id_seq'), primary_key=True) #: The uuid of this submission, used almost everywhere in Railgun system. uuid = db.Column(db.String(32), unique=True) #: The creation time of this submission. #: Value of datetime is in UTC timezone, however, tzinfo is not stored. ctime = db.Column(db.DateTime, default=lambda: datetime.utcnow()) #: Link with the associated homework. hwid = db.Column(db.String(32), index=True) #: The programming language of this submission, maximum 32 characters. lang = db.Column(db.String(32)) #: The state of this submission. One of the following states: #: #: ============ ============================================== #: State Description #: ============ ============================================== #: Pending The submission is waiting to be executed. #: Running The submission is being executed. #: Accepted The submission successfully executed. #: Rejected Some error occurred executing the submission. #: ============ ============================================== state = db.Column(db.Enum(*HANDIN_STATES), default='Pending', index=True) #: The total score of this submission. Usually the sum of all scorers. score = db.Column(db.Float, default=0.0) #: The score scale of this submission, determined by `ctime` and the #: deadline configurations of the homework. scale = db.Column(db.Float, default=0.0) #: The brief comment of this submission. #: #: Actual type should be :class:`railgun.common.lazy_i18n.GetTextString`, #: serialized by :mod:`pickle` and stored as byte sequence. result = db.Column(db.PickleType) #: The compiler error of this submission. #: #: Actual type should be :class:`railgun.common.lazy_i18n.GetTextString`, #: serialized by :mod:`pickle` and stored as byte sequence. compile_error = db.Column(LongPickleType, default=None) #: The program exit code of this submission. exitcode = db.Column(db.Integer) #: The program standard output of this submission. stdout = db.Column(db.Text) #: The program standard error output of this submission. stderr = db.Column(db.Text) #: List of scores from each scorer. #: #: Actual type is :class:`list` of `railgun.common.hw.HwPartialScore`, #: serialized by :mod:`pickle` and stored as byte sequence. partials = db.Column(LongPickleType) #: Link with the associated user, usually mapped to a foreign key. user_id = db.Column(db.Integer, db.ForeignKey('users.id')) #: Refer to the associated user object. user = db.relationship('User') # Basic model object interface def __repr__(self): return '<Handin(%s)>' % self.uuid
[docs] def get_ctime(self): """Attach `UTC` timezone object to :attr:`ctime` and return the new datetime object. """ return from_plain_date(self.ctime, UTC)
[docs] def set_ctime(self, ctime): """Set :attr:`ctime` to the value of `ctime`, but dettach timezone.""" self.ctime = to_plain_date(ctime)
[docs] def get_state(self): """The state is stored as plain text in the database. Get the translated version of the state text. """ return gettext(self.state)
[docs] def is_accepted(self): """Whether this handin has been accepted?""" return self.state == 'Accepted'
[docs] def get_result(self): """The brief comment on this submission may be instance of :class:`railgun.common.lazy_i18n.GetTextString` or :data:`None`. If we call ``unicode(result)`` to get the translated text, we may have the risk to get the literal of "None". This is a safe method to get empty string if :attr:`result` is :data:`None`, or the translated text. """ return unicode(self.result) if self.result else u''
[docs] def get_stdout(self): """Safe method to get empty string if :attr:`stdout` is :data:`None`, or the translated :attr:`stdout`. """ return unicode(self.stdout) if self.stdout else u''
[docs] def get_stderr(self): """Safe method to get empty string if :attr:`stderr` is :data:`None`, or the translated :attr:`stderr`. """ return unicode(self.stderr) if self.stderr else u''
[docs] def get_compile_error(self): """Safe method to get empty string if :attr:`compile_error` is :data:`None`, or the translated :attr:`compile_error`. """ return unicode(self.compile_error) if self.compile_error else u''
class Vote(db.Model): """An instance of :class:`Vote` is a vote initiated by an admini.""" __tablename__ = 'vote' # Table arguments. Inrecognized arguments will be ignored by certain # database engine. __table_args__ = {'mysql_engine': 'InnoDB'} #: The main id of a vote in the relational database. id = db.Column(db.Integer, db.Sequence('vote_id_seq'), primary_key=True) #: Whether the vote is open? is_open = db.Column(db.Boolean, default=False) #: The title of this vote, maximum 255 characters. title = db.Column(db.String(255)) #: The description of this vote (in HTML) desc = db.Column(db.Text, default='') #: How many options should a user select at least? min_select = db.Column(db.Integer, default=0) #: How many options should a user select at most? max_select = db.Column(db.Integer, default=65535) #: The JSON definition of this vote. #: #: This field is used to store "JSON source code", which we use to quickly #: create a new vote. It is not a permanent design, only used when I'm #: preparing for voting system in a hurry, and do not have enough time #: to provide a powerful administration interface. Further developers #: may remove this field and abolish this feature. json_source = db.Column(db.Text) def __repr__(self): return '<Vote(%s)>' % self.title class VoteItem(db.Model): """An instance of :class:`VoteItem` is an option in a vote.""" __tablename__ = 'vote_items' # Table arguments. Inrecognized arguments will be ignored by certain # database engine. __table_args__ = {'mysql_engine': 'InnoDB'} #: The main id of a vote in the relational database. id = db.Column(db.Integer, db.Sequence('vote_item_id_seq'), primary_key=True) #: The logo url of this option, maximum 255 characters. logo = db.Column(db.String(255)) #: The title of this option, maximum 255 characters. title = db.Column(db.String(255)) #: The short description of this vote (in HTML) desc = db.Column(db.Text, default='') #: Link with the associated vote, usually mapped to a foreign key. vote_id = db.Column(db.Integer, db.ForeignKey(Vote.id)) #: Refer to the associated vote object. vote = db.relationship( Vote, backref=db.backref('items', cascade="all, delete-orphan", uselist=True) ) def __repr__(self): return '<VoteItem(%s)>' % self.title class UserVote(db.Model): """An instance of :class:`UserVote` is an option made by a user.""" __tablename__ = 'user_votes' # Table arguments. Inrecognized arguments will be ignored by certain # database engine. __table_args__ = {'mysql_engine': 'InnoDB'} #: The main id of a vote in the relational database. id = db.Column(db.Integer, db.Sequence('user_vote_id_seq'), primary_key=True) #: Link with the associated vote item. vote_item_id = db.Column(db.Integer, db.ForeignKey(VoteItem.id)) #: Refer to the associated vote item object. vote_item = db.relationship( VoteItem, backref=db.backref('user_votes', cascade="all, delete-orphan") ) #: Link with the associated vote user. user_id = db.Column(db.Integer, db.ForeignKey(User.id)) def __repr__(self): return '<UserVote(%s on %s)>' % (self.user_id, self.vote_item_id) def assign_values(obj, dict): """Assign `obj` the field values from `dict`.""" for c in obj.__table__.columns: if c.name in dict: setattr(obj, c.name, dict[c.name]) # If the system uses SQL database, we try to create "db" directory. if app.config['SQLALCHEMY_DATABASE_URI'].startswith('sqlite://'): dpath = os.path.join(app.config['RAILGUN_ROOT'], 'db') if not os.path.isdir(dpath): os.makedirs(dpath) db.create_all()