Implement complex permissions for worklists and boards

We default to two levels of permissions, each with potential for
containing multiple users. This commit adds a database migration
which adds two tables (worklist_permissions and board_permissions)
to represent the relationships between worklists and permissions
and boards and permissions.

Board permissions override worklist permissions if the worklist is
a lane in that board.

Existing boards and worklists will have permissions created such
that the creator has full permissions on the board/worklist.

Change-Id: I4168572cc98a93a59d8fe6f35d48432959ac7adf
This commit is contained in:
Adam Coldrick 2015-12-02 16:09:14 +00:00
parent f7c2d2320c
commit 03d2a20fbe
8 changed files with 470 additions and 69 deletions

View File

@ -15,7 +15,6 @@
from oslo_config import cfg
from pecan import abort
from pecan import expose
from pecan import request
from pecan import response
from pecan import rest
@ -39,8 +38,9 @@ def visible(board, user=None):
if not board:
return False
if user and board.private:
# TODO(SotK): Permissions
return user == board.creator_id
permissions = boards_api.get_permissions(board.id, user)
return any(name in permissions
for name in ['edit_board', 'move_cards'])
return not board.private
@ -49,8 +49,7 @@ def editable(board, user=None):
return False
if not user:
return False
# TODO(SotK): Permissions
return user == board.creator_id
return 'edit_board' in boards_api.get_permissions(board.id, user)
def get_lane(list_id, board):
@ -79,6 +78,47 @@ def update_lanes(board_dict, board_id):
del board_dict['lanes']
class PermissionsController(rest.RestController):
"""Manages operations on board permissions."""
@decorators.db_exceptions
@secure(checks.guest)
@wsme_pecan.wsexpose([wtypes.text], int)
def get(self, board_id):
"""Get board permissions for the current user.
:param board_id: The ID of the board.
"""
return boards_api.get_permissions(board_id, request.current_user_id)
@decorators.db_exceptions
@secure(checks.authenticated)
@wsme_pecan.wsexpose(wtypes.text, int,
body=wtypes.DictType(wtypes.text, wtypes.text))
def post(self, board_id, permission):
"""Add a new permission to the board.
:param board_id: The ID of the board.
:param permission: The dict to use to create the permission.
"""
return boards_api.create_permission(board_id)
@decorators.db_exceptions
@secure(checks.authenticated)
@wsme_pecan.wsexpose(wtypes.text, int,
body=wtypes.DictType(wtypes.text, wtypes.text))
def put(self, board_id, permission):
"""Update a permission of the board.
:param board_id: The ID of the board.
:param permission: The new contents of the permission.
"""
return boards_api.update_permission(board_id, permission).codename
class BoardsController(rest.RestController):
"""Manages operations on boards."""
@ -98,6 +138,8 @@ class BoardsController(rest.RestController):
board_model = wmodels.Board.from_db_model(board)
board_model.lanes = [wmodels.Lane.from_db_model(lane)
for lane in board.lanes]
board_model.owners = boards_api.get_owners(id)
board_model.users = boards_api.get_users(id)
return board_model
else:
raise exc.NotFound(_("Board %s not found") % id)
@ -105,9 +147,10 @@ class BoardsController(rest.RestController):
@decorators.db_exceptions
@secure(checks.guest)
@wsme_pecan.wsexpose([wmodels.Board], wtypes.text, int, int,
bool, wtypes.text, wtypes.text)
bool, int, wtypes.text, wtypes.text)
def get_all(self, title=None, creator_id=None, project_id=None,
archived=False, sort_field='id', sort_dir='asc'):
archived=False, user_id=None, sort_field='id',
sort_dir='asc'):
"""Retrieve definitions of all of the boards.
:param title: A string to filter the title by.
@ -120,6 +163,7 @@ class BoardsController(rest.RestController):
"""
boards = boards_api.get_all(title=title,
creator_id=creator_id,
user_id=user_id,
project_id=project_id,
sort_field=sort_field,
sort_dir=sort_dir)
@ -131,6 +175,8 @@ class BoardsController(rest.RestController):
board_model = wmodels.Board.from_db_model(board)
board_model.lanes = [wmodels.Lane.from_db_model(lane)
for lane in board.lanes]
board_model.owners = boards_api.get_owners(board.id)
board_model.users = boards_api.get_users(board.id)
visible_boards.append(board_model)
# Apply the query response headers
@ -154,11 +200,30 @@ class BoardsController(rest.RestController):
abort(400, _("You can't select the creator of a board."))
board_dict.update({"creator_id": user_id})
lanes = board_dict.pop('lanes')
owners = board_dict.pop('owners')
users = board_dict.pop('users')
if not owners:
owners = [user_id]
if not users:
users = []
created_board = boards_api.create(board_dict)
for lane in lanes:
boards_api.add_lane(created_board.id, lane.as_dict())
edit_permission = {
'name': 'edit_board_%d' % created_board.id,
'codename': 'edit_board',
'users': owners
}
move_permission = {
'name': 'move_cards_%d' % created_board.id,
'codename': 'move_cards',
'users': users
}
boards_api.create_permission(created_board.id, edit_permission)
boards_api.create_permission(created_board.id, move_permission)
return wmodels.Board.from_db_model(created_board)
@decorators.db_exceptions
@ -177,13 +242,15 @@ class BoardsController(rest.RestController):
board_dict = board.as_dict(omit_unset=True)
update_lanes(board_dict, id)
boards_api.update(id, board_dict)
updated_board = boards_api.update(id, board_dict)
if visible(board, user_id):
board_model = wmodels.Board.from_db_model(board)
board_model = wmodels.Board.from_db_model(updated_board)
if board.lanes:
board_model.lanes = [wmodels.Lane.from_db_model(lane)
for lane in board.lanes]
board_model.owners = boards_api.get_owners(id)
board_model.users = boards_api.get_users(id)
return board_model
else:
raise exc.NotFound(_("Board %s not found") % id)
@ -202,40 +269,9 @@ class BoardsController(rest.RestController):
if not editable(board, user_id):
raise exc.NotFound(_("Board %s not found") % id)
board_dict = wmodels.Board.from_db_model(board).as_dict(
omit_unset=True)
board_dict['lanes'] = board.lanes
board_dict.update({"archived": True})
boards_api.update(id, board_dict)
boards_api.update(id, {"archived": True})
for lane in board_dict['lanes']:
worklist = lane.worklist
worklist_dict = wmodels.Worklist.from_db_model(worklist).as_dict(
omit_unset=True)
worklist_dict.update({'archived': True})
worklists_api.update(worklist.id, worklist_dict)
for lane in board.lanes:
worklists_api.update(lane.worklist.id, {"archived": True})
@decorators.db_exceptions
@secure(checks.guest)
@wsme_pecan.wsexpose(wtypes.DictType(str, bool), int)
def permissions(self, id):
"""Get the permissions the current user has for the board.
:param id: The ID of the board to check permissions for.
"""
board = boards_api.get(id)
user_id = request.current_user_id
return {
'edit_board': editable(board, user_id),
'move_cards': editable(board, user_id) # TODO(SotK): check lanes
}
@expose()
def _route(self, args, request):
if request.method == 'GET' and len(args) > 1:
if args[1] == "permissions":
# Request to a permissions endpoint
return self.permissions, [args[0]]
return super(BoardsController, self)._route(args, request)
permissions = PermissionsController()

View File

@ -472,6 +472,12 @@ class Worklist(base.APIBase):
"""A flag to identify whether the contents are obtained by a filter or are
stored in the database."""
owners = wtypes.ArrayType(int)
"""A list of the IDs of the users who have full permissions."""
users = wtypes.ArrayType(int)
"""A list of the IDs of the users who can move items in the worklist."""
# NOTE(SotK): Criteria/Criterion is used as the existing code in the webclient
# refers to such filters as Criteria.
@ -549,3 +555,9 @@ class Board(base.APIBase):
lanes = wtypes.ArrayType(Lane)
"""A list containing the representions of the lanes in this board."""
owners = wtypes.ArrayType(int)
"""A list of the IDs of the users who have full permissions."""
users = wtypes.ArrayType(int)
"""A list of the IDs of the users who can move cards in the board."""

View File

@ -26,6 +26,7 @@ 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 boards as boards_api
from storyboard.db.api import worklists as worklists_api
from storyboard.openstack.common.gettextutils import _ # noqa
@ -39,9 +40,17 @@ def visible(worklist, user=None, hide_lanes=False):
return False
if not worklist:
return False
if worklists_api.is_lane(worklist):
board = boards_api.get_from_lane(worklist)
permissions = boards_api.get_permissions(board.id, user)
if board.private:
return any(name in permissions
for name in ['edit_board', 'move_cards'])
return not board.private
if user and worklist.private:
# TODO(SotK): Permissions
return user == worklist.creator_id
permissions = worklists_api.get_permissions(worklist.id, user)
return any(name in permissions
for name in ['edit_worklist', 'move_items'])
return not worklist.private
@ -50,8 +59,55 @@ def editable(worklist, user=None):
return False
if not user:
return False
# TODO(SotK): Permissions
return user == worklist.creator_id
if worklists_api.is_lane(worklist):
board = boards_api.get_from_lane(worklist)
permissions = boards_api.get_permissions(board.id, user)
return any(name in permissions
for name in ['edit_board', 'move_cards'])
return 'edit_worklist' in worklists_api.get_permissions(worklist.id, user)
class PermissionsController(rest.RestController):
"""Manages operations on worklist permissions."""
@decorators.db_exceptions
@secure(checks.guest)
@wsme_pecan.wsexpose([wtypes.text], int)
def get(self, worklist_id):
"""Get worklist permissions for the current user.
:param worklist_id: The ID of the worklist.
"""
return worklists_api.get_permissions(
worklist_id, request.current_user_id)
@decorators.db_exceptions
@secure(checks.authenticated)
@wsme_pecan.wsexpose(wtypes.text, int,
body=wtypes.DictType(wtypes.text, wtypes.text))
def post(self, worklist_id, permission):
"""Add a new permission to the worklist.
:param worklist_id: The ID of the worklist.
:param permission: The dict to use to create the permission.
"""
return worklists_api.create_permission(worklist_id)
@decorators.db_exceptions
@secure(checks.authenticated)
@wsme_pecan.wsexpose(wtypes.text, int,
body=wtypes.DictType(wtypes.text, wtypes.text))
def put(self, worklist_id, permission):
"""Update a permission of the worklist.
:param worklist_id: The ID of the worklist.
:param permission: The new contents of the permission.
"""
return worklists_api.update_permission(
worklist_id, permission).codename
class ItemsSubcontroller(rest.RestController):
@ -149,23 +205,27 @@ class WorklistsController(rest.RestController):
user_id = request.current_user_id
if worklist and visible(worklist, user_id):
return wmodels.Worklist.from_db_model(worklist)
worklist_model = wmodels.Worklist.from_db_model(worklist)
worklist_model.owners = worklists_api.get_owners(worklist.id)
worklist_model.users = worklists_api.get_users(worklist.id)
return worklist_model
else:
raise exc.NotFound(_("Worklist %s not found") % worklist_id)
@decorators.db_exceptions
@secure(checks.guest)
@wsme_pecan.wsexpose([wmodels.Worklist], wtypes.text, int, int,
bool, bool, wtypes.text, wtypes.text, int)
bool, int, bool, wtypes.text, wtypes.text, int)
def get_all(self, title=None, creator_id=None, project_id=None,
archived=False, hide_lanes=True, sort_field='id',
sort_dir='asc', board_id=None):
archived=False, user_id=None, hide_lanes=True,
sort_field='id', sort_dir='asc', board_id=None):
"""Retrieve definitions of all of the worklists.
:param title: A string to filter the title by.
:param creator_id: Filter worklists by their creator.
:param project_id: Filter worklists by project ID.
:param archived: Filter worklists by whether they are archived or not.
:param user_id: Filter worklists by the users with permissions.
:param hide_lanes: If true, don't return worklists which are lanes in
a board.
:param sort_field: The name of the field to sort on.
@ -179,14 +239,19 @@ class WorklistsController(rest.RestController):
project_id=project_id,
archived=archived,
board_id=board_id,
user_id=user_id,
sort_field=sort_field,
sort_dir=sort_dir)
user_id = request.current_user_id
visible_worklists = [wmodels.Worklist.from_db_model(w)
for w in worklists
if w.archived == archived
and visible(w, user_id, hide_lanes)]
visible_worklists = []
for worklist in worklists:
if (visible(worklist, user_id, hide_lanes) and
worklist.archived == archived):
worklist_model = wmodels.Worklist.from_db_model(worklist)
worklist_model.owners = worklists_api.get_owners(worklist.id)
worklist_model.users = worklists_api.get_users(worklist.id)
visible_worklists.append(worklist_model)
# Apply the query response headers
response.headers['X-Total'] = str(len(visible_worklists))
@ -208,9 +273,28 @@ class WorklistsController(rest.RestController):
if worklist.creator_id and worklist.creator_id != user_id:
abort(400, _("You can't select the creator of a worklist."))
worklist_dict.update({"creator_id": user_id})
owners = worklist_dict.pop('owners')
users = worklist_dict.pop('users')
if not owners:
owners = [user_id]
if not users:
users = []
created_worklist = worklists_api.create(worklist_dict)
edit_permission = {
'name': 'edit_worklist_%d' % created_worklist.id,
'codename': 'edit_worklist',
'users': owners
}
move_permission = {
'name': 'move_items_%d' % created_worklist.id,
'codename': 'move_items',
'users': users
}
worklists_api.create_permission(created_worklist.id, edit_permission)
worklists_api.create_permission(created_worklist.id, move_permission)
return wmodels.Worklist.from_db_model(created_worklist)
@decorators.db_exceptions
@ -230,7 +314,14 @@ class WorklistsController(rest.RestController):
updated_worklist = worklists_api.update(
id, worklist.as_dict(omit_unset=True))
return wmodels.Worklist.from_db_model(updated_worklist)
if visible(updated_worklist, user_id):
worklist_model = wmodels.Worklist.from_db_model(updated_worklist)
worklist_model.owners = worklists_api.get_owners(
updated_worklist.id)
worklist_model.users = worklists_api.get_users(updated_worklist.id)
return worklist_model
else:
raise exc.NotFound(_("Worklist %s not found"))
@decorators.db_exceptions
@secure(checks.authenticated)
@ -246,8 +337,7 @@ class WorklistsController(rest.RestController):
if not editable(worklist, user_id):
raise exc.NotFound(_("Worklist %s not found") % worklist_id)
worklist_dict = wmodels.Worklist.from_db_model(worklist).as_dict()
worklist_dict.update({"archived": True})
worklists_api.update(worklist_id, worklist_dict)
worklists_api.update(worklist_id, {"archived": True})
items = ItemsSubcontroller()
permissions = PermissionsController()

View File

@ -18,6 +18,7 @@ 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 users as users_api
from storyboard.db import models
from storyboard.openstack.common.gettextutils import _ # noqa
@ -35,8 +36,17 @@ def get(id):
return _board_get(id)
def get_all(title=None, creator_id=None, project_id=None,
def get_all(title=None, creator_id=None, user_id=None, project_id=None,
sort_field=None, sort_dir=None, **kwargs):
if user_id is not None:
user = users_api.user_get(user_id)
boards = []
for board in get_all():
if any(permission in board.permissions
for permission in user.permissions):
boards.append(board)
return boards
return api_base.entity_get_all(models.Board,
title=title,
creator_id=creator_id,
@ -82,3 +92,62 @@ def update_lane(board_id, lane, new_lane):
raise ClientSideError(_("A lane must have a worklist_id."))
api_base.entity_update(models.BoardWorklist, lane.id, new_lane)
def get_from_lane(worklist):
for board in get_all():
if worklist.id in [lane.list_id for lane in board.lanes]:
return board
def get_owners(board_id):
board = _board_get(board_id)
for permission in board.permissions:
if permission.codename == 'edit_board':
return [user.id for user in permission.users]
def get_users(board_id):
board = _board_get(board_id)
for permission in board.permissions:
if permission.codename == 'move_cards':
return [user.id for user in permission.users]
def get_permissions(board_id, user_id):
board = _board_get(board_id)
user = users_api.user_get(user_id)
if user is not None:
return [permission.codename for permission in board.permissions
if permission in user.permissions]
return []
def create_permission(board_id, permission_dict, session=None):
board = _board_get(board_id, session=session)
users = permission_dict.pop('users')
permission = api_base.entity_create(
models.Permission, permission_dict, session=session)
board.permissions.append(permission)
for user_id in users:
user = users_api.user_get(user_id, session=session)
user.permissions.append(permission)
return permission
def update_permission(board_id, permission_dict):
board = _board_get(board_id)
id = None
for permission in board.permissions:
if permission.codename == permission_dict['codename']:
id = permission.id
users = permission_dict.pop('users')
permission_dict['users'] = []
for user_id in users:
user = users_api.user_get(user_id)
permission_dict['users'].append(user)
if id is None:
raise ClientSideError(_("Permission %s does not exist")
% permission_dict['codename'])
return api_base.entity_update(models.Permission, id, permission_dict)

View File

@ -18,9 +18,10 @@ from storyboard.db import models
from storyboard.plugin.user_preferences import PREFERENCE_DEFAULTS
def user_get(user_id, filter_non_public=False):
def user_get(user_id, filter_non_public=False, session=None):
entity = api_base.entity_get(models.User, user_id,
filter_non_public=filter_non_public)
filter_non_public=filter_non_public,
session=session)
return entity

View File

@ -21,6 +21,7 @@ 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 users as users_api
from storyboard.db import models
from storyboard.openstack.common.gettextutils import _ # noqa
@ -38,9 +39,17 @@ def get(worklist_id):
return _worklist_get(worklist_id)
def get_all(title=None, creator_id=None, project_id=None,
board_id=None, sort_field=None, sort_dir=None,
**kwargs):
def get_all(title=None, creator_id=None, project_id=None, board_id=None,
user_id=None, sort_field=None, sort_dir=None, **kwargs):
if user_id is not None:
user = users_api.user_get(user_id)
worklists = []
for worklist in get_all():
if any(permission in worklist.permissions
for permission in user.permissions):
worklists.append(worklist)
return worklists
if board_id is not None:
board = boards.get(board_id)
return [lane.worklist for lane in board.lanes]
@ -168,3 +177,56 @@ def is_lane(worklist):
if lanes:
return True
return False
def get_owners(worklist_id):
worklist = _worklist_get(worklist_id)
for permission in worklist.permissions:
if permission.codename == 'edit_worklist':
return [user.id for user in permission.users]
def get_users(worklist_id):
worklist = _worklist_get(worklist_id)
for permission in worklist.permissions:
if permission.codename == 'move_items':
return [user.id for user in permission.users]
def get_permissions(worklist_id, user_id):
worklist = _worklist_get(worklist_id)
user = users_api.user_get(user_id)
if user is not None:
return [permission.codename for permission in worklist.permissions
if permission in user.permissions]
return []
def create_permission(worklist_id, permission_dict, session=None):
worklist = _worklist_get(worklist_id, session=session)
users = permission_dict.pop('users')
permission = api_base.entity_create(
models.Permission, permission_dict, session=session)
worklist.permissions.append(permission)
for user_id in users:
user = users_api.user_get(user_id, session=session)
user.permissions.append(permission)
return permission
def update_permission(worklist_id, permission_dict):
worklist = _worklist_get(worklist_id)
id = None
for permission in worklist.permissions:
if permission.codename == permission_dict['codename']:
id = permission.id
users = permission_dict.pop('users')
permission_dict['users'] = []
for user_id in users:
user = users_api.user_get(user_id)
permission_dict['users'].append(user)
if id is None:
raise ClientSideError(_("Permission %s does not exist")
% permission_dict['codename'])
return api_base.entity_update(models.Permission, id, permission_dict)

View File

@ -0,0 +1,118 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""Add detailed permissions to boards and worklists
Revision ID: 050
Revises: 049
Create Date: 2015-10-09 10:25:47.338906
"""
# revision identifiers, used by Alembic.
revision = '050'
down_revision = '049'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
from storyboard.db.api import base as api_base
from storyboard.db.api import boards
from storyboard.db.api import worklists
from storyboard.db import models
def upgrade(active_plugins=None, options=None):
op.create_table('board_permissions',
sa.Column('board_id', sa.Integer(), nullable=True),
sa.Column('permission_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['board_id'], ['boards.id'], ),
sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], )
)
op.create_table('worklist_permissions',
sa.Column('worklist_id', sa.Integer(), nullable=True),
sa.Column('permission_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], ),
sa.ForeignKeyConstraint(['worklist_id'], ['worklists.id'], )
)
session = api_base.get_session(in_request=False)
for board in boards.get_all(session=session):
edit_permission = {
'name': 'edit_board_%d' % board.id,
'codename': 'edit_board',
'users': [board.creator_id]
}
move_permission = {
'name': 'move_cards_%d' % board.id,
'codename': 'move_cards',
'users': []
}
print('Creating permissions for Board with id: %d' % board.id)
boards.create_permission(board.id, edit_permission, session=session)
boards.create_permission(board.id, move_permission, session=session)
for worklist in worklists.get_all(session=session):
edit_permission = {
'name': 'edit_worklist_%d' % worklist.id,
'codename': 'edit_worklist',
'users': [worklist.creator_id]
}
move_permission = {
'name': 'move_items_%d' % worklist.id,
'codename': 'move_items',
'users': []
}
print('Creating permissions for Worklist with id: %d' % worklist.id)
worklists.create_permission(
worklist.id, edit_permission, session=session)
worklists.create_permission(
worklist.id, move_permission, session=session)
session.flush()
op.drop_constraint(u'boards_ibfk_2', 'boards', type_='foreignkey')
op.drop_column(u'boards', 'permission_id')
op.drop_constraint(u'worklists_ibfk_2', 'worklists', type_='foreignkey')
op.drop_column(u'worklists', 'permission_id')
def downgrade(active_plugins=None, options=None):
op.add_column(u'worklists', sa.Column('permission_id',
mysql.INTEGER(display_width=11), autoincrement=False,
nullable=True))
op.create_foreign_key(u'worklists_ibfk_2', 'worklists', 'permissions',
['permission_id'], ['id'])
op.add_column(u'boards', sa.Column('permission_id',
mysql.INTEGER(display_width=11), autoincrement=False,
nullable=True))
op.create_foreign_key(u'boards_ibfk_2', 'boards', 'permissions',
['permission_id'], ['id'])
session = api_base.get_session(in_request=False)
to_delete = []
for board in boards.get_all(session=session):
for permission in board.permissions:
to_delete.append(permission)
for worklist in worklists.get_all(session=session):
for permission in worklist.permissions:
to_delete.append(permission)
op.drop_table('worklist_permissions')
op.drop_table('board_permissions')
for permission in to_delete:
api_base.entity_hard_delete(
models.Permission, permission.id, session=session)

View File

@ -150,7 +150,8 @@ class User(FullText, ModelBuilder, Base):
is_superuser = Column(Boolean, default=False)
last_login = Column(UTCDateTime)
teams = relationship("Team", secondary="team_membership")
permissions = relationship("Permission", secondary="user_permissions")
permissions = relationship(
"Permission", secondary="user_permissions", backref="users")
enable_login = Column(Boolean, default=True)
preferences = relationship("UserPreference")
@ -544,12 +545,12 @@ class Worklist(FullText, ModelBuilder, Base):
title = Column(Unicode(CommonLength.top_middle_length), nullable=True)
creator_id = Column(Integer, ForeignKey('users.id'))
project_id = Column(Integer, ForeignKey('projects.id'))
permission_id = Column(Integer, ForeignKey('permissions.id'))
private = Column(Boolean, default=False)
archived = Column(Boolean, default=False)
automatic = Column(Boolean, default=False)
items = relationship(WorklistItem)
criteria = relationship(WorklistCriteria)
permissions = relationship("Permission", secondary="worklist_permissions")
_public_fields = ["id", "title", "creator_id", "project_id",
"permission_id", "private", "archived", "automatic"]
@ -574,10 +575,22 @@ class Board(FullText, ModelBuilder, Base):
description = Column(UnicodeText(), nullable=True)
creator_id = Column(Integer, ForeignKey('users.id'))
project_id = Column(Integer, ForeignKey('projects.id'))
permission_id = Column(Integer, ForeignKey('permissions.id'))
private = Column(Boolean, default=False)
archived = Column(Boolean, default=False)
lanes = relationship(BoardWorklist)
permissions = relationship("Permission", secondary="board_permissions")
_public_fields = ["id", "title", "description", "creator_id",
"project_id", "permission_id", "private", "archived"]
board_permissions = Table(
'board_permissions', Base.metadata,
Column('board_id', Integer, ForeignKey('boards.id')),
Column('permission_id', Integer, ForeignKey('permissions.id')),
)
worklist_permissions = Table(
'worklist_permissions', Base.metadata,
Column('worklist_id', Integer, ForeignKey('worklists.id')),
Column('permission_id', Integer, ForeignKey('permissions.id')),
)