diff --git a/pkg/cmd/user.go b/pkg/cmd/user.go
index 7e9f09f83..9f3a2c9d0 100644
--- a/pkg/cmd/user.go
+++ b/pkg/cmd/user.go
@@ -188,7 +188,7 @@ var userCreateCmd = &cobra.Command{
log.Fatalf("Error creating new user: %s", err)
}
- err = models.CreateNewNamespaceForUser(s, newUser)
+ err = models.CreateNewProjectForUser(s, newUser)
if err != nil {
_ = s.Rollback()
log.Fatalf("Error creating new namespace for user: %s", err)
diff --git a/pkg/models/label_rights.go b/pkg/models/label_rights.go
index b05b4c704..187dc99fc 100644
--- a/pkg/models/label_rights.go
+++ b/pkg/models/label_rights.go
@@ -77,7 +77,7 @@ func (l *Label) hasAccessToLabel(s *xorm.Session, a web.Auth) (has bool, maxRigh
builder.
Select("id").
From("tasks").
- Where(builder.In("project_id", getUserProjectsStatement(u.ID).Select("l.id"))),
+ Where(builder.In("project_id", getUserProjectsStatement(u.ID, "", false).Select("l.id"))),
)
ll := &LabelTask{}
diff --git a/pkg/models/label_task.go b/pkg/models/label_task.go
index 190db7fe2..d16d0557e 100644
--- a/pkg/models/label_task.go
+++ b/pkg/models/label_task.go
@@ -180,7 +180,7 @@ func GetLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*Lab
builder.
Select("id").
From("tasks").
- Where(builder.In("project_id", getUserProjectsStatement(opts.GetForUser).Select("l.id"))),
+ Where(builder.In("project_id", getUserProjectsStatement(opts.GetForUser, "", false).Select("l.id"))),
), cond)
}
if opts.GetUnusedLabels {
diff --git a/pkg/models/namespace.go b/pkg/models/namespace.go
deleted file mode 100644
index c78a23096..000000000
--- a/pkg/models/namespace.go
+++ /dev/null
@@ -1,774 +0,0 @@
-// Vikunja is a to-do list application to facilitate your life.
-// Copyright 2018-2021 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 (
- "sort"
- "strconv"
- "strings"
- "time"
-
- "code.vikunja.io/api/pkg/db"
-
- "code.vikunja.io/api/pkg/events"
- "code.vikunja.io/api/pkg/log"
- "code.vikunja.io/api/pkg/user"
-
- "code.vikunja.io/web"
- "xorm.io/builder"
- "xorm.io/xorm"
-)
-
-// Namespace holds informations about a namespace
-type Namespace struct {
- // The unique, numeric id of this namespace.
- ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"namespace"`
- // The name of this namespace.
- Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(1|250)" minLength:"1" maxLength:"250"`
- // The description of the namespace
- Description string `xorm:"longtext null" json:"description"`
- OwnerID int64 `xorm:"bigint not null INDEX" json:"-"`
-
- // The hex color of this namespace
- HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
-
- // Whether or not a namespace is archived.
- IsArchived bool `xorm:"not null default false" json:"is_archived" query:"is_archived"`
-
- // The user who owns this namespace
- Owner *user.User `xorm:"-" json:"owner" valid:"-"`
-
- // The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it.
- // Will only returned when retreiving one namespace.
- Subscription *Subscription `xorm:"-" json:"subscription,omitempty"`
-
- // A timestamp when this namespace was created. You cannot change this value.
- Created time.Time `xorm:"created not null" json:"created"`
- // A timestamp when this namespace was last updated. You cannot change this value.
- Updated time.Time `xorm:"updated not null" json:"updated"`
-
- // If set to true, will only return the namespaces, not their projects.
- NamespacesOnly bool `xorm:"-" json:"-" query:"namespaces_only"`
-
- web.CRUDable `xorm:"-" json:"-"`
- web.Rights `xorm:"-" json:"-"`
-}
-
-// SharedProjectsPseudoNamespace is a pseudo namespace used to hold shared projects
-var SharedProjectsPseudoNamespace = Namespace{
- ID: -1,
- Title: "Shared Projects",
- Description: "Projects of other users shared with you via teams or directly.",
- Created: time.Now(),
- Updated: time.Now(),
-}
-
-// FavoritesPseudoNamespace is a pseudo namespace used to hold favorited projects and tasks
-var FavoritesPseudoNamespace = Namespace{
- ID: -2,
- Title: "Favorites",
- Description: "Favorite projects and tasks.",
- Created: time.Now(),
- Updated: time.Now(),
-}
-
-// SavedFiltersPseudoNamespace is a pseudo namespace used to hold saved filters
-var SavedFiltersPseudoNamespace = Namespace{
- ID: -3,
- Title: "Filters",
- Description: "Saved filters.",
- Created: time.Now(),
- Updated: time.Now(),
-}
-
-// TableName makes beautiful table names
-func (Namespace) TableName() string {
- return "namespaces"
-}
-
-// GetSimpleByID gets a namespace without things like the owner, it more or less only checks if it exists.
-func getNamespaceSimpleByID(s *xorm.Session, id int64) (namespace *Namespace, err error) {
- if id == 0 {
- return nil, ErrNamespaceDoesNotExist{ID: id}
- }
-
- // Get the namesapce with shared projects
- if id == -1 {
- return &SharedProjectsPseudoNamespace, nil
- }
-
- if id == FavoritesPseudoNamespace.ID {
- return &FavoritesPseudoNamespace, nil
- }
-
- if id == SavedFiltersPseudoNamespace.ID {
- return &SavedFiltersPseudoNamespace, nil
- }
-
- namespace = &Namespace{}
-
- exists, err := s.Where("id = ?", id).Get(namespace)
- if err != nil {
- return
- }
- if !exists {
- return nil, ErrNamespaceDoesNotExist{ID: id}
- }
-
- return
-}
-
-// GetNamespaceByID returns a namespace object by its ID
-func GetNamespaceByID(s *xorm.Session, id int64) (namespace *Namespace, err error) {
- namespace, err = getNamespaceSimpleByID(s, id)
- if err != nil {
- return
- }
-
- // Get the namespace Owner
- namespace.Owner, err = user.GetUserByID(s, namespace.OwnerID)
- return
-}
-
-// CheckIsArchived returns an ErrNamespaceIsArchived if the namepace is archived.
-func (n *Namespace) CheckIsArchived(s *xorm.Session) error {
- exists, err := s.
- Where("id = ? AND is_archived = true", n.ID).
- Exist(&Namespace{})
- if err != nil {
- return err
- }
- if exists {
- return ErrNamespaceIsArchived{NamespaceID: n.ID}
- }
- return nil
-}
-
-// ReadOne gets one namespace
-// @Summary Gets one namespace
-// @Description Returns a namespace by its ID.
-// @tags namespace
-// @Accept json
-// @Produce json
-// @Security JWTKeyAuth
-// @Param id path int true "Namespace ID"
-// @Success 200 {object} models.Namespace "The Namespace"
-// @Failure 403 {object} web.HTTPError "The user does not have access to that namespace."
-// @Failure 500 {object} models.Message "Internal error"
-// @Router /namespaces/{id} [get]
-func (n *Namespace) ReadOne(s *xorm.Session, a web.Auth) (err error) {
- nn, err := GetNamespaceByID(s, n.ID)
- if err != nil {
- return err
- }
- *n = *nn
-
- n.Subscription, err = GetSubscription(s, SubscriptionEntityNamespace, n.ID, a)
- return
-}
-
-// NamespaceWithProjects represents a namespace with project meta informations
-type NamespaceWithProjects struct {
- Namespace `xorm:"extends"`
- Projects []*Project `xorm:"-" json:"projects"`
-}
-
-type NamespaceWithProjectsAndTasks struct {
- Namespace
- Projects []*ProjectWithTasksAndBuckets `xorm:"-" json:"projects"`
-}
-
-func makeNamespaceSlice(namespaces map[int64]*NamespaceWithProjects, userMap map[int64]*user.User, subscriptions map[int64]*Subscription) []*NamespaceWithProjects {
- all := make([]*NamespaceWithProjects, 0, len(namespaces))
- for _, n := range namespaces {
- n.Owner = userMap[n.OwnerID]
- n.Subscription = subscriptions[n.ID]
- all = append(all, n)
- for _, l := range n.Projects {
- if n.Subscription != nil && l.Subscription == nil {
- l.Subscription = n.Subscription
- }
- }
- }
- sort.Slice(all, func(i, j int) bool {
- return all[i].ID < all[j].ID
- })
-
- return all
-}
-
-func getNamespaceFilterCond(search string) (filterCond builder.Cond) {
- filterCond = db.ILIKE("namespaces.title", search)
-
- if search == "" {
- return
- }
-
- vals := strings.Split(search, ",")
-
- if len(vals) == 0 {
- return
- }
-
- ids := []int64{}
- for _, val := range vals {
- v, err := strconv.ParseInt(val, 10, 64)
- if err != nil {
- log.Debugf("Namespace search string part '%s' is not a number: %s", val, err)
- continue
- }
- ids = append(ids, v)
- }
-
- if len(ids) > 0 {
- filterCond = builder.In("namespaces.id", ids)
- }
-
- return
-}
-
-func getNamespaceArchivedCond(archived bool) builder.Cond {
- // Adding a 1=1 condition by default here because xorm always needs a condition and cannot handle nil conditions
- var isArchivedCond builder.Cond = builder.Eq{"1": 1}
- if !archived {
- isArchivedCond = builder.And(
- builder.Eq{"namespaces.is_archived": false},
- )
- }
-
- return isArchivedCond
-}
-
-func getNamespacesWithProjects(s *xorm.Session, namespaces *map[int64]*NamespaceWithProjects, search string, isArchived bool, page, perPage int, userID int64) (numberOfTotalItems int64, err error) {
- isArchivedCond := getNamespaceArchivedCond(isArchived)
- filterCond := getNamespaceFilterCond(search)
-
- limit, start := getLimitFromPageIndex(page, perPage)
- query := s.Select("namespaces.*").
- Table("namespaces").
- Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id").
- Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id").
- Join("LEFT", "users_namespaces", "users_namespaces.namespace_id = namespaces.id").
- Where("team_members.user_id = ?", userID).
- Or("namespaces.owner_id = ?", userID).
- Or("users_namespaces.user_id = ?", userID).
- GroupBy("namespaces.id").
- Where(filterCond).
- Where(isArchivedCond)
- if limit > 0 {
- query = query.Limit(limit, start)
- }
- err = query.Find(namespaces)
- if err != nil {
- return 0, err
- }
-
- numberOfTotalItems, err = s.
- Table("namespaces").
- Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id").
- Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id").
- Join("LEFT", "users_namespaces", "users_namespaces.namespace_id = namespaces.id").
- Where("team_members.user_id = ?", userID).
- Or("namespaces.owner_id = ?", userID).
- Or("users_namespaces.user_id = ?", userID).
- And("namespaces.is_archived = false").
- GroupBy("namespaces.id").
- Where(filterCond).
- Where(isArchivedCond).
- Count(&NamespaceWithProjects{})
- return numberOfTotalItems, err
-}
-
-func getNamespaceOwnerIDs(namespaces map[int64]*NamespaceWithProjects) (namespaceIDs, ownerIDs []int64) {
- for _, nsp := range namespaces {
- namespaceIDs = append(namespaceIDs, nsp.ID)
- ownerIDs = append(ownerIDs, nsp.OwnerID)
- }
-
- return
-}
-
-func getNamespaceSubscriptions(s *xorm.Session, namespaceIDs []int64, userID int64) (map[int64]*Subscription, error) {
- subscriptionsMap := make(map[int64]*Subscription)
- if len(namespaceIDs) == 0 {
- return subscriptionsMap, nil
- }
-
- subscriptions := []*Subscription{}
- err := s.
- Where("entity_type = ? AND user_id = ?", SubscriptionEntityNamespace, userID).
- In("entity_id", namespaceIDs).
- Find(&subscriptions)
- if err != nil {
- return nil, err
- }
- for _, sub := range subscriptions {
- sub.Entity = sub.EntityType.String()
- subscriptionsMap[sub.EntityID] = sub
- }
-
- return subscriptionsMap, err
-}
-
-func getProjectsForNamespaces(s *xorm.Session, namespaceIDs []int64, archived bool) ([]*Project, error) {
- projects := []*Project{}
- projectQuery := s.
- OrderBy("position").
- In("namespace_id", namespaceIDs)
-
- if !archived {
- projectQuery.And("is_archived = false")
- }
- err := projectQuery.Find(&projects)
- return projects, err
-}
-
-func getSharedProjectsInNamespace(s *xorm.Session, archived bool, doer *user.User) (sharedProjectsNamespace *NamespaceWithProjects, err error) {
- // Create our pseudo namespace to hold the shared projects
- sharedProjectsPseudonamespace := SharedProjectsPseudoNamespace
- sharedProjectsPseudonamespace.OwnerID = doer.ID
- sharedProjectsNamespace = &NamespaceWithProjects{
- sharedProjectsPseudonamespace,
- []*Project{},
- }
-
- // Get all projects individually shared with our user (not via a namespace)
- individualProjects := []*Project{}
- iProjectQuery := s.Select("l.*").
- Table("projects").
- Alias("l").
- Join("LEFT", []string{"team_projects", "tl"}, "l.id = tl.project_id").
- Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tl.team_id").
- Join("LEFT", []string{"users_projects", "ul"}, "ul.project_id = l.id").
- Where(builder.And(
- builder.Eq{"tm.user_id": doer.ID},
- builder.Neq{"l.owner_id": doer.ID},
- )).
- Or(builder.And(
- builder.Eq{"ul.user_id": doer.ID},
- builder.Neq{"l.owner_id": doer.ID},
- )).
- GroupBy("l.id")
- if !archived {
- iProjectQuery.And("l.is_archived = false")
- }
- err = iProjectQuery.Find(&individualProjects)
- if err != nil {
- return
- }
-
- // Make the namespace -1 so we now later which one it was
- // + Append it to all projects we already have
- for _, l := range individualProjects {
- l.NamespaceID = sharedProjectsNamespace.ID
- }
-
- sharedProjectsNamespace.Projects = individualProjects
-
- // Remove the sharedProjectsPseudonamespace if we don't have any shared projects
- if len(individualProjects) == 0 {
- sharedProjectsNamespace = nil
- }
-
- return
-}
-
-func getFavoriteProjects(s *xorm.Session, projects []*Project, namespaceIDs []int64, doer *user.User) (favoriteNamespace *NamespaceWithProjects, err error) {
- // Create our pseudo namespace with favorite projects
- pseudoFavoriteNamespace := FavoritesPseudoNamespace
- pseudoFavoriteNamespace.OwnerID = doer.ID
- favoriteNamespace = &NamespaceWithProjects{
- Namespace: pseudoFavoriteNamespace,
- Projects: []*Project{{}},
- }
- *favoriteNamespace.Projects[0] = FavoritesPseudoProject // Copying the project to be able to modify it later
- favoriteNamespace.Projects[0].Owner = doer
-
- for _, project := range projects {
- if !project.IsFavorite {
- continue
- }
- favoriteNamespace.Projects = append(favoriteNamespace.Projects, project)
- }
-
- // Check if we have any favorites or favorited projects and remove the favorites namespace from the project if not
- cond := builder.
- Select("tasks.id").
- From("tasks").
- Join("INNER", "projects", "tasks.project_id = projects.id").
- Join("INNER", "namespaces", "projects.namespace_id = namespaces.id").
- Where(builder.In("namespaces.id", namespaceIDs))
-
- var favoriteCount int64
- favoriteCount, err = s.
- Where(builder.And(
- builder.Eq{"user_id": doer.ID},
- builder.Eq{"kind": FavoriteKindTask},
- builder.In("entity_id", cond),
- )).
- Count(&Favorite{})
- if err != nil {
- return
- }
-
- // If we don't have any favorites in the favorites pseudo project, remove that pseudo project from the namespace
- if favoriteCount == 0 {
- for in, l := range favoriteNamespace.Projects {
- if l.ID == FavoritesPseudoProject.ID {
- favoriteNamespace.Projects = append(favoriteNamespace.Projects[:in], favoriteNamespace.Projects[in+1:]...)
- break
- }
- }
- }
-
- // If we don't have any favorites in the namespace, remove it
- if len(favoriteNamespace.Projects) == 0 {
- return nil, nil
- }
-
- return
-}
-
-func getSavedFilters(s *xorm.Session, doer *user.User) (savedFiltersNamespace *NamespaceWithProjects, err error) {
- savedFilters, err := getSavedFiltersForUser(s, doer)
- if err != nil {
- return
- }
-
- if len(savedFilters) == 0 {
- return nil, nil
- }
-
- savedFiltersPseudoNamespace := SavedFiltersPseudoNamespace
- savedFiltersPseudoNamespace.OwnerID = doer.ID
- savedFiltersNamespace = &NamespaceWithProjects{
- Namespace: savedFiltersPseudoNamespace,
- Projects: make([]*Project, 0, len(savedFilters)),
- }
-
- for _, filter := range savedFilters {
- filterProject := filter.toProject()
- filterProject.NamespaceID = savedFiltersNamespace.ID
- filterProject.Owner = doer
- savedFiltersNamespace.Projects = append(savedFiltersNamespace.Projects, filterProject)
- }
-
- return
-}
-
-// ReadAll gets all namespaces a user has access to
-// @Summary Get all namespaces a user has access to
-// @Description Returns all namespaces a user has access to.
-// @tags namespace
-// @Accept json
-// @Produce json
-// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
-// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
-// @Param s query string false "Search namespaces by name."
-// @Param is_archived query bool false "If true, also returns all archived namespaces."
-// @Param namespaces_only query bool false "If true, also returns only namespaces without their projects."
-// @Security JWTKeyAuth
-// @Success 200 {array} models.NamespaceWithProjects "The Namespaces."
-// @Failure 500 {object} models.Message "Internal error"
-// @Router /namespaces [get]
-//
-//nolint:gocyclo
-func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
- if _, is := a.(*LinkSharing); is {
- return nil, 0, 0, ErrGenericForbidden{}
- }
-
- // This map will hold all namespaces and their projects. The key is usually the id of the namespace.
- // We're using a map here because it makes a few things like adding projects or removing pseudo namespaces easier.
- namespaces := make(map[int64]*NamespaceWithProjects)
-
- //////////////////////////////
- // Projects with their namespaces
-
- doer, err := user.GetFromAuth(a)
- if err != nil {
- return nil, 0, 0, err
- }
-
- numberOfTotalItems, err = getNamespacesWithProjects(s, &namespaces, search, n.IsArchived, page, perPage, doer.ID)
- if err != nil {
- return nil, 0, 0, err
- }
-
- namespaceIDs, ownerIDs := getNamespaceOwnerIDs(namespaces)
-
- if len(namespaceIDs) == 0 {
- return nil, 0, 0, nil
- }
-
- subscriptionsMap, err := getNamespaceSubscriptions(s, namespaceIDs, doer.ID)
- if err != nil {
- return nil, 0, 0, err
- }
-
- ownerMap, err := user.GetUsersByIDs(s, ownerIDs)
- if err != nil {
- return nil, 0, 0, err
- }
- ownerMap[doer.ID] = doer
-
- if n.NamespacesOnly {
- all := makeNamespaceSlice(namespaces, ownerMap, subscriptionsMap)
- return all, len(all), numberOfTotalItems, nil
- }
-
- // Get all projects
- projects, err := getProjectsForNamespaces(s, namespaceIDs, n.IsArchived)
- if err != nil {
- return nil, 0, 0, err
- }
-
- ///////////////
- // Shared Projects
-
- sharedProjectsNamespace, err := getSharedProjectsInNamespace(s, n.IsArchived, doer)
- if err != nil {
- return nil, 0, 0, err
- }
-
- if sharedProjectsNamespace != nil {
- namespaces[sharedProjectsNamespace.ID] = sharedProjectsNamespace
- projects = append(projects, sharedProjectsNamespace.Projects...)
- }
-
- /////////////////
- // Saved Filters
-
- savedFiltersNamespace, err := getSavedFilters(s, doer)
- if err != nil {
- return nil, 0, 0, err
- }
-
- if savedFiltersNamespace != nil {
- namespaces[savedFiltersNamespace.ID] = savedFiltersNamespace
- projects = append(projects, savedFiltersNamespace.Projects...)
- }
-
- /////////////////
- // Add project details (favorite state, among other things)
- err = addProjectDetails(s, projects, a)
- if err != nil {
- return
- }
-
- /////////////////
- // Favorite projects
-
- favoritesNamespace, err := getFavoriteProjects(s, projects, namespaceIDs, doer)
- if err != nil {
- return nil, 0, 0, err
- }
-
- if favoritesNamespace != nil {
- namespaces[favoritesNamespace.ID] = favoritesNamespace
- }
-
- //////////////////////
- // Put it all together
-
- for _, project := range projects {
- if project.NamespaceID == SharedProjectsPseudoNamespace.ID || project.NamespaceID == SavedFiltersPseudoNamespace.ID {
- // Shared projects and filtered projects are already in the namespace
- continue
- }
- namespaces[project.NamespaceID].Projects = append(namespaces[project.NamespaceID].Projects, project)
- }
-
- all := makeNamespaceSlice(namespaces, ownerMap, subscriptionsMap)
- return all, len(all), numberOfTotalItems, err
-}
-
-// Create implements the creation method via the interface
-// @Summary Creates a new namespace
-// @Description Creates a new namespace.
-// @tags namespace
-// @Accept json
-// @Produce json
-// @Security JWTKeyAuth
-// @Param namespace body models.Namespace true "The namespace you want to create."
-// @Success 201 {object} models.Namespace "The created namespace."
-// @Failure 400 {object} web.HTTPError "Invalid namespace object provided."
-// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
-// @Failure 500 {object} models.Message "Internal error"
-// @Router /namespaces [put]
-func (n *Namespace) Create(s *xorm.Session, a web.Auth) (err error) {
- // Check if we have at least a title
- if n.Title == "" {
- return ErrNamespaceNameCannotBeEmpty{NamespaceID: 0, UserID: a.GetID()}
- }
-
- n.Owner, err = user.GetUserByID(s, a.GetID())
- if err != nil {
- return
- }
- n.OwnerID = n.Owner.ID
-
- if _, err = s.Insert(n); err != nil {
- return err
- }
-
- err = events.Dispatch(&NamespaceCreatedEvent{
- Namespace: n,
- Doer: a,
- })
- if err != nil {
- return err
- }
-
- return
-}
-
-// CreateNewNamespaceForUser creates a new namespace for a user. To prevent import cycles, we can't do that
-// directly in the user.Create function.
-func CreateNewNamespaceForUser(s *xorm.Session, user *user.User) (err error) {
- newN := &Namespace{
- Title: user.Username,
- Description: user.Username + "'s namespace.",
- }
- return newN.Create(s, user)
-}
-
-// Delete deletes a namespace
-// @Summary Deletes a namespace
-// @Description Delets a namespace
-// @tags namespace
-// @Produce json
-// @Security JWTKeyAuth
-// @Param id path int true "Namespace ID"
-// @Success 200 {object} models.Message "The namespace was successfully deleted."
-// @Failure 400 {object} web.HTTPError "Invalid namespace object provided."
-// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
-// @Failure 500 {object} models.Message "Internal error"
-// @Router /namespaces/{id} [delete]
-func (n *Namespace) Delete(s *xorm.Session, a web.Auth) (err error) {
- return deleteNamespace(s, n, a, true)
-}
-
-func deleteNamespace(s *xorm.Session, n *Namespace, a web.Auth, withProjects bool) (err error) {
- // Check if the namespace exists
- _, err = GetNamespaceByID(s, n.ID)
- if err != nil {
- return
- }
-
- // Delete the namespace
- _, err = s.ID(n.ID).Delete(&Namespace{})
- if err != nil {
- return
- }
-
- namespaceDeleted := &NamespaceDeletedEvent{
- Namespace: n,
- Doer: a,
- }
-
- if !withProjects {
- return events.Dispatch(namespaceDeleted)
- }
-
- // Delete all projects with their tasks
- projects, err := GetProjectsByNamespaceID(s, n.ID, &user.User{})
- if err != nil {
- return
- }
-
- if len(projects) == 0 {
- return events.Dispatch(namespaceDeleted)
- }
-
- // Looping over all projects to let the project handle properly cleaning up the tasks and everything else associated with it.
- for _, project := range projects {
- err = project.Delete(s, a)
- if err != nil {
- return err
- }
- }
-
- return events.Dispatch(namespaceDeleted)
-}
-
-// Update implements the update method via the interface
-// @Summary Updates a namespace
-// @Description Updates a namespace.
-// @tags namespace
-// @Accept json
-// @Produce json
-// @Security JWTKeyAuth
-// @Param id path int true "Namespace ID"
-// @Param namespace body models.Namespace true "The namespace with updated values you want to update."
-// @Success 200 {object} models.Namespace "The updated namespace."
-// @Failure 400 {object} web.HTTPError "Invalid namespace object provided."
-// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
-// @Failure 500 {object} models.Message "Internal error"
-// @Router /namespace/{id} [post]
-func (n *Namespace) Update(s *xorm.Session, a web.Auth) (err error) {
- // Check if we have at least a name
- if n.Title == "" {
- return ErrNamespaceNameCannotBeEmpty{NamespaceID: n.ID}
- }
-
- // Check if the namespace exists
- currentNamespace, err := GetNamespaceByID(s, n.ID)
- if err != nil {
- return
- }
-
- // Check if the namespace is archived and the update is not un-archiving it
- if currentNamespace.IsArchived && n.IsArchived {
- return ErrNamespaceIsArchived{NamespaceID: n.ID}
- }
-
- // Check if the (new) owner exists
- if n.Owner != nil {
- n.OwnerID = n.Owner.ID
- if currentNamespace.OwnerID != n.OwnerID {
- n.Owner, err = user.GetUserByID(s, n.OwnerID)
- if err != nil {
- return
- }
- }
- }
-
- // We need to specify the cols we want to update here to be able to un-archive projects
- colsToUpdate := []string{
- "title",
- "is_archived",
- "hex_color",
- }
- if n.Description != "" {
- colsToUpdate = append(colsToUpdate, "description")
- }
-
- // Do the actual update
- _, err = s.
- ID(currentNamespace.ID).
- Cols(colsToUpdate...).
- Update(n)
- if err != nil {
- return err
- }
-
- return events.Dispatch(&NamespaceUpdatedEvent{
- Namespace: n,
- Doer: a,
- })
-}
diff --git a/pkg/models/namespace_rights.go b/pkg/models/namespace_rights.go
deleted file mode 100644
index f03dbf698..000000000
--- a/pkg/models/namespace_rights.go
+++ /dev/null
@@ -1,145 +0,0 @@
-// Vikunja is a to-do list application to facilitate your life.
-// Copyright 2018-2021 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/builder"
- "xorm.io/xorm"
-)
-
-// CanWrite checks if a user has write access to a namespace
-func (n *Namespace) CanWrite(s *xorm.Session, a web.Auth) (bool, error) {
- can, _, err := n.checkRight(s, a, RightWrite, RightAdmin)
- return can, err
-}
-
-// IsAdmin returns true or false if the user is admin on that namespace or not
-func (n *Namespace) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
- is, _, err := n.checkRight(s, a, RightAdmin)
- return is, err
-}
-
-// CanRead checks if a user has read access to that namespace
-func (n *Namespace) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
- return n.checkRight(s, a, RightRead, RightWrite, RightAdmin)
-}
-
-// CanUpdate checks if the user can update the namespace
-func (n *Namespace) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
- return n.IsAdmin(s, a)
-}
-
-// CanDelete checks if the user can delete a namespace
-func (n *Namespace) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
- return n.IsAdmin(s, a)
-}
-
-// CanCreate checks if the user can create a new namespace
-func (n *Namespace) CanCreate(_ *xorm.Session, a web.Auth) (bool, error) {
- if _, is := a.(*LinkSharing); is {
- return false, nil
- }
-
- // This is currently a dummy function, later on we could imagine global limits etc.
- return true, nil
-}
-
-func (n *Namespace) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, int, error) {
-
- // If the auth is a link share, don't do anything
- if _, is := a.(*LinkSharing); is {
- return false, 0, nil
- }
-
- // Get the namespace and check the right
- nn, err := getNamespaceSimpleByID(s, n.ID)
- if err != nil {
- return false, 0, err
- }
-
- if a.GetID() == nn.OwnerID ||
- nn.ID == SharedProjectsPseudoNamespace.ID ||
- nn.ID == FavoritesPseudoNamespace.ID ||
- nn.ID == SavedFiltersPseudoNamespace.ID {
- return true, int(RightAdmin), nil
- }
-
- /*
- The following loop creates an sql condition like this one:
-
- namespaces.owner_id = 1 OR
- (users_namespaces.user_id = 1 AND users_namespaces.right = 1) OR
- (team_members.user_id = 1 AND team_namespaces.right = 1) OR
-
-
- for each passed right. That way, we can check with a single sql query (instead if 8)
- if the user has the right to see the project or not.
- */
-
- var conds []builder.Cond
- conds = append(conds, builder.Eq{"namespaces.owner_id": a.GetID()})
- for _, r := range rights {
- // User conditions
- // If the namespace was shared directly with the user and the user has the right
- conds = append(conds, builder.And(
- builder.Eq{"users_namespaces.user_id": a.GetID()},
- builder.Eq{"users_namespaces.right": r},
- ))
-
- // Team rights
- // If the namespace was shared directly with the team and the team has the right
- conds = append(conds, builder.And(
- builder.Eq{"team_members.user_id": a.GetID()},
- builder.Eq{"team_namespaces.right": r},
- ))
- }
-
- type allRights struct {
- UserNamespace NamespaceUser `xorm:"extends"`
- TeamNamespace TeamNamespace `xorm:"extends"`
- }
-
- var maxRights = 0
- r := &allRights{}
- exists, err := s.
- Select("*").
- Table("namespaces").
- // User stuff
- Join("LEFT", "users_namespaces", "users_namespaces.namespace_id = namespaces.id").
- // Teams stuff
- Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id").
- Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id").
- // The actual condition
- Where(builder.And(
- builder.Or(
- conds...,
- ),
- builder.Eq{"namespaces.id": n.ID},
- )).
- Exist(r)
-
- // Figure out the max right and return it
- if int(r.UserNamespace.Right) > maxRights {
- maxRights = int(r.UserNamespace.Right)
- }
- if int(r.TeamNamespace.Right) > maxRights {
- maxRights = int(r.TeamNamespace.Right)
- }
-
- return exists, maxRights, err
-}
diff --git a/pkg/models/namespace_team.go b/pkg/models/namespace_team.go
deleted file mode 100644
index 883256182..000000000
--- a/pkg/models/namespace_team.go
+++ /dev/null
@@ -1,244 +0,0 @@
-// Vikunja is a to-do list application to facilitate your life.
-// Copyright 2018-2021 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 (
- "time"
-
- "code.vikunja.io/api/pkg/db"
-
- "code.vikunja.io/api/pkg/events"
- "code.vikunja.io/web"
-
- "xorm.io/xorm"
-)
-
-// TeamNamespace defines the relationship between a Team and a Namespace
-type TeamNamespace struct {
- // The unique, numeric id of this namespace <-> team relation.
- ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"`
- // The team id.
- TeamID int64 `xorm:"bigint not null INDEX" json:"team_id" param:"team"`
- // The namespace id.
- NamespaceID int64 `xorm:"bigint not null INDEX" json:"-" param:"namespace"`
- // The right this team has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
- Right Right `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
-
- // A timestamp when this relation was created. You cannot change this value.
- Created time.Time `xorm:"created not null" json:"created"`
- // A timestamp when this relation was last updated. You cannot change this value.
- Updated time.Time `xorm:"updated not null" json:"updated"`
-
- web.CRUDable `xorm:"-" json:"-"`
- web.Rights `xorm:"-" json:"-"`
-}
-
-// TableName makes beautiful table names
-func (TeamNamespace) TableName() string {
- return "team_namespaces"
-}
-
-// Create creates a new team <-> namespace relation
-// @Summary Add a team to a namespace
-// @Description Gives a team access to a namespace.
-// @tags sharing
-// @Accept json
-// @Produce json
-// @Security JWTKeyAuth
-// @Param id path int true "Namespace ID"
-// @Param namespace body models.TeamNamespace true "The team you want to add to the namespace."
-// @Success 201 {object} models.TeamNamespace "The created team<->namespace relation."
-// @Failure 400 {object} web.HTTPError "Invalid team namespace object provided."
-// @Failure 404 {object} web.HTTPError "The team does not exist."
-// @Failure 403 {object} web.HTTPError "The team does not have access to the namespace"
-// @Failure 500 {object} models.Message "Internal error"
-// @Router /namespaces/{id}/teams [put]
-func (tn *TeamNamespace) Create(s *xorm.Session, a web.Auth) (err error) {
-
- // Check if the rights are valid
- if err = tn.Right.isValid(); err != nil {
- return
- }
-
- // Check if the team exists
- team, err := GetTeamByID(s, tn.TeamID)
- if err != nil {
- return err
- }
-
- // Check if the namespace exists
- namespace, err := GetNamespaceByID(s, tn.NamespaceID)
- if err != nil {
- return err
- }
-
- // Check if the team already has access to the namespace
- exists, err := s.
- Where("team_id = ?", tn.TeamID).
- And("namespace_id = ?", tn.NamespaceID).
- Get(&TeamNamespace{})
- if err != nil {
- return
- }
- if exists {
- return ErrTeamAlreadyHasAccess{tn.TeamID, tn.NamespaceID}
- }
-
- // Insert the new team
- _, err = s.Insert(tn)
- if err != nil {
- return err
- }
-
- return events.Dispatch(&NamespaceSharedWithTeamEvent{
- Namespace: namespace,
- Team: team,
- Doer: a,
- })
-}
-
-// Delete deletes a team <-> namespace relation based on the namespace & team id
-// @Summary Delete a team from a namespace
-// @Description Delets a team from a namespace. The team won't have access to the namespace anymore.
-// @tags sharing
-// @Produce json
-// @Security JWTKeyAuth
-// @Param namespaceID path int true "Namespace ID"
-// @Param teamID path int true "team ID"
-// @Success 200 {object} models.Message "The team was successfully deleted."
-// @Failure 403 {object} web.HTTPError "The team does not have access to the namespace"
-// @Failure 404 {object} web.HTTPError "team or namespace does not exist."
-// @Failure 500 {object} models.Message "Internal error"
-// @Router /namespaces/{namespaceID}/teams/{teamID} [delete]
-func (tn *TeamNamespace) Delete(s *xorm.Session, _ web.Auth) (err error) {
-
- // Check if the team exists
- _, err = GetTeamByID(s, tn.TeamID)
- if err != nil {
- return
- }
-
- // Check if the team has access to the namespace
- has, err := s.
- Where("team_id = ? AND namespace_id = ?", tn.TeamID, tn.NamespaceID).
- Get(&TeamNamespace{})
- if err != nil {
- return
- }
- if !has {
- return ErrTeamDoesNotHaveAccessToNamespace{TeamID: tn.TeamID, NamespaceID: tn.NamespaceID}
- }
-
- // Delete the relation
- _, err = s.
- Where("team_id = ?", tn.TeamID).
- And("namespace_id = ?", tn.NamespaceID).
- Delete(TeamNamespace{})
-
- return
-}
-
-// ReadAll implements the method to read all teams of a namespace
-// @Summary Get teams on a namespace
-// @Description Returns a namespace with all teams which have access on a given namespace.
-// @tags sharing
-// @Accept json
-// @Produce json
-// @Param id path int true "Namespace ID"
-// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
-// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
-// @Param s query string false "Search teams by its name."
-// @Security JWTKeyAuth
-// @Success 200 {array} models.TeamWithRight "The teams with the right they have."
-// @Failure 403 {object} web.HTTPError "No right to see the namespace."
-// @Failure 500 {object} models.Message "Internal error"
-// @Router /namespaces/{id}/teams [get]
-func (tn *TeamNamespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
- // Check if the user can read the namespace
- n := Namespace{ID: tn.NamespaceID}
- canRead, _, err := n.CanRead(s, a)
- if err != nil {
- return nil, 0, 0, err
- }
- if !canRead {
- return nil, 0, 0, ErrNeedToHaveNamespaceReadAccess{NamespaceID: tn.NamespaceID, UserID: a.GetID()}
- }
-
- // Get the teams
- all := []*TeamWithRight{}
- limit, start := getLimitFromPageIndex(page, perPage)
- query := s.
- Table("teams").
- Join("INNER", "team_namespaces", "team_id = teams.id").
- Where("team_namespaces.namespace_id = ?", tn.NamespaceID).
- Where(db.ILIKE("teams.name", search))
- if limit > 0 {
- query = query.Limit(limit, start)
- }
- err = query.Find(&all)
- if err != nil {
- return nil, 0, 0, err
- }
-
- teams := []*Team{}
- for _, t := range all {
- teams = append(teams, &t.Team)
- }
-
- err = addMoreInfoToTeams(s, teams)
- if err != nil {
- return
- }
-
- numberOfTotalItems, err = s.
- Table("teams").
- Join("INNER", "team_namespaces", "team_id = teams.id").
- Where("team_namespaces.namespace_id = ?", tn.NamespaceID).
- Where("teams.name LIKE ?", "%"+search+"%").
- Count(&TeamWithRight{})
-
- return all, len(all), numberOfTotalItems, err
-}
-
-// Update updates a team <-> namespace relation
-// @Summary Update a team <-> namespace relation
-// @Description Update a team <-> namespace relation. Mostly used to update the right that team has.
-// @tags sharing
-// @Accept json
-// @Produce json
-// @Param namespaceID path int true "Namespace ID"
-// @Param teamID path int true "Team ID"
-// @Param namespace body models.TeamNamespace true "The team you want to update."
-// @Security JWTKeyAuth
-// @Success 200 {object} models.TeamNamespace "The updated team <-> namespace relation."
-// @Failure 403 {object} web.HTTPError "The team does not have admin-access to the namespace"
-// @Failure 404 {object} web.HTTPError "Team or namespace does not exist."
-// @Failure 500 {object} models.Message "Internal error"
-// @Router /namespaces/{namespaceID}/teams/{teamID} [post]
-func (tn *TeamNamespace) Update(s *xorm.Session, _ web.Auth) (err error) {
-
- // Check if the right is valid
- if err := tn.Right.isValid(); err != nil {
- return err
- }
-
- _, err = s.
- Where("namespace_id = ? AND team_id = ?", tn.NamespaceID, tn.TeamID).
- Cols("right").
- Update(tn)
- return
-}
diff --git a/pkg/models/namespace_team_rights.go b/pkg/models/namespace_team_rights.go
deleted file mode 100644
index edde7125a..000000000
--- a/pkg/models/namespace_team_rights.go
+++ /dev/null
@@ -1,40 +0,0 @@
-// Vikunja is a to-do list application to facilitate your life.
-// Copyright 2018-2021 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"
-)
-
-// CanCreate checks if one can create a new team <-> namespace relation
-func (tn *TeamNamespace) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
- n := &Namespace{ID: tn.NamespaceID}
- return n.IsAdmin(s, a)
-}
-
-// CanDelete checks if a user can remove a team from a namespace. Only namespace admins can do that.
-func (tn *TeamNamespace) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
- n := &Namespace{ID: tn.NamespaceID}
- return n.IsAdmin(s, a)
-}
-
-// CanUpdate checks if a user can update a team from a Only namespace admins can do that.
-func (tn *TeamNamespace) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
- n := &Namespace{ID: tn.NamespaceID}
- return n.IsAdmin(s, a)
-}
diff --git a/pkg/models/namespace_team_rights_test.go b/pkg/models/namespace_team_rights_test.go
deleted file mode 100644
index e9dc37c67..000000000
--- a/pkg/models/namespace_team_rights_test.go
+++ /dev/null
@@ -1,107 +0,0 @@
-// Vikunja is a to-do list application to facilitate your life.
-// Copyright 2018-2021 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 (
- "testing"
- "time"
-
- "code.vikunja.io/api/pkg/db"
- "code.vikunja.io/api/pkg/user"
-
- "code.vikunja.io/web"
-)
-
-func TestTeamNamespace_CanDoSomething(t *testing.T) {
- type fields struct {
- ID int64
- TeamID int64
- NamespaceID int64
- Right Right
- Created time.Time
- Updated time.Time
- CRUDable web.CRUDable
- Rights web.Rights
- }
- type args struct {
- a web.Auth
- }
- tests := []struct {
- name string
- fields fields
- args args
- want map[string]bool
- }{
- {
- name: "CanDoSomething Normally",
- fields: fields{
- NamespaceID: 3,
- },
- args: args{
- a: &user.User{ID: 3},
- },
- want: map[string]bool{"CanCreate": true, "CanDelete": true, "CanUpdate": true},
- },
- {
- name: "CanDoSomething for a nonexistant namespace",
- fields: fields{
- NamespaceID: 300,
- },
- args: args{
- a: &user.User{ID: 3},
- },
- want: map[string]bool{"CanCreate": false, "CanDelete": false, "CanUpdate": false},
- },
- {
- name: "CanDoSomething where the user does not have the rights",
- fields: fields{
- NamespaceID: 3,
- },
- args: args{
- a: &user.User{ID: 4},
- },
- want: map[string]bool{"CanCreate": false, "CanDelete": false, "CanUpdate": false},
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
-
- tn := &TeamNamespace{
- ID: tt.fields.ID,
- TeamID: tt.fields.TeamID,
- NamespaceID: tt.fields.NamespaceID,
- Right: tt.fields.Right,
- Created: tt.fields.Created,
- Updated: tt.fields.Updated,
- CRUDable: tt.fields.CRUDable,
- Rights: tt.fields.Rights,
- }
- if got, _ := tn.CanCreate(s, tt.args.a); got != tt.want["CanCreate"] {
- t.Errorf("TeamNamespace.CanCreate() = %v, want %v", got, tt.want["CanCreate"])
- }
- if got, _ := tn.CanDelete(s, tt.args.a); got != tt.want["CanDelete"] {
- t.Errorf("TeamNamespace.CanDelete() = %v, want %v", got, tt.want["CanDelete"])
- }
- if got, _ := tn.CanUpdate(s, tt.args.a); got != tt.want["CanUpdate"] {
- t.Errorf("TeamNamespace.CanUpdate() = %v, want %v", got, tt.want["CanUpdate"])
- }
- _ = s.Close()
- })
- }
-}
diff --git a/pkg/models/namespace_team_test.go b/pkg/models/namespace_team_test.go
deleted file mode 100644
index 9417df459..000000000
--- a/pkg/models/namespace_team_test.go
+++ /dev/null
@@ -1,298 +0,0 @@
-// Vikunja is a to-do list application to facilitate your life.
-// Copyright 2018-2021 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 (
- "reflect"
- "runtime"
- "testing"
- "time"
-
- "code.vikunja.io/api/pkg/db"
- "code.vikunja.io/api/pkg/user"
- "code.vikunja.io/web"
- "github.com/stretchr/testify/assert"
-)
-
-func TestTeamNamespace_ReadAll(t *testing.T) {
- u := &user.User{ID: 1}
-
- t.Run("normal", func(t *testing.T) {
- tn := TeamNamespace{
- NamespaceID: 3,
- }
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- teams, _, _, err := tn.ReadAll(s, u, "", 1, 50)
- assert.NoError(t, err)
- assert.Equal(t, reflect.TypeOf(teams).Kind(), reflect.Slice)
- ts := reflect.ValueOf(teams)
- assert.Equal(t, ts.Len(), 2)
- _ = s.Close()
- })
- t.Run("nonexistant namespace", func(t *testing.T) {
- tn := TeamNamespace{
- NamespaceID: 9999,
- }
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- _, _, _, err := tn.ReadAll(s, u, "", 1, 50)
- assert.Error(t, err)
- assert.True(t, IsErrNamespaceDoesNotExist(err))
- _ = s.Close()
- })
- t.Run("no right for namespace", func(t *testing.T) {
- tn := TeamNamespace{
- NamespaceID: 17,
- }
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- _, _, _, err := tn.ReadAll(s, u, "", 1, 50)
- assert.Error(t, err)
- assert.True(t, IsErrNeedToHaveNamespaceReadAccess(err))
- _ = s.Close()
- })
- t.Run("search", func(t *testing.T) {
- tn := TeamNamespace{
- NamespaceID: 3,
- }
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- teams, _, _, err := tn.ReadAll(s, u, "READ_only_on_project6", 1, 50)
- assert.NoError(t, err)
- assert.Equal(t, reflect.TypeOf(teams).Kind(), reflect.Slice)
- ts := teams.([]*TeamWithRight)
- assert.Len(t, ts, 1)
- assert.Equal(t, int64(2), ts[0].ID)
-
- _ = s.Close()
- })
-}
-
-func TestTeamNamespace_Create(t *testing.T) {
- u := &user.User{ID: 1}
-
- t.Run("normal", func(t *testing.T) {
- tn := TeamNamespace{
- TeamID: 1,
- NamespaceID: 1,
- Right: RightAdmin,
- }
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- allowed, _ := tn.CanCreate(s, u)
- assert.True(t, allowed)
- err := tn.Create(s, u)
- assert.NoError(t, err)
-
- err = s.Commit()
- assert.NoError(t, err)
-
- db.AssertExists(t, "team_namespaces", map[string]interface{}{
- "team_id": 1,
- "namespace_id": 1,
- "right": RightAdmin,
- }, false)
- })
- t.Run("team already has access", func(t *testing.T) {
- tn := TeamNamespace{
- TeamID: 1,
- NamespaceID: 3,
- Right: RightRead,
- }
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- err := tn.Create(s, u)
- assert.Error(t, err)
- assert.True(t, IsErrTeamAlreadyHasAccess(err))
- _ = s.Close()
- })
- t.Run("invalid team right", func(t *testing.T) {
- tn := TeamNamespace{
- TeamID: 1,
- NamespaceID: 3,
- Right: RightUnknown,
- }
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- err := tn.Create(s, u)
- assert.Error(t, err)
- assert.True(t, IsErrInvalidRight(err))
- _ = s.Close()
- })
- t.Run("nonexistant team", func(t *testing.T) {
- tn := TeamNamespace{
- TeamID: 9999,
- NamespaceID: 1,
- }
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- err := tn.Create(s, u)
- assert.Error(t, err)
- assert.True(t, IsErrTeamDoesNotExist(err))
- _ = s.Close()
- })
- t.Run("nonexistant namespace", func(t *testing.T) {
- tn := TeamNamespace{
- TeamID: 1,
- NamespaceID: 9999,
- }
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- err := tn.Create(s, u)
- assert.Error(t, err)
- assert.True(t, IsErrNamespaceDoesNotExist(err))
- _ = s.Close()
- })
-}
-
-func TestTeamNamespace_Delete(t *testing.T) {
- u := &user.User{ID: 1}
-
- t.Run("normal", func(t *testing.T) {
- tn := TeamNamespace{
- TeamID: 7,
- NamespaceID: 9,
- }
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- allowed, _ := tn.CanDelete(s, u)
- assert.True(t, allowed)
- err := tn.Delete(s, u)
- assert.NoError(t, err)
- err = s.Commit()
- assert.NoError(t, err)
-
- db.AssertMissing(t, "team_namespaces", map[string]interface{}{
- "team_id": 7,
- "namespace_id": 9,
- })
- })
- t.Run("nonexistant team", func(t *testing.T) {
- tn := TeamNamespace{
- TeamID: 9999,
- NamespaceID: 3,
- }
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- err := tn.Delete(s, u)
- assert.Error(t, err)
- assert.True(t, IsErrTeamDoesNotExist(err))
- _ = s.Close()
- })
- t.Run("nonexistant namespace", func(t *testing.T) {
- tn := TeamNamespace{
- TeamID: 1,
- NamespaceID: 9999,
- }
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- err := tn.Delete(s, u)
- assert.Error(t, err)
- assert.True(t, IsErrTeamDoesNotHaveAccessToNamespace(err))
- _ = s.Close()
- })
-}
-
-func TestTeamNamespace_Update(t *testing.T) {
- type fields struct {
- ID int64
- TeamID int64
- NamespaceID int64
- Right Right
- Created time.Time
- Updated time.Time
- CRUDable web.CRUDable
- Rights web.Rights
- }
- tests := []struct {
- name string
- fields fields
- wantErr bool
- errType func(err error) bool
- }{
- {
- name: "Test Update Normally",
- fields: fields{
- NamespaceID: 3,
- TeamID: 1,
- Right: RightAdmin,
- },
- },
- {
- name: "Test Update to write",
- fields: fields{
- NamespaceID: 3,
- TeamID: 1,
- Right: RightWrite,
- },
- },
- {
- name: "Test Update to Read",
- fields: fields{
- NamespaceID: 3,
- TeamID: 1,
- Right: RightRead,
- },
- },
- {
- name: "Test Update with invalid right",
- fields: fields{
- NamespaceID: 3,
- TeamID: 1,
- Right: 500,
- },
- wantErr: true,
- errType: IsErrInvalidRight,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
-
- tl := &TeamNamespace{
- ID: tt.fields.ID,
- TeamID: tt.fields.TeamID,
- NamespaceID: tt.fields.NamespaceID,
- Right: tt.fields.Right,
- Created: tt.fields.Created,
- Updated: tt.fields.Updated,
- CRUDable: tt.fields.CRUDable,
- Rights: tt.fields.Rights,
- }
- err := tl.Update(s, &user.User{ID: 1})
- if (err != nil) != tt.wantErr {
- t.Errorf("TeamNamespace.Update() error = %v, wantErr %v", err, tt.wantErr)
- }
- if (err != nil) && tt.wantErr && !tt.errType(err) {
- t.Errorf("TeamNamespace.Update() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
- }
-
- err = s.Commit()
- assert.NoError(t, err)
-
- if !tt.wantErr {
- db.AssertExists(t, "team_namespaces", map[string]interface{}{
- "team_id": tt.fields.TeamID,
- "namespace_id": tt.fields.NamespaceID,
- "right": tt.fields.Right,
- }, false)
- }
- })
- }
-}
diff --git a/pkg/models/namespace_test.go b/pkg/models/namespace_test.go
deleted file mode 100644
index 3b6e54f51..000000000
--- a/pkg/models/namespace_test.go
+++ /dev/null
@@ -1,372 +0,0 @@
-// Vikunja is a to-do list application to facilitate your life.
-// Copyright 2018-2021 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 (
- "testing"
-
- "code.vikunja.io/api/pkg/db"
- "code.vikunja.io/api/pkg/user"
- "github.com/stretchr/testify/assert"
-)
-
-func TestNamespace_Create(t *testing.T) {
-
- // Dummy namespace
- dummynamespace := Namespace{
- Title: "Test",
- Description: "Lorem Ipsum",
- }
-
- user1 := &user.User{ID: 1}
-
- t.Run("normal", func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- err := dummynamespace.Create(s, user1)
- assert.NoError(t, err)
- err = s.Commit()
- assert.NoError(t, err)
-
- db.AssertExists(t, "namespaces", map[string]interface{}{
- "title": "Test",
- "description": "Lorem Ipsum",
- }, false)
- })
- t.Run("no title", func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- n2 := Namespace{}
- err := n2.Create(s, user1)
- assert.Error(t, err)
- assert.True(t, IsErrNamespaceNameCannotBeEmpty(err))
- _ = s.Close()
- })
- t.Run("nonexistant user", func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- nUser := &user.User{ID: 9482385}
- dnsp2 := dummynamespace
- err := dnsp2.Create(s, nUser)
- assert.Error(t, err)
- assert.True(t, user.IsErrUserDoesNotExist(err))
- _ = s.Close()
- })
-}
-
-func TestNamespace_ReadOne(t *testing.T) {
- u := &user.User{ID: 1}
-
- t.Run("normal", func(t *testing.T) {
- n := &Namespace{ID: 1}
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- defer s.Close()
-
- err := n.ReadOne(s, u)
- assert.NoError(t, err)
- assert.Equal(t, n.Title, "testnamespace")
- })
- t.Run("nonexistant", func(t *testing.T) {
- n := &Namespace{ID: 99999}
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- defer s.Close()
-
- err := n.ReadOne(s, u)
- assert.Error(t, err)
- assert.True(t, IsErrNamespaceDoesNotExist(err))
- })
- t.Run("with subscription", func(t *testing.T) {
- n := &Namespace{ID: 8}
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- defer s.Close()
-
- err := n.ReadOne(s, &user.User{ID: 6})
- assert.NoError(t, err)
- assert.NotNil(t, n.Subscription)
- })
-}
-
-func TestNamespace_Update(t *testing.T) {
- u := &user.User{ID: 1}
-
- t.Run("normal", func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- n := &Namespace{
- ID: 1,
- Title: "Lorem Ipsum",
- }
- err := n.Update(s, u)
- assert.NoError(t, err)
- err = s.Commit()
- assert.NoError(t, err)
-
- db.AssertExists(t, "namespaces", map[string]interface{}{
- "id": 1,
- "title": "Lorem Ipsum",
- }, false)
- })
- t.Run("nonexisting", func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- n := &Namespace{
- ID: 99999,
- Title: "Lorem Ipsum",
- }
- err := n.Update(s, u)
- assert.Error(t, err)
- assert.True(t, IsErrNamespaceDoesNotExist(err))
- _ = s.Close()
- })
- t.Run("nonexisting owner", func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- n := &Namespace{
- ID: 1,
- Title: "Lorem Ipsum",
- Owner: &user.User{ID: 99999},
- }
- err := n.Update(s, u)
- assert.Error(t, err)
- assert.True(t, user.IsErrUserDoesNotExist(err))
- _ = s.Close()
- })
- t.Run("no title", func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- n := &Namespace{
- ID: 1,
- }
- err := n.Update(s, u)
- assert.Error(t, err)
- assert.True(t, IsErrNamespaceNameCannotBeEmpty(err))
- _ = s.Close()
- })
-}
-
-func TestNamespace_Delete(t *testing.T) {
- u := &user.User{ID: 1}
-
- t.Run("normal", func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- n := &Namespace{
- ID: 1,
- }
- err := n.Delete(s, u)
- assert.NoError(t, err)
- err = s.Commit()
- assert.NoError(t, err)
-
- db.AssertMissing(t, "namespaces", map[string]interface{}{
- "id": 1,
- })
- })
- t.Run("nonexisting", func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- n := &Namespace{
- ID: 9999,
- }
- err := n.Delete(s, u)
- assert.Error(t, err)
- assert.True(t, IsErrNamespaceDoesNotExist(err))
- _ = s.Close()
- })
-}
-
-func TestNamespace_ReadAll(t *testing.T) {
- user1 := &user.User{ID: 1}
- user6 := &user.User{ID: 6}
- user7 := &user.User{ID: 7}
- user11 := &user.User{ID: 11}
- user12 := &user.User{ID: 12}
-
- t.Run("normal", func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- defer s.Close()
-
- n := &Namespace{}
- nn, _, _, err := n.ReadAll(s, user1, "", 1, -1)
- assert.NoError(t, err)
- namespaces := nn.([]*NamespaceWithProjects)
- assert.NotNil(t, namespaces)
- assert.Len(t, namespaces, 11) // Total of 11 including shared, favorites and saved filters
- assert.Equal(t, int64(-3), namespaces[0].ID) // The first one should be the one with saved filters
- assert.Equal(t, int64(-2), namespaces[1].ID) // The second one should be the one with favorites
- assert.Equal(t, int64(-1), namespaces[2].ID) // The third one should be the one with the shared namespaces
- // Ensure every project and namespace are not archived
- for _, namespace := range namespaces {
- assert.False(t, namespace.IsArchived)
- for _, project := range namespace.Projects {
- assert.False(t, project.IsArchived)
- }
- }
- })
- t.Run("no own shared projects", func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- defer s.Close()
-
- n := &Namespace{}
- nn, _, _, err := n.ReadAll(s, user6, "", 1, -1)
- assert.NoError(t, err)
- namespaces := nn.([]*NamespaceWithProjects)
- assert.NotNil(t, namespaces)
- assert.Equal(t, int64(-1), namespaces[1].ID) // The third one should be the one with the shared namespaces
-
- sharedProjectOccurences := make(map[int64]int64)
- for _, project := range namespaces[1].Projects {
- assert.NotEqual(t, user1.ID, project.OwnerID)
- sharedProjectOccurences[project.ID]++
- }
-
- for projectID, occ := range sharedProjectOccurences {
- assert.Equal(t, int64(1), occ, "shared project %d is present %d times, should be 1", projectID, occ)
- }
- })
- t.Run("namespaces only", func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- defer s.Close()
-
- n := &Namespace{
- NamespacesOnly: true,
- }
- nn, _, _, err := n.ReadAll(s, user1, "", 1, -1)
- assert.NoError(t, err)
- namespaces := nn.([]*NamespaceWithProjects)
- assert.NotNil(t, namespaces)
- assert.Len(t, namespaces, 8) // Total of 8 - excluding shared, favorites and saved filters (normally 11)
- // Ensure every namespace does not contain projects
- for _, namespace := range namespaces {
- assert.Nil(t, namespace.Projects)
- }
- })
- t.Run("ids only", func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- defer s.Close()
-
- n := &Namespace{
- NamespacesOnly: true,
- }
- nn, _, _, err := n.ReadAll(s, user7, "13,14", 1, -1)
- assert.NoError(t, err)
- namespaces := nn.([]*NamespaceWithProjects)
- assert.NotNil(t, namespaces)
- assert.Len(t, namespaces, 2)
- assert.Equal(t, int64(13), namespaces[0].ID)
- assert.Equal(t, int64(14), namespaces[1].ID)
- })
- t.Run("ids only but ids with other people's namespace", func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- defer s.Close()
-
- n := &Namespace{
- NamespacesOnly: true,
- }
- nn, _, _, err := n.ReadAll(s, user1, "1,w", 1, -1)
- assert.NoError(t, err)
- namespaces := nn.([]*NamespaceWithProjects)
- assert.NotNil(t, namespaces)
- assert.Len(t, namespaces, 1)
- assert.Equal(t, int64(1), namespaces[0].ID)
- })
- t.Run("archived", func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- defer s.Close()
-
- n := &Namespace{
- IsArchived: true,
- }
- nn, _, _, err := n.ReadAll(s, user1, "", 1, -1)
- namespaces := nn.([]*NamespaceWithProjects)
- assert.NoError(t, err)
- assert.NotNil(t, namespaces)
- assert.Len(t, namespaces, 12) // Total of 12 including shared & favorites, one is archived
- assert.Equal(t, int64(-3), namespaces[0].ID) // The first one should be the one with shared filters
- assert.Equal(t, int64(-2), namespaces[1].ID) // The second one should be the one with favorites
- assert.Equal(t, int64(-1), namespaces[2].ID) // The third one should be the one with the shared namespaces
- })
- t.Run("no favorites", func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- defer s.Close()
-
- n := &Namespace{}
- nn, _, _, err := n.ReadAll(s, user11, "", 1, -1)
- namespaces := nn.([]*NamespaceWithProjects)
- assert.NoError(t, err)
- // Assert the first namespace is not the favorites namespace
- assert.NotEqual(t, FavoritesPseudoNamespace.ID, namespaces[0].ID)
- })
- t.Run("no favorite tasks but namespace", func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- defer s.Close()
-
- n := &Namespace{}
- nn, _, _, err := n.ReadAll(s, user12, "", 1, -1)
- namespaces := nn.([]*NamespaceWithProjects)
- assert.NoError(t, err)
- // Assert the first namespace is the favorites namespace and contains projects
- assert.Equal(t, FavoritesPseudoNamespace.ID, namespaces[0].ID)
- assert.NotEqual(t, 0, namespaces[0].Projects)
- })
- t.Run("no saved filters", func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- defer s.Close()
-
- n := &Namespace{}
- nn, _, _, err := n.ReadAll(s, user11, "", 1, -1)
- namespaces := nn.([]*NamespaceWithProjects)
- assert.NoError(t, err)
- // Assert the first namespace is not the favorites namespace
- assert.NotEqual(t, SavedFiltersPseudoNamespace.ID, namespaces[0].ID)
- })
- t.Run("no results", func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- defer s.Close()
-
- n := &Namespace{}
- nn, _, _, err := n.ReadAll(s, user1, "some search string which will never return results", 1, -1)
- assert.NoError(t, err)
- assert.Nil(t, nn)
- })
- t.Run("search", func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- defer s.Close()
-
- n := &Namespace{}
- nn, _, _, err := n.ReadAll(s, user6, "NamespACE7", 1, -1)
- assert.NoError(t, err)
- namespaces := nn.([]*NamespaceWithProjects)
- assert.NotNil(t, namespaces)
- assert.Len(t, namespaces, 2)
- assert.Equal(t, int64(7), namespaces[1].ID)
- })
-}
diff --git a/pkg/models/namespace_users.go b/pkg/models/namespace_users.go
deleted file mode 100644
index 716df0181..000000000
--- a/pkg/models/namespace_users.go
+++ /dev/null
@@ -1,251 +0,0 @@
-// Vikunja is a to-do list application to facilitate your life.
-// Copyright 2018-2021 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 (
- "time"
-
- "code.vikunja.io/api/pkg/db"
-
- "code.vikunja.io/api/pkg/events"
- user2 "code.vikunja.io/api/pkg/user"
- "code.vikunja.io/web"
-
- "xorm.io/xorm"
-)
-
-// NamespaceUser represents a namespace <-> user relation
-type NamespaceUser struct {
- // The unique, numeric id of this namespace <-> user relation.
- ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"namespace"`
- // The username.
- Username string `xorm:"-" json:"user_id" param:"user"`
- UserID int64 `xorm:"bigint not null INDEX" json:"-"`
- // The namespace id
- NamespaceID int64 `xorm:"bigint not null INDEX" json:"-" param:"namespace"`
- // The right this user has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
- Right Right `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
-
- // A timestamp when this relation was created. You cannot change this value.
- Created time.Time `xorm:"created not null" json:"created"`
- // A timestamp when this relation was last updated. You cannot change this value.
- Updated time.Time `xorm:"updated not null" json:"updated"`
-
- web.CRUDable `xorm:"-" json:"-"`
- web.Rights `xorm:"-" json:"-"`
-}
-
-// TableName is the table name for NamespaceUser
-func (NamespaceUser) TableName() string {
- return "users_namespaces"
-}
-
-// Create creates a new namespace <-> user relation
-// @Summary Add a user to a namespace
-// @Description Gives a user access to a namespace.
-// @tags sharing
-// @Accept json
-// @Produce json
-// @Security JWTKeyAuth
-// @Param id path int true "Namespace ID"
-// @Param namespace body models.NamespaceUser true "The user you want to add to the namespace."
-// @Success 201 {object} models.NamespaceUser "The created user<->namespace relation."
-// @Failure 400 {object} web.HTTPError "Invalid user namespace object provided."
-// @Failure 404 {object} web.HTTPError "The user does not exist."
-// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
-// @Failure 500 {object} models.Message "Internal error"
-// @Router /namespaces/{id}/users [put]
-func (nu *NamespaceUser) Create(s *xorm.Session, a web.Auth) (err error) {
- // Reset the id
- nu.ID = 0
-
- // Check if the right is valid
- if err := nu.Right.isValid(); err != nil {
- return err
- }
-
- // Check if the namespace exists
- n, err := GetNamespaceByID(s, nu.NamespaceID)
- if err != nil {
- return
- }
-
- // Check if the user exists
- user, err := user2.GetUserByUsername(s, nu.Username)
- if err != nil {
- return err
- }
- nu.UserID = user.ID
-
- // Check if the user already has access or is owner of that namespace
- // We explicitly DO NOT check for teams here
- if n.OwnerID == nu.UserID {
- return ErrUserAlreadyHasNamespaceAccess{UserID: nu.UserID, NamespaceID: nu.NamespaceID}
- }
-
- exist, err := s.
- Where("namespace_id = ? AND user_id = ?", nu.NamespaceID, nu.UserID).
- Get(&NamespaceUser{})
- if err != nil {
- return
- }
- if exist {
- return ErrUserAlreadyHasNamespaceAccess{UserID: nu.UserID, NamespaceID: nu.NamespaceID}
- }
-
- // Insert user <-> namespace relation
- _, err = s.Insert(nu)
- if err != nil {
- return err
- }
-
- return events.Dispatch(&NamespaceSharedWithUserEvent{
- Namespace: n,
- User: user,
- Doer: a,
- })
-}
-
-// Delete deletes a namespace <-> user relation
-// @Summary Delete a user from a namespace
-// @Description Delets a user from a namespace. The user won't have access to the namespace anymore.
-// @tags sharing
-// @Produce json
-// @Security JWTKeyAuth
-// @Param namespaceID path int true "Namespace ID"
-// @Param userID path int true "user ID"
-// @Success 200 {object} models.Message "The user was successfully deleted."
-// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
-// @Failure 404 {object} web.HTTPError "user or namespace does not exist."
-// @Failure 500 {object} models.Message "Internal error"
-// @Router /namespaces/{namespaceID}/users/{userID} [delete]
-func (nu *NamespaceUser) Delete(s *xorm.Session, _ web.Auth) (err error) {
-
- // Check if the user exists
- user, err := user2.GetUserByUsername(s, nu.Username)
- if err != nil {
- return
- }
- nu.UserID = user.ID
-
- // Check if the user has access to the namespace
- has, err := s.
- Where("user_id = ? AND namespace_id = ?", nu.UserID, nu.NamespaceID).
- Get(&NamespaceUser{})
- if err != nil {
- return
- }
- if !has {
- return ErrUserDoesNotHaveAccessToNamespace{NamespaceID: nu.NamespaceID, UserID: nu.UserID}
- }
-
- _, err = s.
- Where("user_id = ? AND namespace_id = ?", nu.UserID, nu.NamespaceID).
- Delete(&NamespaceUser{})
- return
-}
-
-// ReadAll gets all users who have access to a namespace
-// @Summary Get users on a namespace
-// @Description Returns a namespace with all users which have access on a given namespace.
-// @tags sharing
-// @Accept json
-// @Produce json
-// @Param id path int true "Namespace ID"
-// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
-// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
-// @Param s query string false "Search users by its name."
-// @Security JWTKeyAuth
-// @Success 200 {array} models.UserWithRight "The users with the right they have."
-// @Failure 403 {object} web.HTTPError "No right to see the namespace."
-// @Failure 500 {object} models.Message "Internal error"
-// @Router /namespaces/{id}/users [get]
-func (nu *NamespaceUser) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
- // Check if the user has access to the namespace
- l := Namespace{ID: nu.NamespaceID}
- canRead, _, err := l.CanRead(s, a)
- if err != nil {
- return nil, 0, 0, err
- }
- if !canRead {
- return nil, 0, 0, ErrNeedToHaveNamespaceReadAccess{}
- }
-
- // Get all users
- all := []*UserWithRight{}
- limit, start := getLimitFromPageIndex(page, perPage)
- query := s.
- Join("INNER", "users_namespaces", "user_id = users.id").
- Where("users_namespaces.namespace_id = ?", nu.NamespaceID).
- Where(db.ILIKE("users.username", search))
- if limit > 0 {
- query = query.Limit(limit, start)
- }
- err = query.Find(&all)
- if err != nil {
- return nil, 0, 0, err
- }
-
- // Obfuscate all user emails
- for _, u := range all {
- u.Email = ""
- }
-
- numberOfTotalItems, err = s.
- Join("INNER", "users_namespaces", "user_id = users.id").
- Where("users_namespaces.namespace_id = ?", nu.NamespaceID).
- Where("users.username LIKE ?", "%"+search+"%").
- Count(&UserWithRight{})
-
- return all, len(all), numberOfTotalItems, err
-}
-
-// Update updates a user <-> namespace relation
-// @Summary Update a user <-> namespace relation
-// @Description Update a user <-> namespace relation. Mostly used to update the right that user has.
-// @tags sharing
-// @Accept json
-// @Produce json
-// @Param namespaceID path int true "Namespace ID"
-// @Param userID path int true "User ID"
-// @Param namespace body models.NamespaceUser true "The user you want to update."
-// @Security JWTKeyAuth
-// @Success 200 {object} models.NamespaceUser "The updated user <-> namespace relation."
-// @Failure 403 {object} web.HTTPError "The user does not have admin-access to the namespace"
-// @Failure 404 {object} web.HTTPError "User or namespace does not exist."
-// @Failure 500 {object} models.Message "Internal error"
-// @Router /namespaces/{namespaceID}/users/{userID} [post]
-func (nu *NamespaceUser) Update(s *xorm.Session, _ web.Auth) (err error) {
-
- // Check if the right is valid
- if err := nu.Right.isValid(); err != nil {
- return err
- }
-
- // Check if the user exists
- user, err := user2.GetUserByUsername(s, nu.Username)
- if err != nil {
- return err
- }
- nu.UserID = user.ID
-
- _, err = s.
- Where("namespace_id = ? AND user_id = ?", nu.NamespaceID, nu.UserID).
- Cols("right").
- Update(nu)
- return
-}
diff --git a/pkg/models/namespace_users_rights.go b/pkg/models/namespace_users_rights.go
deleted file mode 100644
index 00f7e77ca..000000000
--- a/pkg/models/namespace_users_rights.go
+++ /dev/null
@@ -1,42 +0,0 @@
-// Vikunja is a to-do list application to facilitate your life.
-// Copyright 2018-2021 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"
-)
-
-// CanCreate checks if the user can create a new user <-> namespace relation
-func (nu *NamespaceUser) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
- return nu.canDoNamespaceUser(s, a)
-}
-
-// CanDelete checks if the user can delete a user <-> namespace relation
-func (nu *NamespaceUser) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
- return nu.canDoNamespaceUser(s, a)
-}
-
-// CanUpdate checks if the user can update a user <-> namespace relation
-func (nu *NamespaceUser) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
- return nu.canDoNamespaceUser(s, a)
-}
-
-func (nu *NamespaceUser) canDoNamespaceUser(s *xorm.Session, a web.Auth) (bool, error) {
- n := &Namespace{ID: nu.NamespaceID}
- return n.IsAdmin(s, a)
-}
diff --git a/pkg/models/namespace_users_rights_test.go b/pkg/models/namespace_users_rights_test.go
deleted file mode 100644
index f8a8f7304..000000000
--- a/pkg/models/namespace_users_rights_test.go
+++ /dev/null
@@ -1,107 +0,0 @@
-// Vikunja is a to-do list application to facilitate your life.
-// Copyright 2018-2021 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 (
- "testing"
- "time"
-
- "code.vikunja.io/api/pkg/db"
- "code.vikunja.io/api/pkg/user"
-
- "code.vikunja.io/web"
-)
-
-func TestNamespaceUser_CanDoSomething(t *testing.T) {
- type fields struct {
- ID int64
- UserID int64
- NamespaceID int64
- Right Right
- Created time.Time
- Updated time.Time
- CRUDable web.CRUDable
- Rights web.Rights
- }
- type args struct {
- a web.Auth
- }
- tests := []struct {
- name string
- fields fields
- args args
- want map[string]bool
- }{
- {
- name: "CanDoSomething Normally",
- fields: fields{
- NamespaceID: 3,
- },
- args: args{
- a: &user.User{ID: 3},
- },
- want: map[string]bool{"CanCreate": true, "CanDelete": true, "CanUpdate": true},
- },
- {
- name: "CanDoSomething for a nonexistant namespace",
- fields: fields{
- NamespaceID: 300,
- },
- args: args{
- a: &user.User{ID: 3},
- },
- want: map[string]bool{"CanCreate": false, "CanDelete": false, "CanUpdate": false},
- },
- {
- name: "CanDoSomething where the user does not have the rights",
- fields: fields{
- NamespaceID: 3,
- },
- args: args{
- a: &user.User{ID: 4},
- },
- want: map[string]bool{"CanCreate": false, "CanDelete": false, "CanUpdate": false},
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- defer s.Close()
-
- nu := &NamespaceUser{
- ID: tt.fields.ID,
- UserID: tt.fields.UserID,
- NamespaceID: tt.fields.NamespaceID,
- Right: tt.fields.Right,
- Created: tt.fields.Created,
- Updated: tt.fields.Updated,
- CRUDable: tt.fields.CRUDable,
- Rights: tt.fields.Rights,
- }
- if got, _ := nu.CanCreate(s, tt.args.a); got != tt.want["CanCreate"] {
- t.Errorf("NamespaceUser.CanCreate() = %v, want %v", got, tt.want["CanCreate"])
- }
- if got, _ := nu.CanDelete(s, tt.args.a); got != tt.want["CanDelete"] {
- t.Errorf("NamespaceUser.CanDelete() = %v, want %v", got, tt.want["CanDelete"])
- }
- if got, _ := nu.CanUpdate(s, tt.args.a); got != tt.want["CanUpdate"] {
- t.Errorf("NamespaceUser.CanUpdate() = %v, want %v", got, tt.want["CanUpdate"])
- }
- })
- }
-}
diff --git a/pkg/models/namespace_users_test.go b/pkg/models/namespace_users_test.go
deleted file mode 100644
index 634e796b8..000000000
--- a/pkg/models/namespace_users_test.go
+++ /dev/null
@@ -1,436 +0,0 @@
-// Vikunja is a to-do list application to facilitate your life.
-// Copyright 2018-2021 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 (
- "reflect"
- "runtime"
- "testing"
- "time"
-
- "code.vikunja.io/api/pkg/db"
- "code.vikunja.io/api/pkg/user"
- "code.vikunja.io/web"
- "github.com/stretchr/testify/assert"
- "gopkg.in/d4l3k/messagediff.v1"
-)
-
-func TestNamespaceUser_Create(t *testing.T) {
- type fields struct {
- ID int64
- Username string
- UserID int64
- NamespaceID int64
- Right Right
- Created time.Time
- Updated time.Time
- CRUDable web.CRUDable
- Rights web.Rights
- }
- type args struct {
- a web.Auth
- }
- tests := []struct {
- name string
- fields fields
- args args
- wantErr bool
- errType func(err error) bool
- }{
- {
- name: "NamespaceUsers Create normally",
- fields: fields{
- Username: "user1",
- UserID: 1,
- NamespaceID: 2,
- },
- },
- {
- name: "NamespaceUsers Create for duplicate",
- fields: fields{
- Username: "user1",
- NamespaceID: 3,
- },
- wantErr: true,
- errType: IsErrUserAlreadyHasNamespaceAccess,
- },
- {
- name: "NamespaceUsers Create with invalid right",
- fields: fields{
- Username: "user1",
- NamespaceID: 2,
- Right: 500,
- },
- wantErr: true,
- errType: IsErrInvalidRight,
- },
- {
- name: "NamespaceUsers Create with inexisting project",
- fields: fields{
- Username: "user1",
- NamespaceID: 2000,
- },
- wantErr: true,
- errType: IsErrNamespaceDoesNotExist,
- },
- {
- name: "NamespaceUsers Create with inexisting user",
- fields: fields{
- Username: "user500",
- NamespaceID: 2,
- },
- wantErr: true,
- errType: user.IsErrUserDoesNotExist,
- },
- {
- name: "NamespaceUsers Create with the owner as shared user",
- fields: fields{
- Username: "user1",
- NamespaceID: 1,
- },
- wantErr: true,
- errType: IsErrUserAlreadyHasNamespaceAccess,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
-
- un := &NamespaceUser{
- ID: tt.fields.ID,
- Username: tt.fields.Username,
- NamespaceID: tt.fields.NamespaceID,
- Right: tt.fields.Right,
- Created: tt.fields.Created,
- Updated: tt.fields.Updated,
- CRUDable: tt.fields.CRUDable,
- Rights: tt.fields.Rights,
- }
- err := un.Create(s, tt.args.a)
- if (err != nil) != tt.wantErr {
- t.Errorf("NamespaceUser.Create() error = %v, wantErr %v", err, tt.wantErr)
- }
- if (err != nil) && tt.wantErr && !tt.errType(err) {
- t.Errorf("NamespaceUser.Create() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
- }
- err = s.Commit()
- assert.NoError(t, err)
-
- if !tt.wantErr {
- db.AssertExists(t, "users_namespaces", map[string]interface{}{
- "user_id": tt.fields.UserID,
- "namespace_id": tt.fields.NamespaceID,
- }, false)
- }
- })
- }
-}
-
-func TestNamespaceUser_ReadAll(t *testing.T) {
- user1 := &UserWithRight{
- User: user.User{
- ID: 1,
- Username: "user1",
- Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
- Issuer: "local",
- EmailRemindersEnabled: true,
- OverdueTasksRemindersEnabled: true,
- OverdueTasksRemindersTime: "09:00",
- Created: testCreatedTime,
- Updated: testUpdatedTime,
- },
- Right: RightRead,
- }
- user2 := &UserWithRight{
- User: user.User{
- ID: 2,
- Username: "user2",
- Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
- Issuer: "local",
- EmailRemindersEnabled: true,
- OverdueTasksRemindersEnabled: true,
- OverdueTasksRemindersTime: "09:00",
- Created: testCreatedTime,
- Updated: testUpdatedTime,
- },
- Right: RightRead,
- }
-
- type fields struct {
- ID int64
- UserID int64
- NamespaceID int64
- Right Right
- Created time.Time
- Updated time.Time
- CRUDable web.CRUDable
- Rights web.Rights
- }
- type args struct {
- search string
- a web.Auth
- page int
- }
- tests := []struct {
- name string
- fields fields
- args args
- want interface{}
- wantErr bool
- errType func(err error) bool
- }{
- {
- name: "Test readall normal",
- fields: fields{
- NamespaceID: 3,
- },
- args: args{
- a: &user.User{ID: 3},
- },
- want: []*UserWithRight{
- user1,
- user2,
- },
- },
- {
- name: "Test ReadAll by a user who does not have access to the project",
- fields: fields{
- NamespaceID: 3,
- },
- args: args{
- a: &user.User{ID: 4},
- },
- wantErr: true,
- errType: IsErrNeedToHaveNamespaceReadAccess,
- },
- {
- name: "Search",
- fields: fields{
- NamespaceID: 3,
- },
- args: args{
- a: &user.User{ID: 3},
- search: "usER2",
- },
- want: []*UserWithRight{
- user2,
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
- defer s.Close()
-
- un := &NamespaceUser{
- ID: tt.fields.ID,
- UserID: tt.fields.UserID,
- NamespaceID: tt.fields.NamespaceID,
- Right: tt.fields.Right,
- Created: tt.fields.Created,
- Updated: tt.fields.Updated,
- CRUDable: tt.fields.CRUDable,
- Rights: tt.fields.Rights,
- }
- got, _, _, err := un.ReadAll(s, tt.args.a, tt.args.search, tt.args.page, 50)
- if (err != nil) != tt.wantErr {
- t.Errorf("NamespaceUser.ReadAll() error = %v, wantErr %v", err, tt.wantErr)
- return
- }
- if (err != nil) && tt.wantErr && !tt.errType(err) {
- t.Errorf("NamespaceUser.ReadAll() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
- }
- if diff, equal := messagediff.PrettyDiff(got, tt.want); !equal {
- t.Errorf("NamespaceUser.ReadAll() = %v, want %v, diff: %v", got, tt.want, diff)
- }
- })
- }
-}
-
-func TestNamespaceUser_Update(t *testing.T) {
- type fields struct {
- ID int64
- Username string
- UserID int64
- NamespaceID int64
- Right Right
- Created time.Time
- Updated time.Time
- CRUDable web.CRUDable
- Rights web.Rights
- }
- tests := []struct {
- name string
- fields fields
- wantErr bool
- errType func(err error) bool
- }{
- {
- name: "Test Update Normally",
- fields: fields{
- NamespaceID: 3,
- Username: "user1",
- UserID: 1,
- Right: RightAdmin,
- },
- },
- {
- name: "Test Update to write",
- fields: fields{
- NamespaceID: 3,
- Username: "user1",
- UserID: 1,
- Right: RightWrite,
- },
- },
- {
- name: "Test Update to Read",
- fields: fields{
- NamespaceID: 3,
- Username: "user1",
- UserID: 1,
- Right: RightRead,
- },
- },
- {
- name: "Test Update with invalid right",
- fields: fields{
- NamespaceID: 3,
- Username: "user1",
- Right: 500,
- },
- wantErr: true,
- errType: IsErrInvalidRight,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
-
- nu := &NamespaceUser{
- ID: tt.fields.ID,
- Username: tt.fields.Username,
- NamespaceID: tt.fields.NamespaceID,
- Right: tt.fields.Right,
- Created: tt.fields.Created,
- Updated: tt.fields.Updated,
- CRUDable: tt.fields.CRUDable,
- Rights: tt.fields.Rights,
- }
- err := nu.Update(s, &user.User{ID: 1})
- if (err != nil) != tt.wantErr {
- t.Errorf("NamespaceUser.Update() error = %v, wantErr %v", err, tt.wantErr)
- }
- if (err != nil) && tt.wantErr && !tt.errType(err) {
- t.Errorf("NamespaceUser.Update() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
- }
- err = s.Commit()
- assert.NoError(t, err)
-
- if !tt.wantErr {
- db.AssertExists(t, "users_namespaces", map[string]interface{}{
- "user_id": tt.fields.UserID,
- "namespace_id": tt.fields.NamespaceID,
- "right": tt.fields.Right,
- }, false)
- }
- })
- }
-}
-
-func TestNamespaceUser_Delete(t *testing.T) {
- type fields struct {
- ID int64
- Username string
- UserID int64
- NamespaceID int64
- Right Right
- Created time.Time
- Updated time.Time
- CRUDable web.CRUDable
- Rights web.Rights
- }
- tests := []struct {
- name string
- fields fields
- wantErr bool
- errType func(err error) bool
- }{
- {
- name: "Try deleting some unexistant user",
- fields: fields{
- Username: "user1000",
- NamespaceID: 2,
- },
- wantErr: true,
- errType: user.IsErrUserDoesNotExist,
- },
- {
- name: "Try deleting a user which does not has access but exists",
- fields: fields{
- Username: "user1",
- NamespaceID: 4,
- },
- wantErr: true,
- errType: IsErrUserDoesNotHaveAccessToNamespace,
- },
- {
- name: "Try deleting normally",
- fields: fields{
- Username: "user1",
- UserID: 1,
- NamespaceID: 3,
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- db.LoadAndAssertFixtures(t)
- s := db.NewSession()
-
- nu := &NamespaceUser{
- ID: tt.fields.ID,
- Username: tt.fields.Username,
- NamespaceID: tt.fields.NamespaceID,
- Right: tt.fields.Right,
- Created: tt.fields.Created,
- Updated: tt.fields.Updated,
- CRUDable: tt.fields.CRUDable,
- Rights: tt.fields.Rights,
- }
- err := nu.Delete(s, &user.User{ID: 1})
- if (err != nil) != tt.wantErr {
- t.Errorf("NamespaceUser.Delete() error = %v, wantErr %v", err, tt.wantErr)
- }
- if (err != nil) && tt.wantErr && !tt.errType(err) {
- t.Errorf("NamespaceUser.Delete() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
- }
- err = s.Commit()
- assert.NoError(t, err)
-
- if !tt.wantErr {
- db.AssertMissing(t, "users_namespaces", map[string]interface{}{
- "user_id": tt.fields.UserID,
- "namespace_id": tt.fields.NamespaceID,
- })
- }
- })
- }
-}
diff --git a/pkg/models/project.go b/pkg/models/project.go
index d1212ea49..a0fe41c17 100644
--- a/pkg/models/project.go
+++ b/pkg/models/project.go
@@ -47,13 +47,14 @@ type Project struct {
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
OwnerID int64 `xorm:"bigint INDEX not null" json:"-"`
- NamespaceID int64 `xorm:"bigint INDEX not null" json:"namespace_id" param:"namespace"`
ParentProjectID int64 `xorm:"bigint INDEX null" json:"parent_project_id"`
+ ChildProjects []*Project `xorm:"-" json:"child_projects"`
+
// The user who created this project.
Owner *user.User `xorm:"-" json:"owner" valid:"-"`
- // Whether or not a project is archived.
+ // Whether a project is archived.
IsArchived bool `xorm:"not null default false" json:"is_archived" query:"is_archived"`
// The id of the file this project has set as background
@@ -92,7 +93,7 @@ type ProjectWithTasksAndBuckets struct {
}
// TableName returns a better name for the projects table
-func (l *Project) TableName() string {
+func (p *Project) TableName() string {
return "projects"
}
@@ -104,70 +105,42 @@ type ProjectBackgroundType struct {
// ProjectBackgroundUpload represents the project upload background type
const ProjectBackgroundUpload string = "upload"
-// FavoritesPseudoProject holds all tasks marked as favorites
-var FavoritesPseudoProject = Project{
+// SharedProjectsPseudoProject is a pseudo project used to hold shared projects
+var SharedProjectsPseudoProject = &Project{
ID: -1,
- Title: "Favorites",
- Description: "This project has all tasks marked as favorites.",
- NamespaceID: FavoritesPseudoNamespace.ID,
- IsFavorite: true,
+ Title: "Shared Projects",
+ Description: "Projects of other users shared with you via teams or directly.",
Created: time.Now(),
Updated: time.Now(),
}
-// GetProjectsByNamespaceID gets all projects in a namespace
-func GetProjectsByNamespaceID(s *xorm.Session, nID int64, doer *user.User) (projects []*Project, err error) {
- switch nID {
- case SharedProjectsPseudoNamespace.ID:
- nnn, err := getSharedProjectsInNamespace(s, false, doer)
- if err != nil {
- return nil, err
- }
- if nnn != nil && nnn.Projects != nil {
- projects = nnn.Projects
- }
- case FavoritesPseudoNamespace.ID:
- namespaces := make(map[int64]*NamespaceWithProjects)
- _, err := getNamespacesWithProjects(s, &namespaces, "", false, 0, -1, doer.ID)
- if err != nil {
- return nil, err
- }
- namespaceIDs, _ := getNamespaceOwnerIDs(namespaces)
- ls, err := getProjectsForNamespaces(s, namespaceIDs, false)
- if err != nil {
- return nil, err
- }
- nnn, err := getFavoriteProjects(s, ls, namespaceIDs, doer)
- if err != nil {
- return nil, err
- }
- if nnn != nil && nnn.Projects != nil {
- projects = nnn.Projects
- }
- case SavedFiltersPseudoNamespace.ID:
- nnn, err := getSavedFilters(s, doer)
- if err != nil {
- return nil, err
- }
- if nnn != nil && nnn.Projects != nil {
- projects = nnn.Projects
- }
- default:
- err = s.Select("l.*").
- Alias("l").
- Join("LEFT", []string{"namespaces", "n"}, "l.namespace_id = n.id").
- Where("l.is_archived = false").
- Where("n.is_archived = false OR n.is_archived IS NULL").
- Where("namespace_id = ?", nID).
- Find(&projects)
- }
- if err != nil {
- return nil, err
- }
+// FavoriteProjectsPseudoProject is a pseudo namespace used to hold favorite projects and tasks
+var FavoriteProjectsPseudoProject = &Project{
+ ID: -2,
+ Title: "Favorites",
+ Description: "Favorite projects and tasks.",
+ Created: time.Now(),
+ Updated: time.Now(),
+}
- // get more project details
- err = addProjectDetails(s, projects, doer)
- return projects, err
+// SavedFiltersPseudoProject is a pseudo namespace used to hold saved filters
+var SavedFiltersPseudoProject = &Project{
+ ID: -3,
+ Title: "Filters",
+ Description: "Saved filters.",
+ Created: time.Now(),
+ Updated: time.Now(),
+}
+
+// FavoritesPseudoProject holds all tasks marked as favorites
+var FavoritesPseudoProject = Project{
+ ID: -1,
+ Title: "Favorites",
+ Description: "This project has all tasks marked as favorites.",
+ ParentProjectID: FavoriteProjectsPseudoProject.ID,
+ IsFavorite: true,
+ Created: time.Now(),
+ Updated: time.Now(),
}
// ReadAll gets all projects a user has access to
@@ -185,7 +158,7 @@ func GetProjectsByNamespaceID(s *xorm.Session, nID int64, doer *user.User) (proj
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects [get]
-func (l *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
+func (p *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
// Check if we're dealing with a share auth
shareAuth, ok := a.(*LinkSharing)
if ok {
@@ -193,26 +166,67 @@ func (l *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int,
if err != nil {
return nil, 0, 0, err
}
- projects := []*Project{project}
+ projects := map[int64]*Project{}
+ projects[project.ID] = project
err = addProjectDetails(s, projects, a)
return projects, 0, 0, err
}
- projects, resultCount, totalItems, err := getRawProjectsForUser(
+ doer, err := user.GetFromAuth(a)
+ if err != nil {
+ return nil, 0, 0, err
+ }
+
+ allProjects, resultCount, totalItems, err := getRawProjectsForUser(
s,
&projectOptions{
- search: search,
- user: &user.User{ID: a.GetID()},
- page: page,
- perPage: perPage,
- isArchived: l.IsArchived,
+ search: search,
+ user: doer,
+ page: page,
+ perPage: perPage,
+ getArchived: p.IsArchived,
})
if err != nil {
return nil, 0, 0, err
}
- // Add more project details
- err = addProjectDetails(s, projects, a)
+ /////////////////
+ // Saved Filters
+
+ savedFiltersProject, err := getSavedFilterProjects(s, doer)
+ if err != nil {
+ return nil, 0, 0, err
+ }
+
+ if savedFiltersProject != nil {
+ allProjects[savedFiltersProject.ID] = savedFiltersProject
+ }
+
+ /////////////////
+ // Add project details (favorite state, among other things)
+ err = addProjectDetails(s, allProjects, a)
+ if err != nil {
+ return
+ }
+
+ //////////////////////////
+ // Putting it all together
+
+ var projects []*Project
+ for _, p := range allProjects {
+ if p.ParentProjectID != 0 {
+ if allProjects[p.ParentProjectID].ChildProjects == nil {
+ allProjects[p.ParentProjectID].ChildProjects = []*Project{}
+ }
+ allProjects[p.ParentProjectID].ChildProjects = append(allProjects[p.ParentProjectID].ChildProjects, p)
+ continue
+ }
+
+ // The projects variable will contain all projects which have no parents
+ // And because we're using the same pointers for everything, those will contain child projects
+ projects = append(projects, p)
+ }
+
return projects, resultCount, totalItems, err
}
@@ -228,61 +242,61 @@ func (l *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int,
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{id} [get]
-func (l *Project) ReadOne(s *xorm.Session, a web.Auth) (err error) {
+func (p *Project) ReadOne(s *xorm.Session, a web.Auth) (err error) {
- if l.ID == FavoritesPseudoProject.ID {
+ if p.ID == FavoritesPseudoProject.ID {
// Already "built" the project in CanRead
return nil
}
// Check for saved filters
- if getSavedFilterIDFromProjectID(l.ID) > 0 {
- sf, err := getSavedFilterSimpleByID(s, getSavedFilterIDFromProjectID(l.ID))
+ if getSavedFilterIDFromProjectID(p.ID) > 0 {
+ sf, err := getSavedFilterSimpleByID(s, getSavedFilterIDFromProjectID(p.ID))
if err != nil {
return err
}
- l.Title = sf.Title
- l.Description = sf.Description
- l.Created = sf.Created
- l.Updated = sf.Updated
- l.OwnerID = sf.OwnerID
+ p.Title = sf.Title
+ p.Description = sf.Description
+ p.Created = sf.Created
+ p.Updated = sf.Updated
+ p.OwnerID = sf.OwnerID
}
// Get project owner
- l.Owner, err = user.GetUserByID(s, l.OwnerID)
+ p.Owner, err = user.GetUserByID(s, p.OwnerID)
if err != nil {
return err
}
// Check if the namespace is archived and set the namespace to archived if it is not already archived individually.
- if !l.IsArchived {
- err = l.CheckIsArchived(s)
+ if !p.IsArchived {
+ err = p.CheckIsArchived(s)
if err != nil {
if !IsErrNamespaceIsArchived(err) && !IsErrProjectIsArchived(err) {
return
}
- l.IsArchived = true
+ p.IsArchived = true
}
}
// Get any background information if there is one set
- if l.BackgroundFileID != 0 {
+ if p.BackgroundFileID != 0 {
// Unsplash image
- l.BackgroundInformation, err = GetUnsplashPhotoByFileID(s, l.BackgroundFileID)
+ p.BackgroundInformation, err = GetUnsplashPhotoByFileID(s, p.BackgroundFileID)
if err != nil && !files.IsErrFileIsNotUnsplashFile(err) {
return
}
if err != nil && files.IsErrFileIsNotUnsplashFile(err) {
- l.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload}
+ p.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload}
}
}
- l.IsFavorite, err = isFavorite(s, l.ID, a, FavoriteKindProject)
+ p.IsFavorite, err = isFavorite(s, p.ID, a, FavoriteKindProject)
if err != nil {
return
}
- l.Subscription, err = GetSubscription(s, SubscriptionEntityProject, l.ID, a)
+ p.Subscription, err = GetSubscription(s, SubscriptionEntityProject, p.ID, a)
return
}
@@ -345,62 +359,32 @@ func GetProjectsByIDs(s *xorm.Session, projectIDs []int64) (projects map[int64]*
}
type projectOptions struct {
- search string
- user *user.User
- page int
- perPage int
- isArchived bool
+ search string
+ user *user.User
+ page int
+ perPage int
+ getArchived bool
}
-func getUserProjectsStatement(userID int64) *builder.Builder {
+func getUserProjectsStatement(userID int64, search string, getArchived bool) *builder.Builder {
dialect := config.DatabaseType.GetString()
if dialect == "sqlite" {
dialect = builder.SQLITE
}
- return builder.Dialect(dialect).
- Select("l.*").
- From("projects", "l").
- Join("INNER", "namespaces n", "l.namespace_id = n.id").
- Join("LEFT", "team_namespaces tn", "tn.namespace_id = n.id").
- Join("LEFT", "team_members tm", "tm.team_id = tn.team_id").
- Join("LEFT", "team_projects tl", "l.id = tl.project_id").
- Join("LEFT", "team_members tm2", "tm2.team_id = tl.team_id").
- Join("LEFT", "users_projects ul", "ul.project_id = l.id").
- Join("LEFT", "users_namespaces un", "un.namespace_id = l.namespace_id").
- Where(builder.Or(
- builder.Eq{"tm.user_id": userID},
- builder.Eq{"tm2.user_id": userID},
- builder.Eq{"ul.user_id": userID},
- builder.Eq{"un.user_id": userID},
- builder.Eq{"l.owner_id": userID},
- )).
- OrderBy("position").
- GroupBy("l.id")
-}
-
-// Gets the projects only, without any tasks or so
-func getRawProjectsForUser(s *xorm.Session, opts *projectOptions) (projects []*Project, resultCount int, totalItems int64, err error) {
- fullUser, err := user.GetUserByID(s, opts.user.ID)
- if err != nil {
- return nil, 0, 0, err
- }
-
// Adding a 1=1 condition by default here because xorm always needs a condition and cannot handle nil conditions
- var isArchivedCond builder.Cond = builder.Eq{"1": 1}
- if !opts.isArchived {
- isArchivedCond = builder.And(
+ var getArchivedCond builder.Cond = builder.Eq{"1": 1}
+ if !getArchived {
+ getArchivedCond = builder.And(
builder.Eq{"l.is_archived": false},
builder.Eq{"n.is_archived": false},
)
}
- limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
-
var filterCond builder.Cond
ids := []int64{}
- if opts.search != "" {
- vals := strings.Split(opts.search, ",")
+ if search != "" {
+ vals := strings.Split(search, ",")
for _, val := range vals {
v, err := strconv.ParseInt(val, 10, 64)
if err != nil {
@@ -411,65 +395,114 @@ func getRawProjectsForUser(s *xorm.Session, opts *projectOptions) (projects []*P
}
}
- filterCond = db.ILIKE("l.title", opts.search)
+ filterCond = db.ILIKE("l.title", search)
if len(ids) > 0 {
filterCond = builder.In("l.id", ids)
}
- // Gets all Projects where the user is either owner or in a team which has access to the project
- // Or in a team which has namespace read access
+ return builder.Dialect(dialect).
+ Select("l.*").
+ From("projects", "l").
+ // TODO: remove namespaces
+ Join("INNER", "namespaces n", "l.namespace_id = n.id").
+ Join("LEFT", "team_namespaces tn", "tn.namespace_id = n.id").
+ Join("LEFT", "team_members tm", "tm.team_id = tn.team_id").
+ Join("LEFT", "team_projects tl", "l.id = tl.project_id").
+ Join("LEFT", "team_members tm2", "tm2.team_id = tl.team_id").
+ Join("LEFT", "users_projects ul", "ul.project_id = l.id").
+ Join("LEFT", "users_namespaces un", "un.namespace_id = l.namespace_id").
+ Where(builder.And(
+ builder.Or(
+ builder.Eq{"tm.user_id": userID},
+ builder.Eq{"tm2.user_id": userID},
+ builder.Eq{"ul.user_id": userID},
+ builder.Eq{"un.user_id": userID},
+ builder.Eq{"l.owner_id": userID},
+ ),
+ filterCond,
+ getArchivedCond,
+ )).
+ OrderBy("position").
+ GroupBy("l.id")
+}
- query := getUserProjectsStatement(fullUser.ID).
- Where(filterCond).
- Where(isArchivedCond)
- if limit > 0 {
- query = query.Limit(limit, start)
- }
- err = s.SQL(query).Find(&projects)
+// Gets the projects with their children without any tasks
+func getRawProjectsForUser(s *xorm.Session, opts *projectOptions) (projects map[int64]*Project, resultCount int, totalItems int64, err error) {
+ fullUser, err := user.GetUserByID(s, opts.user.ID)
if err != nil {
return nil, 0, 0, err
}
- query = getUserProjectsStatement(fullUser.ID).
- Where(filterCond).
- Where(isArchivedCond)
+ limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
+
+ // Gets all projects where the user is either owner or it was shared to them
+
+ allProjects := make(map[int64]*Project)
+
+ query := getUserProjectsStatement(fullUser.ID, opts.search, opts.getArchived)
+ if limit > 0 {
+ query = query.Limit(limit, start)
+ }
+ err = s.SQL(query).Find(&allProjects)
+ if err != nil {
+ return nil, 0, 0, err
+ }
+
+ query = getUserProjectsStatement(fullUser.ID, opts.search, opts.getArchived)
totalItems, err = s.
SQL(query.Select("count(*)")).
Count(&Project{})
- return projects, len(projects), totalItems, err
+
+ if len(allProjects) == 0 {
+ return nil, 0, totalItems, nil
+ }
+
+ return projects, len(allProjects), totalItems, err
+}
+
+func getSavedFilterProjects(s *xorm.Session, doer *user.User) (savedFiltersNamespace *Project, err error) {
+ savedFilters, err := getSavedFiltersForUser(s, doer)
+ if err != nil {
+ return
+ }
+
+ if len(savedFilters) == 0 {
+ return nil, nil
+ }
+
+ savedFiltersPseudoNamespace := SavedFiltersPseudoProject
+ savedFiltersPseudoNamespace.OwnerID = doer.ID
+ *savedFiltersNamespace = *savedFiltersPseudoNamespace
+ savedFiltersNamespace.ChildProjects = make([]*Project, 0, len(savedFilters))
+
+ for _, filter := range savedFilters {
+ filterProject := filter.toProject()
+ filterProject.ParentProjectID = savedFiltersNamespace.ID
+ filterProject.Owner = doer
+ savedFiltersNamespace.ChildProjects = append(savedFiltersNamespace.ChildProjects, filterProject)
+ }
+
+ return
}
// addProjectDetails adds owner user objects and project tasks to all projects in the slice
-func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err error) {
+func addProjectDetails(s *xorm.Session, projects map[int64]*Project, a web.Auth) (err error) {
if len(projects) == 0 {
return
}
var ownerIDs []int64
- for _, l := range projects {
- ownerIDs = append(ownerIDs, l.OwnerID)
- }
-
- // Get all project owners
- owners := map[int64]*user.User{}
- if len(ownerIDs) > 0 {
- err = s.In("id", ownerIDs).Find(&owners)
- if err != nil {
- return
- }
- }
-
- var fileIDs []int64
var projectIDs []int64
- for _, l := range projects {
- projectIDs = append(projectIDs, l.ID)
- if o, exists := owners[l.OwnerID]; exists {
- l.Owner = o
- }
- if l.BackgroundFileID != 0 {
- l.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload}
- }
- fileIDs = append(fileIDs, l.BackgroundFileID)
+ var fileIDs []int64
+ for _, p := range projects {
+ ownerIDs = append(ownerIDs, p.OwnerID)
+ projectIDs = append(projectIDs, p.ID)
+ fileIDs = append(fileIDs, p.BackgroundFileID)
+ }
+
+ owners, err := user.GetUsersByIDs(s, ownerIDs)
+ if err != nil {
+ return err
}
favs, err := getFavorites(s, projectIDs, a, FavoriteKindProject)
@@ -479,19 +512,26 @@ func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err er
subscriptions, err := GetSubscriptions(s, SubscriptionEntityProject, projectIDs, a)
if err != nil {
- log.Errorf("An error occurred while getting project subscriptions for a namespace item: %s", err.Error())
+ log.Errorf("An error occurred while getting project subscriptions for a project: %s", err.Error())
subscriptions = make(map[int64]*Subscription)
}
- for _, project := range projects {
+ for _, p := range projects {
+ if o, exists := owners[p.OwnerID]; exists {
+ p.Owner = o
+ }
+ if p.BackgroundFileID != 0 {
+ p.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload}
+ }
+
// Don't override the favorite state if it was already set from before (favorite saved filters do this)
- if project.IsFavorite {
+ if p.IsFavorite {
continue
}
- project.IsFavorite = favs[project.ID]
+ p.IsFavorite = favs[p.ID]
- if subscription, exists := subscriptions[project.ID]; exists {
- project.Subscription = subscription
+ if subscription, exists := subscriptions[p.ID]; exists {
+ p.Subscription = subscription
}
}
@@ -521,46 +561,36 @@ func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err er
return
}
-// NamespaceProject is a meta type to be able to join a project with its namespace
-type NamespaceProject struct {
- Project Project `xorm:"extends"`
- Namespace Namespace `xorm:"extends"`
-}
-
-// CheckIsArchived returns an ErrProjectIsArchived or ErrNamespaceIsArchived if the project or its namespace is archived.
-func (l *Project) CheckIsArchived(s *xorm.Session) (err error) {
- // When creating a new project, we check if the namespace is archived
- if l.ID == 0 {
- n := &Namespace{ID: l.NamespaceID}
- return n.CheckIsArchived(s)
+// CheckIsArchived returns an ErrProjectIsArchived or ErrNamespaceIsArchived 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 {
+ p := &Project{ID: p.ParentProjectID}
+ return p.CheckIsArchived(s)
}
- nl := &NamespaceProject{}
- exists, err := s.
- Table("projects").
- Join("LEFT", "namespaces", "projects.namespace_id = namespaces.id").
- Where("projects.id = ? AND (projects.is_archived = true OR namespaces.is_archived = true)", l.ID).
- Get(nl)
+ p, err := GetProjectSimpleByID(s, p.ID)
if err != nil {
- return
+ return err
}
- if exists && nl.Project.ID != 0 && nl.Project.IsArchived {
- return ErrProjectIsArchived{ProjectID: l.ID}
- }
- if exists && nl.Namespace.ID != 0 && nl.Namespace.IsArchived {
- return ErrNamespaceIsArchived{NamespaceID: nl.Namespace.ID}
+
+ // TODO: parent project
+
+ if p.IsArchived {
+ return ErrProjectIsArchived{ProjectID: p.ID}
}
+
return nil
}
func checkProjectBeforeUpdateOrDelete(s *xorm.Session, project *Project) error {
- if project.NamespaceID < 0 {
- return &ErrProjectCannotBelongToAPseudoNamespace{ProjectID: project.ID, NamespaceID: project.NamespaceID}
+ if project.ParentProjectID < 0 {
+ return &ErrProjectCannotBelongToAPseudoNamespace{ProjectID: project.ID, NamespaceID: project.ParentProjectID}
}
- // Check if the namespace exists
- if project.NamespaceID > 0 {
- _, err := GetNamespaceByID(s, project.NamespaceID)
+ // Check if the parent project exists
+ if project.ParentProjectID > 0 {
+ _, err := GetProjectSimpleByID(s, project.ParentProjectID)
if err != nil {
return err
}
@@ -635,19 +665,21 @@ func CreateProject(s *xorm.Session, project *Project, auth web.Auth) (err error)
})
}
+// CreateNewProjectForUser creates a new inbox project for a user. To prevent import cycles, we can't do that
+// directly in the user.Create function.
+func CreateNewProjectForUser(s *xorm.Session, user *user.User) (err error) {
+ p := &Project{
+ Title: "Inbox",
+ }
+ return p.Create(s, user)
+}
+
func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProjectBackground bool) (err error) {
err = checkProjectBeforeUpdateOrDelete(s, project)
if err != nil {
return
}
- if project.NamespaceID == 0 {
- return &ErrProjectMustBelongToANamespace{
- ProjectID: project.ID,
- NamespaceID: project.NamespaceID,
- }
- }
-
// We need to specify the cols we want to update here to be able to un-archive projects
colsToUpdate := []string{
"title",
@@ -721,27 +753,27 @@ func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProje
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{id} [post]
-func (l *Project) Update(s *xorm.Session, a web.Auth) (err error) {
- fid := getSavedFilterIDFromProjectID(l.ID)
+func (p *Project) Update(s *xorm.Session, a web.Auth) (err error) {
+ fid := getSavedFilterIDFromProjectID(p.ID)
if fid > 0 {
f, err := getSavedFilterSimpleByID(s, fid)
if err != nil {
return err
}
- f.Title = l.Title
- f.Description = l.Description
- f.IsFavorite = l.IsFavorite
+ f.Title = p.Title
+ f.Description = p.Description
+ f.IsFavorite = p.IsFavorite
err = f.Update(s, a)
if err != nil {
return err
}
- *l = *f.toProject()
+ *p = *f.toProject()
return nil
}
- return UpdateProject(s, l, a, false)
+ return UpdateProject(s, p, a, false)
}
func updateProjectLastUpdated(s *xorm.Session, project *Project) error {
@@ -773,13 +805,13 @@ func updateProjectByTaskID(s *xorm.Session, taskID int64) (err error) {
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{namespaceID}/projects [put]
-func (l *Project) Create(s *xorm.Session, a web.Auth) (err error) {
- err = CreateProject(s, l, a)
+func (p *Project) Create(s *xorm.Session, a web.Auth) (err error) {
+ err = CreateProject(s, p, a)
if err != nil {
return
}
- return l.ReadOne(s, a)
+ return p.ReadOne(s, a)
}
// Delete implements the delete method of CRUDable
@@ -794,7 +826,7 @@ func (l *Project) Create(s *xorm.Session, a web.Auth) (err error) {
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{id} [delete]
-func (l *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
+func (p *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
fullList, err := GetProjectSimpleByID(s, l.ID)
if err != nil {
@@ -802,14 +834,14 @@ func (l *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
}
// Delete the project
- _, err = s.ID(l.ID).Delete(&Project{})
+ _, err = s.ID(p.ID).Delete(&Project{})
if err != nil {
return
}
// 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{l}, a, &taskOptions{})
+ tasks, _, _, err := getRawTasksForProjects(s, []*Project{p}, a, &taskOptions{})
if err != nil {
return
}
@@ -827,7 +859,7 @@ func (l *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
}
return events.Dispatch(&ProjectDeletedEvent{
- Project: l,
+ Project: p,
Doer: a,
})
}
diff --git a/pkg/models/project_rights.go b/pkg/models/project_rights.go
index da8285fb2..cf41ce780 100644
--- a/pkg/models/project_rights.go
+++ b/pkg/models/project_rights.go
@@ -24,15 +24,15 @@ import (
)
// CanWrite return whether the user can write on that project or not
-func (l *Project) CanWrite(s *xorm.Session, a web.Auth) (bool, error) {
+func (p *Project) CanWrite(s *xorm.Session, a web.Auth) (bool, error) {
// The favorite project can't be edited
- if l.ID == FavoritesPseudoProject.ID {
+ if p.ID == FavoritesPseudoProject.ID {
return false, nil
}
// Get the project and check the right
- originalProject, err := GetProjectSimpleByID(s, l.ID)
+ originalProject, err := GetProjectSimpleByID(s, p.ID)
if err != nil {
return false, err
}
@@ -67,66 +67,66 @@ func (l *Project) CanWrite(s *xorm.Session, a web.Auth) (bool, error) {
}
// CanRead checks if a user has read access to a project
-func (l *Project) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
+func (p *Project) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
// The favorite project needs a special treatment
- if l.ID == FavoritesPseudoProject.ID {
+ if p.ID == FavoritesPseudoProject.ID {
owner, err := user.GetFromAuth(a)
if err != nil {
return false, 0, err
}
- *l = FavoritesPseudoProject
- l.Owner = owner
+ *p = FavoritesPseudoProject
+ p.Owner = owner
return true, int(RightRead), nil
}
// Saved Filter Projects need a special case
- if getSavedFilterIDFromProjectID(l.ID) > 0 {
- sf := &SavedFilter{ID: getSavedFilterIDFromProjectID(l.ID)}
+ if getSavedFilterIDFromProjectID(p.ID) > 0 {
+ sf := &SavedFilter{ID: getSavedFilterIDFromProjectID(p.ID)}
return sf.CanRead(s, a)
}
// Check if the user is either owner or can read
var err error
- originalProject, err := GetProjectSimpleByID(s, l.ID)
+ originalProject, err := GetProjectSimpleByID(s, p.ID)
if err != nil {
return false, 0, err
}
- *l = *originalProject
+ *p = *originalProject
// Check if we're dealing with a share auth
shareAuth, ok := a.(*LinkSharing)
if ok {
- return l.ID == shareAuth.ProjectID &&
+ return p.ID == shareAuth.ProjectID &&
(shareAuth.Right == RightRead || shareAuth.Right == RightWrite || shareAuth.Right == RightAdmin), int(shareAuth.Right), nil
}
- if l.isOwner(&user.User{ID: a.GetID()}) {
+ if p.isOwner(&user.User{ID: a.GetID()}) {
return true, int(RightAdmin), nil
}
- return l.checkRight(s, a, RightRead, RightWrite, RightAdmin)
+ return p.checkRight(s, a, RightRead, RightWrite, RightAdmin)
}
// CanUpdate checks if the user can update a project
-func (l *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err error) {
+func (p *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err error) {
// The favorite project can't be edited
- if l.ID == FavoritesPseudoProject.ID {
+ if p.ID == FavoritesPseudoProject.ID {
return false, nil
}
// Get the project
- ol, err := GetProjectSimpleByID(s, l.ID)
+ ol, err := GetProjectSimpleByID(s, p.ID)
if err != nil {
return false, err
}
- // Check if we're moving the project into a different namespace.
+ // Check if we're moving the project to a different parent project.
// If that is the case, we need to verify permissions to do so.
- if l.NamespaceID != 0 && l.NamespaceID != ol.NamespaceID {
- newNamespace := &Namespace{ID: l.NamespaceID}
- can, err := newNamespace.CanWrite(s, a)
+ if p.ParentProjectID != 0 && p.ParentProjectID != ol.ParentProjectID {
+ newProject := &Project{ID: p.ParentProjectID}
+ can, err := newProject.CanWrite(s, a)
if err != nil {
return false, err
}
@@ -135,7 +135,7 @@ func (l *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err er
}
}
- fid := getSavedFilterIDFromProjectID(l.ID)
+ fid := getSavedFilterIDFromProjectID(p.ID)
if fid > 0 {
sf, err := getSavedFilterSimpleByID(s, fid)
if err != nil {
@@ -145,34 +145,40 @@ func (l *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err er
return sf.CanUpdate(s, a)
}
- canUpdate, err = l.CanWrite(s, a)
+ canUpdate, err = p.CanWrite(s, a)
// If the project is archived and the user tries to un-archive it, let the request through
- if IsErrProjectIsArchived(err) && !l.IsArchived {
+ if IsErrProjectIsArchived(err) && !p.IsArchived {
err = nil
}
return canUpdate, err
}
// CanDelete checks if the user can delete a project
-func (l *Project) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
- return l.IsAdmin(s, a)
+func (p *Project) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
+ return p.IsAdmin(s, a)
}
// CanCreate checks if the user can create a project
-func (l *Project) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
- // A user can create a project if they have write access to the namespace
- n := &Namespace{ID: l.NamespaceID}
- return n.CanWrite(s, a)
+func (p *Project) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
+ // If the new namespace has a parent, check that
+ if p.ParentProjectID != 0 {
+ // TODO: check the parent's parent (and so on)
+ parent := &Project{ID: p.ParentProjectID}
+ return parent.CanCreate(s, a)
+ }
+
+ // Otherwise just allow
+ return true, nil
}
// IsAdmin returns whether the user has admin rights on the project or not
-func (l *Project) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
+func (p *Project) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
// The favorite project can't be edited
- if l.ID == FavoritesPseudoProject.ID {
+ if p.ID == FavoritesPseudoProject.ID {
return false, nil
}
- originalProject, err := GetProjectSimpleByID(s, l.ID)
+ originalProject, err := GetProjectSimpleByID(s, p.ID)
if err != nil {
return false, err
}
@@ -194,12 +200,12 @@ func (l *Project) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
}
// Little helper function to check if a user is project owner
-func (l *Project) isOwner(u *user.User) bool {
- return l.OwnerID == u.ID
+func (p *Project) isOwner(u *user.User) bool {
+ return p.OwnerID == u.ID
}
// Checks n different rights for any given user
-func (l *Project) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, int, error) {
+func (p *Project) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, int, error) {
/*
The following loop creates a sql condition like this one:
@@ -242,53 +248,44 @@ func (l *Project) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool
conds = append(conds, builder.Eq{"n.owner_id": a.GetID()})
type allProjectRights struct {
- UserNamespace *NamespaceUser `xorm:"extends"`
- UserProject *ProjectUser `xorm:"extends"`
+ UserProject *ProjectUser `xorm:"extends"`
+ TeamProject *TeamProject `xorm:"extends"`
- TeamNamespace *TeamNamespace `xorm:"extends"`
- TeamProject *TeamProject `xorm:"extends"`
-
- NamespaceOwnerID int64 `xorm:"namespaces_owner_id"`
+ OwnerID int64 `xorm:"namespaces_owner_id"`
}
r := &allProjectRights{}
var maxRight = 0
exists, err := s.
- Select("l.*, un.right, ul.right, tn.right, tl.right, n.owner_id as namespaces_owner_id").
+ Select("p.*, un.right, ul.right, tn.right, tl.right, n.owner_id as namespaces_owner_id").
Table("projects").
- Alias("l").
+ Alias("p").
// 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").
+ Join("LEFT", []string{"users_namespaces", "un"}, "un.namespace_id = p.namespace_id").
+ Join("LEFT", []string{"users_projects", "ul"}, "ul.project_id = p.id").
+ Join("LEFT", []string{"namespaces", "n"}, "n.id = p.namespace_id").
// Team stuff
- Join("LEFT", []string{"team_namespaces", "tn"}, " l.namespace_id = tn.namespace_id").
+ Join("LEFT", []string{"team_namespaces", "tn"}, " p.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_projects", "tl"}, "p.id = tl.project_id").
Join("LEFT", []string{"team_members", "tm2"}, "tm2.team_id = tl.team_id").
// The actual condition
Where(builder.And(
builder.Or(
conds...,
),
- builder.Eq{"l.id": l.ID},
+ builder.Eq{"p.id": p.ID},
)).
Get(r)
// Figure out the max right and return it
- if int(r.UserNamespace.Right) > maxRight {
- maxRight = int(r.UserNamespace.Right)
- }
if int(r.UserProject.Right) > maxRight {
maxRight = int(r.UserProject.Right)
}
- if int(r.TeamNamespace.Right) > maxRight {
- maxRight = int(r.TeamNamespace.Right)
- }
if int(r.TeamProject.Right) > maxRight {
maxRight = int(r.TeamProject.Right)
}
- if r.NamespaceOwnerID == a.GetID() {
+ if r.OwnerID == a.GetID() {
maxRight = int(RightAdmin)
}
diff --git a/pkg/models/saved_filters.go b/pkg/models/saved_filters.go
index b2a5958f6..09a120b70 100644
--- a/pkg/models/saved_filters.go
+++ b/pkg/models/saved_filters.go
@@ -102,7 +102,7 @@ func (sf *SavedFilter) toProject() *Project {
Created: sf.Created,
Updated: sf.Updated,
Owner: sf.Owner,
- NamespaceID: SavedFiltersPseudoNamespace.ID,
+ NamespaceID: SavedFiltersPseudoProject.ID,
}
}
diff --git a/pkg/models/user_delete.go b/pkg/models/user_delete.go
index 55858c149..d0009042c 100644
--- a/pkg/models/user_delete.go
+++ b/pkg/models/user_delete.go
@@ -87,45 +87,6 @@ func deleteUsers() {
}
}
-func getNamespacesToDelete(s *xorm.Session, u *user.User) (namespacesToDelete []*Namespace, err error) {
- namespacesToDelete = []*Namespace{}
- nm := &Namespace{IsArchived: true}
- res, _, _, err := nm.ReadAll(s, u, "", 1, -1)
- if err != nil {
- return nil, err
- }
-
- if res == nil {
- return nil, nil
- }
-
- namespaces := res.([]*NamespaceWithProjects)
- for _, n := range namespaces {
- if n.ID < 0 {
- continue
- }
-
- hadUsers, err := ensureNamespaceAdminUser(s, &n.Namespace)
- if err != nil {
- return nil, err
- }
- if hadUsers {
- continue
- }
- hadTeams, err := ensureNamespaceAdminTeam(s, &n.Namespace)
- if err != nil {
- return nil, err
- }
- if hadTeams {
- continue
- }
-
- namespacesToDelete = append(namespacesToDelete, &n.Namespace)
- }
-
- return
-}
-
func getProjectsToDelete(s *xorm.Session, u *user.User) (projectsToDelete []*Project, err error) {
projectsToDelete = []*Project{}
lm := &Project{IsArchived: true}
@@ -170,24 +131,11 @@ func getProjectsToDelete(s *xorm.Session, u *user.User) (projectsToDelete []*Pro
// This action is irrevocable.
// Public to allow deletion from the CLI.
func DeleteUser(s *xorm.Session, u *user.User) (err error) {
- namespacesToDelete, err := getNamespacesToDelete(s, u)
- if err != nil {
- return err
- }
-
projectsToDelete, err := getProjectsToDelete(s, u)
if err != nil {
return err
}
- // Delete everything not shared with anybody else
- for _, n := range namespacesToDelete {
- err = deleteNamespace(s, n, u, false)
- if err != nil {
- return err
- }
- }
-
for _, l := range projectsToDelete {
err = l.Delete(s, u)
if err != nil {
@@ -205,58 +153,6 @@ func DeleteUser(s *xorm.Session, u *user.User) (err error) {
})
}
-func ensureNamespaceAdminUser(s *xorm.Session, n *Namespace) (hadUsers bool, err error) {
- namespaceUsers := []*NamespaceUser{}
- err = s.Where("namespace_id = ?", n.ID).Find(&namespaceUsers)
- if err != nil {
- return
- }
-
- if len(namespaceUsers) == 0 {
- return false, nil
- }
-
- for _, lu := range namespaceUsers {
- if lu.Right == RightAdmin {
- // Project already has more than one admin, no need to do anything
- return true, nil
- }
- }
-
- firstUser := namespaceUsers[0]
- firstUser.Right = RightAdmin
- _, err = s.Where("id = ?", firstUser.ID).
- Cols("right").
- Update(firstUser)
- return true, err
-}
-
-func ensureNamespaceAdminTeam(s *xorm.Session, n *Namespace) (hadTeams bool, err error) {
- namespaceTeams := []*TeamNamespace{}
- err = s.Where("namespace_id = ?", n.ID).Find(&namespaceTeams)
- if err != nil {
- return
- }
-
- if len(namespaceTeams) == 0 {
- return false, nil
- }
-
- for _, lu := range namespaceTeams {
- if lu.Right == RightAdmin {
- // Project already has more than one admin, no need to do anything
- return true, nil
- }
- }
-
- firstTeam := namespaceTeams[0]
- firstTeam.Right = RightAdmin
- _, err = s.Where("id = ?", firstTeam.ID).
- Cols("right").
- Update(firstTeam)
- return true, err
-}
-
func ensureProjectAdminUser(s *xorm.Session, l *Project) (hadUsers bool, err error) {
projectUsers := []*ProjectUser{}
err = s.Where("project_id = ?", l.ID).Find(&projectUsers)
diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go
index a5649eb84..c2510b668 100644
--- a/pkg/modules/auth/openid/openid.go
+++ b/pkg/modules/auth/openid/openid.go
@@ -244,7 +244,7 @@ func getOrCreateUser(s *xorm.Session, cl *claims, issuer, subject string) (u *us
}
// And create its namespace
- err = models.CreateNewNamespaceForUser(s, u)
+ err = models.CreateNewProjectForUser(s, u)
if err != nil {
return nil, err
}
diff --git a/pkg/routes/api/v1/project_by_namespace.go b/pkg/routes/api/v1/project_by_namespace.go
deleted file mode 100644
index 1229e35c7..000000000
--- a/pkg/routes/api/v1/project_by_namespace.go
+++ /dev/null
@@ -1,97 +0,0 @@
-// Vikunja is a to-do list application to facilitate your life.
-// Copyright 2018-2021 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 v1
-
-import (
- "net/http"
- "strconv"
-
- "code.vikunja.io/api/pkg/db"
- "xorm.io/xorm"
-
- "code.vikunja.io/api/pkg/models"
- "code.vikunja.io/api/pkg/user"
- "code.vikunja.io/web/handler"
- "github.com/labstack/echo/v4"
-)
-
-// GetProjectsByNamespaceID is the web handler to get all projects belonging to a namespace
-// TODO: deprecate this in favour of namespace.ReadOne() <-- should also return the projects
-// @Summary Get all projects in a namespace
-// @Description Returns all projects inside of a namespace.
-// @tags namespace
-// @Accept json
-// @Produce json
-// @Param namespaceID path int true "Namespace ID"
-// @Security JWTKeyAuth
-// @Success 200 {array} models.Project "The projects."
-// @Failure 403 {object} models.Message "No access to that namespace."
-// @Failure 404 {object} models.Message "The namespace does not exist."
-// @Failure 500 {object} models.Message "Internal error"
-// @Router /namespaces/{namespaceID}/projects [get]
-func GetProjectsByNamespaceID(c echo.Context) error {
- s := db.NewSession()
- defer s.Close()
-
- // Get our namespace
- namespace, err := getNamespace(s, c)
- if err != nil {
- return handler.HandleHTTPError(err, c)
- }
-
- // Get the projects
- doer, err := user.GetCurrentUser(c)
- if err != nil {
- return handler.HandleHTTPError(err, c)
- }
-
- projects, err := models.GetProjectsByNamespaceID(s, namespace.ID, doer)
- if err != nil {
- return handler.HandleHTTPError(err, c)
- }
- return c.JSON(http.StatusOK, projects)
-}
-
-func getNamespace(s *xorm.Session, c echo.Context) (namespace *models.Namespace, err error) {
- // Check if we have our ID
- id := c.Param("namespace")
- // Make int
- namespaceID, err := strconv.ParseInt(id, 10, 64)
- if err != nil {
- return
- }
-
- if namespaceID == -1 {
- namespace = &models.SharedProjectsPseudoNamespace
- return
- }
-
- // Check if the user has acces to that namespace
- u, err := user.GetCurrentUser(c)
- if err != nil {
- return
- }
- namespace = &models.Namespace{ID: namespaceID}
- canRead, _, err := namespace.CanRead(s, u)
- if err != nil {
- return namespace, err
- }
- if !canRead {
- return nil, echo.ErrForbidden
- }
- return
-}
diff --git a/pkg/routes/api/v1/user_register.go b/pkg/routes/api/v1/user_register.go
index 40f7d233a..b6e6d40ea 100644
--- a/pkg/routes/api/v1/user_register.go
+++ b/pkg/routes/api/v1/user_register.go
@@ -63,7 +63,7 @@ func RegisterUser(c echo.Context) error {
}
// Add its namespace
- err = models.CreateNewNamespaceForUser(s, newUser)
+ err = models.CreateNewProjectForUser(s, newUser)
if err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)
diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go
index d883bcb06..8de2f5aed 100644
--- a/pkg/routes/routes.go
+++ b/pkg/routes/routes.go
@@ -22,7 +22,7 @@
// @description * `x-pagination-total-pages`: The total number of available pages for this request
// @description * `x-pagination-result-count`: The number of items returned for this request.
// @description # Rights
-// @description All endpoints which return a single item (project, task, namespace, etc.) - no array - will also return a `x-max-right` header with the max right the user has on this item as an int where `0` is `Read Only`, `1` is `Read & Write` and `2` is `Admin`.
+// @description All endpoints which return a single item (project, task, etc.) - no array - will also return a `x-max-right` header with the max right the user has on this item as an int where `0` is `Read Only`, `1` is `Read & Write` and `2` is `Admin`.
// @description This can be used to show or hide ui elements based on the rights the user has.
// @description # Authorization
// @description **JWT-Auth:** Main authorization method, used for most of the requests. Needs `Authorization: Bearer `-header to authenticate successfully.
@@ -488,38 +488,6 @@ func registerAPIRoutes(a *echo.Group) {
a.DELETE("/filters/:filter", savedFiltersHandler.DeleteWeb)
a.POST("/filters/:filter", savedFiltersHandler.UpdateWeb)
- namespaceHandler := &handler.WebHandler{
- EmptyStruct: func() handler.CObject {
- return &models.Namespace{}
- },
- }
- a.GET("/namespaces", namespaceHandler.ReadAllWeb)
- a.PUT("/namespaces", namespaceHandler.CreateWeb)
- a.GET("/namespaces/:namespace", namespaceHandler.ReadOneWeb)
- a.POST("/namespaces/:namespace", namespaceHandler.UpdateWeb)
- a.DELETE("/namespaces/:namespace", namespaceHandler.DeleteWeb)
- a.GET("/namespaces/:namespace/projects", apiv1.GetProjectsByNamespaceID)
-
- namespaceTeamHandler := &handler.WebHandler{
- EmptyStruct: func() handler.CObject {
- return &models.TeamNamespace{}
- },
- }
- a.GET("/namespaces/:namespace/teams", namespaceTeamHandler.ReadAllWeb)
- a.PUT("/namespaces/:namespace/teams", namespaceTeamHandler.CreateWeb)
- a.DELETE("/namespaces/:namespace/teams/:team", namespaceTeamHandler.DeleteWeb)
- a.POST("/namespaces/:namespace/teams/:team", namespaceTeamHandler.UpdateWeb)
-
- namespaceUserHandler := &handler.WebHandler{
- EmptyStruct: func() handler.CObject {
- return &models.NamespaceUser{}
- },
- }
- a.GET("/namespaces/:namespace/users", namespaceUserHandler.ReadAllWeb)
- a.PUT("/namespaces/:namespace/users", namespaceUserHandler.CreateWeb)
- a.DELETE("/namespaces/:namespace/users/:user", namespaceUserHandler.DeleteWeb)
- a.POST("/namespaces/:namespace/users/:user", namespaceUserHandler.UpdateWeb)
-
teamHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
return &models.Team{}