Loading exercise/tests/create_exercise_tests.py +10 −28 Original line number Diff line number Diff line import json import os.path import shutil from io import BytesIO Loading @@ -8,14 +7,11 @@ from zipfile import ZipFile from django.conf import settings from django.core.files.uploadedfile import SimpleUploadedFile from django.test import Client from django.test import Client, override_settings from django.urls import reverse from graphene_django.utils import camelize from graphene_django.utils.testing import graphql_query from common_lib.graphql import ( GraphQLApiTestCase, DeleteDefinitionAction, CreateExerciseAction, ) from common_lib.utils import get_model, get_definition_id Loading Loading @@ -56,23 +52,14 @@ def upload_definition(name: str) -> int: return get_definition_id(response.status_code, response.content) def delete_definition(definition_id: int, url: str) -> None: action = DeleteDefinitionAction({"definition_id": definition_id}) response = graphql_query( action.query_string(), variables=camelize(action.get_variables()), graphql_url=url, ) if response.status_code != 200: raise Exception( f"Failed to delete definition: {response.status_code} {response.contet}" ) content: dict = json.loads(response.content) if content.get("errors", None) is not None: raise Exception(f"Failed to delete definition: {content}") TEST_DATA_STORAGE = "create_exercise_test_data" @override_settings( DATA_STORAGE=TEST_DATA_STORAGE, DEFINITION_STORAGE=os.path.join(TEST_DATA_STORAGE, "definitions"), EXERCISE_STORAGE=os.path.join(TEST_DATA_STORAGE, "exercises"), ) class CreateExerciseTestCase(GraphQLApiTestCase): roles_definition: Definition emails_definition: Definition Loading @@ -87,18 +74,13 @@ class CreateExerciseTestCase(GraphQLApiTestCase): @classmethod def tearDownClass(cls): delete_definition(cls.roles_definition.id, cls.GRAPHQL_URL) delete_definition(cls.emails_definition.id, cls.GRAPHQL_URL) # now that we override the storage path specifically for tests, # we can simply just remove the whole directory shutil.rmtree(settings.DATA_STORAGE) def create_exercise(self, action: CreateExerciseAction) -> Exercise: self.run_action(action) exercise_id = action.result["exercise"]["id"] # we need to explicitly remove the directories that have been created during exercise creation # because each of the test function runs with a "clean" db, # which only includes the data from the setUpTestData function, # unlike definition directories, the exercise directories do not get deleted # during the deletion of the definitions, because they do not exist at that point shutil.rmtree(os.path.join(settings.EXERCISE_STORAGE, exercise_id)) return get_model(Exercise, id=exercise_id) def test_invalid_create_exercise(self): Loading exercise_definition/lib/definition_validator.py +0 −6 Original line number Diff line number Diff line Loading @@ -359,9 +359,7 @@ def _validate_milestones( ) is not None: return err final_count = 0 for milestone in milestones: # type: YamlMilestone final_count += milestone.final if not milestone.team_visible and milestone.roles != "": return Err( "Milestones, which are not visible to teams should not have " Loading @@ -377,10 +375,6 @@ def _validate_milestones( ) is not None: return err if final_count == 0: return Err( "Improperly configured milestones.yml, at least one milestone must be set as final" ) return None Loading rolling-changelog.txt +2 −0 Original line number Diff line number Diff line Loading @@ -3,6 +3,8 @@ change: rename PerformTeamToolAction mutation to UseTool #94 change: reach_milestone is replaced by fields activate_milestone and deactivate_milestone #98 change: add option to override some definition config values #109 change: add an optional parameter 'instructor' to file download REST API endpoint #119 feat: add mutation for milestone modification #116 fix: fix move time not working for exercise loop #106 new feature: REST API endpoints for database export and import #79 running_exercise/lib/milestone_handler.py +15 −2 Original line number Diff line number Diff line from typing import List, Optional import ast from typing import List from django.utils import timezone from common_lib.exc_handler import RunningExerciseOperationException from common_lib.subscription_handler import SubscriptionHandler from common_lib.utils import ( get_model, Loading @@ -11,7 +12,6 @@ from common_lib.utils import ( ) from exercise.models import Team, MilestoneState from exercise_definition.models import Milestone from common_lib.exc_handler import RunningExerciseOperationException def is_milestone_visible(team: Team, milestone: Milestone) -> bool: Loading Loading @@ -128,3 +128,16 @@ def update_milestones( ], t, ) def instructor_modify_milestone( team_id: int, milestone: str, activate: bool ) -> None: team = get_model(Team, id=team_id) if not team.exercise.running: raise RunningExerciseOperationException( "Cannot modify milestone state in an exercise that is not currently running" ) update_milestones( team, milestone if activate else "", milestone if not activate else "" ) running_exercise/lib/utils.py +4 −1 Original line number Diff line number Diff line Loading @@ -59,4 +59,7 @@ def elapsed_exercise_minutes() -> int: if running_exercise is None or running_exercise.exercise_start is None: return -1 return (timezone.now() - running_exercise.exercise_start).seconds // 60 current_time = timezone.now() + timedelta( seconds=running_exercise.time_delta ) return (current_time - running_exercise.exercise_start).seconds // 60 Loading
exercise/tests/create_exercise_tests.py +10 −28 Original line number Diff line number Diff line import json import os.path import shutil from io import BytesIO Loading @@ -8,14 +7,11 @@ from zipfile import ZipFile from django.conf import settings from django.core.files.uploadedfile import SimpleUploadedFile from django.test import Client from django.test import Client, override_settings from django.urls import reverse from graphene_django.utils import camelize from graphene_django.utils.testing import graphql_query from common_lib.graphql import ( GraphQLApiTestCase, DeleteDefinitionAction, CreateExerciseAction, ) from common_lib.utils import get_model, get_definition_id Loading Loading @@ -56,23 +52,14 @@ def upload_definition(name: str) -> int: return get_definition_id(response.status_code, response.content) def delete_definition(definition_id: int, url: str) -> None: action = DeleteDefinitionAction({"definition_id": definition_id}) response = graphql_query( action.query_string(), variables=camelize(action.get_variables()), graphql_url=url, ) if response.status_code != 200: raise Exception( f"Failed to delete definition: {response.status_code} {response.contet}" ) content: dict = json.loads(response.content) if content.get("errors", None) is not None: raise Exception(f"Failed to delete definition: {content}") TEST_DATA_STORAGE = "create_exercise_test_data" @override_settings( DATA_STORAGE=TEST_DATA_STORAGE, DEFINITION_STORAGE=os.path.join(TEST_DATA_STORAGE, "definitions"), EXERCISE_STORAGE=os.path.join(TEST_DATA_STORAGE, "exercises"), ) class CreateExerciseTestCase(GraphQLApiTestCase): roles_definition: Definition emails_definition: Definition Loading @@ -87,18 +74,13 @@ class CreateExerciseTestCase(GraphQLApiTestCase): @classmethod def tearDownClass(cls): delete_definition(cls.roles_definition.id, cls.GRAPHQL_URL) delete_definition(cls.emails_definition.id, cls.GRAPHQL_URL) # now that we override the storage path specifically for tests, # we can simply just remove the whole directory shutil.rmtree(settings.DATA_STORAGE) def create_exercise(self, action: CreateExerciseAction) -> Exercise: self.run_action(action) exercise_id = action.result["exercise"]["id"] # we need to explicitly remove the directories that have been created during exercise creation # because each of the test function runs with a "clean" db, # which only includes the data from the setUpTestData function, # unlike definition directories, the exercise directories do not get deleted # during the deletion of the definitions, because they do not exist at that point shutil.rmtree(os.path.join(settings.EXERCISE_STORAGE, exercise_id)) return get_model(Exercise, id=exercise_id) def test_invalid_create_exercise(self): Loading
exercise_definition/lib/definition_validator.py +0 −6 Original line number Diff line number Diff line Loading @@ -359,9 +359,7 @@ def _validate_milestones( ) is not None: return err final_count = 0 for milestone in milestones: # type: YamlMilestone final_count += milestone.final if not milestone.team_visible and milestone.roles != "": return Err( "Milestones, which are not visible to teams should not have " Loading @@ -377,10 +375,6 @@ def _validate_milestones( ) is not None: return err if final_count == 0: return Err( "Improperly configured milestones.yml, at least one milestone must be set as final" ) return None Loading
rolling-changelog.txt +2 −0 Original line number Diff line number Diff line Loading @@ -3,6 +3,8 @@ change: rename PerformTeamToolAction mutation to UseTool #94 change: reach_milestone is replaced by fields activate_milestone and deactivate_milestone #98 change: add option to override some definition config values #109 change: add an optional parameter 'instructor' to file download REST API endpoint #119 feat: add mutation for milestone modification #116 fix: fix move time not working for exercise loop #106 new feature: REST API endpoints for database export and import #79
running_exercise/lib/milestone_handler.py +15 −2 Original line number Diff line number Diff line from typing import List, Optional import ast from typing import List from django.utils import timezone from common_lib.exc_handler import RunningExerciseOperationException from common_lib.subscription_handler import SubscriptionHandler from common_lib.utils import ( get_model, Loading @@ -11,7 +12,6 @@ from common_lib.utils import ( ) from exercise.models import Team, MilestoneState from exercise_definition.models import Milestone from common_lib.exc_handler import RunningExerciseOperationException def is_milestone_visible(team: Team, milestone: Milestone) -> bool: Loading Loading @@ -128,3 +128,16 @@ def update_milestones( ], t, ) def instructor_modify_milestone( team_id: int, milestone: str, activate: bool ) -> None: team = get_model(Team, id=team_id) if not team.exercise.running: raise RunningExerciseOperationException( "Cannot modify milestone state in an exercise that is not currently running" ) update_milestones( team, milestone if activate else "", milestone if not activate else "" )
running_exercise/lib/utils.py +4 −1 Original line number Diff line number Diff line Loading @@ -59,4 +59,7 @@ def elapsed_exercise_minutes() -> int: if running_exercise is None or running_exercise.exercise_start is None: return -1 return (timezone.now() - running_exercise.exercise_start).seconds // 60 current_time = timezone.now() + timedelta( seconds=running_exercise.time_delta ) return (current_time - running_exercise.exercise_start).seconds // 60