From b39c5580c2c030193cb953b88731b26986d4bc45 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 13 Mar 2024 23:48:34 +0100 Subject: [PATCH] feat(views): add crud handlers and routes for views --- docs/content/doc/usage/errors.md | 25 ++--- pkg/models/error.go | 27 ++++++ pkg/models/project_view.go | 149 ++++++++++++++++++++++++++++++ pkg/models/project_view_rights.go | 58 ++++++++++++ pkg/routes/routes.go | 14 +++ 5 files changed, 261 insertions(+), 12 deletions(-) create mode 100644 pkg/models/project_view_rights.go diff --git a/docs/content/doc/usage/errors.md b/docs/content/doc/usage/errors.md index 4aff46443..b2b26225c 100644 --- a/docs/content/doc/usage/errors.md +++ b/docs/content/doc/usage/errors.md @@ -55,19 +55,20 @@ This document describes the different errors Vikunja can return. ## Project -| ErrorCode | HTTP Status Code | Description | -|-----------|------------------|-------------------------------------------------------------------------------------------------------------------------------------| -| 3001 | 404 | The project does not exist. | -| 3004 | 403 | The user needs to have read permissions on that project to perform that action. | -| 3005 | 400 | The project title cannot be empty. | -| 3006 | 404 | The project share does not exist. | -| 3007 | 400 | A project with this identifier already exists. | +| ErrorCode | HTTP Status Code | Description | +|-----------|------------------|------------------------------------------------------------------------------------------------------------------------------------| +| 3001 | 404 | The project does not exist. | +| 3004 | 403 | The user needs to have read permissions on that project to perform that action. | +| 3005 | 400 | The project title cannot be empty. | +| 3006 | 404 | The project share does not exist. | +| 3007 | 400 | A project with this identifier already exists. | | 3008 | 412 | The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project. | -| 3009 | 412 | The project cannot belong to a dynamically generated parent project like "Favorites". | -| 3010 | 412 | This project cannot be a child of itself. | -| 3011 | 412 | This project cannot have a cyclic relationship to a parent project. | -| 3012 | 412 | This project cannot be deleted because a user has set it as their default project. | -| 3013 | 412 | This project cannot be archived because a user has set it as their default project. | +| 3009 | 412 | The project cannot belong to a dynamically generated parent project like "Favorites". | +| 3010 | 412 | This project cannot be a child of itself. | +| 3011 | 412 | This project cannot have a cyclic relationship to a parent project. | +| 3012 | 412 | This project cannot be deleted because a user has set it as their default project. | +| 3013 | 412 | This project cannot be archived because a user has set it as their default project. | +| 3014 | 404 | This project view does not exist. | ## Task diff --git a/pkg/models/error.go b/pkg/models/error.go index 8eb151830..1b54b955e 100644 --- a/pkg/models/error.go +++ b/pkg/models/error.go @@ -412,6 +412,33 @@ func (err *ErrCannotArchiveDefaultProject) HTTPError() web.HTTPError { } } +// ErrProjectViewDoesNotExist represents an error where the default project is being deleted +type ErrProjectViewDoesNotExist struct { + ProjectViewID int64 +} + +// IsErrProjectViewDoesNotExist checks if an error is a project is archived error. +func IsErrProjectViewDoesNotExist(err error) bool { + _, ok := err.(*ErrProjectViewDoesNotExist) + return ok +} + +func (err *ErrProjectViewDoesNotExist) Error() string { + return fmt.Sprintf("Project view does not exist [ProjectViewID: %d]", err.ProjectViewID) +} + +// ErrCodeProjectViewDoesNotExist holds the unique world-error code of this error +const ErrCodeProjectViewDoesNotExist = 3014 + +// HTTPError holds the http error description +func (err *ErrProjectViewDoesNotExist) HTTPError() web.HTTPError { + return web.HTTPError{ + HTTPCode: http.StatusNotFound, + Code: ErrCodeProjectViewDoesNotExist, + Message: "This project view does not exist.", + } +} + // ============== // Task errors // ============== diff --git a/pkg/models/project_view.go b/pkg/models/project_view.go index 012f065fa..02b633f5e 100644 --- a/pkg/models/project_view.go +++ b/pkg/models/project_view.go @@ -19,6 +19,7 @@ package models import ( "code.vikunja.io/web" "time" + "xorm.io/xorm" ) type ProjectViewKind int @@ -57,3 +58,151 @@ type ProjectView struct { func (p *ProjectView) TableName() string { return "project_views" } + +// ReadAll gets all project views +// @Summary Get all project views for a project +// @Description Returns all project views for a sepcific project +// @tags project +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Param project path int true "Project ID" +// @Success 200 {array} models.ProjectView "The project views" +// @Failure 500 {object} models.Message "Internal error" +// @Router /projects/{project}/views [get] +func (p *ProjectView) ReadAll(s *xorm.Session, a web.Auth, _ string, _ int, _ int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) { + + pp := &Project{ID: p.ProjectID} + can, _, err := pp.CanRead(s, a) + if err != nil { + return nil, 0, 0, err + } + if !can { + return nil, 0, 0, ErrGenericForbidden{} + } + + projectViews := []*ProjectView{} + err = s. + Where("project_id = ?", p.ProjectID). + Find(&projectViews) + if err != nil { + return + } + + totalCount, err := s. + Where("project_id = ?", p.ProjectID). + Count(&ProjectView{}) + if err != nil { + return + } + + return projectViews, len(projectViews), totalCount, nil +} + +// ReadOne implements the CRUD method to get one project view +// @Summary Get one project view +// @Description Returns a project view by its ID. +// @tags project +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Param project path int true "Project ID" +// @Param id path int true "Project View ID" +// @Success 200 {object} models.ProjectView "The project view" +// @Failure 403 {object} web.HTTPError "The user does not have access to this project view" +// @Failure 500 {object} models.Message "Internal error" +// @Router /projects/{project}/views/{id} [get] +func (p *ProjectView) ReadOne(s *xorm.Session, _ web.Auth) (err error) { + view, err := GetProjectViewByID(s, p.ID) + if err != nil { + return err + } + + *p = *view + return +} + +// Delete removes the project view +// @Summary Delete a project view +// @Description Deletes a project view. +// @tags project +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Param project path int true "Project ID" +// @Param id path int true "Project View ID" +// @Success 200 {object} models.Message "The project view was successfully deleted." +// @Failure 403 {object} web.HTTPError "The user does not have access to the project view" +// @Failure 500 {object} models.Message "Internal error" +// @Router /projects/{project}/views/{id} [delete] +func (p *ProjectView) Delete(s *xorm.Session, a web.Auth) (err error) { + _, err = s. + Where("id = ? AND projec_id = ?", p.ID, p.ProjectID). + Delete(&ProjectView{}) + return +} + +// Create adds a new project view +// @Summary Create a project view +// @Description Create a project view in a specific project. +// @tags project +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Param project path int true "Project ID" +// @Param view body models.ProjectView true "The project view you want to create." +// @Success 200 {object} models.ProjectView "The created project view" +// @Failure 403 {object} web.HTTPError "The user does not have access to create a project view" +// @Failure 500 {object} models.Message "Internal error" +// @Router /projects/{project}/views [put] +func (p *ProjectView) Create(s *xorm.Session, a web.Auth) (err error) { + _, err = s.Insert(p) + return +} + +// Update is the handler to update a project view +// @Summary Updates a project view +// @Description Updates a project view. +// @tags project +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Param project path int true "Project ID" +// @Param id path int true "Project View ID" +// @Param view body models.ProjectView true "The project view with updated values you want to change." +// @Success 200 {object} models.ProjectView "The updated project view." +// @Failure 400 {object} web.HTTPError "Invalid project view object provided." +// @Failure 500 {object} models.Message "Internal error" +// @Router /projects/{project}/views/{id} [post] +func (p *ProjectView) Update(s *xorm.Session, _ web.Auth) (err error) { + // Check if the project view exists + _, err = GetProjectViewByID(s, p.ID) + if err != nil { + return + } + + _, err = s.ID(p.ID).Update(p) + if err != nil { + return + } + + return +} + +func GetProjectViewByID(s *xorm.Session, id int64) (view *ProjectView, err error) { + exists, err := s. + Where("id = ?", id). + NoAutoCondition(). + Get(view) + if err != nil { + return nil, err + } + + if !exists { + return nil, &ErrProjectViewDoesNotExist{ + ProjectViewID: id, + } + } + + return +} diff --git a/pkg/models/project_view_rights.go b/pkg/models/project_view_rights.go new file mode 100644 index 000000000..7b4a92777 --- /dev/null +++ b/pkg/models/project_view_rights.go @@ -0,0 +1,58 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public Licensee as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package models + +import ( + "code.vikunja.io/web" + "xorm.io/xorm" +) + +func (p *ProjectView) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) { + pp, err := p.getProject(s) + if err != nil { + return false, 0, err + } + return pp.CanRead(s, a) +} + +func (p *ProjectView) CanDelete(s *xorm.Session, a web.Auth) (bool, error) { + pp, err := p.getProject(s) + if err != nil { + return false, err + } + return pp.CanUpdate(s, a) +} + +func (p *ProjectView) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) { + pp, err := p.getProject(s) + if err != nil { + return false, err + } + return pp.CanUpdate(s, a) +} + +func (p *ProjectView) CanCreate(s *xorm.Session, a web.Auth) (bool, error) { + pp, err := p.getProject(s) + if err != nil { + return false, err + } + return pp.CanUpdate(s, a) +} + +func (p *ProjectView) getProject(s *xorm.Session) (pp *Project, err error) { + return GetProjectSimpleByID(s, p.ProjectID) +} diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index d59970059..1936eaf5e 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -590,6 +590,7 @@ func registerAPIRoutes(a *echo.Group) { a.GET("/webhooks/events", apiv1.GetAvailableWebhookEvents) } + // Reactions reactionProvider := &handler.WebHandler{ EmptyStruct: func() handler.CObject { return &models.Reaction{} @@ -598,6 +599,19 @@ func registerAPIRoutes(a *echo.Group) { a.GET("/:entitykind/:entityid/reactions", reactionProvider.ReadAllWeb) a.POST("/:entitykind/:entityid/reactions/delete", reactionProvider.DeleteWeb) a.PUT("/:entitykind/:entityid/reactions", reactionProvider.CreateWeb) + + // Project views + projectViewProvider := &handler.WebHandler{ + EmptyStruct: func() handler.CObject { + return &models.ProjectView{} + }, + } + + a.GET("/projects/:project/views", projectViewProvider.ReadAllWeb) + a.GET("/projects/:project/views/:view", projectViewProvider.ReadOneWeb) + a.PUT("/projects/:project/views", projectViewProvider.CreateWeb) + a.DELETE("/projects/:project/views/:view", projectViewProvider.DeleteWeb) + a.POST("/projects/:project/views/:view", projectViewProvider.UpdateWeb) } func registerMigrations(m *echo.Group) {