Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(boards2): add views to list board members #3803

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
Loading