From dd59bcf577612695e34839f98d0a1be93ef13465 Mon Sep 17 00:00:00 2001 From: Amy Gale Ruth Bowersox Date: Fri, 13 Feb 2026 15:47:25 -0700 Subject: [PATCH] landed all of confererence activity reports functionality --- conferenceadmin.go | 54 ++++++++++++++++++- database/conference.go | 52 ++++++++++++++++++ database/topic.go | 38 ++++++++++++++ docs/MISSINGFUNCS.md | 2 +- errors.go | 3 ++ ui/templates.go | 14 ++++- ui/views/conf_reportout.jet | 102 ++++++++++++++++++++++++++++++++++++ 7 files changed, 261 insertions(+), 4 deletions(-) create mode 100644 ui/views/conf_reportout.jet diff --git a/conferenceadmin.go b/conferenceadmin.go index 9a8c9e0..2b3d1e8 100644 --- a/conferenceadmin.go +++ b/conferenceadmin.go @@ -446,13 +446,63 @@ func ConfCustom(ctxt ui.AmContext) (string, any) { func ConfReports(ctxt ui.AmContext) (string, any) { comm := ctxt.CurrentCommunity() conf := ctxt.GetScratch("currentConference").(*database.Conference) + myLevel := ctxt.GetScratch("levelInConference").(uint16) + if !conf.TestPermission("Conference.Read", myLevel) { + return "error", ENOPERM + } + ctxt.VarMap().Set("confName", conf.Name) ctxt.VarMap().Set("selfLink", fmt.Sprintf("/comm/%s/conf/%s/activity", comm.Alias, ctxt.GetScratch("currentAlias"))) if ctxt.HasParameter("r") { - // TODO: generate report here - return "error", nil + // generate a report + reportMode := ctxt.Parameter("r") + var reportTypeSel int + switch reportMode { + case "post": + reportTypeSel = database.ActivityReportPosters + case "read": + reportTypeSel = database.ActivityReportReaders + default: + return "error", EINVAL + } + ctxt.VarMap().Set("reportMode", reportMode) + if ctxt.HasParameter("t") { + topicId := ctxt.QueryParamInt("t", -1) + if topicId > 0 { + topic, err := database.AmGetTopic(ctxt.Ctx(), int32(topicId)) + if err != nil { + return "error", err + } + ctxt.VarMap().Set("topic", topic) + report, err := topic.GetActivity(ctxt.Ctx(), reportTypeSel) + if err != nil { + return "error", err + } + ctxt.VarMap().Set("report", report) + if reportTypeSel == database.ActivityReportPosters { + ctxt.SetFrameTitle("Users Posting in Topic " + topic.Name) + } else { + ctxt.SetFrameTitle("Users Reading Topic " + topic.Name) + } + } else { + return "error", "Invalid topic ID specified" + } + } else { + report, err := conf.GetActivity(ctxt.Ctx(), reportTypeSel) + if err != nil { + return "error", err + } + ctxt.VarMap().Set("report", report) + if reportTypeSel == database.ActivityReportPosters { + ctxt.SetFrameTitle("Users Posting in Conference " + conf.Name) + } else { + ctxt.SetFrameTitle("Users Reading Conference " + conf.Name) + } + } + return "framed", "conf_reportout.jet" } else { + // generate the listing topicList, err := database.AmListTopics(ctxt.Ctx(), conf.ConfId, ctxt.CurrentUserId(), database.TopicViewAll, database.TopicSortNumber, true) if err != nil { return "error", err diff --git a/database/conference.go b/database/conference.go index d6a1250..ef1065f 100644 --- a/database/conference.go +++ b/database/conference.go @@ -674,6 +674,58 @@ func (c *Conference) RemoveCustomBlocks(ctx context.Context) error { return err } +// ActivityReport is used to get activity reports from the conference or topic. +type ActivityReport struct { + Uid int32 + Username string + LastRead *time.Time + LastPost *time.Time +} + +// Activity report types. +const ( + ActivityReportPosters = 0 // report on all posters + ActivityReportReaders = 1 // report on all readers +) + +/* GetActivity returns a list of ActivityReport objects detailing the conference activity. + * Parameters: + * ctx - Standard Go context value. + * reportType - Determines which report to generate: + * ActivityReportPosters - Report on all posters in the conference. + * ActivityReportReaders - Report on all readers in the conference. + * Returns: + * List of ActivityReport objects detailing the conference activity. + * Standard Go error status. + */ +func (c *Conference) GetActivity(ctx context.Context, reportType int) ([]ActivityReport, error) { + var myfield string + switch reportType { + case ActivityReportPosters: + myfield = "s.last_post" + case ActivityReportReaders: + myfield = "s.last_read" + default: + return nil, errors.New("invalid report type parameter") + } + sql := fmt.Sprintf(`SELECT s.uid, u.username, s.last_read, s.last_post FROM confsettings s, users u WHERE u.uid = s.uid + AND s.confid = ? AND u.is_anon = 0 AND ISNULL(%s) = 0 ORDER BY %s DESC`, myfield, myfield) + rs, err := amdb.QueryContext(ctx, sql, c.ConfId) + if err != nil { + return nil, err + } + rc := make([]ActivityReport, 0) + for rs.Next() { + var cur ActivityReport + err = rs.Scan(&(cur.Uid), &(cur.Username), &(cur.LastRead), &(cur.LastPost)) + if err != nil { + return nil, err + } + rc = append(rc, cur) + } + return rc, nil +} + /* AmGetConference returns a conference given its ID. * Parameters: * ctx - Standard Go context value. diff --git a/database/topic.go b/database/topic.go index d1ced63..5bf24dc 100644 --- a/database/topic.go +++ b/database/topic.go @@ -290,6 +290,44 @@ func (t *Topic) GetSubscribers(ctx context.Context) ([]int32, error) { return rc, err } +/* GetActivity returns a list of ActivityReport objects detailing the topic activity. + * Parameters: + * ctx - Standard Go context value. + * reportType - Determines which report to generate: + * ActivityReportPosters - Report on all posters in the topic. + * ActivityReportReaders - Report on all readers in the topic. + * Returns: + * List of ActivityReport objects detailing the topic activity. + * Standard Go error status. + */ +func (t *Topic) GetActivity(ctx context.Context, reportType int) ([]ActivityReport, error) { + var myfield string + switch reportType { + case ActivityReportPosters: + myfield = "s.last_post" + case ActivityReportReaders: + myfield = "s.last_read" + default: + return nil, errors.New("invalid report type parameter") + } + sql := fmt.Sprintf(`SELECT s.uid, u.username, s.last_read, s.last_post FROM topicsettings s, users u WHERE u.uid = s.uid + AND s.topicid = ? AND u.is_anon = 0 AND ISNULL(%s) = 0 ORDER BY %s DESC`, myfield, myfield) + rs, err := amdb.QueryContext(ctx, sql, t.TopicId) + if err != nil { + return nil, err + } + rc := make([]ActivityReport, 0) + for rs.Next() { + var cur ActivityReport + err = rs.Scan(&(cur.Uid), &(cur.Username), &(cur.LastRead), &(cur.LastPost)) + if err != nil { + return nil, err + } + rc = append(rc, cur) + } + return rc, nil +} + // backgroundPurgeTopic removes all posts from a topic that's been deleted. func backgroundPurgeTopic(ctx context.Context, topicid int32) error { success := false diff --git a/docs/MISSINGFUNCS.md b/docs/MISSINGFUNCS.md index c4a1964..fc4675c 100644 --- a/docs/MISSINGFUNCS.md +++ b/docs/MISSINGFUNCS.md @@ -68,7 +68,7 @@ _(italicized items can be deferred)_ - ~~Add alias~~ - ~~Manage members~~ - ~~Custom appearance~~ - - Activity reports + - ~~Activity reports~~ - E-mail - Export Messages - Import Messages diff --git a/errors.go b/errors.go index c9ffd94..54003e7 100644 --- a/errors.go +++ b/errors.go @@ -21,6 +21,9 @@ import ( // EBUTTON is the standard error for an unknown button. var EBUTTON error = errors.New("invalid or unknown button pressed") +// EINVAL is the standard error for an invalid parameter. +var EINVAL error = errors.New("invalid parameter to operation") + // ELOGIN is the standard error for not being logged in var ELOGIN error = errors.New("you are not logged in") diff --git a/ui/templates.go b/ui/templates.go index 108cb76..cfbc784 100644 --- a/ui/templates.go +++ b/ui/templates.go @@ -146,7 +146,19 @@ func userContactInfo(a jet.Arguments) reflect.Value { // displayDateTime formats a date and time value. func displayDateTime(a jet.Arguments) reflect.Value { - timeval := a.Get(0).Convert(reflect.TypeFor[time.Time]()).Interface().(time.Time) + var timeval time.Time + p0 := a.Get(0) + if p0.CanConvert(reflect.TypeFor[time.Time]()) { + timeval = p0.Convert(reflect.TypeFor[time.Time]()).Interface().(time.Time) + } else if p0.CanConvert(reflect.TypeFor[*time.Time]()) { + ptr := p0.Convert(reflect.TypeFor[*time.Time]()).Interface().(*time.Time) + if ptr == nil { + return reflect.ValueOf("<>") + } + timeval = *ptr + } else { + return reflect.ValueOf("<>") + } ctxt := a.Get(1).Convert(reflect.TypeFor[AmContext]()).Interface().(AmContext) prefs, err := ctxt.CurrentUser().Prefs(ctxt.Ctx()) if err == nil { diff --git a/ui/views/conf_reportout.jet b/ui/views/conf_reportout.jet new file mode 100644 index 0000000..523a768 --- /dev/null +++ b/ui/views/conf_reportout.jet @@ -0,0 +1,102 @@ +{* + * Amsterdam Web Communities System + * Copyright (c) 2025-2026 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/. + *} +
+ +
+
+ {{ if isset(topic) }} + {{ if reportMode == "post" }} +

Posters in Topic:

+ {{ else }} +

Readers in Topic:

+ {{ end }} +

{{ topic.Name | raw }}

+ {{ else }} + {{ if reportMode == "post" }} +

Posters in Conference:

+ {{ else }} +

Readers in Conference:

+ {{ end }} +

{{ confName }}

+ {{ end }} +
+
+
+ + + + + +
+
+ + + + + {{ if reportMode == "post" }} + + + {{ else }} + + + {{ end }} + + + + {{ if len(report) == 0 }} + + + + {{ else }} + {{ range _, r := report }} + + + {{ if reportMode == "post" }} + + + {{ else }} + + {{ if isset(r.LastPost) }} + + {{ else }} + + {{ end }} + {{ end }} + + {{ end }} + {{ end }} + +
User NameLast PostedLast ReadLast ReadLast Posted
+ {{ if isset(topic) }} + {{ if reportMode == "post" }} + No posters found in topic "{{ topic.Name | raw }}." + {{ else }} + No readers found in topic "{{ topic.Name | raw }}." + {{ end }} + {{ else }} + {{ if reportMode == "post" }} + No posters found in conference "{{ confName }}." + {{ else }} + No readers found in conference "{{ confName }}." + {{ end }} + {{ end }} +
+ {{ r.Username }} + {{ DisplayDateTime(r.LastPost, .) }}{{ DisplayDateTime(r.LastRead, .) }}{{ DisplayDateTime(r.LastRead, .) }}{{ DisplayDateTime(r.LastPost, .) }}Never
+
+
+ + + +