import vanilla import csv import os import json from datetime import datetime from django.db.models import ExpressionWrapper, F, DurationField from django.views.generic import ListView, TemplateView, View from django.http import HttpResponse from django.core.urlresolvers import reverse from otree.models import Session, Participant from otree.views.export import get_export_response from .models import Exit, FocusEvent, allowed_export_tracker_requests from .export import export_wide from . import __version__ as otree_tools_version, cp # TIME STAMPS VIEWS FOR TRACKING_TIME and TRACKING_FOCUS class EnterExitMixin: def get_queryset(self): return Exit.export.time() class PaginatedListView(ListView): navbar_active_tag = None export_link_name = None export_activated = None def get_context_data(self, **kwargs): c = super().get_context_data(**kwargs) c['nbar'] = self.navbar_active_tag curpage_num = c['page_obj'].number paginator = c['paginator'] epsilon = 3 c['allowed_range'] = range(max(1, curpage_num - epsilon), min(curpage_num + epsilon, paginator.num_pages) + 1) if self.export_link_name: c['export_link'] = reverse(self.export_link_name) c['export_activated'] = self.export_activated or False return c class EnterExitEventList(EnterExitMixin, PaginatedListView): template_name = 'otree_tools/trackers_export/all_timespent_list.html' url_name = 'time_spent_timestamps' url_pattern = r'^time_spent_per_page/$' display_name = 'Time spent per page [otree-tools]' context_object_name = 'timestamps' paginate_by = 50 navbar_active_tag = 'time' export_activated = True def get_context_data(self, **kwargs): c = super().get_context_data(**kwargs) cp(c) objs = c['object_list'] # The following is needed because of the weird error in django: the way it deals with # durationfield sometimes results in None when two date fields are subtracted. # If someone reads this in the future, please send me some hugs, because before I figured # this out, it completely ate my brain. for o in objs: if o['diff'] is None: o['diff'] = o['timestamp'] - o['enter_timestamp'] return c class TempFileCSVExport(View): """Receives a temp file name prepared in consumers, and returns it to a user. That is mostly for dealing with large files that can provoke server timeout errors.""" url_name = 'export_tracker_data' url_pattern = fr'^export_tracker_data/(?P{allowed_export_tracker_requests})/(?P.*)/$' content_type = 'text/csv' def get(self, request, *args, **kwargs): file_path = kwargs.get('temp_file_name') tracker_type = kwargs.get('tracker_type') curtime = datetime.now().strftime("%d_%m_%Y__%H_%M_%S") filename = f'{tracker_type}_{curtime}.csv' fsock = open(file_path, "r") response = HttpResponse(fsock, content_type=self.content_type) response['Content-Disposition'] = f'attachment; filename="{filename}"' fsock.close() try: os.remove(file_path) except FileNotFoundError: pass # TODO logger.warning('temp file not found') return response ########### BLOCK: FOCUS EVENTS EXPORT BLOCK ############################################################## class FocusPerPageReport(PaginatedListView): template_name = 'otree_tools/trackers_export/focus_per_page_report.html' url_name = 'focus_per_page_report' url_pattern = r'^focus_per_page_report/$' display_name = 'Focused/unfocused [otree-tools]' context_object_name = 'focusevents' paginate_by = 50 navbar_active_tag = 'focus' export_activated = True def get_queryset(self): return FocusEvent.objects.get_per_page_report() class FocusEventList(PaginatedListView): template_name = 'otree_tools/trackers_export/focus_event_list.html' url_name = 'focus_events' url_pattern = r'^focus_events/$' display_name = 'Focus/unfocus events [otree-tools]' context_object_name = 'focusevents' queryset = FocusEvent.objects.all() paginate_by = 50 navbar_active_tag = 'focus' export_activated = True ############ END OF: FOCUS EVENTS EXPORT BLOCK ############################################################# # END OF TIME STAMPS BLOCK # Welcoming page class HomeView(TemplateView): template_name = 'otree_tools/home.html' url_name = 'otree_tools_home' url_pattern = r'^otree_tools/$' display_name = 'otree-tools Home page' navbar_active_tag = 'home' def get_context_data(self, **kwargs): c = super().get_context_data(**kwargs) c['nbar'] = self.navbar_active_tag c['version'] = otree_tools_version return c ########### BLOCK: EXPORTING PARTICIPANT.VARS MODULE ############################################################## class UniversalEncoder(json.JSONEncoder): """A 'universal encoder for exporting participant.vars.""" def default(self, obj): if isinstance(obj, (set, range)): return list(obj) try: return json.JSONEncoder.default(self, obj) except TypeError: try: return str(obj) # the last resort - if built-in __str__ of the object causes an error we fail silently except: return '' class PVarsMixin(object): """Mixin to use for exporting participant.vars both for text and html.""" def get_session(self): return Session.objects.get(pk=self.kwargs.get('pk')) def get_queryset(self): headers_to_add = ['session.code', 'player.code'] session = self.get_session() data = Participant.objects.filter(session=session).values('vars', 'code', 'session__code') # todo: sort the headers keys = sorted(list(set().union(*(d['vars'].keys() for d in data))), key=lambda s: s.lower()) self.heads = headers_to_add + keys q = list() for i in data: dict_to_row = json.loads(json.dumps(i['vars'], cls=UniversalEncoder)) row = [i['session__code'], i['code']] + [dict_to_row.get(key, '') for key in keys] q.append(row) return q class ListPVarsView(PVarsMixin, PaginatedListView): """Returns all participant vars in the database - per session.""" template_name = 'otree_tools/pvars_list.html' url_name = 'pvars_list' url_pattern = r'^session/(?P[a-zA-Z0-9_-]+)/pvars/$' model = Participant context_object_name = 'pvars' paginate_by = 20 navbar_active_tag = 'export' export_link_name = None def get_context_data(self, **kwargs): c = super().get_context_data(**kwargs) session = self.get_session() c['session'] = session c['heads'] = self.heads c['export_link'] = reverse('pvars_csv_export', kwargs={'pk': session.pk}) return c class PVarsCSVExport(PVarsMixin, TemplateView): """Returns the textual file all participant vars in the database - per session.""" url_name = 'pvars_csv_export' url_pattern = r'^session/(?P[a-zA-Z0-9_-]+)/pvars_csv_export/$' response_class = HttpResponse content_type = 'text/csv' def get(self, request, *args, **kwargs): response = HttpResponse(content_type='text/csv') session_code = self.get_session().code filename = f'{session_code}_participant_vars.csv' response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename) pvars = self.get_queryset() writer = csv.writer(response) writer.writerow(self.heads) for p in pvars: writer.writerow(p) return response ############ END OF: EXPORTING PARTICIPANT.VARS MODULE ############################################################# ########### BLOCK: SPECIFIC SESSION EXPORT ############################################################## class AllSessionsList(PaginatedListView): template_name = 'otree_tools/all_session_list.html' url_name = 'individual_sessions_export' url_pattern = r'^individual_sessions_export/$' display_name = 'Exporting data from individual sessions' navbar_active_tag = 'export' queryset = Session.objects.all() context_object_name = 'sessions' paginate_by = 10 class SpecificSessionDataView(vanilla.TemplateView): url_pattern = r'^session_data/(?P.*)/(?P.*)/$' url_name = 'session_data' def get(self, request, *args, **kwargs): session_code = kwargs['session_code'] response, file_extension = get_export_response( request, session_code) export_wide(response, file_extension, session_code=session_code) return response ############ END OF: SPECIFIC SESSION EXPORT #############################################################