Skip to content

Commit b550e08

Browse files
committed
Record whether an archive is the selected one
This tracks who chose it and when.
1 parent 42c8ffc commit b550e08

File tree

5 files changed

+226
-4
lines changed

5 files changed

+226
-4
lines changed

code_submitter/server.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,23 @@
1414
from starlette.middleware.authentication import AuthenticationMiddleware
1515

1616
from . import auth, config
17-
from .tables import Archive
17+
from .tables import Archive, ChoiceHistory
1818

1919
database = databases.Database(config.DATABASE_URL, force_rollback=config.TESTING)
2020
templates = Jinja2Templates(directory='templates')
2121

2222

2323
@requires('authenticated')
2424
async def homepage(request: Request) -> Response:
25+
chosen = await database.fetch_one(
26+
select([ChoiceHistory]).select_from(
27+
ChoiceHistory.join(Archive),
28+
).where(
29+
Archive.c.team == request.user.team,
30+
).order_by(
31+
ChoiceHistory.c.created.desc(),
32+
),
33+
)
2534
uploads = await database.fetch_all(
2635
select(
2736
[
@@ -40,6 +49,7 @@ async def homepage(request: Request) -> Response:
4049
)
4150
return templates.TemplateResponse('index.html', {
4251
'request': request,
52+
'chosen': chosen,
4353
'uploads': uploads,
4454
})
4555

@@ -68,13 +78,20 @@ async def upload(request: Request) -> Response:
6878
except zipfile.BadZipFile:
6979
return Response("Must upload a ZIP file", status_code=400)
7080

71-
await database.execute(
81+
archive_id = await database.execute(
7282
Archive.insert().values(
7383
content=contents,
7484
username=request.user.username,
7585
team=request.user.team,
7686
),
7787
)
88+
if form.get('choose'):
89+
await database.execute(
90+
ChoiceHistory.insert().values(
91+
archive_id=archive_id,
92+
username=request.user.username,
93+
),
94+
)
7895

7996
return RedirectResponse(
8097
request.url_for('homepage'),

code_submitter/tables.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,19 @@
1818
server_default=sqlalchemy.func.now(),
1919
),
2020
)
21+
22+
ChoiceHistory = sqlalchemy.Table(
23+
'choice_history',
24+
metadata,
25+
sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True), # noqa:A003
26+
sqlalchemy.Column('archive_id', sqlalchemy.ForeignKey('archive.id'), nullable=False),
27+
28+
sqlalchemy.Column('username', sqlalchemy.String, nullable=False),
29+
30+
sqlalchemy.Column(
31+
'created',
32+
sqlalchemy.DateTime(timezone=True),
33+
nullable=False,
34+
server_default=sqlalchemy.func.now(),
35+
),
36+
)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Create choice history table
2+
3+
Revision ID: d4e3b890e3d7
4+
Revises: eda5c539028e
5+
Create Date: 2020-07-09 18:07:04.107503
6+
7+
"""
8+
import sqlalchemy as sa
9+
from alembic import op
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = 'd4e3b890e3d7'
14+
down_revision = 'eda5c539028e'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade() -> None:
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.create_table(
22+
'choice_history',
23+
sa.Column('id', sa.Integer(), nullable=False),
24+
sa.Column('archive_id', sa.Integer(), nullable=False),
25+
sa.Column('username', sa.String(), nullable=False),
26+
sa.Column(
27+
'created',
28+
sa.DateTime(timezone=True),
29+
server_default=sa.text('(CURRENT_TIMESTAMP)'),
30+
nullable=False,
31+
),
32+
sa.ForeignKeyConstraint(['archive_id'], ['archive.id'], ),
33+
sa.PrimaryKeyConstraint('id')
34+
)
35+
# ### end Alembic commands ###
36+
37+
38+
def downgrade() -> None:
39+
# ### commands auto generated by Alembic - please adjust! ###
40+
op.drop_table('choice_history')
41+
# ### end Alembic commands ###

templates/index.html

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@
1414
.row {
1515
margin-top: 2em;
1616
}
17+
tr.chosen .info {
18+
display: none;
19+
}
20+
tr.chosen:hover .info {
21+
display: block;
22+
border: 1px solid silver;
23+
position: absolute;
24+
width: fit-content;
25+
padding: 0.2em 0.5em;
26+
background: aliceblue;
27+
}
1728
</style>
1829
</head>
1930
<body>
@@ -38,6 +49,18 @@ <h3>Upload a new submission</h3>
3849
required
3950
/>
4051
</div>
52+
<div class="form-group form-check">
53+
<input
54+
class="form-check-input"
55+
type="checkbox"
56+
name="choose"
57+
id="choose"
58+
checked
59+
/>
60+
<label class="form-check-label" for="choose">
61+
Update selection to use this archive
62+
</label>
63+
</div>
4164
<button class="btn btn-primary" type="submit">Upload</button>
4265
</form>
4366
</div>
@@ -49,12 +72,23 @@ <h3>Your uploads</h3>
4972
<th scope="col">Id</th>
5073
<th scope="col">Uploaded</th>
5174
<th scope="col">By</th>
75+
<th scope="col">Selected</th>
5276
</tr>
5377
{% for upload in uploads %}
54-
<tr>
78+
<tr
79+
class="{% if upload.id == chosen.archive_id %}chosen{% endif %}"
80+
>
5581
<td>{{ upload.id }}</td>
5682
<td>{{ upload.created }}</td>
5783
<td>{{ upload.username }}</td>
84+
<td>
85+
{% if upload.id == chosen.archive_id %}
86+
<span></span>
87+
<span class="info">
88+
Chosen by {{ chosen.username }} at {{ chosen.created }}
89+
</span>
90+
{% endif %}
91+
</td>
5892
</tr>
5993
{% endfor %}
6094
</table>

tests/tests_app.py

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
import tempfile
66
import unittest
77
from typing import IO, TypeVar, Awaitable
8+
from unittest import mock
89

910
import alembic # type: ignore[import]
1011
from sqlalchemy import create_engine
1112
from alembic.config import Config # type: ignore[import]
1213
from starlette.config import environ
1314
from starlette.testclient import TestClient
14-
from code_submitter.tables import Archive
15+
from code_submitter.tables import Archive, ChoiceHistory
1516

1617
T = TypeVar('T')
1718

@@ -109,6 +110,72 @@ def test_shows_own_and_own_team_uploads(self) -> None:
109110
self.assertNotIn('8888888888', html)
110111
self.assertNotIn('someone_else', html)
111112

113+
def test_shows_chosen_archive(self) -> None:
114+
self.await_(self.database.execute(
115+
# Another team's archive we shouldn't be able to see.
116+
Archive.insert().values(
117+
id=8888888888,
118+
content=b'',
119+
username='someone_else',
120+
team='ABC',
121+
created=datetime.datetime(2020, 8, 8, 12, 0),
122+
),
123+
))
124+
self.await_(self.database.execute(
125+
Archive.insert().values(
126+
id=2222222222,
127+
content=b'',
128+
username='a_colleague',
129+
team='SRZ',
130+
created=datetime.datetime(2020, 2, 2, 12, 0),
131+
),
132+
))
133+
self.await_(self.database.execute(
134+
Archive.insert().values(
135+
id=1111111111,
136+
content=b'',
137+
username='test_user',
138+
team='SRZ',
139+
created=datetime.datetime(2020, 1, 1, 12, 0),
140+
),
141+
))
142+
self.await_(self.database.execute(
143+
# An invalid choice -- you shouldn't be able to select archives for
144+
# another team.
145+
ChoiceHistory.insert().values(
146+
archive_id=8888888888,
147+
username='test_user',
148+
created=datetime.datetime(2020, 9, 9, 12, 0),
149+
),
150+
))
151+
self.await_(self.database.execute(
152+
ChoiceHistory.insert().values(
153+
archive_id=2222222222,
154+
username='test_user',
155+
created=datetime.datetime(2020, 3, 3, 12, 0),
156+
),
157+
))
158+
159+
response = self.session.get('/')
160+
self.assertEqual(200, response.status_code)
161+
162+
html = response.text
163+
self.assertIn('2020-01-01', html)
164+
self.assertIn('1111111111', html)
165+
self.assertIn('test_user', html)
166+
167+
self.assertIn('2020-02-02', html)
168+
self.assertIn('2222222222', html)
169+
self.assertIn('a_colleague', html)
170+
171+
self.assertIn('2020-03-03', html)
172+
173+
self.assertNotIn('2020-08-08', html)
174+
self.assertNotIn('8888888888', html)
175+
self.assertNotIn('someone_else', html)
176+
177+
self.assertNotIn('2020-09-09', html)
178+
112179
def test_upload_file(self) -> None:
113180
contents = io.BytesIO()
114181
with zipfile.ZipFile(contents, mode='w') as zip_file:
@@ -143,6 +210,48 @@ def test_upload_file(self) -> None:
143210
"Wrong team stored in the database",
144211
)
145212

213+
choices = self.await_(
214+
self.database.fetch_all(ChoiceHistory.select()),
215+
)
216+
self.assertEqual([], choices, "Should not have created a choice")
217+
218+
def test_upload_and_choose_file(self) -> None:
219+
contents = io.BytesIO()
220+
with zipfile.ZipFile(contents, mode='w') as zip_file:
221+
zip_file.writestr('robot.py', 'print("I am a robot")')
222+
223+
response = self.session.post(
224+
'/upload',
225+
data={'choose': 'on'},
226+
files={'archive': ('whatever.zip', contents.getvalue(), 'application/zip')},
227+
)
228+
self.assertEqual(302, response.status_code)
229+
self.assertEqual('http://testserver/', response.headers['location'])
230+
231+
archive, = self.await_(
232+
self.database.fetch_all(Archive.select()),
233+
)
234+
235+
self.assertEqual(
236+
contents.getvalue(),
237+
archive['content'],
238+
"Wrong content stored in the database",
239+
)
240+
241+
choices = self.await_(
242+
self.database.fetch_all(ChoiceHistory.select()),
243+
)
244+
self.assertEqual(
245+
[{
246+
'archive_id': archive['id'],
247+
'username': 'test_user',
248+
'id': mock.ANY,
249+
'created': mock.ANY,
250+
}],
251+
[dict(x) for x in choices],
252+
"Should not have created a choice",
253+
)
254+
146255
def test_upload_bad_file(self) -> None:
147256
response = self.session.post(
148257
'/upload',
@@ -155,3 +264,8 @@ def test_upload_bad_file(self) -> None:
155264
)
156265

157266
self.assertEqual([], archives, "Wrong content stored in the database")
267+
268+
choices = self.await_(
269+
self.database.fetch_all(ChoiceHistory.select()),
270+
)
271+
self.assertEqual([], choices, "Should not have created a choice")

0 commit comments

Comments
 (0)