# like common, but can import models import importlib.util import os import time from dataclasses import dataclass, asdict from pathlib import Path from starlette.staticfiles import StaticFiles from otree import settings from otree.database import db from otree.models_concrete import PageTimeBatch @dataclass class TimeSpentRow: session_code: str participant_id_in_session: int participant_code: str page_index: int app_name: str page_name: str epoch_time_completed: int round_number: int timeout_happened: int is_wait_page: int page_completion_buffer = [] page_completion_last_write = 0 BUFFER_SIZE = 50 def write_row_to_page_buffer(row: TimeSpentRow): d = asdict(row) row = ','.join(map(str, d.values())) + '\n' page_completion_buffer.append(row) if ( len(page_completion_buffer) > BUFFER_SIZE or time.time() - page_completion_last_write > 60 * 2 ): write_page_completion_buffer() def make_page_completion_row( *, view, app_name, participant__id_in_session, participant__code, session_code, is_wait_page, ): now = int(time.time()) row = TimeSpentRow( app_name=app_name, page_index=view._index_in_pages, page_name=type(view).__name__, epoch_time_completed=now, round_number=view.round_number, participant_id_in_session=participant__id_in_session, participant_code=participant__code, session_code=session_code, timeout_happened=int(bool(getattr(view, 'timeout_happened', False))), is_wait_page=is_wait_page, ) write_row_to_page_buffer(row) def write_page_completion_buffer(): global page_completion_last_write db.add(PageTimeBatch(text=''.join(page_completion_buffer))) page_completion_last_write = time.time() page_completion_buffer.clear() class OTreeStaticFiles(StaticFiles): # copied from starlette, just to change 'statics' to 'static', # and to fail silently if the dir does not exist. def get_directories(self, directory, packages): directories = [] if directory is not None: directories.append(directory) for package in packages or []: spec = importlib.util.find_spec(package) assert ( spec is not None and spec.origin is not None ), f"Package {package!r} could not be found, or maybe __init__.py is missing" package_directory = os.path.normpath( os.path.join(spec.origin, "..", "static") ) if os.path.isdir(package_directory): directories.append(package_directory) return directories def assert_file_exists(self, path): if path in existing_filenames_cache: return for _dir in self.all_directories: if Path(_dir, path).is_file(): existing_filenames_cache.add(path) return raise FileNotFoundError(path) existing_filenames_cache = set() def url_of_static(path): """ naming: - it shouldn't start with 'static' because that would distract from @staticmethod in autocomplete, which is much more important. - url_of_static is more specific than url_for_static (which looks like vars_for_template but works differently) better than hardcoding '/static/', which will fail silently if the file doesn't exist. this would be useful for 2 situations: - live pages, where {% static %} can't be used because the template was already rendered - for use with js_vars (don't want {% static %} mixed in with JS code) """ from otree.asgi import app static_files_app.assert_file_exists(path) return app.router.url_path_for('static', path=path) static_files_app = OTreeStaticFiles( directory='_static', packages=['otree'] + settings.OTREE_APPS )