-
-
Notifications
You must be signed in to change notification settings - Fork 79
Expand file tree
/
Copy pathproject.v
More file actions
322 lines (299 loc) · 9.25 KB
/
Copy pathproject.v
File metadata and controls
322 lines (299 loc) · 9.25 KB
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
// Copyright (c) 2019-2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by a GPL license that can be found in the LICENSE file.
module main
import time
import veb
struct Project {
id int @[primary; sql: serial]
mut:
repo_id int
name string
description string
created_at int
label_id int
milestone_id int
iteration_id int
assignee_id int
}
struct ProjectColumn {
id int @[primary; sql: serial]
mut:
project_id int
name string
position int
wip_limit int
}
struct ProjectCard {
id int @[primary; sql: serial]
mut:
column_id int
title string
note string
position int
issue_id int // 0 if a free-form note
created_at int
}
fn (p &Project) formatted_name() veb.RawHtml {
return html_escape_text(p.name)
}
fn (mut app App) add_project(repo_id int, name string, description string) !int {
return app.add_advanced_project(repo_id, name, description, 0, 0, 0, 0)
}
fn (mut app App) add_advanced_project(repo_id int, name string, description string, label_id int,
milestone_id int, iteration_id int, assignee_id int) !int {
if repo_id <= 0 || !valid_short_name(name) || !valid_body(description) {
return error('invalid project board')
}
if label_id > 0 && app.find_repo_label_by_id(repo_id, label_id) == none {
return error('board label filter belongs to another project')
}
if milestone_id > 0 {
milestone := app.find_milestone(milestone_id) or { return error('board milestone not found') }
if milestone.repo_id != repo_id {
return error('board milestone filter belongs to another project')
}
}
if iteration_id > 0 {
iteration := app.find_iteration(iteration_id) or { return error('board iteration not found') }
repo := app.find_repo_by_id(repo_id) or { return error('project not found') }
org := app.get_org_by_id(iteration.org_id) or { return error('iteration group not found') }
if repo.user_name != org.name && !repo.user_name.starts_with(org.name + '/') {
return error('board iteration filter belongs to another group')
}
}
if assignee_id > 0 {
repo := app.find_repo_by_id(repo_id) or { return error('project not found') }
if app.repo_access_level(assignee_id, repo) < project_access_reporter {
return error('board assignee filter is not a project member')
}
}
project_id := db_insert_returning_id(mut app.db, 'Project', ['repo_id', 'name', 'description',
'created_at', 'label_id', 'milestone_id', 'iteration_id', 'assignee_id'], [
repo_id.str(),
name.trim_space(),
description,
int(time.now().unix()).str(),
label_id.str(),
milestone_id.str(),
iteration_id.str(),
assignee_id.str(),
])!
if project_id != 0 {
for i, col_name in ['Todo', 'In progress', 'Done'] {
app.add_project_column(project_id, col_name, i) or {
app.delete_project(project_id) or {}
return err
}
}
}
return project_id
}
fn (mut app App) list_repo_projects(repo_id int) []Project {
return sql app.db {
select from Project where repo_id == repo_id order by id desc
} or { []Project{} }
}
fn (mut app App) find_project(id int) ?Project {
rows := sql app.db {
select from Project where id == id limit 1
} or { []Project{} }
if rows.len == 0 {
return none
}
return rows.first()
}
fn (mut app App) delete_project(id int) ! {
cols := app.list_project_columns(id)
for col in cols {
sql app.db {
delete from ProjectCard where column_id == col.id
}!
}
sql app.db {
delete from ProjectColumn where project_id == id
}!
sql app.db {
delete from Project where id == id
}!
}
fn (mut app App) delete_repo_projects(repo_id int) ! {
prs := app.list_repo_projects(repo_id)
for pr in prs {
app.delete_project(pr.id) or {}
}
}
fn (mut app App) add_project_column(project_id int, name string, position int) !int {
return app.add_project_column_with_limit(project_id, name, position, 0)
}
fn (mut app App) add_project_column_with_limit(project_id int, name string, position int,
wip_limit int) !int {
if project_id <= 0 || !valid_short_name(name) || position < 0 || wip_limit < 0 || wip_limit > 10000 {
return error('invalid board column')
}
return db_insert_returning_id(mut app.db, 'ProjectColumn', ['project_id', 'name', 'position',
'wip_limit'], [
project_id.str(),
name.trim_space(),
position.str(),
wip_limit.str(),
])
}
fn (mut app App) list_project_columns(project_id int) []ProjectColumn {
return sql app.db {
select from ProjectColumn where project_id == project_id order by position
} or { []ProjectColumn{} }
}
fn (mut app App) find_project_column(id int) ?ProjectColumn {
rows := sql app.db {
select from ProjectColumn where id == id limit 1
} or { []ProjectColumn{} }
if rows.len == 0 {
return none
}
return rows.first()
}
fn (mut app App) delete_project_column(id int) ! {
sql app.db {
delete from ProjectCard where column_id == id
}!
sql app.db {
delete from ProjectColumn where id == id
}!
}
fn (mut app App) add_project_card(column_id int, title string, note string) ! {
app.add_project_card_for_issue(column_id, title, note, 0)!
}
fn (mut app App) add_project_card_for_issue(column_id int, title string, note string, issue_id int) ! {
column := app.find_project_column(column_id) or { return error('board column not found') }
project := app.find_project(column.project_id) or { return error('board not found') }
if !valid_title(title) || !valid_body(note) {
return error('invalid board card')
}
if issue_id > 0 {
issue := app.find_issue_by_id(issue_id) or { return error('issue not found') }
if issue.repo_id != project.repo_id || issue.is_pr {
return error('issue belongs to another project')
}
}
project_columns := app.list_project_columns(project.id)
mut tx := db_begin_transaction(mut app.db)!
mut committed := false
defer {
if !committed {
tx.rollback() or {}
}
}
locked := tx.execute('update ${sql_table('Project')} set ${sql_table('id')} = ${sql_table('id')}
where ${sql_table('id')} = ${project.id} returning ${sql_table('id')}')!
if locked.len != 1 {
return error('board not found')
}
pos := sql tx {
select count from ProjectCard where column_id == column_id
} or { 0 }
if column.wip_limit > 0 && pos >= column.wip_limit {
return error('column work-in-progress limit has been reached')
}
if issue_id > 0 {
for project_column in project_columns {
project_column_id := project_column.id
duplicate_count := sql tx {
select count from ProjectCard where column_id == project_column_id && issue_id == issue_id
} or { 0 }
if duplicate_count > 0 {
return error('issue is already on this board')
}
}
}
c := ProjectCard{
column_id: column_id
title: title
note: note
position: pos
issue_id: issue_id
created_at: int(time.now().unix())
}
sql tx {
insert c into ProjectCard
}!
tx.commit()!
committed = true
}
fn (mut app App) list_project_cards(column_id int) []ProjectCard {
return sql app.db {
select from ProjectCard where column_id == column_id order by position
} or { []ProjectCard{} }
}
fn (mut app App) find_project_card(id int) ?ProjectCard {
rows := sql app.db {
select from ProjectCard where id == id limit 1
} or { []ProjectCard{} }
if rows.len == 0 {
return none
}
return rows.first()
}
fn (mut app App) move_project_card(card_id int, new_column_id int) ! {
card := app.find_project_card(card_id) or { return error('board card not found') }
destination := app.find_project_column(new_column_id) or { return error('board column not found') }
source := app.find_project_column(card.column_id) or { return error('board column not found') }
if source.project_id != destination.project_id {
return error('card cannot move to another board')
}
mut tx := db_begin_transaction(mut app.db)!
mut committed := false
defer {
if !committed {
tx.rollback() or {}
}
}
locked := tx.execute('update ${sql_table('Project')} set ${sql_table('id')} = ${sql_table('id')}
where ${sql_table('id')} = ${source.project_id} returning ${sql_table('id')}')!
if locked.len != 1 {
return error('board not found')
}
if card.column_id != new_column_id && destination.wip_limit > 0 {
count := sql tx {
select count from ProjectCard where column_id == new_column_id
} or { 0 }
if count >= destination.wip_limit {
return error('column work-in-progress limit has been reached')
}
}
sql tx {
update ProjectCard set column_id = new_column_id where id == card_id
}!
tx.commit()!
committed = true
}
fn (app &App) board_candidate_issues(project Project) []Issue {
mut candidates := sql app.db {
select from Issue where repo_id == project.repo_id && is_pr == false order by id desc
} or { []Issue{} }
if project.label_id > 0 {
links := sql app.db {
select from IssueLabel where label_id == project.label_id
} or { []IssueLabel{} }
ids := links.map(it.issue_id)
candidates = candidates.filter(it.id in ids)
}
if project.milestone_id > 0 {
candidates = candidates.filter(it.milestone_id == project.milestone_id)
}
if project.iteration_id > 0 {
candidates = candidates.filter(it.iteration_id == project.iteration_id)
}
if project.assignee_id > 0 {
links := sql app.db {
select from IssueAssignee where user_id == project.assignee_id
} or { []IssueAssignee{} }
ids := links.map(it.issue_id)
candidates = candidates.filter(it.id in ids)
}
return candidates
}
fn (mut app App) delete_project_card(id int) ! {
sql app.db {
delete from ProjectCard where id == id
}!
}