This repository has been archived by the owner on Aug 1, 2019. It is now read-only.
/
wmodels.py
805 lines (593 loc) · 23.8 KB
/
wmodels.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
# Copyright (c) 2014 Mirantis Inc.
#
# 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.
from datetime import datetime
from pecan import request
from wsme import types as wtypes
from storyboard.api.v1 import base
from storyboard.common.custom_types import NameType
from storyboard.common import event_resolvers
from storyboard.common import event_types
from storyboard.db.api import boards as boards_api
from storyboard.db.api import comments as comments_api
from storyboard.db.api import due_dates as due_dates_api
from storyboard.db.api import stories as stories_api
from storyboard.db.api import tasks as tasks_api
from storyboard.db.api import worklists as worklists_api
from storyboard.db import models
class Comment(base.APIBase):
"""Any user may leave comments for stories. Also comments api is used by
gerrit to leave service comments.
"""
content = wtypes.text
"""The content of the comment."""
is_active = bool
"""Is this an active comment, or has it been deleted?"""
in_reply_to = int
"""The ID of the parent comment, if any."""
class SystemInfo(base.APIBase):
"""Represents the system information for Storyboard
"""
version = wtypes.text
"""The application version."""
@classmethod
def sample(cls):
return cls(
version="338c2d6")
class Project(base.APIBase):
"""The Storyboard Registry describes the open source world as ProjectGroups
and Projects. Each ProjectGroup may be responsible for several Projects.
For example, the OpenStack Infrastructure ProjectGroup has Zuul, Nodepool,
Storyboard as Projects, among others.
"""
name = NameType()
"""The Project unique name. This name will be displayed in the URL.
At least 3 alphanumeric symbols. Minus and dot symbols are allowed as
separators.
"""
description = wtypes.text
"""Details about the project's work, highlights, goals, and how to
contribute. Use plain text, paragraphs are preserved and URLs are
linked in pages.
"""
is_active = bool
"""Is this an active project, or has it been deleted?"""
repo_url = wtypes.text
"""This is a repo link for this project"""
autocreate_branches = bool
"""This flag means that storyboard will try to create task branches
automatically from the branches declared in the code repository.
"""
@classmethod
def sample(cls):
return cls(
name="StoryBoard",
description="This is an awesome project.",
is_active=True,
repo_url="git://git.openstack.org/openstack-infra/storyboard.git")
class ProjectGroup(base.APIBase):
"""Represents a group of projects."""
name = NameType()
"""The Project Group unique name. This name will be displayed in the URL.
At least 3 alphanumeric symbols. Minus and dot symbols are allowed as
separators.
"""
title = wtypes.text
"""The full name of the project group, which can contain spaces, special
characters, etc.
"""
@classmethod
def sample(cls):
return cls(
name="Infra",
title="Awesome projects")
class TaskStatusCount(base.APIBase):
"""Represents a task status and number of occurrences within a story."""
key = wtypes.text
count = int
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
broken down into a series of Tasks, which will each target a specific
Project and branch.
"""
title = wtypes.text
"""A descriptive label for the story, to show in listings."""
description = wtypes.text
"""A complete description of the goal this story wants to cover."""
is_bug = bool
"""Is this a bug or a feature :)"""
creator_id = int
"""User ID of the Story creator"""
story_type_id = int
"""ID of story type"""
status = wtypes.text
"""The derived status of the story, one of 'active', 'merged', 'invalid'"""
task_statuses = wtypes.ArrayType(TaskStatusCount)
"""The summary of each tasks/status."""
tags = wtypes.ArrayType(wtypes.text)
"""Tag list assigned to this story."""
due_dates = wtypes.ArrayType(int)
@classmethod
def sample(cls):
return cls(
title="Use Storyboard to manage Storyboard",
description="We should use Storyboard to manage Storyboard.",
is_bug=False,
creator_id=1,
task_statuses=[TaskStatusCount],
story_type_id=1,
status="active",
tags=["t1", "t2"])
def summarize_task_statuses(self, story_summary):
"""Populate the task_statuses array."""
self.task_statuses = []
for task_status in models.Task.TASK_STATUSES:
task_count = TaskStatusCount(
key=task_status, count=getattr(story_summary, task_status))
self.task_statuses.append(task_count)
class Tag(base.APIBase):
name = wtypes.text
"""The tag name"""
@classmethod
def sample(cls):
return cls(name="low_hanging_fruit")
class Task(base.APIBase):
"""A Task represents an actionable work item, targeting a specific Project
and a specific branch. It is part of a Story. There may be multiple tasks
in a story, pointing to different projects or different branches. Each task
is generally linked to a code change proposed in Gerrit.
"""
title = wtypes.text
"""An optional short label for the task, to show in listings."""
# TODO(ruhe): replace with enum
status = wtypes.text
"""Status.
Allowed values: ['todo', 'inprogress', 'invalid', 'review', 'merged'].
Human readable versions are left to the UI.
"""
creator_id = int
"""Id of the User who has created this Task"""
story_id = int
"""The ID of the corresponding Story."""
link = wtypes.text
"""A related resource for this task."""
project_id = int
"""The ID of the corresponding Project."""
assignee_id = int
"""The ID of the invidiual to whom this task is assigned."""
priority = wtypes.text
"""The priority for this task, one of 'low', 'medium', 'high'"""
branch_id = int
"""The ID of corresponding Branch"""
milestone_id = int
"""The ID of corresponding Milestone"""
due_dates = wtypes.ArrayType(int)
class Branch(base.APIBase):
"""Represents a branch."""
name = wtypes.text
"""The branch unique name. This name will be displayed in the URL.
At least 3 alphanumeric symbols.
"""
project_id = int
"""The ID of the corresponding Project."""
expired = bool
"""A binary flag that marks branches that should no longer be
selectable in tasks."""
expiration_date = datetime
"""Last date the expired flag was switched to True."""
autocreated = bool
"""A flag that marks autocreated entries, so that they can
be auto-expired when the corresponding branch is deleted in the git repo.
"""
restricted = bool
"""This flag marks branch as restricted."""
@classmethod
def sample(cls):
return cls(
name="Storyboard-branch",
project_id=1,
expired=True,
expiration_date=datetime(2015, 1, 1, 1, 1),
autocreated=False,
restricted=False
)
class Milestone(base.APIBase):
"""Represents a milestone."""
name = wtypes.text
"""The milestone unique name. This name will be displayed in the URL.
At least 3 alphanumeric symbols.
"""
branch_id = int
"""The ID of the corresponding Branch."""
expired = bool
"""a binary flag that marks milestones that should no longer be
selectable in completed tasks."""
expiration_date = datetime
"""Last date the expired flag was switched to True."""
@classmethod
def sample(cls):
return cls(
name="Storyboard-milestone",
branch_id=1,
expired=True,
expiration_date=datetime(2015, 1, 1, 1, 1)
)
class Team(base.APIBase):
"""The Team is a group od Users with a fixed set of permissions.
"""
name = NameType()
"""The Team unique name. This name will be displayed in the URL.
At least 3 alphanumeric symbols. Minus and dot symbols are allowed as
separators.
"""
description = wtypes.text
"""Details about the team.
"""
@classmethod
def sample(cls):
return cls(
name="StoryBoard-core",
description="Core reviewers of StoryBoard team.")
class TimeLineEvent(base.APIBase):
"""An event object should be created each time a story or a task state
changes.
"""
event_type = wtypes.text
"""This type should serve as a hint for the web-client when rendering
a comment."""
event_info = wtypes.text
"""A JSON encoded field with details about the event."""
story_id = int
"""The ID of the corresponding Story."""
author_id = int
"""The ID of User who has left the comment."""
comment_id = int
"""The id of a comment linked to this event."""
comment = Comment
"""The resolved comment."""
@staticmethod
def resolve_event_values(event):
if event.comment_id:
comment = comments_api.comment_get(event.comment_id)
event.comment = Comment.from_db_model(comment)
event = TimeLineEvent._resolve_info(event)
return event
@staticmethod
def _resolve_info(event):
if event.event_type == event_types.STORY_CREATED:
return event_resolvers.story_created(event)
elif event.event_type == event_types.STORY_DETAILS_CHANGED:
return event_resolvers.story_details_changed(event)
elif event.event_type == event_types.USER_COMMENT:
return event_resolvers.user_comment(event)
elif event.event_type == event_types.TASK_CREATED:
return event_resolvers.task_created(event)
elif event.event_type == event_types.TASK_STATUS_CHANGED:
return event_resolvers.task_status_changed(event)
elif event.event_type == event_types.TASK_PRIORITY_CHANGED:
return event_resolvers.task_priority_changed(event)
elif event.event_type == event_types.TASK_ASSIGNEE_CHANGED:
return event_resolvers.task_assignee_changed(event)
elif event.event_type == event_types.TASK_DETAILS_CHANGED:
return event_resolvers.task_details_changed(event)
elif event.event_type == event_types.TASK_DELETED:
return event_resolvers.task_deleted(event)
elif event.event_type == event_types.TAGS_ADDED:
return event_resolvers.tags_added(event)
elif event.event_type == event_types.TAGS_DELETED:
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."""
user_id = int
"""The ID of corresponding user."""
refresh_token = wtypes.text
"""The refresh token."""
expires_in = int
"""The number of seconds after creation when this token expires."""
@classmethod
def sample(cls):
return cls(
user_id=1,
refresh_token="a_unique_refresh_token",
expires_in=3600
)
class AccessToken(base.APIBase):
"""Represents a user access token."""
user_id = int
"""The ID of User to whom this token belongs."""
access_token = wtypes.text
"""The access token."""
expires_in = int
"""The number of seconds after creation when this token expires."""
refresh_token = RefreshToken
"""The associated refresh token."""
@classmethod
def sample(cls):
return cls(
user_id=1,
access_token="a_unique_access_token",
expires_in=3600)
class TaskStatus(base.APIBase):
key = wtypes.text
name = wtypes.text
class FilterCriterion(base.APIBase):
"""Represents a filter used to construct an automatic worklist."""
type = wtypes.text
"""The type of objects to filter, Story or Task."""
title = wtypes.text
"""The title of the criterion, as displayed in the UI."""
filter_id = int
"""The ID of the WorklistFilter this criterion is for."""
negative = bool
"""Whether to return all items matching or not matching the criterion."""
value = wtypes.text
"""The value to use as a criterion."""
field = wtypes.text
"""The field to filter by."""
class WorklistFilter(base.APIBase):
"""Represents a set of criteria to filter items using AND."""
type = wtypes.text
"""The type of objects to filter, Story or Task."""
list_id = int
"""The ID of the Worklist this filter is for."""
filter_criteria = wtypes.ArrayType(FilterCriterion)
"""The list of criteria to apply."""
def resolve_criteria(self, filter):
self.filter_criteria = [FilterCriterion.from_db_model(criterion)
for criterion in filter.criteria]
class DueDate(base.APIBase):
"""Represents a due date for tasks/stories."""
name = wtypes.text
"""The name of the due date."""
date = datetime
"""The date of the due date"""
private = bool
"""A flag to identify whether this is a private or public due date."""
creator_id = int
"""The ID of the user that created the DueDate."""
tasks = wtypes.ArrayType(Task)
"""A list containing all the tasks with this due date."""
stories = wtypes.ArrayType(Story)
"""A list containing all the stories with this due date."""
count = int
"""The number of tasks and stories with this dues date."""
owners = wtypes.ArrayType(int)
"""A list of the IDs of the users who can change this date."""
users = wtypes.ArrayType(int)
"""A list of the IDs of the users who can assign this date to tasks."""
board_id = int
"""The ID of a board which contains this due date.
Used by PUT requests adding the due date to a board."""
worklist_id = int
"""The ID of a worklist which contains this due date.
Used by PUT requests adding the due date to a worklist."""
editable = bool
"""Whether or not the due date is editable by the request sender."""
assignable = bool
"""Whether or not the due date is assignable by the request sender."""
def resolve_count_in_board(self, due_date, board):
self.count = 0
for lane in board.lanes:
for card in lane.worklist.items:
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]
self.stories = [Story.from_db_model(story)
for story in due_date.stories]
self.count = len(self.tasks) + len(self.stories)
def resolve_permissions(self, due_date, user=None):
"""Resolve the permissions groups of the due date."""
self.owners = due_dates_api.get_owners(due_date)
self.users = due_dates_api.get_users(due_date)
self.editable = due_dates_api.editable(due_date, user)
self.assignable = due_dates_api.assignable(due_date, user)
class WorklistItem(base.APIBase):
"""Represents an item in a worklist.
The item could be either a story or a task.
"""
list_id = int
"""The ID of the Worklist this item belongs to."""
item_id = int
"""The ID of the Task or Story for this item."""
item_type = wtypes.text
"""The type of this item, either "story" or "task"."""
list_position = int
"""The position of this item in the Worklist."""
archived = bool
"""Whether or not this item is archived."""
display_due_date = int
"""The ID of the due date displayed on this item."""
resolved_due_date = DueDate
"""The due date displayed on this item."""
task = Task
story = Story
def resolve_due_date(self, worklist_item):
due_date = due_dates_api.get(worklist_item.display_due_date)
resolved = None
if due_dates_api.visible(due_date, request.current_user_id):
resolved = DueDate.from_db_model(due_date)
self.resolved_due_date = resolved
def resolve_item(self, item):
user_id = request.current_user_id
if item.item_type == 'story':
story = stories_api.story_get(item.item_id)
if story is None:
return False
self.story = Story.from_db_model(story)
due_dates = [date.id for date in story.due_dates
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)
if task is None or task.story is None:
return False
self.task = Task.from_db_model(task)
due_dates = [date.id for date in task.due_dates
if due_dates_api.visible(date, user_id)]
self.task.due_dates = due_dates
return True
class Worklist(base.APIBase):
"""Represents a worklist."""
title = wtypes.text
"""The title of the worklist."""
creator_id = int
"""The ID of the User who created this worklist."""
project_id = int
"""The ID of the Project this worklist is associated with."""
permission_id = int
"""The ID of the Permission which defines who can edit this worklist."""
private = bool
"""A flag to identify if this is a private or public worklist."""
archived = bool
"""A flag to identify whether or not the worklist has been archived."""
automatic = bool
"""A flag to identify whether the contents are obtained by a filter or are
stored in the database."""
filters = wtypes.ArrayType(WorklistFilter)
"""A list of filters used if this is an "automatic" worklist."""
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."""
items = wtypes.ArrayType(WorklistItem)
"""The items in the worklist."""
def resolve_items(self, worklist):
"""Resolve the contents of this worklist."""
self.items = []
user_id = request.current_user_id
if worklist.automatic:
self._resolve_automatic_items(worklist, user_id)
else:
self._resolve_set_items(worklist, user_id)
def _resolve_automatic_items(self, worklist, user_id):
for item in worklists_api.filter_items(worklist):
item_model = WorklistItem(**item)
valid = item_model.resolve_item(item_model)
if not valid:
continue
item_model.resolve_due_date(item_model)
self.items.append(item_model)
self.items.sort(key=lambda x: x.list_position)
def _resolve_set_items(self, worklist, user_id):
for item in worklist.items:
if item.archived:
continue
item_model = WorklistItem.from_db_model(item)
valid = item_model.resolve_item(item)
if not valid:
continue
item_model.resolve_due_date(item)
self.items.append(item_model)
self.items.sort(key=lambda x: x.list_position)
def resolve_permissions(self, worklist):
self.owners = worklists_api.get_owners(worklist)
self.users = worklists_api.get_users(worklist)
def resolve_filters(self, worklist):
self.filters = []
for filter in worklist.filters:
model = WorklistFilter.from_db_model(filter)
model.resolve_criteria(filter)
self.filters.append(model)
class Lane(base.APIBase):
"""Represents a lane in a kanban board."""
board_id = int
"""The ID of the board containing the lane."""
list_id = int
"""The ID of the worklist which represents the lane."""
worklist = Worklist
"""The worklist which represents the lane."""
position = int
"""The position of the lane in the board."""
def resolve_list(self, lane, resolve_items=True):
"""Resolve the worklist which represents the lane."""
self.worklist = Worklist.from_db_model(lane.worklist)
self.worklist.resolve_permissions(lane.worklist)
if resolve_items:
self.worklist.resolve_items(lane.worklist)
else:
self.worklist.items = [WorklistItem.from_db_model(item)
for item in lane.worklist.items]
class Board(base.APIBase):
"""Represents a kanban board made up of worklists."""
title = wtypes.text
"""The title of the board."""
description = wtypes.text
"""The description of the board."""
creator_id = int
"""The ID of the User who created this board."""
project_id = int
"""The ID of the Project this board is associated with."""
permission_id = int
"""The ID of the Permission which defines who can edit this board."""
private = bool
"""A flag to identify whether this is a private or public board."""
archived = bool
"""A flag to identify whether or not the board has been archived."""
lanes = wtypes.ArrayType(Lane)
"""A list containing the representions of the lanes in this board."""
due_dates = wtypes.ArrayType(DueDate)
"""A list containing the due dates used 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."""
def resolve_lanes(self, board, resolve_items=True):
"""Resolve the lanes of the board."""
self.lanes = []
for lane in board.lanes:
lane_model = Lane.from_db_model(lane)
lane_model.resolve_list(lane, resolve_items)
self.lanes.append(lane_model)
self.lanes.sort(key=lambda x: x.position)
def resolve_due_dates(self, board):
self.due_dates = []
for due_date in board.due_dates:
if due_dates_api.visible(due_date, request.current_user_id):
due_date_model = DueDate.from_db_model(due_date)
due_date_model.resolve_items(due_date)
due_date_model.resolve_permissions(
due_date, request.current_user_id)
due_date_model.resolve_count_in_board(due_date, board)
self.due_dates.append(due_date_model)
def resolve_permissions(self, board):
"""Resolve the permissions groups of the board."""
self.owners = boards_api.get_owners(board)
self.users = boards_api.get_users(board)