added dialog for editing community profile
This commit is contained in:
@@ -12,6 +12,7 @@ package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.erbosoft.com/amy/amsterdam/database"
|
||||
@@ -48,3 +49,76 @@ func CommunityAdminMenu(ctxt ui.AmContext) (string, any, error) {
|
||||
ctxt.VarMap().Set("amsterdam_pageTitle", menu.Title+" - "+comm.Name)
|
||||
return "framed_template", "menu.jet", nil
|
||||
}
|
||||
|
||||
func setupCommunityProfileDialog(dlg *ui.Dialog, comm *database.Community) {
|
||||
dlg.SetCommunity(comm)
|
||||
if comm.IsAdmin {
|
||||
dlg.Field("comtype").Disabled = true
|
||||
dlg.Field("joinkey").Disabled = true
|
||||
dlg.Field("membersonly").Disabled = true
|
||||
dlg.Field("hidemode").Disabled = true
|
||||
dlg.Field("read_lvl").Disabled = true
|
||||
dlg.Field("write_lvl").Disabled = true
|
||||
dlg.Field("create_lvl").Disabled = true
|
||||
dlg.Field("delete_lvl").Disabled = true
|
||||
dlg.Field("join_lvl").Disabled = true
|
||||
}
|
||||
}
|
||||
|
||||
func CommunityProfileForm(ctxt ui.AmContext) (string, any, error) {
|
||||
err := ctxt.SetCommunityContext(ctxt.URLParam("cid"))
|
||||
if err != nil {
|
||||
ctxt.SetRC(http.StatusNotFound)
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
comm := ctxt.CurrentCommunity()
|
||||
if !comm.TestPermission("Community.Write", ctxt.EffectiveLevel()) {
|
||||
ctxt.SetRC(http.StatusForbidden)
|
||||
return ui.ErrorPage(ctxt, errors.New("you are not permitted to access this page"))
|
||||
}
|
||||
var ci *database.ContactInfo
|
||||
ci, err = comm.ContactInfo()
|
||||
if err != nil {
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
flags, err := comm.Flags()
|
||||
if err != nil {
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
dlg, err := ui.AmLoadDialog("commprofile")
|
||||
if err == nil {
|
||||
setupCommunityProfileDialog(dlg, comm)
|
||||
dlg.Field("cc").Value = fmt.Sprintf("%d", comm.Id)
|
||||
dlg.Field("name").Value = comm.Name
|
||||
dlg.Field("alias").Value = comm.Alias
|
||||
dlg.Field("synopsis").SetVal(comm.Synopsis)
|
||||
dlg.Field("rules").SetVal(comm.Rules)
|
||||
dlg.Field("language").SetVal(comm.Language)
|
||||
dlg.Field("url").SetVal(ci.URL)
|
||||
// TODO: set logo URL
|
||||
dlg.Field("company").SetVal(ci.Company)
|
||||
dlg.Field("addr1").SetVal(ci.Addr1)
|
||||
dlg.Field("addr2").SetVal(ci.Addr2)
|
||||
dlg.Field("loc").SetVal(ci.Locality)
|
||||
dlg.Field("reg").SetVal(ci.Region)
|
||||
dlg.Field("pcode").SetVal(ci.PostalCode)
|
||||
dlg.Field("country").SetVal(ci.Country)
|
||||
if comm.Public() {
|
||||
dlg.Field("comtype").Value = "0"
|
||||
dlg.Field("joinkey").Value = ""
|
||||
} else {
|
||||
dlg.Field("comtype").Value = "1"
|
||||
dlg.Field("joinkey").SetVal(comm.JoinKey)
|
||||
}
|
||||
dlg.Field("membersonly").SetChecked(comm.MembersOnly)
|
||||
dlg.Field("hidemode").Value = comm.HideMode()
|
||||
dlg.Field("read_lvl").Value = fmt.Sprintf("%d", comm.ReadLevel)
|
||||
dlg.Field("write_lvl").Value = fmt.Sprintf("%d", comm.WriteLevel)
|
||||
dlg.Field("create_lvl").Value = fmt.Sprintf("%d", comm.CreateLevel)
|
||||
dlg.Field("delete_lvl").Value = fmt.Sprintf("%d", comm.DeleteLevel)
|
||||
dlg.Field("join_lvl").Value = fmt.Sprintf("%d", comm.JoinLevel)
|
||||
dlg.Field("pic_in_post").SetChecked(flags.Get(database.CommunityFlagPicturesInPosts))
|
||||
return dlg.Render(ctxt)
|
||||
}
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
|
||||
+139
-1
@@ -47,14 +47,38 @@ type Community struct {
|
||||
Rules *string `dd:"rules"`
|
||||
JoinKey *string `db:"joinkey"`
|
||||
Alias string `db:"alias"`
|
||||
flags *util.OptionSet
|
||||
}
|
||||
|
||||
// CommunityProperties represents a property entry for a community.
|
||||
type CommunityProperties struct {
|
||||
Cid int32 `db:"cid"`
|
||||
Index int32 `db:"ndx"`
|
||||
Data *string `db:"data"`
|
||||
}
|
||||
|
||||
// Community property indexes defined.
|
||||
const (
|
||||
CommunityPropFlags = int32(0) // "flags" user property
|
||||
)
|
||||
|
||||
// Flag values for community property index CommunityPropFlags defined.
|
||||
const (
|
||||
CommunityFlagPicturesInPosts = uint(0)
|
||||
)
|
||||
|
||||
// communityCache is the cache for Community objects.
|
||||
var communityCache *lru.TwoQueueCache = nil
|
||||
|
||||
// getCommunityMutex is a mutex on AmGetCommunity.
|
||||
var getCommunityMutex sync.Mutex
|
||||
|
||||
// communityPropCache is the cache for CommunityProperties objects.
|
||||
var communityPropCache *lru.Cache = nil
|
||||
|
||||
// getCommunityPropMutex is a mutex on AmGetCommunityProperty.
|
||||
var getCommunityPropMutex sync.Mutex
|
||||
|
||||
// memberCacheData caches membership information for communities.
|
||||
type memberCacheData struct {
|
||||
isMember bool
|
||||
@@ -76,7 +100,7 @@ func stuffMembership(cid int32, uid int32, member bool, locked bool, level uint1
|
||||
memberMutex.Unlock()
|
||||
}
|
||||
|
||||
// init initializes the community cache.
|
||||
// init initializes the caches.
|
||||
func init() {
|
||||
var err error
|
||||
communityCache, err = lru.New2Q(50)
|
||||
@@ -87,6 +111,10 @@ func init() {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
communityPropCache, err = lru.New(100)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Public returns true if the community is public.
|
||||
@@ -121,6 +149,16 @@ func (c *Community) LanguageTag() (*language.Tag, error) {
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (c *Community) HideMode() string {
|
||||
if c.HideFromSearch {
|
||||
return "BOTH"
|
||||
} else if c.HideFromDirectory {
|
||||
return "DIRECTORY"
|
||||
} else {
|
||||
return "NONE"
|
||||
}
|
||||
}
|
||||
|
||||
/* Membership returns the details of the specified user's membership in the community.
|
||||
* Parameters:
|
||||
* u - The user to check the membership of.
|
||||
@@ -200,6 +238,35 @@ func (c *Community) PermissionLevel(perm string) uint16 {
|
||||
}
|
||||
}
|
||||
|
||||
// GetFlags retrieves the flags from the properties.
|
||||
func (c *Community) Flags() (*util.OptionSet, error) {
|
||||
c.Mutex.Lock()
|
||||
defer c.Mutex.Unlock()
|
||||
if c.flags == nil {
|
||||
s, err := AmGetCommunityProperty(c.Id, CommunityPropFlags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s == nil {
|
||||
return nil, fmt.Errorf("missing flags for community %d", c.Id)
|
||||
}
|
||||
c.flags = util.OptionSetFromString(*s)
|
||||
}
|
||||
return c.flags, nil
|
||||
}
|
||||
|
||||
// SaveFlags writes the flags to the database and stores them.
|
||||
func (c *Community) SaveFlags(f *util.OptionSet) error {
|
||||
s := f.AsString()
|
||||
c.Mutex.Lock()
|
||||
defer c.Mutex.Unlock()
|
||||
err := AmSetCommunityProperty(c.Id, CommunityPropFlags, &s)
|
||||
if err == nil {
|
||||
c.flags = f
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
/* AmGetCommunity returns a reference to the specified community.
|
||||
* Parameters:
|
||||
* id - The ID of the community.
|
||||
@@ -342,3 +409,74 @@ func AmAutoJoinCommunities(user *User) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// internalGetProp is a helper used by the property functions.
|
||||
func internalGetCommProp(cid int32, ndx int32) (*CommunityProperties, error) {
|
||||
var err error = nil
|
||||
key := fmt.Sprintf("%d:%d", cid, ndx)
|
||||
getCommunityPropMutex.Lock()
|
||||
defer getCommunityPropMutex.Unlock()
|
||||
rc, ok := communityPropCache.Get(key)
|
||||
if !ok {
|
||||
var dbdata []CommunityProperties
|
||||
err = amdb.Select(&dbdata, "SELECT * from propcomm WHERE cid = ? AND ndx = ?", cid, ndx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(dbdata) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if len(dbdata) > 1 {
|
||||
return nil, fmt.Errorf("AmGetCommunityProperty(%d): too many responses(%d)", cid, len(dbdata))
|
||||
}
|
||||
rc = &(dbdata[0])
|
||||
communityPropCache.Add(key, rc)
|
||||
}
|
||||
return rc.(*CommunityProperties), nil
|
||||
}
|
||||
|
||||
/* AmGetCommunityProperty retrieves the value of a user property.
|
||||
* Parameters:
|
||||
* cid - The ID of the community to get the property for.
|
||||
* ndx - The index of the property to retrieve.
|
||||
* Returns:
|
||||
* Value of the property string.
|
||||
* Standard Go error status.
|
||||
*/
|
||||
func AmGetCommunityProperty(cid int32, ndx int32) (*string, error) {
|
||||
p, err := internalGetCommProp(cid, ndx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.Data, nil
|
||||
}
|
||||
|
||||
/* AmSetCommunityProperty sets the value of a community property.
|
||||
* Parameters:
|
||||
* cid - The ID of the community to set the property for.
|
||||
* ndx - The index of the property to set.
|
||||
* val - The new value of the property.
|
||||
* Returns:
|
||||
* Standard Go error status.
|
||||
*/
|
||||
func AmSetCommunityProperty(cid int32, ndx int32, val *string) error {
|
||||
p, err := internalGetCommProp(cid, ndx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
getCommunityPropMutex.Lock()
|
||||
defer getCommunityPropMutex.Unlock()
|
||||
if p != nil {
|
||||
_, err = amdb.Exec("UPDATE propcomm SET data = ? WHERE cid = ? AND ndx = ?", val, cid, ndx)
|
||||
if err == nil {
|
||||
p.Data = val
|
||||
}
|
||||
} else {
|
||||
prop := CommunityProperties{Cid: cid, Index: ndx, Data: val}
|
||||
_, err := amdb.NamedExec("INSERT INTO propcomm (cid, ndx, data) VALUES(:cid, :ndx, :data)", prop)
|
||||
if err == nil {
|
||||
communityPropCache.Add(fmt.Sprintf("%d:%d", cid, ndx), prop)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ package database
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -184,6 +185,7 @@ type Role interface {
|
||||
ID() string
|
||||
Name() string
|
||||
Level() uint16
|
||||
LevelStr() string
|
||||
}
|
||||
|
||||
func (r *CfgRole) ID() string {
|
||||
@@ -198,6 +200,10 @@ func (r *CfgRole) Level() uint16 {
|
||||
return r.level
|
||||
}
|
||||
|
||||
func (r *CfgRole) LevelStr() string {
|
||||
return fmt.Sprintf("%d", r.level)
|
||||
}
|
||||
|
||||
// RoleList defines a list of security roles.
|
||||
type RoleList interface {
|
||||
Roles() []Role
|
||||
|
||||
@@ -184,5 +184,3 @@ permissions:
|
||||
role: "Community.AnyAdmin"
|
||||
- name: "Community.MassMail"
|
||||
role: "Community.AnyAdmin"
|
||||
- name: "Community.Destroy"
|
||||
role: "Community.Host"
|
||||
|
||||
@@ -66,6 +66,7 @@ func setupEcho() *echo.Echo {
|
||||
e.GET("/sysadmin", ui.AmWrap(SysAdminMenu))
|
||||
e.GET("/comm/:cid/profile", ui.AmWrap(ShowCommunity))
|
||||
e.GET("/comm/:cid/admin", ui.AmWrap(CommunityAdminMenu))
|
||||
e.GET("/comm/:cid/admin/profile", ui.AmWrap(CommunityProfileForm))
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
+98
-24
@@ -23,17 +23,26 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// DialogItemChoice holds a dialog item choice (only needed in case of defined dropdowns)
|
||||
type DialogItemChoice struct {
|
||||
Id string `yaml:"id"`
|
||||
Text string `yaml:"text"`
|
||||
Default bool `yaml:"default,omitempty"`
|
||||
}
|
||||
|
||||
// DialogItem holds the dialog item definition.
|
||||
type DialogItem struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Caption string `yaml:"caption,omitempty"`
|
||||
Subcaption string `yaml:"subcaption,omitempty"`
|
||||
Required bool `yaml:"required,omitempty"`
|
||||
Size int `yaml:"size,omitempty"`
|
||||
MaxLength int `yaml:"maxlength,omitempty"`
|
||||
Value string `yaml:"value,omitempty"`
|
||||
Param string `yaml:"param,omitempty"`
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Caption string `yaml:"caption,omitempty"`
|
||||
Subcaption string `yaml:"subcaption,omitempty"`
|
||||
Required bool `yaml:"required,omitempty"`
|
||||
Disabled bool `yaml:"disabled,omitempty"`
|
||||
Size int `yaml:"size,omitempty"`
|
||||
MaxLength int `yaml:"maxlength,omitempty"`
|
||||
Value string `yaml:"value,omitempty"`
|
||||
Param string `yaml:"param,omitempty"`
|
||||
Choices []DialogItemChoice `yaml:"choices,omitempty"`
|
||||
AuxData any
|
||||
}
|
||||
|
||||
@@ -44,6 +53,7 @@ type Dialog struct {
|
||||
Options string `yaml:"options,omitempty"`
|
||||
MenuSelector string `yaml:"menuSelector,omitempty"`
|
||||
Title string `yaml:"title"`
|
||||
Subtitle string `yaml:"subtitle,omitempty"`
|
||||
Action string `yaml:"action"`
|
||||
Instructions string `yaml:"instructions,omitempty"`
|
||||
Fields []DialogItem `yaml:"fields"`
|
||||
@@ -82,6 +92,9 @@ func AmLoadDialog(name string) (*Dialog, error) {
|
||||
if fld.Type == "date" && fld.Param == "" {
|
||||
d.Fields[i].Param = "year:-100"
|
||||
}
|
||||
if fld.Type == "dropdown" && len(fld.Choices) == 0 {
|
||||
return nil, fmt.Errorf("dropdown field %s in dialog %s has no choices", fld.Name, name)
|
||||
}
|
||||
}
|
||||
return &d, nil
|
||||
}
|
||||
@@ -184,11 +197,29 @@ func (fld *DialogItem) SetVal(p *string) {
|
||||
}
|
||||
}
|
||||
|
||||
// SetInt sets the value of a field to an integer.
|
||||
func (fld *DialogItem) SetInt(v int) {
|
||||
fld.Value = fmt.Sprintf("%d", v)
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the field is empty.
|
||||
func (fld *DialogItem) IsEmpty() bool {
|
||||
return len(fld.Value) == 0
|
||||
}
|
||||
|
||||
// SetCommunity alters a dialog's content to reflect the community.
|
||||
func (d *Dialog) SetCommunity(comm *database.Community) {
|
||||
d.Title = strings.ReplaceAll(d.Title, "[CNAME]", comm.Name)
|
||||
d.Subtitle = strings.ReplaceAll(d.Subtitle, "[CNAME]", comm.Name)
|
||||
d.Action = strings.ReplaceAll(d.Action, "[CID]", comm.Alias)
|
||||
for i, fld := range d.Fields {
|
||||
switch fld.Type {
|
||||
case "userphoto", "communitylogo":
|
||||
d.Fields[i].Param = strings.ReplaceAll(fld.Param, "[CID]", comm.Alias)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Field returns a pointer to a dialog's field, given its name.
|
||||
* Parameters:
|
||||
* name - The name of the field to find.
|
||||
@@ -224,6 +255,46 @@ func (d *Dialog) Render(ctxt AmContext) (string, any, error) {
|
||||
if d.Fields[i].Value == "" {
|
||||
d.Fields[i].Value = config.GlobalConfig.Defaults.TimeZone
|
||||
}
|
||||
case "dropdown":
|
||||
defv := ""
|
||||
for _, ch := range fld.Choices {
|
||||
if ch.Default {
|
||||
defv = ch.Id
|
||||
break
|
||||
}
|
||||
}
|
||||
if d.Fields[i].Value == "" {
|
||||
d.Fields[i].Value = defv
|
||||
} else {
|
||||
ok := false
|
||||
for _, ch := range fld.Choices {
|
||||
if d.Fields[i].Value == ch.Id {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
d.Fields[i].Value = defv
|
||||
}
|
||||
}
|
||||
case "rolelist":
|
||||
rl := database.AmRoleList(fld.Param)
|
||||
defv := rl.Default().LevelStr()
|
||||
if d.Fields[i].Value == "" {
|
||||
d.Fields[i].Value = defv
|
||||
} else {
|
||||
ok := false
|
||||
for _, r := range rl.Roles() {
|
||||
if d.Fields[i].Value == r.LevelStr() {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
if !ok {
|
||||
d.Fields[i].Value = defv
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: want to do something like dropdown but not sure what yet
|
||||
}
|
||||
}
|
||||
if d.MenuSelector != "" && d.MenuSelector != "nochange" {
|
||||
@@ -312,7 +383,7 @@ func (d *Dialog) LoadFromForm(ctxt AmContext) {
|
||||
}
|
||||
}
|
||||
d.Fields[i].AuxData = dvals
|
||||
case "userphoto":
|
||||
case "userphoto", "communitylogo":
|
||||
d.Fields[i].Value = ctxt.FormField(fmt.Sprintf("%s_data", fld.Name))
|
||||
default:
|
||||
d.Fields[i].Value = ctxt.FormField(fld.Name)
|
||||
@@ -468,20 +539,23 @@ func validateDateField(fld *DialogItem) error {
|
||||
|
||||
// validators maps the field types to validator functions.
|
||||
var validators = map[string]validatorFunc{
|
||||
"ams_id": validateAmsIdField,
|
||||
"button": nilValidator,
|
||||
"checkbox": nilValidator,
|
||||
"countrylist": validateCountryField,
|
||||
"date": validateDateField,
|
||||
"email": validateEmailField,
|
||||
"header": nilValidator,
|
||||
"hidden": nilValidator,
|
||||
"integer": validateIntegerField,
|
||||
"localelist": nilValidator, // TODO
|
||||
"password": validateTextField,
|
||||
"text": validateTextField,
|
||||
"tzlist": nilValidator, // TODO
|
||||
"userphoto": nilValidator,
|
||||
"ams_id": validateAmsIdField,
|
||||
"button": nilValidator,
|
||||
"checkbox": nilValidator,
|
||||
"communitylogo": nilValidator,
|
||||
"countrylist": validateCountryField,
|
||||
"date": validateDateField,
|
||||
"dropdown": nilValidator,
|
||||
"email": validateEmailField,
|
||||
"header": nilValidator,
|
||||
"hidden": nilValidator,
|
||||
"integer": validateIntegerField,
|
||||
"localelist": nilValidator,
|
||||
"password": validateTextField,
|
||||
"rolelist": nilValidator,
|
||||
"text": validateTextField,
|
||||
"tzlist": nilValidator,
|
||||
"userphoto": nilValidator,
|
||||
}
|
||||
|
||||
/* Validate validates the values in the dialog.
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
#
|
||||
# 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/.
|
||||
#
|
||||
name: "community.profile"
|
||||
formName: "comprofform"
|
||||
menuSelector: "community"
|
||||
title: "Edit Community Profile:"
|
||||
subtitle: "[CNAME]"
|
||||
action: "/comm/[CID]/admin/profile"
|
||||
fields:
|
||||
- type: "hidden"
|
||||
name: "cc"
|
||||
value: ""
|
||||
- type: "header"
|
||||
name: "header1"
|
||||
caption: "Basic Information"
|
||||
- type: "text"
|
||||
name: "name"
|
||||
caption: "Community Name"
|
||||
required: true
|
||||
size: 32
|
||||
maxLength: 128
|
||||
- type: "ams_id"
|
||||
name: "alias"
|
||||
caption: "Community Alias"
|
||||
required: true
|
||||
size: 32
|
||||
maxlength: 32
|
||||
- type: "text"
|
||||
name: "synopsis"
|
||||
caption: "Synopsis"
|
||||
size: 32
|
||||
maxLength: 255
|
||||
- type: "text"
|
||||
name: "rules"
|
||||
caption: "Rules"
|
||||
size: 32
|
||||
maxLength: 255
|
||||
- type: "localelist"
|
||||
name: "language"
|
||||
caption: "Primary Language"
|
||||
required: true
|
||||
- type: "text"
|
||||
name: "url"
|
||||
caption: "Home Page"
|
||||
subcaption: "(URL)"
|
||||
size: 32
|
||||
maxlength: 255
|
||||
- type: "communitylogo"
|
||||
name: "logo"
|
||||
caption: "Community logo"
|
||||
param: "/TODO/comm/[CID]/admin/logo"
|
||||
- type: "header"
|
||||
name: "header2"
|
||||
caption: "Location"
|
||||
- type: "text"
|
||||
name: "company"
|
||||
caption: "Company"
|
||||
size: 32
|
||||
maxlength: 255
|
||||
- type: "text"
|
||||
name: "addr1"
|
||||
caption: "Address"
|
||||
size: 32
|
||||
maxlength: 255
|
||||
- type: "text"
|
||||
name: "addr2"
|
||||
caption: "Address"
|
||||
subcaption: "(line 2)"
|
||||
size: 32
|
||||
maxlength: 255
|
||||
- type: "text"
|
||||
name: "loc"
|
||||
caption: "City"
|
||||
required: true
|
||||
size: 32
|
||||
maxlength: 64
|
||||
- type: "text"
|
||||
name: "reg"
|
||||
caption: "State/Province"
|
||||
required: true
|
||||
size: 32
|
||||
maxlength: 64
|
||||
- type: "text"
|
||||
name: "pcode"
|
||||
caption: "Zip/Postal Code"
|
||||
required: true
|
||||
size: 32
|
||||
maxlength: 64
|
||||
- type: "countrylist"
|
||||
name: "country"
|
||||
caption: "Country"
|
||||
required: true
|
||||
- type: "header"
|
||||
name: "header3"
|
||||
caption: "Security"
|
||||
- type: "dropdown"
|
||||
name: "comtype"
|
||||
caption: "Community type"
|
||||
required: true
|
||||
choices:
|
||||
- id: "0"
|
||||
text: "Public"
|
||||
default: true
|
||||
- id: "1"
|
||||
text: "Private"
|
||||
- type: "text"
|
||||
name: "joinkey"
|
||||
caption: "Join Key"
|
||||
subcaption: "(for private communities)"
|
||||
size: 32
|
||||
maxLength: 64
|
||||
- type: "checkbox"
|
||||
name: "membersonly"
|
||||
caption: "Allow only members to access this community"
|
||||
- type: "dropdown"
|
||||
name: "hidemode"
|
||||
caption: "Community visibility"
|
||||
required: true
|
||||
choices:
|
||||
- id: "NONE"
|
||||
text: "Show in both directory and search"
|
||||
default: true
|
||||
- id: "DIRECTORY"
|
||||
text: "Hide in directory, but not in search"
|
||||
- id: "BOTH"
|
||||
text: "Hide in both directory and search"
|
||||
- type: "rolelist"
|
||||
name: "read_lvl"
|
||||
caption: "Security level required to read contents"
|
||||
required: true
|
||||
param: "Community.Read"
|
||||
- type: "rolelist"
|
||||
name: "write_lvl"
|
||||
caption: "Security level required to update profile"
|
||||
required: true
|
||||
param: "Community.Write"
|
||||
- type: "rolelist"
|
||||
name: "create_lvl"
|
||||
caption: "Security level required to create new subobjects"
|
||||
required: true
|
||||
param: "Community.Create"
|
||||
- type: "rolelist"
|
||||
name: "delete_lvl"
|
||||
caption: "Security level required to delete community"
|
||||
required: true
|
||||
param: "Community.Delete"
|
||||
- type: "rolelist"
|
||||
name: "join_lvl"
|
||||
caption: "Security level required to join community"
|
||||
required: true
|
||||
param: "Community.Join"
|
||||
- type: "header"
|
||||
name: "header4"
|
||||
caption: "Conferencing Options"
|
||||
- type: "checkbox"
|
||||
name: "pic_in_post"
|
||||
caption: "Display user pictures next to posts in conferences"
|
||||
subcaption: "(by default; user can override)"
|
||||
- type: "button"
|
||||
name: "update"
|
||||
caption: "Update"
|
||||
param: "blue"
|
||||
- type: "button"
|
||||
name: "cancel"
|
||||
caption: "Cancel"
|
||||
param: "red"
|
||||
+5
-5
@@ -61,15 +61,15 @@ menudefs:
|
||||
community. Use with care and review all changes before applying them to the community.
|
||||
items:
|
||||
- text: "Community Profile"
|
||||
link: "/TODO/comm/[CID]/admin/profile"
|
||||
permission: "Community.ShowAdmin"
|
||||
link: "/comm/[CID]/admin/profile"
|
||||
permission: "Community.Write"
|
||||
- text: "Set Community Category"
|
||||
link: "/TODO/comm/[CID]/admin/category"
|
||||
permission: "Community.ShowAdmin"
|
||||
permission: "Community.Write"
|
||||
ifdef: "USECAT"
|
||||
- text: "Set Community Services"
|
||||
link: "/TODO/comm/[CID]/admin/services"
|
||||
permission: "Community.ShowAdmin"
|
||||
permission: "Community.Write"
|
||||
disabled: true
|
||||
- text: "Membership Control"
|
||||
link: "/TODO/comm/[CID]/admin/members"
|
||||
@@ -82,5 +82,5 @@ menudefs:
|
||||
permission: "Community.ShowAdmin"
|
||||
- text: "Delete Community"
|
||||
link: "/TODO/comm/[CID]/admin/delete"
|
||||
permission: "Community.Destroy"
|
||||
permission: "Community.Delete"
|
||||
hazard: true
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.erbosoft.com/amy/amsterdam/config"
|
||||
"git.erbosoft.com/amy/amsterdam/database"
|
||||
"git.erbosoft.com/amy/amsterdam/util"
|
||||
"github.com/CloudyKit/jet/v6"
|
||||
"github.com/CloudyKit/jet/v6/loaders/embedfs"
|
||||
@@ -235,6 +236,10 @@ func SetupTemplates() {
|
||||
s := a.Get(0).Convert(reflect.TypeFor[string]()).String()
|
||||
return reflect.ValueOf(AmMenu(s))
|
||||
})
|
||||
views.AddGlobalFunc("AmRoleList", func(a jet.Arguments) reflect.Value {
|
||||
s := a.Get(0).Convert(reflect.TypeFor[string]()).String()
|
||||
return reflect.ValueOf(database.AmRoleList(s))
|
||||
})
|
||||
views.AddGlobalFunc("CapitalizeString", func(a jet.Arguments) reflect.Value {
|
||||
s := a.Get(0).Convert(reflect.TypeFor[string]()).String()
|
||||
return reflect.ValueOf(util.CapitalizeString(s))
|
||||
|
||||
+85
-25
@@ -10,6 +10,9 @@
|
||||
<div class="p-4">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-blue-800 text-4xl font-bold mb-2">{{ amsterdam_dialog.Title }}</h1>
|
||||
{{ if amsterdam_dialog.Subtitle != "" }}
|
||||
<span class="text-blue-800 text-2xl font-bold ml-2">{{ amsterdam_dialog.Subtitle }}</span>
|
||||
{{ end }}
|
||||
<hr class="border-2 border-gray-400 w-4/5 mb-4">
|
||||
</div>
|
||||
<form name="{{ amsterdam_dialog.FormName }}" method="POST" action="{{ amsterdam_dialog.Action }}" class="max-w-2xl">
|
||||
@@ -58,26 +61,28 @@
|
||||
{{ range amsterdam_dialog.Fields }}
|
||||
{{ if .Type == "text" || .Type == "ams_id" || .Type == "email" }}
|
||||
<div class="flex items-center">
|
||||
<label for="{{ .Name }}" class="w-64 text-right pr-4 text-black text-sm">
|
||||
<label for="{{ .Name }}"
|
||||
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
|
||||
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}:
|
||||
{{ if .Required }}<span class="text-red-600">*</span>{{ end }}
|
||||
</label>
|
||||
<input type="text" id="{{ .Name }}" name="{{ .Name }}"
|
||||
{{ if .Size > 0 }}size="{{ .Size }}"{{ end }}
|
||||
{{ if .MaxLength > 0 }}maxlength="{{ .MaxLength }}"{{ end }}
|
||||
value="{{ .Value }}"
|
||||
value="{{ .Value }}" {{ if .Disabled }}disabled{{ end }}
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
|
||||
</div>
|
||||
{{ else if .Type == "integer" }}
|
||||
<div class="flex items-center">
|
||||
<label for="{{ .Name }}" class="w-64 text-right pr-4 text-black text-sm">
|
||||
<label for="{{ .Name }}"
|
||||
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
|
||||
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}:
|
||||
{{ if .Required }}<span class="text-red-600">*</span>{{ end }}
|
||||
</label>
|
||||
<input type="text" id="{{ .Name }}" name="{{ .Name }}"
|
||||
{{ if .Size > 0 }}size="{{ .Size }}"{{ end }}
|
||||
{{ if .MaxLength > 0 }}maxlength="{{ .MaxLength }}"{{ end }}
|
||||
value="{{ .Value }}"
|
||||
value="{{ .Value }}" {{ if .Disabled }}disabled{{ end }}
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
|
||||
{{ vr := .ValueRange() }}
|
||||
{{ if vr.Low != -1 && vr.High != -1 }}
|
||||
@@ -86,36 +91,69 @@
|
||||
</div>
|
||||
{{ else if .Type == "password" }}
|
||||
<div class="flex items-center">
|
||||
<label for="{{ .Name }}" class="w-64 text-right pr-4 text-black text-sm">
|
||||
<label for="{{ .Name }}"
|
||||
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
|
||||
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}:
|
||||
{{ if .Required }}<span class="text-red-600">*</span>{{ end }}
|
||||
</label>
|
||||
<input type="password" id="{{ .Name }}" name="{{ .Name }}"
|
||||
{{ if .Size > 0 }}size="{{ .Size }}"{{ end }}
|
||||
{{ if .MaxLength > 0 }}maxlength="{{ .MaxLength }}"{{ end }}
|
||||
value="{{ .Value }}"
|
||||
value="{{ .Value }}" {{ if .Disabled }}disabled{{ end }}
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
|
||||
</div>
|
||||
{{ else if .Type == "checkbox" }}
|
||||
<div class="flex items-center">
|
||||
<div class="w-24 text-right pr-4">
|
||||
<input type="checkbox" id="{{ .Name }}" name="{{ .Name }}"
|
||||
value="Y" {{ if .Value != "" }}checked{{ end }}
|
||||
value="Y" {{ if .Value != "" }}checked{{ end }} {{ if .Disabled }}disabled{{ end }}
|
||||
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" />
|
||||
</div>
|
||||
<label for="{{ .Name }}" class="flex-1 text-black text-sm">
|
||||
<label for="{{ .Name }}" class="flex-1 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
|
||||
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}
|
||||
{{ if .Required }}<span class="text-red-600">*</span>{{ end }}
|
||||
</label>
|
||||
</div>
|
||||
{{ else if .Type == "countrylist" }}
|
||||
{{ else if .Type == "dropdown" }}
|
||||
<div class="flex items-center">
|
||||
<label for="{{ .Name }}" class="w-64 text-right pr-4 text-black text-sm">
|
||||
<label for="{{ .Name }}"
|
||||
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
|
||||
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}
|
||||
{{ if .Required }}<span class="text-red-600">*</span>{{ end }}
|
||||
</label>
|
||||
{{ v := .Value }}
|
||||
<select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }}
|
||||
<select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }} {{ if .Disabled }}disabled{{ end }}
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
{{ range .Choices }}
|
||||
<option value="{{ .Id }}" {{ if .Id == v }}selected{{ end }}>{{ .Text }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
</div>
|
||||
{{ else if .Type == "rolelist" }}
|
||||
<div class="flex items-center">
|
||||
<label for="{{ .Name }}"
|
||||
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
|
||||
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}
|
||||
{{ if .Required }}<span class="text-red-600">*</span>{{ end }}
|
||||
</label>
|
||||
{{ v := .Value }}
|
||||
{{ rl := AmRoleList(.Param) }}
|
||||
<select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }} {{ if .Disabled }}disabled{{ end }}
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
{{ range rl.Roles() }}
|
||||
<option value="{{ .LevelStr() }}" {{ if .LevelStr() == v }}selected{{ end }}>{{ .Name() }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
</div>
|
||||
{{ else if .Type == "countrylist" }}
|
||||
<div class="flex items-center">
|
||||
<label for="{{ .Name }}"
|
||||
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
|
||||
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}
|
||||
{{ if .Required }}<span class="text-red-600">*</span>{{ end }}
|
||||
</label>
|
||||
{{ v := .Value }}
|
||||
<select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }} {{ if .Disabled }}disabled{{ end }}
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="XX" {{ if v == "XX" }}selected{{ end }}>🏳️ (unknown)</option>
|
||||
{{ range GetCountryList() }}
|
||||
@@ -126,12 +164,13 @@
|
||||
</div>
|
||||
{{ else if .Type == "localelist" }}
|
||||
<div class="flex items-center">
|
||||
<label for="{{ .Name }}" class="w-64 text-right pr-4 text-black text-sm">
|
||||
<label for="{{ .Name }}"
|
||||
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
|
||||
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}
|
||||
{{ if .Required }}<span class="text-red-600">*</span>{{ end }}
|
||||
</label>
|
||||
{{ v := .Value }}
|
||||
<select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }}
|
||||
<select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }} {{ if .Disabled }}disabled{{ end }}
|
||||
class="flex-1 max-w-md px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
{{ range GetLanguageList() }}
|
||||
<option value="{{ .Tag }}" {{ if .Tag == v }}selected{{ end }}>{{ .Name }}</option>
|
||||
@@ -140,12 +179,13 @@
|
||||
</div>
|
||||
{{ else if .Type == "tzlist" }}
|
||||
<div class="flex items-center">
|
||||
<label for="{{ .Name }}" class="w-64 text-right pr-4 text-black text-sm">
|
||||
<label for="{{ .Name }}"
|
||||
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
|
||||
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}
|
||||
{{ if .Required }}<span class="text-red-600">*</span>{{ end }}
|
||||
</label>
|
||||
{{ v := .Value }}
|
||||
<select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }}
|
||||
<select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }} {{ if .Disabled }}disabled{{ end }}
|
||||
class="flex-1 max-w-md px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
{{ range GetTimeZoneList() }}
|
||||
<option value="{{ . }}" {{ if v == . }}selected{{ end }}>{{ . }}</option>
|
||||
@@ -155,25 +195,27 @@
|
||||
{{ else if .Type == "date" }}
|
||||
{{ dv := .DateValues() }}
|
||||
<div class="flex items-center">
|
||||
<label class="w-64 text-right pr-4 text-black text-sm">
|
||||
<label class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
|
||||
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}
|
||||
{{ if .Required }}<span class="text-red-600">*</span>{{ end }}
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<select name="{{ .Name }}_month"
|
||||
<select name="{{ .Name }}_month" {{ if .Disabled }}disabled{{ end }}
|
||||
class="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="-1" {{ if dv[0] == -1 }}selected{{ end }}>---</option>
|
||||
{{ range i := GetMonthList() }}
|
||||
<option value="{{ i + 1 }}" {{ if dv[0] == i + 1 }}selected{{ end }}>{{ . }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
<select name="{{ .Name }}_day" class="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<select name="{{ .Name }}_day" {{ if .Disabled }}disabled{{ end }}
|
||||
class="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="-1" {{ if dv[1] == -1 }}selected{{ end }}>---</option>
|
||||
{{ range MakeIntRange(1, 32, 1) }}
|
||||
<option value="{{ . }}" {{ if dv[1] == . }}selected{{ end }}>{{ . }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
<select name="{{ .Name }}_year" class="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<select name="{{ .Name }}_year" {{ if .Disabled }}disabled{{ end }}
|
||||
class="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="-1" {{ if dv[2] == -1 }}selected{{ end }}>---</option>
|
||||
{{ range MakeYearRange(.Param) }}
|
||||
<option value="{{ . }}" {{ if dv[2] == . }}selected{{ end }}>{{ . }}</option>
|
||||
@@ -183,13 +225,31 @@
|
||||
</div>
|
||||
{{ else if .Type == "userphoto" }}
|
||||
<div class="flex items-start">
|
||||
<label class="w-64 text-right pr-4 text-black text-sm pt-2">{{ .Caption }}
|
||||
<label class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm pt-2">{{ .Caption }}
|
||||
{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }} (click to change):</label>
|
||||
<input type="hidden" name="{{ .Name }}_data" value="{{ .Value }}"/>
|
||||
<a href="/profile_photo?tgt={{ target | url }}"
|
||||
class="border-2 border-gray-300 rounded hover:border-blue-500 transition-colors">
|
||||
<img src="{{ .Value }}" alt="Click to upload photo" class="w-25 h-25">
|
||||
</a>
|
||||
{{ if .Disabled }}
|
||||
<img src="{{ .Value }}" class="w-25 h-25">
|
||||
{{ else }}
|
||||
<a href="/profile_photo?tgt={{ target | url }}"
|
||||
class="border-2 border-gray-300 rounded hover:border-blue-500 transition-colors">
|
||||
<img src="{{ .Value }}" alt="Click to upload photo" class="w-25 h-25">
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ else if .Type == "communitylogo" }}
|
||||
<div class="flex items-start">
|
||||
<label class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm pt-2">{{ .Caption }}
|
||||
{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }} (click to change):</label>
|
||||
<input type="hidden" name="{{ .Name }}_data" value="{{ .Value }}"/>
|
||||
{{ if .Disabled }}
|
||||
<img src="/img/builtin/default-community.jpg" class="w-28 h-16 rounded">
|
||||
{{ else }}
|
||||
<a href="{{ .Param }}"
|
||||
class="border-2 border-gray-300 rounded hover:border-blue-500 transition-colors">
|
||||
<img src="/img/builtin/default-community.jpg" alt="Click to upload logo" class="w-28 h-16 rounded">
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ else if .Type == "header" }}
|
||||
<h2 class="text-lg font-bold text-black mb-4">{{ .Caption }}</h2>
|
||||
@@ -202,7 +262,7 @@
|
||||
{{ range amsterdam_dialog.Fields }}
|
||||
{{ if .Type == "button" }}
|
||||
{{ clstmp := "bg-" + .Param + "-600 hover:bg-" + .Param + "-700" }}
|
||||
<button type="submit" name="{{ .Name }}"
|
||||
<button type="submit" name="{{ .Name }}" {{ if .Disabled }}disabled{{ end }}
|
||||
class="{{ clstmp }} text-white px-6 py-2 rounded font-medium transition-colors">{{ .Caption }}</button>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
Reference in New Issue
Block a user