Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions backend/main/leaderboard_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package main

import (
"encoding/json"
"net/http"
"testing"

"github.com/DapperCollectives/CAST/backend/main/test_utils"
"github.com/stretchr/testify/assert"
)
func TestGetCommunityLeaderboard(t *testing.T) {
clearTable("communities")
clearTable("community_users")
clearTable("proposals")
clearTable("votes")
communityId := otu.AddCommunities(1)[0]
proposalIdA := otu.AddActiveProposals(communityId, 1)[0]
proposalIdB := otu.AddActiveProposals(communityId, 2)[0]
proposalIdC := otu.AddActiveProposals(communityId, 3)[0]
voteChoice := "a"

otu.CreateVoteAPI(proposalIdA, otu.GenerateValidVotePayload("user1", proposalIdA, voteChoice))
otu.CreateVoteAPI(proposalIdB, otu.GenerateValidVotePayload("user1", proposalIdB, voteChoice))
otu.CreateVoteAPI(proposalIdC, otu.GenerateValidVotePayload("user1", proposalIdC, voteChoice))
otu.CreateVoteAPI(proposalIdA, otu.GenerateValidVotePayload("user2", proposalIdA, voteChoice))
otu.CreateVoteAPI(proposalIdB, otu.GenerateValidVotePayload("user2", proposalIdB, voteChoice))

response := otu.GetCommunityLeaderboardAPI(communityId)
checkResponseCode(t, http.StatusOK, response.Code)

var p test_utils.PaginatedResponseWithLeaderboardUser
json.Unmarshal(response.Body.Bytes(), &p)

assert.Equal(t, 2, len(p.Data))
assert.Equal(t, 3, p.Data[0].Score)
assert.Equal(t, 2, p.Data[1].Score)
}
70 changes: 41 additions & 29 deletions backend/main/models/community_users.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ type CommunityUserType struct {
Is_member bool `json:"isMember" validate:"required"`
}

type LeaderboardUser struct {
Addr string `json:"addr" validate:"required"`
Score int `json:"score" validate:"required"`
}

type UserTypes []string

var USER_TYPES = UserTypes{"member", "author", "admin"}
Expand Down Expand Up @@ -59,20 +64,17 @@ func GetUsersForCommunity(db *s.Database, communityId, start, count int) ([]Comm
LIMIT $2 OFFSET $3
`, communityId, count, start)

// If we get pgx.ErrNoRows, just return an empty array
// and obfuscate error
if err != nil && err.Error() != pgx.ErrNoRows.Error() {
return nil, 0, err
} else if err != nil && err.Error() == pgx.ErrNoRows.Error() {
return []CommunityUserType{}, 0, nil
}

// Get total number of users
var totalRecords int
var totalUsers int
countSql := `SELECT COUNT(*) FROM (SELECT addr FROM community_users WHERE community_id = $1 group BY community_users.addr) as temp_users_addr`
_ = db.Conn.QueryRow(db.Context, countSql, communityId).Scan(&totalRecords)
_ = db.Conn.QueryRow(db.Context, countSql, communityId).Scan(&totalUsers)

return users, totalRecords, nil
return users, totalUsers, nil
}

func GetUsersForCommunityByType(db *s.Database, communityId, start, count int, user_type string) ([]CommunityUser, int, error) {
Expand All @@ -83,20 +85,42 @@ func GetUsersForCommunityByType(db *s.Database, communityId, start, count int, u
LIMIT $3 OFFSET $4
`, communityId, user_type, count, start)

// If we get pgx.ErrNoRows, just return an empty array
// and obfuscate error
if err != nil && err.Error() != pgx.ErrNoRows.Error() {
return nil, 0, err
} else if err != nil && err.Error() == pgx.ErrNoRows.Error() {
return []CommunityUser{}, 0, nil
}

// Get total number of users by type
var totalRecords int
var totalUsers int
countSql := `SELECT COUNT(*) FROM community_users WHERE community_id = $1 AND user_type = $2`
_ = db.Conn.QueryRow(db.Context, countSql, communityId, user_type).Scan(&totalRecords)
_ = db.Conn.QueryRow(db.Context, countSql, communityId, user_type).Scan(&totalUsers)

return users, totalUsers, nil
}

func GetCommunityLeaderboard(db *s.Database, communityId, start, count int) ([]LeaderboardUser, int, error) {
var users = []LeaderboardUser{}
err := pgxscan.Select(db.Context, db.Conn, &users,
`
SELECT v.addr, count(*) AS score FROM votes v
JOIN proposals p ON p.id = v.proposal_id
WHERE p.community_id = $1
GROUP BY v.addr
ORDER BY score DESC
LIMIT $2 OFFSET $3
`, communityId, count, start)

if err != nil && err.Error() != pgx.ErrNoRows.Error() {
return nil, 0, err
} else if err != nil && err.Error() == pgx.ErrNoRows.Error() {
return []LeaderboardUser{}, 0, nil
}

var totalUsers int
countSql := `SELECT COUNT(*) FROM community_users WHERE community_id = $1`
_ = db.Conn.QueryRow(db.Context, countSql, communityId).Scan(&totalUsers)

return users, totalRecords, nil
return users, totalUsers, nil
}

func GetCommunitiesForUser(db *s.Database, addr string, start, count int) ([]UserCommunity, int, error) {
Expand All @@ -112,26 +136,23 @@ func GetCommunitiesForUser(db *s.Database, addr string, start, count int) ([]Use
LIMIT $2 OFFSET $3
`, addr, count, start)

// If we get pgx.ErrNoRows, just return an empty array
// and obfuscate error
if err != nil && err.Error() != pgx.ErrNoRows.Error() {
return nil, 0, err
} else if err != nil && err.Error() == pgx.ErrNoRows.Error() {
return []UserCommunity{}, 0, nil
}

// Get total number of communities by user
var totalRecords int
var totalCommunities int
countSql := `
SELECT
COUNT(communities.id)
FROM communities
LEFT JOIN community_users ON community_users.community_id = communities.id
WHERE community_users.addr = $1
`
_ = db.Conn.QueryRow(db.Context, countSql, addr).Scan(&totalRecords)
_ = db.Conn.QueryRow(db.Context, countSql, addr).Scan(&totalCommunities)

return communities, totalRecords, nil
return communities, totalCommunities, nil
}

func (u *CommunityUser) GetCommunityUser(db *s.Database) error {
Expand All @@ -149,8 +170,6 @@ func GetAllRolesForUserInCommunity(db *s.Database, addr string, communityId int)
SELECT * FROM community_users WHERE community_id = $1 AND addr = $2
`, communityId, addr)

// If we get pgx.ErrNoRows, just return an empty array
// and obfuscate error
if err != nil && err.Error() != pgx.ErrNoRows.Error() {
return nil, err
} else if err != nil && err.Error() == pgx.ErrNoRows.Error() {
Expand All @@ -166,15 +185,13 @@ func (u *CommunityUser) Remove(db *s.Database) error {
WHERE community_id = $1 AND addr = $2 AND user_type = $3
`, u.Community_id, u.Addr, u.User_type)

return err // will be nil unless something went wrong
return err
}

// Create any of the 3 roles for the user that they dont have already
func GrantAdminRolesToAddress(db *s.Database, communityId int, addr string) error {
userTypes := UserTypes{"admin", "author", "member"}
for _, role := range userTypes {
userRole := CommunityUser{Addr: addr, Community_id: communityId, User_type: role}
// Check if role exists. If we throw a ErrNoRows err, create the role
if err := userRole.GetCommunityUser(db); err != nil {
if err := userRole.CreateCommunityUser(db); err != nil {
log.Error().Err(err).Msgf("db error creating role %s for addr %s for communityId %d", role, addr, communityId)
Expand All @@ -185,12 +202,10 @@ func GrantAdminRolesToAddress(db *s.Database, communityId int, addr string) erro
return nil
}

// Create either author or member role for the user that they dont have already
func GrantAuthorRolesToAddress(db *s.Database, communityId int, addr string) error {
userTypes := UserTypes{"author", "member"}
for _, role := range userTypes {
userRole := CommunityUser{Addr: addr, Community_id: communityId, User_type: role}
// Check if role exists. If we throw a ErrNoRows err, create the role
if err := userRole.GetCommunityUser(db); err != nil {
if err := userRole.CreateCommunityUser(db); err != nil {
log.Error().Err(err).Msgf("db error creating role %s for addr %s for communityId %d", role, addr, communityId)
Expand All @@ -209,11 +224,9 @@ func (u *CommunityUser) CreateCommunityUser(db *s.Database) error {
RETURNING community_id, addr, user_type
`, u.Community_id, u.Addr, u.User_type).Scan(&u.Community_id, &u.Addr, &u.User_type)

return err // will be nil unless something went wrong
return err
}

// when a user creates a community, they are automatically assigned
// all roles
func GrantRolesToCommunityCreator(db *s.Database, addr string, communityId int) error {
for _, userType := range USER_TYPES {
communityUser := CommunityUser{Addr: addr, Community_id: communityId, User_type: userType}
Expand All @@ -225,7 +238,6 @@ func GrantRolesToCommunityCreator(db *s.Database, addr string, communityId int)
return nil
}

// Validate account's role
func EnsureRoleForCommunity(db *s.Database, addr string, communityId int, userType string) error {
user := CommunityUser{Addr: addr, Community_id: communityId, User_type: userType}
return user.GetCommunityUser(db)
Expand Down
30 changes: 30 additions & 0 deletions backend/main/server/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ func (a *App) initializeRoutes() {
Methods("GET")
a.Router.HandleFunc("/communities/{communityId:[0-9]+}/users/{addr:0x[a-zA-Z0-9]{16}}/{userType:[a-zA-Z]+}", a.handleRemoveUserRole).
Methods("DELETE", "OPTIONS")
a.Router.HandleFunc("/communities/{communityId:[0-9]+}/leaderboard", a.handleGetCommunityLeaderboard).Methods("GET")
// Utilities
a.Router.HandleFunc("/accounts/admin", a.getAdminList).Methods("GET")
a.Router.HandleFunc("/accounts/blocklist", a.getCommunityBlocklist).Methods("GET")
Expand Down Expand Up @@ -1654,6 +1655,35 @@ func (a *App) handleGetCommunityUsersByType(w http.ResponseWriter, r *http.Reque
respondWithJSON(w, http.StatusOK, response)
}

func (a *App) handleGetCommunityLeaderboard(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
communityId, err := strconv.Atoi(vars["communityId"])

if err != nil {
respondWithError(w, http.StatusBadRequest, "Invalid Community ID")
return
}

count, _ := strconv.Atoi(r.FormValue("count"))
start, _ := strconv.Atoi(r.FormValue("start"))
if count > 100 || count < 1 {
count = 100
}
if start < 0 {
start = 0
}

users, totalRecords, err := models.GetCommunityLeaderboard(a.DB, communityId, start, count)
if err != nil {
respondWithError(w, http.StatusInternalServerError, err.Error())
return
}

response := shared.GetPaginatedResponseWithPayload(users, start, count, totalRecords)
respondWithJSON(w, http.StatusOK, response)

}

func (a *App) handleGetUserCommunities(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
addr := vars["addr"]
Expand Down
14 changes: 14 additions & 0 deletions backend/main/test_utils/community_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ type PaginatedResponseWithUserType struct {
Next int `json:"next"`
}

type PaginatedResponseWithLeaderboardUser struct {
Data []models.LeaderboardUser `json:"data"`
Start int `json:"start"`
Count int `json:"count"`
TotalRecords int `json:"totalRecords"`
Next int `json:"next"`
}

var (
AdminAddr = "0xf8d6e0586b0a20c7"
UserOneAddr = "0x01cf0e2f2f715450"
Expand Down Expand Up @@ -175,6 +183,12 @@ func (otu *OverflowTestUtils) GetCommunityAPI(id int) *httptest.ResponseRecorder
return response
}

func (otu *OverflowTestUtils) GetCommunityLeaderboardAPI(id int) *httptest.ResponseRecorder {
req, _ := http.NewRequest("GET", "/communities/"+strconv.Itoa(id)+"/leaderboard", nil)
response := otu.ExecuteRequest(req)
return response
}

func (otu *OverflowTestUtils) GetCommunityUsersAPI(id int) *httptest.ResponseRecorder {
req, _ := http.NewRequest("GET", "/communities/"+strconv.Itoa(id)+"/users", nil)
response := otu.ExecuteRequest(req)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { Bin, ValidCheckMark, InvalidCheckMark } from 'components/Svg';
import { WrapperResponsive, Loader, AddButton } from 'components';
import { useCommunityUsers } from 'hooks';
Expand All @@ -23,8 +23,17 @@ export const CommunityUsersForm = ({
submitComponent,
validateEachAddress = false,
onClearField = () => {},
autoFocusOnLoad = false,
} = {}) => {
const canDeleteAddress = addrList.length > 1;

const refOnFirstInput = useRef();

useEffect(() => {
if (refOnFirstInput.current) {
refOnFirstInput.current.focus();
}
}, [refOnFirstInput]);
return (
<div className="border-light rounded-lg columns is-flex-direction-column is-mobile m-0 p-6 mb-6 p-4-mobile mb-4-mobile">
<div className="columns flex-1">
Expand Down Expand Up @@ -80,6 +89,11 @@ export const CommunityUsersForm = ({
width: '100%',
...(isInvalid ? {} : undefined),
}}
ref={
addrList.length === 1 && addr === '' && autoFocusOnLoad
? refOnFirstInput
: undefined
}
/>
<div
style={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ function CommunityEditorProfile({
}
if (
communityName.trim().length === 0 ||
communityDescription.trim() === body ||
(communityName === name && communityDescription === body)
(communityName === name &&
communityDescription === body &&
image.file === undefined)
) {
setEnableSave(false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { useJoinCommunity, useUserRoleOnCommunity } from 'hooks';
export default function JoinCommunityButton({
communityId,
setTotalMembers = () => {},
// callback to notify leaveCommunity was called
onLeaveCommunity = async () => {},
}) {
const { createCommunityUser, deleteUserFromCommunity } = useJoinCommunity();
const { injectedProvider, user } = useWebContext();
Expand All @@ -22,29 +24,36 @@ export default function JoinCommunityButton({
}, [user.addr, memberState]);

const refresh = (updateFn) => {
// setting this to null and then to a valid address retriggers query to get memberState
setAddr(null);
setAddr(user.addr);
setTotalMembers(updateFn);
};

const joinCommunity = () =>
createCommunityUser(communityId, user, injectedProvider).then(
({ success }) => {
if (success) {
refresh((totalMembers) => ++totalMembers);
}
}
const joinCommunity = async () => {
const { success } = await createCommunityUser(
communityId,
user,
injectedProvider
);
if (success) {
refresh((totalMembers) => ++totalMembers);
}
};

const leaveCommunity = () =>
deleteUserFromCommunity(communityId, user, injectedProvider).then(
({ success }) => {
if (success) {
refresh((totalMembers) => --totalMembers);
}
}
const leaveCommunity = async () => {
const { success } = await deleteUserFromCommunity(
communityId,
user,
injectedProvider
);

if (success) {
refresh((totalMembers) => --totalMembers);
await onLeaveCommunity();
}
};

if (!addr || (isMember !== true && isMember !== false)) return null;

return (
Expand Down
Loading