completed member export and removed some "breaking" bugs, still work to be done on the XML output
This commit is contained in:
+1
-1
@@ -361,7 +361,7 @@ func MemberList(ctxt ui.AmContext) (string, any) {
|
|||||||
ctxt.VarMap().Set("oper", "st")
|
ctxt.VarMap().Set("oper", "st")
|
||||||
ctxt.VarMap().Set("term", "")
|
ctxt.VarMap().Set("term", "")
|
||||||
ctxt.VarMap().Set("ofs", ofs)
|
ctxt.VarMap().Set("ofs", ofs)
|
||||||
ctxt.SetFrameTitle("List Members")
|
ctxt.SetFrameTitle("Members of Community " + comm.Name)
|
||||||
listMax := int(ctxt.Globals().MaxCommunityMemberPage)
|
listMax := int(ctxt.Globals().MaxCommunityMemberPage)
|
||||||
results, total, err := comm.ListMembers(ctxt.Ctx(), database.ListMembersFieldNone, database.ListMembersOperNone, "", ofs*listMax, listMax, showHidden)
|
results, total, err := comm.ListMembers(ctxt.Ctx(), database.ListMembersFieldNone, database.ListMembersOperNone, "", ofs*listMax, listMax, showHidden)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+38
-1
@@ -14,6 +14,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -21,12 +22,48 @@ import (
|
|||||||
|
|
||||||
"git.erbosoft.com/amy/amsterdam/database"
|
"git.erbosoft.com/amy/amsterdam/database"
|
||||||
"git.erbosoft.com/amy/amsterdam/email"
|
"git.erbosoft.com/amy/amsterdam/email"
|
||||||
|
"git.erbosoft.com/amy/amsterdam/exports"
|
||||||
"git.erbosoft.com/amy/amsterdam/ui"
|
"git.erbosoft.com/amy/amsterdam/ui"
|
||||||
"git.erbosoft.com/amy/amsterdam/util"
|
"git.erbosoft.com/amy/amsterdam/util"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/* ExportCommunityMembers exports the members of the community as XML.
|
||||||
|
* Parameters:
|
||||||
|
* ctxt - The AmContext for the request.
|
||||||
|
* Returns:
|
||||||
|
* Command string dictating what to be rendered.
|
||||||
|
* Data as a parameter for the command string.
|
||||||
|
*/
|
||||||
|
func ExportCommunityMembers(ctxt ui.AmContext) (string, any) {
|
||||||
|
comm := ctxt.CurrentCommunity()
|
||||||
|
if !comm.TestPermission("Community.ShowAdmin", ctxt.EffectiveLevel()) {
|
||||||
|
return "error", ENOACCESS
|
||||||
|
}
|
||||||
|
|
||||||
|
// use a dedicated goroutine to generate the streamed XML and send it into one end of a pipe
|
||||||
|
filename := time.Now().Format("exported-users-20060102.xml")
|
||||||
|
r, w := io.Pipe()
|
||||||
|
go func() {
|
||||||
|
start := time.Now()
|
||||||
|
err := exports.VIUStreamCommunityMemberList(context.Background(), w, comm)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("ExportCommunityMembers task failed with %v", err)
|
||||||
|
s := fmt.Sprintf("<!-- ***PROCESSING ERROR*** %v -->\r\n", err)
|
||||||
|
w.Write([]byte(s))
|
||||||
|
}
|
||||||
|
w.Close()
|
||||||
|
dur := time.Since(start)
|
||||||
|
log.Infof("ExportCommunityMembers task completed in %v", dur)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Now we connect the outlet end of the pipe to the output to the browser.
|
||||||
|
ctxt.SetOutputType("text/xml")
|
||||||
|
ctxt.SetHeader("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
|
||||||
|
return "stream", r
|
||||||
|
}
|
||||||
|
|
||||||
/* CommunityAdminMenu renders the community administration menu.
|
/* CommunityAdminMenu renders the community administration menu.
|
||||||
* Parameters:
|
* Parameters:
|
||||||
* ctxt - The AmContext for the request.
|
* ctxt - The AmContext for the request.
|
||||||
@@ -35,7 +72,7 @@ import (
|
|||||||
* Data as a parameter for the command string.
|
* Data as a parameter for the command string.
|
||||||
*/
|
*/
|
||||||
func CommunityAdminMenu(ctxt ui.AmContext) (string, any) {
|
func CommunityAdminMenu(ctxt ui.AmContext) (string, any) {
|
||||||
comm := ctxt.CurrentCommunity() // set by middleware
|
comm := ctxt.CurrentCommunity()
|
||||||
if !comm.TestPermission("Community.ShowAdmin", ctxt.EffectiveLevel()) {
|
if !comm.TestPermission("Community.ShowAdmin", ctxt.EffectiveLevel()) {
|
||||||
return "error", ENOACCESS
|
return "error", ENOACCESS
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -14,7 +14,6 @@ import (
|
|||||||
_ "embed"
|
_ "embed"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -180,7 +179,7 @@ func overlayStringArray(loaded, defaulted []string) []string {
|
|||||||
m[s] = true
|
m[s] = true
|
||||||
}
|
}
|
||||||
rc := make([]string, 0, len(m))
|
rc := make([]string, 0, len(m))
|
||||||
for s := range maps.Keys(m) {
|
for s := range m {
|
||||||
rc = append(rc, s)
|
rc = append(rc, s)
|
||||||
}
|
}
|
||||||
return rc
|
return rc
|
||||||
|
|||||||
@@ -195,6 +195,18 @@ lists:
|
|||||||
- "Community.Member"
|
- "Community.Member"
|
||||||
- "UnrestrictedUser"
|
- "UnrestrictedUser"
|
||||||
- "Community.Cohost"
|
- "Community.Cohost"
|
||||||
|
- name: "Community.AllUserlevels"
|
||||||
|
roles:
|
||||||
|
- "NotInList"
|
||||||
|
- "Global.Anonymous"
|
||||||
|
- "Global.Unverified"
|
||||||
|
- "Global.Normal"
|
||||||
|
- "Community.Member"
|
||||||
|
- "UnrestrictedUser"
|
||||||
|
- "Community.Cohost"
|
||||||
|
- "Community.Host"
|
||||||
|
- "Global.PFY"
|
||||||
|
- "Global.BOFH"
|
||||||
- name: "Conference.Read"
|
- name: "Conference.Read"
|
||||||
roles:
|
roles:
|
||||||
- "Global.Anonymous"
|
- "Global.Anonymous"
|
||||||
|
|||||||
+3
-3
@@ -100,7 +100,7 @@ func (p *UserPrefs) Localizer() lctime.Localizer {
|
|||||||
func (p *UserPrefs) LanguageTag() *language.Tag {
|
func (p *UserPrefs) LanguageTag() *language.Tag {
|
||||||
lt, err := language.Parse(p.ReadLocale())
|
lt, err := language.Parse(p.ReadLocale())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("BOGUS LANGUAGE TAG %s in user prefs for uid %d", p.LocaleID, p.Uid)
|
log.Errorf("BOGUS LANGUAGE TAG %s in user prefs for uid %d", p.LocaleID, p.Uid)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return <
|
return <
|
||||||
@@ -115,7 +115,7 @@ func (p *UserPrefs) MessagePrinter() *message.Printer {
|
|||||||
func (p *UserPrefs) Location() *time.Location {
|
func (p *UserPrefs) Location() *time.Location {
|
||||||
rc, err := time.LoadLocation(p.TimeZoneID)
|
rc, err := time.LoadLocation(p.TimeZoneID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("BOGUS TIMEZONE TAG %s in user prefs for uid %d", p.TimeZoneID, p.Uid)
|
log.Errorf("BOGUS TIMEZONE TAG %s in user prefs for uid %d", p.TimeZoneID, p.Uid)
|
||||||
return time.Local
|
return time.Local
|
||||||
}
|
}
|
||||||
return rc
|
return rc
|
||||||
@@ -669,7 +669,7 @@ func crackAuthString(authString string) (int32, string, error) {
|
|||||||
* Parameters:
|
* Parameters:
|
||||||
* ctx - Standard Go context value.
|
* ctx - Standard Go context value.
|
||||||
* authString - The stored cookie authentication string.
|
* authString - The stored cookie authentication string.
|
||||||
* remoteIP - The remote IP address wheter trhe user is logging in from.
|
* remoteIP - The remote IP address where the user is logging in from.
|
||||||
* Returns:
|
* Returns:
|
||||||
* Pointer to the authenticated User, or nil.
|
* Pointer to the authenticated User, or nil.
|
||||||
* Standard Go error status.
|
* Standard Go error status.
|
||||||
|
|||||||
+12
-12
@@ -199,12 +199,12 @@ type VCKey struct {
|
|||||||
func VCardFromContactInfo(ctx context.Context, target *VCard, ci *database.ContactInfo) error {
|
func VCardFromContactInfo(ctx context.Context, target *VCard, ci *database.ContactInfo) error {
|
||||||
target.Version = "2.0"
|
target.Version = "2.0"
|
||||||
target.FullName = ci.FullName(false)
|
target.FullName = ci.FullName(false)
|
||||||
target.Name.Family = util.IIF(ci.FamilyName != nil, *ci.FamilyName, "")
|
target.Name.Family = util.SRef(ci.FamilyName)
|
||||||
target.Name.Given = util.IIF(ci.GivenName != nil, *ci.GivenName, "")
|
target.Name.Given = util.SRef(ci.GivenName)
|
||||||
target.Name.Middle = util.IIF(ci.MiddleInit != nil, *ci.MiddleInit+".", "")
|
target.Name.Middle = util.SRef(ci.MiddleInit)
|
||||||
target.Name.Prefix = util.IIF(ci.Prefix != nil, *ci.Prefix, "")
|
target.Name.Prefix = util.SRef(ci.Prefix)
|
||||||
target.Name.Suffix = util.IIF(ci.Suffix != nil, *ci.Suffix, "")
|
target.Name.Suffix = util.SRef(ci.Suffix)
|
||||||
target.URL = util.IIF(ci.URL != nil, *ci.URL, "")
|
target.URL = util.SRef(ci.URL)
|
||||||
if ci.LastUpdate != nil {
|
if ci.LastUpdate != nil {
|
||||||
target.LastUpdate = ci.LastUpdate.Format(ISO8601)
|
target.LastUpdate = ci.LastUpdate.Format(ISO8601)
|
||||||
}
|
}
|
||||||
@@ -213,12 +213,12 @@ func VCardFromContactInfo(ctx context.Context, target *VCard, ci *database.Conta
|
|||||||
addr[0].Home.Local = "HOME"
|
addr[0].Home.Local = "HOME"
|
||||||
addr[0].Postal.Local = "POSTAL"
|
addr[0].Postal.Local = "POSTAL"
|
||||||
addr[0].Preferred.Local = "PREF"
|
addr[0].Preferred.Local = "PREF"
|
||||||
addr[0].Street = util.IIF(ci.Addr1 != nil, *ci.Addr1, "")
|
addr[0].Street = util.SRef(ci.Addr1)
|
||||||
addr[0].ExtAddr = util.IIF(ci.Addr2 != nil, *ci.Addr2, "")
|
addr[0].ExtAddr = util.SRef(ci.Addr2)
|
||||||
addr[0].Locality = util.IIF(ci.Locality != nil, *ci.Locality, "")
|
addr[0].Locality = util.SRef(ci.Locality)
|
||||||
addr[0].Region = util.IIF(ci.Region != nil, *ci.Region, "")
|
addr[0].Region = util.SRef(ci.Region)
|
||||||
addr[0].PCode = util.IIF(ci.PostalCode != nil, *ci.PostalCode, "")
|
addr[0].PCode = util.SRef(ci.PostalCode)
|
||||||
addr[0].Country = util.IIF(ci.Country != nil, *ci.Country, "")
|
addr[0].Country = util.SRef(ci.Country)
|
||||||
target.Address = &addr
|
target.Address = &addr
|
||||||
|
|
||||||
phcount := util.IIF(ci.Phone != nil, 1, 0) + util.IIF(ci.Fax != nil, 1, 0) + util.IIF(ci.Mobile != nil, 1, 0)
|
phcount := util.IIF(ci.Phone != nil, 1, 0) + util.IIF(ci.Fax != nil, 1, 0) + util.IIF(ci.Mobile != nil, 1, 0)
|
||||||
|
|||||||
+62
-2
@@ -12,6 +12,9 @@ package exports
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.erbosoft.com/amy/amsterdam/database"
|
"git.erbosoft.com/amy/amsterdam/database"
|
||||||
"git.erbosoft.com/amy/amsterdam/util"
|
"git.erbosoft.com/amy/amsterdam/util"
|
||||||
@@ -73,6 +76,7 @@ type VIUCommunityJoin struct {
|
|||||||
Community string `xml:",chardata"` // name of community joined
|
Community string `xml:",chardata"` // name of community joined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VIUUserFromUser fills in a VIUUser structure with details from the given user.
|
||||||
func VIUUserFromUser(ctx context.Context, target *VIUUser, u *database.User) error {
|
func VIUUserFromUser(ctx context.Context, target *VIUUser, u *database.User) error {
|
||||||
// Fill base fields first.
|
// Fill base fields first.
|
||||||
target.ID = int(u.Uid)
|
target.ID = int(u.Uid)
|
||||||
@@ -80,7 +84,7 @@ func VIUUserFromUser(ctx context.Context, target *VIUUser, u *database.User) err
|
|||||||
target.Password.Prehashed = true
|
target.Password.Prehashed = true
|
||||||
target.Password.Hash = u.Passhash
|
target.Password.Hash = u.Passhash
|
||||||
target.PasswordReminder = u.PassReminder
|
target.PasswordReminder = u.PassReminder
|
||||||
target.Description = util.IIF(u.Description != nil, *u.Description, "")
|
target.Description = util.SRef(u.Description)
|
||||||
|
|
||||||
// Get the contact info.
|
// Get the contact info.
|
||||||
ci, err := u.ContactInfo(ctx)
|
ci, err := u.ContactInfo(ctx)
|
||||||
@@ -126,10 +130,66 @@ func VIUUserFromUser(ctx context.Context, target *VIUUser, u *database.User) err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fill options from user flags.
|
||||||
target.Options.PostPictures = flags.Get(database.UserFlagPicturesInPosts)
|
target.Options.PostPictures = flags.Get(database.UserFlagPicturesInPosts)
|
||||||
target.Options.OptOut = flags.Get(database.UserFlagMassMailOptOut)
|
target.Options.OptOut = flags.Get(database.UserFlagMassMailOptOut)
|
||||||
target.Options.NoPhoto = flags.Get(database.UserFlagDisallowSetPhoto)
|
target.Options.NoPhoto = flags.Get(database.UserFlagDisallowSetPhoto)
|
||||||
|
|
||||||
// TODO - fill in Joins
|
// Get the list of communities for the user and set up the Joins list.
|
||||||
|
comms, err := database.AmGetCommunitiesForUser(ctx, u.Uid)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
target.Joins = make([]VIUCommunityJoin, len(comms))
|
||||||
|
roles := database.AmRoleList("Community.AllUserlevels")
|
||||||
|
for i, c := range comms {
|
||||||
|
_, _, level, err := c.Membership(ctx, u)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
target.Joins[i].Role = roles.FindForLevel(level).ID()
|
||||||
|
target.Joins[i].Community = c.Alias
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VIUStreamCommunityMembersList streams a list of users of the given community to the given stream in VIU format.
|
||||||
|
func VIUStreamCommunityMemberList(ctx context.Context, w io.Writer, comm *database.Community) error {
|
||||||
|
// Get the list of community members.
|
||||||
|
total, err := comm.MemberCount(ctx, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
users, _, err := comm.ListMembers(ctx, database.ListMembersFieldNone, database.ListMembersOperNone, "", 0, total, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the header of the file.
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(xml.Header)
|
||||||
|
b.WriteString("\r\n<venice-import-users>\r\n")
|
||||||
|
_, err = w.Write([]byte(b.String()))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the XML encoder.
|
||||||
|
enc := xml.NewEncoder(w)
|
||||||
|
enc.Indent(" ", " ")
|
||||||
|
|
||||||
|
// Build each user and then encode them to the output.
|
||||||
|
for _, u := range users {
|
||||||
|
var encodedUser VIUUser
|
||||||
|
err = VIUUserFromUser(ctx, &encodedUser, u)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error converting user %d: %v", u.Uid, err)
|
||||||
|
}
|
||||||
|
enc.Encode(encodedUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the trailing tag.
|
||||||
|
_, err = w.Write([]byte("</venice-import-users>\r\n"))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ var GetAndPost = []string{http.MethodGet, http.MethodPost}
|
|||||||
// setupEcho creates, configures, and returns a new Echo instance.
|
// setupEcho creates, configures, and returns a new Echo instance.
|
||||||
func setupEcho() *echo.Echo {
|
func setupEcho() *echo.Echo {
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
|
e.HideBanner = true
|
||||||
e.Logger = &EchoLogrusAdapter{}
|
e.Logger = &EchoLogrusAdapter{}
|
||||||
e.Renderer = &ui.TemplateRenderer{}
|
e.Renderer = &ui.TemplateRenderer{}
|
||||||
e.HTTPErrorHandler = AmErrorHandler
|
e.HTTPErrorHandler = AmErrorHandler
|
||||||
@@ -125,6 +126,7 @@ func setupEcho() *echo.Echo {
|
|||||||
commGroup.POST("/unjoin", ui.AmWrap(UnjoinCommunityConfirm))
|
commGroup.POST("/unjoin", ui.AmWrap(UnjoinCommunityConfirm))
|
||||||
commGroup.GET("/members", ui.AmWrap(MemberList))
|
commGroup.GET("/members", ui.AmWrap(MemberList))
|
||||||
commGroup.POST("/members", ui.AmWrap(MemberSearch))
|
commGroup.POST("/members", ui.AmWrap(MemberSearch))
|
||||||
|
commGroup.GET("/members/export", ui.AmWrap(ExportCommunityMembers))
|
||||||
commGroup.GET("/invite", ui.AmWrap(InviteToCommunity))
|
commGroup.GET("/invite", ui.AmWrap(InviteToCommunity))
|
||||||
commGroup.GET("/find", ui.AmWrap(FindPostsPageCommunity))
|
commGroup.GET("/find", ui.AmWrap(FindPostsPageCommunity))
|
||||||
commGroup.POST("/find", ui.AmWrap(FindPostsCommunity))
|
commGroup.POST("/find", ui.AmWrap(FindPostsCommunity))
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<!-- Page Title with Tabs -->
|
<!-- Page Title with Tabs -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<div class="flex items-baseline gap-2">
|
<div class="flex items-baseline gap-2">
|
||||||
<h1 class="text-blue-800 text-4xl font-bold">Find:</h1>
|
<h1 class="text-blue-800 text-4xl font-bold">Members of Community:</h1>
|
||||||
<span class="text-blue-800 text-2xl">{{ comm.Name }}</span>
|
<span class="text-blue-800 text-2xl">{{ comm.Name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<hr class="border-2 border-gray-400 w-4/5 mt-2 mb-6">
|
<hr class="border-2 border-gray-400 w-4/5 mt-2 mb-6">
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
{{ if canExport }}
|
{{ if canExport }}
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
[
|
[
|
||||||
<a href="/TODO/comm/{{ comm.Alias }}/member_export" class="text-blue-700 hover:text-blue-900">Export Member List</a>
|
<a href="/comm/{{ comm.Alias }}/members/export" class="text-blue-700 hover:text-blue-900">Export Member List</a>
|
||||||
]
|
]
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@@ -189,3 +189,11 @@ func IIF[A any](expr bool, v1, v2 A) A {
|
|||||||
return v2
|
return v2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SRef dereferences the string pointer if it's not nil, or returns an empty string if it is nil.
|
||||||
|
func SRef(s *string) string {
|
||||||
|
if s == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return *s
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user