landed code for creating community (untested)

This commit is contained in:
2025-10-18 22:38:27 -06:00
parent fcbff708a5
commit 021807e53e
9 changed files with 426 additions and 30 deletions
+133 -7
View File
@@ -54,6 +54,7 @@ func CommunityAdminMenu(ctxt ui.AmContext) (string, any, error) {
return "framed_template", "menu.jet", nil
}
// setupCommunityProfileDialog sets up fields in the Community Profile dialog.
func setupCommunityProfileDialog(dlg *ui.Dialog, comm *database.Community) {
dlg.SetCommunity(comm)
if comm.IsAdmin {
@@ -108,7 +109,6 @@ func CommunityProfileForm(ctxt ui.AmContext) (string, any, error) {
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)
@@ -143,10 +143,16 @@ func CommunityProfileForm(ctxt ui.AmContext) (string, any, error) {
return ui.ErrorPage(ctxt, err)
}
// levelFld is a quick routine to extract a level value from a drop-down.
func levelFld(d *ui.Dialog, name string) uint16 {
v, _ := strconv.Atoi(d.Field(name).Value)
return uint16(v)
// validateJoinKey is an extra validation step for the join key.
func validateJoinKey(dlg *ui.Dialog) error {
if dlg.Field("comtype").Value == "1" {
if dlg.Field("joinkey").IsEmpty() {
return errors.New("private community must specify a join key")
}
} else {
dlg.Field("joinkey").Value = ""
}
return nil
}
/* EditCommunityProfile updates the community's profile from the dialog.
@@ -182,6 +188,10 @@ func EditCommunityProfile(ctxt ui.AmContext) (string, any, error) {
if err != nil {
return dlg.RenderError(ctxt, err.Error())
}
err = validateJoinKey(dlg)
if err != nil {
return dlg.RenderError(ctxt, err.Error())
}
var ci *database.ContactInfo
ci, err = comm.ContactInfo()
if err != nil {
@@ -222,8 +232,8 @@ func EditCommunityProfile(ctxt ui.AmContext) (string, any, error) {
}
err = comm.SetProfileData(dlg.Field("name").Value, dlg.Field("alias").Value, dlg.Field("synopsis").ValPtr(),
dlg.Field("rules").ValPtr(), dlg.Field("language").ValPtr(), joinkey, dlg.Field("membersonly").IsChecked(),
hidedir, hidesearch, levelFld(dlg, "read_lvl"), levelFld(dlg, "write_lvl"), levelFld(dlg, "create_lvl"),
levelFld(dlg, "delete_lvl"), levelFld(dlg, "join_lvl"))
hidedir, hidesearch, dlg.Field("read_lvl").GetLevel(), dlg.Field("write_lvl").GetLevel(),
dlg.Field("create_lvl").GetLevel(), dlg.Field("delete_lvl").GetLevel(), dlg.Field("join_lvl").GetLevel())
}
if err == nil {
flags.Set(database.CommunityFlagPicturesInPosts, dlg.Field("pic_in_post").IsChecked())
@@ -241,6 +251,14 @@ func EditCommunityProfile(ctxt ui.AmContext) (string, any, error) {
return ui.ErrorPage(ctxt, err)
}
/* CommunityLogoForm renders the form for changing the community logo.
* Parameters:
* ctxt - The AmContext for the request.
* Returns:
* Command string dictating what to be rendered.
* Data as a parameter for the command string.
* Standard Go error status.
*/
func CommunityLogoForm(ctxt ui.AmContext) (string, any, error) {
err := ctxt.SetCommunityContext(ctxt.URLParam("cid"))
if err != nil {
@@ -263,6 +281,14 @@ func CommunityLogoForm(ctxt ui.AmContext) (string, any, error) {
return ui.ErrorPage(ctxt, err)
}
/* EditCommunityLogo handles setting the community logo.
* Parameters:
* ctxt - The AmContext for the request.
* Returns:
* Command string dictating what to be rendered.
* Data as a parameter for the command string.
* Standard Go error status.
*/
func EditCommunityLogo(ctxt ui.AmContext) (string, any, error) {
err := ctxt.SetCommunityContext(ctxt.URLParam("cid"))
if err != nil {
@@ -341,3 +367,103 @@ func EditCommunityLogo(ctxt ui.AmContext) (string, any, error) {
}
return ui.ErrorPage(ctxt, errors.New("invalid button detected in logo upload"))
}
/* CreateCommunityForm renders the form for creating a new community.
* Parameters:
* ctxt - The AmContext for the request.
* Returns:
* Command string dictating what to be rendered.
* Data as a parameter for the command string.
* Standard Go error status.
*/
func CreateCommunityForm(ctxt ui.AmContext) (string, any, error) {
user := ctxt.CurrentUser()
if user.BaseLevel < uint16(ctxt.Globals().CommunityCreateLevel) {
ctxt.SetRC(http.StatusForbidden)
return ui.ErrorPage(ctxt, errors.New("you are not permitted to create a community"))
}
dlg, err := ui.AmLoadDialog("create_comm")
if err != nil {
dlg.Field("language").Value = "en-US"
dlg.Field("country").Value = "US"
return dlg.Render(ctxt)
}
return ui.ErrorPage(ctxt, err)
}
/* CreateCommunity creates a new community.
* Parameters:
* ctxt - The AmContext for the request.
* Returns:
* Command string dictating what to be rendered.
* Data as a parameter for the command string.
* Standard Go error status.
*/
func CreateCommunity(ctxt ui.AmContext) (string, any, error) {
user := ctxt.CurrentUser()
if user.BaseLevel < uint16(ctxt.Globals().CommunityCreateLevel) {
ctxt.SetRC(http.StatusForbidden)
return ui.ErrorPage(ctxt, errors.New("you are not permitted to create a community"))
}
dlg, err := ui.AmLoadDialog("create_comm")
if err != nil {
dlg.LoadFromForm(ctxt)
action := dlg.WhichButton(ctxt)
if action == "cancel" {
return "redirect", "/", nil
}
if action == "create" {
err = dlg.Validate()
if err != nil {
return dlg.RenderError(ctxt, err.Error())
}
err = validateJoinKey(dlg)
if err != nil {
return dlg.RenderError(ctxt, err.Error())
}
var testcomm *database.Community
testcomm, err = database.AmGetCommunityByAlias(dlg.Field("alias").Value)
if err != nil {
return dlg.RenderError(ctxt, err.Error())
}
if testcomm != nil {
return dlg.RenderError(ctxt, fmt.Sprintf("A community with the alias \"%s\" already exists; please try again.", testcomm.Alias))
}
var hideDir, hideSearch bool
switch dlg.Field("hidemode").Value {
case "NONE":
hideDir = false
hideSearch = false
case "DIRECTORY":
hideDir = true
hideSearch = false
case "BOTH":
hideDir = true
hideSearch = true
}
var comm *database.Community
comm, err = database.AmCreateCommunity(dlg.Field("name").Value, dlg.Field("alias").Value, user.Uid,
dlg.Field("language").ValPtr(), dlg.Field("synopsis").ValPtr(), dlg.Field("rules").ValPtr(),
dlg.Field("joinkey").ValPtr(), hideDir, hideSearch, ctxt.RemoteIP())
if err != nil {
return dlg.RenderError(ctxt, err.Error())
}
ci := database.AmNewCommunityContactInfo(user.Uid, comm.Id)
ci.Locality = dlg.Field("loc").ValPtr()
ci.Region = dlg.Field("reg").ValPtr()
ci.PostalCode = dlg.Field("pcode").ValPtr()
ci.Country = dlg.Field("country").ValPtr()
_, err = ci.Save()
if err == nil {
err = comm.SetContactID(ci.ContactId)
}
if err != nil {
return dlg.RenderError(ctxt, err.Error())
}
// new community is now created! redirect to the new profile
return "redirect", fmt.Sprintf("/comm/%s/profile", comm.Alias), nil
}
return dlg.RenderError(ctxt, "No known button click on POST to community creation.")
}
return ui.ErrorPage(ctxt, err)
}
+12
View File
@@ -46,6 +46,18 @@ const (
AuditAdminChangeUserAccount = 111
AuditAdminSetAccountSecurity = 112
AuditAdminLockUnlockAccount = 113
AuditCommunityCreate = 201
AuditCommunitySetMembership = 202
AuditCommuntiyContactInfo = 203
AuditCommunityFeatureSet = 204
AuditCommunityName = 205
AuditCommunityAlias = 206
AuditCommunityCategory = 207
AuditCommunityHideInfo = 208
AuditCommunityMembersOnly = 209
AuditCommunityJoinKey = 210
AuditCommunitySecurity = 211
AuditCommunityDelete = 212
)
// auditWriteQueue is a channel to store audit records in the background.
+125 -9
View File
@@ -10,6 +10,7 @@
package database
import (
"errors"
"fmt"
"slices"
"strconv"
@@ -248,10 +249,11 @@ func (c *Community) Flags() (*util.OptionSet, error) {
return nil, err
}
if s == nil {
return nil, fmt.Errorf("missing flags for community %d", c.Id)
}
c.flags = util.NewOptionSet()
} else {
c.flags = util.OptionSetFromString(*s)
}
}
return c.flags, nil
}
@@ -302,6 +304,17 @@ func (c *Community) SetProfileData(name string, alias string, synopsis *string,
return err
}
// SetContactID sets the contact ID for the community.
func (c *Community) SetContactID(cid int32) error {
c.Mutex.Lock()
defer c.Mutex.Unlock()
if _, err := amdb.Exec("UPDATE communities SET contactid = ? WHERE commid = ?", cid, c.Id); err != nil {
return err
}
c.ContactId = cid
return nil
}
/* AmGetCommunity returns a reference to the specified community.
* Parameters:
* id - The ID of the community.
@@ -331,6 +344,27 @@ func AmGetCommunity(id int32) (*Community, error) {
return rc.(*Community), err
}
/* AmGetCommunityByAlias returns a reference to the specified community.
* Parameters:
* alias - The alias for the community.
* Returns:
* Pointer to Community containing community data, or nil
* Standard Go error status (nil if community not found)
*/
func AmGetCommunityByAlias(alias string) (*Community, error) {
rs, err := amdb.Query("SELECT commid FROM communities WHERE alias = ?", alias)
if err == nil {
if rs.Next() {
var cid int32
rs.Scan(&cid)
return AmGetCommunity(cid)
} else {
return nil, nil
}
}
return nil, 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.
@@ -349,17 +383,13 @@ func AmGetCommunityFromParam(param string) (*Community, error) {
}
// else fall through to trying as alias
}
rs, err := amdb.Query("SELECT commid FROM communities WHERE alias = ?", param)
rc, err := AmGetCommunityByAlias(param)
if err == nil {
if rs.Next() {
var cid int32
rs.Scan(&cid)
return AmGetCommunity(cid)
} else {
if rc == nil {
return nil, fmt.Errorf("community with alias \"%s\" not found", param)
}
}
return nil, err
return rc, err
}
/* AmGetCommunitiesForUser returns a list of communities the user is a member of.
@@ -482,6 +512,8 @@ func AmGetCommunityProperty(cid int32, ndx int32) (*string, error) {
p, err := internalGetCommProp(cid, ndx)
if err != nil {
return nil, err
} else if p == nil {
return nil, nil
}
return p.Data, nil
}
@@ -515,3 +547,87 @@ func AmSetCommunityProperty(cid int32, ndx int32, val *string) error {
}
return err
}
/* AmCreateCommunity creates a new community.
* Parameters:
* name - The name for the new community.
* alias - The alias for the new community. Must be unique.
* hostUid - The UID of the creator and new host of the community.
* language - Community default language.
* synopsis - Community synopsis string.
* rules - Community rules string.
* joinkey - Community join key, or empty string for a public community.
* hideDirectory - true to hide this community from the directory listings.
* hideSearch - true to hide this community from searches.
* remoteIP - Remote IP address for audit record.
* Returns:
* Pointer to new Community record, or nil.
* Standard Go error status.
*/
func AmCreateCommunity(name string, alias string, hostUid int32, language *string, synopsis *string,
rules *string, joinkey *string, hideDirectory bool, hideSearch bool, remoteIP string) (*Community, error) {
var ar *AuditRecord = nil
defer func() {
AmStoreAudit(ar)
}()
unlock := true
amdb.Exec("LOCK TABLES communities WRITE, commftrs WRITE, commmember WRITE;")
defer func() {
if unlock {
amdb.Exec("UNLOCK TABLES;")
}
}()
// validate alias does not already exist
rs, err := amdb.Query("SELECT commid FROM communities WHERE alias = ?", alias)
if err != nil {
return nil, err
}
if rs.Next() {
return nil, errors.New("a community with that alias already exists")
}
// establish the community record
_, err = amdb.Exec(`INSERT INTO communities (createdate, lastaccess, lastupdate, read_lvl, write_lvl,
create_lvl, delete_lvl, join_lvl, host_uid, hide_dir, hide_search, commname, language,
synopsis, rules, joinkey, alias) VALUES (NOW(), NOW(), NOW(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
AmRoleList("Community.Read").Default().Level(), AmRoleList("Community.Write").Default().Level(),
AmRoleList("Community.Create").Default().Level(), AmRoleList("Community.Delete").Default().Level(),
AmRoleList("Community.Join").Default().Level(), hostUid, hideDirectory, hideSearch, name, language,
synopsis, rules, joinkey, alias)
if err != nil {
return nil, err
}
// Read back the community, which also puts it in the cache.
comm, err := AmGetCommunityByAlias(alias)
if err != nil {
return nil, err
} else if comm == nil {
return nil, errors.New("unable to find newly-generated community")
}
// Establish the community services.
err = AmEstablishCommunityServices(comm.Id)
if err != nil {
return nil, err
}
// Ensure the new host has host privileges in the community. The host's membership is "locked" so they
// can't unjoin and leave the community hostless.
_, err = amdb.Exec("INSERT INTO commmember (commid, uid, granted_lvl, locked) VALUES (?, ?, ?, 1)", comm.Id, hostUid,
AmDefaultRole("Community.Creator").Level())
if err != nil {
return nil, err
}
stuffMembership(comm.Id, hostUid, true, true, AmDefaultRole("Community.Creator").Level())
amdb.Exec("UNLOCK TABLES;")
unlock = false
// operation was a success - add an audit record
ar = AmNewAudit(AuditCommunityCreate, hostUid, remoteIP, fmt.Sprintf("id=%d", comm.Id),
fmt.Sprintf("name=%s", comm.Name), fmt.Sprintf("alias=%s", comm.Alias))
return comm, nil
}
+12
View File
@@ -250,3 +250,15 @@ func AmNewUserContactInfo(uid int32) *ContactInfo {
rc := ContactInfo{OwnerUid: uid, OwnerCommId: -1}
return &rc
}
/* AmNewCommunityContactInfo creates a new contact info record for the community.
* Parameters:
* uid - The UID of the host of this community.
* cid - The community ID of the owning community.
* Returns:
* New ContactInfo structure.
*/
func AmNewCommunityContactInfo(uid int32, cid int32) *ContactInfo {
rc := ContactInfo{OwnerUid: uid, OwnerCommId: cid}
return &rc
}
+24
View File
@@ -114,3 +114,27 @@ func AmGetCommunityServices(cid int32) ([]*ServiceDef, error) {
}
return rc.([]*ServiceDef), nil
}
/* AmEstablishCommunityServices extablishes the service (feature) records for a new community.
* Parameters:
* cid - ID of the new community.
* Returns:
* Standard Go error status.
*/
func AmEstablishCommunityServices(cid int32) error {
dom := serviceRoot.byName["community"]
a := make([]*ServiceDef, 0, len(dom.Services))
for i, svc := range dom.Services {
if svc.Default {
_, err := amdb.Exec("INSERT INTO commftrs (commid, ftr_code) VALUES (?, ?)", cid, svc.Index)
if err != nil {
return err
}
a = append(a, &(dom.Services[i]))
}
}
servicesCacheMutex.Lock()
servicesCache.Add(cid, a)
servicesCacheMutex.Unlock()
return nil
}
+2
View File
@@ -64,6 +64,8 @@ func setupEcho() *echo.Echo {
e.GET("/user/:uname", ui.AmWrap(ShowProfile))
e.POST("/quick_email", ui.AmWrap(QuickEMail))
e.GET("/sysadmin", ui.AmWrap(SysAdminMenu))
e.GET("/create_comm", ui.AmWrap(CreateCommunityForm))
e.POST("/create_comm", ui.AmWrap(CreateCommunity))
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))
-3
View File
@@ -13,9 +13,6 @@ title: "Edit Community Profile:"
subtitle: "[CNAME]"
action: "/comm/[CID]/admin/profile"
fields:
- type: "hidden"
name: "cc"
value: ""
- type: "header"
name: "header1"
caption: "Basic Information"
+107
View File
@@ -0,0 +1,107 @@
#
# 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: "create.community"
formName: "createcommform"
menuSelector: "top"
title: "Create New Community"
action: "/create_comm"
fields:
- 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: "header"
name: "header2"
caption: "Location"
- 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: "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: "button"
name: "create"
caption: "Create"
param: "blue"
- type: "button"
name: "cancel"
caption: "Cancel"
param: "red"
+1 -1
View File
@@ -35,7 +35,7 @@
{{ if sb.Flags["canCreate"] }}|{{ end }}
{{ end }}
{{ if sb.Flags["canCreate"] }}
<a href="/TODO/create_comm" class="text-blue-700 hover:text-blue-900">Create New</a>
<a href="/create_comm" class="text-blue-700 hover:text-blue-900">Create New</a>
{{ end }}
]
</span>