diff --git a/docs/content/doc/usage/errors.md b/docs/content/doc/usage/errors.md index f6670737b..6c31ced38 100644 --- a/docs/content/doc/usage/errors.md +++ b/docs/content/doc/usage/errors.md @@ -54,17 +54,19 @@ 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. | -| 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. | +| 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. | ## Task diff --git a/pkg/db/fixtures/users.yml b/pkg/db/fixtures/users.yml index f22e63aad..e02b19b44 100644 --- a/pkg/db/fixtures/users.yml +++ b/pkg/db/fixtures/users.yml @@ -12,6 +12,7 @@ password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234 email: 'user2@example.com' issuer: local + default_project_id: 4 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 - @@ -20,6 +21,7 @@ password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234 email: 'user3@example.com' issuer: local + default_project_id: 4 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 - diff --git a/pkg/models/error.go b/pkg/models/error.go index 6f9283e6d..d85a6798f 100644 --- a/pkg/models/error.go +++ b/pkg/models/error.go @@ -347,6 +347,60 @@ func (err *ErrProjectCannotHaveACyclicRelationship) HTTPError() web.HTTPError { } } +// ErrCannotDeleteDefaultProject represents an error where the default project is being deleted +type ErrCannotDeleteDefaultProject struct { + ProjectID int64 +} + +// IsErrCannotDeleteDefaultProject checks if an error is a project is archived error. +func IsErrCannotDeleteDefaultProject(err error) bool { + _, ok := err.(*ErrCannotDeleteDefaultProject) + return ok +} + +func (err *ErrCannotDeleteDefaultProject) Error() string { + return fmt.Sprintf("Default project cannot be deleted [ProjectID: %d]", err.ProjectID) +} + +// ErrCodeCannotDeleteDefaultProject holds the unique world-error code of this error +const ErrCodeCannotDeleteDefaultProject = 3012 + +// HTTPError holds the http error description +func (err *ErrCannotDeleteDefaultProject) HTTPError() web.HTTPError { + return web.HTTPError{ + HTTPCode: http.StatusPreconditionFailed, + Code: ErrCodeCannotDeleteDefaultProject, + Message: "This project cannot be deleted because it is the default project of a user.", + } +} + +// ErrCannotArchiveDefaultProject represents an error where the default project is being deleted +type ErrCannotArchiveDefaultProject struct { + ProjectID int64 +} + +// IsErrCannotArchiveDefaultProject checks if an error is a project is archived error. +func IsErrCannotArchiveDefaultProject(err error) bool { + _, ok := err.(*ErrCannotArchiveDefaultProject) + return ok +} + +func (err *ErrCannotArchiveDefaultProject) Error() string { + return fmt.Sprintf("Default project cannot be archived [ProjectID: %d]", err.ProjectID) +} + +// ErrCodeCannotArchiveDefaultProject holds the unique world-error code of this error +const ErrCodeCannotArchiveDefaultProject = 3013 + +// HTTPError holds the http error description +func (err *ErrCannotArchiveDefaultProject) HTTPError() web.HTTPError { + return web.HTTPError{ + HTTPCode: http.StatusPreconditionFailed, + Code: ErrCodeCannotArchiveDefaultProject, + Message: "This project cannot be archived because it is the default project of a user.", + } +} + // ============== // Task errors // ============== diff --git a/pkg/models/label_task_test.go b/pkg/models/label_task_test.go index 52d84d57e..590282926 100644 --- a/pkg/models/label_task_test.go +++ b/pkg/models/label_task_test.go @@ -44,6 +44,7 @@ func TestLabelTask_ReadAll(t *testing.T) { EmailRemindersEnabled: true, OverdueTasksRemindersEnabled: true, OverdueTasksRemindersTime: "09:00", + DefaultProjectID: 4, Created: testCreatedTime, Updated: testUpdatedTime, }, diff --git a/pkg/models/label_test.go b/pkg/models/label_test.go index 4f9c4dc3c..7fd28e4a0 100644 --- a/pkg/models/label_test.go +++ b/pkg/models/label_test.go @@ -106,6 +106,7 @@ func TestLabel_ReadAll(t *testing.T) { EmailRemindersEnabled: true, OverdueTasksRemindersEnabled: true, OverdueTasksRemindersTime: "09:00", + DefaultProjectID: 4, Created: testCreatedTime, Updated: testUpdatedTime, }, @@ -233,6 +234,7 @@ func TestLabel_ReadOne(t *testing.T) { EmailRemindersEnabled: true, OverdueTasksRemindersEnabled: true, OverdueTasksRemindersTime: "09:00", + DefaultProjectID: 4, Created: testCreatedTime, Updated: testUpdatedTime, }, diff --git a/pkg/models/project.go b/pkg/models/project.go index df82bfb26..4fc648830 100644 --- a/pkg/models/project.go +++ b/pkg/models/project.go @@ -739,6 +739,17 @@ func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProje return } + if project.IsArchived { + isDefaultProject, err := project.isDefaultProject(s) + if err != nil { + return err + } + + if isDefaultProject { + return &ErrCannotArchiveDefaultProject{ProjectID: project.ID} + } + } + // We need to specify the cols we want to update here to be able to un-archive projects colsToUpdate := []string{ "title", @@ -907,6 +918,12 @@ func (p *Project) Create(s *xorm.Session, a web.Auth) (err error) { return p.ReadOne(s, a) } +func (p *Project) isDefaultProject(s *xorm.Session) (is bool, err error) { + return s. + Where("default_project_id = ?", p.ID). + Exist(&user.User{}) +} + // Delete implements the delete method of CRUDable // @Summary Deletes a project // @Description Delets a project @@ -921,6 +938,14 @@ func (p *Project) Create(s *xorm.Session, a web.Auth) (err error) { // @Router /projects/{id} [delete] func (p *Project) Delete(s *xorm.Session, a web.Auth) (err error) { + isDefaultProject, err := p.isDefaultProject(s) + if err != nil { + return err + } + if isDefaultProject { + return &ErrCannotDeleteDefaultProject{ProjectID: p.ID} + } + // Delete all tasks on that project // Using the loop to make sure all related entities to all tasks are properly deleted as well. tasks, _, _, err := getRawTasksForProjects(s, []*Project{p}, a, &taskOptions{}) diff --git a/pkg/models/project_test.go b/pkg/models/project_test.go index d316729d5..c0efd9585 100644 --- a/pkg/models/project_test.go +++ b/pkg/models/project_test.go @@ -219,6 +219,28 @@ func TestProject_CreateOrUpdate(t *testing.T) { assert.True(t, IsErrProjectCannotBelongToAPseudoParentProject(err)) }) }) + t.Run("archive default project of the same user", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + project := Project{ + ID: 4, + IsArchived: true, + } + err := project.Update(s, &user.User{ID: 3}) + assert.Error(t, err) + assert.True(t, IsErrCannotArchiveDefaultProject(err)) + }) + t.Run("archive default project of another user", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + project := Project{ + ID: 4, + IsArchived: true, + } + err := project.Update(s, &user.User{ID: 2}) + assert.Error(t, err) + assert.True(t, IsErrCannotArchiveDefaultProject(err)) + }) }) } @@ -255,6 +277,26 @@ func TestProject_Delete(t *testing.T) { "id": 1, }) }) + t.Run("default project of the same user", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + project := Project{ + ID: 4, + } + err := project.Delete(s, &user.User{ID: 3}) + assert.Error(t, err) + assert.True(t, IsErrCannotDeleteDefaultProject(err)) + }) + t.Run("default project of a different user", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + project := Project{ + ID: 4, + } + err := project.Delete(s, &user.User{ID: 2}) + assert.Error(t, err) + assert.True(t, IsErrCannotDeleteDefaultProject(err)) + }) } func TestProject_DeleteBackgroundFileIfExists(t *testing.T) { diff --git a/pkg/models/project_users_test.go b/pkg/models/project_users_test.go index 82eb7e7f2..4e77d51dc 100644 --- a/pkg/models/project_users_test.go +++ b/pkg/models/project_users_test.go @@ -166,6 +166,7 @@ func TestProjectUser_ReadAll(t *testing.T) { EmailRemindersEnabled: true, OverdueTasksRemindersEnabled: true, OverdueTasksRemindersTime: "09:00", + DefaultProjectID: 4, Created: testCreatedTime, Updated: testUpdatedTime, }, diff --git a/pkg/models/task_collection_test.go b/pkg/models/task_collection_test.go index eee0843e8..100fded17 100644 --- a/pkg/models/task_collection_test.go +++ b/pkg/models/task_collection_test.go @@ -50,6 +50,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { EmailRemindersEnabled: true, OverdueTasksRemindersEnabled: true, OverdueTasksRemindersTime: "09:00", + DefaultProjectID: 4, Created: testCreatedTime, Updated: testUpdatedTime, } diff --git a/pkg/models/user_project_test.go b/pkg/models/user_project_test.go index 38ab1e885..2a39c9422 100644 --- a/pkg/models/user_project_test.go +++ b/pkg/models/user_project_test.go @@ -44,6 +44,7 @@ func TestListUsersFromProject(t *testing.T) { EmailRemindersEnabled: true, OverdueTasksRemindersEnabled: true, OverdueTasksRemindersTime: "09:00", + DefaultProjectID: 4, Created: testCreatedTime, Updated: testUpdatedTime, } @@ -55,6 +56,7 @@ func TestListUsersFromProject(t *testing.T) { EmailRemindersEnabled: true, OverdueTasksRemindersEnabled: true, OverdueTasksRemindersTime: "09:00", + DefaultProjectID: 4, Created: testCreatedTime, Updated: testUpdatedTime, }