Miscellanea

Celery usages

This section briefly describes currently used applications of Celery across the infrastructure.

Getting current time

The main source of the current time in the request processing should be the request.timestamp variable. This variable contains the time when the request was initiated and, when used consistently, allows the admins to time travel.

Usage of timezone.now() is highly discouraged.

class oioioi.base.middleware.TimestampingMiddleware(get_response)[source]

Middleware which adds an attribute timestamp to each request object, representing the request time as datetime.datetime instance.

It should be placed as close to the begging of the list of middlewares as possible.

Current contest mechanism

class oioioi.contests.middleware.CurrentContestMiddleware(get_response)[source]

Middleware which tracks the currently visited contest and stores it to be used in other parts of the current contest mechanism.

It is assumed that all contest-specific url patterns are defined in the contest_patterns variable in each module’s urlconf. These patterns are extended with non contest-specific patterns defined in the urlpatterns variable and then used to generate URLs prefixed with a contest ID (thus the non contest-specific URLs come in two versions, with and without a contest ID prefix). If a request matches a contest ID-prefixed URL and the ID is valid, the contest becomes the current contest. If the ID is not valid, a 404 Not Found is generated.

After a contest becomes the current contest, the corresponding Contest instance is available in request.contest. In addition to that, our custom reverse() function automatically prefixes generated URLs with the contest’s ID if appropriate.

Using settings.CONTEST_MODE, the administrator may decide that users should, if possible, be forcibly put into a contest. Then, if there is no contest ID in a request’s URL, but the URL also comes with a contest-specific version and a contest exists, a redirection is performed to one of the existing contests. Which one it is is determined by the following algorithm:

  1. If last contest is saved in session, this value is used.

  2. If the session value is not available or invalid, settings.DEFAULT_CONTEST is used.

  3. If not set, the most recently created contest will be chosen.

URL patterns may be explicitly defined as requiring that no contest is given using the noncontest_patterns variable in each module’s urlconf. Again, using settings.CONTEST_MODE, the administrator may decide that if a contest is available, users cannot access those URLs. Trying to access them then generates a 403 Permission Denied unless one is a superuser.

oioioi.contests.current_contest.reverse(target, *args, **kwargs)[source]

A modified URL reverser that takes into account the current contest and generates URLs that are appropriately prefixed. With it we substitute the original urls.reverse function.

The choice of prefixing the URL with a particular contest ID (or not prefixing at all) by the function is made as follows:

  • If a contest_id kwarg is given which is not None then the URL, if successfully reversed, is prefixed with it.

  • If a contest_id kwarg equal to None is given then the URL, if successfully reversed, will not be prefixed.

  • If the kwarg isn’t given but a contest is active when calling the function then that contest is used for the generated URL.

  • If the above fails or there is no active contest then no contest will be used.

Our reverser uses the special structure of each app’s urls.py file:

  • Urls pointing to views that require a contest are defined in the contest_patterns pattern list. Those only have a contest-prefixed version.

  • Urls pointing to views that require no contest being active are defined in the noncontest_patterns pattern list. Those only have a non contest-prefixed version.

  • Urls pointing to views that can run both with and without current contest are defined in the urlpatterns pattern list. Those have both versions.

These files are preprocessed to be used by the reverser. Urls defined in oioioi.urls are not preprocessed, so they only have a non-prefixed version, even though they could exist within a contest.

Note that there is no point defining patterns that receive a contest_id kwarg. That particular kwarg is interpreted differently and will never be actually matched in the url pattern when reversing.

You need to take into account the behavior of reverse when defining your own custom urlconf (that means patterns lying outside an app’s urls.py file, e.g. for testing purposes), because it won’t be preprocessed. For that we created the make_patterns() function.

oioioi.contests.urls.make_patterns(neutrals=None, contests=None, noncontests=None, globs=None)[source]

Creates url patterns to be used in a custom urlconf.

Use this function when you create a custom urlconf, for example when writing tests. It will allow our reverse() function to run correctly when using this urlconf.

DON’T use this function when defining patterns in your app’s urls.py file. Instead just define the following variables (though all of them are optional), and the file will be preprocessed by us:

  • contest_patterns - these patterns will generate urls with prefix /c/<contest_id>/ and a request whose path matches such an url will have an attribute called contest. For more information read CurrentContestMiddleware’s documentation. Use this variable if your view needs needs a contest.

  • urlpatterns - these patterns will generate urls both with and without the prefix. If your view doesn’t depend on the contest or its behavior is conditional on the existence of a contest, you should use this variable (this should be the default choice).

  • noncontest_patterns - these patterns will generate urls without the prefix. Use this variable if you think that users accessing your views should not currently participate in any contest.

When creating a custom urlconf, you can use this function and each parameter (with one exception) represents one of the mentioned variables:

Parameters
  • neutrals – represents urlpatterns

  • contests – represents contest_patterns

  • noncontests – represents noncontest_patterns

  • globs – represents global url patterns - those defined in oioioi.urls. These urls won’t be prefixed by us with /c/<contest_id>/, but they could already contain urls in this form. When you create your custom urlconf and you want to use all of the existing OIOIOI urls, you can use this param to pass them (e.g.: from oioioi import urls; make_patterns(..., globs=urls.urlpatterns))

Typically the function’s return value will be assigned to urlpatterns.

oioioi.contests.admin.contest_site = <oioioi.contests.admin.ContestProxyAdminSite object>

Every contest-dependent model admin should be registered in this site using the contest_register method. You can also register non-dependent model admins like you would normally do using the register method. Model admins registered using the contest_register method “don’t exist” when there is no active contest, that is, they can only be accessed by a contest-prefixed URL and they don’t show up in /admin/ (but they do in /c/<contest_id>/admin/).

oioioi.contests.processors.register_current_contest(request)[source]

A template context processor which makes the current contest available to the templates.

The current Contest instance is added to the template context as a contest variable.

Must be used together with CurrentContestMiddleware.

Exclusive contests

class oioioi.contestexcl.middleware.ExclusiveContestsMiddleware(*args, **kwargs)[source]

Middleware which checks whether the user participate in an exclusive contest, which is a contest that blocks other contests, and sets the current contest to that contest.

It works as follows:

  1. If ONLY_DEFAULT_CONTEST is set, only the default contest is taken into account.

  2. All contests with active ExclusivenessConfig instance are acquired from the database.

  3. They are filtered with a special selector function, which by default checks if the user is not a contest admin. In addition, process_view accepts another selector function as an argument. If it is present, the contest list is filtered with a logical conjunction of the default selector and the selector passed as an argument (it may be useful with mixins).

  4. If there is only one contest left, the request.contest variable is set to this contest or a redirect is made if necessary.

  5. If there is more than one contest left, the user is logged out, an error message is displayed and an e-mail describing the situation is sent to the administrators.

Checking for instance-level permissions in templates

To check for model-level permissions, one may use the standard Django mechanism. To check for instance-level permissions, use {% check_perm %} template tag.

oioioi.base.templatetags.check_perm.check_perm(parser, token)[source]

A template tag to look up object permissions.

The current user is tested agains the given permission on the given object. Current user is taken from the template context, so the django.contrib.auth.context_processors.auth template context processor must be present in settings.TEMPLATE_CONTEXT_PROCESSORS.

Usage:

{% load check_perm %}

{% check_perm "some_permission" for some_object as variable %}
{% if variable %}
<p>This is shown if the user has some_permission on some_object.</p>
{% endif %}

Conditions

class oioioi.base.permissions.Condition(condition, *args, **kwargs)[source]

Class representing a condition (a function which returns a boolean based on its arguments) intended for use with views and menu items.

Technically, an instance of this class is a callable object wrapping a function.

Additionally, it implements basic logical operators: AND (&), OR (|), and (~) – a logical negation.

Parameters

condition (fun: *args, **kwargs → bool) – the function to be wrapped

class oioioi.base.permissions.RequestBasedCondition(condition, *args, **kwargs)[source]

Subclass of the Condition class.

It is a special condition class representing a condition which takes request as its only argument.

It allows the usage of oioioi.base.utils.request_cached().

oioioi.base.permissions.make_condition(condition_class=Condition)[source]

Decorator which transforms a function into an instance of a given condition_class (subclass of Condition).

oioioi.base.permissions.make_request_condition(func)

Shortcut for make_condition(RequestBasedCondition). See example usage below.

To assign a condition to a view use the enforce_condition decorator:

oioioi.base.permissions.enforce_condition(condition, template=None, login_redirect=True)[source]

Decorator for views that checks that the request passes the given condition.

condition must be an instance of Condition.

If the condition returns False and template is not None, a suitable TemplateResponse is returned.

If template is None and the user is not authenticated and the login_redirect flag is set to True, a redirect to the login page is issued, otherwise PermissionDenied is raised.

If the condition returns an instance of AccessDenied with a specific response to use, this response is used instead of calling the decorated view.

Parameters
  • condition (Condition) – condition to check

  • template (basestring) – template name to return when condition fails

Additionally, the enforce_condition decorator adds a condition attribute to the view, which can be later used by oioioi.base.menu.MenuRegistry.register_decorator().

Mixing it all together in a simple example:

@make_request_condition
def is_superuser(request):
    return request.user.is_superuser

@enforce_condition(is_superuser & ~is_superuser)
def not_accessible_view(request):
    pass

Switching users (su)

The SU app is used to change the current logged in user on-the-fly.

In order to achieve this goal, the module introduces concept of effective and real user privileges known from Unix-like systems. The effective user is stored in request.user field, while the real in request.real_user.

On-the-fly means that current session variables are preserved while changing effective user, which may be also a pitfall if some code stores there data directly connected with current user scope.

oioioi.su.utils.su_to_user(request, user, backend_path=None)[source]

Changes current effective user to user.

After changing to user, original request.user is saved in request.real_user. If given, backend_path should be dotted name of authentication backend, otherwise it’s inherited from current user.

oioioi.su.utils.reset_to_real_user(request)[source]

Changes effective user back to real user

Zeus integration (zeus)

The zeus app is used for integration between oioioi and zeus, a system for grading distributed programing problems.

Zeus instances are configured in settings.ZEUS_INSTANCES, which is a dict mapping zeus_id - unique identifier of a zeus instance - to (zeus_url, zeus_login, zeus_secret) - base URL for zeus api (ZBU) and credentials. It is also possible to use a mock instance (ZeusTestServer) which allows manual testing for development purposes.

API specification

Communication with zeus is done over HTTPS protocol, in a REST-like style. Data is encoded using JSON. OIOIOI authorizes itself to zeus using HTTP Basic Authentication, with login and secret fixed for a zeus instance.

Prefix ? means optional attribute.

Sending submissions

Request

POST ZBU/dcj_problem/zeus_problem_id/submissions
Data sent

{
“submission_type”: submission_type :: Base64String(SMALL|LARGE),
“return_url”: return_url :: Base64String,
“username”: username :: Base64String,
“metadata”: metadata :: Base64String,
“source_code”: source_code :: Base64String,
“language”: source_language :: Base64String(CPP|…),
}
Result

Code 200 and data:

{ “submission_id”: unique_job_id :: Uint }

or code 4xx|5xx and data:

{ ? “error”: error_description :: Base64String }

username and metadata fields are not used by Zeus and sent for debugging purposes only.

Receiving results

Zeus hits the “return_url” from submission data once it is graded.

Data received

{
“compilation_output”: output :: Base64String,
}

in case of compilation failure or

{
“tests_info”: list_of_results :: [TestInfo],
}

in case of compilation success, where

TestInfo = {
“time_limit_ms”: execution_time_limit :: Int,
“memory_limit_byte”: memory_limit :: Int,
“verdict”: test_status :: Base64String(OK|WA|TLE|RE|RV|OLE|MSE|MCE),
“runtime”: max_of_times_at_all_machines :: Int,
“metadata”: more_data_about_test :: Base64String,
? “nof_nodes”: number_of_nodes :: Int,
}
Our response

Code 200 and HttpResponse("Recorded!") or code 4xx|5xx and a lot of HTML (for example the one which normally displays a message Internal Server Error in a browser). When server received invalid JSON or strings are not encoded with Base64, then it will response with code 400 and nice error message.

MSE and MCE are statuses meaning that size or count of outgoing messages sent by submitted program has exceeded the limit.

Metadata is subject to coordination between judges and contest admin. It may be passed through zeus, but in the most recent workflow we sent meaningless metadata to zeus and received meaningful metadata (zeus admins were provided with a file containing metadata for each test). It is designed to contain additional data about grouping, scoring etc. Currently we expect it to be in format:

test name,group name,max score

Test name will be shown to users. All tests with the same, non-empty group name will be grouped together. All tests in group shall have the same max score. Example tests are expected to be in the group 0.

Selenium tests

This module allows us to run selenium tests for OIOIOI. As opposed to regular django tests these are integration tests run under production-like environment, with external filetracker and database.

USELEFUL TIPS AND TRICKS

  1. Do not run tests straight by invoking test_selenium.sh script. Instead build docker images without the -d flag in another terminal (just copy and paste the command from the script and remove -d) [docker-compose -f docker-compose-selenium.yml up]. Why? You can easily navigate through the logs there real-time (instance is visible at 8001 port).

  2. Run the docker images and test from a different terminal (using pytest). There is one problem with this, though. When you need to reload the database, just connect to the docker and manually restart it.

  3. Use firefox to take correct XPATHs to some elements (just inspect and copy). The alternatives are third-party extensions to Chrome, they can even export test to python2 code - but watch out here, sometimes the IDs in OIOIOI are broken (e.g. the ID of a date is a date itself).

  4. When in trouble you can connect your own webdriver (f.e. chrome webdriver) and write tests to see the results right at your screen.

  5. Firefox and selenium are broken. Clicking the element once is not enough for most cases, just click twice with an exception catching phrases or send some KEY presses to the element.

Test creation process

Tests may be created in two ways:
  • Simply write test in python, basing on existing one,

  • Use Selenium IDE plugin for Firefox.

Writing tests with Selenium IDE

With Selenium IDE it’s possible to record mouse and keyboard events in order to create a test.

Before you start look at Predefined selenium actions, there are some helpful methods that simplify common tasks like login or form submit.

Let’s assume you have recorded a test. What’s next?

  1. Export the test to proper python file.
    File -> Export Test Case As… -> Python 2 / unittest / WebDriver
  2. Remove setUp and tearDown functions and set OIOIOISeleniumTestCase as a superclass.

  3. Important! Even if test is doing what it’s supposed to do, there’s still some work left. Selenium recorder will recognize elements in the easiest possible way. That’s not what we want, things may change and test will broke because of insufficient description. Simplified html access section may be helpful now. The goal is to find elements that are poorly accessed by Selenium (for ex. by a class that is not guaranteed to be unique). Usually it applies to tables, buttons. The best solution is to access elements by ID’s, if there’s no corresponding ID, just do the best you can. It’s recommended to find elements by xpath, that way you can nicely access nested elements with specific classes, ids or tags.

Running Selenium tests

Selenium tests are excluded from default Django tests because they need different environment. In OIOIOI’s root directory there’s script test_selenium.sh which runs all Selenium tests. Any arguments passed to it are forwarded to pytest. This script is responsible for launching docker containers with fresh OIOIOI and stuff needed by Selenium. By default it take some time to perform whole process so you may customize script to just wipe data between launches etc.

What to do when tests are not working

Selenium tests easily get broken after frontend updates – for example adding new buttons. If there were errors in testing the first place to go are the logs. Unfortunately, you can’t rely on screenshots because stylesheets can’t be parsed by Selenium (maybe that will be fixed soon…). The best alternative we came up with is printing the whole page source and url after each important step of a test.

Sometimes the tests themselves may be working fine and the real culprit is Docker or Selenium. Sometimes new Firefox version may become incompatible with Selenium, so update each of them with care.

Predefined selenium actions

Methods to perform common actions.

If you write some code that fits this section, don’t hesitate to add it there!

Simplified html access

If you write some code that fits this section, don’t hesitate to add it there!