1
0
mirror of https://github.com/mattermost/focalboard.git synced 2024-11-27 08:31:20 +02:00

Add Members History support (#2595)

* Adding Members History support

* Adding unit tests for boards members history

* Changing insert_at to a milliseconds from epoch field

* Addressing PR review comment

* Addressing PR review comment

* Fix golangci-lint

* Fix migrations in postgres

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
Jesús Espino 2022-03-29 10:17:19 +02:00 committed by GitHub
parent f7fe5df8e8
commit 8212e5401e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 175 additions and 1 deletions

View File

@ -311,3 +311,23 @@ func (b *Board) IsValid() error {
}
return nil
}
// BoardMemberHistoryEntry stores the information of the membership of a user on a board
// swagger:model
type BoardMemberHistoryEntry struct {
// The ID of the board
// required: true
BoardID string `json:"boardId"`
// The ID of the user
// required: true
UserID string `json:"userId"`
// The action that added this history entry (created or deleted)
// required: false
Action string `json:"action"`
// The insertion time
// required: true
InsertAt int64 `json:"insertAt"`
}

View File

@ -505,6 +505,21 @@ func (mr *MockStoreMockRecorder) GetBoardAndCardByID(arg0 interface{}) *gomock.C
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardAndCardByID", reflect.TypeOf((*MockStore)(nil).GetBoardAndCardByID), arg0)
}
// GetBoardMemberHistory mocks base method.
func (m *MockStore) GetBoardMemberHistory(arg0, arg1 string, arg2 uint64) ([]*model.BoardMemberHistoryEntry, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBoardMemberHistory", arg0, arg1, arg2)
ret0, _ := ret[0].([]*model.BoardMemberHistoryEntry)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetBoardMemberHistory indicates an expected call of GetBoardMemberHistory.
func (mr *MockStoreMockRecorder) GetBoardMemberHistory(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardMemberHistory", reflect.TypeOf((*MockStore)(nil).GetBoardMemberHistory), arg0, arg1, arg2)
}
// GetBoardsForUserAndTeam mocks base method.
func (m *MockStore) GetBoardsForUserAndTeam(arg0, arg1 string) ([]*model.Board, error) {
m.ctrl.T.Helper()

View File

@ -151,6 +151,28 @@ func (s *SQLStore) boardMembersFromRows(rows *sql.Rows) ([]*model.BoardMember, e
return boardMembers, nil
}
func (s *SQLStore) boardMemberHistoryEntriesFromRows(rows *sql.Rows) ([]*model.BoardMemberHistoryEntry, error) {
boardMemberHistoryEntries := []*model.BoardMemberHistoryEntry{}
for rows.Next() {
var boardMemberHistoryEntry model.BoardMemberHistoryEntry
err := rows.Scan(
&boardMemberHistoryEntry.BoardID,
&boardMemberHistoryEntry.UserID,
&boardMemberHistoryEntry.Action,
&boardMemberHistoryEntry.InsertAt,
)
if err != nil {
return nil, err
}
boardMemberHistoryEntries = append(boardMemberHistoryEntries, &boardMemberHistoryEntry)
}
return boardMemberHistoryEntries, nil
}
func (s *SQLStore) getBoardByCondition(db sq.BaseRunner, conditions ...interface{}) (*model.Board, error) {
boards, err := s.getBoardsByCondition(db, conditions...)
if err != nil {
@ -405,6 +427,11 @@ func (s *SQLStore) saveMember(db sq.BaseRunner, bm *model.BoardMember) (*model.B
"scheme_viewer": bm.SchemeViewer,
}
oldMember, err := s.getMemberForBoard(db, bm.BoardID, bm.UserID)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, err
}
query := s.getQueryBuilder(db).
Insert(s.tablePrefix + "board_members").
SetMap(queryValues)
@ -425,6 +452,17 @@ func (s *SQLStore) saveMember(db sq.BaseRunner, bm *model.BoardMember) (*model.B
return nil, err
}
if oldMember == nil {
addToMembersHistory := s.getQueryBuilder(db).
Insert(s.tablePrefix+"board_members_history").
Columns("board_id", "user_id", "action", "insert_at").
Values(bm.BoardID, bm.UserID, "created", model.GetMillis())
if _, err := addToMembersHistory.Exec(); err != nil {
return nil, err
}
}
return bm, nil
}
@ -434,10 +472,27 @@ func (s *SQLStore) deleteMember(db sq.BaseRunner, boardID, userID string) error
Where(sq.Eq{"board_id": boardID}).
Where(sq.Eq{"user_id": userID})
if _, err := deleteQuery.Exec(); err != nil {
result, err := deleteQuery.Exec()
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected > 0 {
addToMembersHistory := s.getQueryBuilder(db).
Insert(s.tablePrefix+"board_members_history").
Columns("board_id", "user_id", "action", "insert_at").
Values(boardID, userID, "deleted", model.GetMillis())
if _, err := addToMembersHistory.Exec(); err != nil {
return err
}
}
return nil
}
@ -549,3 +604,30 @@ func (s *SQLStore) searchBoardsForUserAndTeam(db sq.BaseRunner, term, userID, te
return s.boardsFromRows(rows)
}
func (s *SQLStore) getBoardMemberHistory(db sq.BaseRunner, boardID, userID string, limit uint64) ([]*model.BoardMemberHistoryEntry, error) {
query := s.getQueryBuilder(db).
Select("board_id", "user_id", "action", "insert_at").
From(s.tablePrefix + "board_members_history").
Where(sq.Eq{"board_id": boardID}).
Where(sq.Eq{"user_id": userID}).
OrderBy("insert_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`getBoardMemberHistory ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
memberHistory, err := s.boardMemberHistoryEntriesFromRows(rows)
if err != nil {
return nil, err
}
return memberHistory, nil
}

View File

@ -0,0 +1 @@
DROP TABLE {{.prefix}}board_members_history;

View File

@ -0,0 +1,18 @@
CREATE TABLE {{.prefix}}board_members_history (
{{if .postgres}}id SERIAL PRIMARY KEY,{{end}}
{{if .sqlite}}id INTEGER PRIMARY KEY AUTOINCREMENT,{{end}}
{{if .mysql}}id INT PRIMARY KEY AUTO_INCREMENT,{{end}}
board_id VARCHAR(36) NOT NULL,
user_id VARCHAR(36) NOT NULL,
action VARCHAR(10),
insert_at BIGINT NOT NULL
) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}};
CREATE INDEX idx_boardmembershistory_user_id ON {{.prefix}}board_members_history(user_id);
CREATE INDEX idx_boardmembershistory_board_id_userid ON {{.prefix}}board_members_history(board_id, user_id);
INSERT INTO {{.prefix}}board_members_history (board_id, user_id, action, insert_at) SELECT board_id, user_id, 'created',
{{if .postgres}}CAST(extract(epoch from now()) * 1000 AS BIGINT){{end}}
{{if .sqlite}}strftime('%s')*1000{{end}}
{{if .mysql}}UNIX_TIMESTAMP(now())*1000{{end}}
from {{.prefix}}board_members;

View File

@ -309,6 +309,11 @@ func (s *SQLStore) GetBoardAndCardByID(blockID string) (*model.Board, *model.Blo
}
func (s *SQLStore) GetBoardMemberHistory(boardID string, userID string, limit uint64) ([]*model.BoardMemberHistoryEntry, error) {
return s.getBoardMemberHistory(s.db, boardID, userID, limit)
}
func (s *SQLStore) GetBoardsForUserAndTeam(userID string, teamID string) ([]*model.Board, error) {
return s.getBoardsForUserAndTeam(s.db, userID, teamID)

View File

@ -90,6 +90,7 @@ type Store interface {
SaveMember(bm *model.BoardMember) (*model.BoardMember, error)
DeleteMember(boardID, userID string) error
GetMemberForBoard(boardID, userID string) (*model.BoardMember, error)
GetBoardMemberHistory(boardID, userID string, limit uint64) ([]*model.BoardMemberHistoryEntry, error)
GetMembersForBoard(boardID string) ([]*model.BoardMember, error)
GetMembersForUser(userID string) ([]*model.BoardMember, error)
SearchBoardsForUserAndTeam(term, userID, teamID string) ([]*model.Board, error)

View File

@ -517,12 +517,20 @@ func testSaveMember(t *testing.T, store store.Store) {
SchemeAdmin: true,
}
memberHistory, err := store.GetBoardMemberHistory(boardID, userID, 0)
require.NoError(t, err)
initialMemberHistory := len(memberHistory)
nbm, err := store.SaveMember(bm)
require.NoError(t, err)
require.Equal(t, userID, nbm.UserID)
require.Equal(t, boardID, nbm.BoardID)
require.True(t, nbm.SchemeAdmin)
memberHistory, err = store.GetBoardMemberHistory(boardID, userID, 0)
require.NoError(t, err)
require.Len(t, memberHistory, initialMemberHistory+1)
})
t.Run("should correctly update a member", func(t *testing.T) {
@ -533,6 +541,10 @@ func testSaveMember(t *testing.T, store store.Store) {
SchemeViewer: true,
}
memberHistory, err := store.GetBoardMemberHistory(boardID, userID, 0)
require.NoError(t, err)
initialMemberHistory := len(memberHistory)
nbm, err := store.SaveMember(bm)
require.NoError(t, err)
require.Equal(t, userID, nbm.UserID)
@ -541,6 +553,10 @@ func testSaveMember(t *testing.T, store store.Store) {
require.False(t, nbm.SchemeAdmin)
require.True(t, nbm.SchemeEditor)
require.True(t, nbm.SchemeViewer)
memberHistory, err = store.GetBoardMemberHistory(boardID, userID, 0)
require.NoError(t, err)
require.Len(t, memberHistory, initialMemberHistory)
})
}
@ -626,7 +642,15 @@ func testDeleteMember(t *testing.T, store store.Store) {
boardID := testBoardID
t.Run("should return nil if deleting a nonexistent member", func(t *testing.T) {
memberHistory, err := store.GetBoardMemberHistory(boardID, userID, 0)
require.NoError(t, err)
initialMemberHistory := len(memberHistory)
require.NoError(t, store.DeleteMember(boardID, userID))
memberHistory, err = store.GetBoardMemberHistory(boardID, userID, 0)
require.NoError(t, err)
require.Len(t, memberHistory, initialMemberHistory)
})
t.Run("should correctly delete a member", func(t *testing.T) {
@ -640,11 +664,19 @@ func testDeleteMember(t *testing.T, store store.Store) {
require.NoError(t, err)
require.NotNil(t, nbm)
memberHistory, err := store.GetBoardMemberHistory(boardID, userID, 0)
require.NoError(t, err)
initialMemberHistory := len(memberHistory)
require.NoError(t, store.DeleteMember(boardID, userID))
rbm, err := store.GetMemberForBoard(boardID, userID)
require.ErrorIs(t, err, sql.ErrNoRows)
require.Nil(t, rbm)
memberHistory, err = store.GetBoardMemberHistory(boardID, userID, 0)
require.NoError(t, err)
require.Len(t, memberHistory, initialMemberHistory+1)
})
}