Skip to content

Commit

Permalink
Validering av kjøreplan
Browse files Browse the repository at this point in the history
Fixes #118

Validerer:
- at race ikkje har 0 contestants
- at races er i kronlogisk rekkefølge
- at antall contestants i race stemmer med raceplan
- at antall contestants stemmer med ageclasses
  • Loading branch information
stigbd committed Dec 11, 2022
1 parent 3468449 commit 01d55c2
Show file tree
Hide file tree
Showing 8 changed files with 615 additions and 5 deletions.
45 changes: 45 additions & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,51 @@
)


@session(python=["3.10"])
def clean(session: Session) -> None:
"""Clean the project."""
session.run(
"py3clean",
".",
external=True,
)
session.run(
"rm",
"-rf",
".cache",
external=True,
)
session.run(
"rm",
"-rf",
".pytest_cache",
external=True,
)
session.run(
"rm",
"-rf",
".pytype",
external=True,
)
session.run(
"rm",
"-rf",
"dist",
external=True,
)
session.run(
"rm",
"-rf",
".mypy_cache",
external=True,
)
session.run(
"rm",
".coverage",
external=True,
)


@session(python="3.10")
def unit_tests(session: Session) -> None:
"""Run the unit test suite."""
Expand Down
7 changes: 5 additions & 2 deletions race_service/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
from typing import Any

from aiohttp import web
from aiohttp_middlewares import cors_middleware, error_middleware
from aiohttp_middlewares.cors import cors_middleware
from aiohttp_middlewares.error import error_middleware
import motor.motor_asyncio

from .views import (
Expand All @@ -24,6 +25,7 @@
StartlistView,
TimeEventsView,
TimeEventView,
ValidateRaceplanView,
)


Expand Down Expand Up @@ -53,6 +55,7 @@ async def create_app() -> web.Application:
"/raceplans/generate-raceplan-for-event", GenerateRaceplanForEventView
),
web.view("/raceplans/{raceplanId}", RaceplanView),
web.view("/raceplans/{raceplanId}/validate", ValidateRaceplanView),
web.view("/races", RacesView),
web.view("/races/{raceId}", RaceView),
web.view("/races/{raceId}/race-results", RaceResultsView),
Expand Down Expand Up @@ -91,6 +94,6 @@ async def mongo_context(app: Any) -> Any:

mongo.close()

app.cleanup_ctx.append(mongo_context)
app.cleanup_ctx.append(mongo_context) # type: ignore

return app
91 changes: 90 additions & 1 deletion race_service/commands/raceplans_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
EventNotFoundException,
EventsAdapter,
RaceplansAdapter,
RacesAdapter,
)
from race_service.models import Raceplan
from race_service.services import (
RaceplanAllreadyExistException,
RaceplansService,
Expand Down Expand Up @@ -87,6 +89,93 @@ async def generate_raceplan_for_event( # noqa: C901
"Something went wrong when creating raceplan."
) from None

@classmethod
async def validate_raceplan( # noqa: C901
cls: Any, db: Any, token: str, raceplan: Raceplan
) -> Dict[int, List[str]]:
"""Validate a given raceplan and return validation results."""
# First we get the event from the event-service:
try:
event = await get_event(token, raceplan.event_id)
except EventNotFoundException as e: # pragma: no cover
raise e from e
# We fetch the competition-format:
try:
competition_format = await get_competition_format(
token, raceplan.event_id, event["competition_format"]
)
except CompetitionFormatNotFoundException as e: # pragma: no cover
raise CompetitionFormatNotSupportedException(
f'Competition-format {event["competition_format"]} is not supported.'
) from e
# Then we fetch the raceclasses:
raceclasses = await get_raceclasses(token, raceplan.event_id)

results: Dict[int, List[str]] = {}

races: List[Dict] = []
for race_id in raceplan.races:
race = await RacesAdapter.get_race_by_id(db, race_id)
races.append(race)

races.sort(key=lambda x: x["order"])

# Check if races are in chronological order:
for i in range(0, len(races) - 1):
if races[i]["start_time"] >= races[i + 1]["start_time"]:
results[races[i + 1]["order"]] = [
"Start time is not in chronological order."
]

# Check each race and sum up the number of contestants:
sum_no_of_contestants = 0
for race in races:
# Check if race has contestants:
if race["no_of_contestants"] == 0:
if race["order"] in results:
results[race["order"]].append("Race has no contestants.")
else:
results[race["order"]] = [("Race has no contestants.")]

# Sum up the number of contestants in first rounds:
if race["round"] in [
competition_format["rounds_ranked_classes"][0],
competition_format["rounds_non_ranked_classes"][0],
]:
sum_no_of_contestants += race["no_of_contestants"]

# Check if the sum of contestants in races is equal to the number of contestants in the raceplan:
if sum_no_of_contestants != raceplan.no_of_contestants:
results[0] = [
f"The sum of contestants in races ({sum_no_of_contestants})"
f" is not equal to the number of contestants in the raceplan ({raceplan.no_of_contestants})."
]

# Check if the number of contestants in the plan is equal to
# the number of contestants in the raceclasses:
no_of_contestants_in_raceclasses = sum(
raceclass["no_of_contestants"] for raceclass in raceclasses
)
if raceplan.no_of_contestants != no_of_contestants_in_raceclasses:
if 0 in results:
results[0].append(
(
f"Number of contestants in raceplan ({raceplan.no_of_contestants})"
" is not equal to the number of contestants"
f" in the raceclasses ({no_of_contestants_in_raceclasses})."
)
)
else: # pragma: no cover
results[0] = [
(
f"Number of contestants in raceplan ({raceplan.no_of_contestants})"
f" is not equal to the number of contestants"
f"in the raceclasses ({no_of_contestants_in_raceclasses})."
)
]

return results


# helpers
async def get_raceplan(db: Any, token: str, event_id: str) -> None:
Expand Down Expand Up @@ -249,6 +338,6 @@ async def get_raceclasses(token: str, event_id: str) -> List[dict]: # noqa: C90
for _raceclasses in raceclasses_grouped:
if len(set([r["ranking"] for r in _raceclasses])) > 1:
raise InconsistentValuesInRaceclassesException(
f'Ranking-value differs in group {raceclass["group"]}.'
f'Ranking-value differs in group {_raceclasses[0]["group"]}.'
)
return raceclasses
1 change: 1 addition & 0 deletions race_service/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
RaceplanAllreadyExistException,
RaceplanNotFoundException,
RaceplansService,
validate_raceplan,
)
from .races_service import (
RaceNotFoundException,
Expand Down
2 changes: 1 addition & 1 deletion race_service/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from .liveness import Ping, Ready
from .race_results import RaceResultsView, RaceResultView
from .raceplans import RaceplansView, RaceplanView
from .raceplans_commands import GenerateRaceplanForEventView
from .raceplans_commands import GenerateRaceplanForEventView, ValidateRaceplanView
from .races import RacesView, RaceView
from .start_entries import StartEntriesView, StartEntryView
from .startlists import StartlistsView, StartlistView
Expand Down
37 changes: 36 additions & 1 deletion race_service/views/raceplans_commands.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Resource module for raceplan command resources."""
import json
import os

from aiohttp import hdrs
Expand Down Expand Up @@ -26,7 +27,11 @@
NoRaceclassesInEventException,
RaceplansCommands,
)
from race_service.services import RaceplanAllreadyExistException
from race_service.services import (
RaceplanAllreadyExistException,
RaceplanNotFoundException,
RaceplansService,
)
from .utils import extract_token_from_request

load_dotenv()
Expand Down Expand Up @@ -73,3 +78,33 @@ async def post(self) -> Response:
raise HTTPBadRequest(reason=str(e)) from e
headers = MultiDict([(hdrs.LOCATION, f"{BASE_URL}/raceplans/{raceplan_id}")])
return Response(status=201, headers=headers)


class ValidateRaceplanView(View):
"""Class representing the validation of a given raceplan."""

async def post(self) -> Response:
"""Post route function."""
# Authorize:
db = self.request.app["db"]
token = extract_token_from_request(self.request)
assert token # noqa: S101
try:
await UsersAdapter.authorize(token, roles=["admin", "event-admin"])
except Exception as e: # pragma: no cover
raise e

raceplan_id = self.request.match_info["raceplanId"]

# Fetch the raceplan:
try:
raceplan = await RaceplansService.get_raceplan_by_id(db, raceplan_id)
except RaceplanNotFoundException as e: # pragma: no cover
raise HTTPNotFound(reason=str(e)) from e

# Validate
result = await RaceplansCommands.validate_raceplan(db, token, raceplan)
headers = MultiDict([(hdrs.CONTENT_TYPE, "application/json")])
body = json.dumps(result, default=str, ensure_ascii=False)

return Response(status=200, headers=headers, body=body)
23 changes: 23 additions & 0 deletions tests/contract/test_raceplans.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,29 @@ async def test_get_raceplan(
i += 1


@pytest.mark.contract
@pytest.mark.asyncio
async def test_validate_raceplan(
http_service: Any, token: MockFixture, context: dict
) -> None:
"""Should return 200 OK and and a body with a list of results."""
raceplan_id = context["id"]
url = f"{http_service}/raceplans/{raceplan_id}/validate"
headers = {
hdrs.AUTHORIZATION: f"Bearer {token}",
}
session = ClientSession()
async with session.post(url, headers=headers) as response:
status = response.status
results = await response.json()
await session.close()

assert status == 200
assert "application/json" in response.headers[hdrs.CONTENT_TYPE]
assert type(results) is dict
assert len(results) == 0


@pytest.mark.contract
@pytest.mark.asyncio
async def test_update_race(
Expand Down
Loading

0 comments on commit 01d55c2

Please sign in to comment.