From 82ddeb69a727d5afaa4888e7a8d3a5f9e2cca93d Mon Sep 17 00:00:00 2001 From: Anler Hp Date: Fri, 11 Jul 2014 01:02:52 +0200 Subject: [PATCH] USs: class-based to function-based services --- taiga/base/utils/db.py | 32 +++++++++++++ taiga/base/utils/text.py | 5 +++ taiga/projects/userstories/api.py | 8 ++-- taiga/projects/userstories/services.py | 62 ++++++++++++++------------ tests/integration/test_userstories.py | 39 ++++++++++++++++ tests/unit/test_utils.py | 40 ++++++++++++++++- 6 files changed, 151 insertions(+), 35 deletions(-) create mode 100644 tests/integration/test_userstories.py diff --git a/taiga/base/utils/db.py b/taiga/base/utils/db.py index be029ba0..aa629495 100644 --- a/taiga/base/utils/db.py +++ b/taiga/base/utils/db.py @@ -46,6 +46,7 @@ def reload_attribute(model_instance, attr_name): def save_in_bulk(instances, callback=None, **save_options): """Save a list of model instances. + :params instances: List of model instances. :params callback: Callback to call after each save. :params save_options: Additional options to use when saving each instance. """ @@ -55,3 +56,34 @@ def save_in_bulk(instances, callback=None, **save_options): for instance in instances: instance.save(**save_options) callback(instance) + + +@transaction.atomic +def update_in_bulk(instances, list_of_new_values, callback=None): + """Update a list of model instances. + + :params instances: List of model instances. + :params new_values: List of dicts where each dict is the new data corresponding to the instance + in the same index position as the dict. + """ + if callback is None: + callback = functions.identity + + for instance, new_values in zip(instances, list_of_new_values): + for attribute, value in new_values.items(): + setattr(instance, attribute, value) + instance.save() + callback(instance) + + +@transaction.atomic +def update_in_bulk_with_ids(ids, list_of_new_values, model): + """Update a table using a list of ids. + + :params ids: List of ids. + :params new_values: List of dicts or duples where each dict/duple is the new data corresponding + to the instance in the same index position as the dict. + :param model: Model of the ids. + """ + for id, new_values in zip(ids, list_of_new_values): + model.objects.filter(id=id).update(**new_values) diff --git a/taiga/base/utils/text.py b/taiga/base/utils/text.py index c1d8709b..df9b0259 100644 --- a/taiga/base/utils/text.py +++ b/taiga/base/utils/text.py @@ -25,3 +25,8 @@ def strip_lines(text): output = output.replace("\n", " ") return output.strip() + + +def split_in_lines(text): + """Split a block of text in lines removing unnecessary spaces from each line.""" + return (line for line in map(str.strip, text.split("\n")) if line) diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index a162fecc..20c7a90d 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -79,9 +79,8 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi if request.user != project.owner and not has_project_perm(request.user, project, 'add_userstory'): raise exc.PermissionDenied(_("You don't have permisions to create user stories.")) - service = services.UserStoriesService() - user_stories = service.bulk_insert(project, request.user, bulk_stories, - callback_on_success=self.post_save) + user_stories = services.create_userstories_in_bulk( + bulk_stories, callback=self.post_save, project=project, owner=request.user) user_stories_serialized = self.serializer_class(user_stories, many=True) return Response(data=user_stories_serialized.data) @@ -107,8 +106,7 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi if request.user != project.owner and not has_project_perm(request.user, project, 'change_userstory'): raise exc.PermissionDenied(_("You don't have permisions to create user stories.")) - service = services.UserStoriesService() - service.bulk_update_order(project, request.user, bulk_stories) + services.update_userstories_order_in_bulk(bulk_stories) return Response(data=None, status=status.HTTP_204_NO_CONTENT) diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index de1693da..1aa3ee45 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -14,43 +14,47 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.db import transaction -from django.db import connection +from taiga.base.utils import db, text from . import models -class UserStoriesService(object): - @transaction.atomic - def bulk_insert(self, project, user, data, callback_on_success=None): - user_stories = [] +def get_userstories_from_bulk(bulk_data, **additional_fields): + """Convert `bulk_data` into a list of user stories. - items = filter(lambda s: len(s) > 0, - map(lambda s: s.strip(), data.split("\n"))) + :param bulk_data: List of user stories in bulk format. + :param additional_fields: Additional fields when instantiating each user story. - for item in items: - obj = models.UserStory.objects.create(subject=item, project=project, owner=user, - status=project.default_us_status) - user_stories.append(obj) + :return: List of `UserStory` instances. + """ + return [models.UserStory(subject=line, **additional_fields) + for line in text.split_in_lines(bulk_data)] - if callback_on_success: - callback_on_success(obj, True) - return user_stories +def create_userstories_in_bulk(bulk_data, callback=None, **additional_fields): + """Create user stories from `bulk_data`. - @transaction.atomic - def bulk_update_order(self, project, user, data): - # TODO: Create a history snapshot of all updated USs - cursor = connection.cursor() + :param bulk_data: List of user stories in bulk format. + :param callback: Callback to execute after each user story save. + :param additional_fields: Additional fields when instantiating each user story. - sql = """ - prepare bulk_update_order as update userstories_userstory set "order" = $1 - where userstories_userstory.id = $2 and - userstories_userstory.project_id = $3; - """ + :return: List of created `Task` instances. + """ + userstories = get_userstories_from_bulk(bulk_data, **additional_fields) + db.save_in_bulk(userstories, callback) + return userstories - cursor.execute(sql) - for usid, usorder in data: - cursor.execute("EXECUTE bulk_update_order (%s, %s, %s);", - (usorder, usid, project.id)) - cursor.close() + +def update_userstories_order_in_bulk(bulk_data): + """Update the order of some user stories. + + `bulk_data` should be a list of tuples with the following format: + + [(, ), ...] + """ + user_story_ids = [] + new_order_values = [] + for user_story_id, new_order_value in bulk_data: + user_story_ids.append(user_story_id) + new_order_values.append({"order": new_order_value}) + db.update_in_bulk_with_ids(user_story_ids, new_order_values, model=models.UserStory) diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py new file mode 100644 index 00000000..1c8d3537 --- /dev/null +++ b/tests/integration/test_userstories.py @@ -0,0 +1,39 @@ +from unittest import mock + +import pytest + +from taiga.projects.userstories import services, models + +pytestmark = pytest.mark.django_db + + +def test_get_userstories_from_bulk(): + data = """ +User Story #1 +User Story #2 +""" + userstories = services.get_userstories_from_bulk(data) + + assert len(userstories) == 2 + assert userstories[0].subject == "User Story #1" + assert userstories[1].subject == "User Story #2" + + +@mock.patch("taiga.projects.userstories.services.db") +def test_create_userstories_in_bulk(db): + data = """ +User Story #1 +User Story #2 +""" + userstories = services.create_userstories_in_bulk(data) + + db.save_in_bulk.assert_called_once_with(userstories, None) + + +@mock.patch("taiga.projects.userstories.services.db") +def test_update_userstories_order_in_bulk(db): + data = [(1, 1), (2, 2)] + services.update_userstories_order_in_bulk(data) + + db.update_in_bulk_with_ids.assert_called_once_with([1, 2], [{"order": 1}, {"order": 2}], + model=models.UserStory) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index b6b871aa..48e279b8 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -3,7 +3,7 @@ from unittest import mock import django_sites as sites from taiga.base.utils.urls import get_absolute_url, is_absolute_url, build_url -from taiga.base.utils.db import save_in_bulk +from taiga.base.utils.db import save_in_bulk, update_in_bulk, update_in_bulk_with_ids def test_is_absolute_url(): @@ -35,3 +35,41 @@ def test_save_in_bulk_with_a_callback(): save_in_bulk(instances, callback) assert callback.call_count == 2 + + +def test_update_in_bulk(): + instance = mock.Mock() + instances = [instance, instance] + new_values = [{"field1": 1}, {"field2": 2}] + + update_in_bulk(instances, new_values) + + assert instance.save.call_count == 2 + assert instance.field1 == 1 + assert instance.field2 == 2 + + +def test_update_in_bulk_with_a_callback(): + instance = mock.Mock() + callback = mock.Mock() + instances = [instance, instance] + new_values = [{"field1": 1}, {"field2": 2}] + + update_in_bulk(instances, new_values, callback) + + assert callback.call_count == 2 + + +def test_update_in_bulk_with_ids(): + ids = [1, 2] + new_values = [{"field1": 1}, {"field2": 2}] + model = mock.Mock() + + update_in_bulk_with_ids(ids, new_values, model) + + expected_calls = [ + mock.call(id=1), mock.call().update(field1=1), + mock.call(id=2), mock.call().update(field2=2) + ] + + model.objects.filter.assert_has_calls(expected_calls)