partial implementation of community profile page - next, straighten out left menus
This commit is contained in:
+136
@@ -0,0 +1,136 @@
|
|||||||
|
/*
|
||||||
|
* Amsterdam Web Communities System
|
||||||
|
* Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
// Package main contains the high-level Amsterdam logic.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.erbosoft.com/amy/amsterdam/database"
|
||||||
|
"git.erbosoft.com/amy/amsterdam/ui"
|
||||||
|
"github.com/biter777/countries"
|
||||||
|
"golang.org/x/text/language/display"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ShowCommunity(ctxt ui.AmContext) (string, any, error) {
|
||||||
|
me := ctxt.CurrentUser()
|
||||||
|
prefs, err := me.Prefs()
|
||||||
|
if err != nil {
|
||||||
|
return ui.ErrorPage(ctxt, err)
|
||||||
|
}
|
||||||
|
globals, err := database.AmGlobals()
|
||||||
|
if err != nil {
|
||||||
|
return ui.ErrorPage(ctxt, err)
|
||||||
|
}
|
||||||
|
globalFlags, err := globals.Flags()
|
||||||
|
if err != nil {
|
||||||
|
return ui.ErrorPage(ctxt, err)
|
||||||
|
}
|
||||||
|
comm, err := database.AmGetCommunityFromParam(ctxt.URLParam("cid"))
|
||||||
|
if err != nil {
|
||||||
|
ctxt.SetRC(http.StatusNotFound)
|
||||||
|
return ui.ErrorPage(ctxt, err)
|
||||||
|
}
|
||||||
|
member, _, level, err := comm.Membership(me)
|
||||||
|
if err != nil {
|
||||||
|
return ui.ErrorPage(ctxt, err)
|
||||||
|
}
|
||||||
|
effectiveLevel := me.BaseLevel
|
||||||
|
if member && level > effectiveLevel {
|
||||||
|
effectiveLevel = level
|
||||||
|
}
|
||||||
|
ci, err := comm.ContactInfo()
|
||||||
|
if err != nil {
|
||||||
|
return ui.ErrorPage(ctxt, err)
|
||||||
|
}
|
||||||
|
host, err := comm.Host()
|
||||||
|
if err != nil {
|
||||||
|
return ui.ErrorPage(ctxt, err)
|
||||||
|
}
|
||||||
|
var cats []*database.Category
|
||||||
|
if !globalFlags.Get(database.GlobalFlagNoCategories) {
|
||||||
|
cats, err = database.AmGetCategoryHierarchy(comm.CategoryId)
|
||||||
|
if err != nil {
|
||||||
|
return ui.ErrorPage(ctxt, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var pvtAddr bool
|
||||||
|
if database.AmTestPermission("Global.SeeHiddenContactInfo", effectiveLevel) {
|
||||||
|
pvtAddr = false
|
||||||
|
} else {
|
||||||
|
pvtAddr = ci.PrivateAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt.VarMap().Set("commName", comm.Name)
|
||||||
|
// TODO: set photo URL
|
||||||
|
tz := prefs.Location()
|
||||||
|
loc := prefs.Localizer()
|
||||||
|
ctxt.VarMap().Set("dateCreated", loc.Strftime("%x %X", comm.CreateDate.In(tz)))
|
||||||
|
if comm.LastAccess != nil {
|
||||||
|
ctxt.VarMap().Set("dateLastAccess", loc.Strftime("%x %X", (*comm.LastAccess).In(tz)))
|
||||||
|
}
|
||||||
|
if comm.LastUpdate != nil {
|
||||||
|
ctxt.VarMap().Set("dateLastUpdate", loc.Strftime("%x %X", (*comm.LastUpdate).In(tz)))
|
||||||
|
}
|
||||||
|
if !member && effectiveLevel >= comm.JoinLevel {
|
||||||
|
ctxt.VarMap().Set("canJoin", true)
|
||||||
|
}
|
||||||
|
if member && !me.IsAnon {
|
||||||
|
ctxt.VarMap().Set("canInvite", true)
|
||||||
|
}
|
||||||
|
ctxt.VarMap().Set("public", comm.Public())
|
||||||
|
if !globalFlags.Get(database.GlobalFlagNoCategories) {
|
||||||
|
ctxt.VarMap().Set("categories", cats)
|
||||||
|
}
|
||||||
|
if comm.Synopsis != nil && *comm.Synopsis != "" {
|
||||||
|
ctxt.VarMap().Set("description", *comm.Synopsis)
|
||||||
|
}
|
||||||
|
ctxt.VarMap().Set("hostName", host.Username)
|
||||||
|
if ci.Company != nil && *ci.Company != "" {
|
||||||
|
ctxt.VarMap().Set("company", *ci.Company)
|
||||||
|
}
|
||||||
|
if !pvtAddr && ci.Addr1 != nil && *ci.Addr1 != "" {
|
||||||
|
ctxt.VarMap().Set("addr1", *ci.Addr1)
|
||||||
|
}
|
||||||
|
if !pvtAddr && ci.Addr2 != nil && *ci.Addr2 != "" {
|
||||||
|
ctxt.VarMap().Set("addr2", *ci.Addr2)
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
if ci.Locality != nil {
|
||||||
|
b.WriteString(*ci.Locality)
|
||||||
|
if ci.Region != nil {
|
||||||
|
b.WriteString(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ci.Region != nil {
|
||||||
|
b.WriteString(*ci.Region)
|
||||||
|
}
|
||||||
|
if ci.PostalCode != nil {
|
||||||
|
b.WriteString(" " + *ci.PostalCode)
|
||||||
|
}
|
||||||
|
ctxt.VarMap().Set("addrLast", b.String())
|
||||||
|
if ci.Country != nil && *ci.Country != "" {
|
||||||
|
country := countries.ByName(*ci.Country)
|
||||||
|
ctxt.VarMap().Set("country", country.String())
|
||||||
|
}
|
||||||
|
tag, err := comm.LanguageTag()
|
||||||
|
if err == nil && tag != nil {
|
||||||
|
ctxt.VarMap().Set("language", display.Languages(*prefs.LanguageTag()).Name(tag))
|
||||||
|
}
|
||||||
|
if comm.Rules != nil && *comm.Rules != "" {
|
||||||
|
ctxt.VarMap().Set("rules", *comm.Rules)
|
||||||
|
}
|
||||||
|
if ci.URL != nil && *ci.URL != "" {
|
||||||
|
ctxt.VarMap().Set("homePage", *ci.URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt.VarMap().Set("amsterdam_pageTitle", "Community Profile: "+comm.Name)
|
||||||
|
return "framed_template", "comprofile.jet", nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* Amsterdam Web Communities System
|
||||||
|
* Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
// The database package contains database management and storage logic.
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Category is the structure defining a category.
|
||||||
|
type Category struct {
|
||||||
|
CatId int32 `db:"catid"`
|
||||||
|
Parent int32 `db:"parent"`
|
||||||
|
SymLink int32 `db:"symlink"`
|
||||||
|
HideDirectory int32 `db:"hide_dir"`
|
||||||
|
HideSearch int32 `db:"hide_search"`
|
||||||
|
Name string `db:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// allCategories is the list of all categories loaded from the database.
|
||||||
|
var allCategories []Category
|
||||||
|
|
||||||
|
// categoryIdMap maps IDs to categories.
|
||||||
|
var categoryIdMap map[int32]*Category = make(map[int32]*Category)
|
||||||
|
|
||||||
|
// categoryMutex syncs the loading of the categories.
|
||||||
|
var categoryMutex sync.Mutex
|
||||||
|
|
||||||
|
// loadCategories loads the categories list from the database.
|
||||||
|
func loadCategories() error {
|
||||||
|
categoryMutex.Lock()
|
||||||
|
defer categoryMutex.Unlock()
|
||||||
|
if allCategories == nil {
|
||||||
|
rs, err := amdb.Query("SELECT COUNT(*) FROM refcategory")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !rs.Next() {
|
||||||
|
return errors.New("internal error loading categories")
|
||||||
|
}
|
||||||
|
var ncats int32
|
||||||
|
rs.Scan(&ncats)
|
||||||
|
allCategories = make([]Category, 0, ncats)
|
||||||
|
err = amdb.Select(&allCategories, "SELECT * FROM refcategory ORDER BY parent, name")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, c := range allCategories {
|
||||||
|
categoryIdMap[c.CatId] = &(allCategories[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AmGetCategory returns the category for the given name.
|
||||||
|
* Parameters:
|
||||||
|
* catid - The ID of the category to get.
|
||||||
|
* Returns:
|
||||||
|
* Pointer to the appropriate Category, or nil.
|
||||||
|
* Standard Go error status.
|
||||||
|
*/
|
||||||
|
func AmGetCategory(catid int32) (*Category, error) {
|
||||||
|
err := loadCategories()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c := categoryIdMap[catid]
|
||||||
|
d := 5
|
||||||
|
for c.SymLink != -1 {
|
||||||
|
d--
|
||||||
|
if d == 0 {
|
||||||
|
return nil, errors.New("symlink resolution error")
|
||||||
|
}
|
||||||
|
c = categoryIdMap[c.SymLink]
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AmGetCategoryHierarchy returns the category hierarchy for the given ID.
|
||||||
|
* Parameters:
|
||||||
|
* catid - The ID of the category to get.
|
||||||
|
* Returns:
|
||||||
|
* Array of pointers to the categories in hierarchical order, or nil.
|
||||||
|
* Standard Go error status.
|
||||||
|
*/
|
||||||
|
func AmGetCategoryHierarchy(catid int32) ([]*Category, error) {
|
||||||
|
err := loadCategories()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// walk all the way to the "root" (parent = -1)
|
||||||
|
p := catid
|
||||||
|
ia := make([]*Category, 0, 3)
|
||||||
|
for p != -1 {
|
||||||
|
c := categoryIdMap[p]
|
||||||
|
for c.SymLink != -1 {
|
||||||
|
c = categoryIdMap[c.SymLink]
|
||||||
|
}
|
||||||
|
ia = append(ia, c)
|
||||||
|
p = c.Parent
|
||||||
|
}
|
||||||
|
// reverse the array for return
|
||||||
|
rc := make([]*Category, 0, len(ia))
|
||||||
|
for i := range ia {
|
||||||
|
rc = append(rc, ia[len(ia)-(i+1)])
|
||||||
|
}
|
||||||
|
return rc, nil
|
||||||
|
}
|
||||||
+151
-1
@@ -12,10 +12,13 @@ package database
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.erbosoft.com/amy/amsterdam/util"
|
||||||
lru "github.com/hashicorp/golang-lru"
|
lru "github.com/hashicorp/golang-lru"
|
||||||
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Community struct contains the high level data for a community.
|
// Community struct contains the high level data for a community.
|
||||||
@@ -52,6 +55,27 @@ var communityCache *lru.TwoQueueCache = nil
|
|||||||
// getCommunityMutex is a mutex on AmGetCommunity.
|
// getCommunityMutex is a mutex on AmGetCommunity.
|
||||||
var getCommunityMutex sync.Mutex
|
var getCommunityMutex sync.Mutex
|
||||||
|
|
||||||
|
// memberCacheData caches membership information for communities.
|
||||||
|
type memberCacheData struct {
|
||||||
|
isMember bool
|
||||||
|
locked bool
|
||||||
|
level uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
// memberCache contains the memberCacheData entries.
|
||||||
|
var memberCache *lru.Cache = nil
|
||||||
|
|
||||||
|
// memberMutex syncs access to the memberCache.
|
||||||
|
var memberMutex sync.Mutex
|
||||||
|
|
||||||
|
// stuffMembership stuffs a membership record into the cache.
|
||||||
|
func stuffMembership(cid int32, uid int32, member bool, locked bool, level uint16) {
|
||||||
|
key := fmt.Sprintf("%d:%d", cid, uid)
|
||||||
|
memberMutex.Lock()
|
||||||
|
memberCache.Add(key, &memberCacheData{isMember: member, locked: locked, level: level})
|
||||||
|
memberMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// init initializes the community cache.
|
// init initializes the community cache.
|
||||||
func init() {
|
func init() {
|
||||||
var err error
|
var err error
|
||||||
@@ -59,6 +83,98 @@ func init() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
memberCache, err = lru.New(250)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public returns true if the community is public.
|
||||||
|
func (c *Community) Public() bool {
|
||||||
|
return c.JoinKey == nil || *c.JoinKey == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContactInfo returns the contact info structure for the community.
|
||||||
|
func (c *Community) ContactInfo() (*ContactInfo, error) {
|
||||||
|
if c.ContactId < 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return AmGetContactInfo(c.ContactId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host returns the reference to the host of the community.
|
||||||
|
func (c *Community) Host() (*User, error) {
|
||||||
|
if c.HostUid == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return AmGetUser(*c.HostUid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Community) LanguageTag() (*language.Tag, error) {
|
||||||
|
if c.Language == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
t, err := language.Parse(*c.Language)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Membership returns the details of the specified user's membership in the community.
|
||||||
|
* Parameters:
|
||||||
|
* u - The user to check the membership of.
|
||||||
|
* Returns:
|
||||||
|
* true if the user is a member, false if not.
|
||||||
|
* true if the user's membership is "locked" (cannot unjoin), false if not.
|
||||||
|
* User's access level in the community, or 0 if the user is not a member.
|
||||||
|
* Standard Go error status.
|
||||||
|
*/
|
||||||
|
func (c *Community) Membership(u *User) (bool, bool, uint16, error) {
|
||||||
|
key := fmt.Sprintf("%d:%d", c.Id, u.Uid)
|
||||||
|
memberMutex.Lock()
|
||||||
|
defer memberMutex.Unlock()
|
||||||
|
mbr, ok := memberCache.Get(key)
|
||||||
|
if ok {
|
||||||
|
m := mbr.(*memberCacheData)
|
||||||
|
return m.isMember, m.locked, m.level, nil
|
||||||
|
}
|
||||||
|
if AmTestPermission("Community.NoJoinRequired", u.BaseLevel) {
|
||||||
|
// "no join required" - they are effectively a member, but don't cache that
|
||||||
|
return true, false, u.BaseLevel, nil
|
||||||
|
}
|
||||||
|
rs, err := amdb.Query("SELECT locked, granted_lvl FROM commmember WHERE commid = ? AND uid = ?", c.Id, u.Uid)
|
||||||
|
if err == nil {
|
||||||
|
if rs.Next() {
|
||||||
|
var locked bool
|
||||||
|
var level uint16
|
||||||
|
rs.Scan(&locked, &level)
|
||||||
|
memberCache.Add(key, &memberCacheData{isMember: true, locked: locked, level: level})
|
||||||
|
return true, locked, level, nil
|
||||||
|
}
|
||||||
|
memberCache.Add(key, &memberCacheData{isMember: false, locked: false, level: uint16(0)})
|
||||||
|
}
|
||||||
|
return false, false, uint16(0), err
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TestPermission is shorthand that tests if a user has a permission with respect to the community.
|
||||||
|
* Parameters:
|
||||||
|
* user - The user to be checked.
|
||||||
|
* perm - The permission to be tested.
|
||||||
|
* Returns:
|
||||||
|
* true if the user has the permission, false if not.
|
||||||
|
* Standard Go error status.
|
||||||
|
*/
|
||||||
|
func (c *Community) TestPermission(user *User, perm string) (bool, error) {
|
||||||
|
member, _, level, err := c.Membership(user)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
effectiveLevel := user.BaseLevel
|
||||||
|
if member && level > effectiveLevel {
|
||||||
|
effectiveLevel = level
|
||||||
|
}
|
||||||
|
return AmTestPermission(perm, effectiveLevel), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/* AmGetCommunity returns a reference to the specified community.
|
/* AmGetCommunity returns a reference to the specified community.
|
||||||
@@ -79,7 +195,9 @@ func AmGetCommunity(id int32) (*Community, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(dbdata) > 1 {
|
if len(dbdata) == 0 {
|
||||||
|
return nil, fmt.Errorf("community with ID %d not found", id)
|
||||||
|
} else if len(dbdata) > 1 {
|
||||||
return nil, fmt.Errorf("AmGetCommunity(%d): too many responses(%d)", id, len(dbdata))
|
return nil, fmt.Errorf("AmGetCommunity(%d): too many responses(%d)", id, len(dbdata))
|
||||||
}
|
}
|
||||||
rc = &(dbdata[0])
|
rc = &(dbdata[0])
|
||||||
@@ -88,6 +206,37 @@ func AmGetCommunity(id int32) (*Community, error) {
|
|||||||
return rc.(*Community), err
|
return rc.(*Community), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* AmGetCommunityFromParam returns a reference to the specified community based on the parameter.
|
||||||
|
* If the parameter is numeric, it's interpreted as a community ID. Otherwise, it's interpreted
|
||||||
|
* as a community alias.
|
||||||
|
* Parameters:
|
||||||
|
* id - The ID of the community.
|
||||||
|
* Returns:
|
||||||
|
* Pointer to Community containing community data, or nil
|
||||||
|
* Standard Go error status
|
||||||
|
*/
|
||||||
|
func AmGetCommunityFromParam(param string) (*Community, error) {
|
||||||
|
if util.IsNumeric(param) {
|
||||||
|
v, _ := strconv.Atoi(param)
|
||||||
|
c, err := AmGetCommunity(int32(v))
|
||||||
|
if err == nil {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
// else fall through to trying as alias
|
||||||
|
}
|
||||||
|
rs, err := amdb.Query("SELECT commid FROM communities WHERE alias = ?", param)
|
||||||
|
if err == nil {
|
||||||
|
if rs.Next() {
|
||||||
|
var cid int32
|
||||||
|
rs.Scan(&cid)
|
||||||
|
return AmGetCommunity(cid)
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("community with alias \"%s\" not found", param)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
/* AmGetCommunitiesForUser returns a list of communities the user is a member of.
|
/* AmGetCommunitiesForUser returns a list of communities the user is a member of.
|
||||||
* Parameters:
|
* Parameters:
|
||||||
* uid - The ID of the user.
|
* uid - The ID of the user.
|
||||||
@@ -164,6 +313,7 @@ func AmAutoJoinCommunities(user *User) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
stuffMembership(cid, user.Uid, true, lock, grantLevel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
/*
|
||||||
|
* Amsterdam Web Communities System
|
||||||
|
* Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
// The database package contains database management and storage logic.
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.erbosoft.com/amy/amsterdam/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Globals contains the global data.
|
||||||
|
type Globals struct {
|
||||||
|
Mutex sync.Mutex
|
||||||
|
PostsPerPage int32 `db:"posts_per_page"`
|
||||||
|
OldPostsAtTop int32 `db:"old_posts_at_top"`
|
||||||
|
MaxSearchPage int32 `db:"max_search_page"`
|
||||||
|
MaxCommunityMemberPage int32 `db:"max_comm_mbr_page"`
|
||||||
|
MaxConferenceMemberPage int32 `db:"max_conf_mbr_page"`
|
||||||
|
FrontPagePosts int32 `db:"fp_posts"`
|
||||||
|
NumAuditPage int32 `db:"num_audit_page"`
|
||||||
|
CommunityCreateLevel int32 `db:"comm_create_lvl"`
|
||||||
|
flags *util.OptionSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalProperties contains global property entries.
|
||||||
|
type GlobalProperties struct {
|
||||||
|
Index int32 `db:"ndx"`
|
||||||
|
Data string `db:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global property indexes defined.
|
||||||
|
const (
|
||||||
|
GlobalPropFlags = int32(0)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Global flag indexes defined.
|
||||||
|
const (
|
||||||
|
GlobalFlagPicturesInPosts = uint(0)
|
||||||
|
GlobalFlagNoCategories = uint(1)
|
||||||
|
)
|
||||||
|
|
||||||
|
// theGlobals contains the singleton instance of Globals.
|
||||||
|
var theGlobals *Globals = nil
|
||||||
|
|
||||||
|
// globalsMutex controls access to theGlobals.
|
||||||
|
var globalsMutex sync.Mutex
|
||||||
|
|
||||||
|
// globalProps is the global properties store.
|
||||||
|
var globalProps map[int32]string = make(map[int32]string)
|
||||||
|
|
||||||
|
// globalPropMutex controls access to globalProps.
|
||||||
|
var globalPropMutex sync.Mutex
|
||||||
|
|
||||||
|
// Flags returns the global flags.
|
||||||
|
func (g *Globals) Flags() (*util.OptionSet, error) {
|
||||||
|
g.Mutex.Lock()
|
||||||
|
defer g.Mutex.Unlock()
|
||||||
|
if g.flags == nil {
|
||||||
|
s, err := AmGetGlobalProperty(GlobalPropFlags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
g.flags = util.OptionSetFromString(s)
|
||||||
|
}
|
||||||
|
return g.flags, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveFlags saves off the global flags.
|
||||||
|
func (g *Globals) SaveFlags(f *util.OptionSet) error {
|
||||||
|
s := f.AsString()
|
||||||
|
g.Mutex.Lock()
|
||||||
|
defer g.Mutex.Unlock()
|
||||||
|
err := AmSetGlobalProperty(GlobalPropFlags, s)
|
||||||
|
if err == nil {
|
||||||
|
g.flags = f
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AmGlobals returns trhe pointer to the singleton Globals instance.
|
||||||
|
func AmGlobals() (*Globals, error) {
|
||||||
|
globalsMutex.Lock()
|
||||||
|
defer globalsMutex.Unlock()
|
||||||
|
if theGlobals == nil {
|
||||||
|
var dbdata []Globals
|
||||||
|
err := amdb.Select(&dbdata, "SELECT * FROM globals")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(dbdata) > 1 {
|
||||||
|
return nil, errors.New("should only be one globals record")
|
||||||
|
}
|
||||||
|
theGlobals = &(dbdata[0])
|
||||||
|
}
|
||||||
|
return theGlobals, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AmGetGlobalProperty returns the value of a global property.
|
||||||
|
* Parameters:
|
||||||
|
* index - The index of the property to retrieve.
|
||||||
|
* Returns:
|
||||||
|
* Value of the property, or empty string.
|
||||||
|
* Standard Go error status.
|
||||||
|
*/
|
||||||
|
func AmGetGlobalProperty(index int32) (string, error) {
|
||||||
|
globalPropMutex.Lock()
|
||||||
|
defer globalPropMutex.Unlock()
|
||||||
|
rc, ok := globalProps[index]
|
||||||
|
if !ok {
|
||||||
|
rs, err := amdb.Query("SELECT data FROM propglobal WHERE ndx = ?", index)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if rs.Next() {
|
||||||
|
rs.Scan(&rc)
|
||||||
|
globalProps[index] = rc
|
||||||
|
return rc, nil
|
||||||
|
}
|
||||||
|
rc = ""
|
||||||
|
}
|
||||||
|
return rc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AmSetGlobalProperty sets the value of a global property.
|
||||||
|
* Parameters:
|
||||||
|
* index - The index of the property to set.
|
||||||
|
* value - The value of the property to set.
|
||||||
|
* Returns:
|
||||||
|
* Standard Go error status.
|
||||||
|
*/
|
||||||
|
func AmSetGlobalProperty(index int32, value string) error {
|
||||||
|
globalPropMutex.Lock()
|
||||||
|
defer globalPropMutex.Unlock()
|
||||||
|
_, updateMode := globalProps[index]
|
||||||
|
if !updateMode {
|
||||||
|
rs, err := amdb.Query("SELECT data FROM propglobal WHERE ndx = ?", index)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
updateMode = rs.Next()
|
||||||
|
}
|
||||||
|
var err error = nil
|
||||||
|
if updateMode {
|
||||||
|
_, err = amdb.Exec("UPDATE propglobal SET data = ? WHERE ndx = ?", value, index)
|
||||||
|
} else {
|
||||||
|
_, err = amdb.Exec("INSERT INTO propglobal (ndx, data) VALUES (?, ?)", index, value)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
globalProps[index] = value
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#
|
||||||
|
# Amsterdam Web Communities System
|
||||||
|
# Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
||||||
|
#
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
#
|
||||||
|
domains:
|
||||||
|
- domain: "community"
|
||||||
|
services:
|
||||||
|
- id: "Profile"
|
||||||
|
index: 0
|
||||||
|
default: true
|
||||||
|
locked: true
|
||||||
|
requirePermission: ""
|
||||||
|
requireRole: ""
|
||||||
|
linkSequence: 4900
|
||||||
|
link: "/comm/[CID]/profile"
|
||||||
|
title: "Profile"
|
||||||
|
- id: "Admin"
|
||||||
|
index: 1
|
||||||
|
default: true
|
||||||
|
locked: true
|
||||||
|
requirePermission: "Community.Read"
|
||||||
|
requireRole: "Community.AnyAdmin"
|
||||||
|
linkSequence: 5000
|
||||||
|
link: "/TODO/comm/[CID]/admin"
|
||||||
|
title: "Administration"
|
||||||
|
- id: "Conference"
|
||||||
|
index: 3
|
||||||
|
default: true
|
||||||
|
locked: false
|
||||||
|
requirePermission: "Community.Read"
|
||||||
|
requireRole: ""
|
||||||
|
linkSequence: 500
|
||||||
|
link: "/TODO/comm/[CID]/conf"
|
||||||
|
title: "Conferences"
|
||||||
|
- id: "Members"
|
||||||
|
index: 4
|
||||||
|
default: true
|
||||||
|
locked: true
|
||||||
|
requirePermission: "Community.Read"
|
||||||
|
requireRole: "Community.Member"
|
||||||
|
linkSequence: 4800
|
||||||
|
link: "/TODO/comm/[CID]/members"
|
||||||
|
title: "Members"
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
* Amsterdam Web Communities System
|
||||||
|
* Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
// The database package contains database management and storage logic.
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
lru "github.com/hashicorp/golang-lru"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServiceDef holds the definition for an individual service.
|
||||||
|
type ServiceDef struct {
|
||||||
|
Id string `yaml:"id"`
|
||||||
|
Index int16 `yaml:"index"`
|
||||||
|
Default bool `yaml:"default"`
|
||||||
|
Locked bool `yaml:"locked"`
|
||||||
|
RequirePermission string `yaml:"requirePermission"`
|
||||||
|
RequireRole string `yaml:"requireRole"`
|
||||||
|
LinkSequence int `yaml:"linkSequence"`
|
||||||
|
Link int `yaml:"link"`
|
||||||
|
Title string `yaml:"title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceDomain holds each individual configured service domain.
|
||||||
|
type ServiceDomain struct {
|
||||||
|
DomainName string `yaml:"domain"`
|
||||||
|
Services []ServiceDef `yaml:"services"`
|
||||||
|
byId map[string]*ServiceDef
|
||||||
|
byIndex map[int16]*ServiceDef
|
||||||
|
seqOrder []*ServiceDef
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceConfiguration holds the service configuration.
|
||||||
|
type ServiceConfiguration struct {
|
||||||
|
Domains []ServiceDomain `yaml:"domains"`
|
||||||
|
byName map[string]*ServiceDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed servicedefs.yaml
|
||||||
|
var initServiceData []byte
|
||||||
|
|
||||||
|
// The service configuration loaded from YAML.
|
||||||
|
var serviceRoot ServiceConfiguration
|
||||||
|
|
||||||
|
// The services cache for communities.
|
||||||
|
var servicesCache *lru.TwoQueueCache
|
||||||
|
|
||||||
|
// Mutex on the services cache.
|
||||||
|
var servicesCacheMutex sync.Mutex
|
||||||
|
|
||||||
|
// init loads the service configuration and builds all the internal indexes.
|
||||||
|
func init() {
|
||||||
|
var err error
|
||||||
|
if err = yaml.Unmarshal(initServiceData, &serviceRoot); err != nil {
|
||||||
|
panic(err) // can't happen
|
||||||
|
}
|
||||||
|
serviceRoot.byName = make(map[string]*ServiceDomain)
|
||||||
|
for i, dom := range serviceRoot.Domains {
|
||||||
|
serviceRoot.Domains[i].byId = make(map[string]*ServiceDef)
|
||||||
|
serviceRoot.Domains[i].byIndex = make(map[int16]*ServiceDef)
|
||||||
|
sqo := make([]*ServiceDef, 0, len(serviceRoot.Domains[i].Services))
|
||||||
|
for j, svc := range serviceRoot.Domains[i].Services {
|
||||||
|
serviceRoot.Domains[i].byId[svc.Id] = &(serviceRoot.Domains[i].Services[j])
|
||||||
|
serviceRoot.Domains[i].byIndex[svc.Index] = &(serviceRoot.Domains[i].Services[j])
|
||||||
|
sqo = append(sqo, &(serviceRoot.Domains[i].Services[j]))
|
||||||
|
}
|
||||||
|
slices.SortFunc(sqo, func(a, b *ServiceDef) int {
|
||||||
|
return a.LinkSequence - b.LinkSequence
|
||||||
|
})
|
||||||
|
serviceRoot.Domains[i].seqOrder = sqo
|
||||||
|
serviceRoot.byName[dom.DomainName] = &(serviceRoot.Domains[i])
|
||||||
|
}
|
||||||
|
servicesCache, err = lru.New2Q(50)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AmGetCommunityServices returns all the community service definitions for a community.
|
||||||
|
* Parameters:
|
||||||
|
* cid - Community ID to get services for.
|
||||||
|
* Returns:
|
||||||
|
* Array of ServiceDef pointers for the community's services.
|
||||||
|
* Standard Go error status.
|
||||||
|
*/
|
||||||
|
func AmGetCommunityServices(cid int32) ([]*ServiceDef, error) {
|
||||||
|
servicesCacheMutex.Lock()
|
||||||
|
defer servicesCacheMutex.Unlock()
|
||||||
|
rc, ok := servicesCache.Get(cid)
|
||||||
|
if !ok {
|
||||||
|
rs, err := amdb.Query("SELECT ftr_code FROM commftrs WHERE commid = ?", cid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dom := serviceRoot.byName["community"]
|
||||||
|
a := make([]*ServiceDef, 0, len(dom.Services))
|
||||||
|
for rs.Next() {
|
||||||
|
var ndx int16
|
||||||
|
rs.Scan(&ndx)
|
||||||
|
a = append(a, dom.byIndex[ndx])
|
||||||
|
}
|
||||||
|
servicesCache.Add(cid, a)
|
||||||
|
rc = a
|
||||||
|
}
|
||||||
|
return rc.([]*ServiceDef), nil
|
||||||
|
}
|
||||||
@@ -59,6 +59,7 @@ func setupEcho() *echo.Echo {
|
|||||||
e.GET("/user/:uname", ui.AmWrap(ShowProfile))
|
e.GET("/user/:uname", ui.AmWrap(ShowProfile))
|
||||||
e.POST("/quick_email", ui.AmWrap(QuickEMail))
|
e.POST("/quick_email", ui.AmWrap(QuickEMail))
|
||||||
e.GET("/sysadmin", ui.AmWrap(SysAdminMenu))
|
e.GET("/sysadmin", ui.AmWrap(SysAdminMenu))
|
||||||
|
e.GET("/comm/:cid/profile", ui.AmWrap(ShowCommunity))
|
||||||
|
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,141 @@
|
|||||||
|
{*
|
||||||
|
* Amsterdam Web Communities System
|
||||||
|
* Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*}
|
||||||
|
<div class="p-4">
|
||||||
|
<!-- Page Title -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-blue-800 text-4xl font-bold inline">Community Profile:</h1>
|
||||||
|
<span class="text-blue-800 text-2xl font-bold ml-2">{{ commName }}</span>
|
||||||
|
<hr class="border-2 border-gray-400 w-4/5 mt-2 mb-6">
|
||||||
|
</div>
|
||||||
|
<!-- Community Profile Card -->
|
||||||
|
<div class="bg-gray-50 p-6 rounded-lg mb-8 max-w-4xl">
|
||||||
|
<div class="flex gap-6">
|
||||||
|
<!-- Left Column: Image and Metadata -->
|
||||||
|
<div class="flex-shrink-0 w-32">
|
||||||
|
<div class="border-2 border-gray-300 rounded mb-4">
|
||||||
|
<img src="/img/static/default-community.jpg"
|
||||||
|
alt="{{ commName}} community logo" class="w-full h-auto">
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-700 space-y-2 mb-4">
|
||||||
|
<div><strong>Community created:</strong><br>{{ dateCreated }}</div>
|
||||||
|
{{ if isset(dateLastAccess) }}
|
||||||
|
<div><strong>Last accessed:</strong><br>{{ dateLastAccess }}</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ if isset(dateLastUpdate) }}
|
||||||
|
<div><strong>Profile last updated:</strong><br>{{ dateLastUpdate }}</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ if isset(canJoin) }}
|
||||||
|
<div class="text-center">
|
||||||
|
<a href="/TODO/comm/join"
|
||||||
|
class="inline-block bg-blue-600 hover:bg-blue-700 text-white px-4 py-1 rounded text-xs font-medium transition-colors">
|
||||||
|
Join Now
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ if isset(canInvite) }}
|
||||||
|
<div class="text-center">
|
||||||
|
<a href="/TODO/comm/invite"
|
||||||
|
class="inline-block bg-blue-600 hover:bg-blue-700 text-white px-4 py-1 rounded text-xs font-medium transition-colors">
|
||||||
|
Invite
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Column: Community Information -->
|
||||||
|
<div class="flex-1 text-sm text-black">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- Community Type and Category -->
|
||||||
|
<div>
|
||||||
|
{{ if public }}
|
||||||
|
<div class="text-lg font-bold underline mb-2">Public Community</div>
|
||||||
|
{{ else }}
|
||||||
|
<div class="text-lg font-bold underline mb-2">Private Community</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ if isset(categories) }}
|
||||||
|
<div>
|
||||||
|
<strong>Category:</strong>
|
||||||
|
{{ range i := categories }}
|
||||||
|
{{ if i > 0 }}: {{ end }}
|
||||||
|
<a href="/TODO/find/communities-for-category"
|
||||||
|
class="text-blue-700 hover:text-blue-900">{{ .Name }}</a>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ if isset(description) }}
|
||||||
|
<!-- Description -->
|
||||||
|
<div class="italic text-gray-700">{{ description }}</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<!-- Host -->
|
||||||
|
<div>
|
||||||
|
<strong>Host:</strong>
|
||||||
|
<a href="/users/{{ hostName }}" class="text-blue-700 hover:text-blue-900">{{ hostName }}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Location -->
|
||||||
|
<div class="pt-2 border-t border-gray-300">
|
||||||
|
<strong>Location:</strong>
|
||||||
|
<div class="mt-1">
|
||||||
|
{{ if isset(company) }}
|
||||||
|
<div>{{ company }}</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ if isset(addr1) }}
|
||||||
|
<div>{{ addr1 }}</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ if isset(addr2) }}
|
||||||
|
<div>{{ addr2 }}</div>
|
||||||
|
{{ end }}
|
||||||
|
<div>{{ addrLast }}</div>
|
||||||
|
{{ if isset(country) }}
|
||||||
|
<div>{{ country }}</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ if isset(language) || isset(rules) || isset(homePage) }}
|
||||||
|
<!-- Additional Information -->
|
||||||
|
<div class="pt-2 border-t border-gray-300 space-y-2">
|
||||||
|
{{ if isset(language) }}
|
||||||
|
<div><strong>Primary Language:</strong> {{ language }}</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ if isset(rules) }}
|
||||||
|
<div><strong>Standards of Conduct:</strong> {{ rules }}</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ if isset(homePage) }}
|
||||||
|
<div>
|
||||||
|
<strong>Homepage:</strong>
|
||||||
|
<a href="{{ homepage }}" target="_blank"
|
||||||
|
class="text-blue-700 hover:text-blue-900">{{ homepage }}</a>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Community Information Note -->
|
||||||
|
<div class="max-w-4xl p-4 bg-blue-50 border-l-4 border-blue-400">
|
||||||
|
<p class="text-sm text-gray-700">
|
||||||
|
<strong>About This Community:</strong>
|
||||||
|
{{ if public }}
|
||||||
|
This is a public community open to all users of the {{ GlobalConfig.Site.Title }} site.
|
||||||
|
You can join discussions in the conferences, view member lists, and participate in community activities.
|
||||||
|
{{ else }}
|
||||||
|
This is a private community, open by invitation only. You can only participate if you have been invited
|
||||||
|
by one of the community hosts.
|
||||||
|
{{ end }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -26,5 +26,11 @@
|
|||||||
<div class="text-small">You are not a member of any communities.</div>
|
<div class="text-small">You are not a member of any communities.</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<span class="text-black text-xs font-bold">
|
||||||
|
[ <a href="/TODO/manage_comms" class="text-blue-700 hover:text-blue-900">Manage</a> |
|
||||||
|
<a href="/TODO/create_comm" class="text-blue-700 hover:text-blue-900">Create New</a> ]
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+18
-6
@@ -339,6 +339,18 @@ func ShowProfile(ctxt ui.AmContext) (string, any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ui.ErrorPage(ctxt, err)
|
return ui.ErrorPage(ctxt, err)
|
||||||
}
|
}
|
||||||
|
var pvtAddr, pvtPhone, pvtFax, pvtEmail bool
|
||||||
|
if database.AmTestPermission("Global.SeeHiddenContactInfo", me.BaseLevel) {
|
||||||
|
pvtAddr = false
|
||||||
|
pvtPhone = false
|
||||||
|
pvtFax = false
|
||||||
|
pvtEmail = false
|
||||||
|
} else {
|
||||||
|
pvtAddr = ci.PrivateAddr
|
||||||
|
pvtPhone = ci.PrivatePhone
|
||||||
|
pvtFax = ci.PrivateFax
|
||||||
|
pvtEmail = ci.PrivateEmail
|
||||||
|
}
|
||||||
|
|
||||||
// Fill all the page variables for display.
|
// Fill all the page variables for display.
|
||||||
ctxt.VarMap().Set("uid", user.Uid)
|
ctxt.VarMap().Set("uid", user.Uid)
|
||||||
@@ -369,7 +381,7 @@ func ShowProfile(ctxt ui.AmContext) (string, any, error) {
|
|||||||
if user.Description != nil {
|
if user.Description != nil {
|
||||||
ctxt.VarMap().Set("description", *user.Description)
|
ctxt.VarMap().Set("description", *user.Description)
|
||||||
}
|
}
|
||||||
if !ci.PrivateEmail && ci.Email != nil {
|
if !pvtEmail && ci.Email != nil {
|
||||||
ctxt.VarMap().Set("email", *ci.Email)
|
ctxt.VarMap().Set("email", *ci.Email)
|
||||||
}
|
}
|
||||||
if ci.URL != nil && *ci.URL != "" {
|
if ci.URL != nil && *ci.URL != "" {
|
||||||
@@ -378,10 +390,10 @@ func ShowProfile(ctxt ui.AmContext) (string, any, error) {
|
|||||||
if ci.Company != nil {
|
if ci.Company != nil {
|
||||||
ctxt.VarMap().Set("company", *ci.Company)
|
ctxt.VarMap().Set("company", *ci.Company)
|
||||||
}
|
}
|
||||||
if !ci.PrivateAddr && ci.Addr1 != nil {
|
if !pvtAddr && ci.Addr1 != nil {
|
||||||
ctxt.VarMap().Set("addr1", *ci.Addr1)
|
ctxt.VarMap().Set("addr1", *ci.Addr1)
|
||||||
}
|
}
|
||||||
if !ci.PrivateAddr && ci.Addr2 != nil {
|
if !pvtAddr && ci.Addr2 != nil {
|
||||||
ctxt.VarMap().Set("addr2", *ci.Addr2)
|
ctxt.VarMap().Set("addr2", *ci.Addr2)
|
||||||
}
|
}
|
||||||
b.Reset()
|
b.Reset()
|
||||||
@@ -402,13 +414,13 @@ func ShowProfile(ctxt ui.AmContext) (string, any, error) {
|
|||||||
country := countries.ByName(*ci.Country)
|
country := countries.ByName(*ci.Country)
|
||||||
ctxt.VarMap().Set("country", country.String())
|
ctxt.VarMap().Set("country", country.String())
|
||||||
}
|
}
|
||||||
if !ci.PrivatePhone && ci.Phone != nil {
|
if !pvtPhone && ci.Phone != nil {
|
||||||
ctxt.VarMap().Set("phone", *ci.Phone)
|
ctxt.VarMap().Set("phone", *ci.Phone)
|
||||||
}
|
}
|
||||||
if !ci.PrivateFax && ci.Fax != nil {
|
if !pvtFax && ci.Fax != nil {
|
||||||
ctxt.VarMap().Set("fax", *ci.Fax)
|
ctxt.VarMap().Set("fax", *ci.Fax)
|
||||||
}
|
}
|
||||||
if !ci.PrivatePhone && ci.Mobile != nil {
|
if !pvtPhone && ci.Mobile != nil {
|
||||||
ctxt.VarMap().Set("mobile", *ci.Mobile)
|
ctxt.VarMap().Set("mobile", *ci.Mobile)
|
||||||
}
|
}
|
||||||
ctxt.VarMap().Set("amsterdam_pageTitle", fmt.Sprintf("User Profile - %s", user.Username))
|
ctxt.VarMap().Set("amsterdam_pageTitle", fmt.Sprintf("User Profile - %s", user.Username))
|
||||||
|
|||||||
+24
-1
@@ -10,7 +10,20 @@
|
|||||||
// Package util contains utility definitions.
|
// Package util contains utility definitions.
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import "unicode"
|
import (
|
||||||
|
"regexp"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
var numeric *regexp.Regexp
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
re, err := regexp.Compile("^[0-9]+$")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
numeric = re
|
||||||
|
}
|
||||||
|
|
||||||
/* CapitalizeString changes the first character of the string to a capital.
|
/* CapitalizeString changes the first character of the string to a capital.
|
||||||
* Parameters:
|
* Parameters:
|
||||||
@@ -26,3 +39,13 @@ func CapitalizeString(s string) string {
|
|||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* IsNumeric returns true if the string is numeric (all digits).
|
||||||
|
* Parameters:
|
||||||
|
* s - String to be tested.
|
||||||
|
* Returns:
|
||||||
|
* true if string is numeric, false if not.
|
||||||
|
*/
|
||||||
|
func IsNumeric(s string) bool {
|
||||||
|
return numeric.MatchString(s)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user