From 3fdae1d8d7b61b2bcaafa8cf724f55912df39113 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Fri, 27 Sep 2024 17:37:45 -0700 Subject: [PATCH] Various minor tweaks * Demographics page: * Show percents with up to 1 decimal place of precision. * On tablets+ align the percent text to the right. * On photo counts, only include certified active user photos. * On gender/orientation demographics, pad the remaining "No answer" counts with the set of users who have no profile_fields set in the database yet. * Admin certification page: * Add additional "common rejection reasons" * Add a confirm prompt when viewing the Rejected list to avoid accidental approval of previously rejected cert photos. --- pkg/models/demographic/demographic.go | 45 +++++++++++++------------- pkg/models/demographic/queries.go | 23 ++++++++++--- pkg/utility/number_format.go | 25 +++++++------- web/templates/admin/certification.html | 13 +++++--- web/templates/demographics.html | 22 ++++++++++--- 5 files changed, 81 insertions(+), 47 deletions(-) diff --git a/pkg/models/demographic/demographic.go b/pkg/models/demographic/demographic.go index fea99b4..97841cb 100644 --- a/pkg/models/demographic/demographic.go +++ b/pkg/models/demographic/demographic.go @@ -12,6 +12,7 @@ import ( "time" "code.nonshy.com/nonshy/website/pkg/config" + "code.nonshy.com/nonshy/website/pkg/utility" ) // Demographic is the top level container struct with all the insights needed for front-end display. @@ -43,7 +44,7 @@ type People struct { type MemberDemographic struct { Label string // e.g. age range "18-25" or gender Count int64 - Percent int + Percent string } /** @@ -58,32 +59,32 @@ func (d Demographic) PrettyPrint() string { return string(b) } -func (p Photo) PercentExplicit() int { +func (p Photo) PercentExplicit() string { if p.Total == 0 { - return 0 + return "0" } - return int((float64(p.Explicit) / float64(p.Total)) * 100) + return utility.FormatFloatToPrecision((float64(p.Explicit)/float64(p.Total))*100, 1) } -func (p Photo) PercentNonExplicit() int { +func (p Photo) PercentNonExplicit() string { if p.Total == 0 { - return 0 + return "0" } - return int((float64(p.NonExplicit) / float64(p.Total)) * 100) + return utility.FormatFloatToPrecision((float64(p.NonExplicit)/float64(p.Total))*100, 1) } -func (p People) PercentExplicit() int { +func (p People) PercentExplicit() string { if p.Total == 0 { - return 0 + return "0" } - return int((float64(p.ExplicitOptIn) / float64(p.Total)) * 100) + return utility.FormatFloatToPrecision((float64(p.ExplicitOptIn)/float64(p.Total))*100, 1) } -func (p People) PercentExplicitPhoto() int { +func (p People) PercentExplicitPhoto() string { if p.Total == 0 { - return 0 + return "0" } - return int((float64(p.ExplicitPhoto) / float64(p.Total)) * 100) + return utility.FormatFloatToPrecision((float64(p.ExplicitPhoto)/float64(p.Total))*100, 1) } func (p People) IterAgeRanges() []MemberDemographic { @@ -104,16 +105,16 @@ func (p People) IterAgeRanges() []MemberDemographic { for _, age := range values { var ( count = p.ByAgeRange[age] - pct int + pct float64 ) if p.Total > 0 { - pct = int((float64(count) / float64(p.Total)) * 100) + pct = ((float64(count) / float64(p.Total)) * 100) } result = append(result, MemberDemographic{ Label: age, Count: p.ByAgeRange[age], - Percent: pct, + Percent: utility.FormatFloatToPrecision(pct, 1), }) } @@ -141,16 +142,16 @@ func (p People) IterGenders() []MemberDemographic { for _, gender := range values { var ( count = p.ByGender[gender] - pct int + pct float64 ) if p.Total > 0 { - pct = int((float64(count) / float64(p.Total)) * 100) + pct = ((float64(count) / float64(p.Total)) * 100) } result = append(result, MemberDemographic{ Label: gender, Count: p.ByGender[gender], - Percent: pct, + Percent: utility.FormatFloatToPrecision(pct, 1), }) } @@ -178,16 +179,16 @@ func (p People) IterOrientations() []MemberDemographic { for _, gender := range values { var ( count = p.ByOrientation[gender] - pct int + pct float64 ) if p.Total > 0 { - pct = int((float64(count) / float64(p.Total)) * 100) + pct = ((float64(count) / float64(p.Total)) * 100) } result = append(result, MemberDemographic{ Label: gender, Count: p.ByOrientation[gender], - Percent: pct, + Percent: utility.FormatFloatToPrecision(pct, 1), }) } diff --git a/pkg/models/demographic/queries.go b/pkg/models/demographic/queries.go index 8767417..f2d85ef 100644 --- a/pkg/models/demographic/queries.go +++ b/pkg/models/demographic/queries.go @@ -70,8 +70,8 @@ func Generate() (Demographic, error) { func PeopleStatistics() People { var result = People{ ByAgeRange: map[string]int64{}, - ByGender: map[string]int64{}, - ByOrientation: map[string]int64{}, + ByGender: map[string]int64{"": 0}, + ByOrientation: map[string]int64{"": 0}, } type record struct { @@ -216,6 +216,11 @@ func PeopleStatistics() People { } // Ingest the records. + var ( + totalWithAge int64 // will be the total count of users since age is required + totalWithGender int64 + totalWithOrientation int64 + ) for _, row := range records { switch row.MetricType { case "ExplicitCount": @@ -230,19 +235,27 @@ func PeopleStatistics() People { result.ByAgeRange[row.MetricValue] = 0 } result.ByAgeRange[row.MetricValue] += row.MetricCount + totalWithAge += row.MetricCount case "GenderCount": if _, ok := result.ByGender[row.MetricValue]; !ok { result.ByGender[row.MetricValue] = 0 } result.ByGender[row.MetricValue] += row.MetricCount + totalWithGender += row.MetricCount case "OrientationCount": if _, ok := result.ByOrientation[row.MetricValue]; !ok { result.ByOrientation[row.MetricValue] = 0 } result.ByOrientation[row.MetricValue] += row.MetricCount + totalWithOrientation += row.MetricCount } } + // Gender and Orientation: pad out the "no answer" selection with the count of users + // who had no profile_fields stored in the DB at all. + result.ByOrientation[""] += (totalWithAge - totalWithOrientation) + result.ByGender[""] += (totalWithAge - totalWithGender) + return result } @@ -263,8 +276,10 @@ func PhotoStatistics() Photo { count(photos.id) AS c FROM photos - WHERE - photos.visibility = 'public' + JOIN users ON (photos.user_id = users.id) + WHERE photos.visibility = 'public' + AND users.certified IS TRUE + AND users.status = 'active' GROUP BY photos.explicit ORDER BY c DESC `).Scan(&records) diff --git a/pkg/utility/number_format.go b/pkg/utility/number_format.go index d6b8ef2..4e761eb 100644 --- a/pkg/utility/number_format.go +++ b/pkg/utility/number_format.go @@ -6,6 +6,17 @@ import ( "strings" ) +// FormatFloatToPrecision will trim a floating point number to at most a number of decimals of precision. +// +// If the precision is ".0" the decimal place will be stripped entirely. +func FormatFloatToPrecision(v float64, prec int) string { + s := strconv.FormatFloat(v, 'f', prec, 64) + if strings.HasSuffix(s, ".0") { + return strings.Split(s, ".")[0] + } + return s +} + // FormatNumberShort compresses a number into as short a string as possible (e.g. "1.2K" when it gets into the thousands). func FormatNumberShort(value int64) string { // Under 1,000? @@ -20,21 +31,13 @@ func FormatNumberShort(value int64) string { billions = float64(millions) / 1000 ) - formatFloat := func(v float64) string { - s := strconv.FormatFloat(v, 'f', 1, 64) - if strings.HasSuffix(s, ".0") { - return strings.Split(s, ".")[0] - } - return s - } - if thousands < 1000 { - return fmt.Sprintf("%sK", formatFloat(thousands)) + return fmt.Sprintf("%sK", FormatFloatToPrecision(thousands, 1)) } if millions < 1000 { - return fmt.Sprintf("%sM", formatFloat(millions)) + return fmt.Sprintf("%sM", FormatFloatToPrecision(millions, 1)) } - return fmt.Sprintf("%sB", formatFloat(billions)) + return fmt.Sprintf("%sB", FormatFloatToPrecision(billions, 1)) } diff --git a/web/templates/admin/certification.html b/web/templates/admin/certification.html index 3909af1..812dbe9 100644 --- a/web/templates/admin/certification.html +++ b/web/templates/admin/certification.html @@ -177,9 +177,11 @@ - - - + + + + + @@ -211,7 +213,8 @@ {{end}} {{if not (eq .Status "approved")}} - @@ -245,7 +248,7 @@ textarea.addEventListener("change", setApproveState); textarea.addEventListener("keyup", setApproveState); elem.addEventListener("change", (e) => { - textarea.value = elem.value; + textarea.value = elem.value.replaceAll("\\n", "\n"); setApproveState(); }); }) diff --git a/web/templates/demographics.html b/web/templates/demographics.html index c37761d..be51138 100644 --- a/web/templates/demographics.html +++ b/web/templates/demographics.html @@ -92,7 +92,7 @@ {{.Demographic.Photo.PercentNonExplicit}}% -
+
{{.Demographic.Photo.PercentNonExplicit}}%
@@ -109,7 +109,7 @@ {{.Demographic.Photo.PercentExplicit}}% -
+
{{.Demographic.Photo.PercentExplicit}}%
@@ -172,7 +172,7 @@ {{.Percent}}% -
+
{{.Percent}}%
@@ -196,7 +196,7 @@ {{.Percent}}% -
+
{{.Percent}}%
@@ -221,7 +221,7 @@ {{.Percent}}% -
+
{{.Percent}}%
@@ -230,4 +230,16 @@ + + + {{end}}