#!/usr/bin/env python
# -*- coding: utf-8 -*-
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# @file: railgun/common/hw.py
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# This file is released under BSD 2-clause license.
"""Homework assignments in Railgun are defined by xml files. All the
definition xml files along with the resources should be placed in a
single directory. You may refer to :ref:`homework` for more details
about how to define a homework assignment.
When the Railgun system is booted, it will scan all the directories
under ``config.HOMEWORK_DIR`` to load the assignments. Each homework
assignment is stored in a :class:`Homework`. Also, the loaded
:class:`Homework` instances are gathered in a global :class:`HwSet`
instance.
All the classes to represent the homework assignments are defined
in this module. The relationships between these classes are:
.. uml::
HwSet o-- Homework : aggregation
Homework "1" *-- "many" HwInfo : contains
Homework "1" *-- "1" FileRules : contains
Homework "1" *-- "many" HwCode : contains
HwCode "1" *-- "1" FileRules : contains
HwCode "1" *-- "many" HwScorerSettings: contains
In addition, the JSON objects carrying the scores for a submission used
in the communication between the runner host and the website are also
defined in this module. The relationships between these classes are:
.. uml::
HwScore "1" *-- "many" HwPartialScore : contains
You may head over to read more about these classes.
"""
import re
import os
from datetime import datetime
from xml.etree import ElementTree
from itertools import ifilter, chain
from markdown import markdown
from babel.dates import timedelta, get_timezone
import config
from . import fileutil
from .fileutil import file_get_contents
from .dateutil import utc_now, to_utc_date, from_plain_date
from .lazy_i18n import lazystr_to_plain, plain_to_lazystr
from .url import reform_path, UrlMatcher
[docs]def parse_bool(s):
"""Convert a string literal into its boolean value.
:param s: The string literal that represents a boolean value.
:type s: :class:`str`
:return: :data:`True` if s.lower() is one of (true, on, 1, yes),
:data:`False` otherwise.
"""
if not s:
return False
s = str(s).lower()
return (s == 'true' or s == 'on' or s == '1' or s == 'yes')
[docs]class FileRules(object):
"""Manage a set of file rules.
A file rule is a 2-element tuple (pattern, action), where :token:`pattern`
determines what files to be ruled, and :token:`action` determines what
operations these files will take.
:token:`pattern` is a regular expression. It should contain patterns
to the relative paths of files.
:token:`action` may be one of the following operations: `ACCEPT`,
`LOCK`, `HIDE` and `DENY`.
Since the file rules are mainly used to determine what files in a homework
pack can be revealed to students, and what files submitted from the
students can be received.
You may refer to :ref:`hwpack` for more details about these four actions.
"""
#: Indicate that the matching files are revealed to students in the
#: homework attachment, and students can overwrite these files in
#: their submissions.
ACCEPT = 0
#: Indicate that the matching files are revealed to students in the
#: homework attachment, but the students cannot overwrite these files
#: in their submissions.
LOCK = 1
#: Indicate that the matching files are `NOT` revealed to students,
#: `NOR` should they been overwritten.
HIDE = 2
#: Indicate that the matching files are denied, and the submission
#: containing these files will be `REJECTED` immediately.
DENY = 3
def __init__(self):
# list of (action, pattern)
self.data = []
def __repr__(self):
return repr(self.data)
[docs] def get_action(self, filename, default_action=LOCK):
"""Get the action to take on given file.
:param filename: The name of the file.
:type filename: :class:`str`
:param default_action: If no rule in this set is matched, what action
should this file take?
:return: One action out of ``(ACCEPT, LOCK, HIDE, DENY)``.
"""
for a, p in self.data:
if p.match(filename):
return a
# if no rule matches, default takes lock action
return default_action
def _make_action(self, action, pattern):
act = None
if action.isalpha():
act = getattr(FileRules, action.upper(), None)
if act is None:
raise ValueError('Unknown action "%s" in file rule.' % action)
pat = re.compile(pattern)
return (act, pat)
[docs] def append_action(self, action, pattern):
"""Append a file rule (action, pattern) to the end of rule list.
The rules are matched in list order. If a given file is matched
by some rule in the list, all the remaining rules after it will
not be checked any longer.
:param action: The action of the rule.
:type action: One action out of ``(ACCEPT, LOCK, HIDE, DENY)``
:param pattern: The file name pattern of the rule.
:type pattern: Regular expression :class:`str`
"""
self.data.append(self._make_action(action, pattern))
[docs] def prepend_action(self, action, pattern):
"""Prepend a file rule (action, pattern) to the front of rule list.
:param action: The action of the rule.
:type action: One action out of ``(ACCEPT, LOCK, HIDE, DENY)``
:param pattern: The file name pattern of the rule.
:type pattern: Regular expression :class:`str`
"""
self.data.insert(0, self._make_action(action, pattern))
[docs] def filter(self, files, allow_actions, default_action=LOCK):
"""Return a new iterator on given file iterator, where files not
taking operations from `allowed_actions` should be removed.
Because this method return the new file name iterator, not a new list,
you must store the list somewhere if you want to use the result for
more than one times, for example::
rules = FileRules()
accepted_files = rules.filter(
dirtree('.'),
(FileRules.ACCEPT, FileRules.LOCK),
)
:param files: The input file name iterator.
:type files: Iterable file names
:param allow_actions: Set of file rules ``(ACCEPT, LOCK, HIDE, DENY)``.
:type allow_actions: :class:`set` or :class:`tuple`
:param default_action: If no rule in rule list is matched, what action
should a given file take?
:return: The new file name iterator.
"""
return ifilter(
lambda f: self.get_action(f) in allow_actions,
files
)
@staticmethod
[docs] def parse_xml(xmlnode):
"""Load file rules from an xml node object.
This method does not care about the tag name of the given node.
It only parses the children of given node. Here's an example of
a file rule node:
.. code-block:: xml
<rules>
<accept>.*\\.py$</accept>
<lock>.*\\.conf$</lock>
<hide>.*\\.pyc$</hide>
<deny>.*\\.exe$</deny>
<rules>
:param xmlnode: The xml node object that contains file rules.
:type xmlnode: :class:`xml.etree.ElementTree.Element`
"""
ret = FileRules()
# there should be at least one rule in file_rules: code.xml is
ret.append_action('hide', '^code\\.xml$')
if xmlnode is not None:
for nd in xmlnode:
ret.append_action(nd.tag, nd.text.strip())
return ret
[docs]class HwInfo(object):
"""Object to store the homework info (name, description & solution).
The description and the solution should be written in Markdown syntax.
There are some extensions to the original Markdown standard. Refer to
:ref:`hwdesc` for more details.
:param lang: The language for name, description and solution.
:type lang: :class:`str`
:param name: The homework name.
:type name: :class:`str`
:param desc: The homework description (Markdown source code).
:type desc: :class:`str`
:param solve: The homework solution (Markdown source code).
:type solve: :class:`str`
"""
def __init__(self, lang, name, desc, solve):
#: The language name of this info object,
#: should be compatible with request.accept_languages.
self.lang = lang
#: The name of the homework.
self.name = name
#: The description of the homework (Markdown source code).
self.desc = desc
#: The solution of this homework (Markdown source code).
self.solve = solve
#: The formatted description of this homework.
#: :class:`Homework` will call `format_markdown` to set this
#: field, so you may use it for free.
self.formatted_desc = None
#: The formatted solution of this homework.
#: :class:`Homework` will call `format_markdown` to set this
#: field, so you may use it for free.
self.formatted_solve = None
def __repr__(self):
__s = lambda s: s.encode('utf-8') if isinstance(s, unicode) else s
return '<HwInfo(lang=%s,name=%s)>' % (self.lang, __s(self.name))
[docs]class HwScorerSetting(object):
"""Store the settings for a particular scorer in ``code.xml``.
This class is a component in :class:`HwCode`. Different programming
languages may carry different scorers, and may reveal different
level of program output to the students.
:param detail: Whether this scorer should show detail reports?
"""
def __init__(self, detail=None):
#: Whether or not to display the detail of this scorer?
#: If None, Railgun will use `HwCode.reportRuntime` as value.
self.detail = detail
@staticmethod
[docs] def parse_xml(xmlnode):
"""Get scorer settings from given xml node object.
An xml node to configure a particular scorer may be like this:
.. code-block:: xml
<scorer name="CodeStyleScorer" detail="true" />
:param xmlnode: Xml node object holding the settings.
:type xmlnode: :class:`xml.etree.ElementTree.Element`
"""
ret = HwScorerSetting()
if xmlnode.get('detail'):
ret.detail = parse_bool(xmlnode.get('detail'))
return ret
[docs]class HwCode(object):
"""Store the definition of a particular programming language in a
homework assignment.
The Railgun system is designed to allow the students to choose their
preferred programming language the finish their homework. Because of
these, each homework should contain seperated code files for each
individual language.
:class:`HwCode` is the class to store language definitions and settings.
The settings may include compiler parameters, runner parameters and
verbosity of output messages.
All the configurations are loaded from ``code.xml``. Refer to
:ref:`hwstruct` to see how ``code.xml`` is organized.
:param path: The root path of the code package.
:type path: :class:`str`
:param lang: The name of programming language.
:type lang: :class:`str`
"""
def __init__(self, path, lang):
#: The root path of code package, should be ``[hw.path]/code/[lang]``.
self.path = path
#: The programming language of this code package.
self.lang = lang
#: Whether or not students can download an attachment for this
#: programming language?
self.has_attach = True
#: An xml node object that stores the compiler parameters.
#: Let the runner host to parse the parameters.
self.compiler_params = None
#: An xml node object that stores the runner parameters.
#: Let the runner host to parse the parameters.
self.runner_params = None
#: The file match rules of this code package.
self.file_rules = None
#: Whether the compiler messages should be revealed to students?
#: Default value is :data:`True`.
self.reportCompile = True
#: Whether the runtime messages should be revealed to students?
#: Default value is :data:`False`.
self.reportRuntime = False
#: A dict ("Scorer's Type Name" -> :class:`HwScorerSetting`).
#: Store the specialized settings for various scorers.
self.scorers = {}
def __repr__(self):
return '<HwCode(%s)>' % self.path
[docs] def get_scorer(self, typeName):
"""Get the settings of a particular scorer.
:param typeName: The scorer type name. It may be the full name of
the scorer, or a partial name that matches the last part of
the type name. For example, if we call `get_scorer` with:
* common.scorers.BaseScorer
* BaseScorer
We will both get the `common.scorers.BaseScorer`.
:type typeName: :class:`str`
:return: The scorer setting object if found, or :data:`None` otherwise.
:rtype: :class:`HwScorerSetting` or :data:`None`
"""
if typeName in self.scorers:
return self.scorers[typeName]
# Truncate full type name
rpos = typeName.rfind('.')
if rpos >= 0:
typeName = typeName[rpos+1:]
return self.scorers.get(typeName, None)
@staticmethod
[docs] def load(path):
"""Load the definitions as a code package under `path`.
This method will create a :class:`HwCode` object that loads settings
from ``[path]/code.xml``, of which the programming language will be
set to ``os.path.split(path)[1]``.
:param path: The root directory of the code package.
:type path: :class:`str`
:return: A :class:`HwCode` object.
"""
lang = os.path.split(path)[1]
ret = HwCode(path, lang)
tree = ElementTree.parse(os.path.join(path, 'code.xml'))
root = tree.getroot()
# whether or not this HwCode provides download attachment
# default value is True if not given
has_attach_node = root.find('attachment')
ret.has_attach = ((has_attach_node is None) or
parse_bool(has_attach_node.text))
# store compiler & runner param nodes
ret.compiler_params = root.find('compiler')
ret.runner_params = root.find('runner')
# get display settings
ret.reportCompile = parse_bool(root.find('reportCompile').text)
ret.reportRuntime = parse_bool(root.find('reportRuntime').text)
# parse the file match rules
ret.file_rules = FileRules.parse_xml(root.find('files'))
# set default hidden rules
ret.file_rules.prepend_action('hide', '^code\\.xml$')
for r in config.DEFAULT_HIDE_RULES:
ret.file_rules.prepend_action('hide', r)
# Iterate through scorer settings
scorer_node = root.find('scorers')
if scorer_node is not None:
for scorer in scorer_node:
name = scorer.get('name')
if not name:
continue
ret.scorers[name] = HwScorerSetting.parse_xml(scorer)
return ret
[docs]class Homework(object):
"""Store the definition of a homework assignment.
The constructor of this class will only create an empty :class:`Homework`
object. You may use `Homework.load(path)` to construct the instance
according to external homework definition files.
"""
def __init__(self):
#: The url slug of this homework assignment.
self.slug = None
#: The root directory of this homework assignment.
self.path = None
#: Unique id of this homework. The submissions and final scores
#: are associated with homework according to `uuid`.
self.uuid = None
#: :class:`HwInfo` objects for different speaking languages.
self.info = []
#: List of (deadline datetime, scale factor).
self.deadlines = []
#: The file matching rules that affects all programming languages.
self.file_rules = None
#: :class:`HwCode` objects for different programming languages.
self.codes = []
#: Cache the mapping from locale name to :class:`HwInfo` object.
self._locale_to_info = {}
#: Cache the mapping from language name to :class:`HwCode` object.
self._lang_to_code = {}
@staticmethod
[docs] def load(path):
"""Load the definitions of homework under `path`.
This method will load the settings from ``[path]/hw.xml``, and create
all :class:`HwInfo` and :class:`HwCode` objects according to the
sub-directories.
Also, the descriptions and solutions in every locale will be formatted
from Markdown source code to html display text. You may get the html
from the `formatted_desc` and `formatted_solve` attribute of
:class:`HwInfo`.
:param path: The root directory of homework.
:type path: :class:`str`
:return: A :class:`Homework` object.
:raises: :class:`ValueError` if the homework definition is wrong.
"""
# Stage 1: load the homework meta data from hw.xml
ret = Homework()
ret.path = path
ret.slug = os.path.split(path)[1]
tree = ElementTree.parse(os.path.join(path, 'hw.xml'))
for nd in tree.getroot():
if nd.tag == 'uuid':
ret.uuid = nd.text.strip()
elif nd.tag == 'names':
for name in nd.iter('name'):
lang = name.get('lang')
name = name.text.strip()
# To define the desc of homework information, [lang].md
# should be created under desc directory.
desc = file_get_contents(
os.path.join(path, 'desc/%s.md' % lang)
)
# solution may be empty
solve_file = os.path.join(path, 'solve/%s.md' % lang)
solve = None
if os.path.isfile(solve_file):
solve = file_get_contents(solve_file)
ret.info.append(HwInfo(lang, name, desc, solve))
elif nd.tag == 'deadlines':
for due in nd.iter('due'):
# get the timezone of due date
timezone = due.find('timezone')
timezone = timezone.text if timezone is not None else None
if not timezone:
timezone = config.DEFAULT_TIMEZONE
timezone = get_timezone(timezone.strip())
# parse the date string
duedate = from_plain_date(
datetime.strptime(due.find('date').text.strip(),
'%Y-%m-%d %H:%M:%S'),
timezone
)
# parse the factor
scale = float(due.find('scale').text.strip())
# add to deadline list
ret.deadlines.append((to_utc_date(duedate), scale))
elif nd.tag == 'files':
ret.file_rules = FileRules.parse_xml(nd)
# Stage 2: discover all programming languages
code_path = os.path.join(path, 'code')
for pl in os.listdir(code_path):
pl_path = os.path.join(code_path, pl)
pl_meta = os.path.join(pl_path, 'code.xml')
# only the directories with contains 'code.xml' may be programming
# language definition
if not os.path.isfile(pl_meta):
continue
# load the programming language definition
ret.codes.append(HwCode.load(pl_path))
# Stage 3: check integrity
if not ret.info:
raise ValueError('Homework name is missing.')
# Stage 4: set default hidden rules
ret.file_rules.prepend_action('hide', '^hw\\.xml$')
ret.file_rules.prepend_action('hide', '^code/\\.*')
ret.file_rules.prepend_action('hide', '^code$')
ret.file_rules.prepend_action('hide', '^desc/\\.*')
ret.file_rules.prepend_action('hide', '^desc$')
ret.file_rules.prepend_action('hide', '^solve/\\.*')
ret.file_rules.prepend_action('hide', '^solve$')
for r in config.DEFAULT_HIDE_RULES:
ret.file_rules.prepend_action('hide', r)
# Stage 5: Cache necessary objects
ret._cache_formatted_markdown()
ret._cache_mappings()
return ret
def _cache_mappings(self):
"""Cache mappings from key to value."""
self._lang_to_code = {c.lang: c for c in self.codes}
self._locale_to_info = {i.lang: i for i in self.info}
def _cache_formatted_markdown(self):
"""Format the descriptions and solutions in all :class:`HwInfo`.
In the performance profiler, formatting the homework description
can take up to 100ms. So I decide to pre-calculate and cache all
formatted descriptions.
"""
for i in self.info:
i.format_markdown(self.slug)
[docs] def get_name_locales(self):
"""Get the locale names of all :class:`HwInfo` objects.
:return: :class:`list` of all locale names.
"""
return [i.lang for i in self.info]
[docs] def get_code_languages(self):
"""Get the programming language names of all :class:`HwCode` objects.
:return: :class:`list` of all programming language names.
"""
return sorted([c.lang for c in self.codes])
[docs] def get_code(self, lang):
"""Get the :class:`HwCode` object whose programming language is `lang`.
:return: The requested :class:`HwCode` object.
:raises: :class:`KeyError` if given language is not found.
"""
return self._lang_to_code[lang]
[docs] def count_attach(self):
"""Count the number of :class:`HwCode` objects with attachment."""
ret = 0
for c in self.codes:
if c.has_attach:
ret += 1
return ret
[docs] def pack_assignment(self, lang, filename):
"""Pack the attachment for given programming language into `filename`.
All the code and resource files will be packed into a new zip archive
file. This behaviour is controlled by :class:`FileRules` in the
:class:`Homework` and the :class:`HwCode` objects. You may refer
to :ref:`hwpack` for more details.
:param lang: The programming language name.
:type lang: :class:`str`
:param filename: The zip attachment file path.
:type filename: :class:`str`
"""
# select the code package
code = self.get_code(lang)
# prepare the file list in root directory.
# only acceptable and locked files are given to students.
#
# note that `code` and `desc` directories are defaultly hidden.
root_files = self.file_rules.filter(
fileutil.dirtree(self.path),
(FileRules.ACCEPT, FileRules.LOCK)
)
# prepare the file list for given `lang`.
code_files = code.file_rules.filter(
fileutil.dirtree(code.path),
(FileRules.ACCEPT, FileRules.LOCK)
)
# make target file name
with fileutil.makezip(filename) as zipf:
path_prefix = self.slug + '/'
fileutil.packzip(self.path, root_files, zipf, path_prefix)
fileutil.packzip(code.path, code_files, zipf, path_prefix)
[docs] def list_files(self, lang):
"""List all runtime files for given programming language.
This method is typically called when you want to get the list of
files that will be copied to runtime directory when executing
a submission in the given programming language.
.. note::
Some files in the result of this method should not be revealed
to students in a downloadable attachment! Use with caution!
:param lang: The programming language name.
:type lang: :class:`str`
:return: An iterable object of file names.
"""
# select the code package
code = self.get_code(lang)
# get list of root files
root_hide = \
re.compile('^code$|^code/\\.*|^desc$|^desc/\\.*|^hw\\.xml$')
root_files = ifilter(
lambda s: not root_hide.match(s),
fileutil.dirtree(self.path)
)
# get list of code files
code_files = fileutil.dirtree(code.path)
return chain(root_files, code_files)
[docs] def get_next_deadline(self):
"""Get the next deadline of this homework.
:return: A :class:`tuple` of (deadline datetime, score scale) if the
next deadline exists, or :data:`None` if the homework has already
expired.
"""
now = utc_now()
for ddl in self.deadlines:
if ddl[0] >= now:
return (ddl[0], ddl[1])
[docs] def get_last_deadline(self):
"""Get the final deadline of this homework.
:return: A :class:`tuple` of (deadline datetime, score scale) if the
final deadline exists, or :data:`None` if the homework has already
expired.
"""
now = utc_now()
ddl = self.deadlines[-1]
if ddl[0] >= now:
return (ddl[0], ddl[1])
[docs]class HwSet(object):
"""Collection of :class:`Homework` definition objects.
Given the root directory of all homework definitions, :class:`HwSet`
will discover and load all homework assignments under that directory.
:param hwdir: The root directory of all homework definitions.
:type hwdir: :class:`str`
"""
def __init__(self, hwdir):
#: The root directory of all homework definitions.
self.hwdir = hwdir
#: :class:`Homework` objects in the order of `slug`.
self.items = None
#: Cache the mapping from `uuid` to :class:`Homework`.
self.__uuid_to_hw = {}
#: Cache the mapping from `slug` to :class:`Homework`.
self.__slug_to_hw = {}
self.reload()
[docs] def reload(self):
"""Reload the homeworks under root directory.
You may manually call this method to reload the definitions.
:class:`HwSet` will not monitor the file changes.
"""
# load all homeworks
self.items = []
for fn in os.listdir(self.hwdir):
fp = os.path.join(self.hwdir, fn)
if (os.path.isdir(fp) and
os.path.isfile(os.path.join(fp, 'hw.xml'))):
self.items.append(Homework.load(fp))
self.items = sorted(self.items, cmp=lambda a, b: cmp(a.slug, b.slug))
# hash all items
self.__uuid_to_hw = {hw.uuid: hw for hw in self.items}
self.__slug_to_hw = {hw.slug: hw for hw in self.items}
def __iter__(self):
"""Iterate over :class:`Homework` objects."""
return iter(self.items)
[docs] def get_by_uuid(self, uuid):
"""Get the :class:`Homework` object with given `uuid`."""
return self.__uuid_to_hw.get(uuid, None)
[docs] def get_by_slug(self, slug):
"""Get the :class:`Homework` object with given `slug`."""
return self.__slug_to_hw.get(slug, None)
[docs] def get_uuid_list(self):
"""Get the list of `uuid` of all :class:`Homework` objects."""
return [hw.uuid for hw in self.items]
[docs]class HwPartialScore(object):
"""A serializable partial score object.
The final score of a submission is a combinition of all scorers.
The scores from every scorer are multiplied with a given `weight`,
and sumed up to form the final score. A :class:`HwPartialScore` is
the score of one scorer.
You may refer to :ref:`json_HwPartialScore` for more details about
the JSON format.
:param scorer_name: The human readable name of this scorer.
:type scorer_name: :class:`railgun.common.lazy_i18n.GetTextString`
:param scorer_type_name: The type name of this scorer.
:type scorer_type_name: :class:`str`
:param score: The final score, range in [0, 100].
:type score: :class:`float`
:param weight: The weight of this scorer.
:type weight: :class:`float`
:param time: The running time of this scorer (in seconds).
:type time: :class:`float`
:param brief_report: The brief output message of this scorer.
:type brief_report: :class:`railgun.common.lazy_i18n.GetTextString`
:param detail_report: The detailed output messages of this scorer.
:type detail_report: :class:`list` of
:class:`railgun.common.lazy_i18n.GetTextString`
"""
def __init__(self, scorer_name, scorer_type_name, score, weight, time,
brief_report, detail_report):
#: The human readable name of this scorer.
#: (:class:`railgun.common.lazy_i18n.GetTextString`)
self.name = scorer_name
#: The type name of this scorer.
self.typeName = scorer_type_name
#: The final score, range in [0, 100].
self.score = score
#: The weight of this scorer.
self.weight = weight
#: The running time of this scorer (in seconds).
self.time = time
#: The brief output message of this scorer.
#: (:class:`railgun.common.lazy_i18n.GetTextString`)
self.brief = brief_report
#: The detailed output messages of this scorer.
#: (class:`list` of :class:`railgun.common.lazy_i18n.GetTextString`)
self.detail = detail_report
[docs] def to_plain(self):
"""Convert this partial score object to a plain object that can be
serialized in JSON message.
:return: A plain object that is composed only with :class:`dict`,
:class:`list`, :class:`str`, :class:`bool` and numeric types.
"""
return {
'name': lazystr_to_plain(self.name),
'typeName': self.typeName,
'score': self.score,
'weight': self.weight,
'time': self.time,
'brief': lazystr_to_plain(self.brief),
'detail': [lazystr_to_plain(d) for d in self.detail]
}
@property
[docs] def get_time(self):
"""Get the running time of this scorer.
:return: A :class:`datetime.timedelta` object, or :data:`None`
if timing is not performed.
"""
return timedelta(seconds=self.time) if self.time else None
@staticmethod
[docs] def from_plain(obj):
"""Make a :class:`HwPartialScore` object from a plain object.
:raises: :class:`TypeError` or :class:`KeyError` if the given plain
object does not represent a valid :class:`HwScore` object.
"""
return HwPartialScore(
plain_to_lazystr(obj['name']),
obj['typeName'],
obj['score'],
obj['weight'],
obj['time'],
plain_to_lazystr(obj['brief']),
[plain_to_lazystr(o) for o in obj['detail']],
)
[docs]class HwScore(object):
"""A serializable final score object.
You may refer to :ref:`json_HwScore` for more details about the
JSON format.
:param accepted: Whether the submission is accepted?
:type accepted: :class:`bool`
:param result: A brief comment on the submission.
:type result: :class:`railgun.common.lazy_i18n.GetTextString`
:param compile_error: The compiler error message.
:type compile_error: :class:`railgun.common.lazy_i18n.GetTextString`
or :class:`basestring`
:param partials: The :class:`HwPartialScore` objects.
:type partials: :class:`list` of :class:`HwPartialScore`
"""
def __init__(self, accepted, result=None, compile_error=None,
partials=None):
# Whether the state of the submission is accepted?
self.accepted = accepted
#: A brief comment on the submission.
#: (:class:`railgun.common.lazy_i18n.GetTextString`)
self.result = result
#: The compiler error message.
#: (:class:`railgun.common.lazy_i18n.GetTextString` or
#: :class:`basestring`)
self.compile_error = compile_error
#: The :class:`HwPartialScore` objects.
#: (:class:`list` of :class:`HwPartialScore`)
self.partials = partials or []
[docs] def get_score(self):
"""Sum up the final score."""
return sum([p.weight * p.score for p in self.partials])
[docs] def add_partial(self, partial):
"""Add a :class:`HwPartialScore` object."""
self.partials.append(partial)
[docs] def to_plain(self):
"""Convert this partial score object to a plain object that can be
serialized in JSON message.
:return: A plain object that is composed only with :class:`dict`,
:class:`list`, :class:`str`, :class:`bool` and numeric types.
"""
return {
'accepted': self.accepted,
'result': lazystr_to_plain(self.result),
'compile_error': lazystr_to_plain(self.compile_error),
'partials': [p.to_plain() for p in self.partials],
}
@staticmethod
[docs] def from_plain(obj):
"""Make a :class:`HwScore` object from a plain object.
:raises: :class:`TypeError` or :class:`KeyError` if the given plain
object does not represent a valid :class:`HwScore` object.
"""
ret = HwScore(obj['accepted'], plain_to_lazystr(obj['result']),
plain_to_lazystr(obj['compile_error']))
for p in obj['partials']:
ret.partials.append(HwPartialScore.from_plain(p))
return ret