feat(projects): don't allow deleting or archiving the default project
This commit is contained in:
parent
ad0690369f
commit
ef94e0cf86
@ -55,7 +55,7 @@ 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. |
|
||||
@ -65,6 +65,8 @@ This document describes the different errors Vikunja can return.
|
||||
| 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
|
||||
|
||||
|
@ -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
|
||||
-
|
||||
|
@ -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
|
||||
// ==============
|
||||
|
@ -44,6 +44,7 @@ func TestLabelTask_ReadAll(t *testing.T) {
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
DefaultProjectID: 4,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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{})
|
||||
|
@ -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) {
|
||||
|
@ -166,6 +166,7 @@ func TestProjectUser_ReadAll(t *testing.T) {
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
DefaultProjectID: 4,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
|
@ -50,6 +50,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
DefaultProjectID: 4,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user