Loggest thine Stuff
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

450 lines
10 KiB

2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
  1. package projects
  2. import (
  3. "context"
  4. "git.aiterp.net/stufflog3/stufflog3/entities"
  5. "git.aiterp.net/stufflog3/stufflog3/internal/genutils"
  6. "git.aiterp.net/stufflog3/stufflog3/internal/validate"
  7. "git.aiterp.net/stufflog3/stufflog3/models"
  8. "git.aiterp.net/stufflog3/stufflog3/usecases/auth"
  9. "git.aiterp.net/stufflog3/stufflog3/usecases/items"
  10. "git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
  11. "git.aiterp.net/stufflog3/stufflog3/usecases/stats"
  12. "strconv"
  13. "strings"
  14. "time"
  15. )
  16. type Service struct {
  17. Scopes *scopes.Service
  18. Stats *stats.Service
  19. Items *items.Service
  20. Auth *auth.Service
  21. Repository Repository
  22. }
  23. func (s *Service) Find(ctx context.Context, id int) (*Result, error) {
  24. sc := s.Scopes.Context(ctx)
  25. project, err := s.Repository.Find(ctx, sc.ID, id)
  26. if err != nil {
  27. return nil, err
  28. }
  29. requirements, requirementStats, err := s.Repository.ListRequirements(ctx, id)
  30. if err != nil {
  31. return nil, err
  32. }
  33. items, err := s.Items.ListScoped(ctx, models.ItemFilter{
  34. ProjectIDs: []int{id},
  35. })
  36. if err != nil {
  37. return nil, err
  38. }
  39. return generateResult(
  40. *project,
  41. sc.Scope,
  42. requirements,
  43. requirementStats,
  44. items,
  45. ), nil
  46. }
  47. func (s *Service) List(ctx context.Context) ([]Entry, error) {
  48. projects, err := s.Repository.List(ctx, s.Scopes.Context(ctx).ID)
  49. if err != nil {
  50. return nil, err
  51. }
  52. entries := make([]Entry, 0, len(projects))
  53. for _, project := range projects {
  54. entries = append(entries, generateEntry(project, s.Scopes.Context(ctx).Scope))
  55. }
  56. return entries, nil
  57. }
  58. func (s *Service) ListRequirementAllScopes(ctx context.Context) ([]RequirementResult, error) {
  59. allScopes, err := s.Scopes.Context(ctx).Scopes(ctx)
  60. if err != nil {
  61. return nil, err
  62. }
  63. var res []RequirementResult
  64. for _, scope := range allScopes {
  65. scopedRes, err := s.listRequirements(ctx, scope)
  66. if err != nil {
  67. return nil, err
  68. }
  69. res = append(res, scopedRes...)
  70. }
  71. return res, nil
  72. }
  73. func (s *Service) ListRequirements(ctx context.Context) ([]RequirementResult, error) {
  74. return s.listRequirements(ctx, s.Scopes.Context(ctx).Scope)
  75. }
  76. func (s *Service) listRequirements(ctx context.Context, sc scopes.Result) ([]RequirementResult, error) {
  77. requirements, requirementStats, err := s.Repository.ListRequirementsByScope(ctx, sc.ID)
  78. if err != nil {
  79. return nil, err
  80. }
  81. results := make([]RequirementResult, 0, len(requirements))
  82. for _, req := range requirements {
  83. results = append(results, generateRequirementResult(
  84. req,
  85. sc,
  86. requirementStats,
  87. nil,
  88. ))
  89. }
  90. return results, nil
  91. }
  92. func (s *Service) ListByTags(ctx context.Context, tags []string) (interface{}, error) {
  93. projects, err := s.Repository.ListByTags(ctx, s.Scopes.Context(ctx).ID, tags)
  94. if err != nil {
  95. return nil, err
  96. }
  97. entries := make([]Entry, 0, len(projects))
  98. for _, project := range projects {
  99. entries = append(entries, generateEntry(project, s.Scopes.Context(ctx).Scope))
  100. }
  101. return entries, nil
  102. }
  103. func (s *Service) FetchRequirements(ctx context.Context, ids ...int) ([]RequirementResult, error) {
  104. sc := s.Scopes.Context(ctx)
  105. requirements, requirementStats, err := s.Repository.FetchRequirements(ctx, sc.ID, ids...)
  106. if err != nil {
  107. return nil, err
  108. }
  109. items, err := s.Items.ListScoped(ctx, models.ItemFilter{
  110. RequirementIDs: ids,
  111. })
  112. if err != nil {
  113. return nil, err
  114. }
  115. results := make([]RequirementResult, 0, len(requirements))
  116. for _, req := range requirements {
  117. results = append(results, generateRequirementResult(
  118. req,
  119. sc.Scope,
  120. requirementStats,
  121. items,
  122. ))
  123. }
  124. return results, nil
  125. }
  126. func (s *Service) Create(ctx context.Context, project entities.Project) (*Result, error) {
  127. project.Name = strings.Trim(project.Name, "  \t\r\n")
  128. if project.Name == "" {
  129. return nil, models.BadInputError{
  130. Object: "ProjectInput",
  131. Field: "name",
  132. Problem: "Empty name provided",
  133. }
  134. }
  135. if !project.Status.Valid() {
  136. return nil, models.BadInputError{
  137. Object: "ProjectInput",
  138. Field: "status",
  139. Problem: "Non-existent status ID",
  140. }
  141. }
  142. err := validate.Tags(nil, project.Tags, nil)
  143. if err != nil {
  144. return nil, err
  145. }
  146. // Allow importing and scripts to mess with the created time, so only set it to now if it's not set.
  147. if project.CreatedTime.IsZero() {
  148. project.CreatedTime = time.Now()
  149. }
  150. project.OwnerID = s.Auth.GetUser(ctx).ID
  151. project.ScopeID = s.Scopes.Context(ctx).ID
  152. newProject, err := s.Repository.Insert(ctx, project)
  153. if err != nil {
  154. return nil, err
  155. }
  156. return generateResult(
  157. *newProject,
  158. s.Scopes.Context(ctx).Scope,
  159. []entities.Requirement{},
  160. []entities.RequirementStat{},
  161. []items.Result{},
  162. ), nil
  163. }
  164. func (s *Service) Update(ctx context.Context, id int, update models.ProjectUpdate) (*Result, error) {
  165. project, err := s.Find(ctx, id)
  166. if err != nil {
  167. return nil, err
  168. }
  169. err = s.Repository.Update(ctx, project.Project, update)
  170. if err != nil {
  171. return nil, err
  172. }
  173. project.Update(update)
  174. return project, nil
  175. }
  176. func (s *Service) Delete(ctx context.Context, id int) (*Result, error) {
  177. project, err := s.Find(ctx, id)
  178. if err != nil {
  179. return nil, err
  180. }
  181. err = s.Repository.Delete(ctx, project.Project)
  182. if err != nil {
  183. return nil, err
  184. }
  185. return project, nil
  186. }
  187. func (s *Service) FindRequirement(ctx context.Context, id int) (*RequirementResult, error) {
  188. scope := s.Scopes.Context(ctx).Scope
  189. req, reqStats, err := s.Repository.FetchRequirements(ctx, scope.ID, id)
  190. if err != nil {
  191. return nil, err
  192. }
  193. if len(req) == 0 {
  194. return nil, models.NotFoundError("Requirement " + strconv.Itoa(id))
  195. }
  196. reqItems, err := s.Items.ListScoped(ctx, models.ItemFilter{RequirementIDs: []int{id}})
  197. if err != nil {
  198. return nil, err
  199. }
  200. return genutils.Ptr(generateRequirementResult(req[0], scope, reqStats, reqItems)), nil
  201. }
  202. func (s *Service) CreateRequirement(ctx context.Context, id int, requirement entities.Requirement, stats []entities.RequirementStat) (*RequirementResult, error) {
  203. requirement.Name = strings.Trim(requirement.Name, "  \t\r\n")
  204. if requirement.Name == "" {
  205. return nil, models.BadInputError{
  206. Object: "ProjectInput",
  207. Field: "name",
  208. Problem: "Empty name provided",
  209. }
  210. }
  211. if !requirement.Status.Valid() {
  212. return nil, models.BadInputError{
  213. Object: "ProjectInput",
  214. Field: "status",
  215. Problem: "Non-existent status ID",
  216. }
  217. }
  218. err := validate.Tags(nil, requirement.Tags, nil)
  219. if err != nil {
  220. return nil, err
  221. }
  222. project, err := s.Find(ctx, id)
  223. if err != nil {
  224. return nil, err
  225. }
  226. requirement.ScopeID = project.ScopeID
  227. requirement.ProjectID = project.ID
  228. scope := s.Scopes.Context(ctx).Scope
  229. for _, stat := range stats {
  230. if !scope.HasStat(stat.StatID) {
  231. return nil, models.BadInputError{
  232. Object: "ProjectInput",
  233. Field: "stats",
  234. Problem: "Non-existent stat ID",
  235. }
  236. }
  237. }
  238. newRequirement, err := s.Repository.CreateRequirement(ctx, requirement)
  239. if err != nil {
  240. return nil, err
  241. }
  242. goodStats := make([]entities.RequirementStat, 0, len(stats))
  243. for _, stat := range stats {
  244. stat.RequirementID = newRequirement.ID
  245. err = s.Repository.UpsertRequirementStat(ctx, stat)
  246. if err == nil {
  247. goodStats = append(goodStats, stat)
  248. }
  249. }
  250. result := generateRequirementResult(
  251. *newRequirement,
  252. scope,
  253. goodStats,
  254. []items.Result{},
  255. )
  256. return &result, nil
  257. }
  258. func (s *Service) UpdateRequirement(ctx context.Context, projectID, requirementID int, update models.RequirementUpdate, stats []entities.RequirementStat) (*RequirementResult, error) {
  259. project, err := s.Find(ctx, projectID)
  260. if err != nil {
  261. return nil, err
  262. }
  263. req := project.Requirement(requirementID)
  264. if req == nil {
  265. return nil, models.NotFoundError("Requirement")
  266. }
  267. err = validate.Tags(req.Tags, update.AddTags, update.RemoveTags)
  268. if err != nil {
  269. return nil, err
  270. }
  271. if update.ProjectID != nil && *update.ProjectID != project.ID {
  272. dstProject, err := s.Find(ctx, *update.ProjectID)
  273. if err != nil {
  274. return nil, err
  275. }
  276. if dstProject.ScopeID != project.ScopeID {
  277. return nil, models.ForbiddenError("Requirement cannot be moved across scopes.")
  278. }
  279. project = dstProject
  280. }
  281. err = s.Repository.UpdateRequirement(ctx, req.Requirement, update)
  282. if err != nil {
  283. return nil, err
  284. }
  285. req.Requirement.Update(update)
  286. req.refresh(s.Scopes.Context(ctx).Scope)
  287. needsRefresh := false
  288. for _, stat := range stats {
  289. stat.RequirementID = req.ID
  290. if stat.Required >= 0 {
  291. err = s.Repository.UpsertRequirementStat(ctx, stat)
  292. } else {
  293. err = s.Repository.DeleteRequirementStat(ctx, stat)
  294. }
  295. if err == nil {
  296. if !req.updateStat(stat) {
  297. needsRefresh = true
  298. }
  299. }
  300. }
  301. if needsRefresh {
  302. return s.FindRequirement(ctx, requirementID)
  303. }
  304. return req, nil
  305. }
  306. func (s *Service) DeleteRequirement(ctx context.Context, projectID, requirementID int) (*RequirementResult, error) {
  307. project, err := s.Find(ctx, projectID)
  308. if err != nil {
  309. return nil, err
  310. }
  311. req := project.Requirement(requirementID)
  312. if req == nil {
  313. return nil, models.NotFoundError("Requirement")
  314. }
  315. err = s.Repository.DeleteRequirement(ctx, req.Requirement)
  316. if err != nil {
  317. return nil, err
  318. }
  319. return req, nil
  320. }
  321. func (s *Service) UpsertRequirementStat(ctx context.Context, projectID, requirementID int, input entities.RequirementStat) (*RequirementResult, error) {
  322. project, err := s.Find(ctx, projectID)
  323. if err != nil {
  324. return nil, err
  325. }
  326. req := project.Requirement(requirementID)
  327. if req == nil {
  328. return nil, models.NotFoundError("Requirement")
  329. }
  330. input.RequirementID = requirementID
  331. scope := s.Scopes.Context(ctx).Scope
  332. if !scope.HasStat(input.StatID) {
  333. return nil, models.NotFoundError("Stat")
  334. }
  335. err = s.Repository.UpsertRequirementStat(ctx, input)
  336. if err != nil {
  337. return nil, err
  338. }
  339. project, err = s.Find(ctx, projectID)
  340. if err != nil {
  341. return nil, err
  342. }
  343. req = project.Requirement(requirementID)
  344. if req == nil {
  345. return nil, models.NotFoundError("Requirement")
  346. }
  347. return req, nil
  348. }
  349. func (s *Service) DeleteRequirementStat(ctx context.Context, projectID, requirementID, statID int) (*RequirementResult, error) {
  350. project, err := s.Find(ctx, projectID)
  351. if err != nil {
  352. return nil, err
  353. }
  354. req := project.Requirement(requirementID)
  355. if req == nil {
  356. return nil, models.NotFoundError("Requirement")
  357. }
  358. stat := req.Stat(statID)
  359. if stat == nil {
  360. return nil, models.NotFoundError("Stat")
  361. }
  362. err = s.Repository.DeleteRequirementStat(ctx, entities.RequirementStat{
  363. RequirementID: req.ID,
  364. StatID: statID,
  365. })
  366. if err != nil {
  367. return nil, err
  368. }
  369. for i := range req.Stats {
  370. if req.Stats[i].ID == statID {
  371. req.Stats = append(req.Stats[:i], req.Stats[i+1:]...)
  372. break
  373. }
  374. }
  375. return req, nil
  376. }