from io import StringIO import datetime from starlette.responses import Response from . import cbv from starlette.endpoints import HTTPEndpoint import otree.common import otree.models from otree import export from otree.export import BOM, get_installed_apps_with_data from otree.models import Participant from otree.models_concrete import ChatMessage from otree.database import dbq from . import cbv def get_csv_http_response(buffer: StringIO, filename_prefix) -> Response: buffer.seek(0) response = Response(buffer.read()) date = datetime.date.today().isoformat() response.headers['Content-Type'] = 'text/csv' response.headers['Content-Disposition'] = ( f'attachment; filename="{filename_prefix}-{date}.csv"' ) return response class ExportMixin: """Shared logic for all export views""" def file_name_stem(self): raise NotImplementedError def write_to_buf(self, buf, session_code, query_params): raise NotImplementedError def process_export(self, request): query_params = request.query_params session_code = query_params.get('session_code') buf = StringIO() if query_params.get('format') == 'csv_bom': buf.write(BOM) self.write_to_buf(buf, session_code, query_params) return get_csv_http_response(buf, self.file_name_stem()) class BaseExport(ExportMixin, HTTPEndpoint): """REST API export with REST key authentication""" def get(self, request): error_response = cbv.validate_rest_key(request) if error_response: return error_response return self.process_export(request) class LoginProtectedExport(ExportMixin, cbv.AdminView): """Login-protected export for non-API endpoints""" def get(self, request, **kwargs): return self.process_export(request) class ExportWide(BaseExport): '''used by data page''' url_pattern = '/api/export_wide' def file_name_stem(self): return 'all_apps_wide' def write_to_buf(self, buf, session_code, query_params): export.export_wide(buf, session_code=session_code) class ExportPageTimes(BaseExport): url_pattern = '/api/export_page_times' def file_name_stem(self): return 'PageTimes' def write_to_buf(self, buf, session_code, query_params): export.export_page_times(buf) class ExportChat(BaseExport): url_pattern = '/api/export_chat' def file_name_stem(self): return 'ChatMessages' def write_to_buf(self, buf, session_code, query_params): export.export_chat(buf, session_code=session_code) class ExportApp(BaseExport): url_pattern = '/api/export_app' def file_name_stem(self): return self.app_name def write_to_buf(self, buf, session_code, query_params): app_name = query_params.get('app') self.app_name = app_name if not app_name: raise ValueError("app parameter is required") export.export_app(app_name, buf, session_code=session_code) class ExportAppCustom(BaseExport): url_pattern = '/api/export_app_custom' app_name: str function_name: str def file_name_stem(self): return f'{self.app_name}_{self.function_name}' def write_to_buf(self, buf, session_code, query_params): app_name = query_params.get('app') function_name = query_params.get('function_name', 'custom_export') self.app_name = app_name self.function_name = function_name if not app_name: raise ValueError("app parameter is required") export.custom_export_app( app_name, buf, session_code=session_code, function_name=function_name ) class ExportPageTimesLogin(LoginProtectedExport): """Login-protected version of page times export""" url_pattern = '/ExportPageTimes' def file_name_stem(self): return 'PageTimes' def write_to_buf(self, buf, session_code, query_params): export.export_page_times(buf) class ExportChatLogin(LoginProtectedExport): """Login-protected version of chat export""" url_pattern = '/ExportChat' def file_name_stem(self): return 'ChatMessages' def write_to_buf(self, buf, session_code, query_params): export.export_chat(buf, session_code=session_code) class ExportWideLogin(LoginProtectedExport): '''used by 'SessionData' page''' url_pattern = '/ExportWide' def file_name_stem(self): return 'all_apps_wide' def write_to_buf(self, buf, session_code, query_params): export.export_wide(buf, session_code=session_code) class ExportIndex(cbv.AdminView): url_pattern = '/ExportIndex' def vars_for_template(self): from otree.asgi import reverse # can't use settings.OTREE_APPS, because maybe the app # was removed from SESSION_CONFIGS. app_names_with_data = get_installed_apps_with_data() # Find all custom export functions custom_export_list = [] for app_name in app_names_with_data: custom_exports = export.get_custom_export_functions(app_name) for export_function_name in sorted(custom_exports.keys()): custom_export_list.append((app_name, export_function_name)) # Base URLs wide_url = reverse('ExportWide') export_url = reverse('ExportApp') custom_export_url = reverse('ExportAppCustom') page_times_url = reverse('ExportPageTimes') chat_url = reverse('ExportChat') # Wide export table wide_table = [ dict( name='All apps (wide format)', csv_url=wide_url, excel_url=wide_url + '?format=csv_bom', example_session_url=wide_url + '?session_code={SESSION_CODE}', ) ] # Per-app export table app_table = [ dict( name=app_name, csv_url=export_url + '?app=' + app_name, excel_url=export_url + '?format=csv_bom&app=' + app_name, example_session_url=export_url + '?app=' + app_name + '&session_code={SESSION_CODE}', app_name=app_name, ) for app_name in app_names_with_data ] # Custom export table custom_export_table = [] for app_name, export_function_name in custom_export_list: custom_export_table.append( dict( name=f'{app_name} ({export_function_name})', csv_url=custom_export_url + f'?app={app_name}&function_name={export_function_name}', excel_url=custom_export_url + f'?format=csv_bom&app={app_name}&function_name={export_function_name}', example_session_url=custom_export_url + f'?app={app_name}&function_name={export_function_name}' + '&session_code={SESSION_CODE}', app_name=app_name, function_name=export_function_name, ) ) # Other exports (using login-protected endpoints) page_times_login_url = reverse('ExportPageTimesLogin') chat_login_url = reverse('ExportChatLogin') other_exports = [ dict( name='Page times', csv_url=page_times_login_url, excel_url=page_times_login_url + '?format=csv_bom', api_csv_url=page_times_url, api_excel_url=page_times_url + '?format=csv_bom', example_session_url=None, ), ] chat_messages_exist = bool(dbq(ChatMessage).first()) if chat_messages_exist: other_exports.append( dict( name='Chat logs', csv_url=chat_login_url, excel_url=chat_login_url + '?format=csv_bom', api_csv_url=chat_url, api_excel_url=chat_url + '?format=csv_bom', example_session_url=chat_url + '?session_code={SESSION_CODE}', ) ) return dict( db_is_empty=not bool(dbq(Participant).first()), wide_table=wide_table, app_table=app_table, custom_export_table=custom_export_table, other_exports=other_exports, )