diff --git a/MANIFEST.in b/MANIFEST.in index 43965e228..1b3b1101c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,7 +2,6 @@ include LICENSE include README.rst include requirements.txt include requirements_mturk.txt -recursive-include otree/certs * recursive-include otree/static * recursive-include otree/templates * recursive-include otree/project_template * diff --git a/PKG-INFO b/PKG-INFO index 363beb1f4..5ab59f0d6 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: otree -Version: 2.3.2 +Version: 2.5.0 Summary: oTree is a toolset that makes it easy to create and administer web-based social science experiments. Home-page: http://otree.org/ Author: chris@otree.org diff --git a/otree/__init__.py b/otree/__init__.py index 911a4bbad..e74605d16 100644 --- a/otree/__init__.py +++ b/otree/__init__.py @@ -1,4 +1,4 @@ # setup.py imports this module, so this module must not import django # or any other 3rd party packages. -__version__ = '2.3.2' +__version__ = '2.5.0' default_app_config = 'otree.apps.OtreeConfig' diff --git a/otree/api.py b/otree/api.py index c38cca557..37b237b92 100644 --- a/otree/api.py +++ b/otree/api.py @@ -3,8 +3,8 @@ from otree.models import BaseSubsession, BaseGroup, BasePlayer # noqa from otree.constants import BaseConstants # noqa from otree.views import Page, WaitPage # noqa -from otree.common import Currency, currency_range, safe_json # noqa -from otree.bots import Bot, Submission, SubmissionMustFail # noqa - -models = _import_module('otree.models') -widgets = _import_module('otree.forms.widgets') +from otree.currency import Currency, currency_range # noqa +from otree.common import safe_json +from otree.bots import Bot, Submission, SubmissionMustFail, expect # noqa +from otree import models # noqa +from otree.forms import widgets # noqa diff --git a/otree/api.pyi b/otree/api.pyi index df669b7a1..f160caeea 100644 --- a/otree/api.pyi +++ b/otree/api.pyi @@ -1,5 +1,6 @@ -from typing import Union, List, Any -from otree.common import RealWorldCurrency, Currency +from typing import Union, List, Any, Optional + +from otree.currency import RealWorldCurrency, Currency class Currency(Currency): ''' @@ -7,9 +8,11 @@ class Currency(Currency): (if I import, it says the reference to Currency is not found) ''' -def currency_range(first, last, increment) -> List[Currency]: pass -def safe_json(obj): pass +def currency_range(first, last, increment) -> List[Currency]: + pass +def safe_json(obj): + pass # mocking the public API for PyCharm autocomplete. # one downside is that PyCharm doesn't seem to fully autocomplete arguments @@ -54,51 +57,49 @@ class models: def __getattr__(self, item): pass - class BooleanField(bool): def __init__( - self, - *, - choices=None, - widget=None, - initial=None, - label=None, - doc='', - blank=False, - **kwargs): + self, + *, + choices=None, + widget=None, + initial=None, + label=None, + doc='', + blank=False, + **kwargs + ): pass - class StringField(str): def __init__( - self, - *, - choices=None, - widget=None, - initial=None, - label=None, - doc='', - max_length=10000, - blank=False, - **kwargs): + self, + *, + choices=None, + widget=None, + initial=None, + label=None, + doc='', + max_length=10000, + blank=False, + **kwargs + ): pass - class LongStringField(str): def __init__( - self, - *, - initial=None, - label=None, - doc='', - max_length=None, - blank=False, - **kwargs): + self, + *, + initial=None, + label=None, + doc='', + max_length=None, + blank=False, + **kwargs + ): pass - # need to copy-paste the __init__ between # Integer, Float, and Currency # because if I use inheritance, PyCharm doesn't auto-complete # while typing args - class IntegerField(int): def __init__( self, @@ -111,9 +112,9 @@ class models: min=None, max=None, blank=False, - **kwargs): - pass - + **kwargs + ): + pass class FloatField(float): def __init__( self, @@ -126,9 +127,9 @@ class models: min=None, max=None, blank=False, - **kwargs): - pass - + **kwargs + ): + pass class CurrencyField(Currency): def __init__( self, @@ -141,149 +142,189 @@ class models: min=None, max=None, blank=False, - **kwargs): - pass - - + **kwargs + ): + pass class widgets: def __getattr__(self, item): pass - # don't need HiddenInput because you can just write # and then you know the element's selector - class CheckboxInput: pass - class RadioSelect: pass - class RadioSelectHorizontal: pass - class Slider: pass - + class CheckboxInput: + pass + class RadioSelect: + pass + class RadioSelectHorizontal: + pass class Session: - config = None # type: dict - vars = None # type: dict - num_participants = None # type: int - def get_participants(self) -> List[Participant]: pass - def get_subsessions(self) -> List[BaseSubsession]: pass + config: dict + vars: dict + num_participants: int + def get_participants(self) -> List[Participant]: + pass + def get_subsessions(self) -> List[BaseSubsession]: + pass class Participant: - session = None # type: Session - vars = None # type: dict - label = None # type: str - id_in_session = None # type: int - payoff = None # type: Currency - - def get_players(self) -> List[BasePlayer]: pass - def payoff_plus_participation_fee(self) -> RealWorldCurrency: pass - - -class BaseConstants: pass + session: Session + vars: dict + label: str + id_in_session: int + payoff: Currency + def get_players(self) -> List[BasePlayer]: + pass + def payoff_plus_participation_fee(self) -> RealWorldCurrency: + pass +class BaseConstants: + pass class BaseSubsession: - session = None # type: Session - round_number = None # type: int - - def get_groups(self) -> List[BaseGroup]: pass - def get_group_matrix(self) -> List[List[BasePlayer]]: pass + session: Session + round_number: int + def get_groups(self) -> List[BaseGroup]: + pass + def get_group_matrix(self) -> List[List[BasePlayer]]: + pass def set_group_matrix( - self, - group_matrix: Union[List[List[BasePlayer]],List[List[int]]]): pass - def get_players(self) -> List[BasePlayer]: pass - def in_previous_rounds(self) -> List['BaseSubsession']: pass - def in_all_rounds(self) -> List['BaseSubsession']: pass - def creating_session(self): pass - def in_round(self, round_number) -> 'BaseSubsession': pass - def in_rounds(self, first, last) -> List['BaseSubsession']: pass - def group_like_round(self, round_number: int): pass - def group_randomly(self, fixed_id_in_group: bool=False): pass - def vars_for_admin_report(self): pass - + self, group_matrix: Union[List[List[BasePlayer]], List[List[int]]] + ): + pass + def get_players(self) -> List[BasePlayer]: + pass + def in_previous_rounds(self) -> List[BaseSubsession]: + pass + def in_all_rounds(self) -> List[BaseSubsession]: + pass + def creating_session(self): + pass + def in_round(self, round_number) -> BaseSubsession: + pass + def in_rounds(self, first, last) -> List[BaseSubsession]: + pass + def group_like_round(self, round_number: int): + pass + def group_randomly(self, fixed_id_in_group: bool = False): + pass + def vars_for_admin_report(self) -> dict: + pass # this is so PyCharm doesn't flag attributes that are only defined on the app's Subsession, # not on the BaseSubsession - def __getattribute__(self, item): pass + def __getattribute__(self, item): + pass class BaseGroup: - session = None # type: Session - subsession = None # type: BaseSubsession - round_number = None # type: int - - def get_players(self) -> List[BasePlayer]: pass - def get_player_by_role(self, role) -> BasePlayer: pass - def get_player_by_id(self, id_in_group) -> BasePlayer: pass - def in_previous_rounds(self) -> List['BaseGroup']: pass - def in_all_rounds(self) -> List['BaseGroup']: pass - def in_round(self, round_number) -> 'BaseGroup': pass - def in_rounds(self, first: int, last: int) -> List['BaseGroup']: pass - - def __getattribute__(self, item): pass + session: Session + subsession: BaseSubsession + round_number: int + def get_players(self) -> List[BasePlayer]: + pass + def get_player_by_role(self, role) -> BasePlayer: + pass + def get_player_by_id(self, id_in_group) -> BasePlayer: + pass + def in_previous_rounds(self) -> List[BaseGroup]: + pass + def in_all_rounds(self) -> List[BaseGroup]: + pass + def in_round(self, round_number) -> BaseGroup: + pass + def in_rounds(self, first: int, last: int) -> List[BaseGroup]: + pass + def __getattribute__(self, item): + pass class BasePlayer: - id_in_group = None # type: int - payoff = None # type: Currency - participant = None # type: Participant - session = None # type: Session - group = None # type: BaseGroup - subsession = None # type: BaseSubsession - round_number = None # type: int - - def in_previous_rounds(self) -> List['BasePlayer']: pass - def in_all_rounds(self) -> List['BasePlayer']: pass - def get_others_in_group(self) -> List['BasePlayer']: pass - def get_others_in_subsession(self) -> List['BasePlayer']: pass - def role(self) -> str: pass - def in_round(self, round_number) -> 'BasePlayer': pass - def in_rounds(self, first, last) -> List['BasePlayer']: pass - - def __getattribute__(self, item): pass - + id_in_group: int + payoff: Currency + participant: Participant + session: Session + group: BaseGroup + subsession: BaseSubsession + round_number: int + def in_previous_rounds(self) -> List[BasePlayer]: + pass + def in_all_rounds(self) -> List[BasePlayer]: + pass + def get_others_in_group(self) -> List[BasePlayer]: + pass + def get_others_in_subsession(self) -> List[BasePlayer]: + pass + def role(self) -> str: + pass + def in_round(self, round_number) -> BasePlayer: + pass + def in_rounds(self, first, last) -> List[BasePlayer]: + pass + def __getattribute__(self, item): + pass class WaitPage: wait_for_all_groups = False group_by_arrival_time = False - title_text = None - body_text = None - template_name = None - round_number = None # type: int - participant = None # type: Participant - session = None # type: Session - - def is_displayed(self) -> bool: pass - def after_all_players_arrive(self): pass - def get_players_for_group(self, waiting_players): pass - + title_text: str + body_text: str + template_name: str + round_number: int + participant: Participant + session: Session + def is_displayed(self) -> bool: + pass + def after_all_players_arrive(self): + pass + def get_players_for_group(self, waiting_players) -> Optional[list]: + pass class Page: - round_number = None # type: int - template_name = None # type: str - timeout_seconds = None # type: int - timeout_submission = None # type: dict - timeout_happened = None # type: bool - timer_text = None # type: str - participant = None # type: Participant - session = None # type: Session - form_model = None # - form_fields = None # type: List[str] - - def get_form_fields(self) -> List['str']: pass - def vars_for_template(self) -> dict: pass - def before_next_page(self): pass - def is_displayed(self) -> bool: pass - def error_message(self, values): pass - def get_timeout_seconds(self): pass - + round_number: int + template_name: str + timeout_seconds: int + timeout_submission: dict + timeout_happened: bool + timer_text: str + participant: Participant + session: Session + form_model: str + form_fields: List[str] + def get_form_fields(self) -> List[str]: + pass + def vars_for_template(self) -> dict: + pass + def before_next_page(self): + pass + def is_displayed(self) -> bool: + pass + def error_message(self, values) -> Optional[str]: + pass + def get_timeout_seconds(self) -> Optional[float]: + pass + def app_after_this_page(self, upcoming_apps: List[str]) -> Optional[str]: + pass class Bot: - html = '' # type: str - case = None # type: Any - cases = [] # type: List - participant = None # type: Participant - session = None # type: Participant - round_number = None # type: int - -def Submission(PageClass, post_data: dict={}, *, check_html=True, timeout_happened=False): pass -def SubmissionMustFail(PageClass, post_data: dict={}, *, check_html=True, error_fields=[]): pass + html: str + case: Any + cases: List + participant: Participant + session: Participant + round_number: int + +def Submission( + PageClass, post_data: dict = {}, *, check_html=True, timeout_happened=False +): + pass + +def SubmissionMustFail( + PageClass, post_data: dict = {}, *, check_html=True, error_fields=[] +): + pass + +def expect(*args): + pass diff --git a/otree/app_template/models.py b/otree/app_template/models.py index 3ec45e612..561c8fdc3 100644 --- a/otree/app_template/models.py +++ b/otree/app_template/models.py @@ -1,6 +1,12 @@ from otree.api import ( - models, widgets, BaseConstants, BaseSubsession, BaseGroup, BasePlayer, - Currency as c, currency_range + models, + widgets, + BaseConstants, + BaseSubsession, + BaseGroup, + BasePlayer, + Currency as c, + currency_range, ) diff --git a/otree/app_template/pages.py b/otree/app_template/pages.py index 5e7439afd..bc06d2716 100644 --- a/otree/app_template/pages.py +++ b/otree/app_template/pages.py @@ -8,7 +8,6 @@ class MyPage(Page): class ResultsWaitPage(WaitPage): - def after_all_players_arrive(self): pass @@ -17,8 +16,4 @@ class Results(Page): pass -page_sequence = [ - MyPage, - ResultsWaitPage, - Results -] +page_sequence = [MyPage, ResultsWaitPage, Results] diff --git a/otree/app_template/tests.py b/otree/app_template/tests.py index 236de688c..fbb60094e 100644 --- a/otree/app_template/tests.py +++ b/otree/app_template/tests.py @@ -5,6 +5,5 @@ class PlayerBot(Bot): - def play_round(self): pass diff --git a/otree/apps.py b/otree/apps.py index f70def866..58334da91 100644 --- a/otree/apps.py +++ b/otree/apps.py @@ -1,26 +1,32 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- import logging import sys +import sqlite3 +import django.db.utils import colorama from django.apps import AppConfig -from django.conf import settings from django.db.models import signals -import otree -import otree.common_internal -from otree.common_internal import ( - ensure_superuser_exists -) +from otree.common import ensure_superuser_exists from otree.strict_templates import patch_template_silent_failures +try: + from psycopg2.errors import UndefinedColumn, UndefinedTable +except ModuleNotFoundError: + + class UndefinedColumn(Exception): + pass + + class UndefinedTable(Exception): + pass + logger = logging.getLogger('otree') def create_singleton_objects(sender, **kwargs): from otree.models_concrete import UndefinedFormModel + for ModelClass in [UndefinedFormModel]: # if it doesn't already exist, create one. ModelClass.objects.get_or_create() @@ -33,6 +39,33 @@ def create_singleton_objects(sender, **kwargs): ) +def patched_execute(self, sql, params=None): + try: + return self._execute_with_wrappers( + sql, params, many=False, executor=self._execute + ) + except Exception as exc: + + ExceptionClass = type(exc) + tb = sys.exc_info()[2] + # Django seems to reraise with new exceptions, so we need to look at the __cause__: + # sqlite3.OperationalError -> django.db.utils.OperationalError + # psycopg2.errors.UndefinedColumn -> django.db.utils.ProgrammingError + CauseClass = type(exc.__cause__) + + if CauseClass == sqlite3.OperationalError and 'locked' in str(exc): + raise ExceptionClass(f'{exc} - {SQLITE_LOCKING_ADVICE}.').with_traceback( + tb + ) from None + + # this will only work on postgres, but if they are using sqlite they should be using + # devserver anyway. + if CauseClass in (UndefinedColumn, UndefinedTable): + msg = f'{exc} - try resetting the database.' + raise ExceptionClass(msg).with_traceback(tb) from None + raise + + def monkey_patch_db_cursor(): '''Monkey-patch the DB cursor, to catch ProgrammingError and OperationalError. The alternative is to use middleware, but (1) @@ -42,57 +75,21 @@ def monkey_patch_db_cursor(): unrelated to resetdb. This is the most targeted location. ''' - - # In Django 2.0, this method is renamed to _execute. - def execute(self, sql, params=None): - self.db.validate_no_broken_transaction() - with self.db.wrap_database_errors: - try: - if params is None: - return self.cursor.execute(sql) - else: - return self.cursor.execute(sql, params) - except Exception as exc: - ExceptionClass = type(exc) - # it seems there are different exceptions all named - # OperationalError (django.db.OperationalError, - # sqlite.OperationalError, mysql....) - # so, simplest to use the string name - if ExceptionClass.__name__ in ( - 'OperationalError', 'ProgrammingError'): - # these error messages are localized, so we can't - # just check for substring 'column' or 'table' - # all the ProgrammingError and OperationalError - # instances I've seen so far are related to resetdb, - # except for "database is locked" - tb = sys.exc_info()[2] - if 'locked' in str(exc): - advice = SQLITE_LOCKING_ADVICE - import django.db.transaction - else: - advice = 'try resetting the database ("otree resetdb")' - - raise ExceptionClass('{} - {}.'.format( - exc, advice)).with_traceback(tb) from None - else: - raise - from django.db.backends import utils - utils.CursorWrapper.execute = execute + + utils.CursorWrapper.execute = patched_execute def setup_create_default_superuser(): signals.post_migrate.connect( - ensure_superuser_exists, - dispatch_uid='otree.create_superuser' + ensure_superuser_exists, dispatch_uid='otree.create_superuser' ) def setup_create_singleton_objects(): - signals.post_migrate.connect(create_singleton_objects, - dispatch_uid='create_singletons') - - + signals.post_migrate.connect( + create_singleton_objects, dispatch_uid='create_singletons' + ) class OtreeConfig(AppConfig): @@ -109,7 +106,6 @@ def ready(self): colorama.init(autoreset=True) import otree.checks + otree.checks.register_system_checks() patch_template_silent_failures() - - diff --git a/otree/bots/__init__.py b/otree/bots/__init__.py index 0d08d0874..2d1f003ef 100644 --- a/otree/bots/__init__.py +++ b/otree/bots/__init__.py @@ -1,27 +1 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# NOTE: this imports the following submodules and then subclasses several -# classes importing is done via import_module rather than an ordinary import. -# -# The only reason for this is to hide the base classes from IDEs like PyCharm, -# so that those members/attributes don't show up in autocomplete, -# including all the built-in django fields that an ordinary oTree programmer -# will never need or want. if this was a conventional Django project I wouldn't -# do it this way, but because oTree is aimed at newcomers who may need more -# assistance from their IDE, I want to try this approach out. -# -# This module is also a form of documentation of the public API. - -# 2016-07-18: not using the import_module trick for now, because currently, -# the PlayerBot class doesn't have any methods we need to hide -# from importlib import import_module -# otree_bot = import_module('otree.bots.bot') - -from importlib import import_module - -_bot_module = import_module('otree.bots.bot') - -Bot = _bot_module.PlayerBot -Submission = _bot_module.Submission -SubmissionMustFail = _bot_module.SubmissionMustFail +from .bot import PlayerBot as Bot, Submission, SubmissionMustFail, expect diff --git a/otree/bots/bot.py b/otree/bots/bot.py index 4118465cf..7f41b1b66 100644 --- a/otree/bots/bot.py +++ b/otree/bots/bot.py @@ -2,23 +2,24 @@ import re import decimal import logging -import abc -import six +import operator + from urllib.parse import unquote, urlsplit -from six.moves.html_parser import HTMLParser +from html.parser import HTMLParser +import otree.constants from otree.models_concrete import ParticipantToPlayerLookup from django import test -from django.core.urlresolvers import resolve +from django.urls import resolve from django.conf import settings from otree.currency import Currency -from django.apps import apps -from otree import constants_internal from otree.models import Participant, Session -from otree import common_internal -from otree.common_internal import ( - get_dotted_name, get_bots_module, get_admin_secret_code, - get_models_module +from otree import common +from otree.common import ( + get_dotted_name, + get_bots_module, + get_admin_secret_code, + get_models_module, ) ADMIN_SECRET_CODE = get_admin_secret_code() @@ -26,8 +27,11 @@ logger = logging.getLogger('otree.bots') INTERNAL_FORM_FIELDS = { - 'csrfmiddlewaretoken', 'must_fail', 'timeout_happened', - 'admin_secret_code', 'error_fields' + 'csrfmiddlewaretoken', + 'must_fail', + 'timeout_happened', + 'admin_secret_code', + 'error_fields', } DISABLE_CHECK_HTML_INSTRUCTIONS = ''' @@ -39,32 +43,92 @@ yield Submission(views.PageName, {{...}}, check_html=False) ''' -HTML_MISSING_BUTTON_WARNING = (''' +HTML_MISSING_BUTTON_WARNING = ( + ( + ''' Bot is trying to submit page {page_name}, but no button was found in the HTML of the page. (searched for with type='submit' or +

- The above button will expire this HIT early. - You should click this button before deleting the session. - Otherwise, your nonexistent session will still be advertised - on the MTurk website, and MTurk workers will get a "page not found" error. - (However, it is safe to delete the session if all assignments have been - submitted.) + The above button will expire this HIT early. + You should click this button before deleting the session. + Otherwise, your nonexistent session will still be advertised + on the MTurk website, and MTurk workers will get a "page not found" error. + (However, it is safe to delete the session if all assignments have been + submitted.)

-{% else %} - {% if form.errors %} -
- {% blocktrans trimmed %}Please fix the errors in the - form.{% endblocktrans %} -
- {% endif %} + {% else %} + +
{% csrf_token %} - +
+ +

+ If this box is checked, your HIT will not be published to the MTurk live site, but rather + to the MTurk Sandbox, so you can test how it will look to MTurk workers. +

+
- {% include 'otree/forms/layouts/bootstrap.html' %} - {% if missing_next_button_warning %} -
- {{ missing_next_button_warning }} -
- {% endif %} - +

+ When you click the below button, your HIT will be immediately published on MTurk. +

+
-{% endif %} + {% endif %} -{% include "otree/includes/messages.html" %} + {% include "otree/includes/messages.html" %} {% endblock %} diff --git a/otree/templates/otree/admin/MTurkSessionPayments.html b/otree/templates/otree/admin/MTurkSessionPayments.html index bc7c3644d..c85be4d34 100644 --- a/otree/templates/otree/admin/MTurkSessionPayments.html +++ b/otree/templates/otree/admin/MTurkSessionPayments.html @@ -2,287 +2,277 @@ {% load otree %} {% block internal_scripts %} - {{ block.super }} - + {{ block.super }} + {% endblock %} {% block content %} - {{ block.super }} + {{ block.super }} - {% if not published %} -

- The MTurk payments page will appear after you publish this - session - to MTurk. -

- {% else %} -
-

Session

- - - - - + {% if not published %} +

+ The MTurk payments page will appear after you publish this + session + to MTurk. +

+ {% else %} +
+
+
Session type{{ session.config.name }}
+ + + + - - - - + + + + - - - - +
MTurk HIT ID{{ session.mturk_HITId }}
Session code{{ session.code }}
+ Participation fee + + {{ participation_fee }} +
MTurk Hit Id{{ session.mturk_HITId }}
- - Experimenter name - {{ session.experimenter_name|default_if_none:"" }} - + {% if participants_not_reviewed %} +
+ {% csrf_token %} - +

Participants to be reviewed

- {% if participants_not_reviewed %} - - {% csrf_token %} - -

Assignments to be reviewed

- - - - - - - - - - - - +
Participant codeAssignment IdWorker IdProgress - Participation fee (Reward) - - Variable pay (Bonus) - Total pay - Select -
- -
-
+ + + + + + + + + + + - {% for p in participants_not_reviewed %} - - - - - - - - - - - {% endfor %} -
CodeAssignment IDWorker IDProgressCompletion codeWait page total + Bonus payoff + Total pay + Select +
+ +
+
{{ p.code }}{{ p.mturk_assignment_id|default_if_none:"" }}{{ p.mturk_worker_id|default_if_none:"" }}{{ p.current_page_ }} - {{ participation_fee }} - - {{ p.payoff_in_real_world_currency }} - {{ p.payoff_plus_participation_fee }} -
- -
-
-
- - + {% for p in participants_not_reviewed %} + + {{ p.code }} + {{ p.mturk_assignment_id|default_if_none:"" }} + {{ p.mturk_worker_id|default_if_none:"" }} + {{ p.current_page_ }} + {{ p.mturk_answers_formatted }} + {{ p.waiting_seconds }} sec + + {{ p.payoff_in_real_world_currency }} + + {{ p.payoff_plus_participation_fee }} + +
+ +
+ + + {% endfor %} + +
+ + +
+ + {% endif %} +
+ {% include "otree/includes/messages.html" %} {% endblock %} diff --git a/otree/templates/otree/admin/ServerCheck.html b/otree/templates/otree/admin/ServerCheck.html index e66f82616..845724bc4 100644 --- a/otree/templates/otree/admin/ServerCheck.html +++ b/otree/templates/otree/admin/ServerCheck.html @@ -1,75 +1,71 @@ {% extends "otree/BaseAdmin.html" %} {% block title %} - Server Readiness Checks + Server Readiness Checks {% endblock %} {% block content %} - {% if pypi_results.pypi_connection_error %} -
- Update status unknown - Could not connect to PyPI - to check if oTree is up to date. -
- {% elif pypi_results.update_needed %} -
- You are using an old oTree version - {{ pypi_results.update_message }} -
- {% else %} -
- You have a recent version of oTree ({{ pypi_results.installed_version }}). -
- {% endif %} - {% if sqlite %} -
- Using SQLite You are using SQLite, which is only suitable for testing. - Before launching a study, you should upgrade to Postgres. -
+ {% if pypi_results.installed == pypi_results.newest %} +
+ You have the latest version of oTree ({{ pypi_results.installed }}). +
{% else %} -
- You are using a proper database (Postgres, MySQL, etc). -
+
+ You have oTree {{ pypi_results.installed }}. + The newest version is {{ pypi_results.newest|default:"(unknown_pypi_connection_error)" }}. +
{% endif %} - {% if debug %} -
- DEBUG mode is on - You should only use DEBUG mode when testing. - Before launching a study, you should switch DEBUG mode off - by setting the environment variable OTREE_PRODUCTION. -
- {% else %} -
- DEBUG mode is off -
- {% endif %} - {% if not auth_level_ok %} -
- No password protection - To prevent unauthorized server access, you should - set the environment variable OTREE_AUTH_LEVEL. -
- {% else %} -
- Password protection is on. - Your app's AUTH_LEVEL is {{ auth_level }}. -
- {% endif %} + {% if sqlite %} +
+ Using SQLite You are using SQLite, which is only suitable for testing. + Before launching a study, you should upgrade to Postgres. +
+ {% else %} +
+ You are using a proper database (Postgres, MySQL, etc). +
+ {% endif %} - {% if not db_synced %} -
- Database is missing tables - You should reset the database (resetdb). -
- {% else %} -
- Your database appears to be synced. -
- {% endif %} + {% if debug %} +
+ DEBUG mode is on + You should only use DEBUG mode when testing. + Before launching a study, you should switch DEBUG mode off + by setting the environment variable OTREE_PRODUCTION. +
+ {% else %} +
+ DEBUG mode is off +
+ {% endif %} + + {% if not auth_level_ok %} +
+ No password protection + To prevent unauthorized server access, you should + set the environment variable OTREE_AUTH_LEVEL. +
+ {% else %} +
+ Password protection is on. + Your app's AUTH_LEVEL is {{ auth_level }}. +
+ {% endif %} + + {% if not db_synced %} +
+ Database is missing tables + You should reset the database (resetdb). +
+ {% else %} +
+ Your database appears to be synced. +
+ {% endif %} {% endblock %} diff --git a/otree/templates/otree/admin/Session.html b/otree/templates/otree/admin/Session.html index f31933cf3..7feabf3fe 100644 --- a/otree/templates/otree/admin/Session.html +++ b/otree/templates/otree/admin/Session.html @@ -1,5 +1,5 @@ {% extends "otree/BaseAdmin.html" %} -{% load otree_internal staticfiles %} +{% load otree_internal static %} {% block head_title %} {{ session.config.display_name }}: session '{{ session.code }}'{% if session.is_demo %} (demo) {% endif %} diff --git a/otree/templates/otree/admin/SessionPayments.html b/otree/templates/otree/admin/SessionPayments.html index b9ab8da48..f50cfe2c2 100644 --- a/otree/templates/otree/admin/SessionPayments.html +++ b/otree/templates/otree/admin/SessionPayments.html @@ -1,28 +1,28 @@ {% extends "otree/admin/Session.html" %} {% block content %} -{{ block.super }} -
+ {{ block.super }} +

- Generated: {% now "DATETIME_FORMAT" %} + Generated: {% now "DATETIME_FORMAT" %}

Session

- - - - + + + + - - - - + + + + - - - - + + + +
Session config{{ session.config.name }}
Session config{{ session.config.name }}
Session code{{ session.code }}
Session code{{ session.code }}
Experimenter name{{ session.experimenter_name|default_if_none:"" }}
Participation fee{{ participation_fee }}
@@ -30,48 +30,46 @@

Participants

- - - - - - - - - + + + + + + + + - {% for p in participants %} + {% for p in participants %} - - - - - - - + + + + + + - {% endfor %} + {% endfor %}
Participant codeParticipant labelProgressParticipation feePayoff (bonus)TotalNote
CodeLabelProgressWait page totalPayoff (bonus)Total
{{ p.code }}{{ p.label|default_if_none:"" }}{{ p.current_page_ }}{{ participation_fee }}{{ p.payoff_in_real_world_currency }}{{ p.payoff_plus_participation_fee }}{{ p.code }}{{ p.label|default_if_none:"" }}{{ p.current_page_ }}{{ p.waiting_seconds }} seconds{{ p.payoff_in_real_world_currency }}{{ p.payoff_plus_participation_fee }}

Summary

- - - - - - - - + + + + + + + +
Total payments{{ total_payments }}
Mean payment{{ mean_payment }}
Total payments{{ total_payments }}
Mean payment{{ mean_payment }}
-

Notes/Signature

+

Notes

-
-
-
+
+
+
-
+
{% endblock %} diff --git a/otree/templates/otree/admin/SessionSplitScreen.html b/otree/templates/otree/admin/SessionSplitScreen.html index 70e783fec..83e5850f2 100644 --- a/otree/templates/otree/admin/SessionSplitScreen.html +++ b/otree/templates/otree/admin/SessionSplitScreen.html @@ -1,4 +1,4 @@ -{% load staticfiles %} +{% load static %} {% load i18n %} diff --git a/otree/templates/otree/admin/SessionStartLinks.html b/otree/templates/otree/admin/SessionStartLinks.html index 5f0cfc8d9..5d764af07 100644 --- a/otree/templates/otree/admin/SessionStartLinks.html +++ b/otree/templates/otree/admin/SessionStartLinks.html @@ -8,8 +8,7 @@ {% endif %} diff --git a/otree/templates/otree/admin/Sessions.html b/otree/templates/otree/admin/Sessions.html index d0d3e9ff1..85f950abd 100644 --- a/otree/templates/otree/admin/Sessions.html +++ b/otree/templates/otree/admin/Sessions.html @@ -1,5 +1,5 @@ {% extends "otree/BaseAdmin.html" %} -{% load staticfiles %} +{% load static %} {% block internal_scripts %} {{ block.super }} diff --git a/otree/templates/otree/includes/CreateSessionForm.html b/otree/templates/otree/includes/CreateSessionForm.html index da96bc40a..dbb94b1bb 100644 --- a/otree/templates/otree/includes/CreateSessionForm.html +++ b/otree/templates/otree/includes/CreateSessionForm.html @@ -1,4 +1,4 @@ -{% load otree staticfiles %} +{% load otree static %}
{% csrf_token %} @@ -33,7 +33,8 @@ type="submit">

- + +
{% for config in configs %} @@ -109,8 +110,9 @@
General
{% endif %} let $createSessionError = $("#create-session-error"); let $createSessionWait = $("#create-session-wait"); let socket = makeReconnectingWebSocket("/create_session/"); - - $('#btn-create-session').click(function (e) { + let $btnCreateSession = $('#btn-create-session'); + $btnCreateSession.click(function (e) { + $btnCreateSession.prop('disabled', true); $createSessionError.hide(); $createSessionWait.show(); @@ -139,9 +141,11 @@
General
{% endif %} $createSessionError.hide(); $createSessionWait.hide(); window.location.href = data.session_url; + return; // so that we don't re-enable create button } else { $createSessionError.text("Unexpected error: " + message.data); } + $btnCreateSession.prop('disabled', false); } }); diff --git a/otree/templates/otree/includes/OtreeDotOrgFeedbackWidget.html b/otree/templates/otree/includes/OtreeDotOrgFeedbackWidget.html deleted file mode 100644 index 2dbeff6a7..000000000 --- a/otree/templates/otree/includes/OtreeDotOrgFeedbackWidget.html +++ /dev/null @@ -1,43 +0,0 @@ - \ No newline at end of file diff --git a/otree/templates/otree/includes/TimeLimit.js.html b/otree/templates/otree/includes/TimeLimit.js.html index 255ec559c..b17b16549 100644 --- a/otree/templates/otree/includes/TimeLimit.js.html +++ b/otree/templates/otree/includes/TimeLimit.js.html @@ -1,4 +1,4 @@ -{% load staticfiles otree %} +{% load static otree %}