diff --git a/panoptes_client/set_member_subject.py b/panoptes_client/set_member_subject.py index 298e52f..c7ab2db 100644 --- a/panoptes_client/set_member_subject.py +++ b/panoptes_client/set_member_subject.py @@ -4,7 +4,9 @@ class SetMemberSubject(PanoptesObject): _api_slug = 'set_member_subjects' _link_slug = 'set_member_subjects' - _edit_attributes = () + _edit_attributes = ( + 'priority', + ) LinkResolver.register(SetMemberSubject) LinkResolver.register(SetMemberSubject, 'set_member_subject') diff --git a/panoptes_client/subject.py b/panoptes_client/subject.py index db15562..3fa5be4 100644 --- a/panoptes_client/subject.py +++ b/panoptes_client/subject.py @@ -1,4 +1,20 @@ +from redo import retry +from panoptes_client.panoptes import ( + LinkResolver, + ObjectNotSavedException, + Panoptes, + PanoptesAPIException, + PanoptesObject, +) +import mimetypes +from concurrent.futures import ThreadPoolExecutor +from copy import deepcopy +import threading +import requests +import logging +from builtins import str from panoptes_client.subject_workflow_status import SubjectWorkflowStatus +from panoptes_client.set_member_subject import SetMemberSubject _OLD_STR_TYPES = (str,) try: @@ -6,16 +22,6 @@ except NameError: pass -from builtins import range, str - -import logging -import requests -import threading -import time - -from copy import deepcopy -from concurrent.futures import ThreadPoolExecutor -import mimetypes try: import magic @@ -32,14 +38,6 @@ pass MEDIA_TYPE_DETECTION = 'mimetypes' -from panoptes_client.panoptes import ( - LinkResolver, - ObjectNotSavedException, - Panoptes, - PanoptesAPIException, - PanoptesObject, -) -from redo import retry UPLOAD_RETRY_LIMIT = 5 RETRY_BACKOFF_INTERVAL = 5 @@ -58,6 +56,7 @@ "application/json", ] + class Subject(PanoptesObject): _api_slug = 'subjects' _link_slug = 'subjects' @@ -232,7 +231,8 @@ def _detect_media_type(self, media_data=None, manual_mimetype=None): def _validate_media_type(self, media_type=None): if media_type not in ALLOWED_MIME_TYPES: - raise UnknownMediaException(f"File type {media_type} is not allowed.") + raise UnknownMediaException( + f"File type {media_type} is not allowed.") @property def async_save_result(self): @@ -341,7 +341,8 @@ def _add_attached_image( client = Panoptes.client() with client: - json_response, _ = self.http_post('{}/attached_images'.format(self.id), json={'media': media_data}) + json_response, _ = self.http_post( + '{}/attached_images'.format(self.id), json={'media': media_data}) return json_response['media'][0]['src'] @@ -369,7 +370,8 @@ def _save_attached_image(self, attached_media, manual_mimetype=None, metadata=No media_type = None try: media_data = f.read() - media_type = self._detect_media_type(media_data, manual_mimetype) + media_type = self._detect_media_type( + media_data, manual_mimetype) self._validate_media_type(media_type) finally: f.close() @@ -444,7 +446,8 @@ def save_attached_image( if async_save: upload_exec = self._local.save_exec else: - upload_exec = ThreadPoolExecutor(max_workers=ASYNC_SAVE_THREADS) + upload_exec = ThreadPoolExecutor( + max_workers=ASYNC_SAVE_THREADS) future_result = upload_exec.submit( retry, self._save_attached_image, @@ -467,6 +470,38 @@ def save_attached_image( upload_exec.shutdown(wait=True) return future_result + def update_priority(self, priority, subject_set_id=None): + """ + Update the priority of this subject in the subject set. + + If subject_set_id is not provided, the priority will be updated in all subject sets that this subject belongs to. + + - **priority** is an integer value that represents the priority of the subject in the subject set. + + Examples:: + + subject.update_priority(1) + subject.update_priority(2, subject_set_id=1234) + """ + + if self.id is None: + raise ObjectNotSavedException + + self.metadata['priority'] = priority + self.save() + + if subject_set_id is not None: + subject_sets = [subject_set_id] + else: + subject_sets = [s.id for s in self.links.subject_sets] + + for ss_id in subject_sets: + sms = next(SetMemberSubject.where( + subject_set_id=ss_id, + subject_id=self.id)) + sms.priority = priority + sms.save() + class UnknownMediaException(Exception): pass diff --git a/panoptes_client/tests/test_subject.py b/panoptes_client/tests/test_subject.py index 15b5e83..b2ffe5a 100644 --- a/panoptes_client/tests/test_subject.py +++ b/panoptes_client/tests/test_subject.py @@ -1,9 +1,10 @@ import io +import mimetypes import unittest -from unittest.mock import patch, mock_open +from unittest.mock import patch, mock_open, MagicMock +from panoptes_client.panoptes import ObjectNotSavedException from panoptes_client.subject import Subject, UnknownMediaException -import mimetypes class TestSubject(unittest.TestCase): @@ -56,4 +57,76 @@ def test_add_location_invalid_manual_mimetype(self): data = b"fake data" fake_file = io.BytesIO(data) with self.assertRaises(UnknownMediaException): - self.subject.add_location(fake_file, manual_mimetype="application/javascript") \ No newline at end of file + self.subject.add_location( + fake_file, manual_mimetype="application/javascript") + + def test_update_priority_requires_saved_subject(self): + with self.assertRaises(ObjectNotSavedException): + self.subject.update_priority(1) + + def test_update_priority_updates_priority_for_saved_subject(self): + self.subject.id = 123 + set_member_subject_mock = MagicMock() + + with patch.object(self.subject, "save") as mock_save: + with patch( + "panoptes_client.subject.SetMemberSubject.where", + return_value=iter([set_member_subject_mock]), + ) as mock_where: + self.subject.update_priority( + 5, + subject_set_id=456, + ) + + self.assertEqual(self.subject.metadata["priority"], 5) + + mock_save.assert_called_once_with() + mock_where.assert_called_once_with( + subject_set_id=456, + subject_id=123, + ) + + self.assertEqual(set_member_subject_mock.priority, 5) + set_member_subject_mock.save.assert_called_once_with() + + def test_update_priority_updates_all_subject_sets(self): + self.subject.id = 123 + + subject_set_1 = MagicMock(id=456) + subject_set_2 = MagicMock(id=789) + set_member_subject_1 = MagicMock() + set_member_subject_2 = MagicMock() + + with patch.object(self.subject, "save") as mock_save, \ + patch( + "panoptes_client.panoptes.LinkResolver.__getattr__", + return_value=[subject_set_1, subject_set_2], + ), \ + patch( + "panoptes_client.subject.SetMemberSubject.where", + side_effect=[ + iter([set_member_subject_1]), + iter([set_member_subject_2]), + ], + ) as mock_where: + self.subject.update_priority(5) + + self.assertEqual(self.subject.metadata["priority"], 5) + + mock_save.assert_called_once_with() + + self.assertEqual(mock_where.call_count, 2) + mock_where.assert_any_call( + subject_set_id=456, + subject_id=123, + ) + mock_where.assert_any_call( + subject_set_id=789, + subject_id=123, + ) + + self.assertEqual(set_member_subject_1.priority, 5) + set_member_subject_1.save.assert_called_once_with() + + self.assertEqual(set_member_subject_2.priority, 5) + set_member_subject_2.save.assert_called_once_with()