Skip to content

Commit

Permalink
Record whether an archive is the selected one
Browse files Browse the repository at this point in the history
This tracks who chose it and when.
  • Loading branch information
PeterJCLaw committed Jul 9, 2020
1 parent 42c8ffc commit b550e08
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 4 deletions.
21 changes: 19 additions & 2 deletions code_submitter/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,23 @@
from starlette.middleware.authentication import AuthenticationMiddleware

from . import auth, config
from .tables import Archive
from .tables import Archive, ChoiceHistory

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


@requires('authenticated')
async def homepage(request: Request) -> Response:
chosen = await database.fetch_one(
select([ChoiceHistory]).select_from(
ChoiceHistory.join(Archive),
).where(
Archive.c.team == request.user.team,
).order_by(
ChoiceHistory.c.created.desc(),
),
)
uploads = await database.fetch_all(
select(
[
Expand All @@ -40,6 +49,7 @@ async def homepage(request: Request) -> Response:
)
return templates.TemplateResponse('index.html', {
'request': request,
'chosen': chosen,
'uploads': uploads,
})

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

await database.execute(
archive_id = await database.execute(
Archive.insert().values(
content=contents,
username=request.user.username,
team=request.user.team,
),
)
if form.get('choose'):
await database.execute(
ChoiceHistory.insert().values(
archive_id=archive_id,
username=request.user.username,
),
)

return RedirectResponse(
request.url_for('homepage'),
Expand Down
16 changes: 16 additions & 0 deletions code_submitter/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,19 @@
server_default=sqlalchemy.func.now(),
),
)

ChoiceHistory = sqlalchemy.Table(
'choice_history',
metadata,
sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True), # noqa:A003
sqlalchemy.Column('archive_id', sqlalchemy.ForeignKey('archive.id'), nullable=False),

sqlalchemy.Column('username', sqlalchemy.String, nullable=False),

sqlalchemy.Column(
'created',
sqlalchemy.DateTime(timezone=True),
nullable=False,
server_default=sqlalchemy.func.now(),
),
)
41 changes: 41 additions & 0 deletions migrations/versions/d4e3b890e3d7_create_choice_history_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Create choice history table
Revision ID: d4e3b890e3d7
Revises: eda5c539028e
Create Date: 2020-07-09 18:07:04.107503
"""
import sqlalchemy as sa
from alembic import op


# revision identifiers, used by Alembic.
revision = 'd4e3b890e3d7'
down_revision = 'eda5c539028e'
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
'choice_history',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('archive_id', sa.Integer(), nullable=False),
sa.Column('username', sa.String(), nullable=False),
sa.Column(
'created',
sa.DateTime(timezone=True),
server_default=sa.text('(CURRENT_TIMESTAMP)'),
nullable=False,
),
sa.ForeignKeyConstraint(['archive_id'], ['archive.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('choice_history')
# ### end Alembic commands ###
36 changes: 35 additions & 1 deletion templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@
.row {
margin-top: 2em;
}
tr.chosen .info {
display: none;
}
tr.chosen:hover .info {
display: block;
border: 1px solid silver;
position: absolute;
width: fit-content;
padding: 0.2em 0.5em;
background: aliceblue;
}
</style>
</head>
<body>
Expand All @@ -38,6 +49,18 @@ <h3>Upload a new submission</h3>
required
/>
</div>
<div class="form-group form-check">
<input
class="form-check-input"
type="checkbox"
name="choose"
id="choose"
checked
/>
<label class="form-check-label" for="choose">
Update selection to use this archive
</label>
</div>
<button class="btn btn-primary" type="submit">Upload</button>
</form>
</div>
Expand All @@ -49,12 +72,23 @@ <h3>Your uploads</h3>
<th scope="col">Id</th>
<th scope="col">Uploaded</th>
<th scope="col">By</th>
<th scope="col">Selected</th>
</tr>
{% for upload in uploads %}
<tr>
<tr
class="{% if upload.id == chosen.archive_id %}chosen{% endif %}"
>
<td>{{ upload.id }}</td>
<td>{{ upload.created }}</td>
<td>{{ upload.username }}</td>
<td>
{% if upload.id == chosen.archive_id %}
<span></span>
<span class="info">
Chosen by {{ chosen.username }} at {{ chosen.created }}
</span>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
Expand Down
116 changes: 115 additions & 1 deletion tests/tests_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
import tempfile
import unittest
from typing import IO, TypeVar, Awaitable
from unittest import mock

import alembic # type: ignore[import]
from sqlalchemy import create_engine
from alembic.config import Config # type: ignore[import]
from starlette.config import environ
from starlette.testclient import TestClient
from code_submitter.tables import Archive
from code_submitter.tables import Archive, ChoiceHistory

T = TypeVar('T')

Expand Down Expand Up @@ -109,6 +110,72 @@ def test_shows_own_and_own_team_uploads(self) -> None:
self.assertNotIn('8888888888', html)
self.assertNotIn('someone_else', html)

def test_shows_chosen_archive(self) -> None:
self.await_(self.database.execute(
# Another team's archive we shouldn't be able to see.
Archive.insert().values(
id=8888888888,
content=b'',
username='someone_else',
team='ABC',
created=datetime.datetime(2020, 8, 8, 12, 0),
),
))
self.await_(self.database.execute(
Archive.insert().values(
id=2222222222,
content=b'',
username='a_colleague',
team='SRZ',
created=datetime.datetime(2020, 2, 2, 12, 0),
),
))
self.await_(self.database.execute(
Archive.insert().values(
id=1111111111,
content=b'',
username='test_user',
team='SRZ',
created=datetime.datetime(2020, 1, 1, 12, 0),
),
))
self.await_(self.database.execute(
# An invalid choice -- you shouldn't be able to select archives for
# another team.
ChoiceHistory.insert().values(
archive_id=8888888888,
username='test_user',
created=datetime.datetime(2020, 9, 9, 12, 0),
),
))
self.await_(self.database.execute(
ChoiceHistory.insert().values(
archive_id=2222222222,
username='test_user',
created=datetime.datetime(2020, 3, 3, 12, 0),
),
))

response = self.session.get('/')
self.assertEqual(200, response.status_code)

html = response.text
self.assertIn('2020-01-01', html)
self.assertIn('1111111111', html)
self.assertIn('test_user', html)

self.assertIn('2020-02-02', html)
self.assertIn('2222222222', html)
self.assertIn('a_colleague', html)

self.assertIn('2020-03-03', html)

self.assertNotIn('2020-08-08', html)
self.assertNotIn('8888888888', html)
self.assertNotIn('someone_else', html)

self.assertNotIn('2020-09-09', html)

def test_upload_file(self) -> None:
contents = io.BytesIO()
with zipfile.ZipFile(contents, mode='w') as zip_file:
Expand Down Expand Up @@ -143,6 +210,48 @@ def test_upload_file(self) -> None:
"Wrong team stored in the database",
)

choices = self.await_(
self.database.fetch_all(ChoiceHistory.select()),
)
self.assertEqual([], choices, "Should not have created a choice")

def test_upload_and_choose_file(self) -> None:
contents = io.BytesIO()
with zipfile.ZipFile(contents, mode='w') as zip_file:
zip_file.writestr('robot.py', 'print("I am a robot")')

response = self.session.post(
'/upload',
data={'choose': 'on'},
files={'archive': ('whatever.zip', contents.getvalue(), 'application/zip')},
)
self.assertEqual(302, response.status_code)
self.assertEqual('http://testserver/', response.headers['location'])

archive, = self.await_(
self.database.fetch_all(Archive.select()),
)

self.assertEqual(
contents.getvalue(),
archive['content'],
"Wrong content stored in the database",
)

choices = self.await_(
self.database.fetch_all(ChoiceHistory.select()),
)
self.assertEqual(
[{
'archive_id': archive['id'],
'username': 'test_user',
'id': mock.ANY,
'created': mock.ANY,
}],
[dict(x) for x in choices],
"Should not have created a choice",
)

def test_upload_bad_file(self) -> None:
response = self.session.post(
'/upload',
Expand All @@ -155,3 +264,8 @@ def test_upload_bad_file(self) -> None:
)

self.assertEqual([], archives, "Wrong content stored in the database")

choices = self.await_(
self.database.fetch_all(ChoiceHistory.select()),
)
self.assertEqual([], choices, "Should not have created a choice")

0 comments on commit b550e08

Please sign in to comment.