Laid the foundations for better reply attachments.

The attachment manager introduced in the previous patch is now properly localised.

Repurposed AttachmentStore.CountInTopic as a more general CountIn method.
Added an & entity in attachment URLs so that the characters don't get mutated into something weird.
Tried to make the linebreaks a little glitchy in the inline editor, we have a better solution in a mind soon!
Fixed a bug where replies used .ContentHTML instead of .Content which led to a lot of HTML getting in the way of reply edits.
Fixed a bug where reply attachments used the topicID rather than the replyID for their originID entries.
Fixed a bug where the topic attachment counts weren't getting incremented.

Added the topic.select_button_test, topic.copy_button_test and topic.upload_button_test phrases.

Added the attachCount column to the replies table. This commit requires you to run the patcher / updater.
This commit is contained in:
Azareal 2018-12-27 19:12:30 +10:00
parent 6c42df3091
commit bf2af0ae96
14 changed files with 141 additions and 50 deletions

View File

@ -247,6 +247,7 @@ func createTables(adapter qgen.Adapter) error {
tblColumn{"lastUpdated", "datetime", 0, false, false, ""}, tblColumn{"lastUpdated", "datetime", 0, false, false, ""},
tblColumn{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"}, tblColumn{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"},
tblColumn{"likeCount", "int", 0, false, false, "0"}, tblColumn{"likeCount", "int", 0, false, false, "0"},
tblColumn{"attachCount", "int", 0, false, false, "0"},
tblColumn{"words", "int", 0, false, false, "1"}, // ? - replies has a default of 1 and topics has 0? why? tblColumn{"words", "int", 0, false, false, "1"}, // ? - replies has a default of 1 and topics has 0? why?
tblColumn{"actionType", "varchar", 20, false, false, "''"}, tblColumn{"actionType", "varchar", 20, false, false, "''"},
tblColumn{"poll", "int", 0, false, false, "0"}, tblColumn{"poll", "int", 0, false, false, "0"},

View File

@ -26,7 +26,7 @@ type AttachmentStore interface {
MiniTopicGet(id int) (alist []*MiniAttachment, err error) MiniTopicGet(id int) (alist []*MiniAttachment, err error)
Add(sectionID int, sectionTable string, originID int, originTable string, uploadedBy int, path string) (int, error) Add(sectionID int, sectionTable string, originID int, originTable string, uploadedBy int, path string) (int, error)
GlobalCount() int GlobalCount() int
CountInTopic(tid int) int CountIn(originTable string, oid int) int
CountInPath(path string) int CountInPath(path string) int
Delete(aid int) error Delete(aid int) error
} }
@ -36,7 +36,7 @@ type DefaultAttachmentStore struct {
getByTopic *sql.Stmt getByTopic *sql.Stmt
add *sql.Stmt add *sql.Stmt
count *sql.Stmt count *sql.Stmt
countInTopic *sql.Stmt countIn *sql.Stmt
countInPath *sql.Stmt countInPath *sql.Stmt
delete *sql.Stmt delete *sql.Stmt
} }
@ -48,7 +48,7 @@ func NewDefaultAttachmentStore() (*DefaultAttachmentStore, error) {
getByTopic: acc.Select("attachments").Columns("attachID, sectionID, uploadedBy, path").Where("originTable = 'topics' AND originID = ?").Prepare(), getByTopic: acc.Select("attachments").Columns("attachID, sectionID, uploadedBy, path").Where("originTable = 'topics' AND originID = ?").Prepare(),
add: acc.Insert("attachments").Columns("sectionID, sectionTable, originID, originTable, uploadedBy, path").Fields("?,?,?,?,?,?").Prepare(), add: acc.Insert("attachments").Columns("sectionID, sectionTable, originID, originTable, uploadedBy, path").Fields("?,?,?,?,?,?").Prepare(),
count: acc.Count("attachments").Prepare(), count: acc.Count("attachments").Prepare(),
countInTopic: acc.Count("attachments").Where("originTable = 'topics' and originID = ?").Prepare(), countIn: acc.Count("attachments").Where("originTable = ? and originID = ?").Prepare(),
countInPath: acc.Count("attachments").Where("path = ?").Prepare(), countInPath: acc.Count("attachments").Where("path = ?").Prepare(),
delete: acc.Delete("attachments").Where("attachID = ?").Prepare(), delete: acc.Delete("attachments").Where("attachID = ?").Prepare(),
}, acc.FirstError() }, acc.FirstError()
@ -107,8 +107,8 @@ func (store *DefaultAttachmentStore) GlobalCount() (count int) {
return count return count
} }
func (store *DefaultAttachmentStore) CountInTopic(tid int) (count int) { func (store *DefaultAttachmentStore) CountIn(originTable string, oid int) (count int) {
err := store.countInTopic.QueryRow(tid).Scan(&count) err := store.countIn.QueryRow(originTable, oid).Scan(&count)
if err != nil { if err != nil {
LogError(err) LogError(err)
} }

View File

@ -587,7 +587,7 @@ func ParseMessage(msg string, sectionID int, sectionType string /*, user User*/)
// TODO: Reduce the amount of code duplication // TODO: Reduce the amount of code duplication
if media.Type == "attach" { if media.Type == "attach" {
addImage(media.URL + "?sectionID=" + strconv.Itoa(sectionID) + "&sectionType=" + sectionType) addImage(media.URL + "?sectionID=" + strconv.Itoa(sectionID) + "&sectionType=" + sectionType)
continue continue
} else if media.Type == "image" { } else if media.Type == "image" {
addImage(media.URL) addImage(media.URL)

View File

@ -376,6 +376,10 @@
"topic.report_button_text":"Report", "topic.report_button_text":"Report",
"topic.flag_button_text":"Flag", "topic.flag_button_text":"Flag",
"topic.select_button_text":"Select",
"topic.copy_button_text":"Copy",
"topic.upload_button_text":"Upload",
"panel_rank_admins":"Admins", "panel_rank_admins":"Admins",
"panel_rank_mods":"Mods", "panel_rank_mods":"Mods",
"panel_rank_banned":"Banned", "panel_rank_banned":"Banned",

View File

@ -22,6 +22,7 @@ func init() {
addPatch(8, patch8) addPatch(8, patch8)
addPatch(9, patch9) addPatch(9, patch9)
addPatch(10, patch10) addPatch(10, patch10)
addPatch(11, patch11)
} }
func patch0(scanner *bufio.Scanner) (err error) { func patch0(scanner *bufio.Scanner) (err error) {
@ -29,7 +30,6 @@ func patch0(scanner *bufio.Scanner) (err error) {
if err != nil { if err != nil {
return err return err
} }
err = execStmt(qgen.Builder.DropTable("menu_items")) err = execStmt(qgen.Builder.DropTable("menu_items"))
if err != nil { if err != nil {
return err return err
@ -400,7 +400,6 @@ func patch10(scanner *bufio.Scanner) error {
return err return err
} }
// We could probably do something more efficient, but as there shouldn't be too many sites right now, we can probably cheat a little, otherwise it'll take forever to get things done
err = acc().Select("topics").Cols("tid").EachInt(func(tid int) error { err = acc().Select("topics").Cols("tid").EachInt(func(tid int) error {
stid := itoa(tid) stid := itoa(tid)
@ -430,3 +429,41 @@ func patch10(scanner *bufio.Scanner) error {
_, err = acc().Insert("updates").Columns("dbVersion").Fields("0").Exec() _, err = acc().Insert("updates").Columns("dbVersion").Fields("0").Exec()
return err return err
} }
func patch11(scanner *bufio.Scanner) error {
err := execStmt(qgen.Builder.AddColumn("replies", tblColumn{"attachCount", "int", 0, false, false, "0"}))
if err != nil {
return err
}
// Attachments for replies got the topicID rather than the replyID for a while in error, so we want to separate these out
_, err = acc().Update("attachments").Set("originTable = 'freplies'").Where("originTable = 'replies'").Exec()
if err != nil {
return err
}
// We could probably do something more efficient, but as there shouldn't be too many sites right now, we can probably cheat a little, otherwise it'll take forever to get things done
return acc().Select("topics").Cols("tid").EachInt(func(tid int) error {
stid := itoa(tid)
count, err := acc().Count("attachments").Where("originTable = 'topics' and originID = " + stid).Total()
if err != nil {
return err
}
_, err = acc().Update("topics").Set("attachCount = ?").Where("tid = " + stid).Exec(count)
return err
})
/*return acc().Select("replies").Cols("rid").EachInt(func(rid int) error {
srid := itoa(rid)
count, err := acc().Count("attachments").Where("originTable = 'replies' and originID = " + srid).Total()
if err != nil {
return err
}
_, err = acc().Update("replies").Set("attachCount = ?").Where("rid = " + srid).Exec(count)
return err
})*/
}

View File

@ -380,7 +380,7 @@ function mainInit(){
$(".topic_name").html(topicNameInput); $(".topic_name").html(topicNameInput);
$(".topic_name").attr(topicNameInput); $(".topic_name").attr(topicNameInput);
let topicContentInput = $('.topic_content_input').val(); let topicContentInput = $('.topic_content_input').val();
$(".topic_content").html(topicContentInput.replace(/(\n)+/g,"<br />")); $(".topic_content").html(topicContentInput.replace("\n","<br><br>"));
let topicStatusInput = $('.topic_status_input').val(); let topicStatusInput = $('.topic_status_input').val();
$(".topic_status_e:not(.open_edit)").html(topicStatusInput); $(".topic_status_e:not(.open_edit)").html(topicStatusInput);
@ -410,18 +410,25 @@ function mainInit(){
$(".edit_item").click(function(event){ $(".edit_item").click(function(event){
event.preventDefault(); event.preventDefault();
let blockParent = $(this).closest('.editable_parent'); let blockParent = this.closest('.editable_parent');
let block = blockParent.find('.editable_block').eq(0); let srcNode = blockParent.querySelector(".edit_source");
block.html("<textarea style='width: 99%;' name='edit_item'>" + block.html() + "</textarea><br /><a href='" + $(this).closest('a').attr("href") + "'><button class='submit_edit' type='submit'>Update</button></a>"); let block = blockParent.querySelector('.editable_block');
block.classList.add("in_edit");
let source = "";
if(srcNode!=null) source = srcNode.innerText;
else source = block.innerHTML;
// TODO: Add a client template for this
block.innerHTML = "<textarea style='width: 99%;' name='edit_item'>" + source + "</textarea><br /><a href='" + this.closest('a').getAttribute("href") + "'><button class='submit_edit' type='submit'>Update</button></a>";
$(".submit_edit").click(function(event){ $(".submit_edit").click(function(event){
event.preventDefault(); event.preventDefault();
let blockParent = $(this).closest('.editable_parent'); block.classList.remove("in_edit");
let block = blockParent.find('.editable_block').eq(0); let newContent = block.querySelector('textarea').value;
let newContent = block.find('textarea').eq(0).val(); block.innerHTML = newContent.replace("\n","<br><br>");
block.html(newContent); if(srcNode!=null) srcNode.innerText = newContent;
var formAction = $(this).closest('a').attr("href"); let formAction = this.closest('a').getAttribute("href");
// TODO: Bounce the parsed post back and set innerHTML to it?
$.ajax({ url: formAction, type: "POST", error: ajaxError, dataType: "json", data: { isJs: "1", edit_item: newContent } $.ajax({ url: formAction, type: "POST", error: ajaxError, dataType: "json", data: { isJs: "1", edit_item: newContent }
}); });
}); });

View File

@ -8,8 +8,26 @@ import (
"github.com/Azareal/Gosora/common" "github.com/Azareal/Gosora/common"
"github.com/Azareal/Gosora/common/counters" "github.com/Azareal/Gosora/common/counters"
"github.com/Azareal/Gosora/query_gen"
) )
type ReplyStmts struct {
updateAttachs *sql.Stmt
}
var replyStmts ReplyStmts
// TODO: Move this statement somewhere else
func init() {
common.DbInits.Add(func(acc *qgen.Accumulator) error {
replyStmts = ReplyStmts{
// TODO: Less race-y attachment count updates
updateAttachs: acc.Update("replies").Set("attachCount = ?").Where("rid = ?").Prepare(),
}
return acc.FirstError()
})
}
func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
tid, err := strconv.Atoi(r.PostFormValue("tid")) tid, err := strconv.Atoi(r.PostFormValue("tid"))
if err != nil { if err != nil {
@ -35,15 +53,6 @@ func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user common.User)
return common.NoPermissions(w, r, user) return common.NoPermissions(w, r, user)
} }
// Handle the file attachments
// TODO: Stop duplicating this code
if user.Perms.UploadFiles {
_, rerr := uploadAttachment(w, r, user, topic.ParentID, "forums", tid, "replies")
if rerr != nil {
return rerr
}
}
content := common.PreparseMessage(r.PostFormValue("reply-content")) content := common.PreparseMessage(r.PostFormValue("reply-content"))
// TODO: Fully parse the post and put that in the parsed column // TODO: Fully parse the post and put that in the parsed column
rid, err := common.Rstore.Create(topic, content, user.LastIP, user.ID) rid, err := common.Rstore.Create(topic, content, user.LastIP, user.ID)
@ -55,6 +64,16 @@ func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user common.User)
if err != nil { if err != nil {
return common.LocalError("Unable to load the reply", w, r, user) return common.LocalError("Unable to load the reply", w, r, user)
} }
// Handle the file attachments
// TODO: Stop duplicating this code
if user.Perms.UploadFiles {
_, rerr := uploadAttachment(w, r, user, topic.ParentID, "forums", rid, "replies")
if rerr != nil {
return rerr
}
}
if r.PostFormValue("has_poll") == "1" { if r.PostFormValue("has_poll") == "1" {
var maxPollOptions = 10 var maxPollOptions = 10
var pollInputItems = make(map[int]string) var pollInputItems = make(map[int]string)

View File

@ -599,6 +599,7 @@ func deleteAttachment(w http.ResponseWriter, r *http.Request, user common.User,
// TODO: Stop duplicating this code // TODO: Stop duplicating this code
// TODO: Use a transaction here // TODO: Use a transaction here
// TODO: Move this function to neutral ground
func uploadAttachment(w http.ResponseWriter, r *http.Request, user common.User, sid int, sectionTable string, oid int, originTable string) (pathMap map[string]string, rerr common.RouteError) { func uploadAttachment(w http.ResponseWriter, r *http.Request, user common.User, sid int, sectionTable string, oid int, originTable string) (pathMap map[string]string, rerr common.RouteError) {
pathMap = make(map[string]string) pathMap = make(map[string]string)
files, rerr := uploadFilesWithHash(w, r, user, "./attachs/") files, rerr := uploadFilesWithHash(w, r, user, "./attachs/")
@ -619,9 +620,9 @@ func uploadAttachment(w http.ResponseWriter, r *http.Request, user common.User,
pathMap[filename] = strconv.Itoa(aid) pathMap[filename] = strconv.Itoa(aid)
} }
switch sectionTable { switch originTable {
case "topics": case "topics":
_, err = topicStmts.updateAttachs.Exec(common.Attachments.CountInTopic(oid), oid) _, err = topicStmts.updateAttachs.Exec(common.Attachments.CountIn(originTable,oid), oid)
if err != nil { if err != nil {
return nil, common.InternalError(err, w, r) return nil, common.InternalError(err, w, r)
} }
@ -629,6 +630,11 @@ func uploadAttachment(w http.ResponseWriter, r *http.Request, user common.User,
if err != nil { if err != nil {
return nil, common.InternalError(err, w, r) return nil, common.InternalError(err, w, r)
} }
case "replies":
_, err = replyStmts.updateAttachs.Exec(common.Attachments.CountIn(originTable,oid), oid)
if err != nil {
return nil, common.InternalError(err, w, r)
}
} }
} }

View File

@ -10,6 +10,7 @@ CREATE TABLE [replies] (
[lastUpdated] datetime not null, [lastUpdated] datetime not null,
[ipaddress] nvarchar (200) DEFAULT '0.0.0.0.0' not null, [ipaddress] nvarchar (200) DEFAULT '0.0.0.0.0' not null,
[likeCount] int DEFAULT 0 not null, [likeCount] int DEFAULT 0 not null,
[attachCount] int DEFAULT 0 not null,
[words] int DEFAULT 1 not null, [words] int DEFAULT 1 not null,
[actionType] nvarchar (20) DEFAULT '' not null, [actionType] nvarchar (20) DEFAULT '' not null,
[poll] int DEFAULT 0 not null, [poll] int DEFAULT 0 not null,

View File

@ -10,6 +10,7 @@ CREATE TABLE `replies` (
`lastUpdated` datetime not null, `lastUpdated` datetime not null,
`ipaddress` varchar(200) DEFAULT '0.0.0.0.0' not null, `ipaddress` varchar(200) DEFAULT '0.0.0.0.0' not null,
`likeCount` int DEFAULT 0 not null, `likeCount` int DEFAULT 0 not null,
`attachCount` int DEFAULT 0 not null,
`words` int DEFAULT 1 not null, `words` int DEFAULT 1 not null,
`actionType` varchar(20) DEFAULT '' not null, `actionType` varchar(20) DEFAULT '' not null,
`poll` int DEFAULT 0 not null, `poll` int DEFAULT 0 not null,

View File

@ -10,6 +10,7 @@ CREATE TABLE "replies" (
`lastUpdated` timestamp not null, `lastUpdated` timestamp not null,
`ipaddress` varchar (200) DEFAULT '0.0.0.0.0' not null, `ipaddress` varchar (200) DEFAULT '0.0.0.0.0' not null,
`likeCount` int DEFAULT 0 not null, `likeCount` int DEFAULT 0 not null,
`attachCount` int DEFAULT 0 not null,
`words` int DEFAULT 1 not null, `words` int DEFAULT 1 not null,
`actionType` varchar (20) DEFAULT '' not null, `actionType` varchar (20) DEFAULT '' not null,
`poll` int DEFAULT 0 not null, `poll` int DEFAULT 0 not null,

View File

@ -82,17 +82,17 @@
{{if .Topic.Attachments}}<div class="show_on_edit attach_edit_bay" tid="{{.Topic.ID}}"> {{if .Topic.Attachments}}<div class="show_on_edit attach_edit_bay" tid="{{.Topic.ID}}">
{{range .Topic.Attachments}} {{range .Topic.Attachments}}
<div class="attach_item{{if .Image}} attach_image_holder{{end}}"> <div class="attach_item{{if .Image}} attach_image_holder{{end}}">
{{if .Image}}<img src="//{{$.Header.Site.URL}}/attachs/{{.Path}}?sectionID={{.SectionID}}&sectionType=forums" height="24" width="24" />{{end}} {{if .Image}}<img src="//{{$.Header.Site.URL}}/attachs/{{.Path}}?sectionID={{.SectionID}}&sectionType=forums" height=24 width=24 />{{end}}
<span class="attach_item_path" aid="{{.ID}}" fullPath="//{{$.Header.Site.URL}}/attachs/{{.Path}}">{{.Path}}</span> <span class="attach_item_path" aid="{{.ID}}" fullPath="//{{$.Header.Site.URL}}/attachs/{{.Path}}">{{.Path}}</span>
<button class="attach_item_select">Select</button> <button class="attach_item_select">{{lang "topic.select_button_text"}}</button>
<button class="attach_item_copy">Copy</button> <button class="attach_item_copy">{{lang "topic.copy_button_text"}}</button>
</div> </div>
{{end}} {{end}}
<div class="attach_item attach_item_buttons"> <div class="attach_item attach_item_buttons">
{{if .CurrentUser.Perms.UploadFiles}} {{if .CurrentUser.Perms.UploadFiles}}
<input name="upload_files" id="upload_files_op" multiple type="file" style="display: none;" /> <input name="upload_files" id="upload_files_op" multiple type="file" style="display: none;" />
<label for="upload_files_op" class="formbutton add_file_button">Upload</label>{{end}} <label for="upload_files_op" class="formbutton add_file_button">{{lang "topic.upload_button_text"}}</label>{{end}}
<button class="attach_item_delete">Delete</button> <button class="attach_item_delete">{{lang "topic.delete_button_text"}}</button>
</div> </div>
</div>{{end}} </div>{{end}}

View File

@ -11,7 +11,7 @@
<span class="action_icon" style="font-size: 18px;padding-right: 5px;" aria-hidden="true">{{.ActionIcon}}</span> <span class="action_icon" style="font-size: 18px;padding-right: 5px;" aria-hidden="true">{{.ActionIcon}}</span>
<span itemprop="text">{{.ActionType}}</span> <span itemprop="text">{{.ActionType}}</span>
{{else}} {{else}}
{{/** TODO: We might end up with <br>s in the inline editor, fix this **/}} <div class="edit_source auto_hide">{{.Content}}</div>
<div class="editable_block user_content" itemprop="text">{{.ContentHtml}}</div> <div class="editable_block user_content" itemprop="text">{{.ContentHtml}}</div>
<div class="controls button_container{{if .LikeCount}} has_likes{{end}}"> <div class="controls button_container{{if .LikeCount}} has_likes{{end}}">
<div class="action_button_left"> <div class="action_button_left">

View File

@ -710,6 +710,23 @@ button, .formbutton, .panel_right_button:not(.has_inner_button) {
border-radius: 3px; border-radius: 3px;
padding: 16px; padding: 16px;
} }
.user_content.in_edit {
padding: 0px;
background: none;
}
.user_content textarea {
resize: vertical;
height: 150px;
width: 100% !important;
padding: 16px;
}
.user_content.in_edit a {
display: flex;
background-color: #444444;
border-radius: 4px;
margin-top: 4px; /*8 without <br>*/
padding: 6px;
}
.post_item .button_container { .post_item .button_container {
display: flex; display: flex;
margin-top: 8px; margin-top: 8px;
@ -1014,10 +1031,7 @@ input[type=checkbox]:checked + label .sel {
} }
@media(max-width: 500px) { @media(max-width: 500px) {
.sidebar { .sidebar, .topic_view_count {
display: none;
}
.topic_view_count {
display: none; display: none;
} }