Merge "Allow the creation of private stories"

This commit is contained in:
Jenkins 2016-05-10 14:04:27 +00:00 committed by Gerrit Code Review
commit 3779bacc1b
14 changed files with 550 additions and 128 deletions

View File

@ -242,14 +242,16 @@ class DueDatesController(rest.RestController):
tasks = due_date_dict.pop('tasks') tasks = due_date_dict.pop('tasks')
db_tasks = [] db_tasks = []
for task in tasks: for task in tasks:
db_tasks.append(tasks_api.task_get(task.id)) db_tasks.append(tasks_api.task_get(
task.id, current_user=request.current_user_id))
due_date_dict['tasks'] = db_tasks due_date_dict['tasks'] = db_tasks
if 'stories' in due_date_dict: if 'stories' in due_date_dict:
stories = due_date_dict.pop('stories') stories = due_date_dict.pop('stories')
db_stories = [] db_stories = []
for story in stories: for story in stories:
db_stories.append(stories_api.story_get_simple(story.id)) db_stories.append(stories_api.story_get_simple(
story.id, current_user=request.current_user_id))
due_date_dict['stories'] = db_stories due_date_dict['stories'] = db_stories
board = None board = None

View File

@ -13,7 +13,9 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from sqlalchemy import and_, or_
from sqlalchemy.orm import subqueryload from sqlalchemy.orm import subqueryload
from sqlalchemy.sql.expression import false, true
from sqlalchemy_fulltext import FullTextSearch from sqlalchemy_fulltext import FullTextSearch
import sqlalchemy_fulltext.modes as FullTextMode import sqlalchemy_fulltext.modes as FullTextMode
@ -54,10 +56,29 @@ class SqlAlchemySearchImpl(search_engine.SearchEngine):
return query.all() return query.all()
def stories_query(self, q, marker=None, offset=None, def stories_query(self, q, marker=None, offset=None,
limit=None, **kwargs): limit=None, current_user=None, **kwargs):
session = api_base.get_session() session = api_base.get_session()
subquery = api_base.model_query(models.Story, session) subquery = api_base.model_query(models.Story, session)
# Filter out stories that the current user can't see
subquery = subquery.outerjoin(models.story_permissions,
models.Permission,
models.user_permissions,
models.User)
if current_user is not None:
subquery = subquery.filter(
or_(
and_(
models.User.id == current_user,
models.Story.private == true()
),
models.Story.private == false()
)
)
else:
subquery = subquery.filter(models.Story.private == false())
subquery = self._build_fulltext_search(models.Story, subquery, q) subquery = self._build_fulltext_search(models.Story, subquery, q)
subquery = self._apply_pagination(models.Story, subquery = self._apply_pagination(models.Story,
subquery, marker, offset, limit) subquery, marker, offset, limit)
@ -72,9 +93,30 @@ class SqlAlchemySearchImpl(search_engine.SearchEngine):
stories = query.all() stories = query.all()
return stories return stories
def tasks_query(self, q, marker=None, offset=None, limit=None, **kwargs): def tasks_query(self, q, marker=None, offset=None, limit=None,
current_user=None, **kwargs):
session = api_base.get_session() session = api_base.get_session()
query = api_base.model_query(models.Task, session) query = api_base.model_query(models.Task, session)
# Filter out tasks or stories that the current user can't see
query = query.outerjoin(models.Story,
models.story_permissions,
models.Permission,
models.user_permissions,
models.User)
if current_user is not None:
query = query.filter(
or_(
and_(
models.User.id == current_user,
models.Story.private == true()
),
models.Story.private == false()
)
)
else:
query = query.filter(models.Story.private == false())
query = self._build_fulltext_search(models.Task, query, q) query = self._build_fulltext_search(models.Task, query, q)
query = self._apply_pagination( query = self._apply_pagination(
models.Task, query, marker, offset, limit) models.Task, query, marker, offset, limit)

View File

@ -1,4 +1,5 @@
# Copyright (c) 2013 Mirantis Inc. # Copyright (c) 2013 Mirantis Inc.
# Copyright (c) 2016 Codethink Ltd.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -35,6 +36,7 @@ from storyboard.common import decorators
from storyboard.common import exception as exc from storyboard.common import exception as exc
from storyboard.db.api import stories as stories_api from storyboard.db.api import stories as stories_api
from storyboard.db.api import timeline_events as events_api from storyboard.db.api import timeline_events as events_api
from storyboard.db.api import users as users_api
from storyboard.openstack.common.gettextutils import _ # noqa from storyboard.openstack.common.gettextutils import _ # noqa
@ -46,6 +48,10 @@ SEARCH_ENGINE = search_engine.get_engine()
def create_story_wmodel(story): def create_story_wmodel(story):
story_model = wmodels.Story.from_db_model(story) story_model = wmodels.Story.from_db_model(story)
story_model.summarize_task_statuses(story) story_model.summarize_task_statuses(story)
if story.permissions:
story_model.resolve_users(story)
else:
story_model.users = []
return story_model return story_model
@ -65,7 +71,8 @@ class StoriesController(rest.RestController):
:param story_id: An ID of the story. :param story_id: An ID of the story.
""" """
story = stories_api.story_get(story_id) story = stories_api.story_get(
story_id, current_user=request.current_user_id)
if story: if story:
return create_story_wmodel(story) return create_story_wmodel(story)
@ -108,7 +115,8 @@ class StoriesController(rest.RestController):
# Resolve the marker record. # Resolve the marker record.
marker_story = None marker_story = None
if marker: if marker:
marker_story = stories_api.story_get(marker) marker_story = stories_api.story_get(
marker, current_user=request.current_user_id)
stories = stories_api \ stories = stories_api \
.story_get_all(title=title, .story_get_all(title=title,
@ -123,8 +131,10 @@ class StoriesController(rest.RestController):
marker=marker_story, marker=marker_story,
offset=offset, offset=offset,
tags_filter_type=tags_filter_type, tags_filter_type=tags_filter_type,
limit=limit, sort_field=sort_field, limit=limit,
sort_dir=sort_dir) sort_field=sort_field,
sort_dir=sort_dir,
current_user=request.current_user_id)
story_count = stories_api \ story_count = stories_api \
.story_get_count(title=title, .story_get_count(title=title,
description=description, description=description,
@ -135,7 +145,8 @@ class StoriesController(rest.RestController):
project_id=project_id, project_id=project_id,
subscriber_id=subscriber_id, subscriber_id=subscriber_id,
tags=tags, tags=tags,
tags_filter_type=tags_filter_type) tags_filter_type=tags_filter_type,
current_user=request.current_user_id)
# Apply the query response headers. # Apply the query response headers.
if limit: if limit:
@ -181,9 +192,18 @@ class StoriesController(rest.RestController):
if "due_dates" in story_dict: if "due_dates" in story_dict:
del story_dict['due_dates'] del story_dict['due_dates']
users = []
if "users" in story_dict:
users = story_dict.pop("users")
if users is None:
users = [wmodels.User.from_db_model(users_api.user_get(user_id))]
created_story = stories_api.story_create(story_dict) created_story = stories_api.story_create(story_dict)
events_api.story_created_event(created_story.id, user_id, story.title) events_api.story_created_event(created_story.id, user_id, story.title)
if story.private:
stories_api.create_permission(created_story, users)
return wmodels.Story.from_db_model(created_story) return wmodels.Story.from_db_model(created_story)
@decorators.db_exceptions @decorators.db_exceptions
@ -202,7 +222,8 @@ class StoriesController(rest.RestController):
abort(400, _("Now you can't change story type to %s.") % abort(400, _("Now you can't change story type to %s.") %
story.story_type_id) story.story_type_id)
original_story = stories_api.story_get_simple(story_id) original_story = stories_api.story_get_simple(
story_id, current_user=request.current_user_id)
if not original_story: if not original_story:
raise exc.NotFound(_("Story %s not found") % story_id) raise exc.NotFound(_("Story %s not found") % story_id)
@ -224,9 +245,26 @@ class StoriesController(rest.RestController):
if 'tags' in story_dict: if 'tags' in story_dict:
story_dict.pop('tags') story_dict.pop('tags')
users = story_dict.get("users", [])
ids = [user.id for user in users]
if story.private:
if request.current_user_id not in ids \
and not original_story.permissions:
users.append(wmodels.User.from_db_model(
users_api.user_get(request.current_user_id)))
if not original_story.permissions:
stories_api.create_permission(original_story, users)
updated_story = stories_api.story_update( updated_story = stories_api.story_update(
story_id, story_id,
story_dict) story_dict,
current_user=request.current_user_id)
if users == [] and updated_story.private:
abort(400, _("Can't make a private story with no users"))
if story.private:
stories_api.update_permission(updated_story, users)
user_id = request.current_user_id user_id = request.current_user_id
events_api.story_details_changed_event(story_id, user_id, events_api.story_details_changed_event(story_id, user_id,
@ -242,7 +280,8 @@ class StoriesController(rest.RestController):
:param story_id: An ID of the story. :param story_id: An ID of the story.
""" """
stories_api.story_delete(story_id) stories_api.story_delete(
story_id, current_user=request.current_user_id)
comments = CommentsController() comments = CommentsController()
events = TimeLineEventsController() events = TimeLineEventsController()
@ -260,10 +299,12 @@ class StoriesController(rest.RestController):
:return: List of Stories matching the query. :return: List of Stories matching the query.
""" """
user = request.current_user_id
stories = SEARCH_ENGINE.stories_query(q=q, stories = SEARCH_ENGINE.stories_query(q=q,
marker=marker, marker=marker,
offset=offset, offset=offset,
limit=limit) limit=limit,
current_user=user)
return [create_story_wmodel(story) for story in stories] return [create_story_wmodel(story) for story in stories]

View File

@ -158,7 +158,8 @@ class SubscriptionsController(rest.RestController):
# Data sanity check: The resource must exist. # Data sanity check: The resource must exist.
resource = subscription_api.subscription_get_resource( resource = subscription_api.subscription_get_resource(
target_type=subscription.target_type, target_type=subscription.target_type,
target_id=subscription.target_id) target_id=subscription.target_id,
current_user=request.current_user_id)
if not resource: if not resource:
abort(400, _('You cannot subscribe to a nonexistent resource.')) abort(400, _('You cannot subscribe to a nonexistent resource.'))

View File

@ -63,7 +63,8 @@ class TagsController(rest.RestController):
return [wmodels.Tag.from_db_model(t) for t in tags] return [wmodels.Tag.from_db_model(t) for t in tags]
story = stories_api.story_get(story_id) story = stories_api.story_get(
story_id, current_user=request.current_user_id)
if not story: if not story:
raise exc.NotFound("Story %s not found" % story_id) raise exc.NotFound("Story %s not found" % story_id)
@ -78,16 +79,19 @@ class TagsController(rest.RestController):
:param tags: A list of tags to be added. :param tags: A list of tags to be added.
""" """
story = stories_api.story_get(story_id) story = stories_api.story_get(
story_id, current_user=request.current_user_id)
if not story: if not story:
raise exc.NotFound("Story %s not found" % story_id) raise exc.NotFound("Story %s not found" % story_id)
for tag in tags: for tag in tags:
stories_api.story_add_tag(story_id, tag) stories_api.story_add_tag(
story_id, tag, current_user=request.current_user_id)
# For some reason the story gets cached and the tags do not appear. # For some reason the story gets cached and the tags do not appear.
stories_api.api_base.get_session().expunge(story) stories_api.api_base.get_session().expunge(story)
story = stories_api.story_get(story_id) story = stories_api.story_get(
story_id, current_user=request.current_user_id)
events_api.tags_added_event(story_id=story_id, events_api.tags_added_event(story_id=story_id,
author_id=request.current_user_id, author_id=request.current_user_id,
story_title=story.title, story_title=story.title,
@ -115,12 +119,14 @@ class TagsController(rest.RestController):
:param tags: A list of tags to be removed. :param tags: A list of tags to be removed.
""" """
story = stories_api.story_get(story_id) story = stories_api.story_get(
story_id, current_user=request.current_user_id)
if not story: if not story:
raise exc.NotFound("Story %s not found" % story_id) raise exc.NotFound("Story %s not found" % story_id)
for tag in tags: for tag in tags:
stories_api.story_remove_tag(story_id, tag) stories_api.story_remove_tag(
story_id, tag, current_user=request.current_user_id)
events_api.tags_deleted_event(story_id=story_id, events_api.tags_deleted_event(story_id=story_id,
author_id=request.current_user_id, author_id=request.current_user_id,

View File

@ -90,7 +90,8 @@ def story_is_valid(task, branch):
"""Check that branch is restricted if story type is restricted. """Check that branch is restricted if story type is restricted.
""" """
story = stories_api.story_get(task.story_id) story = stories_api.story_get(
task.story_id, current_user=request.current_user_id)
if not story: if not story:
raise exc.NotFound("Story %s not found." % task.story_id) raise exc.NotFound("Story %s not found." % task.story_id)
@ -260,7 +261,8 @@ class TasksPrimaryController(rest.RestController):
:param task_id: An ID of the task. :param task_id: An ID of the task.
""" """
task = tasks_api.task_get(task_id) task = tasks_api.task_get(
task_id, current_user=request.current_user_id)
if task: if task:
return wmodels.Task.from_db_model(task) return wmodels.Task.from_db_model(task)
@ -314,7 +316,8 @@ class TasksPrimaryController(rest.RestController):
sort_field=sort_field, sort_field=sort_field,
sort_dir=sort_dir, sort_dir=sort_dir,
marker=marker_task, marker=marker_task,
limit=limit) limit=limit,
current_user=request.current_user_id)
task_count = tasks_api \ task_count = tasks_api \
.task_get_count(title=title, .task_get_count(title=title,
link=link, link=link,
@ -325,7 +328,8 @@ class TasksPrimaryController(rest.RestController):
branch_id=branch_id, branch_id=branch_id,
milestone_id=milestone_id, milestone_id=milestone_id,
status=status, status=status,
priority=priority) priority=priority,
current_user=request.current_user_id)
# Apply the query response headers. # Apply the query response headers.
if limit: if limit:
@ -374,7 +378,8 @@ class TasksPrimaryController(rest.RestController):
:param task: A task within the request body. :param task: A task within the request body.
""" """
original_task = copy.deepcopy(tasks_api.task_get(task_id)) original_task = copy.deepcopy(
tasks_api.task_get(task_id, current_user=request.current_user_id))
if not original_task: if not original_task:
raise exc.NotFound(_("Task %s not found.") % task_id) raise exc.NotFound(_("Task %s not found.") % task_id)
@ -395,7 +400,8 @@ class TasksPrimaryController(rest.RestController):
:param task_id: An ID of the task. :param task_id: An ID of the task.
""" """
original_task = copy.deepcopy(tasks_api.task_get(task_id)) original_task = copy.deepcopy(
tasks_api.task_get(task_id, current_user=request.current_user_id))
if not original_task: if not original_task:
raise exc.NotFound(_("Task %s not found.") % task_id) raise exc.NotFound(_("Task %s not found.") % task_id)
@ -419,10 +425,12 @@ class TasksPrimaryController(rest.RestController):
:return: List of Tasks matching the query. :return: List of Tasks matching the query.
""" """
user = request.current_user_id
tasks = SEARCH_ENGINE.tasks_query(q=q, tasks = SEARCH_ENGINE.tasks_query(q=q,
marker=marker, marker=marker,
offset=offset, offset=offset,
limit=limit) limit=limit,
current_user=user)
return [wmodels.Task.from_db_model(task) for task in tasks] return [wmodels.Task.from_db_model(task) for task in tasks]
@ -442,7 +450,8 @@ class TasksNestedController(rest.RestController):
:param story_id: An ID of the story. :param story_id: An ID of the story.
:param task_id: An ID of the task. :param task_id: An ID of the task.
""" """
task = tasks_api.task_get(task_id) task = tasks_api.task_get(
task_id, current_user=request.current_user_id)
if task: if task:
if task.story_id != story_id: if task.story_id != story_id:
@ -482,7 +491,8 @@ class TasksNestedController(rest.RestController):
limit = max(0, limit) limit = max(0, limit)
# Resolve the marker record. # Resolve the marker record.
marker_task = tasks_api.task_get(marker) marker_task = tasks_api.task_get(
marker, current_user=request.current_user_id)
tasks = tasks_api \ tasks = tasks_api \
.task_get_all(title=title, .task_get_all(title=title,
@ -498,7 +508,8 @@ class TasksNestedController(rest.RestController):
sort_field=sort_field, sort_field=sort_field,
sort_dir=sort_dir, sort_dir=sort_dir,
marker=marker_task, marker=marker_task,
limit=limit) limit=limit,
current_user=request.current_user_id)
task_count = tasks_api \ task_count = tasks_api \
.task_get_count(title=title, .task_get_count(title=title,
link=link, link=link,
@ -509,7 +520,8 @@ class TasksNestedController(rest.RestController):
branch_id=branch_id, branch_id=branch_id,
milestone_id=milestone_id, milestone_id=milestone_id,
status=status, status=status,
priority=priority) priority=priority,
current_user=request.current_user_id)
# Apply the query response headers. # Apply the query response headers.
response.headers['X-Limit'] = str(limit) response.headers['X-Limit'] = str(limit)
@ -565,7 +577,8 @@ class TasksNestedController(rest.RestController):
:param task: a task within the request body. :param task: a task within the request body.
""" """
original_task = copy.deepcopy(tasks_api.task_get(task_id)) original_task = copy.deepcopy(
tasks_api.task_get(task_id, current_user=request.current_user_id))
if not original_task: if not original_task:
raise exc.NotFound(_("Task %s not found") % task_id) raise exc.NotFound(_("Task %s not found") % task_id)
@ -591,7 +604,8 @@ class TasksNestedController(rest.RestController):
:param task_id: An ID of the task. :param task_id: An ID of the task.
""" """
original_task = copy.deepcopy(tasks_api.task_get(task_id)) original_task = copy.deepcopy(
tasks_api.task_get(task_id, current_user=request.current_user_id))
if not original_task: if not original_task:
raise exc.NotFound(_("Task %s not found.") % task_id) raise exc.NotFound(_("Task %s not found.") % task_id)

View File

@ -1,4 +1,5 @@
# Copyright (c) 2014 Mirantis Inc. # Copyright (c) 2014 Mirantis Inc.
# Copyright (c) 2016 Codethink Ltd
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -125,6 +126,39 @@ class TaskStatusCount(base.APIBase):
count = int count = int
class User(base.APIBase):
"""Represents a user."""
full_name = wtypes.text
"""Full (Display) name."""
openid = wtypes.text
"""The unique identifier, returned by an OpneId provider"""
email = wtypes.text
"""Email Address."""
# Todo(nkonovalov): use teams to define superusers
is_superuser = bool
last_login = datetime
"""Date of the last login."""
enable_login = bool
"""Whether this user is permitted to log in."""
@classmethod
def sample(cls):
return cls(
full_name="Bart Simpson",
openid="https://login.launchpad.net/+id/Abacaba",
email="skinnerstinks@springfield.net",
is_staff=False,
is_active=True,
is_superuser=True,
last_login=datetime(2014, 1, 1, 16, 42))
class Story(base.APIBase): class Story(base.APIBase):
"""The Story is the main element of StoryBoard. It represents a user story """The Story is the main element of StoryBoard. It represents a user story
(generally a bugfix or a feature) that needs to be implemented. It will be (generally a bugfix or a feature) that needs to be implemented. It will be
@ -158,6 +192,10 @@ class Story(base.APIBase):
due_dates = wtypes.ArrayType(int) due_dates = wtypes.ArrayType(int)
private = bool
users = wtypes.ArrayType(User)
@classmethod @classmethod
def sample(cls): def sample(cls):
return cls( return cls(
@ -178,6 +216,11 @@ class Story(base.APIBase):
key=task_status, count=getattr(story_summary, task_status)) key=task_status, count=getattr(story_summary, task_status))
self.task_statuses.append(task_count) self.task_statuses.append(task_count)
def resolve_users(self, story):
"""Resolve the people who can see the story."""
self.users = [User.from_db_model(user)
for user in story.permissions[0].users]
class Tag(base.APIBase): class Tag(base.APIBase):
@ -390,39 +433,6 @@ class TimeLineEvent(base.APIBase):
return event_resolvers.tags_deleted(event) return event_resolvers.tags_deleted(event)
class User(base.APIBase):
"""Represents a user."""
full_name = wtypes.text
"""Full (Display) name."""
openid = wtypes.text
"""The unique identifier, returned by an OpneId provider"""
email = wtypes.text
"""Email Address."""
# Todo(nkonovalov): use teams to define superusers
is_superuser = bool
last_login = datetime
"""Date of the last login."""
enable_login = bool
"""Whether this user is permitted to log in."""
@classmethod
def sample(cls):
return cls(
full_name="Bart Simpson",
openid="https://login.launchpad.net/+id/Abacaba",
email="skinnerstinks@springfield.net",
is_staff=False,
is_active=True,
is_superuser=True,
last_login=datetime(2014, 1, 1, 16, 42))
class RefreshToken(base.APIBase): class RefreshToken(base.APIBase):
"""Represents a user refresh token.""" """Represents a user refresh token."""
@ -560,15 +570,18 @@ class DueDate(base.APIBase):
def resolve_count_in_board(self, due_date, board): def resolve_count_in_board(self, due_date, board):
self.count = 0 self.count = 0
for lane in board.lanes: for lane in board.lanes:
for card in lane.worklist.items: for card in worklists_api.get_visible_items(
lane.worklist, current_user=request.current_user_id):
if card.display_due_date == due_date.id: if card.display_due_date == due_date.id:
self.count += 1 self.count += 1
def resolve_items(self, due_date): def resolve_items(self, due_date):
"""Resolve the various lists for the due date.""" """Resolve the various lists for the due date."""
self.tasks = [Task.from_db_model(task) for task in due_date.tasks] stories, tasks = due_dates_api.get_visible_items(
due_date, current_user=request.current_user_id)
self.tasks = [Task.from_db_model(task) for task in tasks]
self.stories = [Story.from_db_model(story) self.stories = [Story.from_db_model(story)
for story in due_date.stories] for story in stories]
self.count = len(self.tasks) + len(self.stories) self.count = len(self.tasks) + len(self.stories)
def resolve_permissions(self, due_date, user=None): def resolve_permissions(self, due_date, user=None):
@ -619,7 +632,8 @@ class WorklistItem(base.APIBase):
def resolve_item(self, item): def resolve_item(self, item):
user_id = request.current_user_id user_id = request.current_user_id
if item.item_type == 'story': if item.item_type == 'story':
story = stories_api.story_get(item.item_id) story = stories_api.story_get(
item.item_id, current_user=request.current_user_id)
if story is None: if story is None:
return False return False
self.story = Story.from_db_model(story) self.story = Story.from_db_model(story)
@ -627,7 +641,8 @@ class WorklistItem(base.APIBase):
if due_dates_api.visible(date, user_id)] if due_dates_api.visible(date, user_id)]
self.story.due_dates = due_dates self.story.due_dates = due_dates
elif item.item_type == 'task': elif item.item_type == 'task':
task = tasks_api.task_get(item.item_id) task = tasks_api.task_get(
item.item_id, current_user=request.current_user_id)
if task is None or task.story is None: if task is None or task.story is None:
return False return False
self.task = Task.from_db_model(task) self.task = Task.from_db_model(task)
@ -740,8 +755,10 @@ class Lane(base.APIBase):
if resolve_items: if resolve_items:
self.worklist.resolve_items(lane.worklist) self.worklist.resolve_items(lane.worklist)
else: else:
items = worklists_api.get_visible_items(
lane.worklist, current_user=request.current_user_id)
self.worklist.items = [WorklistItem.from_db_model(item) self.worklist.items = [WorklistItem.from_db_model(item)
for item in lane.worklist.items] for item in items]
class Board(base.APIBase): class Board(base.APIBase):

View File

@ -1,4 +1,4 @@
# Copyright (c) 2015 Codethink Limited # Copyright (c) 2015-2016 Codethink Limited
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -26,6 +26,8 @@ from storyboard.api.auth import authorization_checks as checks
from storyboard.api.v1 import wmodels from storyboard.api.v1 import wmodels
from storyboard.common import decorators from storyboard.common import decorators
from storyboard.common import exception as exc from storyboard.common import exception as exc
from storyboard.db.api import stories as stories_api
from storyboard.db.api import tasks as tasks_api
from storyboard.db.api import worklists as worklists_api from storyboard.db.api import worklists as worklists_api
from storyboard.openstack.common.gettextutils import _ # noqa from storyboard.openstack.common.gettextutils import _ # noqa
@ -214,8 +216,12 @@ class ItemsSubcontroller(rest.RestController):
worklist.items.sort(key=lambda i: i.list_position) worklist.items.sort(key=lambda i: i.list_position)
return [wmodels.WorklistItem.from_db_model(item) visible_items = worklists_api.get_visible_items(
for item in worklist.items] worklist, current_user=request.current_user_id)
return [
wmodels.WorklistItem.from_db_model(item)
for item in visible_items
]
@decorators.db_exceptions @decorators.db_exceptions
@secure(checks.authenticated) @secure(checks.authenticated)
@ -233,7 +239,20 @@ class ItemsSubcontroller(rest.RestController):
if not worklists_api.editable_contents(worklists_api.get(id), if not worklists_api.editable_contents(worklists_api.get(id),
user_id): user_id):
raise exc.NotFound(_("Worklist %s not found") % id) raise exc.NotFound(_("Worklist %s not found") % id)
worklists_api.add_item(id, item_id, item_type, list_position) item = None
if item_type == 'story':
item = stories_api.story_get(
item_id, current_user=request.current_user_id)
elif item_type == 'task':
item = tasks_api.task_get(
item_id, current_user=request.current_user_id)
if item is None:
raise exc.NotFound(_("Item %s refers to a non-existent task or "
"story.") % item_id)
worklists_api.add_item(
id, item_id, item_type, list_position,
current_user=request.current_user_id)
return wmodels.WorklistItem.from_db_model( return wmodels.WorklistItem.from_db_model(
worklists_api.get_item_at_position(id, list_position)) worklists_api.get_item_at_position(id, list_position))
@ -257,9 +276,23 @@ class ItemsSubcontroller(rest.RestController):
if not worklists_api.editable_contents(worklists_api.get(id), if not worklists_api.editable_contents(worklists_api.get(id),
user_id): user_id):
raise exc.NotFound(_("Worklist %s not found") % id) raise exc.NotFound(_("Worklist %s not found") % id)
if worklists_api.get_item_by_id(item_id) is None: card = worklists_api.get_item_by_id(item_id)
if card is None:
raise exc.NotFound(_("Item %s seems to have been deleted, " raise exc.NotFound(_("Item %s seems to have been deleted, "
"try refreshing your page.") % item_id) "try refreshing your page.") % item_id)
item = None
if card.item_type == 'story':
item = stories_api.story_get(
card.item_id, current_user=request.current_user_id)
elif card.item_type == 'task':
item = tasks_api.task_get(
card.item_id, current_user=request.current_user_id)
if item is None:
raise exc.NotFound(_("Item %s refers to a non-existent task or "
"story.") % item_id)
worklists_api.move_item(id, item_id, list_position, list_id) worklists_api.move_item(id, item_id, list_position, list_id)
if display_due_date is not None: if display_due_date is not None:
@ -364,9 +397,11 @@ class WorklistsController(rest.RestController):
worklist.archived == archived): worklist.archived == archived):
worklist_model = wmodels.Worklist.from_db_model(worklist) worklist_model = wmodels.Worklist.from_db_model(worklist)
worklist_model.resolve_permissions(worklist) worklist_model.resolve_permissions(worklist)
visible_items = worklists_api.get_visible_items(
worklist, request.current_user_id)
worklist_model.items = [ worklist_model.items = [
wmodels.WorklistItem.from_db_model(item) wmodels.WorklistItem.from_db_model(item)
for item in worklist.items for item in visible_items
] ]
visible_worklists.append(worklist_model) visible_worklists.append(worklist_model)

View File

@ -13,7 +13,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from sqlalchemy import func from sqlalchemy import and_, func, or_
from sqlalchemy.sql.expression import false, true
from wsme.exc import ClientSideError from wsme.exc import ClientSideError
from storyboard.db.api import base as api_base from storyboard.db.api import base as api_base
@ -109,6 +110,46 @@ def update(id, values):
return api_base.entity_update(models.DueDate, id, values) return api_base.entity_update(models.DueDate, id, values)
def get_visible_items(due_date, current_user=None):
stories = due_date.stories.outerjoin(models.story_permissions,
models.Permission,
models.user_permissions,
models.User)
if current_user is not None:
due_date.stories = due_date.stories.filter(
or_(
and_(
models.User.id == current_user,
models.Story.private == true()
),
models.Story.private == false()
)
)
else:
due_date.stories = due_date.stories.filter(
models.Story.private == false())
tasks = due_date.tasks.outerjoin(models.Story,
models.story_permissions,
models.Permission,
models.user_permissions,
models.User)
if current_user is not None:
tasks = tasks.filter(
or_(
and_(
models.User.id == current_user,
models.Story.private == true()
),
models.Story.private == false()
)
)
else:
tasks = tasks.filter(models.Story.private == false())
return stories, tasks
def get_owners(due_date): def get_owners(due_date):
for permission in due_date.permissions: for permission in due_date.permissions:
if permission.codename == 'edit_date': if permission.codename == 'edit_date':

View File

@ -13,36 +13,76 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from sqlalchemy import and_, or_
from sqlalchemy.orm import subqueryload from sqlalchemy.orm import subqueryload
from sqlalchemy.sql.expression import false, true
from storyboard.common import exception as exc from storyboard.common import exception as exc
from storyboard.db.api import base as api_base from storyboard.db.api import base as api_base
from storyboard.db.api import story_tags from storyboard.db.api import story_tags
from storyboard.db.api import story_types from storyboard.db.api import story_types
from storyboard.db.api import users as users_api
from storyboard.db import models from storyboard.db import models
from storyboard.openstack.common.gettextutils import _ # noqa from storyboard.openstack.common.gettextutils import _ # noqa
def story_get_simple(story_id, session=None): def story_get_simple(story_id, session=None, current_user=None):
return api_base.model_query(models.Story, session) \ query = api_base.model_query(models.Story, session) \
.options(subqueryload(models.Story.tags)) \ .options(subqueryload(models.Story.tags)) \
.filter_by(id=story_id).first() .filter_by(id=story_id)
# Filter out stories that the current user can't see
query = query.outerjoin(models.story_permissions,
models.Permission,
models.user_permissions,
models.User)
if current_user is not None:
query = query.filter(
or_(
and_(
models.User.id == current_user,
models.Story.private == true()
),
models.Story.private == false()
)
)
else:
query = query.filter(models.Story.private == false())
return query.first()
def story_get(story_id, session=None): def story_get(story_id, session=None, current_user=None):
story_query = api_base.model_query(models.StorySummary, session) query = api_base.model_query(models.StorySummary, session)
story_summary = story_query\ query = query.options(subqueryload(models.StorySummary.tags))\
.options(subqueryload(models.StorySummary.tags))\ .filter_by(id=story_id)
.filter_by(id=story_id).first()
return story_summary # Filter out stories that the current user can't see
query = query.outerjoin(models.story_permissions,
models.Permission,
models.user_permissions,
models.User)
if current_user is not None:
query = query.filter(
or_(
and_(
models.User.id == current_user,
models.StorySummary.private == true()
),
models.StorySummary.private == false()
)
)
else:
query = query.filter(models.StorySummary.private == false())
return query.first()
def story_get_all(title=None, description=None, status=None, assignee_id=None, def story_get_all(title=None, description=None, status=None, assignee_id=None,
creator_id=None, project_group_id=None, project_id=None, creator_id=None, project_group_id=None, project_id=None,
subscriber_id=None, tags=None, marker=None, offset=None, subscriber_id=None, tags=None, marker=None, offset=None,
limit=None, tags_filter_type="all", sort_field='id', limit=None, tags_filter_type="all", sort_field='id',
sort_dir='asc'): sort_dir='asc', current_user=None):
# Sanity checks, in case someone accidentally explicitly passes in 'None' # Sanity checks, in case someone accidentally explicitly passes in 'None'
if not sort_field: if not sort_field:
sort_field = 'id' sort_field = 'id'
@ -60,7 +100,8 @@ def story_get_all(title=None, description=None, status=None, assignee_id=None,
project_group_id=project_group_id, project_group_id=project_group_id,
project_id=project_id, project_id=project_id,
tags=tags, tags=tags,
tags_filter_type=tags_filter_type) tags_filter_type=tags_filter_type,
current_user=current_user)
# Filter by subscriber ID # Filter by subscriber ID
if subscriber_id is not None: if subscriber_id is not None:
@ -100,7 +141,7 @@ def story_get_all(title=None, description=None, status=None, assignee_id=None,
def story_get_count(title=None, description=None, status=None, def story_get_count(title=None, description=None, status=None,
assignee_id=None, creator_id=None, project_group_id=None, assignee_id=None, creator_id=None, project_group_id=None,
project_id=None, subscriber_id=None, tags=None, project_id=None, subscriber_id=None, tags=None,
tags_filter_type="all"): tags_filter_type="all", current_user=None):
query = _story_build_query(title=title, query = _story_build_query(title=title,
description=description, description=description,
assignee_id=assignee_id, assignee_id=assignee_id,
@ -108,7 +149,8 @@ def story_get_count(title=None, description=None, status=None,
project_group_id=project_group_id, project_group_id=project_group_id,
project_id=project_id, project_id=project_id,
tags=tags, tags=tags,
tags_filter_type=tags_filter_type) tags_filter_type=tags_filter_type,
current_user=current_user)
# Filter by subscriber ID # Filter by subscriber ID
if subscriber_id is not None: if subscriber_id is not None:
@ -134,7 +176,7 @@ def story_get_count(title=None, description=None, status=None,
def _story_build_query(title=None, description=None, assignee_id=None, def _story_build_query(title=None, description=None, assignee_id=None,
creator_id=None, project_group_id=None, project_id=None, creator_id=None, project_group_id=None, project_id=None,
tags=None, tags_filter_type='all'): tags=None, tags_filter_type='all', current_user=None):
# First build a standard story query. # First build a standard story query.
query = api_base.model_query(models.Story.id).distinct() query = api_base.model_query(models.Story.id).distinct()
@ -145,6 +187,24 @@ def _story_build_query(title=None, description=None, assignee_id=None,
description=description, description=description,
creator_id=creator_id) creator_id=creator_id)
# Filter out stories that the current user can't see
query = query.outerjoin(models.story_permissions,
models.Permission,
models.user_permissions,
models.User)
if current_user:
query = query.filter(
or_(
and_(
models.User.id == current_user,
models.Story.private == true()
),
models.Story.private == false()
)
)
else:
query = query.filter(models.Story.private == false())
# Filtering by tags # Filtering by tags
if tags: if tags:
if tags_filter_type == 'all': if tags_filter_type == 'all':
@ -158,8 +218,9 @@ def _story_build_query(title=None, description=None, assignee_id=None,
# Are we filtering by project group? # Are we filtering by project group?
if project_group_id: if project_group_id:
query = query.join(models.Task, query = query.join(
models.Project, (models.Task, models.Task.story_id == models.Story.id))
query = query.join(models.Project,
models.project_group_mapping, models.project_group_mapping,
models.ProjectGroup) models.ProjectGroup)
query = query.filter(models.ProjectGroup.id == project_group_id) query = query.filter(models.ProjectGroup.id == project_group_id)
@ -167,7 +228,8 @@ def _story_build_query(title=None, description=None, assignee_id=None,
# Are we filtering by task? # Are we filtering by task?
if assignee_id or project_id: if assignee_id or project_id:
if not project_group_id: # We may already have joined this table if not project_group_id: # We may already have joined this table
query = query.join(models.Task) query = query.join(
(models.Task, models.Task.story_id == models.Story.id))
if assignee_id: if assignee_id:
query = query.filter(models.Task.assignee_id == assignee_id) query = query.filter(models.Task.assignee_id == assignee_id)
if project_id: if project_id:
@ -180,12 +242,12 @@ def story_create(values):
return api_base.entity_create(models.Story, values) return api_base.entity_create(models.Story, values)
def story_update(story_id, values): def story_update(story_id, values, current_user=None):
api_base.entity_update(models.Story, story_id, values) api_base.entity_update(models.Story, story_id, values)
return story_get(story_id) return story_get(story_id, current_user=current_user)
def story_add_tag(story_id, tag_name): def story_add_tag(story_id, tag_name, current_user=None):
session = api_base.get_session() session = api_base.get_session()
with session.begin(subtransactions=True): with session.begin(subtransactions=True):
@ -195,7 +257,8 @@ def story_add_tag(story_id, tag_name):
if not tag: if not tag:
tag = story_tags.tag_create({"name": tag_name}) tag = story_tags.tag_create({"name": tag_name})
story = story_get_simple(story_id, session=session) story = story_get_simple(
story_id, session=session, current_user=current_user)
if not story: if not story:
raise exc.NotFound(_("%(name)s %(id)s not found") % raise exc.NotFound(_("%(name)s %(id)s not found") %
{'name': "Story", 'id': story_id}) {'name': "Story", 'id': story_id})
@ -210,12 +273,13 @@ def story_add_tag(story_id, tag_name):
session.expunge(story) session.expunge(story)
def story_remove_tag(story_id, tag_name): def story_remove_tag(story_id, tag_name, current_user=None):
session = api_base.get_session() session = api_base.get_session()
with session.begin(subtransactions=True): with session.begin(subtransactions=True):
story = story_get_simple(story_id, session=session) story = story_get_simple(
story_id, session=session, current_user=current_user)
if not story: if not story:
raise exc.NotFound(_("%(name)s %(id)s not found") % raise exc.NotFound(_("%(name)s %(id)s not found") %
{'name': "Story", 'id': story_id}) {'name': "Story", 'id': story_id})
@ -231,8 +295,8 @@ def story_remove_tag(story_id, tag_name):
session.expunge(story) session.expunge(story)
def story_delete(story_id): def story_delete(story_id, current_user=None):
story = story_get(story_id) story = story_get(story_id, current_user=current_user)
if story: if story:
api_base.entity_hard_delete(models.Story, story_id) api_base.entity_hard_delete(models.Story, story_id)
@ -302,3 +366,38 @@ def story_can_mutate(story, new_story_type_id):
return True return True
return False return False
def create_permission(story, users, session=None):
story = api_base.model_query(models.Story, session) \
.options(subqueryload(models.Story.tags)) \
.filter_by(id=story.id).first()
permission_dict = {
'name': 'view_story_%d' % story.id,
'codename': 'view_story'
}
permission = api_base.entity_create(models.Permission, permission_dict)
story.permissions.append(permission)
for user in users:
user = users_api.user_get(user.id)
user.permissions.append(permission)
return permission
def update_permission(story, users, session=None):
story = api_base.model_query(models.Story, session) \
.options(subqueryload(models.Story.tags)) \
.filter_by(id=story.id).first()
if not story.permissions:
raise exc.NotFound(_("Permissions for story %d not found.")
% story.id)
permission = story.permissions[0]
permission_dict = {
'name': permission.name,
'codename': permission.codename,
'users': [users_api.user_get(user.id) for user in users]
}
return api_base.entity_update(models.Permission,
permission.id,
permission_dict)

View File

@ -14,8 +14,11 @@
# limitations under the License. # limitations under the License.
from sqlalchemy import distinct from sqlalchemy import distinct
from sqlalchemy.orm import subqueryload
from storyboard.db.api import base as api_base from storyboard.db.api import base as api_base
from storyboard.db.api import stories as stories_api
from storyboard.db.api import tasks as tasks_api
from storyboard.db import models from storyboard.db import models
from storyboard.db.models import TimeLineEvent from storyboard.db.models import TimeLineEvent
@ -42,9 +45,13 @@ def subscription_get_all_by_target(target_type, target_id):
target_id=target_id) target_id=target_id)
def subscription_get_resource(target_type, target_id): def subscription_get_resource(target_type, target_id, current_user=None):
if target_type not in SUPPORTED_TYPES: if target_type not in SUPPORTED_TYPES:
return None return None
if target_type == 'story':
return stories_api.story_get(target_id, current_user=current_user)
elif target_type == 'task':
return tasks_api.task_get(target_id, current_user=current_user)
return api_base.entity_get(SUPPORTED_TYPES[target_type], target_id) return api_base.entity_get(SUPPORTED_TYPES[target_type], target_id)
@ -109,9 +116,19 @@ def subscription_get_all_subscriber_ids(resource, resource_id, session=None):
# Make sure the requested resource is going to be handled. # Make sure the requested resource is going to be handled.
affected[resource].add(resource_id) affected[resource].add(resource_id)
users = None
# Resolve either from story->task or from task->story, so the root # Resolve either from story->task or from task->story, so the root
# resource id remains pristine. # resource id remains pristine.
if resource == 'story': if resource == 'story':
# If the story is private, make a whitelist of users to notify.
story = api_base.model_query(models.Story, session) \
.options(subqueryload(models.Story.permissions)) \
.filter_by(id=resource_id).first()
if story.private:
users = [user.id for user in story.permissions[0].users]
# Get this story's tasks # Get this story's tasks
query = api_base.model_query(models.Task.id, session=session) \ query = api_base.model_query(models.Task.id, session=session) \
.filter(models.Task.story_id.in_(affected['story'])) .filter(models.Task.story_id.in_(affected['story']))
@ -125,6 +142,13 @@ def subscription_get_all_subscriber_ids(resource, resource_id, session=None):
affected['story'].add(query.first().story_id) affected['story'].add(query.first().story_id)
story = api_base.model_query(models.Story, session) \
.options(subqueryload(models.Story.permissions)) \
.filter_by(id=query.first().story_id).first()
if story.private:
users = [user.id for user in story.permissions[0].users]
# If there are tasks, there will also be projects. # If there are tasks, there will also be projects.
if affected['task']: if affected['task']:
# Get all the tasks's projects # Get all the tasks's projects
@ -155,6 +179,9 @@ def subscription_get_all_subscriber_ids(resource, resource_id, session=None):
.filter(models.Subscription.target_type == affected_type) \ .filter(models.Subscription.target_type == affected_type) \
.filter(models.Subscription.target_id.in_(affected[affected_type])) .filter(models.Subscription.target_id.in_(affected[affected_type]))
if users is not None:
query = query.filter(models.Subscription.user_id.in_(users))
results = query.all() results = query.all()
subscribers = subscribers.union(r for (r,) in results) subscribers = subscribers.union(r for (r,) in results)

View File

@ -13,16 +13,41 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from sqlalchemy import and_, or_
from sqlalchemy.sql.expression import false, true
from storyboard.db.api import base as api_base from storyboard.db.api import base as api_base
from storyboard.db import models from storyboard.db import models
def task_get(task_id): def task_get(task_id, session=None, current_user=None):
return api_base.entity_get(models.Task, task_id) query = api_base.model_query(models.Task, session)
query = query.filter(models.Task.id == task_id)
# Filter out tasks or stories that the current user can't see
query = query.outerjoin(models.Story,
models.story_permissions,
models.Permission,
models.user_permissions,
models.User)
if current_user is not None:
query = query.filter(
or_(
and_(
models.User.id == current_user,
models.Story.private == true()
),
models.Story.private == false()
)
)
else:
query = query.filter(models.Story.private == false())
return query.first()
def task_get_all(marker=None, limit=None, sort_field=None, sort_dir=None, def task_get_all(marker=None, limit=None, sort_field=None, sort_dir=None,
project_group_id=None, **kwargs): project_group_id=None, current_user=None, **kwargs):
# Sanity checks, in case someone accidentally explicitly passes in 'None' # Sanity checks, in case someone accidentally explicitly passes in 'None'
if not sort_field: if not sort_field:
sort_field = 'id' sort_field = 'id'
@ -30,7 +55,8 @@ def task_get_all(marker=None, limit=None, sort_field=None, sort_dir=None,
sort_dir = 'asc' sort_dir = 'asc'
# Construct the query # Construct the query
query = task_build_query(project_group_id, **kwargs) query = task_build_query(
project_group_id, current_user=current_user, **kwargs)
query = api_base.paginate_query(query=query, query = api_base.paginate_query(query=query,
model=models.Task, model=models.Task,
@ -43,8 +69,9 @@ def task_get_all(marker=None, limit=None, sort_field=None, sort_dir=None,
return query.all() return query.all()
def task_get_count(project_group_id=None, **kwargs): def task_get_count(project_group_id=None, current_user=None, **kwargs):
query = task_build_query(project_group_id, **kwargs) query = task_build_query(
project_group_id, current_user=current_user, **kwargs)
return query.count() return query.count()
@ -63,7 +90,7 @@ def task_delete(task_id):
api_base.entity_hard_delete(models.Task, task_id) api_base.entity_hard_delete(models.Task, task_id)
def task_build_query(project_group_id, **kwargs): def task_build_query(project_group_id, current_user=None, **kwargs):
# Construct the query # Construct the query
query = api_base.model_query(models.Task) query = api_base.model_query(models.Task)
@ -78,6 +105,25 @@ def task_build_query(project_group_id, **kwargs):
model=models.Task, model=models.Task,
**kwargs) **kwargs)
# Filter out tasks or stories that the current user can't see
query = query.outerjoin(models.Story,
models.story_permissions,
models.Permission,
models.user_permissions,
models.User)
if current_user is not None:
query = query.filter(
or_(
and_(
models.User.id == current_user,
models.Story.private == true()
),
models.Story.private == false()
)
)
else:
query = query.filter(models.Story.private == false())
return query return query

View File

@ -13,14 +13,15 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from sqlalchemy.orm import subqueryload from sqlalchemy import and_, or_
from sqlalchemy.sql.expression import false, true
from wsme.exc import ClientSideError from wsme.exc import ClientSideError
from storyboard.common import exception as exc from storyboard.common import exception as exc
from storyboard.db.api import base as api_base from storyboard.db.api import base as api_base
from storyboard.db.api import boards from storyboard.db.api import boards
from storyboard.db.api import stories from storyboard.db.api import stories as stories_api
from storyboard.db.api import tasks from storyboard.db.api import tasks as tasks_api
from storyboard.db.api import users as users_api from storyboard.db.api import users as users_api
from storyboard.db import models from storyboard.db import models
from storyboard.openstack.common.gettextutils import _ # noqa from storyboard.openstack.common.gettextutils import _ # noqa
@ -29,9 +30,7 @@ from storyboard.openstack.common.gettextutils import _ # noqa
def _worklist_get(id, session=None): def _worklist_get(id, session=None):
if not session: if not session:
session = api_base.get_session() session = api_base.get_session()
query = session.query(models.Worklist).options( query = session.query(models.Worklist).filter_by(id=id)
subqueryload(models.Worklist.items)).filter_by(id=id)
return query.first() return query.first()
@ -99,6 +98,51 @@ def get_count(**kwargs):
return api_base.entity_get_count(models.Worklist, **kwargs) return api_base.entity_get_count(models.Worklist, **kwargs)
def get_visible_items(worklist, current_user=None):
stories = worklist.items.filter(models.WorklistItem.item_type == 'story')
stories = stories.join(
(models.Story, models.Story.id == models.WorklistItem.item_id))
stories = stories.outerjoin(models.story_permissions,
models.Permission,
models.user_permissions,
models.User)
if current_user is not None:
stories = stories.filter(
or_(
and_(
models.User.id == current_user,
models.Story.private == true()
),
models.Story.private == false()
)
)
else:
stories = stories.filter(models.Story.private == false())
tasks = worklist.items.filter(models.WorklistItem.item_type == 'task')
tasks = tasks.join(
(models.Task, models.Task.id == models.WorklistItem.item_id))
tasks = tasks.outerjoin(models.Story,
models.story_permissions,
models.Permission,
models.user_permissions,
models.User)
if current_user is not None:
tasks = tasks.filter(
or_(
and_(
models.User.id == current_user,
models.Story.private == true()
),
models.Story.private == false()
)
)
else:
tasks = tasks.filter(models.Story.private == false())
return stories.union(tasks)
def create(values): def create(values):
return api_base.entity_create(models.Worklist, values) return api_base.entity_create(models.Worklist, values)
@ -114,7 +158,8 @@ def has_item(worklist, item_type, item_id):
return False return False
def add_item(worklist_id, item_id, item_type, list_position): def add_item(worklist_id, item_id, item_type, list_position,
current_user=None):
worklist = _worklist_get(worklist_id) worklist = _worklist_get(worklist_id)
if worklist is None: if worklist is None:
raise exc.NotFound(_("Worklist %s not found") % worklist_id) raise exc.NotFound(_("Worklist %s not found") % worklist_id)
@ -146,9 +191,9 @@ def add_item(worklist_id, item_id, item_type, list_position):
# Create a new card # Create a new card
if item_type == 'story': if item_type == 'story':
item = stories.story_get(item_id) item = stories_api.story_get(item_id, current_user=current_user)
elif item_type == 'task': elif item_type == 'task':
item = tasks.task_get(item_id) item = tasks_api.task_get(item_id, current_user=current_user)
else: else:
raise ClientSideError(_("An item in a worklist must be either a " raise ClientSideError(_("An item in a worklist must be either a "
"story or a task")) "story or a task"))
@ -216,12 +261,14 @@ def move_item(worklist_id, item_id, list_position, list_id=None):
# Move the item and clean up the positions. # Move the item and clean up the positions.
new_list = _worklist_get(list_id) new_list = _worklist_get(list_id)
old_list.items.remove(item) old_list.items.remove(item)
old_list.items.sort(key=lambda x: x.list_position) old_list.items = old_list.items.order_by(
models.WorklistItem.list_position)
modified = old_list.items[old_pos:] modified = old_list.items[old_pos:]
for list_item in modified: for list_item in modified:
list_item.list_position -= 1 list_item.list_position -= 1
new_list.items.sort(key=lambda x: x.list_position) new_list.items = new_list.items.order_by(
models.WorklistItem.list_position)
modified = new_list.items[list_position:] modified = new_list.items[list_position:]
for list_item in modified: for list_item in modified:
list_item.list_position += 1 list_item.list_position += 1
@ -230,7 +277,8 @@ def move_item(worklist_id, item_id, list_position, list_id=None):
# Item has changed position in the list. # Item has changed position in the list.
# Update the position of every item between the original # Update the position of every item between the original
# position and the final position. # position and the final position.
old_list.items.sort(key=lambda x: x.list_position) old_list.items = old_list.items.order_by(
models.WorklistItem.list_position)
if old_pos > list_position: if old_pos > list_position:
direction = 'down' direction = 'down'
modified = old_list.items[list_position:old_pos + 1] modified = old_list.items[list_position:old_pos + 1]

View File

@ -33,7 +33,7 @@ from sqlalchemy.ext import declarative
from sqlalchemy import ForeignKey from sqlalchemy import ForeignKey
from sqlalchemy import Integer from sqlalchemy import Integer
from sqlalchemy.orm.collections import attribute_mapped_collection from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.orm import relationship from sqlalchemy.orm import backref, relationship
from sqlalchemy import schema from sqlalchemy import schema
from sqlalchemy import select from sqlalchemy import select
import sqlalchemy.sql.expression as expr import sqlalchemy.sql.expression as expr
@ -468,6 +468,7 @@ class StorySummary(Base):
__table__ = _story_build_summary_query() __table__ = _story_build_summary_query()
tags = relationship('StoryTag', secondary='story_storytags') tags = relationship('StoryTag', secondary='story_storytags')
due_dates = relationship('DueDate', secondary='story_due_dates') due_dates = relationship('DueDate', secondary='story_due_dates')
permissions = relationship('Permission', secondary='story_permissions')
def as_dict(self): def as_dict(self):
d = super(StorySummary, self).as_dict() d = super(StorySummary, self).as_dict()
@ -546,6 +547,7 @@ class WorklistItem(ModelBuilder, Base):
ForeignKey('due_dates.id'), ForeignKey('due_dates.id'),
nullable=True) nullable=True)
archived = Column(Boolean, default=False) archived = Column(Boolean, default=False)
list = relationship('Worklist', backref=backref('items', lazy="dynamic"))
_public_fields = ["id", "list_id", "list_position", "item_type", _public_fields = ["id", "list_id", "list_position", "item_type",
"item_id"] "item_id"]
@ -586,7 +588,6 @@ class Worklist(FullText, ModelBuilder, Base):
private = Column(Boolean, default=False) private = Column(Boolean, default=False)
archived = Column(Boolean, default=False) archived = Column(Boolean, default=False)
automatic = Column(Boolean, default=False) automatic = Column(Boolean, default=False)
items = relationship(WorklistItem)
filters = relationship(WorklistFilter) filters = relationship(WorklistFilter)
permissions = relationship("Permission", secondary="worklist_permissions") permissions = relationship("Permission", secondary="worklist_permissions")
@ -645,10 +646,12 @@ class DueDate(FullText, ModelBuilder, Base):
permissions = relationship('Permission', secondary='due_date_permissions') permissions = relationship('Permission', secondary='due_date_permissions')
tasks = relationship('Task', tasks = relationship('Task',
secondary='task_due_dates', secondary='task_due_dates',
backref='due_dates') backref='due_dates',
lazy="dynamic")
stories = relationship('Story', stories = relationship('Story',
secondary='story_due_dates', secondary='story_due_dates',
backref='due_dates') backref='due_dates',
lazy="dynamic")
boards = relationship('Board', boards = relationship('Board',
secondary='board_due_dates', secondary='board_due_dates',
backref='due_dates') backref='due_dates')