Enable searching users by full email or name
continuous-integration/drone/push Build is passing Details

This commit is contained in:
kolaente 2021-04-07 18:28:58 +02:00
parent 8ddc00bd29
commit 126f3acdc8
Signed by: konrad
GPG Key ID: F40E70337AB24C9B
14 changed files with 191 additions and 42 deletions

View File

@ -120,7 +120,7 @@ var userListCmd = &cobra.Command{
s := db.NewSession()
defer s.Close()
users, err := user.ListUsers(s, "")
users, err := user.ListAllUsers(s)
if err != nil {
_ = s.Rollback()
log.Fatalf("Error getting users: %s", err)

View File

@ -58,6 +58,7 @@
email: 'user7@example.com'
is_active: true
issuer: local
discoverable_by_email: true
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 8
@ -86,6 +87,7 @@
created: 2018-12-01 15:13:12
- id: 11
username: 'user11'
name: 'Some one else'
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
email: 'user11@example.com'
is_active: true
@ -94,10 +96,12 @@
created: 2018-12-01 15:13:12
- id: 12
username: 'user12'
name: 'Name with spaces'
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
email: 'user12@example.com'
is_active: true
issuer: local
discoverable_by_name: true
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 13

View File

@ -28,11 +28,7 @@ func TestUserList(t *testing.T) {
t.Run("Normal test", func(t *testing.T) {
rec, err := newTestRequestWithUser(t, http.MethodPost, apiv1.UserList, &testuser1, "", nil, nil)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `user1`)
assert.Contains(t, rec.Body.String(), `user2`)
assert.Contains(t, rec.Body.String(), `user3`)
assert.Contains(t, rec.Body.String(), `user4`)
assert.Contains(t, rec.Body.String(), `user5`)
assert.Equal(t, "null\n", rec.Body.String())
})
t.Run("Search for user3", func(t *testing.T) {
rec, err := newTestRequestWithUser(t, http.MethodPost, apiv1.UserList, &testuser1, "", map[string][]string{"s": {"user3"}}, nil)

View File

@ -0,0 +1,44 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package migration
import (
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
type users20210407170753 struct {
DiscoverableByName bool `xorm:"bool default false index" json:"-"`
DiscoverableByEmail bool `xorm:"bool default false index" json:"-"`
}
func (users20210407170753) TableName() string {
return "users"
}
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20210407170753",
Description: "Add discoverable by email or name columns",
Migrate: func(tx *xorm.Engine) error {
return tx.Sync2(users20210407170753{})
},
Rollback: func(tx *xorm.Engine) error {
return nil
},
})
}

View File

@ -93,6 +93,7 @@ func TestListUsersFromList(t *testing.T) {
IsActive: true,
Issuer: "local",
EmailRemindersEnabled: true,
DiscoverableByEmail: true,
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -129,6 +130,7 @@ func TestListUsersFromList(t *testing.T) {
testuser11 := &user.User{
ID: 11,
Username: "user11",
Name: "Some one else",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
IsActive: true,
Issuer: "local",
@ -139,10 +141,12 @@ func TestListUsersFromList(t *testing.T) {
testuser12 := &user.User{
ID: 12,
Username: "user12",
Name: "Name with spaces",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
IsActive: true,
Issuer: "local",
EmailRemindersEnabled: true,
DiscoverableByName: true,
Created: testCreatedTime,
Updated: testUpdatedTime,
}

View File

@ -31,11 +31,11 @@ import (
// UserList gets all information about a user
// @Summary Get users
// @Description Lists all users (without emailadresses). Also possible to search for a specific user.
// @Description Search for a user by its username, name or full email. Name (not username) or email require that the user has enabled this in their settings.
// @tags user
// @Accept json
// @Produce json
// @Param s query string false "Search for a user by its name."
// @Param s query string false "The search criteria."
// @Security JWTKeyAuth
// @Success 200 {array} user.User "All (found) users."
// @Failure 400 {object} web.HTTPError "Something's invalid."

View File

@ -38,7 +38,11 @@ type UserSettings struct {
// The new name of the current user.
Name string `json:"name"`
// If enabled, sends email reminders of tasks to the user.
EmailRemindersEnabled bool `xorm:"bool default false" json:"email_reminders_enabled"`
EmailRemindersEnabled bool `json:"email_reminders_enabled"`
// If true, this user can be found by their name or parts of it when searching for it.
DiscoverableByName bool `json:"discoverable_by_name"`
// If true, the user can be found when searching for their exact email.
DiscoverableByEmail bool `json:"discoverable_by_email"`
}
// GetUserAvatarProvider returns the currently set user avatar
@ -161,6 +165,8 @@ func UpdateGeneralUserSettings(c echo.Context) error {
user.Name = us.Name
user.EmailRemindersEnabled = us.EmailRemindersEnabled
user.DiscoverableByEmail = us.DiscoverableByEmail
user.DiscoverableByName = us.DiscoverableByName
_, err = user2.UpdateUser(s, user)
if err != nil {

View File

@ -19,6 +19,8 @@ package v1
import (
"net/http"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/auth"
@ -28,6 +30,11 @@ import (
"github.com/labstack/echo/v4"
)
type userWithSettings struct {
user.User
Settings *UserSettings `json:"settings"`
}
// UserShow gets all informations about the current user
// @Summary Get user information
// @Description Returns the current user object.
@ -48,10 +55,20 @@ func UserShow(c echo.Context) error {
s := db.NewSession()
defer s.Close()
user, err := models.GetUserOrLinkShareUser(s, a)
u, err := models.GetUserOrLinkShareUser(s, a)
if err != nil {
return handler.HandleHTTPError(err, c)
}
return c.JSON(http.StatusOK, user)
us := &userWithSettings{
User: *u,
Settings: &UserSettings{
Name: u.Name,
EmailRemindersEnabled: u.EmailRemindersEnabled,
DiscoverableByName: u.DiscoverableByName,
DiscoverableByEmail: u.DiscoverableByEmail,
},
}
return c.JSON(http.StatusOK, us)
}

View File

@ -6976,7 +6976,7 @@ var doc = `{
"JWTKeyAuth": []
}
],
"description": "Lists all users (without emailadresses). Also possible to search for a specific user.",
"description": "Search for a user by its username, name or full email. Name (not username) or email require that the user has enabled this in their settings.",
"consumes": [
"application/json"
],
@ -6990,7 +6990,7 @@ var doc = `{
"parameters": [
{
"type": "string",
"description": "Search for a user by its name.",
"description": "The search criteria.",
"name": "s",
"in": "query"
}
@ -8534,6 +8534,14 @@ var doc = `{
"v1.UserSettings": {
"type": "object",
"properties": {
"discoverable_by_email": {
"description": "If true, the user can be found when searching for their exact email.",
"type": "boolean"
},
"discoverable_by_name": {
"description": "If true, this user can be found by their name or parts of it when searching for it.",
"type": "boolean"
},
"email_reminders_enabled": {
"description": "If enabled, sends email reminders of tasks to the user.",
"type": "boolean"

View File

@ -6959,7 +6959,7 @@
"JWTKeyAuth": []
}
],
"description": "Lists all users (without emailadresses). Also possible to search for a specific user.",
"description": "Search for a user by its username, name or full email. Name (not username) or email require that the user has enabled this in their settings.",
"consumes": [
"application/json"
],
@ -6973,7 +6973,7 @@
"parameters": [
{
"type": "string",
"description": "Search for a user by its name.",
"description": "The search criteria.",
"name": "s",
"in": "query"
}
@ -8517,6 +8517,14 @@
"v1.UserSettings": {
"type": "object",
"properties": {
"discoverable_by_email": {
"description": "If true, the user can be found when searching for their exact email.",
"type": "boolean"
},
"discoverable_by_name": {
"description": "If true, this user can be found by their name or parts of it when searching for it.",
"type": "boolean"
},
"email_reminders_enabled": {
"description": "If enabled, sends email reminders of tasks to the user.",
"type": "boolean"

View File

@ -1079,6 +1079,12 @@ definitions:
type: object
v1.UserSettings:
properties:
discoverable_by_email:
description: If true, the user can be found when searching for their exact email.
type: boolean
discoverable_by_name:
description: If true, this user can be found by their name or parts of it when searching for it.
type: boolean
email_reminders_enabled:
description: If enabled, sends email reminders of tasks to the user.
type: boolean
@ -5662,9 +5668,9 @@ paths:
get:
consumes:
- application/json
description: Lists all users (without emailadresses). Also possible to search for a specific user.
description: Search for a user by its username, name or full email. Name (not username) or email require that the user has enabled this in their settings.
parameters:
- description: Search for a user by its name.
- description: The search criteria.
in: query
name: s
type: string

View File

@ -70,6 +70,9 @@ type User struct {
// If enabled, sends email reminders of tasks to the user.
EmailRemindersEnabled bool `xorm:"bool default true" json:"-"`
DiscoverableByName bool `xorm:"bool default false index" json:"-"`
DiscoverableByEmail bool `xorm:"bool default false index" json:"-"`
// A timestamp when this task was created. You cannot change this value.
Created time.Time `xorm:"created not null" json:"created"`
// A timestamp when this task was last updated. You cannot change this value.
@ -366,6 +369,8 @@ func UpdateUser(s *xorm.Session, user *User) (updatedUser *User, err error) {
"is_active",
"name",
"email_reminders_enabled",
"discoverable_by_name",
"discoverable_by_email",
).
Update(user)
if err != nil {

View File

@ -373,10 +373,63 @@ func TestListUsers(t *testing.T) {
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "")
all, err := ListAllUsers(s)
assert.NoError(t, err)
assert.Len(t, all, 14)
})
t.Run("no search term", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "")
assert.NoError(t, err)
assert.Len(t, all, 0)
})
t.Run("not discoverable by email", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "user1@example.com")
assert.NoError(t, err)
assert.Len(t, all, 0)
db.AssertExists(t, "users", map[string]interface{}{
"email": "user1@example.com",
}, false)
})
t.Run("not discoverable by name", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "one else")
assert.NoError(t, err)
assert.Len(t, all, 0)
db.AssertExists(t, "users", map[string]interface{}{
"name": "Some one else",
}, false)
})
t.Run("discoverable by email", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "user7@example.com")
assert.NoError(t, err)
assert.Len(t, all, 1)
assert.Equal(t, int64(7), all[0].ID)
})
t.Run("discoverable by partial name", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "with space")
assert.NoError(t, err)
assert.Len(t, all, 1)
assert.Equal(t, int64(12), all[0].ID)
})
}
func TestUserPasswordReset(t *testing.T) {

View File

@ -17,42 +17,40 @@
package user
import (
"strconv"
"strings"
"xorm.io/builder"
"xorm.io/xorm"
"code.vikunja.io/api/pkg/log"
)
// ListUsers returns a list with all users, filtered by an optional searchstring
func ListUsers(s *xorm.Session, searchterm string) (users []*User, err error) {
func ListUsers(s *xorm.Session, search string) (users []*User, err error) {
vals := strings.Split(searchterm, ",")
ids := []int64{}
for _, val := range vals {
v, err := strconv.ParseInt(val, 10, 64)
if err != nil {
log.Debugf("User search string part '%s' is not a number: %s", val, err)
continue
}
ids = append(ids, v)
}
// Prevent searching for placeholders
search = strings.ReplaceAll(search, "%", "")
if len(ids) > 0 {
err = s.
In("id", ids).
Find(&users)
return
}
if searchterm == "" {
err = s.Find(&users)
if search == "" || strings.ReplaceAll(search, " ", "") == "" {
return
}
err = s.
Where("username LIKE ?", "%"+searchterm+"%").
Where(builder.Or(
builder.Like{"username", "%" + search + "%"},
builder.And(
builder.Eq{"email": search},
builder.Eq{"discoverable_by_email": true},
),
builder.And(
builder.Like{"name", "%" + search + "%"},
builder.Eq{"discoverable_by_name": true},
),
)).
Find(&users)
return
}
// ListAllUsers returns all users
func ListAllUsers(s *xorm.Session) (users []*User, err error) {
err = s.Find(&users)
return
}