Skip to content

Commit

Permalink
feat(boards2): add views to list board members (#3803)
Browse files Browse the repository at this point in the history
PR adds two new views to list realm or board members:
- `/r/nt/boards2/v1:members`
- `/r/nt/boards2/v1:BOARD_NAME/members`

Views list the member address and assigned role.
  • Loading branch information
jeronimoalbi authored Feb 25, 2025
1 parent edf5629 commit 129cccb
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 10 deletions.
16 changes: 12 additions & 4 deletions examples/gno.land/r/nt/boards2/v1/board.gno
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ func (board *Board) GetID() BoardID {
return board.id
}

// GetName returns the name of the board.
func (board *Board) GetName() string {
return board.name
}

// GetURL returns the relative URL of the board.
func (board *Board) GetURL() string {
return strings.TrimPrefix(std.CurrentRealm().PkgPath(), "gno.land") + ":" + url.PathEscape(board.name)
Expand Down Expand Up @@ -105,13 +110,12 @@ func (board *Board) DeleteThread(pid PostID) {
//
// Pager is used for pagination if it's not nil.
func (board *Board) Render(p *PaginationOpts) string {
var sb strings.Builder

if board.threads.Size() == 0 {
sb.WriteString("*This board doesn't have any threads.*")
return sb.String()
return "*This board doesn't have any threads.*"
}

var sb strings.Builder

page := p.Iterate(&board.threads, func(_ string, v interface{}) bool {
p := v.(*Post)
if p.isHidden {
Expand Down Expand Up @@ -173,6 +177,10 @@ func (board *Board) GetPostFormURL() string {
return txlink.Call("CreateThread", "boardID", board.id.String())
}

func (board *Board) GetMembersURL() string {
return board.GetURL() + "/members"
}

func createDefaultBoardPermissions(owner std.Address) *DefaultPermissions {
perms := NewDefaultPermissions(commondao.New())
perms.SetSuperRole(RoleOwner)
Expand Down
15 changes: 15 additions & 0 deletions examples/gno.land/r/nt/boards2/v1/permissions.gno
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ type (
// Args is a list of generic arguments.
Args []interface{}

// User contains user info.
User struct {
Address std.Address
Roles []Role
}

// UsersIterFn defines a function type to iterate users.
UsersIterFn func(User) bool

// Permissions define an interface to for permissioned execution.
Permissions interface {
// HasRole checks if a user has a specific role assigned.
Expand All @@ -61,5 +70,11 @@ type (

// HasUser checks if a user exists.
HasUser(std.Address) bool

// UsersCount returns the total number of users the permissioner contains.
UsersCount() int

// IterateUsers iterates permissions' users.
IterateUsers(start, count int, fn UsersIterFn) bool
}
)
24 changes: 18 additions & 6 deletions examples/gno.land/r/nt/boards2/v1/permissions_default.gno
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,21 @@ func (dp DefaultPermissions) HasUser(user std.Address) bool {
return dp.users.Has(user.String())
}

// UsersCount returns the total number of users the permissioner contains.
func (dp DefaultPermissions) UsersCount() int {
return dp.users.Size()
}

// IterateUsers iterates permissions' users.
func (dp DefaultPermissions) IterateUsers(start, count int, fn UsersIterFn) bool {
return dp.users.IterateByOffset(start, count, func(k string, v interface{}) bool {
return fn(User{
Address: std.Address(k),
Roles: v.([]Role),
})
})
}

// WithPermission calls a callback when a user has a specific permission.
// It panics on error or when a handler panics.
// Callbacks are by default called when there is no handle registered for the permission.
Expand Down Expand Up @@ -194,8 +209,7 @@ func (dp DefaultPermissions) handleMemberInvite(args Args, cb func(Args)) {
}

if role == RoleOwner {
caller := std.OriginCaller()
if !dp.HasRole(caller, RoleOwner) {
if !dp.HasRole(std.OriginCaller(), RoleOwner) {
panic("only owners are allowed to invite other owners")
}
}
Expand All @@ -206,8 +220,7 @@ func (dp DefaultPermissions) handleMemberInvite(args Args, cb func(Args)) {
func (dp DefaultPermissions) handleRoleChange(args Args, cb func(Args)) {
// Owners and Admins can change roles.
// Admins should not be able to assign or remove the Owner role from members.
caller := std.OriginCaller()
if dp.HasRole(caller, RoleAdmin) {
if dp.HasRole(std.OriginCaller(), RoleAdmin) {
role, ok := args[2].(Role)
if !ok {
panic("expected a valid member role")
Expand Down Expand Up @@ -253,9 +266,8 @@ func assertValidBoardNameLength(name string) {
func assertBoardNameBelongsToCaller(name string) {
// When the board name is the name of a registered user
// check that caller is the owner of the name.
caller := std.OriginCaller()
user := users.GetUserByName(name)
if user != nil && user.Address != caller {
if user != nil && user.Address != std.OriginCaller() {
panic("board name is a user name registered to a different user")
}
}
75 changes: 75 additions & 0 deletions examples/gno.land/r/nt/boards2/v1/permissions_default_test.gno
Original file line number Diff line number Diff line change
Expand Up @@ -533,3 +533,78 @@ func TestDefaultPermissionsRemoveUser(t *testing.T) {
})
}
}

func TestDefaultPermissionsIterateUsers(t *testing.T) {
users := []User{
{
Address: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
Roles: []Role{"foo"},
},
{
Address: "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj",
Roles: []Role{"foo", "bar"},
},
{
Address: "g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5",
Roles: []Role{"bar"},
},
}

perms := NewDefaultPermissions(commondao.New())
perms.AddRole("foo", "perm1")
perms.AddRole("bar", "perm2")
for _, u := range users {
perms.AddUser(u.Address, u.Roles...)
}

cases := []struct {
name string
start, count, want int
}{
{
name: "exceed users count",
count: 50,
want: 3,
},
{
name: "exact users count",
count: 3,
want: 3,
},
{
name: "two users",
start: 1,
count: 2,
want: 2,
},
{
name: "one user",
start: 1,
count: 1,
want: 1,
},
{
name: "no iteration",
start: 50,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var i int
perms.IterateUsers(0, len(users), func(u User) bool {
urequire.True(t, i < len(users), "expect iterator to respect number of users")
uassert.Equal(t, users[i].Address, u.Address)

urequire.Equal(t, len(users[i].Roles), len(u.Roles), "expect number of roles to match")
for j, r := range u.Roles {
uassert.Equal(t, string(users[i].Roles[j]), string(u.Roles[j]))
}

i++
})

uassert.Equal(t, i, len(users), "expect iterator to iterate all users")
})
}
}
54 changes: 54 additions & 0 deletions examples/gno.land/r/nt/boards2/v1/render.gno
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package boards2

import (
"net/url"
"std"
"strconv"
"strings"

"gno.land/p/demo/mux"
"gno.land/p/jeronimoalbi/pager"
"gno.land/p/moul/txlink"
)

Expand All @@ -23,7 +25,9 @@ const (
func Render(path string) string {
router := mux.NewRouter()
router.HandleFunc("", renderBoardsList)
router.HandleFunc("members", renderMembers)
router.HandleFunc("{board}", renderBoard)
router.HandleFunc("{board}/members", renderMembers)
router.HandleFunc("{board}/{thread}", renderThread)
router.HandleFunc("{board}/{thread}/{reply}", renderReply)

Expand Down Expand Up @@ -66,8 +70,11 @@ func renderBoardListMenu(res *mux.ResponseWriter, req *mux.Request) {
res.Write("\n\n")

if menu == menuMembership {
path := strings.TrimPrefix(std.CurrentRealm().PkgPath(), "gno.land")

res.Write("↳")
res.Write(newButtonLink("invite", txlink.Call("InviteMember", "boardID", "0")) + " ")
res.Write(newButtonLink("members", path+":members") + " ")
res.Write(newButtonLink("remove member", txlink.Call("RemoveMember", "boardID", "0")) + " ")
res.Write(newButtonLink("change member role", txlink.Call("ChangeMemberRole", "boardID", "0")) + "\n\n")
}
Expand Down Expand Up @@ -117,6 +124,7 @@ func renderBoardMenu(board *Board, res *mux.ResponseWriter, req *mux.Request) {
res.Write(newButtonLink("change flagging threshold", board.GetFlaggingThresholdFormURL()) + "\n\n")
case menuMembership:
res.Write(newButtonLink("invite", board.GetInviteMemberFormURL()) + " ")
res.Write(newButtonLink("members", board.GetPath()+"/members") + " ")
res.Write(newButtonLink("remove member", board.GetRemoveMemberFormURL()) + " ")
res.Write(newButtonLink("change member role", board.GetChangeMemberRoleFormURL()) + "\n\n")
}
Expand Down Expand Up @@ -190,6 +198,52 @@ func renderReply(res *mux.ResponseWriter, req *mux.Request) {
res.Write(reply.RenderInner())
}

func renderMembers(res *mux.ResponseWriter, req *mux.Request) {
perms := gPerms
name := req.GetVar("board")
if name != "" {
v, found := gBoardsByName.Get(name)
if !found {
res.Write("Board does not exist: " + name)
return
}

board := v.(*Board)
perms = board.perms

res.Write("# Board Members: " + board.GetName() + "\n\n")
} else {
res.Write("# Boards Members\n\n")
}

p, err := pager.New(req.RawPath, perms.UsersCount())
if err != nil {
res.Write(err.Error())
return
}

perms.IterateUsers(p.Offset(), p.PageSize(), func(u User) bool {
res.Write("- " + u.Address.String() + " " + rolesToString(u.Roles) + "\n")
return false
})

if p.HasPages() {
res.Write("\n\n" + pager.Picker(p))
}
}

func rolesToString(roles []Role) string {
if len(roles) == 0 {
return ""
}

names := make([]string, len(roles))
for i, r := range roles {
names[i] = string(r)
}
return strings.Join(names, ", ")
}

func submenuURL(name string) string {
// TODO: Submenu URL works because no other GET arguments are being used
return "?submenu=" + name
Expand Down

0 comments on commit 129cccb

Please sign in to comment.