Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add optional anchor time to runs [#186531175] #625

Merged
merged 3 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions bundles/admin/scheduleEditor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ function dispatch(dispatch) {
moveSpeedrun: (source, destination, before) => {
dispatch(actions.models.setInternalModelField('speedrun', source, 'moving', true));
dispatch(actions.models.setInternalModelField('speedrun', destination, 'moving', true));
dispatch(actions.models.setInternalModelField('speedrun', source, 'errors', null));
dispatch(
actions.models.command({
type: 'MoveSpeedRun',
Expand All @@ -116,6 +117,9 @@ function dispatch(dispatch) {
other: destination,
before: before ? 1 : 0,
},
fail: json => {
dispatch(actions.models.setInternalModelField('speedrun', source, 'errors', json.error));
},
always: () => {
dispatch(actions.models.setInternalModelField('speedrun', source, 'moving', false));
dispatch(actions.models.setInternalModelField('speedrun', destination, 'moving', false));
Expand Down
57 changes: 35 additions & 22 deletions bundles/admin/scheduleEditor/speedrun.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class Speedrun extends React.Component {

line() {
const { speedrun, draft, connectDragPreview, editModel } = this.props;
const fieldErrors = draft ? draft._fields || {} : {};
const fieldErrors = draft?._fields || {};
const { cancelEdit_, editModel_, updateField_, save_ } = this;
return draft ? (
<React.Fragment>
Expand Down Expand Up @@ -103,28 +103,40 @@ class Speedrun extends React.Component {
speedrun && speedrun.order !== null && speedrun.starttime !== null
? moment(speedrun.starttime).format('dddd, MMMM Do, h:mm a')
: 'Unscheduled';
const spinning = !!(speedrun._internal && (speedrun._internal.moving || speedrun._internal.saving));
const spinning = !!(speedrun._internal?.moving || speedrun._internal?.saving);
const errors = speedrun._internal?.errors;
return (
<tr style={{ opacity: isDragging ? 0.5 : 1 }}>
<td className="small">{starttime}</td>
<td style={{ textAlign: 'center' }}>
{moveSpeedrun ? (
<OrderTarget
spinning={spinning}
connectDragSource={connectDragSource}
nullOrder={saveField && nullOrder_}
target={!!speedrun.order}
targetType={SpeedrunDropTarget}
targetProps={{
pk: speedrun.pk,
legalMove: legalMove_,
moveSpeedrun: moveSpeedrun,
}}
/>
) : null}
</td>
{this.line()}
</tr>
<>
{errors && Object.entries(errors).map(([key, errors]) => <ErrorList key={key} errors={errors} />)}
<tr style={{ opacity: isDragging ? 0.5 : 1 }}>
<td className="small">
{starttime}
{speedrun.anchor_time ? (
<>
<br />
Anchored
</>
) : null}
</td>
<td style={{ textAlign: 'center' }}>
{moveSpeedrun ? (
<OrderTarget
spinning={spinning}
connectDragSource={connectDragSource}
nullOrder={saveField && nullOrder_}
target={!!speedrun.order}
targetType={SpeedrunDropTarget}
targetProps={{
pk: speedrun.pk,
legalMove: legalMove_,
moveSpeedrun: moveSpeedrun,
}}
/>
) : null}
</td>
{this.line()}
</tr>
</>
);
}

Expand Down Expand Up @@ -173,6 +185,7 @@ const SpeedrunShape = PropTypes.shape({
//console: PropTypes.string.isRequired,
start_time: PropTypes.string,
end_time: PropTypes.string,
anchor_time: PropTypes.string,
description: PropTypes.string.isRequired,
commentators: PropTypes.string.isRequired,
});
Expand Down
15 changes: 10 additions & 5 deletions bundles/public/api/actions/models.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ function onNewDraftModel(model) {

function newDraftModel(model) {
return dispatch => {
dispatch(onSetInternalModelField(model.type, model.pk, 'errors', null));
dispatch(onNewDraftModel(model));
};
}
Expand Down Expand Up @@ -192,9 +193,13 @@ function saveDraftModels(models) {
dispatch(onModelCollectionAdd(model.type, models));
dispatch(onDeleteDraftModel(model));
})
.catch(response => {
const json = response.json();
dispatch(onSaveDraftModelError(model, json ? json.error : response.body(), json ? json.fields : {}));
.catch(async response => {
try {
const json = await response.json();
dispatch(onSaveDraftModelError(model, json.error, json.message_dict || { __all__: json.messages }));
} catch (e) {
dispatch(onSaveDraftModelError(model, await response.body()));
}
})
.finally(() => {
dispatch(setInternalModelField(model.type, model.pk, 'saving', false));
Expand Down Expand Up @@ -285,9 +290,9 @@ function command(command) {
command.done();
}
})
.catch(() => {
.catch(e => {
if (typeof command.fail === 'function') {
command.fail();
e.json().then(json => command.fail(json));
}
})
.finally(() => {
Expand Down
1 change: 1 addition & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ def setUp(self):
def format_run(cls, run):
return dict(
fields=dict(
anchor_time=run.anchor_time,
canonical_url=(
'http://testserver' + reverse('tracker:run', args=(run.id,))
),
Expand Down
93 changes: 89 additions & 4 deletions tests/test_speedrun.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import pytz
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.test import TransactionTestCase
from django.urls import reverse

Expand Down Expand Up @@ -98,6 +99,49 @@ def test_update_runners_on_m2m(self):
self.run1.deprecated_runners, ', '.join(sorted([self.runner2.name]))
)

def test_anchor_time(self):
self.run3.anchor_time = self.run3.starttime
self.run3.save()
self.run1.clean()
with self.subTest('run time drift'), self.assertRaises(ValidationError):
self.run1.run_time = '1:00:00'
self.run1.clean()
self.run1.refresh_from_db()
with self.subTest('setup time drift'), self.assertRaises(ValidationError):
self.run1.run_time = '45:00'
self.run1.setup_time = '20:00'
self.run1.clean()
self.run1.refresh_from_db()
with self.subTest('bad anchor order'), self.assertRaises(ValidationError):
self.run2.anchor_time = self.run3.anchor_time + datetime.timedelta(
minutes=5
)
self.run2.clean()
self.run2.refresh_from_db()
with self.subTest('setup time correction'):
self.run2.setup_time = '2:00'
self.run2.save()
self.run2.refresh_from_db()
self.assertEqual(self.run2.setup_time, '0:05:00')
self.run3.refresh_from_db()
self.assertEqual(self.run3.starttime, self.run3.anchor_time)
self.run3.anchor_time += datetime.timedelta(minutes=5)
self.run3.save()
self.run2.refresh_from_db()
self.assertEqual(self.run2.setup_time, '0:10:00')
self.run1.setup_time = '10:00'
self.run1.save()
self.run2.refresh_from_db()
self.assertEqual(self.run2.setup_time, '0:05:00')
self.run2.run_time = '17:00'
self.run2.clean()
self.run2.save()
self.run2.refresh_from_db()
self.assertEqual(self.run2.setup_time, '0:03:00')
with self.subTest('bad anchor time'), self.assertRaises(ValidationError):
self.run3.anchor_time -= datetime.timedelta(days=1)
self.run3.clean()


class TestMoveSpeedRun(TransactionTestCase):
def setUp(self):
Expand Down Expand Up @@ -205,6 +249,18 @@ def test_unordered_to_after(self):
self.assertEqual(self.run3.order, 4)
self.assertEqual(self.run4.order, 3)

def test_too_long_for_anchor(self):
from tracker.views.commands import MoveSpeedRun

self.run2.anchor_time = self.run2.starttime
self.run2.save()

output, status = MoveSpeedRun(
{'moving': self.run3.id, 'other': self.run2.id, 'before': True}
)

self.assertEqual(status, 400)


class TestSpeedRunAdmin(TransactionTestCase):
def setUp(self):
Expand All @@ -219,6 +275,13 @@ def setUp(self):
self.run2 = models.SpeedRun.objects.create(
name='Test Run 2', run_time='0:15:00', setup_time='0:05:00', order=2
)
self.run3 = models.SpeedRun.objects.create(
name='Test Run 3',
run_time='0:35:00',
setup_time='0:05:00',
anchor_time=today_noon + datetime.timedelta(minutes=90),
order=3,
)
if not User.objects.filter(username='admin').exists():
User.objects.create_superuser('admin', 'nobody@example.com', 'password')

Expand All @@ -242,6 +305,7 @@ def test_start_run(self):
data={
'run_time': '0:41:20',
'start_time': '%s 12:51:00' % self.event1.date,
'run_id': self.run2.id,
},
)
self.assertEqual(resp.status_code, 302)
Expand All @@ -250,15 +314,36 @@ def test_start_run(self):
self.assertEqual(self.run1.setup_time, '0:09:40')

def test_invalid_time(self):
self.client.login(username='admin', password='password')
resp = self.client.post(
reverse('admin:start_run', args=(self.run2.id,)),
from tracker.admin.forms import StartRunForm

form = StartRunForm(
initial={
'run_id': self.run2.id,
},
data={
'run_time': '0:41:20',
'start_time': '%s 11:21:00' % self.event1.date,
'run_id': self.run2.id,
},
)
self.assertFalse(form.is_valid())
self.assertFormError(form, None, StartRunForm.Errors.invalid_start_time)

def test_anchor_drift(self):
from tracker.admin.forms import StartRunForm

form = StartRunForm(
initial={
'run_id': self.run2.id,
},
data={
'run_time': '0:41:20',
'start_time': '%s 13:21:00' % self.event1.date,
'run_id': self.run2.id,
},
)
self.assertEqual(resp.status_code, 400)
self.assertFalse(form.is_valid())
self.assertFormError(form, None, StartRunForm.Errors.anchor_time_drift)


class TestSpeedrunList(TransactionTestCase):
Expand Down
46 changes: 26 additions & 20 deletions tracker/admin/event.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import csv
import time
from datetime import timedelta
from decimal import Decimal
from io import BytesIO, StringIO

Expand Down Expand Up @@ -743,6 +742,7 @@ class SpeedRunAdmin(EventLockedMixin, CustomModelAdmin):
'event',
'order',
'starttime',
'anchor_time',
'run_time',
'setup_time',
'runners',
Expand Down Expand Up @@ -789,38 +789,44 @@ def bids(self, instance):
def start_run(self, request, runs):
if len(runs) != 1:
self.message_user(request, 'Pick exactly one run.', level=messages.ERROR)
elif runs[0].event.locked:
self.message_user(request, 'Run event is locked.', level=messages.ERROR)
elif not runs[0].order:
self.message_user(request, 'Run has no order.', level=messages.ERROR)
elif runs[0].order == 1:
self.message_user(request, 'Run is first run.', level=messages.ERROR)
else:
return HttpResponseRedirect(reverse('admin:start_run', args=(runs[0].id,)))
prev = models.SpeedRun.objects.filter(
event=runs[0].event_id, order__lt=runs[0].order
).last()

if not prev:
self.message_user(request, 'Run is first run.', level=messages.ERROR)
else:
return HttpResponseRedirect(
reverse('admin:start_run', args=(runs[0].id,))
)

@staticmethod
@permission_required('tracker.change_speedrun')
def start_run_view(request, run):
run = models.SpeedRun.objects.get(id=run)
run = models.SpeedRun.objects.filter(id=run, event__locked=False).first()
if not run:
raise Http404
prev = models.SpeedRun.objects.filter(
event=run.event, order__lt=run.order
).last()
if not prev:
raise Http404
form = StartRunForm(
data=request.POST if request.method == 'POST' else None,
initial={'run_time': prev.run_time, 'start_time': run.starttime},
initial={
'run_time': prev.run_time,
'start_time': run.starttime,
'run_id': run.id,
},
)
if form.is_valid():
rt = tracker.models.fields.TimestampField.time_string_to_int(
form.cleaned_data['run_time']
)
endtime = prev.starttime + timedelta(milliseconds=rt)
if form.cleaned_data['start_time'] < endtime:
return HttpResponse(
'Entered data would cause previous run to end after current run started',
status=400,
content_type='text/plain',
)
prev.run_time = form.cleaned_data['run_time']
prev.setup_time = str(form.cleaned_data['start_time'] - endtime)
prev.save()
form.save()
prev.refresh_from_db()
messages.info(request, 'Previous run time set to %s' % prev.run_time)
messages.info(request, 'Previous setup time set to %s' % prev.setup_time)
run.refresh_from_db()
Expand All @@ -834,7 +840,7 @@ def start_run_view(request, run):
'admin/tracker/generic_form.html',
{
'site_header': 'Donation Tracker',
'title': 'Set start time for %s' % run,
'title': 'Set start time for %s' % run.name,
'breadcrumbs': (
(
reverse('admin:app_list', kwargs=dict(app_label='tracker')),
Expand Down
Loading
Loading