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 all 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
14 changes: 11 additions & 3 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import urllib.parse
from unittest import skipIf

import django
import post_office.models
from django.contrib.auth import get_user_model
from django.test import RequestFactory, TestCase, override_settings
Expand Down Expand Up @@ -70,6 +72,11 @@ def test_register_inactive_user(self):
)
self.assertContains(resp, 'An e-mail has been sent to your address.')

# TODO: remove skip when 3.2 no longer supported
@skipIf(
django.VERSION < (4, 1),
'assertFormError requires response object until Django 4.1',
)
def test_register_active_user(self):
AuthUser.objects.create(
username='existinguser', email='test@email.com', is_active=True
Expand All @@ -78,8 +85,9 @@ def test_register_active_user(self):
reverse('tracker:register'), data={'email': 'test@email.com'}
)
self.assertFormError(
resp,
'form',
resp.context['form'],
'email',
'This email is already registered. Please log in, (or reset your password if you forgot it).',
[
'This email is already registered. Please log in, (or reset your password if you forgot it).'
],
)
9 changes: 8 additions & 1 deletion tests/test_prize.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import datetime
import random
from decimal import Decimal
from unittest import skipIf
from unittest.mock import patch

import django
import post_office.models
import pytz
from dateutil.parser import parse as parse_date
Expand Down Expand Up @@ -1467,6 +1469,11 @@ def test_prize_key_import_action(self):
reverse('admin:tracker_prize_key_import', args=(self.prize_with_keys.id,)),
)

# TODO: remove skip when 3.2 no longer supported
@skipIf(
django.VERSION < (4, 1),
'assertFormError requires response object until Django 4.1',
)
def test_prize_key_import_form(self):
keys = ['dead-beef-dead-beef-123%d' % i for i in range(5)]
response = self.client.get(
Expand Down Expand Up @@ -1521,7 +1528,7 @@ def test_prize_key_import_form(self):
{'keys': keys[0]},
)
self.assertFormError(
response, 'form', 'keys', ['At least one key already exists.']
response.context['form'], 'keys', ['At least one key already exists.']
)

def test_prize_winner_admin(self):
Expand Down
103 changes: 99 additions & 4 deletions tests/test_speedrun.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import datetime
import random
from unittest import skipIf

import django
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 +101,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 +251,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 +277,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,23 +307,53 @@ 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)
self.run1.refresh_from_db()
self.assertEqual(self.run1.run_time, '0:41:20')
self.assertEqual(self.run1.setup_time, '0:09:40')

@skipIf(
django.VERSION < (4, 1),
'assertFormError requires response object until Django 4.1',
)
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)

@skipIf(
django.VERSION < (4, 1),
'assertFormError requires response object until Django 4.1',
)
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
Loading
Loading