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"
|
"time"
|
||||||
|
|
||||||
"code.nonshy.com/nonshy/website/pkg/config"
|
"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.
|
// 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 {
|
type MemberDemographic struct {
|
||||||
Label string // e.g. age range "18-25" or gender
|
Label string // e.g. age range "18-25" or gender
|
||||||
Count int64
|
Count int64
|
||||||
Percent int
|
Percent string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -58,32 +59,32 @@ func (d Demographic) PrettyPrint() string {
|
||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Photo) PercentExplicit() int {
|
func (p Photo) PercentExplicit() string {
|
||||||
if p.Total == 0 {
|
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 {
|
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 {
|
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 {
|
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 {
|
func (p People) IterAgeRanges() []MemberDemographic {
|
||||||
|
@ -104,16 +105,16 @@ func (p People) IterAgeRanges() []MemberDemographic {
|
||||||
for _, age := range values {
|
for _, age := range values {
|
||||||
var (
|
var (
|
||||||
count = p.ByAgeRange[age]
|
count = p.ByAgeRange[age]
|
||||||
pct int
|
pct float64
|
||||||
)
|
)
|
||||||
if p.Total > 0 {
|
if p.Total > 0 {
|
||||||
pct = int((float64(count) / float64(p.Total)) * 100)
|
pct = ((float64(count) / float64(p.Total)) * 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
result = append(result, MemberDemographic{
|
result = append(result, MemberDemographic{
|
||||||
Label: age,
|
Label: age,
|
||||||
Count: p.ByAgeRange[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 {
|
for _, gender := range values {
|
||||||
var (
|
var (
|
||||||
count = p.ByGender[gender]
|
count = p.ByGender[gender]
|
||||||
pct int
|
pct float64
|
||||||
)
|
)
|
||||||
if p.Total > 0 {
|
if p.Total > 0 {
|
||||||
pct = int((float64(count) / float64(p.Total)) * 100)
|
pct = ((float64(count) / float64(p.Total)) * 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
result = append(result, MemberDemographic{
|
result = append(result, MemberDemographic{
|
||||||
Label: gender,
|
Label: gender,
|
||||||
Count: p.ByGender[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 {
|
for _, gender := range values {
|
||||||
var (
|
var (
|
||||||
count = p.ByOrientation[gender]
|
count = p.ByOrientation[gender]
|
||||||
pct int
|
pct float64
|
||||||
)
|
)
|
||||||
if p.Total > 0 {
|
if p.Total > 0 {
|
||||||
pct = int((float64(count) / float64(p.Total)) * 100)
|
pct = ((float64(count) / float64(p.Total)) * 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
result = append(result, MemberDemographic{
|
result = append(result, MemberDemographic{
|
||||||
Label: gender,
|
Label: gender,
|
||||||
Count: p.ByOrientation[gender],
|
Count: p.ByOrientation[gender],
|
||||||
Percent: pct,
|
Percent: utility.FormatFloatToPrecision(pct, 1),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,8 +70,8 @@ func Generate() (Demographic, error) {
|
||||||
func PeopleStatistics() People {
|
func PeopleStatistics() People {
|
||||||
var result = People{
|
var result = People{
|
||||||
ByAgeRange: map[string]int64{},
|
ByAgeRange: map[string]int64{},
|
||||||
ByGender: map[string]int64{},
|
ByGender: map[string]int64{"": 0},
|
||||||
ByOrientation: map[string]int64{},
|
ByOrientation: map[string]int64{"": 0},
|
||||||
}
|
}
|
||||||
|
|
||||||
type record struct {
|
type record struct {
|
||||||
|
@ -216,6 +216,11 @@ func PeopleStatistics() People {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ingest the records.
|
// 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 {
|
for _, row := range records {
|
||||||
switch row.MetricType {
|
switch row.MetricType {
|
||||||
case "ExplicitCount":
|
case "ExplicitCount":
|
||||||
|
@ -230,19 +235,27 @@ func PeopleStatistics() People {
|
||||||
result.ByAgeRange[row.MetricValue] = 0
|
result.ByAgeRange[row.MetricValue] = 0
|
||||||
}
|
}
|
||||||
result.ByAgeRange[row.MetricValue] += row.MetricCount
|
result.ByAgeRange[row.MetricValue] += row.MetricCount
|
||||||
|
totalWithAge += row.MetricCount
|
||||||
case "GenderCount":
|
case "GenderCount":
|
||||||
if _, ok := result.ByGender[row.MetricValue]; !ok {
|
if _, ok := result.ByGender[row.MetricValue]; !ok {
|
||||||
result.ByGender[row.MetricValue] = 0
|
result.ByGender[row.MetricValue] = 0
|
||||||
}
|
}
|
||||||
result.ByGender[row.MetricValue] += row.MetricCount
|
result.ByGender[row.MetricValue] += row.MetricCount
|
||||||
|
totalWithGender += row.MetricCount
|
||||||
case "OrientationCount":
|
case "OrientationCount":
|
||||||
if _, ok := result.ByOrientation[row.MetricValue]; !ok {
|
if _, ok := result.ByOrientation[row.MetricValue]; !ok {
|
||||||
result.ByOrientation[row.MetricValue] = 0
|
result.ByOrientation[row.MetricValue] = 0
|
||||||
}
|
}
|
||||||
result.ByOrientation[row.MetricValue] += row.MetricCount
|
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
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,8 +276,10 @@ func PhotoStatistics() Photo {
|
||||||
count(photos.id) AS c
|
count(photos.id) AS c
|
||||||
FROM
|
FROM
|
||||||
photos
|
photos
|
||||||
WHERE
|
JOIN users ON (photos.user_id = users.id)
|
||||||
photos.visibility = 'public'
|
WHERE photos.visibility = 'public'
|
||||||
|
AND users.certified IS TRUE
|
||||||
|
AND users.status = 'active'
|
||||||
GROUP BY photos.explicit
|
GROUP BY photos.explicit
|
||||||
ORDER BY c DESC
|
ORDER BY c DESC
|
||||||
`).Scan(&records)
|
`).Scan(&records)
|
||||||
|
|
|
@ -6,6 +6,17 @@ import (
|
||||||
"strings"
|
"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).
|
// 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 {
|
func FormatNumberShort(value int64) string {
|
||||||
// Under 1,000?
|
// Under 1,000?
|
||||||
|
@ -20,21 +31,13 @@ func FormatNumberShort(value int64) string {
|
||||||
billions = float64(millions) / 1000
|
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 {
|
if thousands < 1000 {
|
||||||
return fmt.Sprintf("%sK", formatFloat(thousands))
|
return fmt.Sprintf("%sK", FormatFloatToPrecision(thousands, 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
if millions < 1000 {
|
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.">
|
<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
|
Didn't follow directions
|
||||||
</option>
|
</option>
|
||||||
<option value="The sheet of paper must also include the website name: nonshy">Website name not visible</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="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="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="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>
|
<option value="This is not an acceptable certification photo.">Not acceptable</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
|
@ -211,7 +213,8 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if not (eq .Status "approved")}}
|
{{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 class="icon"><i class="fa fa-check"></i></span>
|
||||||
<span>Approve</span>
|
<span>Approve</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -245,7 +248,7 @@
|
||||||
textarea.addEventListener("change", setApproveState);
|
textarea.addEventListener("change", setApproveState);
|
||||||
textarea.addEventListener("keyup", setApproveState);
|
textarea.addEventListener("keyup", setApproveState);
|
||||||
elem.addEventListener("change", (e) => {
|
elem.addEventListener("change", (e) => {
|
||||||
textarea.value = elem.value;
|
textarea.value = elem.value.replaceAll("\\n", "\n");
|
||||||
setApproveState();
|
setApproveState();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
|
@ -92,7 +92,7 @@
|
||||||
{{.Demographic.Photo.PercentNonExplicit}}%
|
{{.Demographic.Photo.PercentNonExplicit}}%
|
||||||
</progress>
|
</progress>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow nonshy-percent-alignment">
|
||||||
{{.Demographic.Photo.PercentNonExplicit}}%
|
{{.Demographic.Photo.PercentNonExplicit}}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -109,7 +109,7 @@
|
||||||
{{.Demographic.Photo.PercentExplicit}}%
|
{{.Demographic.Photo.PercentExplicit}}%
|
||||||
</progress>
|
</progress>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow nonshy-percent-alignment">
|
||||||
{{.Demographic.Photo.PercentExplicit}}%
|
{{.Demographic.Photo.PercentExplicit}}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -172,7 +172,7 @@
|
||||||
{{.Percent}}%
|
{{.Percent}}%
|
||||||
</progress>
|
</progress>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow nonshy-percent-alignment">
|
||||||
{{.Percent}}%
|
{{.Percent}}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -196,7 +196,7 @@
|
||||||
{{.Percent}}%
|
{{.Percent}}%
|
||||||
</progress>
|
</progress>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow nonshy-percent-alignment">
|
||||||
{{.Percent}}%
|
{{.Percent}}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -221,7 +221,7 @@
|
||||||
{{.Percent}}%
|
{{.Percent}}%
|
||||||
</progress>
|
</progress>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow nonshy-percent-alignment">
|
||||||
{{.Percent}}%
|
{{.Percent}}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -230,4 +230,16 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</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}}
|
{{end}}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user