feat(projects): cleanup namespace leftovers

This commit is contained in:
kolaente 2022-12-29 17:51:55 +01:00
parent c244a0f145
commit 92f0a50996
Signed by: konrad
GPG Key ID: F40E70337AB24C9B
21 changed files with 127 additions and 596 deletions

View File

@ -191,7 +191,7 @@ var userCreateCmd = &cobra.Command{
err = models.CreateNewProjectForUser(s, newUser)
if err != nil {
_ = s.Rollback()
log.Fatalf("Error creating new namespace for user: %s", err)
log.Fatalf("Error creating new project for user: %s", err)
}
if err := s.Commit(); err != nil {

View File

@ -17,7 +17,6 @@
package integrations
import (
"net/url"
"testing"
"code.vikunja.io/api/pkg/models"
@ -26,32 +25,27 @@ import (
)
// This tests the following behaviour:
// 1. A namespace should not be editable if it is archived.
// 1. With the exception being to un-archive it.
// 2. A project which belongs to an archived namespace cannot be edited.
// 2. A project which belongs to an archived project cannot be edited.
// 3. An archived project should not be editable.
// 1. Except for un-archiving it.
// 4. It is not possible to un-archive a project individually if its namespace is archived.
// 5. Creating new projects on an archived namespace should not work.
// 4. It is not possible to un-archive a project individually if its parent project is archived.
// 5. Creating new child projects in an archived project should not work.
// 6. Creating new tasks on an archived project should not work.
// 7. Creating new tasks on a project who's namespace is archived should not work.
// 7. Creating new tasks on a project whose parent project is archived should not work.
// 8. Editing tasks on an archived project should not work.
// 9. Editing tasks on a project who's namespace is archived should not work.
// 10. Archived namespaces should not appear in the project with all namespaces.
// 11. Archived projects should not appear in the project with all projects.
// 12. Projects who's namespace is archived should not appear in the project with all projects.
// 9. Editing tasks on a project whose parent project is archived should not work.
// 11. Archived projects should not appear in the list with all projects.
// 12. Projects whose parent project is archived should not appear in the project with all projects.
//
// All of this is tested through integration tests because it's not yet clear if this will be implemented directly
// or with some kind of middleware.
//
// Maybe the inheritance of projects from namespaces could be solved with some kind of is_archived_inherited flag -
// Maybe the inheritance of projects from parents could be solved with some kind of is_archived_inherited flag -
// that way I'd only need to implement the checking on a project level and update the flag for all projects once the
// namespace is archived. The archived flag would then be used to not accedentially unarchive projects which were
// already individually archived when the namespace was archived.
// Should still test it all though.
// project is archived. The archived flag would then be used to not accedentially unarchive projects which were
// already individually archived when the parent project was archived.
//
// Namespace 16 is archived
// Project 21 belongs to namespace 16
// Project 21 belongs to project 16
// Project 22 is archived individually
func TestArchived(t *testing.T) {
@ -62,13 +56,6 @@ func TestArchived(t *testing.T) {
},
t: t,
}
testNamespaceHandler := webHandlerTest{
user: &testuser1,
strFunc: func() handler.CObject {
return &models.Namespace{}
},
t: t,
}
testTaskHandler := webHandlerTest{
user: &testuser1,
strFunc: func() handler.CObject {
@ -105,34 +92,6 @@ func TestArchived(t *testing.T) {
t: t,
}
t.Run("namespace", func(t *testing.T) {
t.Run("not editable", func(t *testing.T) {
_, err := testNamespaceHandler.testUpdateWithUser(nil, map[string]string{"namespace": "16"}, `{"title":"TestIpsum","is_archived":true}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeNamespaceIsArchived)
})
t.Run("unarchivable", func(t *testing.T) {
rec, err := testNamespaceHandler.testUpdateWithUser(nil, map[string]string{"namespace": "16"}, `{"title":"TestIpsum","is_archived":false}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"is_archived":false`)
})
t.Run("no new projects", func(t *testing.T) {
_, err := testProjectHandler.testCreateWithUser(nil, map[string]string{"namespace": "16"}, `{"title":"Lorem"}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeNamespaceIsArchived)
})
t.Run("should not appear in the project", func(t *testing.T) {
rec, err := testNamespaceHandler.testReadAllWithUser(nil, nil)
assert.NoError(t, err)
assert.NotContains(t, rec.Body.String(), `"title":"Archived testnamespace16"`)
})
t.Run("should appear in the project if explicitly requested", func(t *testing.T) {
rec, err := testNamespaceHandler.testReadAllWithUser(url.Values{"is_archived": []string{"true"}}, nil)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Archived testnamespace16"`)
})
})
t.Run("project", func(t *testing.T) {
taskTests := func(taskID string, errCode int, t *testing.T) {
@ -194,8 +153,8 @@ func TestArchived(t *testing.T) {
})
}
// The project belongs to an archived namespace
t.Run("archived namespace", func(t *testing.T) {
// The project belongs to an archived parent project
t.Run("archived parent project", func(t *testing.T) {
t.Run("not editable", func(t *testing.T) {
_, err := testProjectHandler.testUpdateWithUser(nil, map[string]string{"project": "21"}, `{"title":"TestIpsum","is_archived":true}`)
assert.Error(t, err)

View File

@ -34,9 +34,6 @@ const (
// UserCountKey is the name of the key we use to store total users in redis
UserCountKey = `usercount`
// NamespaceCountKey is the name of the key we use to store the amount of total namespaces in redis
NamespaceCountKey = `namespacecount`
// TaskCountKey is the name of the key we use to store the amount of total tasks in redis
TaskCountKey = `taskcount`
@ -89,18 +86,6 @@ func InitMetrics() {
log.Criticalf("Could not register metrics for %s: %s", UserCountKey, err)
}
// Register total Namespaces count metric
err = registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
Name: "vikunja_namespace_count",
Help: "The total number of namespaces on this instance",
}, func() float64 {
count, _ := GetCount(NamespaceCountKey)
return float64(count)
}))
if err != nil {
log.Criticalf("Could not register metrics for %s: %s", NamespaceCountKey, err)
}
// Register total Tasks count metric
err = registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
Name: "vikunja_task_count",

View File

@ -255,65 +255,37 @@ func (err ErrProjectIsArchived) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeProjectIsArchived, Message: "This project is archived. Editing or creating new tasks is not possible."}
}
// ErrProjectCannotBelongToAPseudoNamespace represents an error where a project cannot belong to a pseudo namespace
type ErrProjectCannotBelongToAPseudoNamespace struct {
ProjectID int64
NamespaceID int64
// ErrProjectCannotBelongToAPseudoParentProject represents an error where a project cannot belong to a pseudo project
type ErrProjectCannotBelongToAPseudoParentProject struct {
ProjectID int64
ParentProjectID int64
}
// IsErrProjectCannotBelongToAPseudoNamespace checks if an error is a project is archived error.
func IsErrProjectCannotBelongToAPseudoNamespace(err error) bool {
_, ok := err.(*ErrProjectCannotBelongToAPseudoNamespace)
// IsErrProjectCannotBelongToAPseudoParentProject checks if an error is a project is archived error.
func IsErrProjectCannotBelongToAPseudoParentProject(err error) bool {
_, ok := err.(*ErrProjectCannotBelongToAPseudoParentProject)
return ok
}
func (err *ErrProjectCannotBelongToAPseudoNamespace) Error() string {
return fmt.Sprintf("Project cannot belong to a pseudo namespace [ProjectID: %d, NamespaceID: %d]", err.ProjectID, err.NamespaceID)
func (err *ErrProjectCannotBelongToAPseudoParentProject) Error() string {
return fmt.Sprintf("Project cannot belong to a pseudo parent project [ProjectID: %d, ParentProjectID: %d]", err.ProjectID, err.ParentProjectID)
}
// ErrCodeProjectCannotBelongToAPseudoNamespace holds the unique world-error code of this error
const ErrCodeProjectCannotBelongToAPseudoNamespace = 3009
// ErrCodeProjectCannotBelongToAPseudoParentProject holds the unique world-error code of this error
const ErrCodeProjectCannotBelongToAPseudoParentProject = 3009
// HTTPError holds the http error description
func (err *ErrProjectCannotBelongToAPseudoNamespace) HTTPError() web.HTTPError {
func (err *ErrProjectCannotBelongToAPseudoParentProject) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusPreconditionFailed,
Code: ErrCodeProjectCannotBelongToAPseudoNamespace,
Message: "This project cannot belong a dynamically generated namespace.",
Code: ErrCodeProjectCannotBelongToAPseudoParentProject,
Message: "This project cannot belong a dynamically generated project.",
}
}
// ErrProjectMustBelongToANamespace represents an error where a project must belong to a namespace
type ErrProjectMustBelongToANamespace struct {
ProjectID int64
NamespaceID int64
}
// IsErrProjectMustBelongToANamespace checks if an error is a project must belong to a namespace error.
func IsErrProjectMustBelongToANamespace(err error) bool {
_, ok := err.(*ErrProjectMustBelongToANamespace)
return ok
}
func (err *ErrProjectMustBelongToANamespace) Error() string {
return fmt.Sprintf("Project must belong to a namespace [ProjectID: %d, NamespaceID: %d]", err.ProjectID, err.NamespaceID)
}
// ErrCodeProjectMustBelongToANamespace holds the unique world-error code of this error
const ErrCodeProjectMustBelongToANamespace = 3010
// HTTPError holds the http error description
func (err *ErrProjectMustBelongToANamespace) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusPreconditionFailed,
Code: ErrCodeProjectMustBelongToANamespace,
Message: "This project must belong to a namespace.",
}
}
// ================
// Project task errors
// ================
// ==============
// Project errors
// ==============
// ErrTaskCannotBeEmpty represents a "ErrProjectDoesNotExist" kind of error. Used if the project does not exist.
type ErrTaskCannotBeEmpty struct{}
@ -875,176 +847,6 @@ func (err ErrUserAlreadyAssigned) HTTPError() web.HTTPError {
}
}
// =================
// Namespace errors
// =================
// ErrNamespaceDoesNotExist represents a "ErrNamespaceDoesNotExist" kind of error. Used if the namespace does not exist.
type ErrNamespaceDoesNotExist struct {
ID int64
}
// IsErrNamespaceDoesNotExist checks if an error is a ErrNamespaceDoesNotExist.
func IsErrNamespaceDoesNotExist(err error) bool {
_, ok := err.(ErrNamespaceDoesNotExist)
return ok
}
func (err ErrNamespaceDoesNotExist) Error() string {
return fmt.Sprintf("Namespace does not exist [ID: %d]", err.ID)
}
// ErrCodeNamespaceDoesNotExist holds the unique world-error code of this error
const ErrCodeNamespaceDoesNotExist = 5001
// HTTPError holds the http error description
func (err ErrNamespaceDoesNotExist) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeNamespaceDoesNotExist, Message: "Namespace not found."}
}
// ErrUserDoesNotHaveAccessToNamespace represents an error, where the user is not the owner of that namespace (used i.e. when deleting a namespace)
type ErrUserDoesNotHaveAccessToNamespace struct {
NamespaceID int64
UserID int64
}
// IsErrUserDoesNotHaveAccessToNamespace checks if an error is a ErrNamespaceDoesNotExist.
func IsErrUserDoesNotHaveAccessToNamespace(err error) bool {
_, ok := err.(ErrUserDoesNotHaveAccessToNamespace)
return ok
}
func (err ErrUserDoesNotHaveAccessToNamespace) Error() string {
return fmt.Sprintf("User does not have access to the namespace [NamespaceID: %d, UserID: %d]", err.NamespaceID, err.UserID)
}
// ErrCodeUserDoesNotHaveAccessToNamespace holds the unique world-error code of this error
const ErrCodeUserDoesNotHaveAccessToNamespace = 5003
// HTTPError holds the http error description
func (err ErrUserDoesNotHaveAccessToNamespace) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeUserDoesNotHaveAccessToNamespace, Message: "This user does not have access to the namespace."}
}
// ErrNamespaceNameCannotBeEmpty represents an error, where a namespace name is empty.
type ErrNamespaceNameCannotBeEmpty struct {
NamespaceID int64
UserID int64
}
// IsErrNamespaceNameCannotBeEmpty checks if an error is a ErrNamespaceDoesNotExist.
func IsErrNamespaceNameCannotBeEmpty(err error) bool {
_, ok := err.(ErrNamespaceNameCannotBeEmpty)
return ok
}
func (err ErrNamespaceNameCannotBeEmpty) Error() string {
return fmt.Sprintf("Namespace name cannot be empty [NamespaceID: %d, UserID: %d]", err.NamespaceID, err.UserID)
}
// ErrCodeNamespaceNameCannotBeEmpty holds the unique world-error code of this error
const ErrCodeNamespaceNameCannotBeEmpty = 5006
// HTTPError holds the http error description
func (err ErrNamespaceNameCannotBeEmpty) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeNamespaceNameCannotBeEmpty, Message: "The namespace name cannot be empty."}
}
// ErrNeedToHaveNamespaceReadAccess represents an error, where the user is not the owner of that namespace (used i.e. when deleting a namespace)
type ErrNeedToHaveNamespaceReadAccess struct {
NamespaceID int64
UserID int64
}
// IsErrNeedToHaveNamespaceReadAccess checks if an error is a ErrNamespaceDoesNotExist.
func IsErrNeedToHaveNamespaceReadAccess(err error) bool {
_, ok := err.(ErrNeedToHaveNamespaceReadAccess)
return ok
}
func (err ErrNeedToHaveNamespaceReadAccess) Error() string {
return fmt.Sprintf("User does not have access to that namespace [NamespaceID: %d, UserID: %d]", err.NamespaceID, err.UserID)
}
// ErrCodeNeedToHaveNamespaceReadAccess holds the unique world-error code of this error
const ErrCodeNeedToHaveNamespaceReadAccess = 5009
// HTTPError holds the http error description
func (err ErrNeedToHaveNamespaceReadAccess) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeNeedToHaveNamespaceReadAccess, Message: "You need to have namespace read access to do this."}
}
// ErrTeamDoesNotHaveAccessToNamespace represents an error, where the Team is not the owner of that namespace (used i.e. when deleting a namespace)
type ErrTeamDoesNotHaveAccessToNamespace struct {
NamespaceID int64
TeamID int64
}
// IsErrTeamDoesNotHaveAccessToNamespace checks if an error is a ErrNamespaceDoesNotExist.
func IsErrTeamDoesNotHaveAccessToNamespace(err error) bool {
_, ok := err.(ErrTeamDoesNotHaveAccessToNamespace)
return ok
}
func (err ErrTeamDoesNotHaveAccessToNamespace) Error() string {
return fmt.Sprintf("Team does not have access to that namespace [NamespaceID: %d, TeamID: %d]", err.NamespaceID, err.TeamID)
}
// ErrCodeTeamDoesNotHaveAccessToNamespace holds the unique world-error code of this error
const ErrCodeTeamDoesNotHaveAccessToNamespace = 5010
// HTTPError holds the http error description
func (err ErrTeamDoesNotHaveAccessToNamespace) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeTeamDoesNotHaveAccessToNamespace, Message: "You need to have access to this namespace to do this."}
}
// ErrUserAlreadyHasNamespaceAccess represents an error where a user already has access to a namespace
type ErrUserAlreadyHasNamespaceAccess struct {
UserID int64
NamespaceID int64
}
// IsErrUserAlreadyHasNamespaceAccess checks if an error is ErrUserAlreadyHasNamespaceAccess.
func IsErrUserAlreadyHasNamespaceAccess(err error) bool {
_, ok := err.(ErrUserAlreadyHasNamespaceAccess)
return ok
}
func (err ErrUserAlreadyHasNamespaceAccess) Error() string {
return fmt.Sprintf("User already has access to that namespace. [User ID: %d, Namespace ID: %d]", err.UserID, err.NamespaceID)
}
// ErrCodeUserAlreadyHasNamespaceAccess holds the unique world-error code of this error
const ErrCodeUserAlreadyHasNamespaceAccess = 5011
// HTTPError holds the http error description
func (err ErrUserAlreadyHasNamespaceAccess) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusConflict, Code: ErrCodeUserAlreadyHasNamespaceAccess, Message: "This user already has access to this namespace."}
}
// ErrNamespaceIsArchived represents an error where a namespace is archived
type ErrNamespaceIsArchived struct {
NamespaceID int64
}
// IsErrNamespaceIsArchived checks if an error is a .
func IsErrNamespaceIsArchived(err error) bool {
_, ok := err.(ErrNamespaceIsArchived)
return ok
}
func (err ErrNamespaceIsArchived) Error() string {
return fmt.Sprintf("Namespace is archived [NamespaceID: %d]", err.NamespaceID)
}
// ErrCodeNamespaceIsArchived holds the unique world-error code of this error
const ErrCodeNamespaceIsArchived = 5012
// HTTPError holds the http error description
func (err ErrNamespaceIsArchived) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeNamespaceIsArchived, Message: "This namespaces is archived. Editing or creating new projects is not possible."}
}
// ============
// Team errors
// ============
@ -1054,7 +856,7 @@ type ErrTeamNameCannotBeEmpty struct {
TeamID int64
}
// IsErrTeamNameCannotBeEmpty checks if an error is a ErrNamespaceDoesNotExist.
// IsErrTeamNameCannotBeEmpty checks if an error is a ErrTeamNameCannotBeEmpty.
func IsErrTeamNameCannotBeEmpty(err error) bool {
_, ok := err.(ErrTeamNameCannotBeEmpty)
return ok
@ -1095,7 +897,7 @@ func (err ErrTeamDoesNotExist) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTeamDoesNotExist, Message: "This team does not exist."}
}
// ErrTeamAlreadyHasAccess represents an error where a team already has access to a project/namespace
// ErrTeamAlreadyHasAccess represents an error where a team already has access to a project
type ErrTeamAlreadyHasAccess struct {
TeamID int64
ID int64
@ -1195,7 +997,7 @@ func (err ErrTeamDoesNotHaveAccessToProject) HTTPError() web.HTTPError {
// User <-> Project errors
// ====================
// ErrUserAlreadyHasAccess represents an error where a user already has access to a project/namespace
// ErrUserAlreadyHasAccess represents an error where a user already has access to a project
type ErrUserAlreadyHasAccess struct {
UserID int64
ProjectID int64

View File

@ -104,43 +104,6 @@ func (t *TaskCommentUpdatedEvent) Name() string {
return "task.comment.edited"
}
//////////////////////
// Namespace Events //
//////////////////////
// NamespaceCreatedEvent represents an event where a namespace has been created
type NamespaceCreatedEvent struct {
Namespace *Namespace
Doer web.Auth
}
// Name defines the name for NamespaceCreatedEvent
func (n *NamespaceCreatedEvent) Name() string {
return "namespace.created"
}
// NamespaceUpdatedEvent represents an event where a namespace has been updated
type NamespaceUpdatedEvent struct {
Namespace *Namespace
Doer web.Auth
}
// Name defines the name for NamespaceUpdatedEvent
func (n *NamespaceUpdatedEvent) Name() string {
return "namespace.updated"
}
// NamespaceDeletedEvent represents a NamespaceDeletedEvent event
type NamespaceDeletedEvent struct {
Namespace *Namespace
Doer web.Auth
}
// TopicName defines the name for NamespaceDeletedEvent
func (t *NamespaceDeletedEvent) Name() string {
return "namespace.deleted"
}
/////////////////
// Project Events //
/////////////////
@ -206,30 +169,6 @@ func (l *ProjectSharedWithTeamEvent) Name() string {
return "project.shared.team"
}
// NamespaceSharedWithUserEvent represents an event where a namespace has been shared with a user
type NamespaceSharedWithUserEvent struct {
Namespace *Namespace
User *user.User
Doer web.Auth
}
// Name defines the name for NamespaceSharedWithUserEvent
func (n *NamespaceSharedWithUserEvent) Name() string {
return "namespace.shared.user"
}
// NamespaceSharedWithTeamEvent represents an event where a namespace has been shared with a team
type NamespaceSharedWithTeamEvent struct {
Namespace *Namespace
Team *Team
Doer web.Auth
}
// Name defines the name for NamespaceSharedWithTeamEvent
func (n *NamespaceSharedWithTeamEvent) Name() string {
return "namespace.shared.team"
}
/////////////////
// Team Events //
/////////////////

View File

@ -57,12 +57,12 @@ func ExportUserData(s *xorm.Session, u *user.User) (err error) {
defer dumpWriter.Close()
// Get the data
err = exportProjectsAndTasks(s, u, dumpWriter)
taskIDs, err := exportProjectsAndTasks(s, u, dumpWriter)
if err != nil {
return err
}
// Task attachment files
err = exportTaskAttachments(s, u, dumpWriter)
err = exportTaskAttachments(s, u, dumpWriter, taskIDs)
if err != nil {
return err
}
@ -121,51 +121,35 @@ func ExportUserData(s *xorm.Session, u *user.User) (err error) {
})
}
func exportProjectsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err error) {
namspaces, _, _, err := (&Namespace{IsArchived: true}).ReadAll(s, u, "", -1, 0)
if err != nil {
return err
}
namespaceIDs := []int64{}
namespaces := []*NamespaceWithProjectsAndTasks{}
projectMap := make(map[int64]*ProjectWithTasksAndBuckets)
projectIDs := []int64{}
for _, n := range namspaces.([]*NamespaceWithProjects) {
if n.ID < 1 {
// Don't include filters
continue
}
nn := &NamespaceWithProjectsAndTasks{
Namespace: n.Namespace,
Projects: []*ProjectWithTasksAndBuckets{},
}
for _, l := range n.Projects {
ll := &ProjectWithTasksAndBuckets{
Project: *l,
BackgroundFileID: l.BackgroundFileID,
Tasks: []*TaskWithComments{},
}
nn.Projects = append(nn.Projects, ll)
projectMap[l.ID] = ll
projectIDs = append(projectIDs, l.ID)
}
namespaceIDs = append(namespaceIDs, n.ID)
namespaces = append(namespaces, nn)
}
if len(namespaceIDs) == 0 {
return nil
}
func exportProjectsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (taskIDs []int64, err error) {
// Get all projects
projects, err := getProjectsForNamespaces(s, namespaceIDs, true)
rawProjectsMap, _, _, err := getRawProjectsForUser(
s,
&projectOptions{
search: "",
user: u,
page: 0,
perPage: -1,
getArchived: true,
})
if err != nil {
return err
return taskIDs, err
}
if len(rawProjectsMap) == 0 {
return
}
projects := []*Project{}
projectsMap := make(map[int64]*ProjectWithTasksAndBuckets, len(rawProjectsMap))
projectIDs := []int64{}
for _, p := range rawProjectsMap {
projects = append(projects, p)
projectsMap[p.ID] = &ProjectWithTasksAndBuckets{
Project: *p,
}
projectIDs = append(projectIDs, p.ID)
}
tasks, _, _, err := getTasksForProjects(s, projects, u, &taskOptions{
@ -173,7 +157,7 @@ func exportProjectsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err
perPage: -1,
})
if err != nil {
return err
return taskIDs, err
}
taskMap := make(map[int64]*TaskWithComments, len(tasks))
@ -181,11 +165,12 @@ func exportProjectsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err
taskMap[t.ID] = &TaskWithComments{
Task: *t,
}
if _, exists := projectMap[t.ProjectID]; !exists {
if _, exists := projectsMap[t.ProjectID]; !exists {
log.Debugf("[User Data Export] Project %d does not exist for task %d, omitting", t.ProjectID, t.ID)
continue
}
projectMap[t.ProjectID].Tasks = append(projectMap[t.ProjectID].Tasks, taskMap[t.ID])
projectsMap[t.ProjectID].Tasks = append(projectsMap[t.ProjectID].Tasks, taskMap[t.ID])
taskIDs = append(taskIDs, t.ID)
}
comments := []*TaskComment{}
@ -212,43 +197,22 @@ func exportProjectsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err
}
for _, b := range buckets {
if _, exists := projectMap[b.ProjectID]; !exists {
if _, exists := projectsMap[b.ProjectID]; !exists {
log.Debugf("[User Data Export] Project %d does not exist for bucket %d, omitting", b.ProjectID, b.ID)
continue
}
projectMap[b.ProjectID].Buckets = append(projectMap[b.ProjectID].Buckets, b)
projectsMap[b.ProjectID].Buckets = append(projectsMap[b.ProjectID].Buckets, b)
}
data, err := json.Marshal(namespaces)
data, err := json.Marshal(projects)
if err != nil {
return err
return taskIDs, err
}
return utils.WriteBytesToZip("data.json", data, wr)
return taskIDs, utils.WriteBytesToZip("data.json", data, wr)
}
func exportTaskAttachments(s *xorm.Session, u *user.User, wr *zip.Writer) (err error) {
projects, _, _, err := getRawProjectsForUser(
s,
&projectOptions{
user: u,
page: -1,
},
)
if err != nil {
return err
}
tasks, _, _, err := getRawTasksForProjects(s, projects, u, &taskOptions{page: -1})
if err != nil {
return err
}
taskIDs := []int64{}
for _, t := range tasks {
taskIDs = append(taskIDs, t.ID)
}
func exportTaskAttachments(s *xorm.Session, u *user.User, wr *zip.Writer, taskIDs []int64) (err error) {
tas, err := getTaskAttachmentsByTaskIDs(s, taskIDs)
if err != nil {
return err

View File

@ -35,8 +35,6 @@ import (
func RegisterListeners() {
events.RegisterListener((&ProjectCreatedEvent{}).Name(), &IncreaseProjectCounter{})
events.RegisterListener((&ProjectDeletedEvent{}).Name(), &DecreaseProjectCounter{})
events.RegisterListener((&NamespaceCreatedEvent{}).Name(), &IncreaseNamespaceCounter{})
events.RegisterListener((&NamespaceDeletedEvent{}).Name(), &DecreaseNamespaceCounter{})
events.RegisterListener((&TaskCreatedEvent{}).Name(), &IncreaseTaskCounter{})
events.RegisterListener((&TaskDeletedEvent{}).Name(), &DecreaseTaskCounter{})
events.RegisterListener((&TeamDeletedEvent{}).Name(), &DecreaseTeamCounter{})
@ -478,37 +476,6 @@ func (s *SendProjectCreatedNotification) Handle(msg *message.Message) (err error
return nil
}
//////
// Namespace events
// IncreaseNamespaceCounter represents a listener
type IncreaseNamespaceCounter struct {
}
// Name defines the name for the IncreaseNamespaceCounter listener
func (s *IncreaseNamespaceCounter) Name() string {
return "namespace.counter.increase"
}
// Hanlde is executed when the event IncreaseNamespaceCounter listens on is fired
func (s *IncreaseNamespaceCounter) Handle(msg *message.Message) (err error) {
return keyvalue.IncrBy(metrics.NamespaceCountKey, 1)
}
// DecreaseNamespaceCounter represents a listener
type DecreaseNamespaceCounter struct {
}
// Name defines the name for the DecreaseNamespaceCounter listener
func (s *DecreaseNamespaceCounter) Name() string {
return "namespace.counter.decrease"
}
// Hanlde is executed when the event DecreaseNamespaceCounter listens on is fired
func (s *DecreaseNamespaceCounter) Handle(msg *message.Message) (err error) {
return keyvalue.DecrBy(metrics.NamespaceCountKey, 1)
}
///////
// Team Events

View File

@ -44,10 +44,7 @@ func GetTables() []interface{} {
&Team{},
&TeamMember{},
&TeamProject{},
&TeamNamespace{},
&Namespace{},
&ProjectUser{},
&NamespaceUser{},
&TaskAssginee{},
&Label{},
&LabelTask{},

View File

@ -37,7 +37,7 @@ import (
type Project struct {
// The unique, numeric id of this project.
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"project"`
// The title of the project. You'll see this in the namespace overview.
// The title of the project. You'll see this in the overview.
Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(1|250)" minLength:"1" maxLength:"250"`
// The description of the project.
Description string `xorm:"longtext null" json:"description"`
@ -64,7 +64,7 @@ type Project struct {
// Contains a very small version of the project background to use as a blurry preview until the actual background is loaded. Check out https://blurha.sh/ to learn how it works.
BackgroundBlurHash string `xorm:"varchar(50) null" json:"background_blur_hash"`
// True if a project is a favorite. Favorite projects show up in a separate namespace. This value depends on the user making the call to the api.
// True if a project is a favorite. Favorite projects show up in a separate parent project. This value depends on the user making the call to the api.
IsFavorite bool `xorm:"-" json:"is_favorite"`
// The subscription status for the user reading this project. You can only read this property, use the subscription endpoints to modify it.
@ -114,7 +114,7 @@ var SharedProjectsPseudoProject = &Project{
Updated: time.Now(),
}
// FavoriteProjectsPseudoProject is a pseudo namespace used to hold favorite projects and tasks
// FavoriteProjectsPseudoProject is a pseudo parent project used to hold favorite projects and tasks
var FavoriteProjectsPseudoProject = &Project{
ID: -2,
Title: "Favorites",
@ -123,7 +123,7 @@ var FavoriteProjectsPseudoProject = &Project{
Updated: time.Now(),
}
// SavedFiltersPseudoProject is a pseudo namespace used to hold saved filters
// SavedFiltersPseudoProject is a pseudo parent project used to hold saved filters
var SavedFiltersPseudoProject = &Project{
ID: -3,
Title: "Filters",
@ -267,13 +267,11 @@ func (p *Project) ReadOne(s *xorm.Session, a web.Auth) (err error) {
if err != nil {
return err
}
// Check if the namespace is archived and set the namespace to archived if it is not already archived individually.
// Check if the project is archived and set it to archived if it is not already archived individually.
if !p.IsArchived {
err = p.CheckIsArchived(s)
if err != nil {
if !IsErrNamespaceIsArchived(err) && !IsErrProjectIsArchived(err) {
return
}
p.IsArchived = true
}
}
@ -561,7 +559,7 @@ func addProjectDetails(s *xorm.Session, projects map[int64]*Project, a web.Auth)
return
}
// CheckIsArchived returns an ErrProjectIsArchived or ErrNamespaceIsArchived if the project or any of its parent projects is archived.
// CheckIsArchived returns an ErrProjectIsArchived if the project or any of its parent projects is archived.
func (p *Project) CheckIsArchived(s *xorm.Session) (err error) {
// When creating a new project, we check if the parent is archived
if p.ID == 0 {
@ -569,14 +567,14 @@ func (p *Project) CheckIsArchived(s *xorm.Session) (err error) {
return p.CheckIsArchived(s)
}
p, err := GetProjectSimpleByID(s, p.ID)
project, err := GetProjectSimpleByID(s, p.ID)
if err != nil {
return err
}
// TODO: parent project
if p.IsArchived {
if project.IsArchived {
return ErrProjectIsArchived{ProjectID: p.ID}
}
@ -585,7 +583,7 @@ func (p *Project) CheckIsArchived(s *xorm.Session) (err error) {
func checkProjectBeforeUpdateOrDelete(s *xorm.Session, project *Project) error {
if project.ParentProjectID < 0 {
return &ErrProjectCannotBelongToAPseudoNamespace{ProjectID: project.ID, NamespaceID: project.ParentProjectID}
return &ErrProjectCannotBelongToAPseudoParentProject{ProjectID: project.ID, ParentProjectID: project.ParentProjectID}
}
// Check if the parent project exists

View File

@ -28,8 +28,8 @@ import (
type ProjectDuplicate struct {
// The project id of the project to duplicate
ProjectID int64 `json:"-" param:"projectid"`
// The target namespace ID
NamespaceID int64 `json:"namespace_id,omitempty"`
// The target parent project
ParentProjectID int64 `json:"parent_project_id,omitempty"`
// The copied project
Project *Project `json:",omitempty"`
@ -47,23 +47,27 @@ func (ld *ProjectDuplicate) CanCreate(s *xorm.Session, a web.Auth) (canCreate bo
return canRead, err
}
// Namespace exists + user has write access to is (-> can create new projects)
ld.Project.NamespaceID = ld.NamespaceID
return ld.Project.CanCreate(s, a)
if ld.ParentProjectID == 0 { // no parent project
return canRead, err
}
// Parent project exists + user has write access to is (-> can create new projects)
parent := &Project{ID: ld.ParentProjectID}
return parent.CanCreate(s, a)
}
// Create duplicates a project
// @Summary Duplicate an existing project
// @Description Copies the project, tasks, files, kanban data, assignees, comments, attachments, lables, relations, backgrounds, user/team rights and link shares from one project to a new namespace. The user needs read access in the project and write access in the namespace of the new project.
// @Description Copies the project, tasks, files, kanban data, assignees, comments, attachments, lables, relations, backgrounds, user/team rights and link shares from one project to a new one. The user needs read access in the project and write access in the parent of the new project.
// @tags project
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param projectID path int true "The project ID to duplicate"
// @Param project body models.ProjectDuplicate true "The target namespace which should hold the copied project."
// @Param project body models.ProjectDuplicate true "The target parent project which should hold the copied project."
// @Success 201 {object} models.ProjectDuplicate "The created project."
// @Failure 400 {object} web.HTTPError "Invalid project duplicate object provided."
// @Failure 403 {object} web.HTTPError "The user does not have access to the project or namespace"
// @Failure 403 {object} web.HTTPError "The user does not have access to the project or its parent."
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{projectID}/duplicate [put]
//
@ -153,7 +157,7 @@ func (ld *ProjectDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
}
// Rights / Shares
// To keep it simple(r) we will only copy rights which are directly used with the project, no namespace changes.
// To keep it simple(r) we will only copy rights which are directly used with the project, not the parent
users := []*ProjectUser{}
err = s.Where("project_id = ?", ld.ProjectID).Find(&users)
if err != nil {

View File

@ -16,7 +16,7 @@
package models
// Right defines the rights users/teams can have for projects/namespaces
// Right defines the rights users/teams can have for projects
type Right int
// define unknown right
@ -30,7 +30,7 @@ const (
RightRead Right = iota
// Can write in a like projects and tasks. Cannot create new projects.
RightWrite
// Can manage a project/namespace, can do everything
// Can manage a project, can do everything
RightAdmin
)

View File

@ -39,7 +39,7 @@ type SavedFilter struct {
// The user who owns this filter
Owner *user.User `xorm:"-" json:"owner" valid:"-"`
// True if the filter is a favorite. Favorite filters show up in a separate namespace together with favorite projects.
// True if the filter is a favorite. Favorite filters show up in a separate parent project together with favorite projects.
IsFavorite bool `xorm:"default false" json:"is_favorite"`
// A timestamp when this filter was created. You cannot change this value.
@ -95,14 +95,14 @@ func getSavedFiltersForUser(s *xorm.Session, auth web.Auth) (filters []*SavedFil
func (sf *SavedFilter) toProject() *Project {
return &Project{
ID: getProjectIDFromSavedFilterID(sf.ID),
Title: sf.Title,
Description: sf.Description,
IsFavorite: sf.IsFavorite,
Created: sf.Created,
Updated: sf.Updated,
Owner: sf.Owner,
NamespaceID: SavedFiltersPseudoProject.ID,
ID: getProjectIDFromSavedFilterID(sf.ID),
Title: sf.Title,
Description: sf.Description,
IsFavorite: sf.IsFavorite,
Created: sf.Created,
Updated: sf.Updated,
Owner: sf.Owner,
ParentProjectID: SavedFiltersPseudoProject.ID,
}
}

View File

@ -30,16 +30,15 @@ import (
type SubscriptionEntityType int
const (
SubscriptionEntityUnknown = iota
SubscriptionEntityNamespace
SubscriptionEntityUnknown = iota
SubscriptionEntityNamespace // Kept even though not used anymore since we don't want to manually change all ids
SubscriptionEntityProject
SubscriptionEntityTask
)
const (
entityNamespace = `namespace`
entityProject = `project`
entityTask = `task`
entityProject = `project`
entityTask = `task`
)
// Subscription represents a subscription for an entity
@ -70,8 +69,6 @@ func (sb *Subscription) TableName() string {
func getEntityTypeFromString(entityType string) SubscriptionEntityType {
switch entityType {
case entityNamespace:
return SubscriptionEntityNamespace
case entityProject:
return SubscriptionEntityProject
case entityTask:
@ -84,8 +81,6 @@ func getEntityTypeFromString(entityType string) SubscriptionEntityType {
// String returns a human-readable string of an entity
func (et SubscriptionEntityType) String() string {
switch et {
case SubscriptionEntityNamespace:
return entityNamespace
case SubscriptionEntityProject:
return entityProject
case SubscriptionEntityTask:
@ -96,8 +91,7 @@ func (et SubscriptionEntityType) String() string {
}
func (et SubscriptionEntityType) validate() error {
if et == SubscriptionEntityNamespace ||
et == SubscriptionEntityProject ||
if et == SubscriptionEntityProject ||
et == SubscriptionEntityTask {
return nil
}
@ -112,7 +106,7 @@ func (et SubscriptionEntityType) validate() error {
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param entity path string true "The entity the user subscribes to. Can be either `namespace`, `project` or `task`."
// @Param entity path string true "The entity the user subscribes to. Can be either `project` or `task`."
// @Param entityID path string true "The numeric id of the entity to subscribe to."
// @Success 201 {object} models.Subscription "The subscription"
// @Failure 403 {object} web.HTTPError "The user does not have access to subscribe to this entity."
@ -153,7 +147,7 @@ func (sb *Subscription) Create(s *xorm.Session, auth web.Auth) (err error) {
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param entity path string true "The entity the user subscribed to. Can be either `namespace`, `project` or `task`."
// @Param entity path string true "The entity the user subscribed to. Can be either `project` or `task`."
// @Param entityID path string true "The numeric id of the subscribed entity to."
// @Success 200 {object} models.Subscription "The subscription"
// @Failure 403 {object} web.HTTPError "The user does not have access to subscribe to this entity."
@ -170,45 +164,20 @@ func (sb *Subscription) Delete(s *xorm.Session, auth web.Auth) (err error) {
}
func getSubscriberCondForEntity(entityType SubscriptionEntityType, entityID int64) (cond builder.Cond) {
if entityType == SubscriptionEntityNamespace {
cond = builder.And(
builder.Eq{"entity_id": entityID},
builder.Eq{"entity_type": SubscriptionEntityNamespace},
)
}
if entityType == SubscriptionEntityProject {
cond = builder.Or(
builder.And(
builder.Eq{"entity_id": entityID},
builder.Eq{"entity_type": SubscriptionEntityProject},
),
builder.And(
builder.Eq{"entity_id": builder.
Select("namespace_id").
From("projects").
Where(builder.Eq{"id": entityID}),
},
builder.Eq{"entity_type": SubscriptionEntityNamespace},
),
return builder.And(
builder.Eq{"entity_id": entityID},
builder.Eq{"entity_type": SubscriptionEntityProject},
)
// TODO: parent?
}
if entityType == SubscriptionEntityTask {
cond = builder.Or(
return builder.Or(
builder.And(
builder.Eq{"entity_id": entityID},
builder.Eq{"entity_type": SubscriptionEntityTask},
),
builder.And(
builder.Eq{"entity_id": builder.
Select("namespace_id").
From("projects").
Join("INNER", "tasks", "projects.id = tasks.project_id").
Where(builder.Eq{"tasks.id": entityID}),
},
builder.Eq{"entity_type": SubscriptionEntityNamespace},
),
builder.And(
builder.Eq{"entity_id": builder.
Select("project_id").
@ -225,8 +194,8 @@ func getSubscriberCondForEntity(entityType SubscriptionEntityType, entityID int6
// GetSubscription returns a matching subscription for an entity and user.
// It will return the next parent of a subscription. That means for tasks, it will first look for a subscription for
// that task, if there is none it will look for a subscription on the project the task belongs to and if that also
// doesn't exist it will check for a subscription for the namespace the project is belonging to.
// that task, if there is none it will look for a subscription on the project the task belongs to.
// TODO: check parent projects
func GetSubscription(s *xorm.Session, entityType SubscriptionEntityType, entityID int64, a web.Auth) (subscription *Subscription, err error) {
subs, err := GetSubscriptions(s, entityType, []int64{entityID}, a)
if err != nil || len(subs) == 0 {

View File

@ -30,9 +30,6 @@ func (sb *Subscription) CanCreate(s *xorm.Session, a web.Auth) (can bool, err er
sb.EntityType = getEntityTypeFromString(sb.Entity)
switch sb.EntityType {
case SubscriptionEntityNamespace:
n := &Namespace{ID: sb.EntityID}
can, _, err = n.CanRead(s, a)
case SubscriptionEntityProject:
l := &Project{ID: sb.EntityID}
can, _, err = l.CanRead(s, a)

View File

@ -237,24 +237,6 @@ func getNativeValueForTaskField(fieldName string, comparator taskFilterComparato
realFieldName := strings.ReplaceAll(strcase.ToCamel(fieldName), "Id", "ID")
if realFieldName == "Namespace" {
if comparator == taskFilterComparatorIn {
vals := strings.Split(value, ",")
valueSlice := []interface{}{}
for _, val := range vals {
v, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return nil, nil, err
}
valueSlice = append(valueSlice, v)
}
return nil, valueSlice, nil
}
nativeValue, err = strconv.ParseInt(value, 10, 64)
return
}
if realFieldName == "Assignees" {
vals := strings.Split(value, ",")
valueSlice := append([]string{}, vals...)

View File

@ -241,7 +241,7 @@ func (t *Team) ReadAll(s *xorm.Session, a web.Auth, search string, page int, per
// Create is the handler to create a team
// @Summary Creates a new team
// @Description Creates a new team in a given namespace. The user needs write-access to the namespace.
// @Description Creates a new team.
// @tags team
// @Accept json
// @Produce json
@ -307,12 +307,6 @@ func (t *Team) Delete(s *xorm.Session, a web.Auth) (err error) {
return
}
// Delete team <-> namespace relations
_, err = s.Where("team_id = ?", t.ID).Delete(&TeamNamespace{})
if err != nil {
return
}
// Delete team <-> projects relations
_, err = s.Where("team_id = ?", t.ID).Delete(&TeamProject{})
if err != nil {

View File

@ -48,7 +48,6 @@ func SetupTests() {
"labels",
"link_shares",
"projects",
"namespaces",
"task_assignees",
"task_attachments",
"task_comments",
@ -57,12 +56,10 @@ func SetupTests() {
"tasks",
"team_projects",
"team_members",
"team_namespaces",
"teams",
"users",
"user_tokens",
"users_projects",
"users_namespaces",
"buckets",
"saved_filters",
"subscriptions",

View File

@ -22,14 +22,11 @@ import (
"xorm.io/xorm"
)
// ProjectUIDs hold all kinds of user IDs from accounts who have somehow access to a project
// ProjectUIDs hold all kinds of user IDs from accounts who have access to a project
type ProjectUIDs struct {
ProjectOwnerID int64 `xorm:"projectOwner"`
NamespaceUserID int64 `xorm:"unID"`
ProjectUserID int64 `xorm:"ulID"`
NamespaceOwnerUserID int64 `xorm:"nOwner"`
TeamNamespaceUserID int64 `xorm:"tnUID"`
TeamProjectUserID int64 `xorm:"tlUID"`
ProjectOwnerID int64 `xorm:"projectOwner"`
ProjectUserID int64 `xorm:"ulID"`
TeamProjectUserID int64 `xorm:"tlUID"`
}
// ListUsersFromProject returns a list with all users who have access to a project, regardless of the method which gave them access
@ -39,39 +36,26 @@ func ListUsersFromProject(s *xorm.Session, l *Project, search string) (users []*
err = s.
Select(`l.owner_id as projectOwner,
un.user_id as unID,
ul.user_id as ulID,
n.owner_id as nOwner,
tm.user_id as tnUID,
tm2.user_id as tlUID`).
Table("projects").
Alias("l").
// User stuff
Join("LEFT", []string{"users_namespaces", "un"}, "un.namespace_id = l.namespace_id").
Join("LEFT", []string{"users_projects", "ul"}, "ul.project_id = l.id").
Join("LEFT", []string{"namespaces", "n"}, "n.id = l.namespace_id").
// Team stuff
Join("LEFT", []string{"team_namespaces", "tn"}, " l.namespace_id = tn.namespace_id").
Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tn.team_id").
Join("LEFT", []string{"team_projects", "tl"}, "l.id = tl.project_id").
Join("LEFT", []string{"team_members", "tm2"}, "tm2.team_id = tl.team_id").
// The actual condition
Where(
builder.Or(
builder.Or(builder.Eq{"ul.right": RightRead}),
builder.Or(builder.Eq{"un.right": RightRead}),
builder.Or(builder.Eq{"tl.right": RightRead}),
builder.Or(builder.Eq{"tn.right": RightRead}),
builder.Or(builder.Eq{"ul.right": RightWrite}),
builder.Or(builder.Eq{"un.right": RightWrite}),
builder.Or(builder.Eq{"tl.right": RightWrite}),
builder.Or(builder.Eq{"tn.right": RightWrite}),
builder.Or(builder.Eq{"ul.right": RightAdmin}),
builder.Or(builder.Eq{"un.right": RightAdmin}),
builder.Or(builder.Eq{"tl.right": RightAdmin}),
builder.Or(builder.Eq{"tn.right": RightAdmin}),
),
builder.Eq{"l.id": l.ID},
).
@ -85,10 +69,7 @@ func ListUsersFromProject(s *xorm.Session, l *Project, search string) (users []*
uidmap[l.OwnerID] = true
for _, u := range userids {
uidmap[u.ProjectUserID] = true
uidmap[u.NamespaceOwnerUserID] = true
uidmap[u.NamespaceUserID] = true
uidmap[u.TeamProjectUserID] = true
uidmap[u.TeamNamespaceUserID] = true
}
uids := make([]int64, 0, len(uidmap))

View File

@ -244,7 +244,7 @@ func getOrCreateUser(s *xorm.Session, cl *claims, issuer, subject string) (u *us
}
}
// And create its namespace
// And create their project
err = models.CreateNewProjectForUser(s, u)
if err != nil {
return nil, err

View File

@ -62,7 +62,7 @@ func RegisterUser(c echo.Context) error {
return handler.HandleHTTPError(err, c)
}
// Add its namespace
// Create their initial project
err = models.CreateNewProjectForUser(s, newUser)
if err != nil {
_ = s.Rollback()

View File

@ -51,10 +51,6 @@ func setupMetrics(a *echo.Group) {
metrics.UserCountKey,
user.User{},
},
{
metrics.NamespaceCountKey,
models.Namespace{},
},
{
metrics.TaskCountKey,
models.Task{},