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.
This commit is contained in:
parent
0c7fc7e866
commit
3fdae1d8d7
|
@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -177,9 +177,11 @@
|
|||
<option value="Your certification pic should depict you holding onto a sheet of paper with your username, site name, and current date written on it.">
|
||||
Didn't follow directions
|
||||
</option>
|
||||
<option value="The sheet of paper must also include the website name: nonshy">Website name not visible</option>
|
||||
<option value="Please take a clearer picture that shows your arm and hand holding onto the sheet of paper">Unclear picture (hand not visible enough)</option>
|
||||
<option value="This photo has been digitally altered, please take a new certification picture and upload it as it comes off your camera">Photoshopped or digitally altered</option>
|
||||
<option value="The sheet of paper must also include the website name: nonshy.">Website name not visible</option>
|
||||
<option value="Please take a clearer picture that shows your arm and hand holding onto the sheet of paper.">Unclear picture (hand not visible enough)</option>
|
||||
<option value="This photo has been digitally altered, please take a new certification picture and upload it as it comes off your camera.">Photoshopped or digitally altered</option>
|
||||
<option value="Your certification photo should feature a hand written message on paper so we know you're a real person. It looks like you added text digitally to this picture, which isn't acceptable because anybody could have downloaded a picture like this from online and added text to it in that way.\n\nPlease take a picture with your certification message hand written on paper and upload it how it came off the camera.">Text appears added digitally</option>
|
||||
<option value="The sheet of paper that you are holding in this picture is not readable. Please take a clearer picture that shows you holding onto a sheet of paper which has the website's name (nonshy), your username, and today's date written and be sure that it is readable.">Cert message is not legible</option>
|
||||
<option value="You had a previous account on nonshy which was suspended and you are not welcome with a new account.">User was previously banned from nonshy</option>
|
||||
<option value="This is not an acceptable certification photo.">Not acceptable</option>
|
||||
</optgroup>
|
||||
|
@ -211,7 +213,8 @@
|
|||
{{end}}
|
||||
|
||||
{{if not (eq .Status "approved")}}
|
||||
<button type="submit" name="verdict" value="approve" class="card-footer-item button is-success">
|
||||
<button type="submit" name="verdict" value="approve" class="card-footer-item button is-success"
|
||||
{{if eq $Root.View "rejected"}}onclick="return confirm('Are you SURE you want to mark this photo as Approved?\n\nKeep in mind you are currently viewing the Rejected list of photos!')"{{end}}>
|
||||
<span class="icon"><i class="fa fa-check"></i></span>
|
||||
<span>Approve</span>
|
||||
</button>
|
||||
|
@ -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();
|
||||
});
|
||||
})
|
||||
|
|
|
@ -92,7 +92,7 @@
|
|||
{{.Demographic.Photo.PercentNonExplicit}}%
|
||||
</progress>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<div class="column is-narrow nonshy-percent-alignment">
|
||||
{{.Demographic.Photo.PercentNonExplicit}}%
|
||||
</div>
|
||||
</div>
|
||||
|
@ -109,7 +109,7 @@
|
|||
{{.Demographic.Photo.PercentExplicit}}%
|
||||
</progress>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<div class="column is-narrow nonshy-percent-alignment">
|
||||
{{.Demographic.Photo.PercentExplicit}}%
|
||||
</div>
|
||||
</div>
|
||||
|
@ -172,7 +172,7 @@
|
|||
{{.Percent}}%
|
||||
</progress>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<div class="column is-narrow nonshy-percent-alignment">
|
||||
{{.Percent}}%
|
||||
</div>
|
||||
</div>
|
||||
|
@ -196,7 +196,7 @@
|
|||
{{.Percent}}%
|
||||
</progress>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<div class="column is-narrow nonshy-percent-alignment">
|
||||
{{.Percent}}%
|
||||
</div>
|
||||
</div>
|
||||
|
@ -221,7 +221,7 @@
|
|||
{{.Percent}}%
|
||||
</progress>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<div class="column is-narrow nonshy-percent-alignment">
|
||||
{{.Percent}}%
|
||||
</div>
|
||||
</div>
|
||||
|
@ -230,4 +230,16 @@
|
|||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Mobile CSS tweaks for this page -->
|
||||
<style type="text/css">
|
||||
.nonshy-percent-alignment {
|
||||
min-width: 4.5rem;
|
||||
}
|
||||
@media screen and (min-width: 769px) {
|
||||
.nonshy-percent-alignment {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
|
|
Loading…
Reference in New Issue
Block a user