2020-02-07 17:27:45 +01:00
// Vikunja is a to-do list application to facilitate your life.
2020-01-09 18:33:22 +01:00
// Copyright 2018-2020 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
// it under the terms of the GNU General Public License as published by
// 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
// GNU General Public License for more details.
2018-11-26 21:17:33 +01:00
//
2019-12-04 20:39:56 +01:00
// You should have received a copy of the GNU General Public License
// 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-10-11 22:10:03 +02:00
"strings"
"time"
2020-05-26 22:07:55 +02:00
"code.vikunja.io/api/pkg/files"
2019-07-16 16:15:40 +02:00
"code.vikunja.io/api/pkg/metrics"
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.
ID int64 ` xorm:"int(11) autoincr not null unique pk" json:"id" param:"list" `
// 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
OwnerID int64 ` xorm:"int(11) INDEX not null" json:"-" `
2020-06-27 22:59:58 +02:00
NamespaceID int64 ` xorm:"int(11) 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:"-" `
2019-01-03 23:22:06 +01:00
// An array of tasks which belong to the list.
2019-12-01 14:38:11 +01:00
// Deprecated: you should use the dedicated task list endpoint because it has support for pagination and filtering
Tasks [ ] * Task ` xorm:"-" json:"-" `
2018-07-04 08:15:47 +02: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" `
2020-09-06 16:20:16 +02:00
// True if a list is a favorite. Favorite lists show up in a separate namespace.
IsFavorite bool ` xorm:"default false" json:"is_favorite" `
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
}
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-01-26 18:08:06 +01:00
func GetListsByNamespaceID ( nID int64 , doer * user . User ) ( lists [ ] * List , err error ) {
2018-12-04 11:16:42 +01:00
if nID == - 1 {
err = x . Select ( "l.*" ) .
Table ( "list" ) .
Join ( "LEFT" , [ ] string { "team_list" , "tl" } , "l.id = tl.list_id" ) .
Join ( "LEFT" , [ ] string { "team_members" , "tm" } , "tm.team_id = tl.team_id" ) .
Join ( "LEFT" , [ ] string { "users_list" , "ul" } , "ul.list_id = l.id" ) .
2020-03-15 22:50:39 +01:00
Join ( "LEFT" , [ ] string { "namespaces" , "n" } , "l.namespace_id = n.id" ) .
2018-12-04 11:16:42 +01:00
Where ( "tm.user_id = ?" , doer . ID ) .
2020-03-15 22:50:39 +01:00
Where ( "l.is_archived = false" ) .
Where ( "n.is_archived = false" ) .
2018-12-04 11:16:42 +01:00
Or ( "ul.user_id = ?" , doer . ID ) .
GroupBy ( "l.id" ) .
Find ( & lists )
} else {
2020-03-15 22:50:39 +01:00
err = x . Select ( "l.*" ) .
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
err = AddListDetails ( lists )
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]
2019-10-23 23:11:40 +02:00
func ( l * List ) ReadAll ( 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 {
2019-09-07 15:19:23 +02:00
list := & List { ID : shareAuth . ListID }
err := list . GetSimpleByID ( )
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 }
2019-08-31 22:56:41 +02:00
err = AddListDetails ( lists )
2019-10-23 23:11:40 +02:00
return lists , 0 , 0 , err
2019-08-31 22:56:41 +02:00
}
2020-03-15 22:50:39 +01:00
lists , resultCount , totalItems , err := getRawListsForUser ( & 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
2019-08-31 22:56:41 +02:00
err = AddListDetails ( lists )
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]
2018-07-21 15:08:46 +02:00
func ( l * List ) ReadOne ( ) ( 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 {
sf , err := getSavedFilterSimpleByID ( getSavedFilterIDFromListID ( l . 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
}
2018-10-06 13:05:29 +02:00
// Get list owner
2020-01-26 18:08:06 +01:00
l . Owner , err = user . GetUserByID ( 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 {
err = l . CheckIsArchived ( )
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-05-26 22:07:55 +02:00
l . BackgroundInformation , err = GetUnsplashPhotoByFileID ( l . BackgroundFileID )
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
}
2020-03-22 21:39:57 +01:00
return nil
2018-10-06 13:05:29 +02:00
}
2018-10-06 13:07:42 +02:00
// GetSimpleByID gets a list with only the basic items, aka no tasks or user objects. Returns an error if the list does not exist.
2018-10-06 13:05:29 +02:00
func ( l * List ) GetSimpleByID ( ) ( err error ) {
2020-08-01 18:54:38 +02:00
s := x . NewSession ( )
err = l . getSimpleByID ( s )
if err != nil {
_ = s . Rollback ( )
return err
}
return nil
}
func ( l * List ) getSimpleByID ( s * xorm . Session ) ( err error ) {
2018-10-06 13:05:29 +02:00
if l . ID < 1 {
return ErrListDoesNotExist { ID : l . ID }
}
// 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.
id := l . ID
* l = List { }
2020-08-01 18:54:38 +02:00
exists , err := s . Where ( "id = ?" , id ) . Get ( l )
2018-10-06 13:05:29 +02:00
if err != nil {
return
}
if ! exists {
return ErrListDoesNotExist { ID : l . ID }
}
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
func GetListSimplByTaskID ( taskID int64 ) ( l * List , err error ) {
// 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
exists , err := x .
Select ( "list.*" ) .
Table ( List { } ) .
Join ( "INNER" , "tasks" , "list.id = tasks.list_id" ) .
Where ( "tasks.id = ?" , taskID ) .
Get ( & list )
if err != nil {
return
}
if ! exists {
return & List { } , ErrListDoesNotExist { ID : l . ID }
}
return & list , nil
}
2020-03-15 22:50:39 +01:00
type listOptions struct {
search string
user * user . User
page int
perPage int
isArchived bool
}
2018-11-02 17:59:49 +01:00
// Gets the lists only, without any tasks or so
2020-03-15 22:50:39 +01:00
func getRawListsForUser ( opts * listOptions ) ( lists [ ] * List , resultCount int , totalItems int64 , err error ) {
fullUser , err := user . GetUserByID ( 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 )
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
2020-04-12 19:29:24 +02:00
query := x . Select ( "l.*" ) .
2018-11-02 17:59:49 +01:00
Table ( "list" ) .
Alias ( "l" ) .
Join ( "INNER" , [ ] string { "namespaces" , "n" } , "l.namespace_id = n.id" ) .
Join ( "LEFT" , [ ] string { "team_namespaces" , "tn" } , "tn.namespace_id = n.id" ) .
Join ( "LEFT" , [ ] string { "team_members" , "tm" } , "tm.team_id = tn.team_id" ) .
Join ( "LEFT" , [ ] string { "team_list" , "tl" } , "l.id = tl.list_id" ) .
Join ( "LEFT" , [ ] string { "team_members" , "tm2" } , "tm2.team_id = tl.team_id" ) .
Join ( "LEFT" , [ ] string { "users_list" , "ul" } , "ul.list_id = l.id" ) .
Join ( "LEFT" , [ ] string { "users_namespace" , "un" } , "un.namespace_id = l.namespace_id" ) .
2020-04-12 19:29:24 +02:00
Where ( builder . Or (
builder . Eq { "tm.user_id" : fullUser . ID } ,
builder . Eq { "tm2.user_id" : fullUser . ID } ,
builder . Eq { "ul.user_id" : fullUser . ID } ,
builder . Eq { "un.user_id" : fullUser . ID } ,
builder . Eq { "l.owner_id" : fullUser . ID } ,
) ) .
2018-11-02 17:59:49 +01:00
GroupBy ( "l.id" ) .
2020-03-15 22:50:39 +01:00
Where ( "l.title LIKE ?" , "%" + opts . search + "%" ) .
2020-04-12 19:29:24 +02:00
Where ( isArchivedCond )
if limit > 0 {
query = query . Limit ( limit , start )
}
err = 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
2019-10-23 23:11:40 +02:00
totalItems , err = x .
Table ( "list" ) .
Alias ( "l" ) .
Join ( "INNER" , [ ] string { "namespaces" , "n" } , "l.namespace_id = n.id" ) .
Join ( "LEFT" , [ ] string { "team_namespaces" , "tn" } , "tn.namespace_id = n.id" ) .
Join ( "LEFT" , [ ] string { "team_members" , "tm" } , "tm.team_id = tn.team_id" ) .
Join ( "LEFT" , [ ] string { "team_list" , "tl" } , "l.id = tl.list_id" ) .
Join ( "LEFT" , [ ] string { "team_members" , "tm2" } , "tm2.team_id = tl.team_id" ) .
Join ( "LEFT" , [ ] string { "users_list" , "ul" } , "ul.list_id = l.id" ) .
Join ( "LEFT" , [ ] string { "users_namespace" , "un" } , "un.namespace_id = l.namespace_id" ) .
2020-04-12 19:29:24 +02:00
Where ( builder . Or (
builder . Eq { "tm.user_id" : fullUser . ID } ,
builder . Eq { "tm2.user_id" : fullUser . ID } ,
builder . Eq { "ul.user_id" : fullUser . ID } ,
builder . Eq { "un.user_id" : fullUser . ID } ,
builder . Eq { "l.owner_id" : fullUser . ID } ,
) ) .
2019-10-23 23:11:40 +02:00
GroupBy ( "l.id" ) .
2020-03-15 22:50:39 +01:00
Where ( "l.title LIKE ?" , "%" + opts . search + "%" ) .
Where ( isArchivedCond ) .
2019-10-23 23:11:40 +02:00
Count ( & List { } )
return lists , len ( lists ) , totalItems , err
2018-11-02 17:59:49 +01:00
}
2018-10-31 13:42:38 +01:00
// AddListDetails adds owner user objects and list tasks to all lists in the slice
func AddListDetails ( lists [ ] * List ) ( err error ) {
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 { }
2018-10-05 19:16:14 +02:00
err = x . In ( "id" , ownerIDs ) . Find ( & owners )
if err != nil {
return
}
2020-06-16 18:57:08 +02:00
var fileIDs [ ] int64
for _ , l := range lists {
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 )
}
// Unsplash background file info
us := [ ] * UnsplashPhoto { }
err = x . In ( "file_id" , fileIDs ) . Find ( & us )
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.
func ( l * List ) CheckIsArchived ( ) ( err error ) {
// When creating a new list, we check if the namespace is archived
if l . ID == 0 {
n := & Namespace { ID : l . NamespaceID }
return n . CheckIsArchived ( )
}
nl := & NamespaceList { }
exists , err := x .
Table ( "list" ) .
Join ( "LEFT" , "namespaces" , "list.namespace_id = namespaces.id" ) .
Where ( "list.id = ? AND (list.is_archived = true OR namespaces.is_archived = true)" , l . ID ) .
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
}
2020-05-16 12:58:37 +02:00
// GenerateListIdentifier generates a unique random list identifier based on the list title.
// If it is not able to find one, the list identifier will be empty.
func GenerateListIdentifier ( l * List , sess * xorm . Engine ) ( err error ) {
// The general idea here is to take the title and slice it into pieces, until we found a unique piece.
var exists = true
2020-10-11 22:10:03 +02:00
titleSlug := [ ] rune ( strings . ReplaceAll ( strings . ToUpper ( l . Title ) , " " , "" ) )
2020-05-16 12:58:37 +02:00
// We can save at most 10 characters in the db, so we need to ensure it has at most 10 characters
if len ( titleSlug ) > 10 {
titleSlug = titleSlug [ 0 : 9 ]
}
var i = 0
for exists {
// Prevent endless looping
if i == len ( titleSlug ) {
break
}
// Take a random part of the title slug, starting at the beginning
2020-07-07 09:48:12 +02:00
l . Identifier = string ( titleSlug [ i : ] )
2020-05-16 12:58:37 +02:00
exists , err = sess .
Where ( "identifier = ?" , l . Identifier ) .
And ( "id != ?" , l . ID ) .
Exist ( & List { } )
if err != nil {
return
}
i ++
}
return nil
}
2019-07-16 16:15:40 +02:00
// CreateOrUpdateList updates a list or creates it if it doesn't exist
func CreateOrUpdateList ( list * List ) ( err error ) {
// Check if the namespace exists
2020-09-06 16:20:16 +02:00
if list . NamespaceID != 0 && list . NamespaceID != FavoritesPseudoNamespace . ID {
2019-07-16 16:15:40 +02:00
_ , err = GetNamespaceByID ( list . NamespaceID )
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-03-22 18:45:10 +01:00
exists , err := x .
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 }
}
}
2020-05-16 12:58:37 +02:00
// Generate a random list identifier base on the list title
if list . ID == 0 && list . Identifier == "" {
err = GenerateListIdentifier ( list , x )
if err != nil {
return
}
}
2019-07-16 16:15:40 +02:00
if list . ID == 0 {
_ , err = x . Insert ( list )
metrics . UpdateCount ( 1 , metrics . ListCountKey )
} 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" ,
2020-09-06 16:20:16 +02:00
"is_favorite" ,
2020-03-15 22:50:39 +01:00
}
if list . Description != "" {
colsToUpdate = append ( colsToUpdate , "description" )
}
_ , err = x .
ID ( list . ID ) .
Cols ( colsToUpdate ... ) .
Update ( list )
2019-07-16 16:15:40 +02:00
}
if err != nil {
return
}
err = list . GetSimpleByID ( )
if err != nil {
return
}
err = list . ReadOne ( )
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]
func ( l * List ) Update ( ) ( err error ) {
return CreateOrUpdateList ( l )
}
2020-08-01 18:54:38 +02:00
func updateListLastUpdated ( list * List ) ( err error ) {
s := x . NewSession ( )
err = updateListLastUpdatedS ( s , list )
if err != nil {
_ = s . Rollback ( )
return err
}
return nil
}
func updateListLastUpdatedS ( s * xorm . Session , list * List ) error {
_ , err := s . ID ( list . ID ) . Cols ( "updated" ) . Update ( list )
2019-07-16 16:15:40 +02:00
return err
}
func updateListByTaskID ( taskID int64 ) ( err error ) {
// need to get the task to update the list last updated timestamp
task , err := GetTaskByIDSimple ( taskID )
if err != nil {
return err
}
return updateListLastUpdated ( & List { ID : task . ListID } )
}
// 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."
// @Success 200 {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]
func ( l * List ) Create ( a web . Auth ) ( err error ) {
2020-03-15 22:50:39 +01:00
err = l . CheckIsArchived ( )
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
2020-04-26 15:51:59 +02:00
err = CreateOrUpdateList ( l )
if err != nil {
return
}
// Create a new first bucket for this list
b := & Bucket {
ListID : l . ID ,
Title : "New Bucket" ,
}
return b . Create ( a )
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]
func ( l * List ) Delete ( ) ( err error ) {
// Delete the list
_ , err = x . ID ( l . ID ) . Delete ( & List { } )
if err != nil {
return
}
metrics . UpdateCount ( - 1 , metrics . ListCountKey )
// Delete all todotasks on that list
2019-08-14 22:19:04 +02:00
_ , err = x . Where ( "list_id = ?" , l . ID ) . Delete ( & Task { } )
2019-07-16 16:15:40 +02:00
return
}
2020-05-26 22:07:55 +02:00
// SetListBackground sets a background file as list background in the db
func SetListBackground ( listID int64 , background * files . File ) ( err error ) {
l := & List {
ID : listID ,
BackgroundFileID : background . ID ,
}
_ , err = x .
Where ( "id = ?" , l . ID ) .
Cols ( "background_file_id" ) .
Update ( l )
return
}