From d31111cf0133b223a8e665e6798b8ae09aa5c8a9 Mon Sep 17 00:00:00 2001 From: Bobby Date: Sat, 19 Jul 2025 15:22:22 +0530 Subject: post metadata update via edit --- controllers/posts.go | 151 ++++++++++++++++++++++++++++-- controllers/register.go | 8 +- database/images.go | 4 + database/posts.go | 2 +- database/user.go | 17 +++- models/user.go | 5 +- router/routes.go | 5 +- static/css/main.css | 92 ++++++++++++++++++- templates/posts/edit.django | 207 +++++++++++++++++++++++++++++++++++++++++- templates/posts/single.django | 19 +++- 10 files changed, 488 insertions(+), 22 deletions(-) diff --git a/controllers/posts.go b/controllers/posts.go index 038ca1f..57738b5 100644 --- a/controllers/posts.go +++ b/controllers/posts.go @@ -4,6 +4,7 @@ import ( "errors" "imageboard/config" "imageboard/database" + "imageboard/models" "imageboard/utils/auth" "imageboard/utils/format" "imageboard/utils/handlers" @@ -307,7 +308,7 @@ func PostsSinglePostPageController(ctx *fiber.Ctx) error { }) } -func PostsSinglePostFavouriteController(ctx *fiber.Ctx) error { +func PostsSinglePostFavouritePostController(ctx *fiber.Ctx) error { if !auth.IsAuthenticated(ctx) { return ctx.Redirect(auth.GetLoginURLWithNextField(ctx), fiber.StatusFound) } @@ -339,10 +340,10 @@ func PostsSinglePostFavouriteController(ctx *fiber.Ctx) error { return InternalServerErrorController(ctx, err) } - return ctx.Redirect("/posts/" + postID) + return ctx.Redirect(auth.GetRedirectURL(ctx), fiber.StatusSeeOther) } -func PostsSinglePostEditController(ctx *fiber.Ctx) error { +func PostsSinglePostEditPageController(ctx *fiber.Ctx) error { if !auth.IsAuthenticated(ctx) { return ctx.Redirect(auth.GetLoginURLWithNextField(ctx), fiber.StatusFound) } @@ -365,13 +366,151 @@ func PostsSinglePostEditController(ctx *fiber.Ctx) error { return InternalServerErrorController(ctx, err) } - if post.Uploader.Username != auth.GetCurrentUser(ctx).Username || !auth.GetCurrentUser(ctx).CanEditPosts() { + currentUser := auth.GetCurrentUser(ctx) + if post.Uploader.Username != currentUser.Username && !currentUser.CanEditPosts() { return ForbiddenController(ctx, errors.New("You do not have permission to edit this post")) } + users, err := database.ListAllUsers() + if err != nil { + return InternalServerErrorController(ctx, err) + } + approvers, err := database.ListAllApprovers() + if err != nil { + return InternalServerErrorController(ctx, err) + } + + postTags := make([]map[string]models.Tag, 0, len(post.Tags)) + for _, tag := range post.Tags { + switch tag.Type { + case config.TagTypeGeneral: + postTags = append(postTags, map[string]models.Tag{"general": tag}) + case config.TagTypeArtist: + postTags = append(postTags, map[string]models.Tag{"artist": tag}) + case config.TagTypeCharacter: + postTags = append(postTags, map[string]models.Tag{"character": tag}) + case config.TagTypeCopyright: + postTags = append(postTags, map[string]models.Tag{"copyright": tag}) + case config.TagTypeMeta: + postTags = append(postTags, map[string]models.Tag{"meta": tag}) + default: + postTags = append(postTags, map[string]models.Tag{"general": tag}) + } + } + ctx.Locals("Title", config.PT_POST_EDIT+" #"+format.Int64ToString(int64(post.ID))) return shortcuts.Render(ctx, config.TEMPLATE_POST_EDIT, fiber.Map{ - "Post": post, - "CDNURL": format.GetCDNURL(), + "Post": post, + "CDNURL": format.GetCDNURL(), + "Users": users, + "Approvers": approvers, + "PostTags": postTags, }) } + +func PostsSinglePostEditPostController(ctx *fiber.Ctx) error { + if !auth.IsAuthenticated(ctx) { + return ctx.Redirect(auth.GetLoginURLWithNextField(ctx), fiber.StatusFound) + } + + postID := ctx.Params("id") + if postID == "" { + return NotFoundController(ctx) + } + + uintPostID, err := format.StringToUint(postID) + if err != nil { + return NotFoundController(ctx) + } + + post, err := database.GetPostByID(uintPostID) + if err != nil { + if err.Error() == "record not found" { + return NotFoundController(ctx) + } + return InternalServerErrorController(ctx, err) + } + + currentUser := auth.GetCurrentUser(ctx) + if post.Uploader.Username != currentUser.Username && !currentUser.CanEditPosts() { + return ForbiddenController(ctx, errors.New("You do not have permission to edit this post")) + } + + title := ctx.FormValue("title") + description := ctx.FormValue("description") + sourceURL := ctx.FormValue("source_url") + rating := ctx.FormValue("rating") + + updates := make(map[string]interface{}) + + if title != post.Title { + updates["title"] = title + } + + if description != post.Description { + updates["description"] = description + } + + if sourceURL != post.SourceURL { + updates["source_url"] = sourceURL + } + + if rating != "" && rating != string(post.Rating) { + ratingEnum, err := transformers.ConvertStringRatingToType(rating) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid rating value") + } + updates["rating"] = ratingEnum + } + + if currentUser.CanApprovePosts() { + isApproved := ctx.FormValue("is_approved") == "1" + if isApproved != post.IsApproved { + updates["is_approved"] = isApproved + } + } + + if currentUser.CanDeletePosts() { + isDeleted := ctx.FormValue("is_deleted") == "1" + if isDeleted != post.IsDeleted { + updates["is_deleted"] = isDeleted + } + } + + if currentUser.IsAdmin() { + uploaderID := ctx.FormValue("uploader") + if uploaderID != "" { + uintUploaderID, err := format.StringToUint(uploaderID) + if err == nil && uintUploaderID != post.UploaderID { + updates["uploader_id"] = uintUploaderID + } + } + + approverID := ctx.FormValue("approver") + if approverID != "" { + if approverID == "0" { + if post.ApproverID != nil { + updates["approver_id"] = nil + } + } else { + uintApproverID, err := format.StringToUint(approverID) + if err == nil && (post.ApproverID == nil || *post.ApproverID != uintApproverID) { + updates["approver_id"] = uintApproverID + } + } + } + } + + if len(updates) > 0 { + if err := database.UpdateImage(post.ID, updates); err != nil { + return InternalServerErrorController(ctx, err) + } + } + + nextURL := ctx.FormValue("next") + if nextURL == "" { + nextURL = "/posts/" + format.Int64ToString(int64(post.ID)) + } + + return ctx.Redirect(nextURL, fiber.StatusSeeOther) +} diff --git a/controllers/register.go b/controllers/register.go index 8952c91..6d1383f 100644 --- a/controllers/register.go +++ b/controllers/register.go @@ -58,9 +58,11 @@ func RegisterPostController(ctx *fiber.Ctx) error { } user := &models.User{ - Username: form.Username, - Email: form.Email, - Password: form.Password, + Username: form.Username, + Email: form.Email, + Password: form.Password, + PostsRequireApproval: true, + Level: config.UserLevelMember, } if err := database.CreateUser(user); err != nil { diff --git a/database/images.go b/database/images.go index 95b5339..40b6390 100644 --- a/database/images.go +++ b/database/images.go @@ -80,3 +80,7 @@ func CreateImageSizeWithTx(tx *gorm.DB, imageID uint, sizeType config.ImageSizeT return &imageSize, nil } + +func UpdateImage(imageID uint, updates map[string]interface{}) error { + return DB.Model(&models.Image{}).Where("id = ?", imageID).Updates(updates).Error +} diff --git a/database/posts.go b/database/posts.go index 1aeaecd..9736feb 100644 --- a/database/posts.go +++ b/database/posts.go @@ -31,7 +31,7 @@ func GetPosts(limit int, ratings []config.Rating, tags []string) ([]models.Image func GetPostByID(postID uint) (*models.Image, error) { var post models.Image - if err := DB.Preload("Sizes").Preload("Uploader").Preload("Tags").First(&post, postID).Error; err != nil { + if err := DB.Preload("Sizes").Preload("Uploader").Preload("Approver").Preload("Tags").First(&post, postID).Error; err != nil { return nil, err } return &post, nil diff --git a/database/user.go b/database/user.go index 4fe7e18..cae46ae 100644 --- a/database/user.go +++ b/database/user.go @@ -1,6 +1,9 @@ package database -import "imageboard/models" +import ( + "imageboard/config" + "imageboard/models" +) func GetUserByUsername(username string) (*models.User, error) { var user models.User @@ -10,6 +13,18 @@ func GetUserByUsername(username string) (*models.User, error) { return &user, nil } +func ListAllUsers() ([]models.User, error) { + var users []models.User + err := DB.Where("is_deleted = ?", false).Order("LOWER(username) ASC").Find(&users).Error + return users, err +} + +func ListAllApprovers() ([]models.User, error) { + var users []models.User + err := DB.Where("is_deleted = ? AND level >= ?", false, config.UserLevelJanitor).Order("LOWER(username) ASC").Find(&users).Error + return users, err +} + func GetUserByID(userID uint) (*models.User, error) { var user models.User if err := DB.Where("id = ?", userID).First(&user).Error; err != nil { diff --git a/models/user.go b/models/user.go index 6ed3270..0fab1e7 100644 --- a/models/user.go +++ b/models/user.go @@ -34,7 +34,7 @@ type User struct { } func (u *User) BeforeCreate(tx *gorm.DB) error { - u.Username = strings.TrimSpace(u.Username) + u.Username = strings.TrimSpace(strings.ToLower(u.Username)) u.Email = strings.TrimSpace(strings.ToLower(u.Email)) if u.Username == "" { @@ -51,7 +51,8 @@ func (u *User) BeforeCreate(tx *gorm.DB) error { } if userCount == 0 { - u.Level = config.UserLevelSuperAdmin // First user becomes Super Admin + u.Level = config.UserLevelSuperAdmin + u.PostsRequireApproval = false } if len(u.Username) < 3 && u.Level < config.UserLevelSuperAdmin { diff --git a/router/routes.go b/router/routes.go index 9d8c4fd..fd1f898 100644 --- a/router/routes.go +++ b/router/routes.go @@ -18,8 +18,9 @@ func Initialize(router *fiber.App) { posts.Post("/new", controllers.PostsUploadPostController) posts.Get("/new/ilinkfetch", controllers.PostsUploadImageLinkProxyController) posts.Get("/:id", controllers.PostsSinglePostPageController) - posts.Post("/:id/favourite", controllers.PostsSinglePostFavouriteController) - posts.Get("/:id/edit", controllers.PostsSinglePostEditController) + posts.Post("/:id/favourite", controllers.PostsSinglePostFavouritePostController) + posts.Get("/:id/edit", controllers.PostsSinglePostEditPageController) + posts.Post("/:id/edit", controllers.PostsSinglePostEditPostController) login := router.Group("/login") login.Get("/", controllers.LoginPageController) diff --git a/static/css/main.css b/static/css/main.css index cdd39e5..640303c 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -198,6 +198,7 @@ main { .itext, input[type="text"], +textarea, input[type="email"], input[type="password"], input[type="number"], @@ -210,6 +211,7 @@ input[type="url"] { .itext:focus, input[type="text"]:focus, +textarea:focus, input[type="email"]:focus, input[type="password"]:focus, input[type="number"]:focus, @@ -219,6 +221,30 @@ input[type="url"]:focus { outline: none; } +select { + background-color: #1a0033; + border: 1px solid #9999ff; + color: #ccccff; + padding: 3px 5px; + width: 100%; +} + +select:focus { + border-color: #ff99cc; + background-color: #260040; + outline: none; +} + +option { + background-color: #1a0033; + color: #ccccff; +} + +option:hover { + background-color: #260040; + color: #ff99cc; +} + input[type="button"], button[type="button"], input[type="submit"] { @@ -352,7 +378,15 @@ footer::before { gap: 4px; } -.fg-sub small { +.fg-sub-radio { + display: flex; + flex-direction: row; + gap: 8px; + align-items: center; +} + +.fg-sub small, +.fg-sub-radio small { color: #ff99cc; } @@ -363,6 +397,7 @@ footer::before { } .fg-sub input, +.fg-sub textarea, .fg-sub.itext { border-style: double; border-width: 3px; @@ -370,6 +405,13 @@ footer::before { width: 100%; } +.fg-sub select { + border-style: double; + border-width: 3px; + border-color: #9999ff; + width: 100%; +} + .fbtngrp { margin: 8px 0 0 0; } @@ -400,6 +442,13 @@ footer::before { text-align: center; } +.info { + background-color: #001a33; + border: 1px solid #0066cc; + padding: 8px; + margin-bottom: 16px; +} + .upload-drag-box { border: 2px dashed #9999ff; background-color: #1a0033; @@ -765,6 +814,10 @@ footer::before { color: #ff6b6b; } +.post-rating>a { + color: inherit; +} + .post-rating.Safe { color: #4caf50; } @@ -886,7 +939,6 @@ footer::before { .post-detail-label { color: #cccccc; - white-space: nowrap; } .post-favourite-actions { @@ -920,4 +972,40 @@ footer::before { width: 16px; height: 16px; fill: currentColor; +} + +.edit-post { + display: flex; + flex-direction: row; + height: 100%; + align-items: stretch; +} + +.edit-main { + flex: 1; + padding-right: 8px; + border-right: 1px solid #4d4d80; +} + +.edit-sidebar { + width: 264px; + flex-shrink: 0; + padding-left: 8px; + height: 100%; + display: flex; + flex-direction: column; + gap: 8px; +} + +.edit-sidebar>.post-detail-item { + align-items: flex-start; +} + +.edit-sidebar>.post-detail-item>.post-detail-label { + width: 72px; + flex-shrink: 0; +} + +.edit-sidebar>.post-detail-item>.post-detail-value { + word-break: break-all; } \ No newline at end of file diff --git a/templates/posts/edit.django b/templates/posts/edit.django index b4f13a4..93d582d 100644 --- a/templates/posts/edit.django +++ b/templates/posts/edit.django @@ -1,4 +1,209 @@ {% extends 'layouts/main.django' %} {% block content %} - Edit Post Page +
+
+ {% if Post.Title %} +

{{ Post.Title }}

+ {% else %} +

Post #{{ Post.ID }}

+ {% endif %} +
+ {% if Error %} +
{{ Error }}
+ {% endif %} +
+
+ +
+
+ + Optional title for the post. If left empty, the post will be titled "Post #{{ Post.ID }}". +
+
+
+
+ +
+
+ + Optional description for the post. This can be used to provide more context or details about the content of the post. Learn more about syntax. +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ {% if 'Safe' in Post.Rating %} + + {% else %} + + {% endif %} + + {% if 'Sensitive' in Post.Rating %} + + {% else %} + + {% endif %} + + {% if 'Questionable' in Post.Rating %} + + {% else %} + + {% endif %} + + {% if 'Explicit' in Post.Rating %} + + {% else %} + + {% endif %} + +
+
+ {% if User.CanApprovePosts %} +
+
+ +
+
+ {% if Post.IsApproved %} + + {% else %} + + {% endif %} + +
+
+ {% endif %} + {% if User.CanDeletePosts %} +
+
+ +
+
+ {% if Post.IsDeleted %} + + {% else %} + + {% endif %} + +
+
+ {% endif %} + {% if User.IsAdmin %} +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ {% endif %} + + +
+

Tags

+
+
+ General Tags: + + {% if PostTags.general|length > 0 %} + {% for tag in PostTags.general %} + {{ tag.Name }} + {% endfor %} + {% else %} + No general tags + {% endif %} + +
+ + +
+
+
+ {{ Post.Title }} +
+ ID: + {{ Post.ID }} +
+
+ Uploader: + {{ Post.Uploader.Username }} +
+
+ Approver: + {% if Post.Approver.ID %} + {{ Post.Approver.Username }} + {% else %} + {% if Post.IsApproved %} + N/A + {% else %} + Not Approved + {% endif %} + {% endif %} +
+
+ Filename: + {{ Post.FileName }} +
+
+ Type: + {{ Post.ContentType }} +
+
+ MD5: + {{ Post.MD5Hash }} +
+
+ ViewCount: + {{ Post.ViewCount }} +
+
+ Favourites: + {{ Post.FavouriteCount }} +
+
+ Comments: + {{ Post.CommentCount }} +
+
+
{% endblock %} diff --git a/templates/posts/single.django b/templates/posts/single.django index ec788c3..56fc670 100644 --- a/templates/posts/single.django +++ b/templates/posts/single.django @@ -83,10 +83,15 @@ Large Original {% if User and Post.Uploader.Username == User.Username or User.CanEditTags %} - | Edit Post + | Edit Post {% endif %} + {% if not Post.IsApproved %} +
+ This post is pending approval. See mod queues. +
+ {% endif %}
{{ Post.Title }}
@@ -108,11 +113,11 @@ {{ Post.GetOriginalSize.Width }}x{{ Post.GetOriginalSize.Height }} ({{ Post.GetOriginalSize.GetFileSizeFormatted }})
- Favourites: + Favourites: {{ Post.FavouriteCount }}
- +
Rating: - {{ Post.Rating }} + {{ Post.Rating }}
@@ -158,6 +163,12 @@
+
+
+ Description: + {{ Post.Description|default:'No description provided.' }} +
+
{% endif %} {% endblock %} -- cgit v1.2.3