Merge "Allow the creation of private stories"
This commit is contained in:
commit
3779bacc1b
@ -242,14 +242,16 @@ class DueDatesController(rest.RestController):
|
||||
tasks = due_date_dict.pop('tasks')
|
||||
db_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
|
||||
|
||||
if 'stories' in due_date_dict:
|
||||
stories = due_date_dict.pop('stories')
|
||||
db_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
|
||||
|
||||
board = None
|
||||
|
@ -13,7 +13,9 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from sqlalchemy import and_, or_
|
||||
from sqlalchemy.orm import subqueryload
|
||||
from sqlalchemy.sql.expression import false, true
|
||||
from sqlalchemy_fulltext import FullTextSearch
|
||||
import sqlalchemy_fulltext.modes as FullTextMode
|
||||
|
||||
@ -54,10 +56,29 @@ class SqlAlchemySearchImpl(search_engine.SearchEngine):
|
||||
return query.all()
|
||||
|
||||
def stories_query(self, q, marker=None, offset=None,
|
||||
limit=None, **kwargs):
|
||||
limit=None, current_user=None, **kwargs):
|
||||
session = api_base.get_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._apply_pagination(models.Story,
|
||||
subquery, marker, offset, limit)
|
||||
@ -72,9 +93,30 @@ class SqlAlchemySearchImpl(search_engine.SearchEngine):
|
||||
stories = query.all()
|
||||
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()
|
||||
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._apply_pagination(
|
||||
models.Task, query, marker, offset, limit)
|
||||
|
@ -1,4 +1,5 @@
|
||||
# Copyright (c) 2013 Mirantis Inc.
|
||||
# Copyright (c) 2016 Codethink Ltd.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (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.db.api import stories as stories_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
|
||||
|
||||
|
||||
@ -46,6 +48,10 @@ SEARCH_ENGINE = search_engine.get_engine()
|
||||
def create_story_wmodel(story):
|
||||
story_model = wmodels.Story.from_db_model(story)
|
||||
story_model.summarize_task_statuses(story)
|
||||
if story.permissions:
|
||||
story_model.resolve_users(story)
|
||||
else:
|
||||
story_model.users = []
|
||||
return story_model
|
||||
|
||||
|
||||
@ -65,7 +71,8 @@ class StoriesController(rest.RestController):
|
||||
|
||||
: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:
|
||||
return create_story_wmodel(story)
|
||||
@ -108,7 +115,8 @@ class StoriesController(rest.RestController):
|
||||
# Resolve the marker record.
|
||||
marker_story = None
|
||||
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 \
|
||||
.story_get_all(title=title,
|
||||
@ -123,8 +131,10 @@ class StoriesController(rest.RestController):
|
||||
marker=marker_story,
|
||||
offset=offset,
|
||||
tags_filter_type=tags_filter_type,
|
||||
limit=limit, sort_field=sort_field,
|
||||
sort_dir=sort_dir)
|
||||
limit=limit,
|
||||
sort_field=sort_field,
|
||||
sort_dir=sort_dir,
|
||||
current_user=request.current_user_id)
|
||||
story_count = stories_api \
|
||||
.story_get_count(title=title,
|
||||
description=description,
|
||||
@ -135,7 +145,8 @@ class StoriesController(rest.RestController):
|
||||
project_id=project_id,
|
||||
subscriber_id=subscriber_id,
|
||||
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.
|
||||
if limit:
|
||||
@ -181,9 +192,18 @@ class StoriesController(rest.RestController):
|
||||
if "due_dates" in story_dict:
|
||||
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)
|
||||
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)
|
||||
|
||||
@decorators.db_exceptions
|
||||
@ -202,7 +222,8 @@ class StoriesController(rest.RestController):
|
||||
abort(400, _("Now you can't change story type to %s.") %
|
||||
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:
|
||||
raise exc.NotFound(_("Story %s not found") % story_id)
|
||||
@ -224,9 +245,26 @@ class StoriesController(rest.RestController):
|
||||
if 'tags' in story_dict:
|
||||
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(
|
||||
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
|
||||
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.
|
||||
"""
|
||||
stories_api.story_delete(story_id)
|
||||
stories_api.story_delete(
|
||||
story_id, current_user=request.current_user_id)
|
||||
|
||||
comments = CommentsController()
|
||||
events = TimeLineEventsController()
|
||||
@ -260,10 +299,12 @@ class StoriesController(rest.RestController):
|
||||
:return: List of Stories matching the query.
|
||||
"""
|
||||
|
||||
user = request.current_user_id
|
||||
stories = SEARCH_ENGINE.stories_query(q=q,
|
||||
marker=marker,
|
||||
offset=offset,
|
||||
limit=limit)
|
||||
limit=limit,
|
||||
current_user=user)
|
||||
|
||||
return [create_story_wmodel(story) for story in stories]
|
||||
|
||||
|
@ -158,7 +158,8 @@ class SubscriptionsController(rest.RestController):
|
||||
# Data sanity check: The resource must exist.
|
||||
resource = subscription_api.subscription_get_resource(
|
||||
target_type=subscription.target_type,
|
||||
target_id=subscription.target_id)
|
||||
target_id=subscription.target_id,
|
||||
current_user=request.current_user_id)
|
||||
if not resource:
|
||||
abort(400, _('You cannot subscribe to a nonexistent resource.'))
|
||||
|
||||
|
@ -63,7 +63,8 @@ class TagsController(rest.RestController):
|
||||
|
||||
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:
|
||||
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.
|
||||
"""
|
||||
|
||||
story = stories_api.story_get(story_id)
|
||||
story = stories_api.story_get(
|
||||
story_id, current_user=request.current_user_id)
|
||||
if not story:
|
||||
raise exc.NotFound("Story %s not found" % story_id)
|
||||
|
||||
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.
|
||||
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,
|
||||
author_id=request.current_user_id,
|
||||
story_title=story.title,
|
||||
@ -115,12 +119,14 @@ class TagsController(rest.RestController):
|
||||
: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:
|
||||
raise exc.NotFound("Story %s not found" % story_id)
|
||||
|
||||
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,
|
||||
author_id=request.current_user_id,
|
||||
|
@ -90,7 +90,8 @@ def story_is_valid(task, branch):
|
||||
"""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:
|
||||
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.
|
||||
"""
|
||||
task = tasks_api.task_get(task_id)
|
||||
task = tasks_api.task_get(
|
||||
task_id, current_user=request.current_user_id)
|
||||
|
||||
if task:
|
||||
return wmodels.Task.from_db_model(task)
|
||||
@ -314,7 +316,8 @@ class TasksPrimaryController(rest.RestController):
|
||||
sort_field=sort_field,
|
||||
sort_dir=sort_dir,
|
||||
marker=marker_task,
|
||||
limit=limit)
|
||||
limit=limit,
|
||||
current_user=request.current_user_id)
|
||||
task_count = tasks_api \
|
||||
.task_get_count(title=title,
|
||||
link=link,
|
||||
@ -325,7 +328,8 @@ class TasksPrimaryController(rest.RestController):
|
||||
branch_id=branch_id,
|
||||
milestone_id=milestone_id,
|
||||
status=status,
|
||||
priority=priority)
|
||||
priority=priority,
|
||||
current_user=request.current_user_id)
|
||||
|
||||
# Apply the query response headers.
|
||||
if limit:
|
||||
@ -374,7 +378,8 @@ class TasksPrimaryController(rest.RestController):
|
||||
: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:
|
||||
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.
|
||||
"""
|
||||
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:
|
||||
raise exc.NotFound(_("Task %s not found.") % task_id)
|
||||
@ -419,10 +425,12 @@ class TasksPrimaryController(rest.RestController):
|
||||
:return: List of Tasks matching the query.
|
||||
"""
|
||||
|
||||
user = request.current_user_id
|
||||
tasks = SEARCH_ENGINE.tasks_query(q=q,
|
||||
marker=marker,
|
||||
offset=offset,
|
||||
limit=limit)
|
||||
limit=limit,
|
||||
current_user=user)
|
||||
|
||||
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 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.story_id != story_id:
|
||||
@ -482,7 +491,8 @@ class TasksNestedController(rest.RestController):
|
||||
limit = max(0, limit)
|
||||
|
||||
# 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 \
|
||||
.task_get_all(title=title,
|
||||
@ -498,7 +508,8 @@ class TasksNestedController(rest.RestController):
|
||||
sort_field=sort_field,
|
||||
sort_dir=sort_dir,
|
||||
marker=marker_task,
|
||||
limit=limit)
|
||||
limit=limit,
|
||||
current_user=request.current_user_id)
|
||||
task_count = tasks_api \
|
||||
.task_get_count(title=title,
|
||||
link=link,
|
||||
@ -509,7 +520,8 @@ class TasksNestedController(rest.RestController):
|
||||
branch_id=branch_id,
|
||||
milestone_id=milestone_id,
|
||||
status=status,
|
||||
priority=priority)
|
||||
priority=priority,
|
||||
current_user=request.current_user_id)
|
||||
|
||||
# Apply the query response headers.
|
||||
response.headers['X-Limit'] = str(limit)
|
||||
@ -565,7 +577,8 @@ class TasksNestedController(rest.RestController):
|
||||
: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:
|
||||
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.
|
||||
"""
|
||||
|
||||
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:
|
||||
raise exc.NotFound(_("Task %s not found.") % task_id)
|
||||
|
@ -1,4 +1,5 @@
|
||||
# Copyright (c) 2014 Mirantis Inc.
|
||||
# Copyright (c) 2016 Codethink Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -125,6 +126,39 @@ class TaskStatusCount(base.APIBase):
|
||||
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):
|
||||
"""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
|
||||
@ -158,6 +192,10 @@ class Story(base.APIBase):
|
||||
|
||||
due_dates = wtypes.ArrayType(int)
|
||||
|
||||
private = bool
|
||||
|
||||
users = wtypes.ArrayType(User)
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(
|
||||
@ -178,6 +216,11 @@ class Story(base.APIBase):
|
||||
key=task_status, count=getattr(story_summary, task_status))
|
||||
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):
|
||||
|
||||
@ -390,39 +433,6 @@ class TimeLineEvent(base.APIBase):
|
||||
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):
|
||||
"""Represents a user refresh token."""
|
||||
|
||||
@ -560,15 +570,18 @@ class DueDate(base.APIBase):
|
||||
def resolve_count_in_board(self, due_date, board):
|
||||
self.count = 0
|
||||
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:
|
||||
self.count += 1
|
||||
|
||||
def resolve_items(self, 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)
|
||||
for story in due_date.stories]
|
||||
for story in stories]
|
||||
self.count = len(self.tasks) + len(self.stories)
|
||||
|
||||
def resolve_permissions(self, due_date, user=None):
|
||||
@ -619,7 +632,8 @@ class WorklistItem(base.APIBase):
|
||||
def resolve_item(self, item):
|
||||
user_id = request.current_user_id
|
||||
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:
|
||||
return False
|
||||
self.story = Story.from_db_model(story)
|
||||
@ -627,7 +641,8 @@ class WorklistItem(base.APIBase):
|
||||
if due_dates_api.visible(date, user_id)]
|
||||
self.story.due_dates = due_dates
|
||||
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:
|
||||
return False
|
||||
self.task = Task.from_db_model(task)
|
||||
@ -740,8 +755,10 @@ class Lane(base.APIBase):
|
||||
if resolve_items:
|
||||
self.worklist.resolve_items(lane.worklist)
|
||||
else:
|
||||
items = worklists_api.get_visible_items(
|
||||
lane.worklist, current_user=request.current_user_id)
|
||||
self.worklist.items = [WorklistItem.from_db_model(item)
|
||||
for item in lane.worklist.items]
|
||||
for item in items]
|
||||
|
||||
|
||||
class Board(base.APIBase):
|
||||
|
@ -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");
|
||||
# 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.common import decorators
|
||||
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.openstack.common.gettextutils import _ # noqa
|
||||
|
||||
@ -214,8 +216,12 @@ class ItemsSubcontroller(rest.RestController):
|
||||
|
||||
worklist.items.sort(key=lambda i: i.list_position)
|
||||
|
||||
return [wmodels.WorklistItem.from_db_model(item)
|
||||
for item in worklist.items]
|
||||
visible_items = worklists_api.get_visible_items(
|
||||
worklist, current_user=request.current_user_id)
|
||||
return [
|
||||
wmodels.WorklistItem.from_db_model(item)
|
||||
for item in visible_items
|
||||
]
|
||||
|
||||
@decorators.db_exceptions
|
||||
@secure(checks.authenticated)
|
||||
@ -233,7 +239,20 @@ class ItemsSubcontroller(rest.RestController):
|
||||
if not worklists_api.editable_contents(worklists_api.get(id),
|
||||
user_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(
|
||||
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),
|
||||
user_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, "
|
||||
"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)
|
||||
|
||||
if display_due_date is not None:
|
||||
@ -364,9 +397,11 @@ class WorklistsController(rest.RestController):
|
||||
worklist.archived == archived):
|
||||
worklist_model = wmodels.Worklist.from_db_model(worklist)
|
||||
worklist_model.resolve_permissions(worklist)
|
||||
visible_items = worklists_api.get_visible_items(
|
||||
worklist, request.current_user_id)
|
||||
worklist_model.items = [
|
||||
wmodels.WorklistItem.from_db_model(item)
|
||||
for item in worklist.items
|
||||
for item in visible_items
|
||||
]
|
||||
visible_worklists.append(worklist_model)
|
||||
|
||||
|
@ -13,7 +13,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# 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 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)
|
||||
|
||||
|
||||
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):
|
||||
for permission in due_date.permissions:
|
||||
if permission.codename == 'edit_date':
|
||||
|
@ -13,36 +13,76 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from sqlalchemy import and_, or_
|
||||
from sqlalchemy.orm import subqueryload
|
||||
from sqlalchemy.sql.expression import false, true
|
||||
|
||||
from storyboard.common import exception as exc
|
||||
from storyboard.db.api import base as api_base
|
||||
from storyboard.db.api import story_tags
|
||||
from storyboard.db.api import story_types
|
||||
from storyboard.db.api import users as users_api
|
||||
from storyboard.db import models
|
||||
from storyboard.openstack.common.gettextutils import _ # noqa
|
||||
|
||||
|
||||
def story_get_simple(story_id, session=None):
|
||||
return api_base.model_query(models.Story, session) \
|
||||
def story_get_simple(story_id, session=None, current_user=None):
|
||||
query = api_base.model_query(models.Story, session) \
|
||||
.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):
|
||||
story_query = api_base.model_query(models.StorySummary, session)
|
||||
story_summary = story_query\
|
||||
.options(subqueryload(models.StorySummary.tags))\
|
||||
.filter_by(id=story_id).first()
|
||||
def story_get(story_id, session=None, current_user=None):
|
||||
query = api_base.model_query(models.StorySummary, session)
|
||||
query = query.options(subqueryload(models.StorySummary.tags))\
|
||||
.filter_by(id=story_id)
|
||||
|
||||
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,
|
||||
creator_id=None, project_group_id=None, project_id=None,
|
||||
subscriber_id=None, tags=None, marker=None, offset=None,
|
||||
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'
|
||||
if not sort_field:
|
||||
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_id=project_id,
|
||||
tags=tags,
|
||||
tags_filter_type=tags_filter_type)
|
||||
tags_filter_type=tags_filter_type,
|
||||
current_user=current_user)
|
||||
|
||||
# Filter by subscriber ID
|
||||
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,
|
||||
assignee_id=None, creator_id=None, project_group_id=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,
|
||||
description=description,
|
||||
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_id=project_id,
|
||||
tags=tags,
|
||||
tags_filter_type=tags_filter_type)
|
||||
tags_filter_type=tags_filter_type,
|
||||
current_user=current_user)
|
||||
|
||||
# Filter by subscriber ID
|
||||
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,
|
||||
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.
|
||||
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,
|
||||
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
|
||||
if tags:
|
||||
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?
|
||||
if project_group_id:
|
||||
query = query.join(models.Task,
|
||||
models.Project,
|
||||
query = query.join(
|
||||
(models.Task, models.Task.story_id == models.Story.id))
|
||||
query = query.join(models.Project,
|
||||
models.project_group_mapping,
|
||||
models.ProjectGroup)
|
||||
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?
|
||||
if assignee_id or project_id:
|
||||
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:
|
||||
query = query.filter(models.Task.assignee_id == assignee_id)
|
||||
if project_id:
|
||||
@ -180,12 +242,12 @@ def story_create(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)
|
||||
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()
|
||||
|
||||
with session.begin(subtransactions=True):
|
||||
@ -195,7 +257,8 @@ def story_add_tag(story_id, tag_name):
|
||||
if not tag:
|
||||
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:
|
||||
raise exc.NotFound(_("%(name)s %(id)s not found") %
|
||||
{'name': "Story", 'id': story_id})
|
||||
@ -210,12 +273,13 @@ def story_add_tag(story_id, tag_name):
|
||||
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()
|
||||
|
||||
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:
|
||||
raise exc.NotFound(_("%(name)s %(id)s not found") %
|
||||
{'name': "Story", 'id': story_id})
|
||||
@ -231,8 +295,8 @@ def story_remove_tag(story_id, tag_name):
|
||||
session.expunge(story)
|
||||
|
||||
|
||||
def story_delete(story_id):
|
||||
story = story_get(story_id)
|
||||
def story_delete(story_id, current_user=None):
|
||||
story = story_get(story_id, current_user=current_user)
|
||||
|
||||
if story:
|
||||
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 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)
|
||||
|
@ -14,8 +14,11 @@
|
||||
# limitations under the License.
|
||||
|
||||
from sqlalchemy import distinct
|
||||
from sqlalchemy.orm import subqueryload
|
||||
|
||||
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.models import TimeLineEvent
|
||||
|
||||
@ -42,9 +45,13 @@ def subscription_get_all_by_target(target_type, 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:
|
||||
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)
|
||||
|
||||
@ -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.
|
||||
affected[resource].add(resource_id)
|
||||
|
||||
users = None
|
||||
|
||||
# Resolve either from story->task or from task->story, so the root
|
||||
# resource id remains pristine.
|
||||
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
|
||||
query = api_base.model_query(models.Task.id, session=session) \
|
||||
.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)
|
||||
|
||||
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 affected['task']:
|
||||
# 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_id.in_(affected[affected_type]))
|
||||
|
||||
if users is not None:
|
||||
query = query.filter(models.Subscription.user_id.in_(users))
|
||||
|
||||
results = query.all()
|
||||
subscribers = subscribers.union(r for (r,) in results)
|
||||
|
||||
|
@ -13,16 +13,41 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# 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 import models
|
||||
|
||||
|
||||
def task_get(task_id):
|
||||
return api_base.entity_get(models.Task, task_id)
|
||||
def task_get(task_id, session=None, current_user=None):
|
||||
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,
|
||||
project_group_id=None, **kwargs):
|
||||
project_group_id=None, current_user=None, **kwargs):
|
||||
# Sanity checks, in case someone accidentally explicitly passes in 'None'
|
||||
if not sort_field:
|
||||
sort_field = 'id'
|
||||
@ -30,7 +55,8 @@ def task_get_all(marker=None, limit=None, sort_field=None, sort_dir=None,
|
||||
sort_dir = 'asc'
|
||||
|
||||
# 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,
|
||||
model=models.Task,
|
||||
@ -43,8 +69,9 @@ def task_get_all(marker=None, limit=None, sort_field=None, sort_dir=None,
|
||||
return query.all()
|
||||
|
||||
|
||||
def task_get_count(project_group_id=None, **kwargs):
|
||||
query = task_build_query(project_group_id, **kwargs)
|
||||
def task_get_count(project_group_id=None, current_user=None, **kwargs):
|
||||
query = task_build_query(
|
||||
project_group_id, current_user=current_user, **kwargs)
|
||||
return query.count()
|
||||
|
||||
|
||||
@ -63,7 +90,7 @@ def task_delete(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
|
||||
query = api_base.model_query(models.Task)
|
||||
|
||||
@ -78,6 +105,25 @@ def task_build_query(project_group_id, **kwargs):
|
||||
model=models.Task,
|
||||
**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
|
||||
|
||||
|
||||
|
@ -13,14 +13,15 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# 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 storyboard.common import exception as exc
|
||||
from storyboard.db.api import base as api_base
|
||||
from storyboard.db.api import boards
|
||||
from storyboard.db.api import stories
|
||||
from storyboard.db.api import tasks
|
||||
from storyboard.db.api import stories as stories_api
|
||||
from storyboard.db.api import tasks as tasks_api
|
||||
from storyboard.db.api import users as users_api
|
||||
from storyboard.db import models
|
||||
from storyboard.openstack.common.gettextutils import _ # noqa
|
||||
@ -29,9 +30,7 @@ from storyboard.openstack.common.gettextutils import _ # noqa
|
||||
def _worklist_get(id, session=None):
|
||||
if not session:
|
||||
session = api_base.get_session()
|
||||
query = session.query(models.Worklist).options(
|
||||
subqueryload(models.Worklist.items)).filter_by(id=id)
|
||||
|
||||
query = session.query(models.Worklist).filter_by(id=id)
|
||||
return query.first()
|
||||
|
||||
|
||||
@ -99,6 +98,51 @@ def get_count(**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):
|
||||
return api_base.entity_create(models.Worklist, values)
|
||||
|
||||
@ -114,7 +158,8 @@ def has_item(worklist, item_type, item_id):
|
||||
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)
|
||||
if worklist is None:
|
||||
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
|
||||
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':
|
||||
item = tasks.task_get(item_id)
|
||||
item = tasks_api.task_get(item_id, current_user=current_user)
|
||||
else:
|
||||
raise ClientSideError(_("An item in a worklist must be either a "
|
||||
"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.
|
||||
new_list = _worklist_get(list_id)
|
||||
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:]
|
||||
for list_item in modified:
|
||||
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:]
|
||||
for list_item in modified:
|
||||
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.
|
||||
# Update the position of every item between the original
|
||||
# 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:
|
||||
direction = 'down'
|
||||
modified = old_list.items[list_position:old_pos + 1]
|
||||
|
@ -33,7 +33,7 @@ from sqlalchemy.ext import declarative
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy import Integer
|
||||
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 select
|
||||
import sqlalchemy.sql.expression as expr
|
||||
@ -468,6 +468,7 @@ class StorySummary(Base):
|
||||
__table__ = _story_build_summary_query()
|
||||
tags = relationship('StoryTag', secondary='story_storytags')
|
||||
due_dates = relationship('DueDate', secondary='story_due_dates')
|
||||
permissions = relationship('Permission', secondary='story_permissions')
|
||||
|
||||
def as_dict(self):
|
||||
d = super(StorySummary, self).as_dict()
|
||||
@ -546,6 +547,7 @@ class WorklistItem(ModelBuilder, Base):
|
||||
ForeignKey('due_dates.id'),
|
||||
nullable=True)
|
||||
archived = Column(Boolean, default=False)
|
||||
list = relationship('Worklist', backref=backref('items', lazy="dynamic"))
|
||||
|
||||
_public_fields = ["id", "list_id", "list_position", "item_type",
|
||||
"item_id"]
|
||||
@ -586,7 +588,6 @@ class Worklist(FullText, ModelBuilder, Base):
|
||||
private = Column(Boolean, default=False)
|
||||
archived = Column(Boolean, default=False)
|
||||
automatic = Column(Boolean, default=False)
|
||||
items = relationship(WorklistItem)
|
||||
filters = relationship(WorklistFilter)
|
||||
permissions = relationship("Permission", secondary="worklist_permissions")
|
||||
|
||||
@ -645,10 +646,12 @@ class DueDate(FullText, ModelBuilder, Base):
|
||||
permissions = relationship('Permission', secondary='due_date_permissions')
|
||||
tasks = relationship('Task',
|
||||
secondary='task_due_dates',
|
||||
backref='due_dates')
|
||||
backref='due_dates',
|
||||
lazy="dynamic")
|
||||
stories = relationship('Story',
|
||||
secondary='story_due_dates',
|
||||
backref='due_dates')
|
||||
backref='due_dates',
|
||||
lazy="dynamic")
|
||||
boards = relationship('Board',
|
||||
secondary='board_due_dates',
|
||||
backref='due_dates')
|
||||
|
Loading…
Reference in New Issue
Block a user