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:
parent
96b33a920f
commit
36ba8c5c4d
|
@ -66,6 +66,10 @@ var (
|
||||||
const (
|
const (
|
||||||
MaxPhotoWidth = 1280
|
MaxPhotoWidth = 1280
|
||||||
ProfilePhotoWidth = 512
|
ProfilePhotoWidth = 512
|
||||||
|
|
||||||
|
// Quotas for uploaded photos.
|
||||||
|
PhotoQuotaUncertified = 6
|
||||||
|
PhotoQuotaCertified = 24
|
||||||
)
|
)
|
||||||
|
|
||||||
// Variables set by main.go to make them readily available.
|
// Variables set by main.go to make them readily available.
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
func Login() http.HandlerFunc {
|
func Login() http.HandlerFunc {
|
||||||
tmpl := templates.Must("account/login.html")
|
tmpl := templates.Must("account/login.html")
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var next = r.FormValue("next")
|
||||||
|
|
||||||
// Posting?
|
// Posting?
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
|
@ -73,11 +74,18 @@ func Login() http.HandlerFunc {
|
||||||
|
|
||||||
// Redirect to their dashboard.
|
// Redirect to their dashboard.
|
||||||
session.Flash(w, r, "Login successful.")
|
session.Flash(w, r, "Login successful.")
|
||||||
templates.Redirect(w, "/me")
|
if strings.HasPrefix(next, "/") {
|
||||||
|
templates.Redirect(w, next)
|
||||||
|
} else {
|
||||||
|
templates.Redirect(w, "/me")
|
||||||
|
}
|
||||||
return
|
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)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package photo
|
package photo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
@ -131,11 +132,15 @@ func Delete() http.HandlerFunc {
|
||||||
// Query params.
|
// Query params.
|
||||||
photoID, err := strconv.Atoi(r.FormValue("id"))
|
photoID, err := strconv.Atoi(r.FormValue("id"))
|
||||||
if err != nil {
|
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.")
|
session.FlashError(w, r, "Photo 'id' parameter required.")
|
||||||
templates.Redirect(w, "/")
|
templates.Redirect(w, "/")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Page to redirect to in case of errors.
|
||||||
|
redirect := fmt.Sprintf("%s?id=%d", r.URL.Path, photoID)
|
||||||
|
|
||||||
// Find this photo by ID.
|
// Find this photo by ID.
|
||||||
photo, err := models.GetPhoto(uint64(photoID))
|
photo, err := models.GetPhoto(uint64(photoID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -162,10 +167,20 @@ func Delete() http.HandlerFunc {
|
||||||
confirm := r.PostFormValue("confirm") == "true"
|
confirm := r.PostFormValue("confirm") == "true"
|
||||||
if !confirm {
|
if !confirm {
|
||||||
session.FlashError(w, r, "Confirm you want to delete this photo.")
|
session.FlashError(w, r, "Confirm you want to delete this photo.")
|
||||||
templates.Redirect(w, r.URL.Path)
|
templates.Redirect(w, redirect)
|
||||||
return
|
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.
|
// Remove the images from disk.
|
||||||
for _, filename := range []string{
|
for _, filename := range []string{
|
||||||
photo.Filename,
|
photo.Filename,
|
||||||
|
@ -180,7 +195,7 @@ func Delete() http.HandlerFunc {
|
||||||
|
|
||||||
if err := photo.Delete(); err != nil {
|
if err := photo.Delete(); err != nil {
|
||||||
session.FlashError(w, r, "Couldn't delete photo: %s", err)
|
session.FlashError(w, r, "Couldn't delete photo: %s", err)
|
||||||
templates.Redirect(w, r.URL.Path)
|
templates.Redirect(w, redirect)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,11 @@ func Upload() http.HandlerFunc {
|
||||||
session.FlashError(w, r, "Unexpected error: couldn't get CurrentUser")
|
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?
|
// Are they POSTing?
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
var (
|
var (
|
||||||
|
@ -47,6 +52,13 @@ func Upload() http.HandlerFunc {
|
||||||
confirm2 = r.PostFormValue("confirm2") == "true"
|
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
|
// They checked both boxes. The browser shouldn't allow them to
|
||||||
// post but validate it here anyway...
|
// post but validate it here anyway...
|
||||||
if !confirm1 || !confirm2 {
|
if !confirm1 || !confirm2 {
|
||||||
|
|
|
@ -21,8 +21,8 @@ func LoginRequired(handler http.Handler) http.Handler {
|
||||||
user, err := session.CurrentUser(r)
|
user, err := session.CurrentUser(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("LoginRequired: %s", err)
|
log.Error("LoginRequired: %s", err)
|
||||||
errhandler := templates.MakeErrorPage("Login Required", "You must be signed in to view this page.", http.StatusForbidden)
|
session.FlashError(w, r, "You must be signed in to view this page.")
|
||||||
errhandler.ServeHTTP(w, r)
|
templates.Redirect(w, "/login?next="+r.URL.RawPath)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,8 +89,8 @@ func CertRequired(handler http.Handler) http.Handler {
|
||||||
currentUser, err := session.CurrentUser(r)
|
currentUser, err := session.CurrentUser(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("LoginRequired: %s", err)
|
log.Error("LoginRequired: %s", err)
|
||||||
errhandler := templates.MakeErrorPage("Login Required", "You must be signed in to view this page.", http.StatusForbidden)
|
session.FlashError(w, r, "You must be signed in to view this page.")
|
||||||
errhandler.ServeHTTP(w, r)
|
templates.Redirect(w, "/login?next="+r.URL.Path)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -96,6 +96,16 @@ func PaginateUserPhotos(userID uint64, visibility []PhotoVisibility, explicitOK
|
||||||
return p, result.Error
|
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)
|
// 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) {
|
func CountExplicitPhotos(userID uint64, visibility []PhotoVisibility) (int64, error) {
|
||||||
query := DB.Where(
|
query := DB.Where(
|
||||||
|
|
|
@ -32,7 +32,7 @@ type User struct {
|
||||||
|
|
||||||
// Relational tables.
|
// Relational tables.
|
||||||
ProfileField []ProfileField
|
ProfileField []ProfileField
|
||||||
ProfilePhotoID uint64
|
ProfilePhotoID *uint64
|
||||||
ProfilePhoto Photo `gorm:"foreignKey:profile_photo_id"`
|
ProfilePhoto Photo `gorm:"foreignKey:profile_photo_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -329,6 +329,12 @@ func (u *User) ProfileFieldIn(field, substr string) bool {
|
||||||
return strings.Contains(value, substr)
|
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.
|
// Save user.
|
||||||
func (u *User) Save() error {
|
func (u *User) Save() error {
|
||||||
result := DB.Save(u)
|
result := DB.Save(u)
|
||||||
|
|
22
pkg/photo/quota.go
Normal file
22
pkg/photo/quota.go
Normal 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
|
||||||
|
}
|
|
@ -41,6 +41,17 @@ func TemplateFuncs(r *http.Request) template.FuncMap {
|
||||||
return labels[1]
|
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 {
|
"Substring": func(value string, n int) string {
|
||||||
if n > len(value) {
|
if n > len(value) {
|
||||||
return value
|
return value
|
||||||
|
@ -54,6 +65,9 @@ func TemplateFuncs(r *http.Request) template.FuncMap {
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
},
|
},
|
||||||
|
"SubtractInt": func(a, b int) int {
|
||||||
|
return a - b
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
<div class="block p-4">
|
<div class="block p-4">
|
||||||
<form action="/login" method="POST">
|
<form action="/login" method="POST">
|
||||||
{{ InputCSRF }}
|
{{ InputCSRF }}
|
||||||
|
<input type="hidden" name="next" value="{{.Next}}">
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="username">Username or email:</label>
|
<label class="label" for="username">Username or email:</label>
|
||||||
|
|
|
@ -108,7 +108,7 @@
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<p class="title is-4">{{.NameOrUsername}}</p>
|
<p class="title is-4">{{$User.NameOrUsername}}</p>
|
||||||
<p class="subtitle is-6">
|
<p class="subtitle is-6">
|
||||||
<span class="icon"><i class="fa fa-user"></i></span>
|
<span class="icon"><i class="fa fa-user"></i></span>
|
||||||
<a href="/u/{{$User.Username}}" target="_blank">{{$User.Username}}</a>
|
<a href="/u/{{$User.Username}}" target="_blank">{{$User.Username}}</a>
|
||||||
|
|
|
@ -57,6 +57,28 @@
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{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="columns">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
|
|
||||||
|
@ -299,7 +321,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div><!-- /columns -->
|
||||||
|
{{end}}<!-- if under quota -->
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user