2020-02-07 17:27:45 +01:00
// Vikunja is a to-do list application to facilitate your life.
2021-02-02 20:19:13 +01:00
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
2018-11-26 21:17:33 +01:00
//
2019-12-04 20:39:56 +01:00
// This program is free software: you can redistribute it and/or modify
2020-12-23 16:41:52 +01:00
// it under the terms of the GNU Affero General Public Licensee as published by
2019-12-04 20:39:56 +01:00
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
2018-11-26 21:17:33 +01:00
//
2019-12-04 20:39:56 +01:00
// 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
2020-12-23 16:41:52 +01:00
// GNU Affero General Public Licensee for more details.
2018-11-26 21:17:33 +01:00
//
2020-12-23 16:41:52 +01:00
// You should have received a copy of the GNU Affero General Public Licensee
2019-12-04 20:39:56 +01:00
// along with this program. If not, see <https://www.gnu.org/licenses/>.
2018-11-26 21:17:33 +01:00
2018-06-10 14:14:10 +02:00
package models
2018-12-01 00:26:56 +01:00
import (
2020-12-22 00:13:15 +01:00
"strconv"
"strings"
2020-10-11 22:10:03 +02:00
"time"
2021-08-01 23:40:25 +02:00
"code.vikunja.io/api/pkg/db"
2021-03-24 22:46:20 +01:00
"code.vikunja.io/api/pkg/config"
2021-02-02 23:48:37 +01:00
"code.vikunja.io/api/pkg/events"
2020-05-26 22:07:55 +02:00
"code.vikunja.io/api/pkg/files"
2021-03-24 22:46:20 +01:00
"code.vikunja.io/api/pkg/log"
2020-01-26 18:08:06 +01:00
"code.vikunja.io/api/pkg/user"
2018-12-01 00:26:56 +01:00
"code.vikunja.io/web"
2020-03-15 22:50:39 +01:00
"xorm.io/builder"
2020-05-16 12:58:37 +02:00
"xorm.io/xorm"
2018-12-01 00:26:56 +01:00
)
2018-11-02 17:59:49 +01:00
2018-08-30 08:09:17 +02:00
// List represents a list of tasks
2018-06-10 14:14:10 +02:00
type List struct {
2019-01-03 23:22:06 +01:00
// The unique, numeric id of this list.
2020-12-18 17:51:22 +01:00
ID int64 ` xorm:"bigint autoincr not null unique pk" json:"id" param:"list" `
2019-01-03 23:22:06 +01:00
// The title of the list. You'll see this in the namespace overview.
2020-09-26 23:02:17 +02:00
Title string ` xorm:"varchar(250) not null" json:"title" valid:"required,runelength(1|250)" minLength:"1" maxLength:"250" `
2019-01-03 23:22:06 +01:00
// The description of the list.
2019-07-21 23:57:19 +02:00
Description string ` xorm:"longtext null" json:"description" `
2019-12-07 23:28:45 +01:00
// The unique list short identifier. Used to build task identifiers.
Identifier string ` xorm:"varchar(10) null" json:"identifier" valid:"runelength(0|10)" minLength:"0" maxLength:"10" `
2020-03-22 22:09:32 +01:00
// The hex color of this list
HexColor string ` xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6" `
2019-12-07 23:28:45 +01:00
2020-12-18 17:51:22 +01:00
OwnerID int64 ` xorm:"bigint INDEX not null" json:"-" `
NamespaceID int64 ` xorm:"bigint INDEX not null" json:"namespace_id" param:"namespace" `
2018-06-10 15:55:56 +02:00
2019-01-03 23:22:06 +01:00
// The user who created this list.
2020-01-26 18:08:06 +01:00
Owner * user . User ` xorm:"-" json:"owner" valid:"-" `
2020-12-16 15:19:09 +01:00
2020-03-15 22:50:39 +01:00
// Whether or not a list is archived.
IsArchived bool ` xorm:"not null default false" json:"is_archived" query:"is_archived" `
2020-05-26 22:07:55 +02:00
// The id of the file this list has set as background
BackgroundFileID int64 ` xorm:"null" json:"-" `
// Holds extra information about the background set since some background providers require attribution or similar. If not null, the background can be accessed at /lists/{listID}/background
BackgroundInformation interface { } ` xorm:"-" json:"background_information" `
2021-07-10 12:21:54 +02:00
// True if a list is a favorite. Favorite lists show up in a separate namespace. This value depends on the user making the call to the api.
IsFavorite bool ` xorm:"-" json:"is_favorite" `
2020-09-06 16:20:16 +02:00
2021-02-14 20:18:14 +01:00
// The subscription status for the user reading this list. You can only read this property, use the subscription endpoints to modify it.
// Will only returned when retreiving one list.
Subscription * Subscription ` xorm:"-" json:"subscription,omitempty" `
2021-07-28 21:06:40 +02:00
// The position this list has when querying all lists. See the tasks.position property on how to use this.
Position float64 ` xorm:"double null" json:"position" `
2020-02-08 13:48:49 +01:00
// A timestamp when this list was created. You cannot change this value.
2020-06-27 19:04:01 +02:00
Created time . Time ` xorm:"created not null" json:"created" `
2020-02-08 13:48:49 +01:00
// A timestamp when this list was last updated. You cannot change this value.
2020-06-27 19:04:01 +02:00
Updated time . Time ` xorm:"updated not null" json:"updated" `
2018-07-08 22:50:01 +02:00
2018-12-01 00:26:56 +01:00
web . CRUDable ` xorm:"-" json:"-" `
web . Rights ` xorm:"-" json:"-" `
2018-06-10 14:14:10 +02:00
}
2021-09-04 21:26:31 +02:00
type ListWithTasksAndBuckets struct {
List
// An array of tasks which belong to the list.
Tasks [ ] * TaskWithComments ` xorm:"-" json:"tasks" `
// Only used for migration.
Buckets [ ] * Bucket ` xorm:"-" json:"buckets" `
BackgroundFileID int64 ` xorm:"null" json:"background_file_id" `
}
2021-03-28 20:17:35 +02:00
// TableName returns a better name for the lists table
func ( l * List ) TableName ( ) string {
return "lists"
}
2020-06-11 19:31:37 +02:00
// ListBackgroundType holds a list background type
type ListBackgroundType struct {
Type string
}
// ListBackgroundUpload represents the list upload background type
const ListBackgroundUpload string = "upload"
2020-09-05 22:16:02 +02:00
// FavoritesPseudoList holds all tasks marked as favorites
var FavoritesPseudoList = List {
ID : - 1 ,
Title : "Favorites" ,
Description : "This list has all tasks marked as favorites." ,
NamespaceID : FavoritesPseudoNamespace . ID ,
2020-09-06 16:20:16 +02:00
IsFavorite : true ,
2020-09-05 22:16:02 +02:00
Created : time . Now ( ) ,
Updated : time . Now ( ) ,
}
2018-07-10 14:02:23 +02:00
// GetListsByNamespaceID gets all lists in a namespace
2020-12-23 16:32:28 +01:00
func GetListsByNamespaceID ( s * xorm . Session , nID int64 , doer * user . User ) ( lists [ ] * List , err error ) {
2021-03-18 21:39:38 +01:00
switch nID {
case SharedListsPseudoNamespace . ID :
nnn , err := getSharedListsInNamespace ( s , false , doer )
if err != nil {
return nil , err
}
if nnn != nil && nnn . Lists != nil {
lists = nnn . Lists
}
case FavoritesPseudoNamespace . ID :
namespaces := make ( map [ int64 ] * NamespaceWithLists )
_ , err := getNamespacesWithLists ( s , & namespaces , "" , false , 0 , - 1 , doer . ID )
if err != nil {
return nil , err
}
namespaceIDs , _ := getNamespaceOwnerIDs ( namespaces )
ls , err := getListsForNamespaces ( s , namespaceIDs , false )
if err != nil {
return nil , err
}
nnn , err := getFavoriteLists ( s , ls , namespaceIDs , doer )
if err != nil {
return nil , err
}
if nnn != nil && nnn . Lists != nil {
lists = nnn . Lists
}
case SavedFiltersPseudoNamespace . ID :
nnn , err := getSavedFilters ( s , doer )
if err != nil {
return nil , err
}
if nnn != nil && nnn . Lists != nil {
lists = nnn . Lists
}
default :
2020-12-23 16:32:28 +01:00
err = s . Select ( "l.*" ) .
2020-03-15 22:50:39 +01:00
Alias ( "l" ) .
Join ( "LEFT" , [ ] string { "namespaces" , "n" } , "l.namespace_id = n.id" ) .
Where ( "l.is_archived = false" ) .
Where ( "n.is_archived = false" ) .
Where ( "namespace_id = ?" , nID ) .
Find ( & lists )
2018-12-04 11:16:42 +01:00
}
2019-02-18 20:32:41 +01:00
if err != nil {
return nil , err
}
2018-12-04 11:16:42 +01:00
// get more list details
2021-07-10 12:21:54 +02:00
err = addListDetails ( s , lists , doer )
2018-07-04 08:56:52 +02:00
return lists , err
2018-07-03 08:48:28 +02:00
}
2018-07-08 22:50:01 +02:00
2018-07-11 13:00:00 +02:00
// ReadAll gets all lists a user has access to
2018-11-12 16:46:35 +01:00
// @Summary Get all lists a user has access to
// @Description Returns all lists a user has access to.
// @tags list
// @Accept json
// @Produce json
2019-10-23 23:11:40 +02:00
// @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."
2018-11-12 16:46:35 +01:00
// @Param s query string false "Search lists by title."
2020-03-15 22:50:39 +01:00
// @Param is_archived query bool false "If true, also returns all archived lists."
2019-01-03 23:22:06 +01:00
// @Security JWTKeyAuth
2018-11-12 16:46:35 +01:00
// @Success 200 {array} models.List "The lists"
2020-06-28 16:25:46 +02:00
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
2018-11-12 16:46:35 +01:00
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists [get]
2020-12-23 16:32:28 +01:00
func ( l * List ) ReadAll ( s * xorm . Session , a web . Auth , search string , page int , perPage int ) ( result interface { } , resultCount int , totalItems int64 , err error ) {
2019-08-31 22:56:41 +02:00
// Check if we're dealing with a share auth
shareAuth , ok := a . ( * LinkSharing )
if ok {
2020-12-23 16:32:28 +01:00
list , err := GetListSimpleByID ( s , shareAuth . ListID )
2019-08-31 22:56:41 +02:00
if err != nil {
2019-10-23 23:11:40 +02:00
return nil , 0 , 0 , err
2019-08-31 22:56:41 +02:00
}
2019-09-07 15:19:23 +02:00
lists := [ ] * List { list }
2021-07-10 12:21:54 +02:00
err = addListDetails ( s , lists , a )
2019-10-23 23:11:40 +02:00
return lists , 0 , 0 , err
2019-08-31 22:56:41 +02:00
}
2020-12-23 16:32:28 +01:00
lists , resultCount , totalItems , err := getRawListsForUser (
s ,
& listOptions {
search : search ,
user : & user . User { ID : a . GetID ( ) } ,
page : page ,
perPage : perPage ,
isArchived : l . IsArchived ,
} )
2018-07-08 22:50:01 +02:00
if err != nil {
2019-10-23 23:11:40 +02:00
return nil , 0 , 0 , err
2018-07-08 22:50:01 +02:00
}
2018-10-05 19:16:14 +02:00
// Add more list details
2021-07-10 12:21:54 +02:00
err = addListDetails ( s , lists , a )
2019-10-23 23:11:40 +02:00
return lists , resultCount , totalItems , err
2018-07-08 22:50:01 +02:00
}
2018-07-09 19:49:27 +02:00
// ReadOne gets one list by its ID
2018-11-12 16:46:35 +01:00
// @Summary Gets one list
// @Description Returns a list by its ID.
// @tags list
// @Accept json
// @Produce json
2019-01-03 23:22:06 +01:00
// @Security JWTKeyAuth
2018-11-12 16:46:35 +01:00
// @Param id path int true "List ID"
// @Success 200 {object} models.List "The list"
2020-06-28 16:25:46 +02:00
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
2018-11-12 16:46:35 +01:00
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{id} [get]
2021-02-02 23:48:37 +01:00
func ( l * List ) ReadOne ( s * xorm . Session , a web . Auth ) ( err error ) {
2020-09-05 22:16:02 +02:00
if l . ID == FavoritesPseudoList . ID {
// Already "built" the list in CanRead
return nil
}
2020-09-26 23:02:17 +02:00
// Check for saved filters
if getSavedFilterIDFromListID ( l . ID ) > 0 {
2020-12-23 16:32:28 +01:00
sf , err := getSavedFilterSimpleByID ( s , getSavedFilterIDFromListID ( l . ID ) )
2020-09-26 23:02:17 +02:00
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
}
2018-10-06 13:05:29 +02:00
// Get list owner
2020-12-23 16:32:28 +01:00
l . Owner , err = user . GetUserByID ( s , l . OwnerID )
2020-03-22 21:39:57 +01:00
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 {
2020-12-23 16:32:28 +01:00
err = l . CheckIsArchived ( s )
2020-03-22 21:39:57 +01:00
if err != nil {
if ! IsErrNamespaceIsArchived ( err ) && ! IsErrListIsArchived ( err ) {
return
}
l . IsArchived = true
}
}
2020-05-26 22:07:55 +02:00
// Get any background information if there is one set
if l . BackgroundFileID != 0 {
2020-06-11 19:31:37 +02:00
// Unsplash image
2020-12-23 16:32:28 +01:00
l . BackgroundInformation , err = GetUnsplashPhotoByFileID ( s , l . BackgroundFileID )
2020-05-26 22:07:55 +02:00
if err != nil && ! files . IsErrFileIsNotUnsplashFile ( err ) {
return
}
2020-06-11 19:31:37 +02:00
if err != nil && files . IsErrFileIsNotUnsplashFile ( err ) {
l . BackgroundInformation = & ListBackgroundType { Type : ListBackgroundUpload }
}
2020-05-26 22:07:55 +02:00
}
2021-07-10 12:21:54 +02:00
l . IsFavorite , err = isFavorite ( s , l . ID , a , FavoriteKindList )
if err != nil {
return
}
2021-02-14 20:18:14 +01:00
l . Subscription , err = GetSubscription ( s , SubscriptionEntityList , l . ID , a )
return
2018-10-06 13:05:29 +02:00
}
2020-12-23 16:32:28 +01:00
// GetListSimpleByID gets a list with only the basic items, aka no tasks or user objects. Returns an error if the list does not exist.
func GetListSimpleByID ( s * xorm . Session , listID int64 ) ( list * List , err error ) {
list = & List { }
2020-08-01 18:54:38 +02:00
2020-12-23 16:32:28 +01:00
if listID < 1 {
return nil , ErrListDoesNotExist { ID : listID }
2018-10-06 13:05:29 +02:00
}
2021-07-28 21:06:40 +02:00
exists , err := s .
Where ( "id = ?" , listID ) .
OrderBy ( "position" ) .
Get ( list )
2018-10-06 13:05:29 +02:00
if err != nil {
return
}
if ! exists {
2020-12-23 16:32:28 +01:00
return nil , ErrListDoesNotExist { ID : listID }
2018-10-06 13:05:29 +02:00
}
2018-07-08 22:50:01 +02:00
return
}
2018-10-05 19:16:14 +02:00
2019-01-08 20:13:07 +01:00
// GetListSimplByTaskID gets a list by a task id
2020-12-23 16:32:28 +01:00
func GetListSimplByTaskID ( s * xorm . Session , taskID int64 ) ( l * List , err error ) {
2019-01-08 20:13:07 +01:00
// We need to re-init our list object, because otherwise xorm creates a "where for every item in that list object,
// leading to not finding anything if the id is good, but for example the title is different.
var list List
2020-12-23 16:32:28 +01:00
exists , err := s .
2021-03-28 20:17:35 +02:00
Select ( "lists.*" ) .
2019-01-08 20:13:07 +01:00
Table ( List { } ) .
2021-03-28 20:17:35 +02:00
Join ( "INNER" , "tasks" , "lists.id = tasks.list_id" ) .
2019-01-08 20:13:07 +01:00
Where ( "tasks.id = ?" , taskID ) .
Get ( & list )
if err != nil {
return
}
if ! exists {
return & List { } , ErrListDoesNotExist { ID : l . ID }
}
return & list , nil
}
2020-12-18 14:54:36 +01:00
// GetListsByIDs returns a map of lists from a slice with list ids
2020-12-23 16:32:28 +01:00
func GetListsByIDs ( s * xorm . Session , listIDs [ ] int64 ) ( lists map [ int64 ] * List , err error ) {
2020-12-18 14:54:36 +01:00
lists = make ( map [ int64 ] * List , len ( listIDs ) )
2021-03-02 18:40:39 +01:00
if len ( listIDs ) == 0 {
return
}
2020-12-23 16:32:28 +01:00
err = s . In ( "id" , listIDs ) . Find ( & lists )
2020-12-18 14:54:36 +01:00
return
}
2020-03-15 22:50:39 +01:00
type listOptions struct {
search string
user * user . User
page int
perPage int
isArchived bool
}
2021-03-24 22:46:20 +01:00
func getUserListsStatement ( userID int64 ) * builder . Builder {
dialect := config . DatabaseType . GetString ( )
if dialect == "sqlite" {
dialect = builder . SQLITE
}
return builder . Dialect ( dialect ) .
Select ( "l.*" ) .
2021-03-28 20:17:35 +02:00
From ( "lists" , "l" ) .
2021-03-24 22:46:20 +01:00
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" ) .
2021-03-28 20:17:35 +02:00
Join ( "LEFT" , "team_lists tl" , "l.id = tl.list_id" ) .
2021-03-24 22:46:20 +01:00
Join ( "LEFT" , "team_members tm2" , "tm2.team_id = tl.team_id" ) .
2021-03-28 20:17:35 +02:00
Join ( "LEFT" , "users_lists ul" , "ul.list_id = l.id" ) .
Join ( "LEFT" , "users_namespaces un" , "un.namespace_id = l.namespace_id" ) .
2021-03-24 22:46:20 +01:00
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 } ,
) ) .
2021-07-28 21:06:40 +02:00
OrderBy ( "position" ) .
2021-03-24 22:46:20 +01:00
GroupBy ( "l.id" )
}
2018-11-02 17:59:49 +01:00
// Gets the lists only, without any tasks or so
2020-12-23 16:32:28 +01:00
func getRawListsForUser ( s * xorm . Session , opts * listOptions ) ( lists [ ] * List , resultCount int , totalItems int64 , err error ) {
fullUser , err := user . GetUserByID ( s , opts . user . ID )
2018-11-02 17:59:49 +01:00
if err != nil {
2019-10-23 23:11:40 +02:00
return nil , 0 , 0 , err
2018-11-02 17:59:49 +01:00
}
2020-03-15 22:50:39 +01:00
// 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 (
builder . Eq { "l.is_archived" : false } ,
builder . Eq { "n.is_archived" : false } ,
)
}
2020-04-12 19:29:24 +02:00
limit , start := getLimitFromPageIndex ( opts . page , opts . perPage )
2020-12-22 00:13:15 +01:00
var filterCond builder . Cond
ids := [ ] int64 { }
2021-03-28 20:17:35 +02:00
if opts . search != "" {
vals := strings . Split ( opts . search , "," )
for _ , val := range vals {
v , err := strconv . ParseInt ( val , 10 , 64 )
if err != nil {
log . Debugf ( "List search string part '%s' is not a number: %s" , val , err )
continue
}
ids = append ( ids , v )
2020-12-22 00:13:15 +01:00
}
}
2021-08-01 23:40:25 +02:00
filterCond = db . ILIKE ( "l.title" , opts . search )
2020-12-22 00:13:15 +01:00
if len ( ids ) > 0 {
filterCond = builder . In ( "l.id" , ids )
}
2018-11-02 17:59:49 +01:00
// Gets all Lists where the user is either owner or in a team which has access to the list
// Or in a team which has namespace read access
2021-03-24 22:46:20 +01:00
query := getUserListsStatement ( fullUser . ID ) .
2020-12-22 00:13:15 +01:00
Where ( filterCond ) .
2020-04-12 19:29:24 +02:00
Where ( isArchivedCond )
if limit > 0 {
query = query . Limit ( limit , start )
}
2021-03-24 22:46:20 +01:00
err = s . SQL ( query ) . Find ( & lists )
2019-10-23 23:11:40 +02:00
if err != nil {
return nil , 0 , 0 , err
}
2018-11-02 17:59:49 +01:00
2021-03-24 22:46:20 +01:00
query = getUserListsStatement ( fullUser . ID ) .
2020-12-22 00:13:15 +01:00
Where ( filterCond ) .
2021-03-24 22:46:20 +01:00
Where ( isArchivedCond )
totalItems , err = s .
SQL ( query . Select ( "count(*)" ) ) .
2019-10-23 23:11:40 +02:00
Count ( & List { } )
return lists , len ( lists ) , totalItems , err
2018-11-02 17:59:49 +01:00
}
2020-12-23 16:32:28 +01:00
// addListDetails adds owner user objects and list tasks to all lists in the slice
2021-07-10 12:21:54 +02:00
func addListDetails ( s * xorm . Session , lists [ ] * List , a web . Auth ) ( err error ) {
2021-02-02 23:48:37 +01:00
if len ( lists ) == 0 {
return
}
2018-10-05 19:16:14 +02:00
var ownerIDs [ ] int64
for _ , l := range lists {
ownerIDs = append ( ownerIDs , l . OwnerID )
}
// Get all list owners
2020-06-16 18:57:08 +02:00
owners := map [ int64 ] * user . User { }
2021-03-02 18:40:39 +01:00
if len ( ownerIDs ) > 0 {
err = s . In ( "id" , ownerIDs ) . Find ( & owners )
if err != nil {
return
}
2018-10-05 19:16:14 +02:00
}
2020-06-16 18:57:08 +02:00
var fileIDs [ ] int64
2021-07-10 12:21:54 +02:00
var listIDs [ ] int64
2020-06-16 18:57:08 +02:00
for _ , l := range lists {
2021-07-10 12:21:54 +02:00
listIDs = append ( listIDs , l . ID )
2020-06-16 18:57:08 +02:00
if o , exists := owners [ l . OwnerID ] ; exists {
l . Owner = o
2018-10-05 19:16:14 +02:00
}
2020-06-16 18:57:08 +02:00
if l . BackgroundFileID != 0 {
l . BackgroundInformation = & ListBackgroundType { Type : ListBackgroundUpload }
}
fileIDs = append ( fileIDs , l . BackgroundFileID )
}
2021-07-10 12:21:54 +02:00
favs , err := getFavorites ( s , listIDs , a , FavoriteKindList )
if err != nil {
return err
}
for _ , list := range lists {
2021-07-20 21:32:25 +02:00
// Don't override the favorite state if it was already set from before (favorite saved filters do this)
if list . IsFavorite {
continue
}
2021-07-10 12:21:54 +02:00
list . IsFavorite = favs [ list . ID ]
}
2021-02-02 23:48:37 +01:00
if len ( fileIDs ) == 0 {
return
}
2020-06-16 18:57:08 +02:00
// Unsplash background file info
us := [ ] * UnsplashPhoto { }
2020-12-23 16:32:28 +01:00
err = s . In ( "file_id" , fileIDs ) . Find ( & us )
2020-06-16 18:57:08 +02:00
if err != nil {
return
}
unsplashPhotos := make ( map [ int64 ] * UnsplashPhoto , len ( us ) )
for _ , u := range us {
unsplashPhotos [ u . FileID ] = u
}
// Build it all into the lists slice
for _ , l := range lists {
2020-09-28 20:53:17 +02:00
// Only override the file info if we have info for unsplash backgrounds
if _ , exists := unsplashPhotos [ l . BackgroundFileID ] ; exists {
l . BackgroundInformation = unsplashPhotos [ l . BackgroundFileID ]
}
2018-10-05 19:16:14 +02:00
}
return
}
2019-07-16 16:15:40 +02:00
2020-03-15 22:50:39 +01:00
// NamespaceList is a meta type to be able to join a list with its namespace
type NamespaceList struct {
List List ` xorm:"extends" `
Namespace Namespace ` xorm:"extends" `
}
// CheckIsArchived returns an ErrListIsArchived or ErrNamespaceIsArchived if the list or its namespace is archived.
2020-12-23 16:32:28 +01:00
func ( l * List ) CheckIsArchived ( s * xorm . Session ) ( err error ) {
2020-03-15 22:50:39 +01:00
// When creating a new list, we check if the namespace is archived
if l . ID == 0 {
n := & Namespace { ID : l . NamespaceID }
2020-12-23 16:32:28 +01:00
return n . CheckIsArchived ( s )
2020-03-15 22:50:39 +01:00
}
nl := & NamespaceList { }
2020-12-23 16:32:28 +01:00
exists , err := s .
2021-03-28 20:17:35 +02:00
Table ( "lists" ) .
Join ( "LEFT" , "namespaces" , "lists.namespace_id = namespaces.id" ) .
Where ( "lists.id = ? AND (lists.is_archived = true OR namespaces.is_archived = true)" , l . ID ) .
2020-03-15 22:50:39 +01:00
Get ( nl )
if err != nil {
return
}
if exists && nl . List . ID != 0 && nl . List . IsArchived {
return ErrListIsArchived { ListID : l . ID }
}
if exists && nl . Namespace . ID != 0 && nl . Namespace . IsArchived {
return ErrNamespaceIsArchived { NamespaceID : nl . Namespace . ID }
}
return nil
}
2019-07-16 16:15:40 +02:00
// CreateOrUpdateList updates a list or creates it if it doesn't exist
2021-02-02 23:48:37 +01:00
func CreateOrUpdateList ( s * xorm . Session , list * List , auth web . Auth ) ( err error ) {
2019-07-16 16:15:40 +02:00
// Check if the namespace exists
2021-10-16 16:50:16 +02:00
if list . NamespaceID > 0 {
2020-12-23 16:32:28 +01:00
_ , err = GetNamespaceByID ( s , list . NamespaceID )
2019-07-16 16:15:40 +02:00
if err != nil {
return err
}
}
2019-12-07 23:28:45 +01:00
// Check if the identifier is unique and not empty
if list . Identifier != "" {
2020-12-23 16:32:28 +01:00
exists , err := s .
2020-03-22 18:45:10 +01:00
Where ( "identifier = ?" , list . Identifier ) .
And ( "id != ?" , list . ID ) .
Exist ( & List { } )
2019-12-07 23:28:45 +01:00
if err != nil {
return err
}
if exists {
return ErrListIdentifierIsNotUnique { Identifier : list . Identifier }
}
}
2019-07-16 16:15:40 +02:00
if list . ID == 0 {
2020-12-23 16:32:28 +01:00
_ , err = s . Insert ( list )
2021-07-10 12:21:54 +02:00
if err != nil {
return
}
2021-07-28 21:06:40 +02:00
list . Position = calculateDefaultPosition ( list . ID , list . Position )
_ , err = s . Where ( "id = ?" , list . ID ) . Update ( list )
if err != nil {
return
}
2021-07-10 12:21:54 +02:00
if list . IsFavorite {
if err := addToFavorites ( s , list . ID , auth , FavoriteKindList ) ; err != nil {
return err
}
}
2019-07-16 16:15:40 +02:00
} else {
2020-03-15 22:50:39 +01:00
// We need to specify the cols we want to update here to be able to un-archive lists
colsToUpdate := [ ] string {
"title" ,
"is_archived" ,
2020-05-16 13:13:11 +02:00
"identifier" ,
2020-06-15 11:32:07 +02:00
"hex_color" ,
2021-03-21 17:49:14 +01:00
"background_file_id" ,
2021-07-28 21:06:40 +02:00
"position" ,
2020-03-15 22:50:39 +01:00
}
if list . Description != "" {
colsToUpdate = append ( colsToUpdate , "description" )
}
2021-07-10 12:21:54 +02:00
wasFavorite , err := isFavorite ( s , list . ID , auth , FavoriteKindList )
if err != nil {
return err
}
if list . IsFavorite && ! wasFavorite {
if err := addToFavorites ( s , list . ID , auth , FavoriteKindList ) ; err != nil {
return err
}
}
if ! list . IsFavorite && wasFavorite {
if err := removeFromFavorite ( s , list . ID , auth , FavoriteKindList ) ; err != nil {
return err
}
}
2020-12-23 16:32:28 +01:00
_ , err = s .
2020-03-15 22:50:39 +01:00
ID ( list . ID ) .
Cols ( colsToUpdate ... ) .
Update ( list )
2021-07-10 12:21:54 +02:00
if err != nil {
return err
}
2019-07-16 16:15:40 +02:00
}
2020-12-23 16:32:28 +01:00
l , err := GetListSimpleByID ( s , list . ID )
2019-07-16 16:15:40 +02:00
if err != nil {
2020-12-23 16:32:28 +01:00
return err
2019-07-16 16:15:40 +02:00
}
2020-12-23 16:32:28 +01:00
* list = * l
2021-02-02 23:48:37 +01:00
err = list . ReadOne ( s , auth )
2019-07-16 16:15:40 +02:00
return
}
// Update implements the update method of CRUDable
// @Summary Updates a list
// @Description Updates a list. This does not include adding a task (see below).
// @tags list
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param id path int true "List ID"
// @Param list body models.List true "The list with updated values you want to update."
// @Success 200 {object} models.List "The updated list."
2020-06-28 16:25:46 +02:00
// @Failure 400 {object} web.HTTPError "Invalid list object provided."
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
2019-07-16 16:15:40 +02:00
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{id} [post]
2021-02-02 23:48:37 +01:00
func ( l * List ) Update ( s * xorm . Session , a web . Auth ) ( err error ) {
2021-04-03 16:49:20 +02:00
fid := getSavedFilterIDFromListID ( l . ID )
if fid > 0 {
f , err := getSavedFilterSimpleByID ( s , fid )
if err != nil {
return err
}
f . Title = l . Title
f . Description = l . Description
2021-07-20 21:32:25 +02:00
f . IsFavorite = l . IsFavorite
2021-04-03 16:49:20 +02:00
err = f . Update ( s , a )
if err != nil {
return err
}
* l = * f . toList ( )
return nil
}
2021-02-02 23:48:37 +01:00
err = CreateOrUpdateList ( s , l , a )
if err != nil {
return err
}
return events . Dispatch ( & ListUpdatedEvent {
List : l ,
Doer : a ,
} )
2020-08-01 18:54:38 +02:00
}
2020-12-23 16:32:28 +01:00
func updateListLastUpdated ( s * xorm . Session , list * List ) error {
2020-08-01 18:54:38 +02:00
_ , err := s . ID ( list . ID ) . Cols ( "updated" ) . Update ( list )
2019-07-16 16:15:40 +02:00
return err
}
2020-12-23 16:32:28 +01:00
func updateListByTaskID ( s * xorm . Session , taskID int64 ) ( err error ) {
2019-07-16 16:15:40 +02:00
// need to get the task to update the list last updated timestamp
2020-12-23 16:32:28 +01:00
task , err := GetTaskByIDSimple ( s , taskID )
2019-07-16 16:15:40 +02:00
if err != nil {
return err
}
2020-12-23 16:32:28 +01:00
return updateListLastUpdated ( s , & List { ID : task . ListID } )
2019-07-16 16:15:40 +02:00
}
// Create implements the create method of CRUDable
// @Summary Creates a new list
// @Description Creates a new list in a given namespace. The user needs write-access to the namespace.
// @tags list
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param namespaceID path int true "Namespace ID"
// @Param list body models.List true "The list you want to create."
2021-05-26 21:56:31 +02:00
// @Success 201 {object} models.List "The created list."
2020-06-28 16:25:46 +02:00
// @Failure 400 {object} web.HTTPError "Invalid list object provided."
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
2019-07-16 16:15:40 +02:00
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{namespaceID}/lists [put]
2020-12-23 16:32:28 +01:00
func ( l * List ) Create ( s * xorm . Session , a web . Auth ) ( err error ) {
err = l . CheckIsArchived ( s )
2020-03-15 22:50:39 +01:00
if err != nil {
return err
}
2020-01-26 18:08:06 +01:00
doer , err := user . GetFromAuth ( a )
2019-07-16 16:15:40 +02:00
if err != nil {
return err
}
l . OwnerID = doer . ID
2019-08-14 21:59:31 +02:00
l . Owner = doer
2019-07-16 16:15:40 +02:00
l . ID = 0 // Otherwise only the first time a new list would be created
2021-02-02 23:48:37 +01:00
err = CreateOrUpdateList ( s , l , a )
2020-04-26 15:51:59 +02:00
if err != nil {
return
}
// Create a new first bucket for this list
b := & Bucket {
ListID : l . ID ,
2021-02-04 20:48:07 +01:00
Title : "Backlog" ,
2020-04-26 15:51:59 +02:00
}
2021-02-02 23:48:37 +01:00
err = b . Create ( s , a )
if err != nil {
return
}
return events . Dispatch ( & ListCreatedEvent {
List : l ,
2021-02-14 20:18:14 +01:00
Doer : doer ,
2021-02-02 23:48:37 +01:00
} )
2019-07-16 16:15:40 +02:00
}
// Delete implements the delete method of CRUDable
// @Summary Deletes a list
// @Description Delets a list
// @tags list
// @Produce json
// @Security JWTKeyAuth
// @Param id path int true "List ID"
// @Success 200 {object} models.Message "The list was successfully deleted."
2020-06-28 16:25:46 +02:00
// @Failure 400 {object} web.HTTPError "Invalid list object provided."
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
2019-07-16 16:15:40 +02:00
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{id} [delete]
2021-02-02 23:48:37 +01:00
func ( l * List ) Delete ( s * xorm . Session , a web . Auth ) ( err error ) {
2019-07-16 16:15:40 +02:00
// Delete the list
2020-12-23 16:32:28 +01:00
_ , err = s . ID ( l . ID ) . Delete ( & List { } )
2019-07-16 16:15:40 +02:00
if err != nil {
return
}
2021-02-02 23:48:37 +01:00
// Delete all tasks on that list
2021-08-11 21:08:10 +02:00
// Using the loop to make sure all related entities to all tasks are properly deleted as well.
tasks , _ , _ , err := getRawTasksForLists ( s , [ ] * List { l } , a , & taskOptions { } )
2021-02-02 23:48:37 +01:00
if err != nil {
return
}
2021-08-11 21:08:10 +02:00
for _ , task := range tasks {
err = task . Delete ( s , a )
if err != nil {
return err
}
}
2021-02-02 23:48:37 +01:00
return events . Dispatch ( & ListDeletedEvent {
List : l ,
Doer : a ,
} )
2019-07-16 16:15:40 +02:00
}
2020-05-26 22:07:55 +02:00
// SetListBackground sets a background file as list background in the db
2020-12-23 16:32:28 +01:00
func SetListBackground ( s * xorm . Session , listID int64 , background * files . File ) ( err error ) {
2020-05-26 22:07:55 +02:00
l := & List {
ID : listID ,
BackgroundFileID : background . ID ,
}
2020-12-23 16:32:28 +01:00
_ , err = s .
2020-05-26 22:07:55 +02:00
Where ( "id = ?" , l . ID ) .
Cols ( "background_file_id" ) .
Update ( l )
return
}