def get_one(self, story_id, event_id): """Retrieve details about one event. Example:: curl https://my.example.org/api/v1/stories/11/events/15994 :param story_id: An ID of the story. It stays in params as a placeholder so that pecan knows where to match an incoming value. It will stay unused, as far as events have their own unique ids. :param event_id: An ID of the event. """ event = events_api.event_get(event_id, current_user=request.current_user_id) if event: wsme_event = wmodels.TimeLineEvent.from_db_model(event) wsme_event = wmodels.TimeLineEvent.resolve_event_values(wsme_event) return wsme_event else: raise exc.NotFound(_("Event %s not found") % event_id)
def delete(self, worklist_id): """Archive this worklist. Though this uses the DELETE command, the worklist is not deleted. Archived worklists remain viewable at the designated URL, but are not returned in search results nor appear on your dashboard. Example:: curl https://my.example.org/api/v1/worklists/30 -X DELETE \\ -H 'Authorization: Bearer MY_ACCESS_TOKEN' :param worklist_id: The ID of the worklist to be archived. """ worklist = worklists_api.get(worklist_id) original = copy.deepcopy(worklist) user_id = request.current_user_id if not worklists_api.editable(worklist, user_id): raise exc.NotFound(_("Worklist %s not found") % worklist_id) updated = worklists_api.update(worklist_id, {"archived": True}) post_timeline_events(original, updated)
def delete(self, worklist_id, filter_id): """Delete a filter from a worklist. Example:: TODO :param worklist_id: The ID of the worklist. :param filter_id: The ID of the filter to be deleted. """ worklist = worklists_api.get(worklist_id) user_id = request.current_user_id if not worklists_api.editable(worklist, user_id): raise exc.NotFound(_("Worklist %s not found") % worklist_id) filter = serialize_filter(worklists_api.get_filter(filter_id)) events_api.worklist_filters_changed_event(worklist_id, user_id, removed=filter) worklists_api.delete_filter(filter_id)
def delete(self, task_id): """Delete this task. Example:: curl https://my.example.org/api/v1/tasks/27 -X DELETE \\ -H 'Authorization: Bearer MY_ACCESS_TOKEN' :param task_id: An ID of the task. """ 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) events_api.task_deleted_event( story_id=original_task.story_id, task_id=original_task.id, task_title=original_task.title, author_id=request.current_user_id) tasks_api.task_delete(task_id)
def put(self, project_id, project): """Modify this project. .. note:: This command is only available to Admin users. Example:: curl https://my.example.org/api/v1/projects/10 -X PUT \\ -H 'Authorization: Bearer MY_ACCESS_TOKEN' \\ -H 'Content-Type: application/json;charset=UTF-8' \\ --data-binary '{"name":"test-project-update",\\ "description":"An updated test project"}' :param project_id: An ID of the project. :param project: A project within the request body. """ result = projects_api.project_update(project_id, project.as_dict(omit_unset=True)) if result: return wmodels.Project.from_db_model(result) else: raise exc.NotFound(_("Project %s not found") % project_id)
def put(self, story_id, task_id, task): """Modify this task. Example:: curl 'https://my.example.org/api/v1/stories/19/tasks/19' -X PUT \\ -H 'Authorization: Bearer MY_ACCESS_TOKEN' \\ -H 'Content-Type: application/json;charset=UTF-8' \\ --data-binary '{"title":"Task Foio","project_id":153,"key":"todo"}' :param story_id: An ID of the story. :param task_id: An ID of the task. :param task: a task within the request body. """ 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) if original_task.story_id != story_id: abort(400, _("URL story_id and task.story_id do not match")) if task.story_id and original_task.story_id != task.story_id: abort( 400, _("the story_id of a task cannot be changed through this API"), ) task = task_is_valid_put(task, original_task) updated_task = tasks_api.task_update(task_id, task.as_dict( omit_unset=True)) post_timeline_events(original_task, updated_task) return wmodels.Task.from_db_model(updated_task)
def put(self, story_id, task_id, task): """Modify this task. :param story_id: An ID of the story. :param task_id: An ID of the task. :param task: a task within the request body. """ 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) if original_task.story_id != story_id: abort(400, _("URL story_id and task.story_id do not match")) task = task_is_valid_put(task, original_task) updated_task = tasks_api.task_update(task_id, task.as_dict( omit_unset=True)) post_timeline_events(original_task, updated_task) return wmodels.Task.from_db_model(updated_task)
def _story_build_query(title=None, description=None, assignee_id=None, creator_id=None, project_group_id=None, project_id=None, updated_since=None, tags=None, board_id=None, worklist_id=None, tags_filter_type='all', current_user=None, session=None): # First build a standard story query. query = api_base.model_query(models.Story.id, session=session).distinct() # Apply basic filters query = api_base.apply_query_filters(query=query, model=models.Story, title=title, description=description, creator_id=creator_id) if updated_since: query = query.filter(models.Story.updated_at > updated_since) # Filter out stories that the current user can't see query = api_base.filter_private_stories(query, current_user) # Filtering by tags if tags: if tags_filter_type == 'all': for tag in tags: query = query.filter(models.Story.tags.any(name=tag)) elif tags_filter_type == 'any': query = query.filter( models.Story.tags.any(models.StoryTag.name.in_(tags))) else: raise exc.NotFound("Tags filter not found.") # Are we filtering by project group? if project_group_id: 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) # 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, models.Task.story_id == models.Story.id)) if assignee_id: query = query.filter(models.Task.assignee_id == assignee_id) if project_id: query = query.filter(models.Task.project_id == project_id) if worklist_id or board_id: query = query.outerjoin( (models.WorklistItem, models.WorklistItem.item_id == models.Story.id)) query = query.filter(models.WorklistItem.item_type == "story") query = query.outerjoin(models.Worklist) # Filter by worklist if worklist_id: query = query.filter(models.Worklist.id == worklist_id) query = api_base.filter_private_worklists(query, current_user, hide_lanes=False) # Filter by board if board_id: query = query.outerjoin(models.BoardWorklist, models.Board) query = api_base.filter_private_boards(query, current_user) query = query.filter(models.Board.id == board_id) query = api_base.filter_private_worklists(query, current_user, hide_lanes=False) return query.distinct()
def put(self, id, item_id, list_position, list_id=None, display_due_date=None): """Update a WorklistItem. Example:: TODO This method also updates the positions of other items in affected worklists, if necessary. :param id: The ID of the worklist. :param item_id: The ID of the worklist_item to be moved. :param display_due_date: The ID of the due date displayed on the item. """ user_id = request.current_user_id if not worklists_api.editable_contents(worklists_api.get(id), user_id): raise exc.NotFound(_("Worklist %s not found") % id) 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) old = { "worklist_id": card.list_id, "item_id": card.item_id, "item_title": item.title, "item_type": card.item_type, "position": card.list_position, "due_date_id": card.display_due_date } new = { "item_id": card.item_id, "item_title": item.title, "item_type": card.item_type } if list_position != card.list_position and list_position is not None: new['position'] = list_position if list_id != card.list_id and list_id is not None: new['worklist_id'] = list_id worklists_api.move_item(id, item_id, list_position, list_id) if display_due_date is not None: if display_due_date == -1: display_due_date = None update_dict = {'display_due_date': display_due_date} worklists_api.update_item(item_id, update_dict) new['due_date_id'] = display_due_date updated = {"old": old, "new": new} events_api.worklist_contents_changed_event(id, user_id, updated=updated) updated = worklists_api.get_item_by_id(item_id) result = wmodels.WorklistItem.from_db_model(updated) result.resolve_due_date(updated) return result
def put(self, id, due_date): """Modify a due date. :param id: The ID of the due date to edit. :param due_date: The new due date within the request body. """ if not due_dates_api.assignable(due_dates_api.get(id), request.current_user_id): raise exc.NotFound(_("Due date %s not found") % id) original_due_date = due_dates_api.get(id) due_date_dict = due_date.as_dict(omit_unset=True) editing = any(prop in due_date_dict for prop in ('name', 'date', 'private')) if editing and not due_dates_api.editable(original_due_date, request.current_user_id): raise exc.NotFound(_("Due date %s not found") % id) if due_date.creator_id \ and due_date.creator_id != original_due_date.creator_id: abort(400, _("You can't select the creator of a due date.")) if 'tasks' in due_date_dict: tasks = due_date_dict.pop('tasks') db_tasks = [] for task in tasks: 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, current_user=request.current_user_id)) due_date_dict['stories'] = db_stories board = None worklist = None if 'board_id' in due_date_dict: board = boards_api.get(due_date_dict['board_id']) if 'worklist_id' in due_date_dict: worklist = worklists_api.get(due_date_dict['worklist_id']) updated_due_date = due_dates_api.update(id, due_date_dict) if board: updated_due_date.boards.append(board) if worklist: updated_due_date.worklists.append(worklist) if due_dates_api.visible(updated_due_date, request.current_user_id): due_date_model = wmodels.DueDate.from_db_model(updated_due_date) due_date_model.resolve_items(updated_due_date) due_date_model.resolve_permissions(updated_due_date, request.current_user_id) return due_date_model else: raise exc.NotFound(_("Due date %s not found") % id)
def put(self, story_id, story): """Modify this story. Example:: curl 'https://my.example.org/api/v1/stories/19' -X PUT \\ -H 'Authorization: Bearer MY_ACCESS_TOKEN' \\ -H 'Content-Type: application/json;charset=UTF-8' \\ --data-binary '{"title":"Modified","description":"New description."}' :param story_id: An ID of the story. :param story: A story within the request body. """ user_id = request.current_user_id # Reject private story types while ACL is not created. if (story.story_type_id and (story.story_type_id == 3 or story.story_type_id == 4)): abort( 400, _("Now you can't change story type to %s.") % story.story_type_id) original_story = stories_api.story_get_simple(story_id, current_user=user_id) if not original_story: raise exc.NotFound(_("Story %s not found") % story_id) if story.creator_id and story.creator_id != original_story.creator_id: abort(400, _("You can't change author of story.")) story_dict = story.as_dict(omit_unset=True) stories_api.story_check_story_type_id(story_dict) if not stories_api.story_can_mutate(original_story, story.story_type_id): abort(400, _("Can't change story type.")) # This is not the place to update tags, including them in # story_dict causes the story/tag relationship to attempt to # update with a list of unicode strings rather than objects # from the database. if 'tags' in story_dict: story_dict.pop('tags') users = story_dict.get("users") teams = story_dict.get("teams") private = story_dict.get("private", original_story.private) if private: # If trying to make a story private with no permissions set, add # the user making the change to the permission so that at least # the story isn't lost to everyone. if not users and not teams and not original_story.permissions: users = [ wmodels.User.from_db_model(users_api.user_get(user_id)) ] original_teams = None original_users = None if original_story.permissions: original_teams = original_story.permissions[0].teams original_users = original_story.permissions[0].users # Don't allow both permission lists to be deliberately emptied # on a private story, to make sure the story remains visible to # at least someone. valid = True if users == [] and teams == []: valid = False elif users == [] and (original_teams == [] and not teams): valid = False elif teams == [] and (original_users == [] and not users): valid = False if not valid and original_story.private: abort(400, _("Can't make a private story have no users or teams")) # If the story doesn't already have permissions, create them. if not original_story.permissions: stories_api.create_permission(original_story, users, teams) updated_story = stories_api.story_update(story_id, story_dict, current_user=user_id) # If the story is private and already has some permissions, update # them as needed. This is done after updating the story in case the # request is trying to both update some story fields and also remove # the user making the change from the ACL. if private and original_story.permissions: stories_api.update_permission(updated_story, users, teams) events_api.story_details_changed_event(story_id, user_id, updated_story.title) return create_story_wmodel(updated_story)
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) # Check if this item has an archived card in this worklist to restore archived = get_item_by_item_id(worklist, item_type, item_id, archived=True) if archived: update = {'archived': False, 'list_position': list_position} api_base.entity_update(models.WorklistItem, archived.id, update) return worklist # If this worklist is a lane, check if the item has an archived card # somewhere in the board to restore if is_lane(worklist): board = boards.get_from_lane(worklist) archived = boards.get_card(board, item_type, item_id, archived=True) if archived: update = { 'archived': False, 'list_id': worklist_id, 'list_position': list_position } api_base.entity_update(models.WorklistItem, archived.id, update) return worklist # Create a new card if item_type == 'story': item = stories_api.story_get(item_id, current_user=current_user) elif item_type == 'task': 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")) if item is None: raise exc.NotFound( _("%(type)s %(id)s not found") % { 'type': item_type, 'id': item_id }) item_dict = { 'list_id': worklist_id, 'item_id': item_id, 'item_type': item_type, 'list_position': list_position } worklist_item = api_base.entity_create(models.WorklistItem, item_dict) if worklist.items is None: worklist.items = [worklist_item] else: worklist.items.append(worklist_item) return worklist
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', current_user=None): # First build a standard story query. query = api_base.model_query(models.Story.id).distinct() # Apply basic filters query = api_base.apply_query_filters(query=query, model=models.Story, title=title, 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': for tag in tags: query = query.filter(models.Story.tags.any(name=tag)) elif tags_filter_type == 'any': query = query.filter( models.Story.tags.any(models.StoryTag.name.in_(tags))) else: raise exc.NotFound("Tags filter not found.") # Are we filtering by project group? if project_group_id: 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) # 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, models.Task.story_id == models.Story.id)) if assignee_id: query = query.filter(models.Task.assignee_id == assignee_id) if project_id: query = query.filter(models.Task.project_id == project_id) return query
def put(self, story_id, story): """Modify this story. :param story_id: An ID of the story. :param story: A story within the request body. """ # Reject private story types while ACL is not created. if (story.story_type_id and (story.story_type_id == 3 or story.story_type_id == 4)): abort( 400, _("Now you can't change story type to %s.") % story.story_type_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) if story.creator_id and story.creator_id != original_story.creator_id: abort(400, _("You can't change author of story.")) story_dict = story.as_dict(omit_unset=True) stories_api.story_check_story_type_id(story_dict) if not stories_api.story_can_mutate(original_story, story.story_type_id): abort(400, _("Can't change story type.")) # This is not the place to update tags, including them in # story_dict causes the story/tag relationship to attempt to # update with a list of unicode strings rather than objects # from the database. 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, 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, updated_story.title) return create_story_wmodel(updated_story)
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) # If the target position is "outside" the list, override it if list_position > worklist.items.count(): list_position = worklist.items.count() # Check if this item has an archived card in this worklist to restore archived = get_item_by_item_id(worklist, item_type, item_id, archived=True) if archived: update_item(archived.id, {'archived': False}) # Move the newly unarchived card into position, and move other cards # to compensate for the move move_item(archived.id, list_position) return archived # If this worklist is a lane, check if the item has an archived card # somewhere in the board to restore if is_lane(worklist): board = boards.get_from_lane(worklist) archived = boards.get_card(board, item_type, item_id, archived=True) if archived: update_item(archived.id, {'archived': False}) # Move the newly unarchived card into position, and move other # cards to compensate for the move move_item(archived.id, list_position, new_list_id=worklist_id) return archived # Create a new card if item_type == 'story': item = stories_api.story_get(item_id, current_user=current_user) elif item_type == 'task': 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")) if item is None: raise exc.NotFound( _("%(type)s %(id)s not found") % { 'type': item_type, 'id': item_id }) card_dict = { 'list_id': worklist_id, 'item_id': item_id, 'item_type': item_type, 'list_position': 99999 # Initialise the card "outside" the list } card = api_base.entity_create(models.WorklistItem, card_dict) # Move the card into position, and move other cards to compensate card = move_item(card.id, list_position) if worklist.items is None: worklist.items = [card] else: worklist.items.append(card) return card