diff --git a/docs/index.rst b/docs/index.rst index 6547c3e..4e29780 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -90,3 +90,9 @@ MapRoulette Task .. automodule:: maproulette.task :members: + +A Task Collection +================= + +.. automodule:: maproulette.taskcollection + :members: diff --git a/maproulette/challenge.py b/maproulette/challenge.py index 7a7391b..04314fd 100644 --- a/maproulette/challenge.py +++ b/maproulette/challenge.py @@ -1,13 +1,11 @@ #!/usr/bin/env python """ -Describes the maproulette challenge +A challenge for MapRoulette. """ class MapRouletteChallenge(object): """ - A MapRoulette challenge. - Typical usage:: challenge = MapRouletteChallenge( diff --git a/maproulette/server.py b/maproulette/server.py index 8248fd7..1148573 100644 --- a/maproulette/server.py +++ b/maproulette/server.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -A MapRoulette Server +A MapRoulette server. """ import requests @@ -9,16 +9,14 @@ class MapRouletteServer(object): """ - A MapRoulette server. - - Typical usage:: + Typical usage:: server = MapRouletteServer( url='http://dev.maproulette.org/api') :param url: The URL for the MapRoulette API server you want to use :type url: String - :rtype: A :class:`MapRouletteServer` + :rtype: A :py:class:`MapRouletteServer` """ ENDPOINTS = { diff --git a/maproulette/task.py b/maproulette/task.py index a42a237..577ad45 100644 --- a/maproulette/task.py +++ b/maproulette/task.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -MapRoulette Tasks +A task for MapRoulette. """ import json @@ -9,8 +9,6 @@ class MapRouletteTask(object): """ - A task for MapRoulette - Typical usage:: task = MapRouletteTask( diff --git a/maproulette/taskcollection.py b/maproulette/taskcollection.py index 52fd89f..b55be65 100644 --- a/maproulette/taskcollection.py +++ b/maproulette/taskcollection.py @@ -1,14 +1,36 @@ #!/usr/bin/env python """ -A collection of Tasks +A collection of tasks for MapRoulette. +This is not a native MapRoulette object, but rather a convenience object +to leverage the bulk insert / update calls in the MapRoulette API. The +MapRouletteTaskCollection class contains one notable method that is not native +to the MapRoulette API: :py:func:`.reconcile`, which reconciles a task collection +with the corresponding challenge on the server. """ from .challenge import MapRouletteChallenge from .task import MapRouletteTask class MapRouletteTaskCollection(object): - """A collection of tasks for MapRoulette.""" + """ + Typical usage:: + + task = MapRouletteTask( + challenge=challenge_obj, + identifier=identifier, + geometries=geometries) + task.create(server_instance) + + :param challenge: An instance of MapRouletteChallenge + :param identifer: A valid Task identifer + :param geometries: One or more geometries serialized as a GeoJSON FeatureCollection + :type geometries: FeatureCollection + :param instruction: A task-level instruction + :param status: The task's initial status (defaults to 'created' in MapRoulette) + + + """ MAX_TASKS = 5000 tasks = None @@ -47,28 +69,49 @@ def reconcile(self, server): """ if not self.challenge.exists(server): raise Exception('Challenge does not exist on server') + existing = MapRouletteTaskCollection.from_server(server, self.challenge) + same = [] new = [] changed = [] deleted = [] + + # reconcile the new tasks with the existing tasks: for task in self.tasks: + # if the task exists on the server... if task.identifier in [existing_task.identifier for existing_task in existing.tasks]: + # and they are equal... if task == existing.get_by_identifier(task.identifier): + # add to 'same' list same.append(task) + # if they are not equal, add to 'changed' list else: changed.append(task) + # if the task does not exist on the server, add to 'new' list else: new.append(task) + + # next, check for tasks on the server that don't exist in the new collection... for task in existing.tasks: if task.identifier not in [task.identifier for task in self.tasks]: + # ... and add those to the 'deleted' list. deleted.append(task) - print '\nsame: {same}\nnew: {new}\nchanged: {changed}\ndeleted: {deleted}'.format( - same=len(same), - new=len(new), - changed=len(changed), - deleted=len(deleted)) + # update the server with new, changed, and deleted tasks + if new: + newCollection = MapRouletteTaskCollection(self.challenge, tasks=new) + newCollection.create(server) + if changed: + changedCollection = MapRouletteTaskCollection(self.challenge, tasks=changed) + changedCollection.update(server) + if deleted: + deletedCollection = MapRouletteTaskCollection(self.challenge, tasks=deleted) + for task in deletedCollection.tasks: + task.status = 'deleted' + deletedCollection.update(server) + # return same, new, changed and deleted tasks + return {'same': same, 'new': new, 'changed': changed, 'deleted': deleted} def add(self, server): """Add task colleciton to the Collection.""" diff --git a/run_tests.py b/run_tests.py index e9e17b9..b593bbc 100644 --- a/run_tests.py +++ b/run_tests.py @@ -11,6 +11,7 @@ class APITests(unittest.TestCase): + # how much is A_TON? A_TON = 100 test_challenge_slug = 'test-{}'.format(uuid.uuid4()) @@ -19,9 +20,15 @@ class APITests(unittest.TestCase): server = MapRouletteServer(url=test_server_url) def test_001_init(self): + """ + Assert that the server is indeed alive. + """ self.assertTrue(isinstance(self.server, MapRouletteServer)) def test_002_challenges(self): + """ + Assert that the server returns a list of challenges + """ challenges = self.server.challenges() self.assertTrue(isinstance(challenges, list)) @@ -85,9 +92,19 @@ def test_009_retrieve_taskcollection_from_server(self): # We already created 1 task in test 006, then A_TON more in test 008 def test_010_reconcile_task_collections(self): + """ + In this test case, we will reconcile a task collection with an + existing one on the server (created in 008). + Compared to the existing task collection, we will remove one task, + add one task, and change one task. + """ + + # get the challenge from server challenge = MapRouletteChallenge.from_server( self.server, self.test_challenge_slug) + # get the task collection to reconcile, start out with the + # existing one on the server task_collection = MapRouletteTaskCollection.from_server( self.server, challenge) @@ -101,14 +118,28 @@ def test_010_reconcile_task_collections(self): geometries=self.__random_point())) # and finally change one task so it appears 'updated' task_collection.tasks[0].geometries = self.__random_point() - task_collection.tasks[0].status = 'updated' - task_collection.reconcile(self.server) + task_collection.tasks[0].status = 'changed' + + # reconcile the two collections + result = task_collection.reconcile(self.server) + + # assert that we indeed have one new, one changed and one deleted task. + self.assertTrue(len(result['new']) == 1) + self.assertTrue(len(result['changed']) == 1) + self.assertTrue(len(result['deleted']) == 1) def __random_point(self): + """ + return a random geographic Point, wrapped in a Feature, wrapped in a + FeatureCollection. It's like the Turducken of geometries. + """ return FeatureCollection([ Feature(geometry=Point((random(), random())))]) def __create_task_collection(self, challenge): + """ + Return a collection of A_TON of tasks with random Point geometries + """ task_collection = MapRouletteTaskCollection(challenge) i = 0 while i < self.A_TON: