diff --git a/mapswipe_workers/mapswipe_workers/definitions.py b/mapswipe_workers/mapswipe_workers/definitions.py index c9dec6d79..aa32d3aac 100644 --- a/mapswipe_workers/mapswipe_workers/definitions.py +++ b/mapswipe_workers/mapswipe_workers/definitions.py @@ -170,6 +170,7 @@ def tutorial(self): ClassificationTutorial, CompletenessTutorial, FootprintTutorial, + StreetTutorial, ) project_type_classes = { @@ -177,5 +178,6 @@ def tutorial(self): 2: FootprintTutorial, 3: ChangeDetectionTutorial, 4: CompletenessTutorial, + 7: StreetTutorial, } return project_type_classes[self.value] diff --git a/mapswipe_workers/mapswipe_workers/project_types/__init__.py b/mapswipe_workers/mapswipe_workers/project_types/__init__.py index 43013b0dc..9560c76ef 100644 --- a/mapswipe_workers/mapswipe_workers/project_types/__init__.py +++ b/mapswipe_workers/mapswipe_workers/project_types/__init__.py @@ -3,6 +3,7 @@ from .arbitrary_geometry.footprint.tutorial import FootprintTutorial from .media_classification.project import MediaClassificationProject from .street.project import StreetProject +from .street.tutorial import StreetTutorial from .tile_map_service.change_detection.project import ChangeDetectionProject from .tile_map_service.change_detection.tutorial import ChangeDetectionTutorial from .tile_map_service.classification.project import ClassificationProject @@ -22,4 +23,5 @@ "FootprintTutorial", "DigitizationProject", "StreetProject", + "StreetTutorial", ] diff --git a/mapswipe_workers/mapswipe_workers/project_types/street/tutorial.py b/mapswipe_workers/mapswipe_workers/project_types/street/tutorial.py index cfbfc0ead..e59a97f09 100644 --- a/mapswipe_workers/mapswipe_workers/project_types/street/tutorial.py +++ b/mapswipe_workers/mapswipe_workers/project_types/street/tutorial.py @@ -1,14 +1,84 @@ +from dataclasses import asdict, dataclass + +from mapswipe_workers.definitions import logger +from mapswipe_workers.firebase.firebase import Firebase +from mapswipe_workers.project_types.street.project import StreetGroup, StreetTask from mapswipe_workers.project_types.tutorial import BaseTutorial +@dataclass +class StreetTutorialTask(StreetTask): + projectId: int + taskId: str + groupId: int + referenceAnswer: int + screen: int + + class StreetTutorial(BaseTutorial): - """The subclass for an TMS Grid based Tutorial.""" + """The subclass for an arbitrary geometry based Tutorial.""" - def save_tutorial(self): - raise NotImplementedError("Currently Street has no Tutorial") + def __init__(self, tutorial_draft): + # this will create the basis attributes + super().__init__(tutorial_draft) + + # self.projectId = tutorial_draft["projectId"] + self.projectType = tutorial_draft["projectType"] + self.tutorial_tasks = tutorial_draft["tutorialTasks"] + self.groups = dict() + self.tasks = dict() def create_tutorial_groups(self): - raise NotImplementedError("Currently Street has no Tutorial") + """Create group for the tutorial based on provided examples in geojson file.""" + # load examples/tasks from file + + group = StreetGroup( + groupId=101, + projectId=self.projectId, + numberOfTasks=len(self.tutorial_tasks), + progress=0, + finishedCount=0, + requiredCount=0, + ) + self.groups[101] = group + + # Add number of tasks for the group here. This needs to be set according to + # the number of features/examples in the geojson file + + logger.info( + f"{self.projectId}" + f" - create_tutorial_groups - " + f"created groups dictionary" + ) def create_tutorial_tasks(self): - raise NotImplementedError("Currently Street has no Tutorial") + """Create the tasks dict based on provided examples in geojson file.""" + task_list = [] + for i, task in enumerate(self.tutorial_tasks["features"]): + task = StreetTutorialTask( + projectId=self.projectId, + groupId=101, + taskId=f"{task['properties']['id']}", + geometry="", + referenceAnswer=task["properties"]["reference"], + screen=task["properties"]["screen"], + ) + task_list.append(asdict(task)) + if task_list: + self.tasks[101] = task_list + else: + logger.info(f"group in project {self.projectId} is not valid.") + + logger.info( + f"{self.projectId}" + f" - create_tutorial_tasks - " + f"created tasks dictionary" + ) + + def save_tutorial(self): + firebase = Firebase() + firebase.save_tutorial_to_firebase( + self, self.groups, self.tasks, useCompression=True + ) + logger.info(self.tutorialDraftId) + firebase.drop_tutorial_draft(self.tutorialDraftId) diff --git a/mapswipe_workers/tests/fixtures/projectDrafts/street.json b/mapswipe_workers/tests/fixtures/projectDrafts/street.json index 1dd5b452a..67d1d8b04 100644 --- a/mapswipe_workers/tests/fixtures/projectDrafts/street.json +++ b/mapswipe_workers/tests/fixtures/projectDrafts/street.json @@ -46,6 +46,5 @@ "requestingOrganisation": "test", "verificationNumber": 3, "groupSize": 25, - "startTimestamp": "2019-07-01T00:00:00.000Z", "samplingThreshold": 0.1 } diff --git a/mapswipe_workers/tests/fixtures/tutorialDrafts/street.json b/mapswipe_workers/tests/fixtures/tutorialDrafts/street.json new file mode 100644 index 000000000..1385ffb52 --- /dev/null +++ b/mapswipe_workers/tests/fixtures/tutorialDrafts/street.json @@ -0,0 +1,110 @@ +{ + "createdBy": "atCSosZACaN0qhcVjtMO1tq9d1G3", + "tutorialDraftId": "test_tile_classification", + "informationPages": [ + { + "blocks": [ + { + "blockNumber": 1, + "blockType": "text", + "textDescription": "This is the first information page" + }, + { + "blockNumber": 2, + "blockType": "image", + "image": "https://firebasestorage.googleapis.com/v0/b/dev-mapswipe.appspot.com/o/tutorialImages%2F1739963139725-block-image-2-1x1.png?alt=media&token=ae584dcd-d351-4bfe-be5f-1e0d38547f72" + } + ], + "pageNumber": 1, + "title": "Information page 1" + } + ], + "lookFor": "cobblestone", + "name": "cobblestone-tutorial", + "projectType": 7, + "screens": [ + null, + { + "hint": { + "description": "This seems to be a tarmac surface.", + "icon": "check", + "title": "Tarmac" + }, + "instructions": { + "description": "Check out if the road surface material is cobblestone here", + "icon": "check", + "title": "Is this cobblestone?" + }, + "success": { + "description": "Correct, this is not cobblestone", + "icon": "check", + "title": "Nice!" + } + }, + { + "hint": { + "description": "That surface does look like cobblestone!", + "icon": "heart-outline", + "title": "Cobblestone" + }, + "instructions": { + "description": "Does this look like cobblestone?", + "icon": "egg-outline", + "title": "How about this one?" + }, + "success": { + "description": "Correct", + "icon": "search-outline", + "title": "Correct" + } + } + ], + "tileServer": { + "credits": "© 2019 Microsoft Corporation, Earthstar Geographics SIO", + "name": "bing" + }, + "tutorialTasks": { + "crs": { + "properties": { + "name": "urn:ogc:def:crs:OGC:1.3:CRS84" + }, + "type": "name" + }, + "features": [ + { + "geometry": { + "coordinates": [ + 13.4514123, + 52.5103378, + 0 + ], + "type": "Point" + }, + "properties": { + "id": "1171343450849316", + "reference": 1, + "screen": 1 + }, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + 13.45285, + 52.508467, + 0 + ], + "type": "Point" + }, + "properties": { + "id": "378811598610667", + "reference": 0, + "screen": 2 + }, + "type": "Feature" + } + ], + "name": "cobblestone-scenario", + "type": "FeatureCollection" + } +} \ No newline at end of file diff --git a/mapswipe_workers/tests/integration/set_up.py b/mapswipe_workers/tests/integration/set_up.py index 74adc6fda..1c3c0bdf4 100644 --- a/mapswipe_workers/tests/integration/set_up.py +++ b/mapswipe_workers/tests/integration/set_up.py @@ -16,20 +16,28 @@ def set_firebase_test_data( - project_type: str, data_type: str, fixture_name: str, identifier: str + project_type: str, + data_type: str, + fixture_name: str, + identifier: str, + tutorial_id: str = None, ): test_dir = os.path.dirname(__file__) fixture_name = fixture_name + ".json" file_path = os.path.join( test_dir, "fixtures", project_type, data_type, fixture_name ) - upload_file_to_firebase(file_path, data_type, identifier) + upload_file_to_firebase(file_path, data_type, identifier, tutorial_id=tutorial_id) -def upload_file_to_firebase(file_path: str, data_type: str, identifier: str): +def upload_file_to_firebase( + file_path: str, data_type: str, identifier: str, tutorial_id: str = None +): with open(file_path) as test_file: test_data = json.load(test_file) + if tutorial_id: + test_data["tutorialId"] = tutorial_id fb_db = auth.firebaseDB() ref = fb_db.reference(f"/v2/{data_type}/{identifier}") ref.set(test_data) @@ -85,15 +93,20 @@ def create_test_project( set_postgres_test_data(project_type, "users", "user") set_firebase_test_data(project_type, "user_groups", "user_group", "") set_firebase_test_data(project_type, "results", fixture_name, project_id) - set_postgres_test_data(project_type, "mapping_sessions", fixture_name, columns=[ - "project_id", - "group_id", - "user_id", - "mapping_session_id", - "start_time", - "end_time", - "items_count", - ]) + set_postgres_test_data( + project_type, + "mapping_sessions", + fixture_name, + columns=[ + "project_id", + "group_id", + "user_id", + "mapping_session_id", + "start_time", + "end_time", + "items_count", + ], + ) set_postgres_test_data(project_type, mapping_sessions_results, fixture_name) if create_user_group_session_data: set_postgres_test_data( @@ -108,7 +121,9 @@ def create_test_project( "created_at", ], ) - set_postgres_test_data(project_type, "mapping_sessions_user_groups", fixture_name) + set_postgres_test_data( + project_type, "mapping_sessions_user_groups", fixture_name + ) time.sleep(5) # Wait for Firebase Functions to complete return project_id @@ -131,12 +146,24 @@ def create_test_user(project_type: str, user_id: str = None) -> str: def create_test_project_draft( - project_type: str, fixture_name: str = "user", identifier: str = "" + project_type: str, + fixture_name: str = "user", + identifier: str = "", + tutorial_id: str = None, ) -> str: """ Create test project drafts in Firebase and return project ids. Project drafts in Firebase are created by project manager using the dashboard. """ + if tutorial_id: + set_firebase_test_data( + project_type, + "projectDrafts", + fixture_name, + identifier, + tutorial_id=tutorial_id, + ) + return identifier if not identifier: identifier = f"test_{fixture_name}" set_firebase_test_data(project_type, "projectDrafts", fixture_name, identifier) diff --git a/mapswipe_workers/tests/integration/tear_down.py b/mapswipe_workers/tests/integration/tear_down.py index 61760781c..33af5d52d 100644 --- a/mapswipe_workers/tests/integration/tear_down.py +++ b/mapswipe_workers/tests/integration/tear_down.py @@ -8,7 +8,7 @@ from mapswipe_workers import auth -def delete_test_data(project_id: str) -> None: +def delete_test_data(project_id: str, tutorial_id: str = None) -> None: """ Delete test project indluding groups, tasks and results from Firebase and Postgres @@ -38,6 +38,12 @@ def delete_test_data(project_id: str) -> None: ref = fb_db.reference(f"v2/users/{project_id}") ref.delete() + if tutorial_id is not None: + ref = fb_db.reference(f"v2/projects/{tutorial_id}") + ref.delete() + ref = fb_db.reference(f"v2/tutorialDrafts/{tutorial_id}") + ref.delete() + # Clear out the user-group used in test. # XXX: Use a firebase simulator for running test. # For CI/CD, use a real firebase with scope using commit hash, diff --git a/mapswipe_workers/tests/integration/test_create_tutorial.py b/mapswipe_workers/tests/integration/test_create_tutorial.py new file mode 100644 index 000000000..e6db39579 --- /dev/null +++ b/mapswipe_workers/tests/integration/test_create_tutorial.py @@ -0,0 +1,45 @@ +import unittest + +from click.testing import CliRunner + +from mapswipe_workers import auth, mapswipe_workers +from mapswipe_workers.utils.create_directories import create_directories +from tests.integration import set_up, tear_down + + +class TestCreateTileClassificationProject(unittest.TestCase): + def setUp(self): + self.tutorial_id = set_up.create_test_tutorial_draft( + "tile_classification", + "tile_classification", + "test_tile_classification_tutorial", + ) + + self.project_id = set_up.create_test_project_draft( + "tile_classification", + "tile_classification", + "test_tile_classification_tutorial", + tutorial_id=self.tutorial_id, + ) + create_directories() + + def tearDown(self): + tear_down.delete_test_data(self.project_id, self.tutorial_id) + + def test_create_tile_classification_project_and_tutorial(self): + runner = CliRunner() + runner.invoke(mapswipe_workers.run_create_tutorials, catch_exceptions=False) + runner.invoke(mapswipe_workers.run_create_projects, catch_exceptions=False) + + fb_db = auth.firebaseDB() + ref = fb_db.reference(f"/v2/projects/{self.project_id}") + result = ref.get() + self.assertEqual(result["tutorialId"], self.tutorial_id) + + ref = fb_db.reference(f"/v2/projects/{self.tutorial_id}") + result = ref.get(shallow=True) + self.assertIsNotNone(result) + + +if __name__ == "__main__": + unittest.main() diff --git a/mapswipe_workers/tests/unittests/test_tutorial.py b/mapswipe_workers/tests/unittests/test_tutorial.py deleted file mode 100644 index 5ba1c209a..000000000 --- a/mapswipe_workers/tests/unittests/test_tutorial.py +++ /dev/null @@ -1,27 +0,0 @@ -import os -import unittest - -from mapswipe_workers.project_types import ClassificationTutorial -from tests.fixtures import FIXTURE_DIR, get_fixture - - -class TestTutorial(unittest.TestCase): - def test_init_tile_classification_project(self): - tutorial_draft = get_fixture( - os.path.join(FIXTURE_DIR, "tutorialDrafts", "tile_classification.json") - ) - self.assertIsNotNone(ClassificationTutorial(tutorial_draft=tutorial_draft)) - - def test_create_tile_classification_tasks(self): - tutorial_draft = get_fixture( - os.path.join(FIXTURE_DIR, "tutorialDrafts", "tile_classification.json") - ) - tutorial = ClassificationTutorial(tutorial_draft=tutorial_draft) - tutorial.create_tutorial_groups() - tutorial.create_tutorial_tasks() - self.assertTrue(tutorial.groups) - self.assertTrue(tutorial.tasks) - - -if __name__ == "__main__": - unittest.main() diff --git a/mapswipe_workers/tests/unittests/test_tutorial_street.py b/mapswipe_workers/tests/unittests/test_tutorial_street.py new file mode 100644 index 000000000..6dd9b0127 --- /dev/null +++ b/mapswipe_workers/tests/unittests/test_tutorial_street.py @@ -0,0 +1,27 @@ +import os +import unittest + +from mapswipe_workers.project_types import StreetTutorial +from tests.fixtures import FIXTURE_DIR, get_fixture + + +class TestTutorial(unittest.TestCase): + def test_init_street_tutorial(self): + tutorial_draft = get_fixture( + os.path.join(FIXTURE_DIR, "tutorialDrafts", "street.json") + ) + self.assertIsNotNone(StreetTutorial(tutorial_draft=tutorial_draft)) + + def test_create_street_tasks(self): + tutorial_draft = get_fixture( + os.path.join(FIXTURE_DIR, "tutorialDrafts", "street.json") + ) + tutorial = StreetTutorial(tutorial_draft=tutorial_draft) + tutorial.create_tutorial_groups() + tutorial.create_tutorial_tasks() + self.assertTrue(tutorial.groups) + self.assertTrue(tutorial.tasks) + + +if __name__ == "__main__": + unittest.main()