Saved filters (#655)
Fix updating saved filters Fix filter not loadable because of missing param declaration Add fancy special cases for postgres exists in db Add special case for postgrs json Add read one test Add rights tests Fix lint Fixed getting a single saved filter from db Add tests for the usual crud methods Add test stubs and TODOs Add test for converting saved filter ids to list ids and vice versa Add test fixture for saved filters and fix existing tests Fix exposed json variables of filters Fix creating saved filters table for tests Add getting saved filters as pseudo namespace Cleanup Refactor getting all namespaces to use a map for easier handling of pseudo namespaces Add custom erros for saved filters Swagger docs Fix lint Add routes for saved filters Add alias for mage build Add method to get a saved filter from the lists endpoint Add getting tasks from a saved filter Add create, update, delete, read one methods Add rights methods for saved filters Fix docs minLength Add saved filters column Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/api/pulls/655 Co-Authored-By: konrad <konrad@kola-entertainments.de> Co-Committed-By: konrad <konrad@kola-entertainments.de>
This commit is contained in:
parent
a6fdf114d1
commit
0fb2edf051
26 changed files with 1650 additions and 119 deletions
|
@ -135,3 +135,10 @@ This document describes the different errors Vikunja can return.
|
|||
| 10002 | 400 | The bucket does not belong to that list. |
|
||||
| 10003 | 412 | You cannot remove the last bucket on a list. |
|
||||
| 10004 | 412 | You cannot add the task to this bucket as it already exceeded the limit of tasks it can hold. |
|
||||
|
||||
## Saved Filters
|
||||
|
||||
| ErrorCode | HTTP Status Code | Description |
|
||||
|-----------|------------------|-------------|
|
||||
| 11001 | 404 | The saved filter does not exist. |
|
||||
| 11002 | 412 | Saved filters are not available for link shares. |
|
||||
|
|
|
@ -57,6 +57,7 @@ var (
|
|||
|
||||
// Aliases are mage aliases of targets
|
||||
Aliases = map[string]interface{}{
|
||||
"build": Build.Build,
|
||||
"do-the-swag": DoTheSwag,
|
||||
"check:go-sec": Check.GoSec,
|
||||
"check:got-swag": Check.GotSwag,
|
||||
|
|
6
pkg/db/fixtures/saved_filters.yml
Normal file
6
pkg/db/fixtures/saved_filters.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
- id: 1
|
||||
filters: '{"sort_by":null,"order_by":null,"filter_by":["start_date","end_date","due_date"],"filter_value":["2018-12-11T03:46:40+00:00","2018-12-13T11:20:01+00:00","2018-11-29T14:00:00+00:00"],"filter_comparator":["greater","less","greater"],"filter_concat":"","filter_include_nulls":false}'
|
||||
title: testfilter1
|
||||
owner_id: 1
|
||||
updated: 2020-09-08 15:13:12
|
||||
created: 2020-09-08 14:13:12
|
|
@ -20,7 +20,10 @@ package db
|
|||
import (
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"testing"
|
||||
"xorm.io/core"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
@ -69,3 +72,32 @@ func InitTestFixtures(tablenames ...string) (err error) {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AssertDBExists checks and asserts the existence of certain entries in the db
|
||||
func AssertDBExists(t *testing.T, table string, values map[string]interface{}, custom bool) {
|
||||
var exists bool
|
||||
var err error
|
||||
v := make(map[string]interface{})
|
||||
// Postgres sometimes needs to build raw sql. Because it won't always need to do this and this isn't fun, it's a flag.
|
||||
if custom {
|
||||
//#nosec
|
||||
sql := "SELECT * FROM " + table + " WHERE "
|
||||
for col, val := range values {
|
||||
sql += col + "=" + fmt.Sprintf("%v", val) + " AND "
|
||||
}
|
||||
sql = sql[:len(sql)-5]
|
||||
exists, err = x.SQL(sql).Get(&v)
|
||||
} else {
|
||||
exists, err = x.Table(table).Where(values).Get(&v)
|
||||
}
|
||||
assert.NoError(t, err, fmt.Sprintf("Failed to assert entries exist in db, error was: %s", err))
|
||||
assert.True(t, exists, fmt.Sprintf("Entries %v do not exist in table %s", values, table))
|
||||
}
|
||||
|
||||
// AssertDBMissing checks and asserts the nonexiste nce of certain entries in the db
|
||||
func AssertDBMissing(t *testing.T, table string, values map[string]interface{}) {
|
||||
v := make(map[string]interface{})
|
||||
exists, err := x.Table(table).Where(values).Exist(&v)
|
||||
assert.NoError(t, err, fmt.Sprintf("Failed to assert entries don't exist in db, error was: %s", err))
|
||||
assert.False(t, exists, fmt.Sprintf("Entries %v exist in table %s", values, table))
|
||||
}
|
||||
|
|
|
@ -258,6 +258,29 @@ func TestTaskCollection(t *testing.T) {
|
|||
assertHandlerErrorCode(t, err, models.ErrCodeInvalidTaskFilterValue)
|
||||
})
|
||||
})
|
||||
t.Run("saved filter", func(t *testing.T) {
|
||||
t.Run("date range", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(
|
||||
nil,
|
||||
map[string]string{"list": "-2"}, // Actually a saved filter - contains the same filter arguments as the start and end date filter from above
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `task #1`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4`)
|
||||
assert.Contains(t, rec.Body.String(), `task #5`)
|
||||
assert.Contains(t, rec.Body.String(), `task #6`)
|
||||
assert.Contains(t, rec.Body.String(), `task #7`)
|
||||
assert.Contains(t, rec.Body.String(), `task #8`)
|
||||
assert.Contains(t, rec.Body.String(), `task #9`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #10`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #11`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #12`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #13`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #14`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ReadAll for all tasks", func(t *testing.T) {
|
||||
|
|
51
pkg/migration/20200906184746.go
Normal file
51
pkg/migration/20200906184746.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 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 General Public License 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"time"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type savedFilters20200906184746 struct {
|
||||
ID int64 `xorm:"autoincr not null unique pk" json:"id"`
|
||||
Filters *models.TaskCollection `xorm:"JSON not null" json:"filters"`
|
||||
Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(1|250)" minLength:"1" maxLength:"250"`
|
||||
Description string `xorm:"longtext null" json:"description"`
|
||||
OwnerID int64 `xorm:"int(11) not null INDEX" json:"-"`
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
Updated time.Time `xorm:"updated not null" json:"updated"`
|
||||
}
|
||||
|
||||
func (savedFilters20200906184746) TableName() string {
|
||||
return "saved_filters"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20200906184746",
|
||||
Description: "Add the saved filters column",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(savedFilters20200906184746{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -1363,3 +1363,62 @@ func (err ErrBucketLimitExceeded) HTTPError() web.HTTPError {
|
|||
Message: "You cannot add the task to this bucket as it already exceeded the limit of tasks it can hold.",
|
||||
}
|
||||
}
|
||||
|
||||
// =============
|
||||
// Saved Filters
|
||||
// =============
|
||||
|
||||
// ErrSavedFilterDoesNotExist represents an error where a kanban bucket does not exist
|
||||
type ErrSavedFilterDoesNotExist struct {
|
||||
SavedFilterID int64
|
||||
}
|
||||
|
||||
// IsErrSavedFilterDoesNotExist checks if an error is ErrSavedFilterDoesNotExist.
|
||||
func IsErrSavedFilterDoesNotExist(err error) bool {
|
||||
_, ok := err.(ErrSavedFilterDoesNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrSavedFilterDoesNotExist) Error() string {
|
||||
return fmt.Sprintf("Saved filter does not exist [SavedFilterID: %d]", err.SavedFilterID)
|
||||
}
|
||||
|
||||
// ErrCodeSavedFilterDoesNotExist holds the unique world-error code of this error
|
||||
const ErrCodeSavedFilterDoesNotExist = 11001
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrSavedFilterDoesNotExist) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusNotFound,
|
||||
Code: ErrCodeSavedFilterDoesNotExist,
|
||||
Message: "This saved filter does not exist.",
|
||||
}
|
||||
}
|
||||
|
||||
// ErrSavedFilterNotAvailableForLinkShare represents an error where a kanban bucket does not exist
|
||||
type ErrSavedFilterNotAvailableForLinkShare struct {
|
||||
SavedFilterID int64
|
||||
LinkShareID int64
|
||||
}
|
||||
|
||||
// IsErrSavedFilterNotAvailableForLinkShare checks if an error is ErrSavedFilterNotAvailableForLinkShare.
|
||||
func IsErrSavedFilterNotAvailableForLinkShare(err error) bool {
|
||||
_, ok := err.(ErrSavedFilterNotAvailableForLinkShare)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrSavedFilterNotAvailableForLinkShare) Error() string {
|
||||
return fmt.Sprintf("Saved filters are not available for link shares [SavedFilterID: %d, LinkShareID: %d]", err.SavedFilterID, err.LinkShareID)
|
||||
}
|
||||
|
||||
// ErrCodeSavedFilterNotAvailableForLinkShare holds the unique world-error code of this error
|
||||
const ErrCodeSavedFilterNotAvailableForLinkShare = 11002
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrSavedFilterNotAvailableForLinkShare) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusPreconditionFailed,
|
||||
Code: ErrCodeSavedFilterNotAvailableForLinkShare,
|
||||
Message: "Saved filters are not available for link shares.",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ type Label struct {
|
|||
// The unique, numeric id of this label.
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"label"`
|
||||
// The title of the lable. You'll see this one on tasks associated with it.
|
||||
Title string `xorm:"varchar(250) not null" json:"title" valid:"runelength(1|250)" minLength:"3" maxLength:"250"`
|
||||
Title string `xorm:"varchar(250) not null" json:"title" valid:"runelength(1|250)" minLength:"1" maxLength:"250"`
|
||||
// The label description.
|
||||
Description string `xorm:"longtext null" json:"description"`
|
||||
// The color this label has
|
||||
|
|
|
@ -32,7 +32,7 @@ type List struct {
|
|||
// 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.
|
||||
Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(1|250)" minLength:"3" maxLength:"250"`
|
||||
Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(1|250)" minLength:"1" maxLength:"250"`
|
||||
// The description of the list.
|
||||
Description string `xorm:"longtext null" json:"description"`
|
||||
// The unique list short identifier. Used to build task identifiers.
|
||||
|
@ -185,6 +185,19 @@ func (l *List) ReadOne() (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Get list owner
|
||||
l.Owner, err = user.GetUserByID(l.OwnerID)
|
||||
if err != nil {
|
||||
|
|
|
@ -81,6 +81,12 @@ func (l *List) CanRead(a web.Auth) (bool, int, error) {
|
|||
return true, int(RightRead), nil
|
||||
}
|
||||
|
||||
// Saved Filter Lists need a special case
|
||||
if getSavedFilterIDFromListID(l.ID) > 0 {
|
||||
sf := &SavedFilter{ID: getSavedFilterIDFromListID(l.ID)}
|
||||
return sf.CanRead(a)
|
||||
}
|
||||
|
||||
// Check if the user is either owner or can read
|
||||
if err := l.GetSimpleByID(); err != nil {
|
||||
return false, 0, err
|
||||
|
|
|
@ -57,6 +57,7 @@ func GetTables() []interface{} {
|
|||
&TaskComment{},
|
||||
&Bucket{},
|
||||
&UnsplashPhoto{},
|
||||
&SavedFilter{},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"github.com/imdario/mergo"
|
||||
"sort"
|
||||
"time"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
@ -30,7 +31,7 @@ type Namespace struct {
|
|||
// The unique, numeric id of this namespace.
|
||||
ID int64 `xorm:"int(11) 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:"5" maxLength:"250"`
|
||||
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:"int(11) not null INDEX" json:"-"`
|
||||
|
@ -53,8 +54,8 @@ type Namespace struct {
|
|||
web.Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// PseudoNamespace is a pseudo namespace used to hold shared lists
|
||||
var PseudoNamespace = Namespace{
|
||||
// SharedListsPseudoNamespace is a pseudo namespace used to hold shared lists
|
||||
var SharedListsPseudoNamespace = Namespace{
|
||||
ID: -1,
|
||||
Title: "Shared Lists",
|
||||
Description: "Lists of other users shared with you via teams or directly.",
|
||||
|
@ -71,6 +72,15 @@ var FavoritesPseudoNamespace = Namespace{
|
|||
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"
|
||||
|
@ -84,7 +94,7 @@ func (n *Namespace) GetSimpleByID() (err error) {
|
|||
|
||||
// Get the namesapce with shared lists
|
||||
if n.ID == -1 {
|
||||
*n = PseudoNamespace
|
||||
*n = SharedListsPseudoNamespace
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -179,13 +189,18 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r
|
|||
return nil, 0, 0, ErrGenericForbidden{}
|
||||
}
|
||||
|
||||
// This map will hold all namespaces and their lists. The key is usually the id of the namespace.
|
||||
// We're using a map here because it makes a few things like adding lists or removing pseudo namespaces easier.
|
||||
namespaces := make(map[int64]*NamespaceWithLists)
|
||||
|
||||
//////////////////////////////
|
||||
// Lists with their namespaces
|
||||
|
||||
doer, err := user.GetFromAuth(a)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
all := []*NamespaceWithLists{}
|
||||
|
||||
// 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 !n.IsArchived {
|
||||
|
@ -194,26 +209,7 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r
|
|||
)
|
||||
}
|
||||
|
||||
// Create our pseudo namespace with favorite lists
|
||||
// We want this one at the beginning, which is why we create it here
|
||||
pseudoFavoriteNamespace := FavoritesPseudoNamespace
|
||||
pseudoFavoriteNamespace.Owner = doer
|
||||
all = append(all, &NamespaceWithLists{
|
||||
Namespace: pseudoFavoriteNamespace,
|
||||
Lists: []*List{{}},
|
||||
})
|
||||
*all[0].Lists[0] = FavoritesPseudoList // Copying the list to be able to modify it later
|
||||
|
||||
// Create our pseudo namespace to hold the shared lists
|
||||
pseudonamespace := PseudoNamespace
|
||||
pseudonamespace.Owner = doer
|
||||
all = append(all, &NamespaceWithLists{
|
||||
pseudonamespace,
|
||||
[]*List{},
|
||||
})
|
||||
|
||||
limit, start := getLimitFromPageIndex(page, perPage)
|
||||
|
||||
query := x.Select("namespaces.*").
|
||||
Table("namespaces").
|
||||
Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id").
|
||||
|
@ -228,15 +224,15 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r
|
|||
if limit > 0 {
|
||||
query = query.Limit(limit, start)
|
||||
}
|
||||
err = query.Find(&all)
|
||||
err = query.Find(&namespaces)
|
||||
if err != nil {
|
||||
return all, 0, 0, err
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
// Make a list of namespace ids
|
||||
var namespaceids []int64
|
||||
var userIDs []int64
|
||||
for _, nsp := range all {
|
||||
for _, nsp := range namespaces {
|
||||
namespaceids = append(namespaceids, nsp.ID)
|
||||
userIDs = append(userIDs, nsp.OwnerID)
|
||||
}
|
||||
|
@ -245,7 +241,7 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r
|
|||
userMap := make(map[int64]*user.User)
|
||||
err = x.In("id", userIDs).Find(&userMap)
|
||||
if err != nil {
|
||||
return all, 0, 0, err
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
// Get all lists
|
||||
|
@ -258,7 +254,34 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r
|
|||
}
|
||||
err = listQuery.Find(&lists)
|
||||
if err != nil {
|
||||
return all, 0, 0, err
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
numberOfTotalItems, err = x.
|
||||
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_namespace", "users_namespace.namespace_id = namespaces.id").
|
||||
Where("team_members.user_id = ?", doer.ID).
|
||||
Or("namespaces.owner_id = ?", doer.ID).
|
||||
Or("users_namespace.user_id = ?", doer.ID).
|
||||
And("namespaces.is_archived = false").
|
||||
GroupBy("namespaces.id").
|
||||
Where("namespaces.title LIKE ?", "%"+search+"%").
|
||||
Count(&NamespaceWithLists{})
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
///////////////
|
||||
// Shared Lists
|
||||
|
||||
// Create our pseudo namespace to hold the shared lists
|
||||
sharedListsPseudonamespace := SharedListsPseudoNamespace
|
||||
sharedListsPseudonamespace.Owner = doer
|
||||
namespaces[sharedListsPseudonamespace.ID] = &NamespaceWithLists{
|
||||
sharedListsPseudonamespace,
|
||||
[]*List{},
|
||||
}
|
||||
|
||||
// Get all lists individually shared with our user (not via a namespace)
|
||||
|
@ -287,9 +310,9 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r
|
|||
lists = append(lists, l)
|
||||
}
|
||||
|
||||
// Remove the pseudonamespace if we don't have any shared lists
|
||||
// Remove the sharedListsPseudonamespace if we don't have any shared lists
|
||||
if len(individualLists) == 0 {
|
||||
all = append(all[:1], all[2:]...)
|
||||
delete(namespaces, sharedListsPseudonamespace.ID)
|
||||
}
|
||||
|
||||
// More details for the lists
|
||||
|
@ -298,22 +321,23 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r
|
|||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
nMap := make(map[int64]*NamespaceWithLists, len(all))
|
||||
/////////////////
|
||||
// Favorite lists
|
||||
|
||||
// Put objects in our namespace list
|
||||
for _, n := range all {
|
||||
|
||||
// Users
|
||||
n.Owner = userMap[n.OwnerID]
|
||||
|
||||
nMap[n.ID] = n
|
||||
// Create our pseudo namespace with favorite lists
|
||||
pseudoFavoriteNamespace := FavoritesPseudoNamespace
|
||||
pseudoFavoriteNamespace.Owner = doer
|
||||
namespaces[pseudoFavoriteNamespace.ID] = &NamespaceWithLists{
|
||||
Namespace: pseudoFavoriteNamespace,
|
||||
Lists: []*List{{}},
|
||||
}
|
||||
*namespaces[pseudoFavoriteNamespace.ID].Lists[0] = FavoritesPseudoList // Copying the list to be able to modify it later
|
||||
|
||||
for _, list := range lists {
|
||||
if list.IsFavorite {
|
||||
nMap[pseudoFavoriteNamespace.ID].Lists = append(nMap[pseudoFavoriteNamespace.ID].Lists, list)
|
||||
namespaces[pseudoFavoriteNamespace.ID].Lists = append(namespaces[pseudoFavoriteNamespace.ID].Lists, list)
|
||||
}
|
||||
nMap[list.NamespaceID].Lists = append(nMap[list.NamespaceID].Lists, list)
|
||||
namespaces[list.NamespaceID].Lists = append(namespaces[list.NamespaceID].Lists, list)
|
||||
}
|
||||
|
||||
// Check if we have any favorites or favorited lists and remove the favorites namespace from the list if not
|
||||
|
@ -329,35 +353,58 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r
|
|||
|
||||
// If we don't have any favorites in the favorites pseudo list, remove that pseudo list from the namespace
|
||||
if favoriteCount == 0 {
|
||||
for in, l := range nMap[pseudoFavoriteNamespace.ID].Lists {
|
||||
for in, l := range namespaces[pseudoFavoriteNamespace.ID].Lists {
|
||||
if l.ID == FavoritesPseudoList.ID {
|
||||
nMap[pseudoFavoriteNamespace.ID].Lists = append(nMap[pseudoFavoriteNamespace.ID].Lists[:in], nMap[pseudoFavoriteNamespace.ID].Lists[in+1:]...)
|
||||
namespaces[pseudoFavoriteNamespace.ID].Lists = append(namespaces[pseudoFavoriteNamespace.ID].Lists[:in], namespaces[pseudoFavoriteNamespace.ID].Lists[in+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have any favorites in the namespace, remove it
|
||||
if len(nMap[pseudoFavoriteNamespace.ID].Lists) == 0 {
|
||||
all = append(all[:0], all[1:]...)
|
||||
if len(namespaces[pseudoFavoriteNamespace.ID].Lists) == 0 {
|
||||
delete(namespaces, pseudoFavoriteNamespace.ID)
|
||||
}
|
||||
|
||||
numberOfTotalItems, err = x.
|
||||
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_namespace", "users_namespace.namespace_id = namespaces.id").
|
||||
Where("team_members.user_id = ?", doer.ID).
|
||||
Or("namespaces.owner_id = ?", doer.ID).
|
||||
Or("users_namespace.user_id = ?", doer.ID).
|
||||
And("namespaces.is_archived = false").
|
||||
GroupBy("namespaces.id").
|
||||
Where("namespaces.title LIKE ?", "%"+search+"%").
|
||||
Count(&NamespaceWithLists{})
|
||||
/////////////////
|
||||
// Saved Filters
|
||||
|
||||
savedFilters, err := getSavedFiltersForUser(a)
|
||||
if err != nil {
|
||||
return all, 0, 0, err
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
if len(savedFilters) > 0 {
|
||||
savedFiltersPseudoNamespace := SavedFiltersPseudoNamespace
|
||||
savedFiltersPseudoNamespace.Owner = doer
|
||||
namespaces[savedFiltersPseudoNamespace.ID] = &NamespaceWithLists{
|
||||
Namespace: savedFiltersPseudoNamespace,
|
||||
Lists: make([]*List, 0, len(savedFilters)),
|
||||
}
|
||||
|
||||
for _, filter := range savedFilters {
|
||||
namespaces[savedFiltersPseudoNamespace.ID].Lists = append(namespaces[savedFiltersPseudoNamespace.ID].Lists, &List{
|
||||
ID: getListIDFromSavedFilterID(filter.ID),
|
||||
Title: filter.Title,
|
||||
Description: filter.Description,
|
||||
Created: filter.Created,
|
||||
Updated: filter.Updated,
|
||||
Owner: doer,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
// Put it all together (and sort it)
|
||||
all := make([]*NamespaceWithLists, 0, len(namespaces))
|
||||
for _, n := range namespaces {
|
||||
n.Owner = userMap[n.OwnerID]
|
||||
all = append(all, n)
|
||||
}
|
||||
sort.Slice(all, func(i, j int) bool {
|
||||
return all[i].ID < all[j].ID
|
||||
})
|
||||
|
||||
return all, len(all), numberOfTotalItems, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -142,12 +142,13 @@ func TestNamespace_ReadAll(t *testing.T) {
|
|||
t.Run("normal", func(t *testing.T) {
|
||||
n := &Namespace{}
|
||||
nn, _, _, err := n.ReadAll(user1, "", 1, -1)
|
||||
namespaces := nn.([]*NamespaceWithLists)
|
||||
assert.NoError(t, err)
|
||||
namespaces := nn.([]*NamespaceWithLists)
|
||||
assert.NotNil(t, namespaces)
|
||||
assert.Len(t, namespaces, 10) // Total of 10 including shared & favorites
|
||||
assert.Equal(t, int64(-2), namespaces[0].ID) // The first one should be the one with favorites
|
||||
assert.Equal(t, int64(-1), namespaces[1].ID) // The second one should be the one with the shared namespaces
|
||||
assert.Len(t, namespaces, 11) // Total of 10 including shared, favorites and saved filters
|
||||
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
|
||||
// Ensure every list and namespace are not archived
|
||||
for _, namespace := range namespaces {
|
||||
assert.False(t, namespace.IsArchived)
|
||||
|
@ -164,9 +165,10 @@ func TestNamespace_ReadAll(t *testing.T) {
|
|||
namespaces := nn.([]*NamespaceWithLists)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, namespaces)
|
||||
assert.Len(t, namespaces, 11) // Total of 11 including shared & favorites, one is archived
|
||||
assert.Equal(t, int64(-2), namespaces[0].ID) // The first one should be the one with favorites
|
||||
assert.Equal(t, int64(-1), namespaces[1].ID) // The second one should be the one with the shared 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) {
|
||||
n := &Namespace{}
|
||||
|
@ -185,4 +187,12 @@ func TestNamespace_ReadAll(t *testing.T) {
|
|||
assert.Equal(t, FavoritesPseudoNamespace.ID, namespaces[0].ID)
|
||||
assert.NotEqual(t, 0, namespaces[0].Lists)
|
||||
})
|
||||
t.Run("no saved filters", func(t *testing.T) {
|
||||
n := &Namespace{}
|
||||
nn, _, _, err := n.ReadAll(user11, "", 1, -1)
|
||||
namespaces := nn.([]*NamespaceWithLists)
|
||||
assert.NoError(t, err)
|
||||
// Assert the first namespace is not the favorites namespace
|
||||
assert.NotEqual(t, SavedFiltersPseudoNamespace.ID, namespaces[0].ID)
|
||||
})
|
||||
}
|
||||
|
|
182
pkg/models/saved_filters.go
Normal file
182
pkg/models/saved_filters.go
Normal file
|
@ -0,0 +1,182 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 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 General Public License 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SavedFilter represents a saved bunch of filters
|
||||
type SavedFilter struct {
|
||||
// The unique numeric id of this saved filter
|
||||
ID int64 `xorm:"autoincr not null unique pk" json:"id" param:"filter"`
|
||||
// The actual filters this filter contains
|
||||
Filters *TaskCollection `xorm:"JSON not null" json:"filters"`
|
||||
// The title of the filter.
|
||||
Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(1|250)" minLength:"1" maxLength:"250"`
|
||||
// The description of the filter
|
||||
Description string `xorm:"longtext null" json:"description"`
|
||||
OwnerID int64 `xorm:"int(11) not null INDEX" json:"-"`
|
||||
|
||||
// The user who owns this filter
|
||||
Owner *user.User `xorm:"-" json:"owner" valid:"-"`
|
||||
|
||||
// A timestamp when this filter was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
// A timestamp when this filter 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 returns a better table name for saved filters
|
||||
func (s *SavedFilter) TableName() string {
|
||||
return "saved_filters"
|
||||
}
|
||||
|
||||
func (s *SavedFilter) getTaskCollection() *TaskCollection {
|
||||
// We're resetting the listID to return tasks from all lists
|
||||
s.Filters.ListID = 0
|
||||
return s.Filters
|
||||
}
|
||||
|
||||
// Returns the saved filter ID from a list ID. Will not check if the filter actually exists.
|
||||
// If the returned ID is zero, means that it is probably invalid.
|
||||
func getSavedFilterIDFromListID(listID int64) (filterID int64) {
|
||||
// We get the id of the saved filter by multiplying the ListID with -1 and subtracting one
|
||||
filterID = listID*-1 - 1
|
||||
// FilterIDs from listIDs are always positive
|
||||
if filterID < 0 {
|
||||
filterID = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getListIDFromSavedFilterID(filterID int64) (listID int64) {
|
||||
listID = filterID*-1 - 1
|
||||
// ListIDs from saved filters are always negative
|
||||
if listID > 0 {
|
||||
listID = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getSavedFiltersForUser(auth web.Auth) (filters []*SavedFilter, err error) {
|
||||
// Link shares can't view or modify saved filters, therefore we can error out right away
|
||||
if _, is := auth.(*LinkSharing); is {
|
||||
return nil, ErrSavedFilterNotAvailableForLinkShare{LinkShareID: auth.GetID()}
|
||||
}
|
||||
|
||||
err = x.Where("owner_id = ?", auth.GetID()).Find(&filters)
|
||||
return
|
||||
}
|
||||
|
||||
// Create creates a new saved filter
|
||||
// @Summary Creates a new saved filter
|
||||
// @Description Creates a new saved filter
|
||||
// @tags filter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {object} models.SavedFilter "The Saved Filter"
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to that saved filter."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /filters [put]
|
||||
func (s *SavedFilter) Create(auth web.Auth) error {
|
||||
s.OwnerID = auth.GetID()
|
||||
_, err := x.Insert(s)
|
||||
return err
|
||||
}
|
||||
|
||||
func getSavedFilterSimpleByID(id int64) (s *SavedFilter, err error) {
|
||||
s = &SavedFilter{}
|
||||
exists, err := x.
|
||||
Where("id = ?", id).
|
||||
Get(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
return nil, ErrSavedFilterDoesNotExist{SavedFilterID: id}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ReadOne returns one saved filter
|
||||
// @Summary Gets one saved filter
|
||||
// @Description Returns a saved filter by its ID.
|
||||
// @tags filter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "Filter ID"
|
||||
// @Success 200 {object} models.SavedFilter "The Saved Filter"
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to that saved filter."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /filters/{id} [get]
|
||||
func (s *SavedFilter) ReadOne() error {
|
||||
// s already contains almost the full saved filter from the rights check, we only need to add the user
|
||||
u, err := user.GetUserByID(s.OwnerID)
|
||||
s.Owner = u
|
||||
return err
|
||||
}
|
||||
|
||||
// Update updates an existing filter
|
||||
// @Summary Updates a saved filter
|
||||
// @Description Updates a saved filter by its ID.
|
||||
// @tags filter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "Filter ID"
|
||||
// @Success 200 {object} models.SavedFilter "The Saved Filter"
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to that saved filter."
|
||||
// @Failure 404 {object} web.HTTPError "The saved filter does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /filters/{id} [post]
|
||||
func (s *SavedFilter) Update() error {
|
||||
_, err := x.
|
||||
Where("id = ?", s.ID).
|
||||
Cols(
|
||||
"title",
|
||||
"description",
|
||||
"filters",
|
||||
).
|
||||
Update(s)
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete removes a saved filter
|
||||
// @Summary Removes a saved filter
|
||||
// @Description Removes a saved filter by its ID.
|
||||
// @tags filter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "Filter ID"
|
||||
// @Success 200 {object} models.SavedFilter "The Saved Filter"
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to that saved filter."
|
||||
// @Failure 404 {object} web.HTTPError "The saved filter does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /filters/{id} [delete]
|
||||
func (s *SavedFilter) Delete() error {
|
||||
_, err := x.Where("id = ?", s.ID).Delete(s)
|
||||
return err
|
||||
}
|
68
pkg/models/saved_filters_rights.go
Normal file
68
pkg/models/saved_filters_rights.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 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 General Public License 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import "code.vikunja.io/web"
|
||||
|
||||
// CanRead checks if a user has the right to read a saved filter
|
||||
func (s *SavedFilter) CanRead(auth web.Auth) (bool, int, error) {
|
||||
can, err := s.canDoFilter(auth)
|
||||
return can, int(RightAdmin), err
|
||||
}
|
||||
|
||||
// CanDelete checks if a user has the right to delete a saved filter
|
||||
func (s *SavedFilter) CanDelete(auth web.Auth) (bool, error) {
|
||||
return s.canDoFilter(auth)
|
||||
}
|
||||
|
||||
// CanUpdate checks if a user has the right to update a saved filter
|
||||
func (s *SavedFilter) CanUpdate(auth web.Auth) (bool, error) {
|
||||
// A normal check would replace the passed struct which in our case would override the values we want to update.
|
||||
sf := &SavedFilter{ID: s.ID}
|
||||
return sf.canDoFilter(auth)
|
||||
}
|
||||
|
||||
// CanCreate checks if a user has the right to update a saved filter
|
||||
func (s *SavedFilter) CanCreate(auth web.Auth) (bool, error) {
|
||||
if _, is := auth.(*LinkSharing); is {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Helper function to check saved filter rights sind they all have the same logic
|
||||
func (s *SavedFilter) canDoFilter(auth web.Auth) (can bool, err error) {
|
||||
// Link shares can't view or modify saved filters, therefore we can error out right away
|
||||
if _, is := auth.(*LinkSharing); is {
|
||||
return false, ErrSavedFilterNotAvailableForLinkShare{LinkShareID: auth.GetID(), SavedFilterID: s.ID}
|
||||
}
|
||||
|
||||
sf, err := getSavedFilterSimpleByID(s.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Only owners are allowed to do something with a saved filter
|
||||
if sf.OwnerID != auth.GetID() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
*s = *sf
|
||||
|
||||
return true, nil
|
||||
}
|
257
pkg/models/saved_filters_test.go
Normal file
257
pkg/models/saved_filters_test.go
Normal file
|
@ -0,0 +1,257 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 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 General Public License 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
func TestSavedFilter_getListIDFromFilter(t *testing.T) {
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
assert.Equal(t, int64(-2), getListIDFromSavedFilterID(1))
|
||||
})
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
assert.Equal(t, int64(0), getListIDFromSavedFilterID(-1))
|
||||
})
|
||||
}
|
||||
|
||||
func TestSavedFilter_getFilterIDFromListID(t *testing.T) {
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
assert.Equal(t, int64(1), getSavedFilterIDFromListID(-2))
|
||||
})
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
assert.Equal(t, int64(0), getSavedFilterIDFromListID(2))
|
||||
})
|
||||
}
|
||||
|
||||
func TestSavedFilter_Create(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
sf := &SavedFilter{
|
||||
Title: "test",
|
||||
Description: "Lorem Ipsum dolor sit amet",
|
||||
Filters: &TaskCollection{}, // Empty filter
|
||||
}
|
||||
|
||||
u := &user.User{ID: 1}
|
||||
err := sf.Create(u)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, u.ID, sf.OwnerID)
|
||||
vals := map[string]interface{}{
|
||||
"title": "'test'",
|
||||
"description": "'Lorem Ipsum dolor sit amet'",
|
||||
"filters": "'{\"sort_by\":null,\"order_by\":null,\"filter_by\":null,\"filter_value\":null,\"filter_comparator\":null,\"filter_concat\":\"\",\"filter_include_nulls\":false}'",
|
||||
"owner_id": 1,
|
||||
}
|
||||
// Postgres can't compare json values directly, see https://dba.stackexchange.com/a/106290/210721
|
||||
if x.Dialect().URI().DBType == schemas.POSTGRES {
|
||||
vals["filters::jsonb"] = vals["filters"].(string) + "::jsonb"
|
||||
delete(vals, "filters")
|
||||
}
|
||||
db.AssertDBExists(t, "saved_filters", vals, true)
|
||||
}
|
||||
|
||||
func TestSavedFilter_ReadOne(t *testing.T) {
|
||||
user1 := &user.User{ID: 1}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
sf := &SavedFilter{
|
||||
ID: 1,
|
||||
}
|
||||
// canRead pre-populates the struct
|
||||
_, _, err := sf.CanRead(user1)
|
||||
assert.NoError(t, err)
|
||||
err = sf.ReadOne()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, sf.Owner)
|
||||
}
|
||||
|
||||
func TestSavedFilter_Update(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
sf := &SavedFilter{
|
||||
ID: 1,
|
||||
Title: "NewTitle",
|
||||
Description: "", // Explicitly reset the description
|
||||
Filters: &TaskCollection{},
|
||||
}
|
||||
err := sf.Update()
|
||||
assert.NoError(t, err)
|
||||
db.AssertDBExists(t, "saved_filters", map[string]interface{}{
|
||||
"id": 1,
|
||||
"title": "NewTitle",
|
||||
"description": "",
|
||||
}, false)
|
||||
}
|
||||
|
||||
func TestSavedFilter_Delete(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
sf := &SavedFilter{
|
||||
ID: 1,
|
||||
}
|
||||
err := sf.Delete()
|
||||
assert.NoError(t, err)
|
||||
db.AssertDBMissing(t, "saved_filters", map[string]interface{}{
|
||||
"id": 1,
|
||||
})
|
||||
}
|
||||
|
||||
func TestSavedFilter_Rights(t *testing.T) {
|
||||
user1 := &user.User{ID: 1}
|
||||
user2 := &user.User{ID: 2}
|
||||
ls := &LinkSharing{ID: 1}
|
||||
|
||||
t.Run("create", func(t *testing.T) {
|
||||
// Should always be true
|
||||
db.LoadAndAssertFixtures(t)
|
||||
can, err := (&SavedFilter{}).CanCreate(user1)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, can)
|
||||
})
|
||||
t.Run("read", func(t *testing.T) {
|
||||
t.Run("owner", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
sf := &SavedFilter{
|
||||
ID: 1,
|
||||
Title: "Lorem",
|
||||
}
|
||||
can, max, err := sf.CanRead(user1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int(RightAdmin), max)
|
||||
assert.True(t, can)
|
||||
})
|
||||
t.Run("not owner", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
sf := &SavedFilter{
|
||||
ID: 1,
|
||||
Title: "Lorem",
|
||||
}
|
||||
can, _, err := sf.CanRead(user2)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, can)
|
||||
})
|
||||
t.Run("nonexisting", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
sf := &SavedFilter{
|
||||
ID: 9999,
|
||||
Title: "Lorem",
|
||||
}
|
||||
can, _, err := sf.CanRead(user1)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrSavedFilterDoesNotExist(err))
|
||||
assert.False(t, can)
|
||||
})
|
||||
t.Run("link share", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
sf := &SavedFilter{
|
||||
ID: 1,
|
||||
Title: "Lorem",
|
||||
}
|
||||
can, _, err := sf.CanRead(ls)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrSavedFilterNotAvailableForLinkShare(err))
|
||||
assert.False(t, can)
|
||||
})
|
||||
})
|
||||
t.Run("update", func(t *testing.T) {
|
||||
t.Run("owner", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
sf := &SavedFilter{
|
||||
ID: 1,
|
||||
Title: "Lorem",
|
||||
}
|
||||
can, err := sf.CanUpdate(user1)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, can)
|
||||
})
|
||||
t.Run("not owner", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
sf := &SavedFilter{
|
||||
ID: 1,
|
||||
Title: "Lorem",
|
||||
}
|
||||
can, err := sf.CanUpdate(user2)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, can)
|
||||
})
|
||||
t.Run("nonexisting", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
sf := &SavedFilter{
|
||||
ID: 9999,
|
||||
Title: "Lorem",
|
||||
}
|
||||
can, err := sf.CanUpdate(user1)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrSavedFilterDoesNotExist(err))
|
||||
assert.False(t, can)
|
||||
})
|
||||
t.Run("link share", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
sf := &SavedFilter{
|
||||
ID: 1,
|
||||
Title: "Lorem",
|
||||
}
|
||||
can, err := sf.CanUpdate(ls)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrSavedFilterNotAvailableForLinkShare(err))
|
||||
assert.False(t, can)
|
||||
})
|
||||
})
|
||||
t.Run("delete", func(t *testing.T) {
|
||||
t.Run("owner", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
sf := &SavedFilter{
|
||||
ID: 1,
|
||||
}
|
||||
can, err := sf.CanDelete(user1)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, can)
|
||||
})
|
||||
t.Run("not owner", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
sf := &SavedFilter{
|
||||
ID: 1,
|
||||
}
|
||||
can, err := sf.CanDelete(user2)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, can)
|
||||
})
|
||||
t.Run("nonexisting", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
sf := &SavedFilter{
|
||||
ID: 9999,
|
||||
Title: "Lorem",
|
||||
}
|
||||
can, err := sf.CanDelete(user1)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrSavedFilterDoesNotExist(err))
|
||||
assert.False(t, can)
|
||||
})
|
||||
t.Run("link share", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
sf := &SavedFilter{
|
||||
ID: 1,
|
||||
Title: "Lorem",
|
||||
}
|
||||
can, err := sf.CanDelete(ls)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrSavedFilterNotAvailableForLinkShare(err))
|
||||
assert.False(t, can)
|
||||
})
|
||||
})
|
||||
}
|
|
@ -24,29 +24,29 @@ import (
|
|||
|
||||
// TaskCollection is a struct used to hold filter details and not clutter the Task struct with information not related to actual tasks.
|
||||
type TaskCollection struct {
|
||||
ListID int64 `param:"list"`
|
||||
Lists []*List
|
||||
ListID int64 `param:"list" json:"-"`
|
||||
Lists []*List `json:"-"`
|
||||
|
||||
// The query parameter to sort by. This is for ex. done, priority, etc.
|
||||
SortBy []string `query:"sort_by"`
|
||||
SortByArr []string `query:"sort_by[]"`
|
||||
SortBy []string `query:"sort_by" json:"sort_by"`
|
||||
SortByArr []string `query:"sort_by[]" json:"-"`
|
||||
// The query parameter to order the items by. This can be either asc or desc, with asc being the default.
|
||||
OrderBy []string `query:"order_by"`
|
||||
OrderByArr []string `query:"order_by[]"`
|
||||
OrderBy []string `query:"order_by" json:"order_by"`
|
||||
OrderByArr []string `query:"order_by[]" json:"-"`
|
||||
|
||||
// The field name of the field to filter by
|
||||
FilterBy []string `query:"filter_by"`
|
||||
FilterByArr []string `query:"filter_by[]"`
|
||||
FilterBy []string `query:"filter_by" json:"filter_by"`
|
||||
FilterByArr []string `query:"filter_by[]" json:"-"`
|
||||
// The value of the field name to filter by
|
||||
FilterValue []string `query:"filter_value"`
|
||||
FilterValueArr []string `query:"filter_value[]"`
|
||||
FilterValue []string `query:"filter_value" json:"filter_value"`
|
||||
FilterValueArr []string `query:"filter_value[]" json:"-"`
|
||||
// The comparator for field and value
|
||||
FilterComparator []string `query:"filter_comparator"`
|
||||
FilterComparatorArr []string `query:"filter_comparator[]"`
|
||||
FilterComparator []string `query:"filter_comparator" json:"filter_comparator"`
|
||||
FilterComparatorArr []string `query:"filter_comparator[]" json:"-"`
|
||||
// The way all filter conditions are concatenated together, can be either "and" or "or".,
|
||||
FilterConcat string `query:"filter_concat"`
|
||||
FilterConcat string `query:"filter_concat" json:"filter_concat"`
|
||||
// If set to true, the result will also include null values
|
||||
FilterIncludeNulls bool `query:"filter_include_nulls"`
|
||||
FilterIncludeNulls bool `query:"filter_include_nulls" json:"filter_include_nulls"`
|
||||
|
||||
web.CRUDable `xorm:"-" json:"-"`
|
||||
web.Rights `xorm:"-" json:"-"`
|
||||
|
@ -102,6 +102,17 @@ func validateTaskField(fieldName string) error {
|
|||
// @Router /lists/{listID}/tasks [get]
|
||||
func (tf *TaskCollection) ReadAll(a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
|
||||
|
||||
// If the list id is < -1 this means we're dealing with a saved filter - in that case we get and populate the filter
|
||||
// -1 is the favorites list which works as intended
|
||||
if tf.ListID < -1 {
|
||||
s, err := getSavedFilterSimpleByID(getSavedFilterIDFromListID(tf.ListID))
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
return s.getTaskCollection().ReadAll(a, search, page, perPage)
|
||||
}
|
||||
|
||||
if len(tf.SortByArr) > 0 {
|
||||
tf.SortBy = append(tf.SortBy, tf.SortByArr...)
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ type Task struct {
|
|||
// The unique, numeric id of this task.
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"listtask"`
|
||||
// The task text. This is what you'll see in the list.
|
||||
Title string `xorm:"varchar(250) not null" json:"title" valid:"runelength(1|250)" minLength:"3" maxLength:"250"`
|
||||
Title string `xorm:"varchar(250) not null" json:"title" valid:"runelength(1|250)" minLength:"1" maxLength:"250"`
|
||||
// The task description.
|
||||
Description string `xorm:"longtext null" json:"description"`
|
||||
// Whether a task is done or not.
|
||||
|
|
|
@ -29,7 +29,7 @@ type Team struct {
|
|||
// The unique, numeric id of this team.
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"team"`
|
||||
// The name of this team.
|
||||
Name string `xorm:"varchar(250) not null" json:"name" valid:"required,runelength(1|250)" minLength:"5" maxLength:"250"`
|
||||
Name string `xorm:"varchar(250) not null" json:"name" valid:"required,runelength(1|250)" minLength:"1" maxLength:"250"`
|
||||
// The team's description.
|
||||
Description string `xorm:"longtext null" json:"description"`
|
||||
CreatedByID int64 `xorm:"int(11) not null INDEX" json:"-"`
|
||||
|
|
|
@ -58,6 +58,7 @@ func SetupTests() {
|
|||
"users_list",
|
||||
"users_namespace",
|
||||
"buckets",
|
||||
"saved_filters",
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
|
|
@ -69,7 +69,7 @@ func getNamespace(c echo.Context) (namespace *models.Namespace, err error) {
|
|||
}
|
||||
|
||||
if namespaceID == -1 {
|
||||
namespace = &models.PseudoNamespace
|
||||
namespace = &models.SharedListsPseudoNamespace
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -431,6 +431,16 @@ func registerAPIRoutes(a *echo.Group) {
|
|||
a.DELETE("/lists/:list/users/:user", listUserHandler.DeleteWeb)
|
||||
a.POST("/lists/:list/users/:user", listUserHandler.UpdateWeb)
|
||||
|
||||
savedFiltersHandler := &handler.WebHandler{
|
||||
EmptyStruct: func() handler.CObject {
|
||||
return &models.SavedFilter{}
|
||||
},
|
||||
}
|
||||
a.GET("/filters/:filter", savedFiltersHandler.ReadOneWeb)
|
||||
a.PUT("/filters", savedFiltersHandler.CreateWeb)
|
||||
a.DELETE("/filters/:filter", savedFiltersHandler.DeleteWeb)
|
||||
a.POST("/filters/:filter", savedFiltersHandler.UpdateWeb)
|
||||
|
||||
namespaceHandler := &handler.WebHandler{
|
||||
EmptyStruct: func() handler.CObject {
|
||||
return &models.Namespace{}
|
||||
|
|
|
@ -172,6 +172,201 @@ var doc = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/filters": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Creates a new saved filter",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"filter"
|
||||
],
|
||||
"summary": "Creates a new saved filter",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The Saved Filter",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.SavedFilter"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The user does not have access to that saved filter.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/filters/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Returns a saved filter by its ID.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"filter"
|
||||
],
|
||||
"summary": "Gets one saved filter",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Filter ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The Saved Filter",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.SavedFilter"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The user does not have access to that saved filter.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Updates a saved filter by its ID.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"filter"
|
||||
],
|
||||
"summary": "Updates a saved filter",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Filter ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The Saved Filter",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.SavedFilter"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The user does not have access to that saved filter.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The saved filter does not exist.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Removes a saved filter by its ID.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"filter"
|
||||
],
|
||||
"summary": "Removes a saved filter",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Filter ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The Saved Filter",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.SavedFilter"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The user does not have access to that saved filter.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The saved filter does not exist.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/info": {
|
||||
"get": {
|
||||
"description": "Returns the version, frontendurl, motd and various settings of Vikunja",
|
||||
|
@ -6403,7 +6598,7 @@ var doc = `{
|
|||
"description": "The task text. This is what you'll see in the list.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 3
|
||||
"minLength": 1
|
||||
},
|
||||
"updated": {
|
||||
"description": "A timestamp when this task was last updated. You cannot change this value.",
|
||||
|
@ -6440,7 +6635,7 @@ var doc = `{
|
|||
"description": "The title of the lable. You'll see this one on tasks associated with it.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 3
|
||||
"minLength": 1
|
||||
},
|
||||
"updated": {
|
||||
"description": "A timestamp when this label was last updated. You cannot change this value.",
|
||||
|
@ -6561,7 +6756,7 @@ var doc = `{
|
|||
"description": "The title of the list. You'll see this in the namespace overview.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 3
|
||||
"minLength": 1
|
||||
},
|
||||
"updated": {
|
||||
"description": "A timestamp when this list was last updated. You cannot change this value.",
|
||||
|
@ -6652,7 +6847,7 @@ var doc = `{
|
|||
"description": "The name of this namespace.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 5
|
||||
"minLength": 1
|
||||
},
|
||||
"updated": {
|
||||
"description": "A timestamp when this namespace was last updated. You cannot change this value.",
|
||||
|
@ -6726,7 +6921,7 @@ var doc = `{
|
|||
"description": "The name of this namespace.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 5
|
||||
"minLength": 1
|
||||
},
|
||||
"updated": {
|
||||
"description": "A timestamp when this namespace was last updated. You cannot change this value.",
|
||||
|
@ -6743,6 +6938,43 @@ var doc = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"models.SavedFilter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created": {
|
||||
"description": "A timestamp when this filter was created. You cannot change this value.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "The description of the filter",
|
||||
"type": "string"
|
||||
},
|
||||
"filters": {
|
||||
"description": "The actual filters this filter contains",
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/models.TaskCollection"
|
||||
},
|
||||
"id": {
|
||||
"description": "The unique numeric id of this saved filter",
|
||||
"type": "integer"
|
||||
},
|
||||
"owner": {
|
||||
"description": "The user who owns this filter",
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/user.User"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the filter.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 1
|
||||
},
|
||||
"updated": {
|
||||
"description": "A timestamp when this filter was last updated. You cannot change this value.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.Task": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -6865,7 +7097,7 @@ var doc = `{
|
|||
"description": "The task text. This is what you'll see in the list.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 3
|
||||
"minLength": 1
|
||||
},
|
||||
"updated": {
|
||||
"description": "A timestamp when this task was last updated. You cannot change this value.",
|
||||
|
@ -6906,6 +7138,54 @@ var doc = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"models.TaskCollection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter_by": {
|
||||
"description": "The field name of the field to filter by",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"filter_comparator": {
|
||||
"description": "The comparator for field and value",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"filter_concat": {
|
||||
"description": "The way all filter conditions are concatenated together, can be either \"and\" or \"or\".,",
|
||||
"type": "string"
|
||||
},
|
||||
"filter_include_nulls": {
|
||||
"description": "If set to true, the result will also include null values",
|
||||
"type": "boolean"
|
||||
},
|
||||
"filter_value": {
|
||||
"description": "The value of the field name to filter by",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"order_by": {
|
||||
"description": "The query parameter to order the items by. This can be either asc or desc, with asc being the default.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"sort_by": {
|
||||
"description": "The query parameter to sort by. This is for ex. done, priority, etc.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.TaskComment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -6984,7 +7264,7 @@ var doc = `{
|
|||
"description": "The name of this team.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 5
|
||||
"minLength": 1
|
||||
},
|
||||
"updated": {
|
||||
"description": "A timestamp when this relation was last updated. You cannot change this value.",
|
||||
|
@ -7095,7 +7375,7 @@ var doc = `{
|
|||
"description": "The username of the user. Is always unique.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 3
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -7130,7 +7410,7 @@ var doc = `{
|
|||
"description": "The name of this team.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 5
|
||||
"minLength": 1
|
||||
},
|
||||
"right": {
|
||||
"type": "integer"
|
||||
|
@ -7168,7 +7448,7 @@ var doc = `{
|
|||
"description": "The username of the user. Is always unique.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 3
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -7315,7 +7595,7 @@ var doc = `{
|
|||
"description": "The username of the user. Is always unique.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 3
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -155,6 +155,201 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/filters": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Creates a new saved filter",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"filter"
|
||||
],
|
||||
"summary": "Creates a new saved filter",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The Saved Filter",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.SavedFilter"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The user does not have access to that saved filter.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/filters/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Returns a saved filter by its ID.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"filter"
|
||||
],
|
||||
"summary": "Gets one saved filter",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Filter ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The Saved Filter",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.SavedFilter"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The user does not have access to that saved filter.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Updates a saved filter by its ID.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"filter"
|
||||
],
|
||||
"summary": "Updates a saved filter",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Filter ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The Saved Filter",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.SavedFilter"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The user does not have access to that saved filter.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The saved filter does not exist.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Removes a saved filter by its ID.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"filter"
|
||||
],
|
||||
"summary": "Removes a saved filter",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Filter ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The Saved Filter",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.SavedFilter"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The user does not have access to that saved filter.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The saved filter does not exist.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/info": {
|
||||
"get": {
|
||||
"description": "Returns the version, frontendurl, motd and various settings of Vikunja",
|
||||
|
@ -6386,7 +6581,7 @@
|
|||
"description": "The task text. This is what you'll see in the list.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 3
|
||||
"minLength": 1
|
||||
},
|
||||
"updated": {
|
||||
"description": "A timestamp when this task was last updated. You cannot change this value.",
|
||||
|
@ -6423,7 +6618,7 @@
|
|||
"description": "The title of the lable. You'll see this one on tasks associated with it.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 3
|
||||
"minLength": 1
|
||||
},
|
||||
"updated": {
|
||||
"description": "A timestamp when this label was last updated. You cannot change this value.",
|
||||
|
@ -6544,7 +6739,7 @@
|
|||
"description": "The title of the list. You'll see this in the namespace overview.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 3
|
||||
"minLength": 1
|
||||
},
|
||||
"updated": {
|
||||
"description": "A timestamp when this list was last updated. You cannot change this value.",
|
||||
|
@ -6635,7 +6830,7 @@
|
|||
"description": "The name of this namespace.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 5
|
||||
"minLength": 1
|
||||
},
|
||||
"updated": {
|
||||
"description": "A timestamp when this namespace was last updated. You cannot change this value.",
|
||||
|
@ -6709,7 +6904,7 @@
|
|||
"description": "The name of this namespace.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 5
|
||||
"minLength": 1
|
||||
},
|
||||
"updated": {
|
||||
"description": "A timestamp when this namespace was last updated. You cannot change this value.",
|
||||
|
@ -6726,6 +6921,43 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"models.SavedFilter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created": {
|
||||
"description": "A timestamp when this filter was created. You cannot change this value.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "The description of the filter",
|
||||
"type": "string"
|
||||
},
|
||||
"filters": {
|
||||
"description": "The actual filters this filter contains",
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/models.TaskCollection"
|
||||
},
|
||||
"id": {
|
||||
"description": "The unique numeric id of this saved filter",
|
||||
"type": "integer"
|
||||
},
|
||||
"owner": {
|
||||
"description": "The user who owns this filter",
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/user.User"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the filter.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 1
|
||||
},
|
||||
"updated": {
|
||||
"description": "A timestamp when this filter was last updated. You cannot change this value.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.Task": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -6848,7 +7080,7 @@
|
|||
"description": "The task text. This is what you'll see in the list.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 3
|
||||
"minLength": 1
|
||||
},
|
||||
"updated": {
|
||||
"description": "A timestamp when this task was last updated. You cannot change this value.",
|
||||
|
@ -6889,6 +7121,54 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"models.TaskCollection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter_by": {
|
||||
"description": "The field name of the field to filter by",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"filter_comparator": {
|
||||
"description": "The comparator for field and value",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"filter_concat": {
|
||||
"description": "The way all filter conditions are concatenated together, can be either \"and\" or \"or\".,",
|
||||
"type": "string"
|
||||
},
|
||||
"filter_include_nulls": {
|
||||
"description": "If set to true, the result will also include null values",
|
||||
"type": "boolean"
|
||||
},
|
||||
"filter_value": {
|
||||
"description": "The value of the field name to filter by",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"order_by": {
|
||||
"description": "The query parameter to order the items by. This can be either asc or desc, with asc being the default.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"sort_by": {
|
||||
"description": "The query parameter to sort by. This is for ex. done, priority, etc.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.TaskComment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -6967,7 +7247,7 @@
|
|||
"description": "The name of this team.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 5
|
||||
"minLength": 1
|
||||
},
|
||||
"updated": {
|
||||
"description": "A timestamp when this relation was last updated. You cannot change this value.",
|
||||
|
@ -7078,7 +7358,7 @@
|
|||
"description": "The username of the user. Is always unique.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 3
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -7113,7 +7393,7 @@
|
|||
"description": "The name of this team.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 5
|
||||
"minLength": 1
|
||||
},
|
||||
"right": {
|
||||
"type": "integer"
|
||||
|
@ -7151,7 +7431,7 @@
|
|||
"description": "The username of the user. Is always unique.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 3
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -7298,7 +7578,7 @@
|
|||
"description": "The username of the user. Is always unique.",
|
||||
"type": "string",
|
||||
"maxLength": 250,
|
||||
"minLength": 3
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -182,7 +182,7 @@ definitions:
|
|||
title:
|
||||
description: The task text. This is what you'll see in the list.
|
||||
maxLength: 250
|
||||
minLength: 3
|
||||
minLength: 1
|
||||
type: string
|
||||
updated:
|
||||
description: A timestamp when this task was last updated. You cannot change this value.
|
||||
|
@ -210,7 +210,7 @@ definitions:
|
|||
title:
|
||||
description: The title of the lable. You'll see this one on tasks associated with it.
|
||||
maxLength: 250
|
||||
minLength: 3
|
||||
minLength: 1
|
||||
type: string
|
||||
updated:
|
||||
description: A timestamp when this label was last updated. You cannot change this value.
|
||||
|
@ -300,7 +300,7 @@ definitions:
|
|||
title:
|
||||
description: The title of the list. You'll see this in the namespace overview.
|
||||
maxLength: 250
|
||||
minLength: 3
|
||||
minLength: 1
|
||||
type: string
|
||||
updated:
|
||||
description: A timestamp when this list was last updated. You cannot change this value.
|
||||
|
@ -367,7 +367,7 @@ definitions:
|
|||
title:
|
||||
description: The name of this namespace.
|
||||
maxLength: 250
|
||||
minLength: 5
|
||||
minLength: 1
|
||||
type: string
|
||||
updated:
|
||||
description: A timestamp when this namespace was last updated. You cannot change this value.
|
||||
|
@ -422,7 +422,7 @@ definitions:
|
|||
title:
|
||||
description: The name of this namespace.
|
||||
maxLength: 250
|
||||
minLength: 5
|
||||
minLength: 1
|
||||
type: string
|
||||
updated:
|
||||
description: A timestamp when this namespace was last updated. You cannot change this value.
|
||||
|
@ -434,6 +434,34 @@ definitions:
|
|||
$ref: '#/definitions/models.Task'
|
||||
type: array
|
||||
type: object
|
||||
models.SavedFilter:
|
||||
properties:
|
||||
created:
|
||||
description: A timestamp when this filter was created. You cannot change this value.
|
||||
type: string
|
||||
description:
|
||||
description: The description of the filter
|
||||
type: string
|
||||
filters:
|
||||
$ref: '#/definitions/models.TaskCollection'
|
||||
description: The actual filters this filter contains
|
||||
type: object
|
||||
id:
|
||||
description: The unique numeric id of this saved filter
|
||||
type: integer
|
||||
owner:
|
||||
$ref: '#/definitions/user.User'
|
||||
description: The user who owns this filter
|
||||
type: object
|
||||
title:
|
||||
description: The title of the filter.
|
||||
maxLength: 250
|
||||
minLength: 1
|
||||
type: string
|
||||
updated:
|
||||
description: A timestamp when this filter was last updated. You cannot change this value.
|
||||
type: string
|
||||
type: object
|
||||
models.Task:
|
||||
properties:
|
||||
assignees:
|
||||
|
@ -531,7 +559,7 @@ definitions:
|
|||
title:
|
||||
description: The task text. This is what you'll see in the list.
|
||||
maxLength: 250
|
||||
minLength: 3
|
||||
minLength: 1
|
||||
type: string
|
||||
updated:
|
||||
description: A timestamp when this task was last updated. You cannot change this value.
|
||||
|
@ -559,6 +587,40 @@ definitions:
|
|||
task_id:
|
||||
type: integer
|
||||
type: object
|
||||
models.TaskCollection:
|
||||
properties:
|
||||
filter_by:
|
||||
description: The field name of the field to filter by
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
filter_comparator:
|
||||
description: The comparator for field and value
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
filter_concat:
|
||||
description: The way all filter conditions are concatenated together, can be either "and" or "or".,
|
||||
type: string
|
||||
filter_include_nulls:
|
||||
description: If set to true, the result will also include null values
|
||||
type: boolean
|
||||
filter_value:
|
||||
description: The value of the field name to filter by
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
order_by:
|
||||
description: The query parameter to order the items by. This can be either asc or desc, with asc being the default.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
sort_by:
|
||||
description: The query parameter to sort by. This is for ex. done, priority, etc.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
models.TaskComment:
|
||||
properties:
|
||||
author:
|
||||
|
@ -615,7 +677,7 @@ definitions:
|
|||
name:
|
||||
description: The name of this team.
|
||||
maxLength: 250
|
||||
minLength: 5
|
||||
minLength: 1
|
||||
type: string
|
||||
updated:
|
||||
description: A timestamp when this relation was last updated. You cannot change this value.
|
||||
|
@ -697,7 +759,7 @@ definitions:
|
|||
username:
|
||||
description: The username of the user. Is always unique.
|
||||
maxLength: 250
|
||||
minLength: 3
|
||||
minLength: 1
|
||||
type: string
|
||||
type: object
|
||||
models.TeamWithRight:
|
||||
|
@ -723,7 +785,7 @@ definitions:
|
|||
name:
|
||||
description: The name of this team.
|
||||
maxLength: 250
|
||||
minLength: 5
|
||||
minLength: 1
|
||||
type: string
|
||||
right:
|
||||
type: integer
|
||||
|
@ -751,7 +813,7 @@ definitions:
|
|||
username:
|
||||
description: The username of the user. Is always unique.
|
||||
maxLength: 250
|
||||
minLength: 3
|
||||
minLength: 1
|
||||
type: string
|
||||
type: object
|
||||
todoist.Migration:
|
||||
|
@ -855,7 +917,7 @@ definitions:
|
|||
username:
|
||||
description: The username of the user. Is always unique.
|
||||
maxLength: 250
|
||||
minLength: 3
|
||||
minLength: 1
|
||||
type: string
|
||||
type: object
|
||||
v1.Token:
|
||||
|
@ -1069,6 +1131,130 @@ paths:
|
|||
summary: Search for a background from unsplash
|
||||
tags:
|
||||
- list
|
||||
/filters:
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Creates a new saved filter
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: The Saved Filter
|
||||
schema:
|
||||
$ref: '#/definitions/models.SavedFilter'
|
||||
"403":
|
||||
description: The user does not have access to that saved filter.
|
||||
schema:
|
||||
$ref: '#/definitions/web.HTTPError'
|
||||
"500":
|
||||
description: Internal error
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
security:
|
||||
- JWTKeyAuth: []
|
||||
summary: Creates a new saved filter
|
||||
tags:
|
||||
- filter
|
||||
/filters/{id}:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Removes a saved filter by its ID.
|
||||
parameters:
|
||||
- description: Filter ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: The Saved Filter
|
||||
schema:
|
||||
$ref: '#/definitions/models.SavedFilter'
|
||||
"403":
|
||||
description: The user does not have access to that saved filter.
|
||||
schema:
|
||||
$ref: '#/definitions/web.HTTPError'
|
||||
"404":
|
||||
description: The saved filter does not exist.
|
||||
schema:
|
||||
$ref: '#/definitions/web.HTTPError'
|
||||
"500":
|
||||
description: Internal error
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
security:
|
||||
- JWTKeyAuth: []
|
||||
summary: Removes a saved filter
|
||||
tags:
|
||||
- filter
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns a saved filter by its ID.
|
||||
parameters:
|
||||
- description: Filter ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: The Saved Filter
|
||||
schema:
|
||||
$ref: '#/definitions/models.SavedFilter'
|
||||
"403":
|
||||
description: The user does not have access to that saved filter.
|
||||
schema:
|
||||
$ref: '#/definitions/web.HTTPError'
|
||||
"500":
|
||||
description: Internal error
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
security:
|
||||
- JWTKeyAuth: []
|
||||
summary: Gets one saved filter
|
||||
tags:
|
||||
- filter
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Updates a saved filter by its ID.
|
||||
parameters:
|
||||
- description: Filter ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: The Saved Filter
|
||||
schema:
|
||||
$ref: '#/definitions/models.SavedFilter'
|
||||
"403":
|
||||
description: The user does not have access to that saved filter.
|
||||
schema:
|
||||
$ref: '#/definitions/web.HTTPError'
|
||||
"404":
|
||||
description: The saved filter does not exist.
|
||||
schema:
|
||||
$ref: '#/definitions/web.HTTPError'
|
||||
"500":
|
||||
description: Internal error
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
security:
|
||||
- JWTKeyAuth: []
|
||||
summary: Updates a saved filter
|
||||
tags:
|
||||
- filter
|
||||
/info:
|
||||
get:
|
||||
description: Returns the version, frontendurl, motd and various settings of Vikunja
|
||||
|
|
|
@ -46,7 +46,7 @@ type User struct {
|
|||
// The unique, numeric id of this user.
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
|
||||
// The username of the user. Is always unique.
|
||||
Username string `xorm:"varchar(250) not null unique" json:"username" valid:"length(1|250)" minLength:"3" maxLength:"250"`
|
||||
Username string `xorm:"varchar(250) not null unique" json:"username" valid:"length(1|250)" minLength:"1" maxLength:"250"`
|
||||
Password string `xorm:"varchar(250) not null" json:"-"`
|
||||
// The user's email address.
|
||||
Email string `xorm:"varchar(250) null" json:"email,omitempty" valid:"email,length(0|250)" maxLength:"250"`
|
||||
|
|
Loading…
Reference in a new issue