Photo Quotas & Postgres Fixes

* Add photo upload quotas.
* Non-certified users can upload few photos; certified users more
* Fix foreign key issues around deleting user profile photos for psql
This commit is contained in:
Noah 2022-08-21 15:40:24 -07:00
parent 96b33a920f
commit 36ba8c5c4d
12 changed files with 126 additions and 11 deletions

View File

@ -66,6 +66,10 @@ var (
const (
MaxPhotoWidth = 1280
ProfilePhotoWidth = 512
// Quotas for uploaded photos.
PhotoQuotaUncertified = 6
PhotoQuotaCertified = 24
)
// Variables set by main.go to make them readily available.

View File

@ -16,6 +16,7 @@ import (
func Login() http.HandlerFunc {
tmpl := templates.Must("account/login.html")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var next = r.FormValue("next")
// Posting?
if r.Method == http.MethodPost {
@ -73,11 +74,18 @@ func Login() http.HandlerFunc {
// Redirect to their dashboard.
session.Flash(w, r, "Login successful.")
if strings.HasPrefix(next, "/") {
templates.Redirect(w, next)
} else {
templates.Redirect(w, "/me")
}
return
}
if err := tmpl.Execute(w, r, nil); err != nil {
var vars = map[string]interface{}{
"Next": next,
}
if err := tmpl.Execute(w, r, vars); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

View File

@ -1,6 +1,7 @@
package photo
import (
"fmt"
"net/http"
"strconv"
@ -131,11 +132,15 @@ func Delete() http.HandlerFunc {
// Query params.
photoID, err := strconv.Atoi(r.FormValue("id"))
if err != nil {
log.Error("photo.Delete: failed to parse `id` param (%s) as int: %s", r.FormValue("id"), err)
session.FlashError(w, r, "Photo 'id' parameter required.")
templates.Redirect(w, "/")
return
}
// Page to redirect to in case of errors.
redirect := fmt.Sprintf("%s?id=%d", r.URL.Path, photoID)
// Find this photo by ID.
photo, err := models.GetPhoto(uint64(photoID))
if err != nil {
@ -162,10 +167,20 @@ func Delete() http.HandlerFunc {
confirm := r.PostFormValue("confirm") == "true"
if !confirm {
session.FlashError(w, r, "Confirm you want to delete this photo.")
templates.Redirect(w, r.URL.Path)
templates.Redirect(w, redirect)
return
}
// Was this our profile picture?
if currentUser.ProfilePhotoID != nil && *currentUser.ProfilePhotoID == photo.ID {
log.Debug("Delete Photo: was the user's profile photo, unset ProfilePhotoID")
if err := currentUser.RemoveProfilePhoto(); err != nil {
session.FlashError(w, r, "Error unsetting your current profile photo: %s", err)
templates.Redirect(w, redirect)
return
}
}
// Remove the images from disk.
for _, filename := range []string{
photo.Filename,
@ -180,7 +195,7 @@ func Delete() http.HandlerFunc {
if err := photo.Delete(); err != nil {
session.FlashError(w, r, "Couldn't delete photo: %s", err)
templates.Redirect(w, r.URL.Path)
templates.Redirect(w, redirect)
return
}

View File

@ -35,6 +35,11 @@ func Upload() http.HandlerFunc {
session.FlashError(w, r, "Unexpected error: couldn't get CurrentUser")
}
// Get the current user's quota.
var photoCount, photoQuota = photo.QuotaForUser(user)
vars["PhotoCount"] = photoCount
vars["PhotoQuota"] = photoQuota
// Are they POSTing?
if r.Method == http.MethodPost {
var (
@ -47,6 +52,13 @@ func Upload() http.HandlerFunc {
confirm2 = r.PostFormValue("confirm2") == "true"
)
// Are they at quota already?
if photoCount >= photoQuota {
session.FlashError(w, r, "You have too many photos to upload a new one. Please delete a photo to make room for a new one.")
templates.Redirect(w, "/photo/u/"+user.Username)
return
}
// They checked both boxes. The browser shouldn't allow them to
// post but validate it here anyway...
if !confirm1 || !confirm2 {

View File

@ -21,8 +21,8 @@ func LoginRequired(handler http.Handler) http.Handler {
user, err := session.CurrentUser(r)
if err != nil {
log.Error("LoginRequired: %s", err)
errhandler := templates.MakeErrorPage("Login Required", "You must be signed in to view this page.", http.StatusForbidden)
errhandler.ServeHTTP(w, r)
session.FlashError(w, r, "You must be signed in to view this page.")
templates.Redirect(w, "/login?next="+r.URL.RawPath)
return
}
@ -89,8 +89,8 @@ func CertRequired(handler http.Handler) http.Handler {
currentUser, err := session.CurrentUser(r)
if err != nil {
log.Error("LoginRequired: %s", err)
errhandler := templates.MakeErrorPage("Login Required", "You must be signed in to view this page.", http.StatusForbidden)
errhandler.ServeHTTP(w, r)
session.FlashError(w, r, "You must be signed in to view this page.")
templates.Redirect(w, "/login?next="+r.URL.Path)
return
}

View File

@ -96,6 +96,16 @@ func PaginateUserPhotos(userID uint64, visibility []PhotoVisibility, explicitOK
return p, result.Error
}
// CountPhotos returns the total number of photos on a user's account.
func CountPhotos(userID uint64) (int64, error) {
var count int64
result := DB.Where(
"user_id = ?",
userID,
).Model(&Photo{}).Count(&count)
return count, result.Error
}
// CountExplicitPhotos returns the number of explicit photos a user has (so non-explicit viewers can see some do exist)
func CountExplicitPhotos(userID uint64, visibility []PhotoVisibility) (int64, error) {
query := DB.Where(

View File

@ -32,7 +32,7 @@ type User struct {
// Relational tables.
ProfileField []ProfileField
ProfilePhotoID uint64
ProfilePhotoID *uint64
ProfilePhoto Photo `gorm:"foreignKey:profile_photo_id"`
}
@ -329,6 +329,12 @@ func (u *User) ProfileFieldIn(field, substr string) bool {
return strings.Contains(value, substr)
}
// RemoveProfilePhoto sets profile_photo_id=null to unset the foreign key.
func (u *User) RemoveProfilePhoto() error {
result := DB.Model(&User{}).Where("id = ?", u.ID).Update("profile_photo_id", nil)
return result.Error
}
// Save user.
func (u *User) Save() error {
result := DB.Save(u)

22
pkg/photo/quota.go Normal file
View File

@ -0,0 +1,22 @@
package photo
import (
"git.kirsle.net/apps/gosocial/pkg/config"
"git.kirsle.net/apps/gosocial/pkg/models"
)
// QuoteForUser returns the current photo usage quota for a given user.
func QuotaForUser(u *models.User) (current, allowed int) {
// Count their photos.
count, _ := models.CountPhotos(u.ID)
// What is their quota at?
var quota int
if !u.Certified {
quota = config.PhotoQuotaUncertified
} else {
quota = config.PhotoQuotaCertified
}
return int(count), quota
}

View File

@ -41,6 +41,17 @@ func TemplateFuncs(r *http.Request) template.FuncMap {
return labels[1]
}
},
"Pluralize": func(count int, labels ...string) string {
if len(labels) < 2 {
labels = []string{"", "s"}
}
if count == 1 {
return labels[0]
} else {
return labels[1]
}
},
"Substring": func(value string, n int) string {
if n > len(value) {
return value
@ -54,6 +65,9 @@ func TemplateFuncs(r *http.Request) template.FuncMap {
}
return result
},
"SubtractInt": func(a, b int) int {
return a - b
},
}
}

View File

@ -13,6 +13,7 @@
<div class="block p-4">
<form action="/login" method="POST">
{{ InputCSRF }}
<input type="hidden" name="next" value="{{.Next}}">
<div class="field">
<label class="label" for="username">Username or email:</label>

View File

@ -108,7 +108,7 @@
</figure>
</div>
<div class="media-content">
<p class="title is-4">{{.NameOrUsername}}</p>
<p class="title is-4">{{$User.NameOrUsername}}</p>
<p class="subtitle is-6">
<span class="icon"><i class="fa fa-user"></i></span>
<a href="/u/{{$User.Username}}" target="_blank">{{$User.Username}}</a>

View File

@ -57,6 +57,28 @@
</div>
{{end}}
<!-- Quota notification -->
{{if not .EditPhoto}}
<div class="notification {{if ge .PhotoCount .PhotoQuota}}is-warning{{else}}is-info{{end}} block">
<p class="block">
You have currently uploaded <strong>{{.PhotoCount}}</strong> of your allowed {{.PhotoQuota}} photos.
{{if ge .PhotoCount .PhotoQuota}}
To upload a new photo, please <a href="/photo/u/{{.CurrentUser.Username}}">delete</a>
an existing photo first to make room.
{{end}}
</p>
<p class="block">
You may upload <strong>{{SubtractInt .PhotoQuota .PhotoCount}}</strong> more photo{{Pluralize (SubtractInt .PhotoQuota .PhotoCount)}}.
{{if not .CurrentUser.Certified}}
After your account has been <a href="/photo/certification">certified</a>, you will be able to upload
additional pictures.
{{end}}
</p>
</div>
{{end}}
{{if or .EditPhoto (lt .PhotoCount .PhotoQuota)}}
<div class="columns">
<div class="column">
@ -299,7 +321,8 @@
</div>
</div>
</div>
</div><!-- /columns -->
{{end}}<!-- if under quota -->
</div>
</form>