-
Notifications
You must be signed in to change notification settings - Fork 5
/
session.py
287 lines (235 loc) · 10.1 KB
/
session.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
import logging
import time
from django.template import TemplateDoesNotExist
from django.template.loader import select_template
import otree.common
import otree.constants
from otree.channels.utils import auto_advance_group
from otree.common import (
random_chars_8,
random_chars_10,
get_admin_secret_code,
get_app_label_from_name,
FieldTrackerWithVarsSupport,
)
from otree.db import models
from otree.models_concrete import RoomToSession
logger = logging.getLogger('otree')
ADMIN_SECRET_CODE = get_admin_secret_code()
class Session(models.Model):
class Meta:
app_label = "otree"
# if i don't set this, it could be in an unpredictable order
ordering = ['pk']
_ft = FieldTrackerWithVarsSupport()
vars: dict = models._PickleField(default=dict)
config: dict = models._PickleField(default=dict, null=True)
# label of this session instance
label = models.CharField(
max_length=300, null=True, blank=True, help_text='For internal record-keeping'
)
experimenter_name = models.CharField(
max_length=300, null=True, blank=True, help_text='For internal record-keeping'
)
code = models.CharField(
default=random_chars_8,
max_length=16,
# set non-nullable, until we make our CharField non-nullable
null=False,
unique=True,
doc="Randomly generated unique identifier for the session.",
)
mturk_HITId = models.CharField(
max_length=300,
null=True,
blank=True,
help_text='Hit id for this session on MTurk',
)
mturk_HITGroupId = models.CharField(
max_length=300,
null=True,
blank=True,
help_text='Hit id for this session on MTurk',
)
# since workers can drop out number of participants on server should be
# greater than number of participants on mturk
# value -1 indicates that this session it not intended to run on mturk
mturk_num_participants = models.IntegerField(
default=-1, help_text="Number of participants on MTurk"
)
mturk_use_sandbox = models.BooleanField(
default=True, help_text="Should this session be created in mturk sandbox?"
)
# use Float instead of DateTime because DateTime
# is a pain to work with (e.g. naive vs aware datetime objects)
# and there is no need here for DateTime
mturk_expiration = models.FloatField(null=True)
archived = models.BooleanField(
default=False,
db_index=True,
doc=(
"If set to True the session won't be visible on the "
"main ViewList for sessions"
),
)
comment = models.TextField(blank=True)
_anonymous_code = models.CharField(
default=random_chars_10, max_length=10, null=False, db_index=True
)
is_demo = models.BooleanField(default=False)
_admin_report_app_names = models.TextField(default='')
_admin_report_num_rounds = models.CharField(default='', max_length=255)
num_participants = models.PositiveIntegerField()
def __unicode__(self):
return self.code
@property
def participation_fee(self):
'''This method is deprecated from public API,
but still useful internally (like data export)'''
return self.config['participation_fee']
@property
def real_world_currency_per_point(self):
'''This method is deprecated from public API,
but still useful internally (like data export)'''
return self.config['real_world_currency_per_point']
@property
def use_browser_bots(self):
return self.config.get('use_browser_bots', False)
def is_mturk(self):
return (not self.is_demo) and (self.mturk_num_participants > 0)
def get_subsessions(self):
lst = []
app_sequence = self.config['app_sequence']
for app in app_sequence:
models_module = otree.common.get_models_module(app)
subsessions = models_module.Subsession.objects.filter(
session=self
).order_by('round_number')
lst.extend(list(subsessions))
return lst
def get_participants(self):
return list(self.participant_set.order_by('id_in_session'))
def mturk_worker_url(self):
# different HITs
# get the same preview page, because they are lumped into the same
# "hit group". This is not documented, but it seems HITs are lumped
# if a certain subset of properties are the same:
# https://forums.aws.amazon.com/message.jspa?messageID=597622#597622
# this seems like the correct design; the only case where this will
# not work is if the HIT was deleted from the server, but in that case,
# the HIT itself should be canceled.
# 2018-06-04:
# the format seems to have changed to this:
# https://worker.mturk.com/projects/{group_id}/tasks?ref=w_pl_prvw
# but the old format still works.
# it seems I can't replace groupId by hitID, which i would like to do
# because it's more precise.
subdomain = "workersandbox" if self.mturk_use_sandbox else 'www'
return "https://{}.mturk.com/mturk/preview?groupId={}".format(
subdomain, self.mturk_HITGroupId
)
def mturk_is_expired(self):
# self.mturk_expiration is offset-aware, so therefore we must compare
# it against an offset-aware value.
return self.mturk_expiration and self.mturk_expiration < time.time()
def mturk_is_active(self):
return self.mturk_HITId and not self.mturk_is_expired()
def advance_last_place_participants(self):
# django.test takes 0.5 sec to import,
# if this is done globally then it adds to each startup
# it's only used here, and often isn't used at all.
# so best to do it only here
# it gets cached
import django.test
client = django.test.Client()
participants = self.get_participants()
# in case some participants haven't started
unvisited_participants = []
for p in participants:
if p._index_in_pages == 0:
unvisited_participants.append(p)
client.get(p._start_url(), follow=True)
if unvisited_participants:
# that's it -- just visit the start URL, advancing by 1
return
last_place_page_index = min([p._index_in_pages for p in participants])
last_place_participants = [
p for p in participants if p._index_in_pages == last_place_page_index
]
for p in last_place_participants:
try:
current_form_page_url = p._current_form_page_url
if current_form_page_url:
resp = client.post(
current_form_page_url,
data={
otree.constants.timeout_happened: True,
otree.constants.admin_secret_code: ADMIN_SECRET_CODE,
},
follow=True,
)
# not sure why, but many users are getting HttpResponseNotFound
if resp.status_code >= 400:
msg = (
'Submitting page {} failed, '
'returned HTTP status code {}.'.format(
current_form_page_url, resp.status_code
)
)
content = resp.content
if len(content) < 600:
msg += ' response content: {}'.format(content)
raise AssertionError(msg)
else:
# it's possible that the slowest user is on a wait page,
# especially if their browser is closed.
# because they were waiting for another user who then
# advanced past the wait page, but they were never
# advanced themselves.
start_url = p._start_url()
resp = client.get(start_url, follow=True)
except:
logging.exception("Failed to advance participants.")
raise
# do the auto-advancing here,
# rather than in increment_index_in_pages,
# because it's only needed here.
otree.channels.utils.sync_group_send_wrapper(
type='auto_advanced', group=auto_advance_group(p.code), event={}
)
def get_room(self):
from otree.room import ROOM_DICT
try:
room_name = RoomToSession.objects.get(session=self).room_name
return ROOM_DICT[room_name]
except RoomToSession.DoesNotExist:
return None
def _get_payoff_plus_participation_fee(self, payoff):
'''For a participant who has the given payoff,
return their payoff_plus_participation_fee
Useful to define it here, for data export
'''
return self.config['participation_fee'] + payoff.to_real_world_currency(self)
def _set_admin_report_app_names(self):
admin_report_app_names = []
num_rounds_list = []
for app_name in self.config['app_sequence']:
models_module = otree.common.get_models_module(app_name)
app_label = get_app_label_from_name(app_name)
try:
select_template(
[f'{app_label}/admin_report.html', f'{app_label}/AdminReport.html']
)
except TemplateDoesNotExist:
pass
else:
admin_report_app_names.append(app_name)
num_rounds_list.append(models_module.Constants.num_rounds)
self._admin_report_app_names = ';'.join(admin_report_app_names)
self._admin_report_num_rounds = ';'.join(str(n) for n in num_rounds_list)
def _admin_report_apps(self):
return self._admin_report_app_names.split(';')
def _admin_report_num_rounds_list(self):
return [int(num) for num in self._admin_report_num_rounds.split(';')]
def has_admin_report(self):
return bool(self._admin_report_app_names)