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
|
## Project
|
||||||
|
|
||||||
| ErrorCode | HTTP Status Code | Description |
|
| ErrorCode | HTTP Status Code | Description |
|
||||||
|-----------|------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------|------------------|-------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| 3001 | 404 | The project does not exist. |
|
| 3001 | 404 | The project does not exist. |
|
||||||
| 3004 | 403 | The user needs to have read permissions on that project to perform that action. |
|
| 3004 | 403 | The user needs to have read permissions on that project to perform that action. |
|
||||||
| 3005 | 400 | The project title cannot be empty. |
|
| 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". |
|
| 3009 | 412 | The project cannot belong to a dynamically generated parent project like "Favorites". |
|
||||||
| 3010 | 412 | This project cannot be a child of itself. |
|
| 3010 | 412 | This project cannot be a child of itself. |
|
||||||
| 3011 | 412 | This project cannot have a cyclic relationship to a parent project. |
|
| 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
|
## Task
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||||
email: 'user2@example.com'
|
email: 'user2@example.com'
|
||||||
issuer: local
|
issuer: local
|
||||||
|
default_project_id: 4
|
||||||
updated: 2018-12-02 15:13:12
|
updated: 2018-12-02 15:13:12
|
||||||
created: 2018-12-01 15:13:12
|
created: 2018-12-01 15:13:12
|
||||||
-
|
-
|
||||||
@ -20,6 +21,7 @@
|
|||||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||||
email: 'user3@example.com'
|
email: 'user3@example.com'
|
||||||
issuer: local
|
issuer: local
|
||||||
|
default_project_id: 4
|
||||||
updated: 2018-12-02 15:13:12
|
updated: 2018-12-02 15:13:12
|
||||||
created: 2018-12-01 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
|
// Task errors
|
||||||
// ==============
|
// ==============
|
||||||
|
@ -44,6 +44,7 @@ func TestLabelTask_ReadAll(t *testing.T) {
|
|||||||
EmailRemindersEnabled: true,
|
EmailRemindersEnabled: true,
|
||||||
OverdueTasksRemindersEnabled: true,
|
OverdueTasksRemindersEnabled: true,
|
||||||
OverdueTasksRemindersTime: "09:00",
|
OverdueTasksRemindersTime: "09:00",
|
||||||
|
DefaultProjectID: 4,
|
||||||
Created: testCreatedTime,
|
Created: testCreatedTime,
|
||||||
Updated: testUpdatedTime,
|
Updated: testUpdatedTime,
|
||||||
},
|
},
|
||||||
|
@ -106,6 +106,7 @@ func TestLabel_ReadAll(t *testing.T) {
|
|||||||
EmailRemindersEnabled: true,
|
EmailRemindersEnabled: true,
|
||||||
OverdueTasksRemindersEnabled: true,
|
OverdueTasksRemindersEnabled: true,
|
||||||
OverdueTasksRemindersTime: "09:00",
|
OverdueTasksRemindersTime: "09:00",
|
||||||
|
DefaultProjectID: 4,
|
||||||
Created: testCreatedTime,
|
Created: testCreatedTime,
|
||||||
Updated: testUpdatedTime,
|
Updated: testUpdatedTime,
|
||||||
},
|
},
|
||||||
@ -233,6 +234,7 @@ func TestLabel_ReadOne(t *testing.T) {
|
|||||||
EmailRemindersEnabled: true,
|
EmailRemindersEnabled: true,
|
||||||
OverdueTasksRemindersEnabled: true,
|
OverdueTasksRemindersEnabled: true,
|
||||||
OverdueTasksRemindersTime: "09:00",
|
OverdueTasksRemindersTime: "09:00",
|
||||||
|
DefaultProjectID: 4,
|
||||||
Created: testCreatedTime,
|
Created: testCreatedTime,
|
||||||
Updated: testUpdatedTime,
|
Updated: testUpdatedTime,
|
||||||
},
|
},
|
||||||
|
@ -739,6 +739,17 @@ func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProje
|
|||||||
return
|
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
|
// We need to specify the cols we want to update here to be able to un-archive projects
|
||||||
colsToUpdate := []string{
|
colsToUpdate := []string{
|
||||||
"title",
|
"title",
|
||||||
@ -907,6 +918,12 @@ func (p *Project) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||||||
return p.ReadOne(s, a)
|
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
|
// Delete implements the delete method of CRUDable
|
||||||
// @Summary Deletes a project
|
// @Summary Deletes a project
|
||||||
// @Description Delets 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]
|
// @Router /projects/{id} [delete]
|
||||||
func (p *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
|
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
|
// Delete all tasks on that project
|
||||||
// Using the loop to make sure all related entities to all tasks are properly deleted as well.
|
// 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{})
|
tasks, _, _, err := getRawTasksForProjects(s, []*Project{p}, a, &taskOptions{})
|
||||||
|
@ -219,6 +219,28 @@ func TestProject_CreateOrUpdate(t *testing.T) {
|
|||||||
assert.True(t, IsErrProjectCannotBelongToAPseudoParentProject(err))
|
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,
|
"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) {
|
func TestProject_DeleteBackgroundFileIfExists(t *testing.T) {
|
||||||
|
@ -166,6 +166,7 @@ func TestProjectUser_ReadAll(t *testing.T) {
|
|||||||
EmailRemindersEnabled: true,
|
EmailRemindersEnabled: true,
|
||||||
OverdueTasksRemindersEnabled: true,
|
OverdueTasksRemindersEnabled: true,
|
||||||
OverdueTasksRemindersTime: "09:00",
|
OverdueTasksRemindersTime: "09:00",
|
||||||
|
DefaultProjectID: 4,
|
||||||
Created: testCreatedTime,
|
Created: testCreatedTime,
|
||||||
Updated: testUpdatedTime,
|
Updated: testUpdatedTime,
|
||||||
},
|
},
|
||||||
|
@ -50,6 +50,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
EmailRemindersEnabled: true,
|
EmailRemindersEnabled: true,
|
||||||
OverdueTasksRemindersEnabled: true,
|
OverdueTasksRemindersEnabled: true,
|
||||||
OverdueTasksRemindersTime: "09:00",
|
OverdueTasksRemindersTime: "09:00",
|
||||||
|
DefaultProjectID: 4,
|
||||||
Created: testCreatedTime,
|
Created: testCreatedTime,
|
||||||
Updated: testUpdatedTime,
|
Updated: testUpdatedTime,
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,7 @@ func TestListUsersFromProject(t *testing.T) {
|
|||||||
EmailRemindersEnabled: true,
|
EmailRemindersEnabled: true,
|
||||||
OverdueTasksRemindersEnabled: true,
|
OverdueTasksRemindersEnabled: true,
|
||||||
OverdueTasksRemindersTime: "09:00",
|
OverdueTasksRemindersTime: "09:00",
|
||||||
|
DefaultProjectID: 4,
|
||||||
Created: testCreatedTime,
|
Created: testCreatedTime,
|
||||||
Updated: testUpdatedTime,
|
Updated: testUpdatedTime,
|
||||||
}
|
}
|
||||||
@ -55,6 +56,7 @@ func TestListUsersFromProject(t *testing.T) {
|
|||||||
EmailRemindersEnabled: true,
|
EmailRemindersEnabled: true,
|
||||||
OverdueTasksRemindersEnabled: true,
|
OverdueTasksRemindersEnabled: true,
|
||||||
OverdueTasksRemindersTime: "09:00",
|
OverdueTasksRemindersTime: "09:00",
|
||||||
|
DefaultProjectID: 4,
|
||||||
Created: testCreatedTime,
|
Created: testCreatedTime,
|
||||||
Updated: testUpdatedTime,
|
Updated: testUpdatedTime,
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user