You can now manage the attachments for an opening post by hitting edit.

The update system now uses the database as the source of truth for the last version rather than lastSchema.json
Refactored several structs and bits of code, so we can avoid allocations for contexts where we never use a relative time.
Clicking on the relative times on the topic list and the forum page should now take you to the post on the last page rather than just the last page.
Added the reltime template function.
Fixed some obsolete bits of code.
Fixed some spelling mistakes.
Fixed a bug where MaxBytesReader was capped at the maxFileSize rather than r.ContentLength.
All of the client side templates should work again now.
Shortened some statement names to save some horizontal space.
accUpdateBuilder and SimpleUpdate now use updatePrebuilder behind the scenes to simplify things.
Renamed selectItem to builder in AccSelectBuilder.
Added a Total() method to accCountBuilder to reduce the amount of boilerplate used for row count queries.
The "_builder" strings have been replaced with empty strings to help save memory, to make things slightly faster and to open the door to removing the query name in many contexts down the line.
Added the open_edit and close_edit client hooks.
Removed many query name checks.
Split the attachment logic into separate functions and de-duplicated it between replies and topics.
Improved the UI for editing topics in Nox.
Used type aliases to reduce the amount of boilerplate in tables.go and patches.go
Reduced the amount of boilerplate in the action post logic.
Eliminated a map and a slice in the topic page for users who haven't given any likes. E.g. Guests.
Fixed some long out-dated parts of the update instructions.
Updated the update instructions to remove mention of the obsolete lastSchema.json
Fixed a bug in init.js where /api/me was being loaded for guests.
Added the MiniTopicGet, GlobalCount and CountInTopic methods to AttachmentStore.
Added the MiniAttachment struct.
Split the mod floaters out into their own template to reduce duplication.
Removed a couple of redundant ParseForms.

Added the common.skipUntilIfExistsOrLine function.
Added the NotFoundJS and NotFoundJSQ functions.
Added the lastReplyID and attachCount columns to the topics table.
This commit is contained in:
Azareal 2018-12-27 15:42:41 +10:00
parent 548227104c
commit 3465e4c08f
53 changed files with 1786 additions and 1333 deletions

1
.gitignore vendored
View File

@ -21,7 +21,6 @@ out/*
*.log *.log
.DS_Store .DS_Store
.vscode/launch.json .vscode/launch.json
schema/lastSchema.json
config/config.go config/config.go
QueryGen QueryGen
RouterGen RouterGen

File diff suppressed because it is too large Load Diff

View File

@ -2,28 +2,128 @@ package common
import ( import (
"database/sql" "database/sql"
"errors"
"strings"
"github.com/Azareal/Gosora/query_gen" "github.com/Azareal/Gosora/query_gen"
) )
var Attachments AttachmentStore var Attachments AttachmentStore
type MiniAttachment struct {
ID int
SectionID int
OriginID int
UploadedBy int
Path string
Image bool
Ext string
}
type AttachmentStore interface { type AttachmentStore interface {
Add(sectionID int, sectionTable string, originID int, originTable string, uploadedBy int, path string) error Get(id int) (*MiniAttachment, error)
MiniTopicGet(id int) (alist []*MiniAttachment, err error)
Add(sectionID int, sectionTable string, originID int, originTable string, uploadedBy int, path string) (int, error)
GlobalCount() int
CountInTopic(tid int) int
CountInPath(path string) int
Delete(aid int) error
} }
type DefaultAttachmentStore struct { type DefaultAttachmentStore struct {
get *sql.Stmt
getByTopic *sql.Stmt
add *sql.Stmt add *sql.Stmt
count *sql.Stmt
countInTopic *sql.Stmt
countInPath *sql.Stmt
delete *sql.Stmt
} }
func NewDefaultAttachmentStore() (*DefaultAttachmentStore, error) { func NewDefaultAttachmentStore() (*DefaultAttachmentStore, error) {
acc := qgen.NewAcc() acc := qgen.NewAcc()
return &DefaultAttachmentStore{ return &DefaultAttachmentStore{
get: acc.Select("attachments").Columns("originID, sectionID, uploadedBy, path").Where("attachID = ?").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(),
countInTopic: acc.Count("attachments").Where("originTable = 'topics' and originID = ?").Prepare(),
countInPath: acc.Count("attachments").Where("path = ?").Prepare(),
delete: acc.Delete("attachments").Where("attachID = ?").Prepare(),
}, acc.FirstError() }, acc.FirstError()
} }
func (store *DefaultAttachmentStore) Add(sectionID int, sectionTable string, originID int, originTable string, uploadedBy int, path string) error { // TODO: Make this more generic so we can use it for reply attachments too
_, err := store.add.Exec(sectionID, sectionTable, originID, originTable, uploadedBy, path) func (store *DefaultAttachmentStore) MiniTopicGet(id int) (alist []*MiniAttachment, err error) {
rows, err := store.getByTopic.Query(id)
defer rows.Close()
for rows.Next() {
attach := &MiniAttachment{OriginID: id}
err := rows.Scan(&attach.ID, &attach.SectionID, &attach.UploadedBy, &attach.Path)
if err != nil {
return nil, err
}
extarr := strings.Split(attach.Path, ".")
if len(extarr) < 2 {
return nil, errors.New("corrupt attachment path")
}
attach.Ext = extarr[len(extarr)-1]
attach.Image = ImageFileExts.Contains(attach.Ext)
alist = append(alist, attach)
}
return alist, rows.Err()
}
func (store *DefaultAttachmentStore) Get(id int) (*MiniAttachment, error) {
attach := &MiniAttachment{ID: id}
err := store.get.QueryRow(id).Scan(&attach.OriginID, &attach.SectionID, &attach.UploadedBy, &attach.Path)
if err != nil {
return nil, err
}
extarr := strings.Split(attach.Path, ".")
if len(extarr) < 2 {
return nil, errors.New("corrupt attachment path")
}
attach.Ext = extarr[len(extarr)-1]
attach.Image = ImageFileExts.Contains(attach.Ext)
return attach, nil
}
func (store *DefaultAttachmentStore) Add(sectionID int, sectionTable string, originID int, originTable string, uploadedBy int, path string) (int, error) {
res, err := store.add.Exec(sectionID, sectionTable, originID, originTable, uploadedBy, path)
if err != nil {
return 0, err
}
lid, err := res.LastInsertId()
return int(lid), err
}
func (store *DefaultAttachmentStore) GlobalCount() (count int) {
err := store.count.QueryRow().Scan(&count)
if err != nil {
LogError(err)
}
return count
}
func (store *DefaultAttachmentStore) CountInTopic(tid int) (count int) {
err := store.countInTopic.QueryRow(tid).Scan(&count)
if err != nil {
LogError(err)
}
return count
}
func (store *DefaultAttachmentStore) CountInPath(path string) (count int) {
err := store.countInPath.QueryRow(path).Scan(&count)
if err != nil {
LogError(err)
}
return count
}
func (store *DefaultAttachmentStore) Delete(aid int) error {
_, err := store.delete.Exec(aid)
return err return err
} }

View File

@ -11,6 +11,7 @@ import (
"log" "log"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/Azareal/Gosora/query_gen" "github.com/Azareal/Gosora/query_gen"
) )
@ -33,7 +34,7 @@ var TmplPtrMap = make(map[string]interface{})
// Anti-spam token with rotated key // Anti-spam token with rotated key
var JSTokenBox atomic.Value // TODO: Move this and some of these other globals somewhere else var JSTokenBox atomic.Value // TODO: Move this and some of these other globals somewhere else
var SessionSigningKeyBox atomic.Value // For MFA to avoid hitting the database unneccesarily var SessionSigningKeyBox atomic.Value // For MFA to avoid hitting the database unneccessarily
var OldSessionSigningKeyBox atomic.Value // Just in case we've signed with a key that's about to go stale so we don't annoy the user too much var OldSessionSigningKeyBox atomic.Value // Just in case we've signed with a key that's about to go stale so we don't annoy the user too much
var IsDBDown int32 = 0 // 0 = false, 1 = true. this is value which should be manipulated with package atomic for representing whether the database is down so we don't spam the log with lots of redundant errors var IsDBDown int32 = 0 // 0 = false, 1 = true. this is value which should be manipulated with package atomic for representing whether the database is down so we don't spam the log with lots of redundant errors

View File

@ -314,12 +314,29 @@ func SecurityError(w http.ResponseWriter, r *http.Request, user User) RouteError
} }
// NotFound is used when the requested page doesn't exist // NotFound is used when the requested page doesn't exist
// ? - Add a JSQ and JS version of this? // ? - Add a JSQ version of this?
// ? - Add a user parameter? // ? - Add a user parameter?
func NotFound(w http.ResponseWriter, r *http.Request, header *Header) RouteError { func NotFound(w http.ResponseWriter, r *http.Request, header *Header) RouteError {
return CustomError(phrases.GetErrorPhrase("not_found_body"), 404, phrases.GetErrorPhrase("not_found_title"), w, r, header, GuestUser) return CustomError(phrases.GetErrorPhrase("not_found_body"), 404, phrases.GetErrorPhrase("not_found_title"), w, r, header, GuestUser)
} }
// ? - Add a user parameter?
func NotFoundJS(w http.ResponseWriter, r *http.Request) RouteError {
w.WriteHeader(401)
writeJsonError(phrases.GetErrorPhrase("not_found_body"), w)
return HandledRouteError()
}
func NotFoundJSQ(w http.ResponseWriter, r *http.Request, header *Header, js bool) RouteError {
if js {
return NotFoundJS(w, r)
}
if header == nil {
header = DefaultHeader(w, GuestUser)
}
return NotFound(w, r, header)
}
// CustomError lets us make custom error types which aren't covered by the generic functions above // CustomError lets us make custom error types which aren't covered by the generic functions above
func CustomError(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, header *Header, user User) RouteError { func CustomError(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, header *Header, user User) RouteError {
if header == nil { if header == nil {

View File

@ -94,7 +94,7 @@ func (list SFileList) JSTmplInit() error {
preLen := len(data) preLen := len(data)
data = replace(data, string(data[spaceIndex:endBrace]), "") data = replace(data, string(data[spaceIndex:endBrace]), "")
data = replace(data, "))\n", "\n") data = replace(data, "))\n", " \n")
endBrace -= preLen - len(data) // Offset it as we've deleted portions endBrace -= preLen - len(data) // Offset it as we've deleted portions
fmt.Println("new endBrace: ", endBrace) fmt.Println("new endBrace: ", endBrace)
fmt.Println("data: ", string(data)) fmt.Println("data: ", string(data))
@ -130,58 +130,38 @@ func (list SFileList) JSTmplInit() error {
} }
} }
each("strconv.Itoa(", func(index int) { each("strconv.Itoa(", func(index int) {
braceAt, hasEndBrace := skipUntilIfExists(data, index, ')') braceAt, hasEndBrace := skipUntilIfExistsOrLine(data, index, ')')
// TODO: Make sure we don't go onto the next line in case someone misplaced a brace
if hasEndBrace { if hasEndBrace {
data[braceAt] = ' ' // Blank it data[braceAt] = ' ' // Blank it
} }
}) })
each("w.Write([]byte(", func(index int) { each("[]byte(", func(index int) {
braceAt, hasEndBrace := skipUntilIfExists(data, index, ')') braceAt, hasEndBrace := skipUntilIfExistsOrLine(data, index, ')')
// TODO: Make sure we don't go onto the next line in case someone misplaced a brace
if hasEndBrace {
data[braceAt] = ' ' // Blank it
}
braceAt, hasEndBrace = skipUntilIfExists(data, braceAt, ')')
if hasEndBrace {
data[braceAt] = ' ' // Blank this one too
}
})
each(" = []byte(", func(index int) {
braceAt, hasEndBrace := skipUntilIfExists(data, index, ')')
// TODO: Make sure we don't go onto the next line in case someone misplaced a brace
if hasEndBrace { if hasEndBrace {
data[braceAt] = ' ' // Blank it data[braceAt] = ' ' // Blank it
} }
}) })
each("w.Write(StringToBytes(", func(index int) { each("StringToBytes(", func(index int) {
braceAt, hasEndBrace := skipUntilIfExists(data, index, ')') braceAt, hasEndBrace := skipUntilIfExistsOrLine(data, index, ')')
// TODO: Make sure we don't go onto the next line in case someone misplaced a brace
if hasEndBrace {
data[braceAt] = ' ' // Blank it
}
braceAt, hasEndBrace = skipUntilIfExists(data, braceAt, ')')
if hasEndBrace {
data[braceAt] = ' ' // Blank this one too
}
})
each(" = StringToBytes(", func(index int) {
braceAt, hasEndBrace := skipUntilIfExists(data, index, ')')
// TODO: Make sure we don't go onto the next line in case someone misplaced a brace
if hasEndBrace { if hasEndBrace {
data[braceAt] = ' ' // Blank it data[braceAt] = ' ' // Blank it
} }
}) })
each("w.Write(", func(index int) { each("w.Write(", func(index int) {
braceAt, hasEndBrace := skipUntilIfExists(data, index, ')') braceAt, hasEndBrace := skipUntilIfExistsOrLine(data, index, ')')
// TODO: Make sure we don't go onto the next line in case someone misplaced a brace
if hasEndBrace { if hasEndBrace {
data[braceAt] = ' ' // Blank it data[braceAt] = ' ' // Blank it
} }
}) })
each("RelativeTime(", func(index int) {
braceAt, _ := skipUntilIfExistsOrLine(data, index, 10)
if data[braceAt-1] == ' ' {
data[braceAt-1] = ')' // Blank it
}
})
each("if ", func(index int) { each("if ", func(index int) {
//fmt.Println("if index: ", index) //fmt.Println("if index: ", index)
braceAt, hasBrace := skipUntilIfExists(data, index, '{') braceAt, hasBrace := skipUntilIfExistsOrLine(data, index, '{')
if hasBrace { if hasBrace {
if data[braceAt-1] != ' ' { if data[braceAt-1] != ' ' {
panic("couldn't find space before brace, found ' " + string(data[braceAt-1]) + "' instead") panic("couldn't find space before brace, found ' " + string(data[braceAt-1]) + "' instead")
@ -210,10 +190,12 @@ func (list SFileList) JSTmplInit() error {
data = replace(data, ", 10;", "") data = replace(data, ", 10;", "")
data = replace(data, shortName+"_tmpl_phrase_id = RegisterTmplPhraseNames([]string{", "[") data = replace(data, shortName+"_tmpl_phrase_id = RegisterTmplPhraseNames([]string{", "[")
data = replace(data, "var plist = GetTmplPhrasesBytes("+shortName+"_tmpl_phrase_id)", "let plist = tmplPhrases[\""+tmplName+"\"];") data = replace(data, "var plist = GetTmplPhrasesBytes("+shortName+"_tmpl_phrase_id)", "let plist = tmplPhrases[\""+tmplName+"\"];")
//data = replace(data, "var phrases = GetTmplPhrasesBytes("+shortName+"_tmpl_phrase_id)", "let phrases = tmplPhrases[\""+tmplName+"\"];\nconsole.log('tmplName:','"+tmplName+"')\nconsole.log('phrases:', phrases);")
data = replace(data, "var cached_var_", "let cached_var_") data = replace(data, "var cached_var_", "let cached_var_")
data = replace(data, " = []byte(", " = ") data = replace(data, "[]byte(", "")
data = replace(data, " = StringToBytes(", " = ") data = replace(data, "StringToBytes(", "")
// TODO: Format dates properly on the client side
data = replace(data, ".Format(\"2006-01-02 15:04:05\"", "")
data = replace(data, ", 10", "")
data = replace(data, "if ", "if(") data = replace(data, "if ", "if(")
data = replace(data, "return nil", "return out") data = replace(data, "return nil", "return out")
data = replace(data, " )", ")") data = replace(data, " )", ")")

View File

@ -190,6 +190,18 @@ func skipUntilIfExists(tmplData []byte, i int, expects byte) (newI int, hasIt bo
return j, false return j, false
} }
func skipUntilIfExistsOrLine(tmplData []byte, i int, expects byte) (newI int, hasIt bool) {
j := i
for ; j < len(tmplData); j++ {
if tmplData[j] == 10 {
return j, false
} else if tmplData[j] == expects {
return j, true
}
}
return j, false
}
func skipUntilCharsExist(tmplData []byte, i int, expects []byte) (newI int, hasIt bool) { func skipUntilCharsExist(tmplData []byte, i int, expects []byte) (newI int, hasIt bool) {
j := i j := i
expectIndex := 0 expectIndex := 0

View File

@ -17,7 +17,6 @@ type ProfileReply struct {
CreatedBy int CreatedBy int
Group int Group int
CreatedAt time.Time CreatedAt time.Time
RelativeCreatedAt string
LastEdit int LastEdit int
LastEditBy int LastEditBy int
ContentLines int ContentLines int

View File

@ -25,7 +25,6 @@ type ReplyUser struct {
CreatedByName string CreatedByName string
Group int Group int
CreatedAt time.Time CreatedAt time.Time
RelativeCreatedAt string
LastEdit int LastEdit int
LastEditBy int LastEditBy int
Avatar string Avatar string
@ -51,7 +50,6 @@ type Reply struct {
CreatedBy int CreatedBy int
Group int Group int
CreatedAt time.Time CreatedAt time.Time
RelativeCreatedAt string
LastEdit int LastEdit int
LastEditBy int LastEditBy int
ContentLines int ContentLines int

View File

@ -40,5 +40,5 @@ func (store *SQLReplyStore) Create(topic *Topic, content string, ipaddress strin
if err != nil { if err != nil {
return 0, err return 0, err
} }
return int(lastID), topic.AddReply(uid) return int(lastID), topic.AddReply(int(lastID), uid)
} }

View File

@ -341,7 +341,7 @@ func HandleUploadRoute(w http.ResponseWriter, r *http.Request, user User, maxFil
size, unit := ConvertByteUnit(float64(maxFileSize)) size, unit := ConvertByteUnit(float64(maxFileSize))
return CustomError("Your upload is too big. Your files need to be smaller than "+strconv.Itoa(int(size))+unit+".", http.StatusExpectationFailed, "Error", w, r, nil, user) return CustomError("Your upload is too big. Your files need to be smaller than "+strconv.Itoa(int(size))+unit+".", http.StatusExpectationFailed, "Error", w, r, nil, user)
} }
r.Body = http.MaxBytesReader(w, r.Body, int64(maxFileSize)) r.Body = http.MaxBytesReader(w, r.Body, r.ContentLength)
err := r.ParseMultipartForm(int64(Megabyte)) err := r.ParseMultipartForm(int64(Megabyte))
if err != nil { if err != nil {

View File

@ -226,11 +226,12 @@ func CompileTemplates() error {
PollOption{1, "Something"}, PollOption{1, "Something"},
}, VoteCount: 7} }, VoteCount: 7}
avatar, microAvatar := BuildAvatar(62, "") avatar, microAvatar := BuildAvatar(62, "")
topic := TopicUser{1, "blah", "Blah", "Hey there!", 0, false, false, now, RelativeTime(now), now, RelativeTime(now), 0, "", "127.0.0.1", 1, 0, 1, "classname", poll.ID, "weird-data", BuildProfileURL("fake-user", 62), "Fake User", Config.DefaultGroup, avatar, microAvatar, 0, "", "", "", "", "", 58, false} miniAttach := []*MiniAttachment{&MiniAttachment{Path: "/"}}
topic := TopicUser{1, "blah", "Blah", "Hey there!", 0, false, false, now, now, 1, 1, 0, "", "127.0.0.1", 1, 0, 1, 0, "classname", poll.ID, "weird-data", BuildProfileURL("fake-user", 62), "Fake User", Config.DefaultGroup, avatar, microAvatar, 0, "", "", "", "", "", 58, false, miniAttach}
var replyList []ReplyUser var replyList []ReplyUser
// TODO: Do we want the UID on this to be 0? // TODO: Do we want the UID on this to be 0?
avatar, microAvatar = BuildAvatar(0, "") avatar, microAvatar = BuildAvatar(0, "")
replyList = append(replyList, ReplyUser{0, 0, "Yo!", "Yo!", 0, "alice", "Alice", Config.DefaultGroup, now, RelativeTime(now), 0, 0, avatar, microAvatar, "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, "", ""}) replyList = append(replyList, ReplyUser{0, 0, "Yo!", "Yo!", 0, "alice", "Alice", Config.DefaultGroup, now, 0, 0, avatar, microAvatar, "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, "", ""})
var varList = make(map[string]tmpl.VarItem) var varList = make(map[string]tmpl.VarItem)
var compile = func(name string, expects string, expectsInt interface{}) (tmpl string, err error) { var compile = func(name string, expects string, expectsInt interface{}) (tmpl string, err error) {
@ -285,7 +286,7 @@ func CompileTemplates() error {
} }
var topicsList []*TopicsRow var topicsList []*TopicsRow
topicsList = append(topicsList, &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, "Date", user3.ID, 1, "", "127.0.0.1", 1, 0, 1, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"}) topicsList = append(topicsList, &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, user3.ID, 1, 1, "", "127.0.0.1", 1, 0, 1, 1, 0, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"})
header2.Title = "Topic List" header2.Title = "Topic List"
topicListPage := TopicListPage{header, topicsList, forumList, Config.DefaultForum, TopicListSort{"lastupdated", false}, Paginator{[]int{1}, 1, 1}} topicListPage := TopicListPage{header, topicsList, forumList, Config.DefaultForum, TopicListSort{"lastupdated", false}, Paginator{[]int{1}, 1, 1}}
/*topicListTmpl, err := compile("topics", "common.TopicListPage", topicListPage) /*topicListTmpl, err := compile("topics", "common.TopicListPage", topicListPage)
@ -439,7 +440,7 @@ func CompileJSTemplates() error {
// TODO: Fix the import loop so we don't have to use this hack anymore // TODO: Fix the import loop so we don't have to use this hack anymore
c.SetBuildTags("!no_templategen,tmplgentopic") c.SetBuildTags("!no_templategen,tmplgentopic")
var topicsRow = &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, "Date", user3.ID, 1, "", "127.0.0.1", 1, 0, 1, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"} var topicsRow = &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, user3.ID, 1, 1, "", "127.0.0.1", 1, 0, 1, 0, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"}
topicListItemTmpl, err := c.Compile("topics_topic.html", "templates/", "*common.TopicsRow", topicsRow, varList) topicListItemTmpl, err := c.Compile("topics_topic.html", "templates/", "*common.TopicsRow", topicsRow, varList)
if err != nil { if err != nil {
return err return err
@ -450,11 +451,12 @@ func CompileJSTemplates() error {
PollOption{1, "Something"}, PollOption{1, "Something"},
}, VoteCount: 7} }, VoteCount: 7}
avatar, microAvatar := BuildAvatar(62, "") avatar, microAvatar := BuildAvatar(62, "")
topic := TopicUser{1, "blah", "Blah", "Hey there!", 62, false, false, now, RelativeTime(now), now, RelativeTime(now), 0, "", "127.0.0.1", 1, 0, 1, "classname", poll.ID, "weird-data", BuildProfileURL("fake-user", 62), "Fake User", Config.DefaultGroup, avatar, microAvatar, 0, "", "", "", "", "", 58, false} miniAttach := []*MiniAttachment{&MiniAttachment{Path: "/"}}
topic := TopicUser{1, "blah", "Blah", "Hey there!", 62, false, false, now, now, 1, 1, 0, "", "127.0.0.1", 1, 0, 1, 0, "classname", poll.ID, "weird-data", BuildProfileURL("fake-user", 62), "Fake User", Config.DefaultGroup, avatar, microAvatar, 0, "", "", "", "", "", 58, false, miniAttach}
var replyList []ReplyUser var replyList []ReplyUser
// TODO: Do we really want the UID here to be zero? // TODO: Do we really want the UID here to be zero?
avatar, microAvatar = BuildAvatar(0, "") avatar, microAvatar = BuildAvatar(0, "")
replyList = append(replyList, ReplyUser{0, 0, "Yo!", "Yo!", 0, "alice", "Alice", Config.DefaultGroup, now, RelativeTime(now), 0, 0, avatar, microAvatar, "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, "", ""}) replyList = append(replyList, ReplyUser{0, 0, "Yo!", "Yo!", 0, "alice", "Alice", Config.DefaultGroup, now, 0, 0, avatar, microAvatar, "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, "", ""})
varList = make(map[string]tmpl.VarItem) varList = make(map[string]tmpl.VarItem)
header.Title = "Topic Name" header.Title = "Topic Name"
@ -639,10 +641,17 @@ func InitTemplates() error {
if !ok { if !ok {
panic("timeInt is not a time.Time") panic("timeInt is not a time.Time")
} }
//return time.String()
return time.Format("2006-01-02 15:04:05") return time.Format("2006-01-02 15:04:05")
} }
fmap["reltime"] = func(timeInt interface{}) interface{} {
time, ok := timeInt.(time.Time)
if !ok {
panic("timeInt is not a time.Time")
}
return RelativeTime(time)
}
fmap["scope"] = func(name interface{}) interface{} { fmap["scope"] = func(name interface{}) interface{} {
return "" return ""
} }

View File

@ -91,6 +91,7 @@ func NewCTemplateSet() *CTemplateSet {
//"langf":true, //"langf":true,
"level": true, "level": true,
"abstime": true, "abstime": true,
"reltime": true,
"scope": true, "scope": true,
"dyntmpl": true, "dyntmpl": true,
}, },
@ -959,6 +960,16 @@ ArgLoop:
// TODO: Refactor this // TODO: Refactor this
litString(leftParam+".Format(\"2006-01-02 15:04:05\")", false) litString(leftParam+".Format(\"2006-01-02 15:04:05\")", false)
break ArgLoop break ArgLoop
case "reltime":
// TODO: Implement level literals
leftOperand := node.Args[pos+1].String()
if len(leftOperand) == 0 {
panic("The leftoperand for function reltime cannot be left blank")
}
leftParam, _ := c.compileIfVarSub(con, leftOperand)
// TODO: Refactor this
litString("common.RelativeTime("+leftParam+")", false)
break ArgLoop
case "scope": case "scope":
literal = true literal = true
break ArgLoop break ArgLoop

View File

@ -30,16 +30,16 @@ type Topic struct {
IsClosed bool IsClosed bool
Sticky bool Sticky bool
CreatedAt time.Time CreatedAt time.Time
RelativeCreatedAt string
LastReplyAt time.Time LastReplyAt time.Time
RelativeLastReplyAt string LastReplyBy int
//LastReplyBy int LastReplyID int
ParentID int ParentID int
Status string // Deprecated. Marked for removal. Status string // Deprecated. Marked for removal.
IPAddress string IPAddress string
ViewCount int64 ViewCount int64
PostCount int PostCount int
LikeCount int LikeCount int
AttachCount int
ClassName string // CSS Class Name ClassName string // CSS Class Name
Poll int Poll int
Data string // Used for report metadata Data string // Used for report metadata
@ -54,16 +54,16 @@ type TopicUser struct {
IsClosed bool IsClosed bool
Sticky bool Sticky bool
CreatedAt time.Time CreatedAt time.Time
RelativeCreatedAt string
LastReplyAt time.Time LastReplyAt time.Time
RelativeLastReplyAt string LastReplyBy int
//LastReplyBy int LastReplyID int
ParentID int ParentID int
Status string // Deprecated. Marked for removal. Status string // Deprecated. Marked for removal.
IPAddress string IPAddress string
ViewCount int64 ViewCount int64
PostCount int PostCount int
LikeCount int LikeCount int
AttachCount int
ClassName string ClassName string
Poll int Poll int
Data string // Used for report metadata Data string // Used for report metadata
@ -81,6 +81,8 @@ type TopicUser struct {
URLName string URLName string
Level int Level int
Liked bool Liked bool
Attachments []*MiniAttachment
} }
type TopicsRow struct { type TopicsRow struct {
@ -92,16 +94,16 @@ type TopicsRow struct {
IsClosed bool IsClosed bool
Sticky bool Sticky bool
CreatedAt time.Time CreatedAt time.Time
//RelativeCreatedAt string
LastReplyAt time.Time LastReplyAt time.Time
RelativeLastReplyAt string
LastReplyBy int LastReplyBy int
LastReplyID int
ParentID int ParentID int
Status string // Deprecated. Marked for removal. -Is there anything we could use it for? Status string // Deprecated. Marked for removal. -Is there anything we could use it for?
IPAddress string IPAddress string
ViewCount int64 ViewCount int64
PostCount int PostCount int
LikeCount int LikeCount int
AttachCount int
LastPage int LastPage int
ClassName string ClassName string
Data string // Used for report metadata Data string // Used for report metadata
@ -126,10 +128,12 @@ type WsTopicsRow struct {
LastReplyAt time.Time LastReplyAt time.Time
RelativeLastReplyAt string RelativeLastReplyAt string
LastReplyBy int LastReplyBy int
LastReplyID int
ParentID int ParentID int
ViewCount int64 ViewCount int64
PostCount int PostCount int
LikeCount int LikeCount int
AttachCount int
ClassName string ClassName string
Creator *WsJSONUser Creator *WsJSONUser
LastUser *WsJSONUser LastUser *WsJSONUser
@ -137,12 +141,14 @@ type WsTopicsRow struct {
ForumLink string ForumLink string
} }
// TODO: Can we get the client side to render the relative times instead?
func (row *TopicsRow) WebSockets() *WsTopicsRow { func (row *TopicsRow) WebSockets() *WsTopicsRow {
return &WsTopicsRow{row.ID, row.Link, row.Title, row.CreatedBy, row.IsClosed, row.Sticky, row.CreatedAt, row.LastReplyAt, row.RelativeLastReplyAt, row.LastReplyBy, row.ParentID, row.ViewCount, row.PostCount, row.LikeCount, row.ClassName, row.Creator.WebSockets(), row.LastUser.WebSockets(), row.ForumName, row.ForumLink} return &WsTopicsRow{row.ID, row.Link, row.Title, row.CreatedBy, row.IsClosed, row.Sticky, row.CreatedAt, row.LastReplyAt, RelativeTime(row.LastReplyAt), row.LastReplyBy, row.LastReplyID, row.ParentID, row.ViewCount, row.PostCount, row.LikeCount, row.AttachCount, row.ClassName, row.Creator.WebSockets(), row.LastUser.WebSockets(), row.ForumName, row.ForumLink}
} }
type TopicStmts struct { type TopicStmts struct {
addRepliesToTopic *sql.Stmt addReplies *sql.Stmt
updateLastReply *sql.Stmt
lock *sql.Stmt lock *sql.Stmt
unlock *sql.Stmt unlock *sql.Stmt
moveTo *sql.Stmt moveTo *sql.Stmt
@ -154,7 +160,7 @@ type TopicStmts struct {
delete *sql.Stmt delete *sql.Stmt
edit *sql.Stmt edit *sql.Stmt
setPoll *sql.Stmt setPoll *sql.Stmt
createActionReply *sql.Stmt createAction *sql.Stmt
getTopicUser *sql.Stmt // TODO: Can we get rid of this? getTopicUser *sql.Stmt // TODO: Can we get rid of this?
getByReplyID *sql.Stmt getByReplyID *sql.Stmt
@ -165,7 +171,8 @@ var topicStmts TopicStmts
func init() { func init() {
DbInits.Add(func(acc *qgen.Accumulator) error { DbInits.Add(func(acc *qgen.Accumulator) error {
topicStmts = TopicStmts{ topicStmts = TopicStmts{
addRepliesToTopic: acc.Update("topics").Set("postCount = postCount + ?, lastReplyBy = ?, lastReplyAt = UTC_TIMESTAMP()").Where("tid = ?").Prepare(), addReplies: acc.Update("topics").Set("postCount = postCount + ?, lastReplyBy = ?, lastReplyAt = UTC_TIMESTAMP()").Where("tid = ?").Prepare(),
updateLastReply: acc.Update("topics").Set("lastReplyID = ?").Where("lastReplyID > ? AND tid = ?").Prepare(),
lock: acc.Update("topics").Set("is_closed = 1").Where("tid = ?").Prepare(), lock: acc.Update("topics").Set("is_closed = 1").Where("tid = ?").Prepare(),
unlock: acc.Update("topics").Set("is_closed = 0").Where("tid = ?").Prepare(), unlock: acc.Update("topics").Set("is_closed = 0").Where("tid = ?").Prepare(),
moveTo: acc.Update("topics").Set("parentID = ?").Where("tid = ?").Prepare(), moveTo: acc.Update("topics").Set("parentID = ?").Where("tid = ?").Prepare(),
@ -177,9 +184,9 @@ func init() {
delete: acc.Delete("topics").Where("tid = ?").Prepare(), delete: acc.Delete("topics").Where("tid = ?").Prepare(),
edit: acc.Update("topics").Set("title = ?, content = ?, parsed_content = ?").Where("tid = ?").Prepare(), // TODO: Only run the content update bits on non-polls, does this matter? edit: acc.Update("topics").Set("title = ?, content = ?, parsed_content = ?").Where("tid = ?").Prepare(), // TODO: Only run the content update bits on non-polls, does this matter?
setPoll: acc.Update("topics").Set("content = '', parsed_content = '', poll = ?").Where("tid = ? AND poll = 0").Prepare(), setPoll: acc.Update("topics").Set("content = '', parsed_content = '', poll = ?").Where("tid = ? AND poll = 0").Prepare(),
createActionReply: acc.Insert("replies").Columns("tid, actionType, ipaddress, createdBy, createdAt, lastUpdated, content, parsed_content").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),'',''").Prepare(), createAction: acc.Insert("replies").Columns("tid, actionType, ipaddress, createdBy, createdAt, lastUpdated, content, parsed_content").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),'',''").Prepare(),
getTopicUser: acc.SimpleLeftJoin("topics", "users", "topics.title, topics.content, topics.createdBy, topics.createdAt, topics.is_closed, topics.sticky, topics.parentID, topics.ipaddress, topics.views, topics.postCount, topics.likeCount, topics.poll, users.name, users.avatar, users.group, users.url_prefix, users.url_name, users.level", "topics.createdBy = users.uid", "tid = ?", "", ""), getTopicUser: acc.SimpleLeftJoin("topics", "users", "topics.title, topics.content, topics.createdBy, topics.createdAt, topics.lastReplyAt, topics.lastReplyBy, topics.lastReplyID, topics.is_closed, topics.sticky, topics.parentID, topics.ipaddress, topics.views, topics.postCount, topics.likeCount, topics.attachCount,topics.poll, users.name, users.avatar, users.group, users.url_prefix, users.url_name, users.level", "topics.createdBy = users.uid", "tid = ?", "", ""),
getByReplyID: acc.SimpleLeftJoin("replies", "topics", "topics.tid, topics.title, topics.content, topics.createdBy, topics.createdAt, topics.is_closed, topics.sticky, topics.parentID, topics.ipaddress, topics.views, topics.postCount, topics.likeCount, topics.poll, topics.data", "replies.tid = topics.tid", "rid = ?", "", ""), getByReplyID: acc.SimpleLeftJoin("replies", "topics", "topics.tid, topics.title, topics.content, topics.createdBy, topics.createdAt, topics.is_closed, topics.sticky, topics.parentID, topics.ipaddress, topics.views, topics.postCount, topics.likeCount, topics.poll, topics.data", "replies.tid = topics.tid", "rid = ?", "", ""),
} }
return acc.FirstError() return acc.FirstError()
@ -197,8 +204,12 @@ func (topic *Topic) cacheRemove() {
} }
// TODO: Write a test for this // TODO: Write a test for this
func (topic *Topic) AddReply(uid int) (err error) { func (topic *Topic) AddReply(rid int, uid int) (err error) {
_, err = topicStmts.addRepliesToTopic.Exec(1, uid, topic.ID) _, err = topicStmts.addReplies.Exec(1, uid, topic.ID)
if err != nil {
return err
}
_, err = topicStmts.updateLastReply.Exec(rid, rid, topic.ID)
topic.cacheRemove() topic.cacheRemove()
return err return err
} }
@ -314,11 +325,20 @@ func (topic *Topic) SetPoll(pollID int) error {
// TODO: Have this go through the ReplyStore? // TODO: Have this go through the ReplyStore?
func (topic *Topic) CreateActionReply(action string, ipaddress string, uid int) (err error) { func (topic *Topic) CreateActionReply(action string, ipaddress string, uid int) (err error) {
_, err = topicStmts.createActionReply.Exec(topic.ID, action, ipaddress, uid) res, err := topicStmts.createAction.Exec(topic.ID, action, ipaddress, uid)
if err != nil { if err != nil {
return err return err
} }
_, err = topicStmts.addRepliesToTopic.Exec(1, uid, topic.ID) _, err = topicStmts.addReplies.Exec(1, uid, topic.ID)
if err != nil {
return err
}
lid, err := res.LastInsertId()
if err != nil {
return err
}
rid := int(lid)
_, err = topicStmts.updateLastReply.Exec(rid, rid, topic.ID)
topic.cacheRemove() topic.cacheRemove()
// ? - Update the last topic cache for the parent forum? // ? - Update the last topic cache for the parent forum?
return err return err
@ -336,7 +356,7 @@ func (topic *Topic) Copy() Topic {
return *topic return *topic
} }
// TODO: Load LastReplyAt? // TODO: Load LastReplyAt and LastReplyID?
func TopicByReplyID(rid int) (*Topic, error) { func TopicByReplyID(rid int) (*Topic, error) {
topic := Topic{ID: 0} topic := Topic{ID: 0}
err := topicStmts.getByReplyID.QueryRow(rid).Scan(&topic.ID, &topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data) err := topicStmts.getByReplyID.QueryRow(rid).Scan(&topic.ID, &topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data)
@ -376,14 +396,15 @@ func GetTopicUser(user *User, tid int) (tu TopicUser, err error) {
} }
tu = TopicUser{ID: tid} tu = TopicUser{ID: tid}
err = topicStmts.getTopicUser.QueryRow(tid).Scan(&tu.Title, &tu.Content, &tu.CreatedBy, &tu.CreatedAt, &tu.IsClosed, &tu.Sticky, &tu.ParentID, &tu.IPAddress, &tu.ViewCount, &tu.PostCount, &tu.LikeCount, &tu.Poll, &tu.CreatedByName, &tu.Avatar, &tu.Group, &tu.URLPrefix, &tu.URLName, &tu.Level) // TODO: This misses some important bits...
err = topicStmts.getTopicUser.QueryRow(tid).Scan(&tu.Title, &tu.Content, &tu.CreatedBy, &tu.CreatedAt, &tu.LastReplyAt, &tu.LastReplyBy, &tu.LastReplyID, &tu.IsClosed, &tu.Sticky, &tu.ParentID, &tu.IPAddress, &tu.ViewCount, &tu.PostCount, &tu.LikeCount, &tu.AttachCount, &tu.Poll, &tu.CreatedByName, &tu.Avatar, &tu.Group, &tu.URLPrefix, &tu.URLName, &tu.Level)
tu.Avatar, tu.MicroAvatar = BuildAvatar(tu.CreatedBy, tu.Avatar) tu.Avatar, tu.MicroAvatar = BuildAvatar(tu.CreatedBy, tu.Avatar)
tu.Link = BuildTopicURL(NameToSlug(tu.Title), tu.ID) tu.Link = BuildTopicURL(NameToSlug(tu.Title), tu.ID)
tu.UserLink = BuildProfileURL(NameToSlug(tu.CreatedByName), tu.CreatedBy) tu.UserLink = BuildProfileURL(NameToSlug(tu.CreatedByName), tu.CreatedBy)
tu.Tag = Groups.DirtyGet(tu.Group).Tag tu.Tag = Groups.DirtyGet(tu.Group).Tag
if tcache != nil { if tcache != nil {
theTopic := Topic{ID: tu.ID, Link: tu.Link, Title: tu.Title, Content: tu.Content, CreatedBy: tu.CreatedBy, IsClosed: tu.IsClosed, Sticky: tu.Sticky, CreatedAt: tu.CreatedAt, LastReplyAt: tu.LastReplyAt, ParentID: tu.ParentID, IPAddress: tu.IPAddress, ViewCount: tu.ViewCount, PostCount: tu.PostCount, LikeCount: tu.LikeCount, Poll: tu.Poll} theTopic := Topic{ID: tu.ID, Link: tu.Link, Title: tu.Title, Content: tu.Content, CreatedBy: tu.CreatedBy, IsClosed: tu.IsClosed, Sticky: tu.Sticky, CreatedAt: tu.CreatedAt, LastReplyAt: tu.LastReplyAt, LastReplyID: tu.LastReplyID, ParentID: tu.ParentID, IPAddress: tu.IPAddress, ViewCount: tu.ViewCount, PostCount: tu.PostCount, LikeCount: tu.LikeCount, AttachCount: tu.AttachCount, Poll: tu.Poll}
//log.Printf("theTopic: %+v\n", theTopic) //log.Printf("theTopic: %+v\n", theTopic)
_ = tcache.Add(&theTopic) _ = tcache.Add(&theTopic)
} }
@ -409,11 +430,13 @@ func copyTopicToTopicUser(topic *Topic, user *User) (tu TopicUser) {
tu.Sticky = topic.Sticky tu.Sticky = topic.Sticky
tu.CreatedAt = topic.CreatedAt tu.CreatedAt = topic.CreatedAt
tu.LastReplyAt = topic.LastReplyAt tu.LastReplyAt = topic.LastReplyAt
tu.LastReplyBy = topic.LastReplyBy
tu.ParentID = topic.ParentID tu.ParentID = topic.ParentID
tu.IPAddress = topic.IPAddress tu.IPAddress = topic.IPAddress
tu.ViewCount = topic.ViewCount tu.ViewCount = topic.ViewCount
tu.PostCount = topic.PostCount tu.PostCount = topic.PostCount
tu.LikeCount = topic.LikeCount tu.LikeCount = topic.LikeCount
tu.AttachCount = topic.AttachCount
tu.Poll = topic.Poll tu.Poll = topic.Poll
tu.Data = topic.Data tu.Data = topic.Data

View File

@ -211,7 +211,7 @@ func (tList *DefaultTopicList) getList(page int, orderby string, argList []inter
} }
// TODO: Prepare common qlist lengths to speed this up in common cases, prepared statements are prepared lazily anyway, so it probably doesn't matter if we do ten or so // TODO: Prepare common qlist lengths to speed this up in common cases, prepared statements are prepared lazily anyway, so it probably doesn't matter if we do ten or so
stmt, err := qgen.Builder.SimpleSelect("topics", "tid, title, content, createdBy, is_closed, sticky, createdAt, lastReplyAt, lastReplyBy, parentID, views, postCount, likeCount", "parentID IN("+qlist+")", orderq, "?,?") stmt, err := qgen.Builder.SimpleSelect("topics", "tid, title, content, createdBy, is_closed, sticky, createdAt, lastReplyAt, lastReplyBy, lastReplyID, parentID, views, postCount, likeCount", "parentID IN("+qlist+")", orderq, "?,?")
if err != nil { if err != nil {
return nil, Paginator{nil, 1, 1}, err return nil, Paginator{nil, 1, 1}, err
} }
@ -230,7 +230,7 @@ func (tList *DefaultTopicList) getList(page int, orderby string, argList []inter
for rows.Next() { for rows.Next() {
// TODO: Embed Topic structs in TopicsRow to make it easier for us to reuse this work in the topic cache // TODO: Embed Topic structs in TopicsRow to make it easier for us to reuse this work in the topic cache
topicItem := TopicsRow{ID: 0} topicItem := TopicsRow{ID: 0}
err := rows.Scan(&topicItem.ID, &topicItem.Title, &topicItem.Content, &topicItem.CreatedBy, &topicItem.IsClosed, &topicItem.Sticky, &topicItem.CreatedAt, &topicItem.LastReplyAt, &topicItem.LastReplyBy, &topicItem.ParentID, &topicItem.ViewCount, &topicItem.PostCount, &topicItem.LikeCount) err := rows.Scan(&topicItem.ID, &topicItem.Title, &topicItem.Content, &topicItem.CreatedBy, &topicItem.IsClosed, &topicItem.Sticky, &topicItem.CreatedAt, &topicItem.LastReplyAt, &topicItem.LastReplyBy, &topicItem.LastReplyID, &topicItem.ParentID, &topicItem.ViewCount, &topicItem.PostCount, &topicItem.LikeCount)
if err != nil { if err != nil {
return nil, Paginator{nil, 1, 1}, err return nil, Paginator{nil, 1, 1}, err
} }
@ -241,8 +241,6 @@ func (tList *DefaultTopicList) getList(page int, orderby string, argList []inter
topicItem.ForumName = forum.Name topicItem.ForumName = forum.Name
topicItem.ForumLink = forum.Link topicItem.ForumLink = forum.Link
//topicItem.RelativeCreatedAt = RelativeTime(topicItem.CreatedAt)
topicItem.RelativeLastReplyAt = RelativeTime(topicItem.LastReplyAt)
// TODO: Create a specialised function with a bit less overhead for getting the last page for a post count // TODO: Create a specialised function with a bit less overhead for getting the last page for a post count
_, _, lastPage := PageOffset(topicItem.PostCount, 1, Config.ItemsPerPage) _, _, lastPage := PageOffset(topicItem.PostCount, 1, Config.ItemsPerPage)
topicItem.LastPage = lastPage topicItem.LastPage = lastPage

View File

@ -57,7 +57,7 @@ func NewDefaultTopicStore(cache TopicCache) (*DefaultTopicStore, error) {
} }
return &DefaultTopicStore{ return &DefaultTopicStore{
cache: cache, cache: cache,
get: acc.Select("topics").Columns("title, content, createdBy, createdAt, lastReplyAt, is_closed, sticky, parentID, ipaddress, views, postCount, likeCount, poll, data").Where("tid = ?").Prepare(), get: acc.Select("topics").Columns("title, content, createdBy, createdAt, lastReplyAt, lastReplyID, is_closed, sticky, parentID, ipaddress, views, postCount, likeCount, attachCount, poll, data").Where("tid = ?").Prepare(),
exists: acc.Select("topics").Columns("tid").Where("tid = ?").Prepare(), exists: acc.Select("topics").Columns("tid").Where("tid = ?").Prepare(),
topicCount: acc.Count("topics").Prepare(), topicCount: acc.Count("topics").Prepare(),
create: acc.Insert("topics").Columns("parentID, title, content, parsed_content, createdAt, lastReplyAt, lastReplyBy, ipaddress, words, createdBy").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?,?").Prepare(), create: acc.Insert("topics").Columns("parentID, title, content, parsed_content, createdAt, lastReplyAt, lastReplyBy, ipaddress, words, createdBy").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?,?").Prepare(),
@ -71,7 +71,7 @@ func (mts *DefaultTopicStore) DirtyGet(id int) *Topic {
} }
topic = &Topic{ID: id} topic = &Topic{ID: id}
err = mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data) err = mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.LastReplyID, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.AttachCount, &topic.Poll, &topic.Data)
if err == nil { if err == nil {
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id) topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
_ = mts.cache.Add(topic) _ = mts.cache.Add(topic)
@ -88,7 +88,7 @@ func (mts *DefaultTopicStore) Get(id int) (topic *Topic, err error) {
} }
topic = &Topic{ID: id} topic = &Topic{ID: id}
err = mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data) err = mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.LastReplyID, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.AttachCount, &topic.Poll, &topic.Data)
if err == nil { if err == nil {
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id) topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
_ = mts.cache.Add(topic) _ = mts.cache.Add(topic)
@ -99,14 +99,14 @@ func (mts *DefaultTopicStore) Get(id int) (topic *Topic, err error) {
// BypassGet will always bypass the cache and pull the topic directly from the database // BypassGet will always bypass the cache and pull the topic directly from the database
func (mts *DefaultTopicStore) BypassGet(id int) (*Topic, error) { func (mts *DefaultTopicStore) BypassGet(id int) (*Topic, error) {
topic := &Topic{ID: id} topic := &Topic{ID: id}
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data) err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.LastReplyID, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.AttachCount, &topic.Poll, &topic.Data)
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id) topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
return topic, err return topic, err
} }
func (mts *DefaultTopicStore) Reload(id int) error { func (mts *DefaultTopicStore) Reload(id int) error {
topic := &Topic{ID: id} topic := &Topic{ID: id}
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data) err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.LastReplyID, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.AttachCount, &topic.Poll, &topic.Data)
if err == nil { if err == nil {
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id) topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
_ = mts.cache.Set(topic) _ = mts.cache.Set(topic)

View File

@ -202,6 +202,7 @@ func ConvertByteInUnit(bytes float64, unit string) (count float64) {
} }
// TODO: Write a test for this // TODO: Write a test for this
// TODO: Localise this?
func FriendlyUnitToBytes(quantity int, unit string) (bytes int, err error) { func FriendlyUnitToBytes(quantity int, unit string) (bytes int, err error) {
switch unit { switch unit {
case "PB": case "PB":
@ -323,7 +324,7 @@ func WeakPassword(password string, username string, email string) error {
return errors.New("Your password needs to be at-least eight characters long") return errors.New("Your password needs to be at-least eight characters long")
} }
if strings.Contains(lowPassword, "test") || /*strings.Contains(password,"123456") || */ strings.Contains(password, "123") || strings.Contains(lowPassword, "password") || strings.Contains(lowPassword, "qwerty") || strings.Contains(lowPassword, "fuck") || strings.Contains(lowPassword, "love") { if strings.Contains(lowPassword, "test") || strings.Contains(password, "123") || strings.Contains(lowPassword, "password") || strings.Contains(lowPassword, "qwerty") || strings.Contains(lowPassword, "fuck") || strings.Contains(lowPassword, "love") {
return errors.New("You may not have 'test', '123', 'password', 'qwerty', 'love' or 'fuck' in your password") return errors.New("You may not have 'test', '123', 'password', 'qwerty', 'love' or 'fuck' in your password")
} }

View File

@ -45,7 +45,7 @@ func (hub *WsHubImpl) Start() {
AddScheduledSecondTask(hub.Tick) AddScheduledSecondTask(hub.Tick)
} }
// This Tick is seperate from the admin one, as we want to process that in parallel with this due to the blocking calls to gopsutil // This Tick is separate from the admin one, as we want to process that in parallel with this due to the blocking calls to gopsutil
func (hub *WsHubImpl) Tick() error { func (hub *WsHubImpl) Tick() error {
// Don't waste CPU time if nothing has happened // Don't waste CPU time if nothing has happened
// TODO: Get a topic list method which strips stickies? // TODO: Get a topic list method which strips stickies?

View File

@ -2,8 +2,6 @@ echo "Updating the dependencies"
go get go get
echo "Updating Gosora" echo "Updating Gosora"
rm ./schema/lastSchema.json
cp ./schema/schema.json ./schema/lastSchema.json
git stash git stash
git pull origin master git pull origin master
git stash apply git stash apply

View File

@ -1,4 +1,3 @@
echo "Building the patcher" echo "Building the patcher"
cp ./schema/schema.json ./schema/lastSchema.json
go generate go generate
go build -o Patcher "./patcher" go build -o Patcher "./patcher"

View File

@ -8,10 +8,6 @@ if %errorlevel% neq 0 (
) )
echo Updating Gosora echo Updating Gosora
cd schema
del /Q lastSchema.json
copy schema.json lastSchema.json
cd ..
git stash git stash
if %errorlevel% neq 0 ( if %errorlevel% neq 0 (
pause pause

View File

@ -4,7 +4,7 @@ The update system is currently under development, but you can run `dev-update.ba
If you run into any issues doing so, please open an issue: https://github.com/Azareal/Gosora/issues/new If you run into any issues doing so, please open an issue: https://github.com/Azareal/Gosora/issues/new
If you want to manually patch Gosora rather than relying on the above scripts to do it, you'll first have to create a copy of `./schema/schema.json` named `./schema/lastSchema.json`, and then, you'll overwrite the files with the new ones with `git pull origin master`. If you want to manually patch Gosora rather than relying on the above scripts to do it, you'll first want to save your changes with `git stash`, and then, you'll overwrite the files with the new ones with `git pull origin master`, and then, you can re-apply your custom changes with `git stash apply`
After that, you'll need to run `go build ./patcher`. After that, you'll need to run `go build ./patcher`.
@ -16,14 +16,9 @@ The update system is currently under development, but you can run `dev-update-li
If you run into any issues doing so, please open an issue: https://github.com/Azareal/Gosora/issues/new If you run into any issues doing so, please open an issue: https://github.com/Azareal/Gosora/issues/new
If you want to manually patch Gosora rather than relying on the above scripts to do it, you'll first have to create a copy of `./schema/schema.json` named `./schema/lastSchema.json`, and then, you'll overwrite the files with the new ones with `git pull origin master`. If you want to manually patch Gosora rather than relying on the above scripts to do it, you'll first want to save your changes with `git stash`, and then, you'll overwrite the files with the new ones with `git pull origin master`, and then, you'll re-apply your changes with `git stash apply`.
After that, you'll need to run the following code block: After that, you'll need to run `go build -o Patcher "./patcher"`
```
cd ./patcher
go build -o Patcher
mv ./Patcher ..
```
Once you've done that, you just need to run `./Patcher` to apply the latest patches to the database, etc. Once you've done that, you just need to run `./Patcher` to apply the latest patches to the database, etc.
@ -46,14 +41,9 @@ Replace that name and email with whatever you like. This name and email only app
If you get an access denied error, then you might need to run `chown -R gosora /home/gosora` and `chgrp -R www-data /home/gosora` to fix the ownership of the files. If you get an access denied error, then you might need to run `chown -R gosora /home/gosora` and `chgrp -R www-data /home/gosora` to fix the ownership of the files.
If you want to manually patch Gosora rather than relying on the above scripts to do it, you'll first have to create a copy of `./schema/schema.json` named `./schema/lastSchema.json`, and then, you'll overwrite the files with the new ones with `git pull origin master`. If you want to manually patch Gosora rather than relying on the above scripts to do it, you'll first want to save your changes with `git stash`, and then, you'll overwrite the files with the new ones with `git pull origin master`, and then, you'll re-apply your changes with `git stash apply`.
After that, you'll need to run: After that, you'll need to run `go build -o Patcher "./patcher"`
```
cd ./patcher
go build -o Patcher
mv ./Patcher ..
```
Once you've done that, you just need to run `./Patcher` to apply the latest patches to the database, etc. Once you've done that, you just need to run `./Patcher` to apply the latest patches to the database, etc.

View File

@ -128,6 +128,8 @@ var RouteMap = map[string]interface{}{
"routes.UnlockTopicSubmit": routes.UnlockTopicSubmit, "routes.UnlockTopicSubmit": routes.UnlockTopicSubmit,
"routes.MoveTopicSubmit": routes.MoveTopicSubmit, "routes.MoveTopicSubmit": routes.MoveTopicSubmit,
"routes.LikeTopicSubmit": routes.LikeTopicSubmit, "routes.LikeTopicSubmit": routes.LikeTopicSubmit,
"routes.AddAttachToTopicSubmit": routes.AddAttachToTopicSubmit,
"routes.RemoveAttachFromTopicSubmit": routes.RemoveAttachFromTopicSubmit,
"routes.ViewTopic": routes.ViewTopic, "routes.ViewTopic": routes.ViewTopic,
"routes.CreateReplySubmit": routes.CreateReplySubmit, "routes.CreateReplySubmit": routes.CreateReplySubmit,
"routes.ReplyEditSubmit": routes.ReplyEditSubmit, "routes.ReplyEditSubmit": routes.ReplyEditSubmit,
@ -261,29 +263,31 @@ var routeMapEnum = map[string]int{
"routes.UnlockTopicSubmit": 103, "routes.UnlockTopicSubmit": 103,
"routes.MoveTopicSubmit": 104, "routes.MoveTopicSubmit": 104,
"routes.LikeTopicSubmit": 105, "routes.LikeTopicSubmit": 105,
"routes.ViewTopic": 106, "routes.AddAttachToTopicSubmit": 106,
"routes.CreateReplySubmit": 107, "routes.RemoveAttachFromTopicSubmit": 107,
"routes.ReplyEditSubmit": 108, "routes.ViewTopic": 108,
"routes.ReplyDeleteSubmit": 109, "routes.CreateReplySubmit": 109,
"routes.ReplyLikeSubmit": 110, "routes.ReplyEditSubmit": 110,
"routes.ProfileReplyCreateSubmit": 111, "routes.ReplyDeleteSubmit": 111,
"routes.ProfileReplyEditSubmit": 112, "routes.ReplyLikeSubmit": 112,
"routes.ProfileReplyDeleteSubmit": 113, "routes.ProfileReplyCreateSubmit": 113,
"routes.PollVote": 114, "routes.ProfileReplyEditSubmit": 114,
"routes.PollResults": 115, "routes.ProfileReplyDeleteSubmit": 115,
"routes.AccountLogin": 116, "routes.PollVote": 116,
"routes.AccountRegister": 117, "routes.PollResults": 117,
"routes.AccountLogout": 118, "routes.AccountLogin": 118,
"routes.AccountLoginSubmit": 119, "routes.AccountRegister": 119,
"routes.AccountLoginMFAVerify": 120, "routes.AccountLogout": 120,
"routes.AccountLoginMFAVerifySubmit": 121, "routes.AccountLoginSubmit": 121,
"routes.AccountRegisterSubmit": 122, "routes.AccountLoginMFAVerify": 122,
"routes.DynamicRoute": 123, "routes.AccountLoginMFAVerifySubmit": 123,
"routes.UploadedFile": 124, "routes.AccountRegisterSubmit": 124,
"routes.StaticFile": 125, "routes.DynamicRoute": 125,
"routes.RobotsTxt": 126, "routes.UploadedFile": 126,
"routes.SitemapXml": 127, "routes.StaticFile": 127,
"routes.BadRoute": 128, "routes.RobotsTxt": 128,
"routes.SitemapXml": 129,
"routes.BadRoute": 130,
} }
var reverseRouteMapEnum = map[int]string{ var reverseRouteMapEnum = map[int]string{
0: "routes.Overview", 0: "routes.Overview",
@ -392,29 +396,31 @@ var reverseRouteMapEnum = map[int]string{
103: "routes.UnlockTopicSubmit", 103: "routes.UnlockTopicSubmit",
104: "routes.MoveTopicSubmit", 104: "routes.MoveTopicSubmit",
105: "routes.LikeTopicSubmit", 105: "routes.LikeTopicSubmit",
106: "routes.ViewTopic", 106: "routes.AddAttachToTopicSubmit",
107: "routes.CreateReplySubmit", 107: "routes.RemoveAttachFromTopicSubmit",
108: "routes.ReplyEditSubmit", 108: "routes.ViewTopic",
109: "routes.ReplyDeleteSubmit", 109: "routes.CreateReplySubmit",
110: "routes.ReplyLikeSubmit", 110: "routes.ReplyEditSubmit",
111: "routes.ProfileReplyCreateSubmit", 111: "routes.ReplyDeleteSubmit",
112: "routes.ProfileReplyEditSubmit", 112: "routes.ReplyLikeSubmit",
113: "routes.ProfileReplyDeleteSubmit", 113: "routes.ProfileReplyCreateSubmit",
114: "routes.PollVote", 114: "routes.ProfileReplyEditSubmit",
115: "routes.PollResults", 115: "routes.ProfileReplyDeleteSubmit",
116: "routes.AccountLogin", 116: "routes.PollVote",
117: "routes.AccountRegister", 117: "routes.PollResults",
118: "routes.AccountLogout", 118: "routes.AccountLogin",
119: "routes.AccountLoginSubmit", 119: "routes.AccountRegister",
120: "routes.AccountLoginMFAVerify", 120: "routes.AccountLogout",
121: "routes.AccountLoginMFAVerifySubmit", 121: "routes.AccountLoginSubmit",
122: "routes.AccountRegisterSubmit", 122: "routes.AccountLoginMFAVerify",
123: "routes.DynamicRoute", 123: "routes.AccountLoginMFAVerifySubmit",
124: "routes.UploadedFile", 124: "routes.AccountRegisterSubmit",
125: "routes.StaticFile", 125: "routes.DynamicRoute",
126: "routes.RobotsTxt", 126: "routes.UploadedFile",
127: "routes.SitemapXml", 127: "routes.StaticFile",
128: "routes.BadRoute", 128: "routes.RobotsTxt",
129: "routes.SitemapXml",
130: "routes.BadRoute",
} }
var osMapEnum = map[string]int{ var osMapEnum = map[string]int{
"unknown": 0, "unknown": 0,
@ -705,7 +711,7 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
counters.GlobalViewCounter.Bump() counters.GlobalViewCounter.Bump()
if prefix == "/static" { if prefix == "/static" {
counters.RouteViewCounter.Bump(125) counters.RouteViewCounter.Bump(127)
req.URL.Path += extraData req.URL.Path += extraData
routes.StaticFile(w, req) routes.StaticFile(w, req)
return return
@ -1780,15 +1786,40 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
err = common.ParseForm(w,req,user) counters.RouteViewCounter.Bump(105)
err = routes.LikeTopicSubmit(w,req,user,extraData)
case "/topic/attach/add/submit/":
err = common.MemberOnly(w,req,user)
if err != nil {
return err
}
err = common.HandleUploadRoute(w,req,user,int(common.Config.MaxRequestSize))
if err != nil {
return err
}
err = common.NoUploadSessionMismatch(w,req,user)
if err != nil { if err != nil {
return err return err
} }
counters.RouteViewCounter.Bump(105)
err = routes.LikeTopicSubmit(w,req,user,extraData)
default:
counters.RouteViewCounter.Bump(106) counters.RouteViewCounter.Bump(106)
err = routes.AddAttachToTopicSubmit(w,req,user,extraData)
case "/topic/attach/remove/submit/":
err = common.NoSessionMismatch(w,req,user)
if err != nil {
return err
}
err = common.MemberOnly(w,req,user)
if err != nil {
return err
}
counters.RouteViewCounter.Bump(107)
err = routes.RemoveAttachFromTopicSubmit(w,req,user,extraData)
default:
counters.RouteViewCounter.Bump(108)
head, err := common.UserCheck(w,req,&user) head, err := common.UserCheck(w,req,&user)
if err != nil { if err != nil {
return err return err
@ -1812,7 +1843,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
counters.RouteViewCounter.Bump(107) counters.RouteViewCounter.Bump(109)
err = routes.CreateReplySubmit(w,req,user) err = routes.CreateReplySubmit(w,req,user)
case "/reply/edit/submit/": case "/reply/edit/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
@ -1825,7 +1856,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
counters.RouteViewCounter.Bump(108) counters.RouteViewCounter.Bump(110)
err = routes.ReplyEditSubmit(w,req,user,extraData) err = routes.ReplyEditSubmit(w,req,user,extraData)
case "/reply/delete/submit/": case "/reply/delete/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
@ -1838,7 +1869,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
counters.RouteViewCounter.Bump(109) counters.RouteViewCounter.Bump(111)
err = routes.ReplyDeleteSubmit(w,req,user,extraData) err = routes.ReplyDeleteSubmit(w,req,user,extraData)
case "/reply/like/submit/": case "/reply/like/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
@ -1851,12 +1882,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
err = common.ParseForm(w,req,user) counters.RouteViewCounter.Bump(112)
if err != nil {
return err
}
counters.RouteViewCounter.Bump(110)
err = routes.ReplyLikeSubmit(w,req,user,extraData) err = routes.ReplyLikeSubmit(w,req,user,extraData)
} }
case "/profile": case "/profile":
@ -1872,7 +1898,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
counters.RouteViewCounter.Bump(111) counters.RouteViewCounter.Bump(113)
err = routes.ProfileReplyCreateSubmit(w,req,user) err = routes.ProfileReplyCreateSubmit(w,req,user)
case "/profile/reply/edit/submit/": case "/profile/reply/edit/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
@ -1885,7 +1911,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
counters.RouteViewCounter.Bump(112) counters.RouteViewCounter.Bump(114)
err = routes.ProfileReplyEditSubmit(w,req,user,extraData) err = routes.ProfileReplyEditSubmit(w,req,user,extraData)
case "/profile/reply/delete/submit/": case "/profile/reply/delete/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
@ -1898,7 +1924,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
counters.RouteViewCounter.Bump(113) counters.RouteViewCounter.Bump(115)
err = routes.ProfileReplyDeleteSubmit(w,req,user,extraData) err = routes.ProfileReplyDeleteSubmit(w,req,user,extraData)
} }
case "/poll": case "/poll":
@ -1914,23 +1940,23 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
counters.RouteViewCounter.Bump(114) counters.RouteViewCounter.Bump(116)
err = routes.PollVote(w,req,user,extraData) err = routes.PollVote(w,req,user,extraData)
case "/poll/results/": case "/poll/results/":
counters.RouteViewCounter.Bump(115) counters.RouteViewCounter.Bump(117)
err = routes.PollResults(w,req,user,extraData) err = routes.PollResults(w,req,user,extraData)
} }
case "/accounts": case "/accounts":
switch(req.URL.Path) { switch(req.URL.Path) {
case "/accounts/login/": case "/accounts/login/":
counters.RouteViewCounter.Bump(116) counters.RouteViewCounter.Bump(118)
head, err := common.UserCheck(w,req,&user) head, err := common.UserCheck(w,req,&user)
if err != nil { if err != nil {
return err return err
} }
err = routes.AccountLogin(w,req,user,head) err = routes.AccountLogin(w,req,user,head)
case "/accounts/create/": case "/accounts/create/":
counters.RouteViewCounter.Bump(117) counters.RouteViewCounter.Bump(119)
head, err := common.UserCheck(w,req,&user) head, err := common.UserCheck(w,req,&user)
if err != nil { if err != nil {
return err return err
@ -1947,7 +1973,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
counters.RouteViewCounter.Bump(118) counters.RouteViewCounter.Bump(120)
err = routes.AccountLogout(w,req,user) err = routes.AccountLogout(w,req,user)
case "/accounts/login/submit/": case "/accounts/login/submit/":
err = common.ParseForm(w,req,user) err = common.ParseForm(w,req,user)
@ -1955,10 +1981,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
counters.RouteViewCounter.Bump(119) counters.RouteViewCounter.Bump(121)
err = routes.AccountLoginSubmit(w,req,user) err = routes.AccountLoginSubmit(w,req,user)
case "/accounts/mfa_verify/": case "/accounts/mfa_verify/":
counters.RouteViewCounter.Bump(120) counters.RouteViewCounter.Bump(122)
head, err := common.UserCheck(w,req,&user) head, err := common.UserCheck(w,req,&user)
if err != nil { if err != nil {
return err return err
@ -1970,7 +1996,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
counters.RouteViewCounter.Bump(121) counters.RouteViewCounter.Bump(123)
err = routes.AccountLoginMFAVerifySubmit(w,req,user) err = routes.AccountLoginMFAVerifySubmit(w,req,user)
case "/accounts/create/submit/": case "/accounts/create/submit/":
err = common.ParseForm(w,req,user) err = common.ParseForm(w,req,user)
@ -1978,7 +2004,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
counters.RouteViewCounter.Bump(122) counters.RouteViewCounter.Bump(124)
err = routes.AccountRegisterSubmit(w,req,user) err = routes.AccountRegisterSubmit(w,req,user)
} }
/*case "/sitemaps": // TODO: Count these views /*case "/sitemaps": // TODO: Count these views
@ -1994,7 +2020,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
w.Header().Del("Content-Type") w.Header().Del("Content-Type")
w.Header().Del("Content-Encoding") w.Header().Del("Content-Encoding")
} }
counters.RouteViewCounter.Bump(124) counters.RouteViewCounter.Bump(126)
req.URL.Path += extraData req.URL.Path += extraData
// TODO: Find a way to propagate errors up from this? // TODO: Find a way to propagate errors up from this?
r.UploadHandler(w,req) // TODO: Count these views r.UploadHandler(w,req) // TODO: Count these views
@ -2004,10 +2030,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
// TODO: Add support for favicons and robots.txt files // TODO: Add support for favicons and robots.txt files
switch(extraData) { switch(extraData) {
case "robots.txt": case "robots.txt":
counters.RouteViewCounter.Bump(126) counters.RouteViewCounter.Bump(128)
return routes.RobotsTxt(w,req) return routes.RobotsTxt(w,req)
/*case "sitemap.xml": /*case "sitemap.xml":
counters.RouteViewCounter.Bump(127) counters.RouteViewCounter.Bump(129)
return routes.SitemapXml(w,req)*/ return routes.SitemapXml(w,req)*/
} }
return common.NotFound(w,req,nil) return common.NotFound(w,req,nil)
@ -2018,7 +2044,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
r.RUnlock() r.RUnlock()
if ok { if ok {
counters.RouteViewCounter.Bump(123) // TODO: Be more specific about *which* dynamic route it is counters.RouteViewCounter.Bump(125) // TODO: Be more specific about *which* dynamic route it is
req.URL.Path += extraData req.URL.Path += extraData
return handle(w,req,user) return handle(w,req,user)
} }
@ -2029,7 +2055,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
} else { } else {
r.DumpRequest(req,"Bad Route") r.DumpRequest(req,"Bad Route")
} }
counters.RouteViewCounter.Bump(128) counters.RouteViewCounter.Bump(130)
return common.NotFound(w,req,nil) return common.NotFound(w,req,nil)
} }
return err return err

View File

@ -92,7 +92,7 @@ func userStoreTest(t *testing.T, newUserID int) {
expect(t, cond, prefix+" "+midfix+" "+suffix) expect(t, cond, prefix+" "+midfix+" "+suffix)
} }
// TODO: Add email checks too? Do them seperately? // TODO: Add email checks too? Do them separately?
var expectUser = func(user *common.User, uid int, name string, group int, super bool, admin bool, mod bool, banned bool) { var expectUser = func(user *common.User, uid int, name string, group int, super bool, admin bool, mod bool, banned bool) {
expect(t, user.ID == uid, fmt.Sprintf("user.ID should be %d. Got '%d' instead.", uid, user.ID)) expect(t, user.ID == uid, fmt.Sprintf("user.ID should be %d. Got '%d' instead.", uid, user.ID))
expect(t, user.Name == name, fmt.Sprintf("user.Name should be '%s', not '%s'", name, user.Name)) expect(t, user.Name == name, fmt.Sprintf("user.Name should be '%s', not '%s'", name, user.Name))

View File

@ -2,6 +2,7 @@ package main
import ( import (
"bufio" "bufio"
"database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
@ -89,22 +90,31 @@ type SchemaFile struct {
MinVersion string // TODO: Minimum version of Gosora to jump to this version, might be tricky as we don't store this in the schema file, maybe store it in the database MinVersion string // TODO: Minimum version of Gosora to jump to this version, might be tricky as we don't store this in the schema file, maybe store it in the database
} }
func patcher(scanner *bufio.Scanner) error { func loadSchema() (schemaFile SchemaFile, err error) {
fmt.Println("Loading the schema file") fmt.Println("Loading the schema file")
data, err := ioutil.ReadFile("./schema/lastSchema.json") data, err := ioutil.ReadFile("./schema/lastSchema.json")
if err != nil { if err != nil {
return err return schemaFile, err
} }
var schemaFile SchemaFile
err = json.Unmarshal(data, &schemaFile) err = json.Unmarshal(data, &schemaFile)
return schemaFile, err
}
func patcher(scanner *bufio.Scanner) error {
var dbVersion int
err := qgen.NewAcc().Select("updates").Columns("dbVersion").QueryRow().Scan(&dbVersion)
if err == sql.ErrNoRows {
schemaFile, err := loadSchema()
if err != nil { if err != nil {
return err return err
} }
dbVersion, err := strconv.Atoi(schemaFile.DBVersion) dbVersion, err = strconv.Atoi(schemaFile.DBVersion)
if err != nil { if err != nil {
return err return err
} }
} else if err != nil {
return err
}
fmt.Println("Applying the patches") fmt.Println("Applying the patches")
var pslice = make([]func(*bufio.Scanner) error, len(patches)) var pslice = make([]func(*bufio.Scanner) error, len(patches))
@ -113,6 +123,7 @@ func patcher(scanner *bufio.Scanner) error {
} }
// Run the queued up patches // Run the queued up patches
var patched int
for index, patch := range pslice { for index, patch := range pslice {
if dbVersion > index { if dbVersion > index {
continue continue
@ -121,6 +132,14 @@ func patcher(scanner *bufio.Scanner) error {
if err != nil { if err != nil {
return err return err
} }
patched++
}
if patched > 0 {
_, err := qgen.NewAcc().Update("updates").Set("dbVersion = ?").Exec(len(pslice))
if err != nil {
return err
}
} }
return nil return nil

View File

@ -7,6 +7,9 @@ import (
"github.com/Azareal/Gosora/query_gen" "github.com/Azareal/Gosora/query_gen"
) )
type tblColumn = qgen.DBTableColumn
type tblKey = qgen.DBTableKey
func init() { func init() {
addPatch(0, patch0) addPatch(0, patch0)
addPatch(1, patch1) addPatch(1, patch1)
@ -18,6 +21,7 @@ func init() {
addPatch(7, patch7) addPatch(7, patch7)
addPatch(8, patch8) addPatch(8, patch8)
addPatch(9, patch9) addPatch(9, patch9)
addPatch(10, patch10)
} }
func patch0(scanner *bufio.Scanner) (err error) { func patch0(scanner *bufio.Scanner) (err error) {
@ -32,11 +36,11 @@ func patch0(scanner *bufio.Scanner) (err error) {
} }
err = execStmt(qgen.Builder.CreateTable("menus", "", "", err = execStmt(qgen.Builder.CreateTable("menus", "", "",
[]qgen.DBTableColumn{ []tblColumn{
qgen.DBTableColumn{"mid", "int", 0, false, true, ""}, tblColumn{"mid", "int", 0, false, true, ""},
}, },
[]qgen.DBTableKey{ []tblKey{
qgen.DBTableKey{"mid", "primary"}, tblKey{"mid", "primary"},
}, },
)) ))
if err != nil { if err != nil {
@ -44,26 +48,26 @@ func patch0(scanner *bufio.Scanner) (err error) {
} }
err = execStmt(qgen.Builder.CreateTable("menu_items", "", "", err = execStmt(qgen.Builder.CreateTable("menu_items", "", "",
[]qgen.DBTableColumn{ []tblColumn{
qgen.DBTableColumn{"miid", "int", 0, false, true, ""}, tblColumn{"miid", "int", 0, false, true, ""},
qgen.DBTableColumn{"mid", "int", 0, false, false, ""}, tblColumn{"mid", "int", 0, false, false, ""},
qgen.DBTableColumn{"name", "varchar", 200, false, false, ""}, tblColumn{"name", "varchar", 200, false, false, ""},
qgen.DBTableColumn{"htmlID", "varchar", 200, false, false, "''"}, tblColumn{"htmlID", "varchar", 200, false, false, "''"},
qgen.DBTableColumn{"cssClass", "varchar", 200, false, false, "''"}, tblColumn{"cssClass", "varchar", 200, false, false, "''"},
qgen.DBTableColumn{"position", "varchar", 100, false, false, ""}, tblColumn{"position", "varchar", 100, false, false, ""},
qgen.DBTableColumn{"path", "varchar", 200, false, false, "''"}, tblColumn{"path", "varchar", 200, false, false, "''"},
qgen.DBTableColumn{"aria", "varchar", 200, false, false, "''"}, tblColumn{"aria", "varchar", 200, false, false, "''"},
qgen.DBTableColumn{"tooltip", "varchar", 200, false, false, "''"}, tblColumn{"tooltip", "varchar", 200, false, false, "''"},
qgen.DBTableColumn{"tmplName", "varchar", 200, false, false, "''"}, tblColumn{"tmplName", "varchar", 200, false, false, "''"},
qgen.DBTableColumn{"order", "int", 0, false, false, "0"}, tblColumn{"order", "int", 0, false, false, "0"},
qgen.DBTableColumn{"guestOnly", "boolean", 0, false, false, "0"}, tblColumn{"guestOnly", "boolean", 0, false, false, "0"},
qgen.DBTableColumn{"memberOnly", "boolean", 0, false, false, "0"}, tblColumn{"memberOnly", "boolean", 0, false, false, "0"},
qgen.DBTableColumn{"staffOnly", "boolean", 0, false, false, "0"}, tblColumn{"staffOnly", "boolean", 0, false, false, "0"},
qgen.DBTableColumn{"adminOnly", "boolean", 0, false, false, "0"}, tblColumn{"adminOnly", "boolean", 0, false, false, "0"},
}, },
[]qgen.DBTableKey{ []tblKey{
qgen.DBTableKey{"miid", "primary"}, tblKey{"miid", "primary"},
}, },
)) ))
if err != nil { if err != nil {
@ -159,25 +163,20 @@ func patch2(scanner *bufio.Scanner) error {
} }
func patch3(scanner *bufio.Scanner) error { func patch3(scanner *bufio.Scanner) error {
err := execStmt(qgen.Builder.CreateTable("registration_logs", "", "", return execStmt(qgen.Builder.CreateTable("registration_logs", "", "",
[]qgen.DBTableColumn{ []tblColumn{
qgen.DBTableColumn{"rlid", "int", 0, false, true, ""}, tblColumn{"rlid", "int", 0, false, true, ""},
qgen.DBTableColumn{"username", "varchar", 100, false, false, ""}, tblColumn{"username", "varchar", 100, false, false, ""},
qgen.DBTableColumn{"email", "varchar", 100, false, false, ""}, tblColumn{"email", "varchar", 100, false, false, ""},
qgen.DBTableColumn{"failureReason", "varchar", 100, false, false, ""}, tblColumn{"failureReason", "varchar", 100, false, false, ""},
qgen.DBTableColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed? tblColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed?
qgen.DBTableColumn{"ipaddress", "varchar", 200, false, false, ""}, tblColumn{"ipaddress", "varchar", 200, false, false, ""},
qgen.DBTableColumn{"doneAt", "createdAt", 0, false, false, ""}, tblColumn{"doneAt", "createdAt", 0, false, false, ""},
}, },
[]qgen.DBTableKey{ []tblKey{
qgen.DBTableKey{"rlid", "primary"}, tblKey{"rlid", "primary"},
}, },
)) ))
if err != nil {
return err
}
return nil
} }
func patch4(scanner *bufio.Scanner) error { func patch4(scanner *bufio.Scanner) error {
@ -229,16 +228,16 @@ func patch4(scanner *bufio.Scanner) error {
} }
err = execStmt(qgen.Builder.CreateTable("pages", "utf8mb4", "utf8mb4_general_ci", err = execStmt(qgen.Builder.CreateTable("pages", "utf8mb4", "utf8mb4_general_ci",
[]qgen.DBTableColumn{ []tblColumn{
qgen.DBTableColumn{"pid", "int", 0, false, true, ""}, tblColumn{"pid", "int", 0, false, true, ""},
qgen.DBTableColumn{"name", "varchar", 200, false, false, ""}, tblColumn{"name", "varchar", 200, false, false, ""},
qgen.DBTableColumn{"title", "varchar", 200, false, false, ""}, tblColumn{"title", "varchar", 200, false, false, ""},
qgen.DBTableColumn{"body", "text", 0, false, false, ""}, tblColumn{"body", "text", 0, false, false, ""},
qgen.DBTableColumn{"allowedGroups", "text", 0, false, false, ""}, tblColumn{"allowedGroups", "text", 0, false, false, ""},
qgen.DBTableColumn{"menuID", "int", 0, false, false, "-1"}, tblColumn{"menuID", "int", 0, false, false, "-1"},
}, },
[]qgen.DBTableKey{ []tblKey{
qgen.DBTableKey{"pid", "primary"}, tblKey{"pid", "primary"},
}, },
)) ))
if err != nil { if err != nil {
@ -267,21 +266,21 @@ func patch5(scanner *bufio.Scanner) error {
} }
err = execStmt(qgen.Builder.CreateTable("users_2fa_keys", "utf8mb4", "utf8mb4_general_ci", err = execStmt(qgen.Builder.CreateTable("users_2fa_keys", "utf8mb4", "utf8mb4_general_ci",
[]qgen.DBTableColumn{ []tblColumn{
qgen.DBTableColumn{"uid", "int", 0, false, false, ""}, tblColumn{"uid", "int", 0, false, false, ""},
qgen.DBTableColumn{"secret", "varchar", 100, false, false, ""}, tblColumn{"secret", "varchar", 100, false, false, ""},
qgen.DBTableColumn{"scratch1", "varchar", 50, false, false, ""}, tblColumn{"scratch1", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch2", "varchar", 50, false, false, ""}, tblColumn{"scratch2", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch3", "varchar", 50, false, false, ""}, tblColumn{"scratch3", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch4", "varchar", 50, false, false, ""}, tblColumn{"scratch4", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch5", "varchar", 50, false, false, ""}, tblColumn{"scratch5", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch6", "varchar", 50, false, false, ""}, tblColumn{"scratch6", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch7", "varchar", 50, false, false, ""}, tblColumn{"scratch7", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch8", "varchar", 50, false, false, ""}, tblColumn{"scratch8", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"createdAt", "createdAt", 0, false, false, ""}, tblColumn{"createdAt", "createdAt", 0, false, false, ""},
}, },
[]qgen.DBTableKey{ []tblKey{
qgen.DBTableKey{"uid", "primary"}, tblKey{"uid", "primary"},
}, },
)) ))
if err != nil { if err != nil {
@ -292,28 +291,18 @@ func patch5(scanner *bufio.Scanner) error {
} }
func patch6(scanner *bufio.Scanner) error { func patch6(scanner *bufio.Scanner) error {
err := execStmt(qgen.Builder.SimpleInsert("settings", "name, content, type", "'rapid_loading','1','bool'")) return execStmt(qgen.Builder.SimpleInsert("settings", "name, content, type", "'rapid_loading','1','bool'"))
if err != nil {
return err
}
return nil
} }
func patch7(scanner *bufio.Scanner) error { func patch7(scanner *bufio.Scanner) error {
err := execStmt(qgen.Builder.CreateTable("users_avatar_queue", "", "", return execStmt(qgen.Builder.CreateTable("users_avatar_queue", "", "",
[]qgen.DBTableColumn{ []tblColumn{
qgen.DBTableColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key tblColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key
}, },
[]qgen.DBTableKey{ []tblKey{
qgen.DBTableKey{"uid", "primary"}, tblKey{"uid", "primary"},
}, },
)) ))
if err != nil {
return err
}
return nil
} }
func renameRoutes(routes map[string]string) error { func renameRoutes(routes map[string]string) error {
@ -369,17 +358,12 @@ func patch8(scanner *bufio.Scanner) error {
if err != nil { if err != nil {
return err return err
} }
err = execStmt(qgen.Builder.CreateTable("updates", "", "", return execStmt(qgen.Builder.CreateTable("updates", "", "",
[]qgen.DBTableColumn{ []tblColumn{
qgen.DBTableColumn{"dbVersion", "int", 0, false, false, "0"}, tblColumn{"dbVersion", "int", 0, false, false, "0"},
}, },
[]qgen.DBTableKey{}, []tblKey{},
)) ))
if err != nil {
return err
}
return nil
} }
func patch9(scanner *bufio.Scanner) error { func patch9(scanner *bufio.Scanner) error {
@ -389,21 +373,60 @@ func patch9(scanner *bufio.Scanner) error {
return err return err
} }
err = execStmt(qgen.Builder.CreateTable("login_logs", "", "", return execStmt(qgen.Builder.CreateTable("login_logs", "", "",
[]qgen.DBTableColumn{ []tblColumn{
qgen.DBTableColumn{"lid", "int", 0, false, true, ""}, tblColumn{"lid", "int", 0, false, true, ""},
qgen.DBTableColumn{"uid", "int", 0, false, false, ""}, tblColumn{"uid", "int", 0, false, false, ""},
qgen.DBTableColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed? tblColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed?
qgen.DBTableColumn{"ipaddress", "varchar", 200, false, false, ""}, tblColumn{"ipaddress", "varchar", 200, false, false, ""},
qgen.DBTableColumn{"doneAt", "createdAt", 0, false, false, ""}, tblColumn{"doneAt", "createdAt", 0, false, false, ""},
}, },
[]qgen.DBTableKey{ []tblKey{
qgen.DBTableKey{"lid", "primary"}, tblKey{"lid", "primary"},
}, },
)) ))
}
var acc = qgen.NewAcc
var itoa = strconv.Itoa
func patch10(scanner *bufio.Scanner) error {
err := execStmt(qgen.Builder.AddColumn("topics", tblColumn{"attachCount", "int", 0, false, false, "0"}))
if err != nil {
return err
}
err = execStmt(qgen.Builder.AddColumn("topics", tblColumn{"lastReplyID", "int", 0, false, false, "0"}))
if err != nil { if err != nil {
return err return err
} }
return nil // 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 {
stid := itoa(tid)
count, err := acc().Count("attachments").Where("originTable = 'topics' and originID = " + stid).Total()
if err != nil {
return err
}
var hasReply = false
err = acc().Select("replies").Cols("rid").Where("tid = " + stid).Orderby("rid DESC").Limit("1").EachInt(func(rid int) error {
hasReply = true
_, err := acc().Update("topics").Set("lastReplyID = ?, attachCount = ?").Where("tid = "+stid).Exec(rid, count)
return err
})
if err != nil {
return err
}
if !hasReply {
_, err = acc().Update("topics").Set("attachCount = ?").Where("tid = " + stid).Exec(count)
}
return err
})
if err != nil {
return err
}
_, err = acc().Insert("updates").Columns("dbVersion").Fields("0").Exec()
return err
} }

View File

@ -224,11 +224,8 @@ function runWebSockets() {
// TODO: Add support for other alert feeds like PM Alerts // TODO: Add support for other alert feeds like PM Alerts
var generalAlerts = document.getElementById("general_alerts"); var generalAlerts = document.getElementById("general_alerts");
if(alertList.length < 8) { if(alertList.length < 8) loadAlerts(generalAlerts);
loadAlerts(generalAlerts); else updateAlertList(generalAlerts);
} else {
updateAlertList(generalAlerts);
}
} }
}); });
} }
@ -374,6 +371,7 @@ function mainInit(){
event.preventDefault(); event.preventDefault();
$('.hide_on_edit').addClass("edit_opened"); $('.hide_on_edit').addClass("edit_opened");
$('.show_on_edit').addClass("edit_opened"); $('.show_on_edit').addClass("edit_opened");
runHook("open_edit");
}); });
$(".topic_item .submit_edit").click(function(event){ $(".topic_item .submit_edit").click(function(event){
@ -388,6 +386,7 @@ function mainInit(){
$('.hide_on_edit').removeClass("edit_opened"); $('.hide_on_edit').removeClass("edit_opened");
$('.show_on_edit').removeClass("edit_opened"); $('.show_on_edit').removeClass("edit_opened");
runHook("close_edit");
let formAction = this.form.getAttribute("action"); let formAction = this.form.getAttribute("action");
$.ajax({ $.ajax({
@ -566,47 +565,106 @@ function mainInit(){
$(".topic_create_form").hide(); $(".topic_create_form").hide();
}); });
function uploadFileHandler() { function uploadFileHandler(fileList,maxFiles = 5, step1 = () => {}, step2 = () => {}) {
var fileList = this.files;
// Truncate the number of files to 5
let files = []; let files = [];
for(var i = 0; i < fileList.length && i < 5; i++) { for(var i = 0; i < fileList.length && i < 5; i++) {
files[i] = fileList[i]; files[i] = fileList[i];
} }
// Iterate over the files
let totalSize = 0; let totalSize = 0;
for(let i = 0; i < files.length; i++) { for(let i = 0; i < files.length; i++) {
console.log("files[" + i + "]",files[i]); console.log("files[" + i + "]",files[i]);
totalSize += files[i]["size"]; totalSize += files[i]["size"];
}
let reader = new FileReader(); if(totalSize > me.Site.MaxRequestSize) {
reader.onload = function(e) { throw("You can't upload this much at once, max: " + me.Site.MaxRequestSize);
var fileDock = document.getElementById("upload_file_dock");
var fileItem = document.createElement("label");
console.log("fileItem",fileItem);
if(!files[i]["name"].indexOf('.' > -1)) {
// TODO: Surely, there's a prettier and more elegant way of doing this?
alert("This file doesn't have an extension");
return;
} }
var ext = files[i]["name"].split('.').pop(); for(let i = 0; i < files.length; i++) {
let reader = new FileReader();
reader.onload = (e) => {
let filename = files[i]["name"];
step1(e,filename)
let reader = new FileReader();
reader.onload = (e2) => {
crypto.subtle.digest('SHA-256',e2.target.result)
.then((hash) => {
const hashArray = Array.from(new Uint8Array(hash))
return hashArray.map(b => ('00' + b.toString(16)).slice(-2)).join('')
}).then(hash => step2(e,hash,filename));
}
reader.readAsArrayBuffer(files[i]);
}
reader.readAsDataURL(files[i]);
}
}
// TODO: Surely, there's a prettier and more elegant way of doing this?
function getExt(filename) {
if(!filename.indexOf('.' > -1)) {
throw("This file doesn't have an extension");
}
return filename.split('.').pop();
}
// Attachment Manager
function uploadAttachHandler2() {
let fileDock = this.closest(".attach_edit_bay");
try {
uploadFileHandler(this.files, 5, () => {},
(e, hash, filename) => {
console.log("hash",hash);
let formData = new FormData();
formData.append("session",me.User.Session);
for(let i = 0; i < this.files.length; i++) {
formData.append("upload_files",this.files[i]);
}
let req = new XMLHttpRequest();
req.addEventListener("load", () => {
let data = JSON.parse(req.responseText);
let fileItem = document.createElement("div");
let ext = getExt(filename);
// TODO: Check if this is actually an image, maybe push ImageFileExts to the client from the server in some sort of gen.js?
// TODO: Use client templates here
fileItem.className = "attach_item attach_image_holder";
fileItem.innerHTML = "<img src='"+e.target.result+"' height=24 width=24 /><span class='attach_item_path' aid='"+data[hash+"."+ext]+"' fullpath='//" + window.location.host + "/attachs/" + hash + "." + ext+"'>"+hash+"."+ext+"</span><button class='attach_item_select'>Select</button><button class='attach_item_copy'>Copy</button>";
fileDock.insertBefore(fileItem,fileDock.querySelector(".attach_item_buttons"));
$(".attach_item_select").unbind("click");
$(".attach_item_copy").unbind("click");
bindAttachItems()
});
req.open("POST","//"+window.location.host+"/topic/attach/add/submit/"+fileDock.getAttribute("tid"));
req.send(formData);
});
} catch(e) {
// TODO: Use a notice instead
alert(e);
}
}
// Quick Topic / Quick Reply
function uploadAttachHandler() {
try {
uploadFileHandler(this.files,5,(e,filename) => {
// TODO: Use client templates here
let fileDock = document.getElementById("upload_file_dock");
let fileItem = document.createElement("label");
console.log("fileItem",fileItem);
let ext = getExt(filename)
fileItem.innerText = "." + ext; fileItem.innerText = "." + ext;
fileItem.className = "formbutton uploadItem"; fileItem.className = "formbutton uploadItem";
// TODO: Check if this is actually an image
fileItem.style.backgroundImage = "url("+e.target.result+")"; fileItem.style.backgroundImage = "url("+e.target.result+")";
fileDock.appendChild(fileItem); fileDock.appendChild(fileItem);
},(e,hash, filename) => {
let reader = new FileReader();
reader.onload = function(e) {
crypto.subtle.digest('SHA-256',e.target.result)
.then(function(hash) {
const hashArray = Array.from(new Uint8Array(hash))
return hashArray.map(b => ('00' + b.toString(16)).slice(-2)).join('')
}).then(function(hash) {
console.log("hash",hash); console.log("hash",hash);
let ext = getExt(filename)
let content = document.getElementById("input_content") let content = document.getElementById("input_content")
console.log("content.value", content.value); console.log("content.value", content.value);
@ -619,21 +677,73 @@ function mainInit(){
// For custom / third party text editors // For custom / third party text editors
attachItemCallback(attachItem); attachItemCallback(attachItem);
}); });
} } catch(e) {
reader.readAsArrayBuffer(files[i]);
}
reader.readAsDataURL(files[i]);
}
if(totalSize > me.Site.MaxRequestSize) {
// TODO: Use a notice instead // TODO: Use a notice instead
alert("You can't upload this much data at once, max: " + me.Site.MaxRequestSize); alert(e);
} }
} }
var uploadFiles = document.getElementById("upload_files"); var uploadFiles = document.getElementById("upload_files");
if(uploadFiles != null) { if(uploadFiles != null) {
uploadFiles.addEventListener("change", uploadFileHandler, false); uploadFiles.addEventListener("change", uploadAttachHandler, false);
} }
var uploadFilesOp = document.getElementById("upload_files_op");
if(uploadFilesOp != null) {
uploadFilesOp.addEventListener("change", uploadAttachHandler2, false);
}
function copyToClipboard(str) {
const el = document.createElement('textarea');
el.value = str;
el.setAttribute('readonly', '');
el.style.position = 'absolute';
el.style.left = '-9999px';
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
}
function bindAttachItems() {
$(".attach_item_select").click(function(){
let hold = $(this).closest(".attach_item");
if(hold.hasClass("attach_item_selected")) {
hold.removeClass("attach_item_selected");
} else {
hold.addClass("attach_item_selected");
}
});
$(".attach_item_copy").click(function(){
let hold = $(this).closest(".attach_item");
let pathNode = hold.find(".attach_item_path");
copyToClipboard(pathNode.attr("fullPath"));
});
}
bindAttachItems();
$(".attach_item_delete").click(function(){
let formData = new URLSearchParams();
formData.append("session",me.User.Session);
let aidList = "";
let elems = document.getElementsByClassName("attach_item_selected");
if(elems == null) return;
for(let i = 0; i < elems.length; i++) {
let pathNode = elems[i].querySelector(".attach_item_path");
console.log("pathNode",pathNode);
aidList += pathNode.getAttribute("aid") + ",";
elems[i].remove();
}
if(aidList.length > 0) aidList = aidList.slice(0, -1);
console.log("aidList",aidList)
formData.append("aids",aidList);
let req = new XMLHttpRequest();
let fileDock = this.closest(".attach_edit_bay");
req.open("POST","//"+window.location.host+"/topic/attach/remove/submit/"+fileDock.getAttribute("tid"),true);
req.send(formData);
});
$(".moderate_link").click((event) => { $(".moderate_link").click((event) => {
event.preventDefault(); event.preventDefault();
@ -643,10 +753,11 @@ function mainInit(){
$(this).click(function(){ $(this).click(function(){
selectedTopics.push(parseInt($(this).attr("data-tid"),10)); selectedTopics.push(parseInt($(this).attr("data-tid"),10));
if(selectedTopics.length==1) { if(selectedTopics.length==1) {
$(".mod_floater_head span").html("What do you want to do with this topic?"); var msg = "What do you want to do with this topic?";
} else { } else {
$(".mod_floater_head span").html("What do you want to do with these "+selectedTopics.length+" topics?"); var msg = "What do you want to do with these "+selectedTopics.length+" topics?";
} }
$(".mod_floater_head span").html(msg);
$(this).addClass("topic_selected"); $(this).addClass("topic_selected");
$(".mod_floater").removeClass("auto_hide"); $(".mod_floater").removeClass("auto_hide");
}); });
@ -670,7 +781,6 @@ function mainInit(){
let selectNode = this.form.querySelector(".mod_floater_options"); let selectNode = this.form.querySelector(".mod_floater_options");
let optionNode = selectNode.options[selectNode.selectedIndex]; let optionNode = selectNode.options[selectNode.selectedIndex];
let action = optionNode.getAttribute("val"); let action = optionNode.getAttribute("val");
//console.log("action", action);
// Handle these specially // Handle these specially
switch(action) { switch(action) {

View File

@ -12,6 +12,8 @@ var hooks = {
"after_phrases":[], "after_phrases":[],
"after_add_alert":[], "after_add_alert":[],
"after_update_alert_list":[], "after_update_alert_list":[],
"open_edit":[],
"close_edit":[],
}; };
var ranInitHooks = {} var ranInitHooks = {}
@ -130,7 +132,7 @@ function fetchPhrases() {
(() => { (() => {
runInitHook("pre_iife"); runInitHook("pre_iife");
let loggedIn = document.head.querySelector("[property='x-loggedin']").content; let loggedIn = document.head.querySelector("[property='x-loggedin']").content;
if(loggedIn) { if(loggedIn=="true") {
fetch("/api/me/") fetch("/api/me/")
.then((resp) => resp.json()) .then((resp) => resp.json())
.then((data) => { .then((data) => {

View File

@ -40,28 +40,41 @@ func (builder *accDeleteBuilder) Run(args ...interface{}) (int, error) {
} }
type accUpdateBuilder struct { type accUpdateBuilder struct {
table string up *updatePrebuilder
set string
where string
build *Accumulator build *Accumulator
} }
func (update *accUpdateBuilder) Set(set string) *accUpdateBuilder { func (update *accUpdateBuilder) Set(set string) *accUpdateBuilder {
update.set = set update.up.set = set
return update return update
} }
func (update *accUpdateBuilder) Where(where string) *accUpdateBuilder { func (update *accUpdateBuilder) Where(where string) *accUpdateBuilder {
if update.where != "" { if update.up.where != "" {
update.where += " AND " update.up.where += " AND "
} }
update.where += where update.up.where += where
return update return update
} }
func (update *accUpdateBuilder) Prepare() *sql.Stmt { func (update *accUpdateBuilder) WhereQ(sel *selectPrebuilder) *accUpdateBuilder {
return update.build.SimpleUpdate(update.table, update.set, update.where) update.up.whereSubQuery = sel
return update
}
func (builder *accUpdateBuilder) Prepare() *sql.Stmt {
if builder.up.whereSubQuery != nil {
return builder.build.prepare(builder.build.adapter.SimpleUpdateSelect(builder.up))
}
return builder.build.prepare(builder.build.adapter.SimpleUpdate(builder.up))
}
func (builder *accUpdateBuilder) Exec(args ...interface{}) (res sql.Result, err error) {
query, err := builder.build.adapter.SimpleUpdate(builder.up)
if err != nil {
return res, err
}
return builder.build.exec(query, args...)
} }
type AccSelectBuilder struct { type AccSelectBuilder struct {
@ -77,17 +90,22 @@ type AccSelectBuilder struct {
build *Accumulator build *Accumulator
} }
func (selectItem *AccSelectBuilder) Columns(columns string) *AccSelectBuilder { func (builder *AccSelectBuilder) Columns(columns string) *AccSelectBuilder {
selectItem.columns = columns builder.columns = columns
return selectItem return builder
} }
func (selectItem *AccSelectBuilder) Where(where string) *AccSelectBuilder { func (builder *AccSelectBuilder) Cols(columns string) *AccSelectBuilder {
if selectItem.where != "" { builder.columns = columns
selectItem.where += " AND " return builder
}
func (builder *AccSelectBuilder) Where(where string) *AccSelectBuilder {
if builder.where != "" {
builder.where += " AND "
} }
selectItem.where += where builder.where += where
return selectItem return builder
} }
// TODO: Don't implement the SQL at the accumulator level but the adapter level // TODO: Don't implement the SQL at the accumulator level but the adapter level
@ -115,28 +133,28 @@ func (selectItem *AccSelectBuilder) InQ(column string, subBuilder *AccSelectBuil
return selectItem return selectItem
} }
func (selectItem *AccSelectBuilder) DateCutoff(column string, quantity int, unit string) *AccSelectBuilder { func (builder *AccSelectBuilder) DateCutoff(column string, quantity int, unit string) *AccSelectBuilder {
selectItem.dateCutoff = &dateCutoff{column, quantity, unit} builder.dateCutoff = &dateCutoff{column, quantity, unit}
return selectItem return builder
} }
func (selectItem *AccSelectBuilder) Orderby(orderby string) *AccSelectBuilder { func (builder *AccSelectBuilder) Orderby(orderby string) *AccSelectBuilder {
selectItem.orderby = orderby builder.orderby = orderby
return selectItem return builder
} }
func (selectItem *AccSelectBuilder) Limit(limit string) *AccSelectBuilder { func (builder *AccSelectBuilder) Limit(limit string) *AccSelectBuilder {
selectItem.limit = limit builder.limit = limit
return selectItem return builder
} }
func (selectItem *AccSelectBuilder) Prepare() *sql.Stmt { func (builder *AccSelectBuilder) Prepare() *sql.Stmt {
// TODO: Phase out the procedural API and use the adapter's OO API? The OO API might need a bit more work before we do that and it needs to be rolled out to MSSQL. // TODO: Phase out the procedural API and use the adapter's OO API? The OO API might need a bit more work before we do that and it needs to be rolled out to MSSQL.
if selectItem.dateCutoff != nil || selectItem.inChain != nil { if builder.dateCutoff != nil || builder.inChain != nil {
selectBuilder := selectItem.build.GetAdapter().Builder().Select().FromAcc(selectItem) selectBuilder := builder.build.GetAdapter().Builder().Select().FromAcc(builder)
return selectItem.build.prepare(selectItem.build.GetAdapter().ComplexSelect(selectBuilder)) return builder.build.prepare(builder.build.GetAdapter().ComplexSelect(selectBuilder))
} }
return selectItem.build.SimpleSelect(selectItem.table, selectItem.columns, selectItem.where, selectItem.orderby, selectItem.limit) return builder.build.SimpleSelect(builder.table, builder.columns, builder.where, builder.orderby, builder.limit)
} }
func (builder *AccSelectBuilder) query() (string, error) { func (builder *AccSelectBuilder) query() (string, error) {
@ -145,15 +163,15 @@ func (builder *AccSelectBuilder) query() (string, error) {
selectBuilder := builder.build.GetAdapter().Builder().Select().FromAcc(builder) selectBuilder := builder.build.GetAdapter().Builder().Select().FromAcc(builder)
return builder.build.GetAdapter().ComplexSelect(selectBuilder) return builder.build.GetAdapter().ComplexSelect(selectBuilder)
} }
return builder.build.adapter.SimpleSelect("_builder", builder.table, builder.columns, builder.where, builder.orderby, builder.limit) return builder.build.adapter.SimpleSelect("", builder.table, builder.columns, builder.where, builder.orderby, builder.limit)
} }
func (selectItem *AccSelectBuilder) Query(args ...interface{}) (*sql.Rows, error) { func (builder *AccSelectBuilder) Query(args ...interface{}) (*sql.Rows, error) {
stmt := selectItem.Prepare() stmt := builder.Prepare()
if stmt != nil { if stmt != nil {
return stmt.Query(args...) return stmt.Query(args...)
} }
return nil, selectItem.build.FirstError() return nil, builder.build.FirstError()
} }
type AccRowWrap struct { type AccRowWrap struct {
@ -245,7 +263,7 @@ func (insert *accInsertBuilder) Prepare() *sql.Stmt {
} }
func (builder *accInsertBuilder) Exec(args ...interface{}) (res sql.Result, err error) { func (builder *accInsertBuilder) Exec(args ...interface{}) (res sql.Result, err error) {
query, err := builder.build.adapter.SimpleInsert("_builder", builder.table, builder.columns, builder.fields) query, err := builder.build.adapter.SimpleInsert("", builder.table, builder.columns, builder.fields)
if err != nil { if err != nil {
return res, err return res, err
} }
@ -253,7 +271,7 @@ func (builder *accInsertBuilder) Exec(args ...interface{}) (res sql.Result, err
} }
func (builder *accInsertBuilder) Run(args ...interface{}) (int, error) { func (builder *accInsertBuilder) Run(args ...interface{}) (int, error) {
query, err := builder.build.adapter.SimpleInsert("_builder", builder.table, builder.columns, builder.fields) query, err := builder.build.adapter.SimpleInsert("", builder.table, builder.columns, builder.fields)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -292,4 +310,13 @@ func (count *accCountBuilder) Prepare() *sql.Stmt {
return count.build.SimpleCount(count.table, count.where, count.limit) return count.build.SimpleCount(count.table, count.where, count.limit)
} }
func (count *accCountBuilder) Total() (total int, err error) {
stmt := count.Prepare()
if stmt == nil {
return 0, count.build.FirstError()
}
err = stmt.QueryRow().Scan(&total)
return total, err
}
// TODO: Add a Sum builder for summing viewchunks up into one number for the dashboard? // TODO: Add a Sum builder for summing viewchunks up into one number for the dashboard?

View File

@ -95,52 +95,56 @@ func (build *Accumulator) Tx(handler func(*TransactionBuilder) error) {
} }
func (build *Accumulator) SimpleSelect(table string, columns string, where string, orderby string, limit string) *sql.Stmt { func (build *Accumulator) SimpleSelect(table string, columns string, where string, orderby string, limit string) *sql.Stmt {
return build.prepare(build.adapter.SimpleSelect("_builder", table, columns, where, orderby, limit)) return build.prepare(build.adapter.SimpleSelect("", table, columns, where, orderby, limit))
} }
func (build *Accumulator) SimpleCount(table string, where string, limit string) *sql.Stmt { func (build *Accumulator) SimpleCount(table string, where string, limit string) *sql.Stmt {
return build.prepare(build.adapter.SimpleCount("_builder", table, where, limit)) return build.prepare(build.adapter.SimpleCount("", table, where, limit))
} }
func (build *Accumulator) SimpleLeftJoin(table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) *sql.Stmt { func (build *Accumulator) SimpleLeftJoin(table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) *sql.Stmt {
return build.prepare(build.adapter.SimpleLeftJoin("_builder", table1, table2, columns, joiners, where, orderby, limit)) return build.prepare(build.adapter.SimpleLeftJoin("", table1, table2, columns, joiners, where, orderby, limit))
} }
func (build *Accumulator) SimpleInnerJoin(table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) *sql.Stmt { func (build *Accumulator) SimpleInnerJoin(table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) *sql.Stmt {
return build.prepare(build.adapter.SimpleInnerJoin("_builder", table1, table2, columns, joiners, where, orderby, limit)) return build.prepare(build.adapter.SimpleInnerJoin("", table1, table2, columns, joiners, where, orderby, limit))
} }
func (build *Accumulator) CreateTable(table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) *sql.Stmt { func (build *Accumulator) CreateTable(table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) *sql.Stmt {
return build.prepare(build.adapter.CreateTable("_builder", table, charset, collation, columns, keys)) return build.prepare(build.adapter.CreateTable("", table, charset, collation, columns, keys))
} }
func (build *Accumulator) SimpleInsert(table string, columns string, fields string) *sql.Stmt { func (build *Accumulator) SimpleInsert(table string, columns string, fields string) *sql.Stmt {
return build.prepare(build.adapter.SimpleInsert("_builder", table, columns, fields)) return build.prepare(build.adapter.SimpleInsert("", table, columns, fields))
} }
func (build *Accumulator) SimpleInsertSelect(ins DBInsert, sel DBSelect) *sql.Stmt { func (build *Accumulator) SimpleInsertSelect(ins DBInsert, sel DBSelect) *sql.Stmt {
return build.prepare(build.adapter.SimpleInsertSelect("_builder", ins, sel)) return build.prepare(build.adapter.SimpleInsertSelect("", ins, sel))
} }
func (build *Accumulator) SimpleInsertLeftJoin(ins DBInsert, sel DBJoin) *sql.Stmt { func (build *Accumulator) SimpleInsertLeftJoin(ins DBInsert, sel DBJoin) *sql.Stmt {
return build.prepare(build.adapter.SimpleInsertLeftJoin("_builder", ins, sel)) return build.prepare(build.adapter.SimpleInsertLeftJoin("", ins, sel))
} }
func (build *Accumulator) SimpleInsertInnerJoin(ins DBInsert, sel DBJoin) *sql.Stmt { func (build *Accumulator) SimpleInsertInnerJoin(ins DBInsert, sel DBJoin) *sql.Stmt {
return build.prepare(build.adapter.SimpleInsertInnerJoin("_builder", ins, sel)) return build.prepare(build.adapter.SimpleInsertInnerJoin("", ins, sel))
} }
func (build *Accumulator) SimpleUpdate(table string, set string, where string) *sql.Stmt { func (build *Accumulator) SimpleUpdate(table string, set string, where string) *sql.Stmt {
return build.prepare(build.adapter.SimpleUpdate("_builder", table, set, where)) return build.prepare(build.adapter.SimpleUpdate(qUpdate(table, set, where)))
}
func (build *Accumulator) SimpleUpdateSelect(table string, set string, where string) *sql.Stmt {
return build.prepare(build.adapter.SimpleUpdateSelect(qUpdate(table, set, where)))
} }
func (build *Accumulator) SimpleDelete(table string, where string) *sql.Stmt { func (build *Accumulator) SimpleDelete(table string, where string) *sql.Stmt {
return build.prepare(build.adapter.SimpleDelete("_builder", table, where)) return build.prepare(build.adapter.SimpleDelete("", table, where))
} }
// I don't know why you need this, but here it is x.x // I don't know why you need this, but here it is x.x
func (build *Accumulator) Purge(table string) *sql.Stmt { func (build *Accumulator) Purge(table string) *sql.Stmt {
return build.prepare(build.adapter.Purge("_builder", table)) return build.prepare(build.adapter.Purge("", table))
} }
func (build *Accumulator) prepareTx(tx *sql.Tx, res string, err error) (stmt *sql.Stmt) { func (build *Accumulator) prepareTx(tx *sql.Tx, res string, err error) (stmt *sql.Stmt) {
@ -155,63 +159,63 @@ func (build *Accumulator) prepareTx(tx *sql.Tx, res string, err error) (stmt *sq
// These ones support transactions // These ones support transactions
func (build *Accumulator) SimpleSelectTx(tx *sql.Tx, table string, columns string, where string, orderby string, limit string) (stmt *sql.Stmt) { func (build *Accumulator) SimpleSelectTx(tx *sql.Tx, table string, columns string, where string, orderby string, limit string) (stmt *sql.Stmt) {
res, err := build.adapter.SimpleSelect("_builder", table, columns, where, orderby, limit) res, err := build.adapter.SimpleSelect("", table, columns, where, orderby, limit)
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }
func (build *Accumulator) SimpleCountTx(tx *sql.Tx, table string, where string, limit string) (stmt *sql.Stmt) { func (build *Accumulator) SimpleCountTx(tx *sql.Tx, table string, where string, limit string) (stmt *sql.Stmt) {
res, err := build.adapter.SimpleCount("_builder", table, where, limit) res, err := build.adapter.SimpleCount("", table, where, limit)
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }
func (build *Accumulator) SimpleLeftJoinTx(tx *sql.Tx, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt) { func (build *Accumulator) SimpleLeftJoinTx(tx *sql.Tx, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt) {
res, err := build.adapter.SimpleLeftJoin("_builder", table1, table2, columns, joiners, where, orderby, limit) res, err := build.adapter.SimpleLeftJoin("", table1, table2, columns, joiners, where, orderby, limit)
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }
func (build *Accumulator) SimpleInnerJoinTx(tx *sql.Tx, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt) { func (build *Accumulator) SimpleInnerJoinTx(tx *sql.Tx, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt) {
res, err := build.adapter.SimpleInnerJoin("_builder", table1, table2, columns, joiners, where, orderby, limit) res, err := build.adapter.SimpleInnerJoin("", table1, table2, columns, joiners, where, orderby, limit)
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }
func (build *Accumulator) CreateTableTx(tx *sql.Tx, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (stmt *sql.Stmt) { func (build *Accumulator) CreateTableTx(tx *sql.Tx, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (stmt *sql.Stmt) {
res, err := build.adapter.CreateTable("_builder", table, charset, collation, columns, keys) res, err := build.adapter.CreateTable("", table, charset, collation, columns, keys)
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }
func (build *Accumulator) SimpleInsertTx(tx *sql.Tx, table string, columns string, fields string) (stmt *sql.Stmt) { func (build *Accumulator) SimpleInsertTx(tx *sql.Tx, table string, columns string, fields string) (stmt *sql.Stmt) {
res, err := build.adapter.SimpleInsert("_builder", table, columns, fields) res, err := build.adapter.SimpleInsert("", table, columns, fields)
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }
func (build *Accumulator) SimpleInsertSelectTx(tx *sql.Tx, ins DBInsert, sel DBSelect) (stmt *sql.Stmt) { func (build *Accumulator) SimpleInsertSelectTx(tx *sql.Tx, ins DBInsert, sel DBSelect) (stmt *sql.Stmt) {
res, err := build.adapter.SimpleInsertSelect("_builder", ins, sel) res, err := build.adapter.SimpleInsertSelect("", ins, sel)
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }
func (build *Accumulator) SimpleInsertLeftJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt) { func (build *Accumulator) SimpleInsertLeftJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt) {
res, err := build.adapter.SimpleInsertLeftJoin("_builder", ins, sel) res, err := build.adapter.SimpleInsertLeftJoin("", ins, sel)
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }
func (build *Accumulator) SimpleInsertInnerJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt) { func (build *Accumulator) SimpleInsertInnerJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt) {
res, err := build.adapter.SimpleInsertInnerJoin("_builder", ins, sel) res, err := build.adapter.SimpleInsertInnerJoin("", ins, sel)
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }
func (build *Accumulator) SimpleUpdateTx(tx *sql.Tx, table string, set string, where string) (stmt *sql.Stmt) { func (build *Accumulator) SimpleUpdateTx(tx *sql.Tx, table string, set string, where string) (stmt *sql.Stmt) {
res, err := build.adapter.SimpleUpdate("_builder", table, set, where) res, err := build.adapter.SimpleUpdate(qUpdate(table, set, where))
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }
func (build *Accumulator) SimpleDeleteTx(tx *sql.Tx, table string, where string) (stmt *sql.Stmt) { func (build *Accumulator) SimpleDeleteTx(tx *sql.Tx, table string, where string) (stmt *sql.Stmt) {
res, err := build.adapter.SimpleDelete("_builder", table, where) res, err := build.adapter.SimpleDelete("", table, where)
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }
// I don't know why you need this, but here it is x.x // I don't know why you need this, but here it is x.x
func (build *Accumulator) PurgeTx(tx *sql.Tx, table string) (stmt *sql.Stmt) { func (build *Accumulator) PurgeTx(tx *sql.Tx, table string) (stmt *sql.Stmt) {
res, err := build.adapter.Purge("_builder", table) res, err := build.adapter.Purge("", table)
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }
@ -220,7 +224,7 @@ func (build *Accumulator) Delete(table string) *accDeleteBuilder {
} }
func (build *Accumulator) Update(table string) *accUpdateBuilder { func (build *Accumulator) Update(table string) *accUpdateBuilder {
return &accUpdateBuilder{table, "", "", build} return &accUpdateBuilder{qUpdate(table, "", ""), build}
} }
func (build *Accumulator) Select(table string) *AccSelectBuilder { func (build *Accumulator) Select(table string) *AccSelectBuilder {

View File

@ -85,60 +85,60 @@ func (build *builder) prepare(res string, err error) (*sql.Stmt, error) {
} }
func (build *builder) SimpleSelect(table string, columns string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) { func (build *builder) SimpleSelect(table string, columns string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleSelect("_builder", table, columns, where, orderby, limit)) return build.prepare(build.adapter.SimpleSelect("", table, columns, where, orderby, limit))
} }
func (build *builder) SimpleCount(table string, where string, limit string) (stmt *sql.Stmt, err error) { func (build *builder) SimpleCount(table string, where string, limit string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleCount("_builder", table, where, limit)) return build.prepare(build.adapter.SimpleCount("", table, where, limit))
} }
func (build *builder) SimpleLeftJoin(table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) { func (build *builder) SimpleLeftJoin(table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleLeftJoin("_builder", table1, table2, columns, joiners, where, orderby, limit)) return build.prepare(build.adapter.SimpleLeftJoin("", table1, table2, columns, joiners, where, orderby, limit))
} }
func (build *builder) SimpleInnerJoin(table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) { func (build *builder) SimpleInnerJoin(table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleInnerJoin("_builder", table1, table2, columns, joiners, where, orderby, limit)) return build.prepare(build.adapter.SimpleInnerJoin("", table1, table2, columns, joiners, where, orderby, limit))
} }
func (build *builder) DropTable(table string) (stmt *sql.Stmt, err error) { func (build *builder) DropTable(table string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.DropTable("_builder", table)) return build.prepare(build.adapter.DropTable("", table))
} }
func (build *builder) CreateTable(table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (stmt *sql.Stmt, err error) { func (build *builder) CreateTable(table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.CreateTable("_builder", table, charset, collation, columns, keys)) return build.prepare(build.adapter.CreateTable("", table, charset, collation, columns, keys))
} }
func (build *builder) AddColumn(table string, column DBTableColumn) (stmt *sql.Stmt, err error) { func (build *builder) AddColumn(table string, column DBTableColumn) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.AddColumn("_builder", table, column)) return build.prepare(build.adapter.AddColumn("", table, column))
} }
func (build *builder) SimpleInsert(table string, columns string, fields string) (stmt *sql.Stmt, err error) { func (build *builder) SimpleInsert(table string, columns string, fields string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleInsert("_builder", table, columns, fields)) return build.prepare(build.adapter.SimpleInsert("", table, columns, fields))
} }
func (build *builder) SimpleInsertSelect(ins DBInsert, sel DBSelect) (stmt *sql.Stmt, err error) { func (build *builder) SimpleInsertSelect(ins DBInsert, sel DBSelect) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleInsertSelect("_builder", ins, sel)) return build.prepare(build.adapter.SimpleInsertSelect("", ins, sel))
} }
func (build *builder) SimpleInsertLeftJoin(ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) { func (build *builder) SimpleInsertLeftJoin(ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleInsertLeftJoin("_builder", ins, sel)) return build.prepare(build.adapter.SimpleInsertLeftJoin("", ins, sel))
} }
func (build *builder) SimpleInsertInnerJoin(ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) { func (build *builder) SimpleInsertInnerJoin(ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleInsertInnerJoin("_builder", ins, sel)) return build.prepare(build.adapter.SimpleInsertInnerJoin("", ins, sel))
} }
func (build *builder) SimpleUpdate(table string, set string, where string) (stmt *sql.Stmt, err error) { func (build *builder) SimpleUpdate(table string, set string, where string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleUpdate("_builder", table, set, where)) return build.prepare(build.adapter.SimpleUpdate(qUpdate(table, set, where)))
} }
func (build *builder) SimpleDelete(table string, where string) (stmt *sql.Stmt, err error) { func (build *builder) SimpleDelete(table string, where string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleDelete("_builder", table, where)) return build.prepare(build.adapter.SimpleDelete("", table, where))
} }
// I don't know why you need this, but here it is x.x // I don't know why you need this, but here it is x.x
func (build *builder) Purge(table string) (stmt *sql.Stmt, err error) { func (build *builder) Purge(table string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.Purge("_builder", table)) return build.prepare(build.adapter.Purge("", table))
} }
func (build *builder) prepareTx(tx *sql.Tx, res string, err error) (*sql.Stmt, error) { func (build *builder) prepareTx(tx *sql.Tx, res string, err error) (*sql.Stmt, error) {
@ -150,62 +150,62 @@ func (build *builder) prepareTx(tx *sql.Tx, res string, err error) (*sql.Stmt, e
// These ones support transactions // These ones support transactions
func (build *builder) SimpleSelectTx(tx *sql.Tx, table string, columns string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) { func (build *builder) SimpleSelectTx(tx *sql.Tx, table string, columns string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleSelect("_builder", table, columns, where, orderby, limit) res, err := build.adapter.SimpleSelect("", table, columns, where, orderby, limit)
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }
func (build *builder) SimpleCountTx(tx *sql.Tx, table string, where string, limit string) (stmt *sql.Stmt, err error) { func (build *builder) SimpleCountTx(tx *sql.Tx, table string, where string, limit string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleCount("_builder", table, where, limit) res, err := build.adapter.SimpleCount("", table, where, limit)
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }
func (build *builder) SimpleLeftJoinTx(tx *sql.Tx, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) { func (build *builder) SimpleLeftJoinTx(tx *sql.Tx, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleLeftJoin("_builder", table1, table2, columns, joiners, where, orderby, limit) res, err := build.adapter.SimpleLeftJoin("", table1, table2, columns, joiners, where, orderby, limit)
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }
func (build *builder) SimpleInnerJoinTx(tx *sql.Tx, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) { func (build *builder) SimpleInnerJoinTx(tx *sql.Tx, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleInnerJoin("_builder", table1, table2, columns, joiners, where, orderby, limit) res, err := build.adapter.SimpleInnerJoin("", table1, table2, columns, joiners, where, orderby, limit)
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }
func (build *builder) CreateTableTx(tx *sql.Tx, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (stmt *sql.Stmt, err error) { func (build *builder) CreateTableTx(tx *sql.Tx, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (stmt *sql.Stmt, err error) {
res, err := build.adapter.CreateTable("_builder", table, charset, collation, columns, keys) res, err := build.adapter.CreateTable("", table, charset, collation, columns, keys)
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }
func (build *builder) SimpleInsertTx(tx *sql.Tx, table string, columns string, fields string) (stmt *sql.Stmt, err error) { func (build *builder) SimpleInsertTx(tx *sql.Tx, table string, columns string, fields string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleInsert("_builder", table, columns, fields) res, err := build.adapter.SimpleInsert("", table, columns, fields)
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }
func (build *builder) SimpleInsertSelectTx(tx *sql.Tx, ins DBInsert, sel DBSelect) (stmt *sql.Stmt, err error) { func (build *builder) SimpleInsertSelectTx(tx *sql.Tx, ins DBInsert, sel DBSelect) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleInsertSelect("_builder", ins, sel) res, err := build.adapter.SimpleInsertSelect("", ins, sel)
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }
func (build *builder) SimpleInsertLeftJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) { func (build *builder) SimpleInsertLeftJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleInsertLeftJoin("_builder", ins, sel) res, err := build.adapter.SimpleInsertLeftJoin("", ins, sel)
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }
func (build *builder) SimpleInsertInnerJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) { func (build *builder) SimpleInsertInnerJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleInsertInnerJoin("_builder", ins, sel) res, err := build.adapter.SimpleInsertInnerJoin("", ins, sel)
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }
func (build *builder) SimpleUpdateTx(tx *sql.Tx, table string, set string, where string) (stmt *sql.Stmt, err error) { func (build *builder) SimpleUpdateTx(tx *sql.Tx, table string, set string, where string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleUpdate("_builder", table, set, where) res, err := build.adapter.SimpleUpdate(qUpdate(table, set, where))
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }
func (build *builder) SimpleDeleteTx(tx *sql.Tx, table string, where string) (stmt *sql.Stmt, err error) { func (build *builder) SimpleDeleteTx(tx *sql.Tx, table string, where string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleDelete("_builder", table, where) res, err := build.adapter.SimpleDelete("", table, where)
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }
// I don't know why you need this, but here it is x.x // I don't know why you need this, but here it is x.x
func (build *builder) PurgeTx(tx *sql.Tx, table string) (stmt *sql.Stmt, err error) { func (build *builder) PurgeTx(tx *sql.Tx, table string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.Purge("_builder", table) res, err := build.adapter.Purge("", table)
return build.prepareTx(tx, res, err) return build.prepareTx(tx, res, err)
} }

View File

@ -11,22 +11,22 @@ type prebuilder struct {
} }
func (build *prebuilder) Select(nlist ...string) *selectPrebuilder { func (build *prebuilder) Select(nlist ...string) *selectPrebuilder {
name := optString(nlist, "_builder") name := optString(nlist, "")
return &selectPrebuilder{name, "", "", "", "", "", nil, nil, "", build.adapter} return &selectPrebuilder{name, "", "", "", "", "", nil, nil, "", build.adapter}
} }
func (build *prebuilder) Insert(nlist ...string) *insertPrebuilder { func (build *prebuilder) Insert(nlist ...string) *insertPrebuilder {
name := optString(nlist, "_builder") name := optString(nlist, "")
return &insertPrebuilder{name, "", "", "", build.adapter} return &insertPrebuilder{name, "", "", "", build.adapter}
} }
func (build *prebuilder) Update(nlist ...string) *updatePrebuilder { func (build *prebuilder) Update(nlist ...string) *updatePrebuilder {
name := optString(nlist, "_builder") name := optString(nlist, "")
return &updatePrebuilder{name, "", "", "", build.adapter} return &updatePrebuilder{name, "", "", "", nil, build.adapter}
} }
func (build *prebuilder) Delete(nlist ...string) *deletePrebuilder { func (build *prebuilder) Delete(nlist ...string) *deletePrebuilder {
name := optString(nlist, "_builder") name := optString(nlist, "")
return &deletePrebuilder{name, "", "", build.adapter} return &deletePrebuilder{name, "", "", build.adapter}
} }
@ -64,10 +64,15 @@ type updatePrebuilder struct {
table string table string
set string set string
where string where string
whereSubQuery *selectPrebuilder
build Adapter build Adapter
} }
func qUpdate(table string, set string, where string) *updatePrebuilder {
return &updatePrebuilder{table: table, set: set, where: where}
}
func (update *updatePrebuilder) Table(table string) *updatePrebuilder { func (update *updatePrebuilder) Table(table string) *updatePrebuilder {
update.table = table update.table = table
return update return update
@ -86,12 +91,17 @@ func (update *updatePrebuilder) Where(where string) *updatePrebuilder {
return update return update
} }
func (update *updatePrebuilder) WhereQ(sel *selectPrebuilder) *updatePrebuilder {
update.whereSubQuery = sel
return update
}
func (update *updatePrebuilder) Text() (string, error) { func (update *updatePrebuilder) Text() (string, error) {
return update.build.SimpleUpdate(update.name, update.table, update.set, update.where) return update.build.SimpleUpdate(update)
} }
func (update *updatePrebuilder) Parse() { func (update *updatePrebuilder) Parse() {
update.build.SimpleUpdate(update.name, update.table, update.set, update.where) update.build.SimpleUpdate(update)
} }
type selectPrebuilder struct { type selectPrebuilder struct {
@ -151,7 +161,7 @@ func (selectItem *selectPrebuilder) FromAcc(accBuilder *AccSelectBuilder) *selec
selectItem.dateCutoff = accBuilder.dateCutoff selectItem.dateCutoff = accBuilder.dateCutoff
if accBuilder.inChain != nil { if accBuilder.inChain != nil {
selectItem.inChain = &selectPrebuilder{"__builder", accBuilder.inChain.table, accBuilder.inChain.columns, accBuilder.inChain.where, accBuilder.inChain.orderby, accBuilder.inChain.limit, accBuilder.inChain.dateCutoff, nil, "", selectItem.build} selectItem.inChain = &selectPrebuilder{"", accBuilder.inChain.table, accBuilder.inChain.columns, accBuilder.inChain.where, accBuilder.inChain.orderby, accBuilder.inChain.limit, accBuilder.inChain.dateCutoff, nil, "", selectItem.build}
selectItem.inColumn = accBuilder.inColumn selectItem.inColumn = accBuilder.inColumn
} }
return selectItem return selectItem

View File

@ -45,9 +45,6 @@ func (adapter *MssqlAdapter) DbVersion() string {
} }
func (adapter *MssqlAdapter) DropTable(name string, table string) (string, error) { func (adapter *MssqlAdapter) DropTable(name string, table string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -59,9 +56,6 @@ func (adapter *MssqlAdapter) DropTable(name string, table string) (string, error
// TODO: Convert any remaining stringy types to nvarchar // TODO: Convert any remaining stringy types to nvarchar
// We may need to change the CreateTable API to better suit Mssql and the other database drivers which are coming up // We may need to change the CreateTable API to better suit Mssql and the other database drivers which are coming up
func (adapter *MssqlAdapter) CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) { func (adapter *MssqlAdapter) CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -142,9 +136,6 @@ func (adapter *MssqlAdapter) parseColumn(column DBTableColumn) (col DBTableColum
// TODO: Test this, not sure if some things work // TODO: Test this, not sure if some things work
func (adapter *MssqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) { func (adapter *MssqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -156,9 +147,6 @@ func (adapter *MssqlAdapter) AddColumn(name string, table string, column DBTable
} }
func (adapter *MssqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) { func (adapter *MssqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -237,9 +225,6 @@ func (adapter *MssqlAdapter) SimpleReplace(name string, table string, columns st
} }
func (adapter *MssqlAdapter) SimpleUpsert(name string, table string, columns string, fields string, where string) (string, error) { func (adapter *MssqlAdapter) SimpleUpsert(name string, table string, columns string, fields string, where string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -332,19 +317,16 @@ func (adapter *MssqlAdapter) SimpleUpsert(name string, table string, columns str
return querystr, nil return querystr, nil
} }
func (adapter *MssqlAdapter) SimpleUpdate(name string, table string, set string, where string) (string, error) { func (adapter *MssqlAdapter) SimpleUpdate(up *updatePrebuilder) (string, error) {
if name == "" { if up.table == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
if set == "" { if up.set == "" {
return "", errors.New("You need to set data in this update statement") return "", errors.New("You need to set data in this update statement")
} }
var querystr = "UPDATE [" + table + "] SET " var querystr = "UPDATE [" + up.table + "] SET "
for _, item := range processSet(set) { for _, item := range processSet(up.set) {
querystr += "[" + item.Column + "] =" querystr += "[" + item.Column + "] ="
for _, token := range item.Expr { for _, token := range item.Expr {
switch token.Type { switch token.Type {
@ -370,9 +352,9 @@ func (adapter *MssqlAdapter) SimpleUpdate(name string, table string, set string,
querystr = querystr[0 : len(querystr)-1] querystr = querystr[0 : len(querystr)-1]
// Add support for BETWEEN x.x // Add support for BETWEEN x.x
if len(where) != 0 { if len(up.where) != 0 {
querystr += " WHERE" querystr += " WHERE"
for _, loc := range processWhere(where) { for _, loc := range processWhere(up.where) {
for _, token := range loc.Expr { for _, token := range loc.Expr {
switch token.Type { switch token.Type {
case "function", "operator", "number", "substitute", "or": case "function", "operator", "number", "substitute", "or":
@ -394,14 +376,15 @@ func (adapter *MssqlAdapter) SimpleUpdate(name string, table string, set string,
querystr = querystr[0 : len(querystr)-4] querystr = querystr[0 : len(querystr)-4]
} }
adapter.pushStatement(name, "update", querystr) adapter.pushStatement(up.name, "update", querystr)
return querystr, nil return querystr, nil
} }
func (adapter *MssqlAdapter) SimpleUpdateSelect(up *updatePrebuilder) (string, error) {
return "", errors.New("not implemented")
}
func (adapter *MssqlAdapter) SimpleDelete(name string, table string, where string) (string, error) { func (adapter *MssqlAdapter) SimpleDelete(name string, table string, where string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -441,9 +424,6 @@ func (adapter *MssqlAdapter) SimpleDelete(name string, table string, where strin
// We don't want to accidentally wipe tables, so we'll have a separate method for purging tables instead // We don't want to accidentally wipe tables, so we'll have a separate method for purging tables instead
func (adapter *MssqlAdapter) Purge(name string, table string) (string, error) { func (adapter *MssqlAdapter) Purge(name string, table string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -452,9 +432,6 @@ func (adapter *MssqlAdapter) Purge(name string, table string) (string, error) {
} }
func (adapter *MssqlAdapter) SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error) { func (adapter *MssqlAdapter) SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -554,9 +531,6 @@ func (adapter *MssqlAdapter) ComplexSelect(preBuilder *selectPrebuilder) (string
} }
func (adapter *MssqlAdapter) SimpleLeftJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) { func (adapter *MssqlAdapter) SimpleLeftJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table1 == "" { if table1 == "" {
return "", errors.New("You need a name for the left table") return "", errors.New("You need a name for the left table")
} }
@ -683,9 +657,6 @@ func (adapter *MssqlAdapter) SimpleLeftJoin(name string, table1 string, table2 s
} }
func (adapter *MssqlAdapter) SimpleInnerJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) { func (adapter *MssqlAdapter) SimpleInnerJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table1 == "" { if table1 == "" {
return "", errors.New("You need a name for the left table") return "", errors.New("You need a name for the left table")
} }
@ -1067,9 +1038,6 @@ func (adapter *MssqlAdapter) SimpleInsertInnerJoin(name string, ins DBInsert, se
} }
func (adapter *MssqlAdapter) SimpleCount(name string, table string, where string, limit string) (string, error) { func (adapter *MssqlAdapter) SimpleCount(name string, table string, where string, limit string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -1116,7 +1084,7 @@ func (adapter *MssqlAdapter) Builder() *prebuilder {
func (adapter *MssqlAdapter) Write() error { func (adapter *MssqlAdapter) Write() error {
var stmts, body string var stmts, body string
for _, name := range adapter.BufferOrder { for _, name := range adapter.BufferOrder {
if name[0] == '_' { if name == "" {
continue continue
} }
stmt := adapter.Buffer[name] stmt := adapter.Buffer[name]

View File

@ -83,21 +83,16 @@ func (adapter *MysqlAdapter) DbVersion() string {
} }
func (adapter *MysqlAdapter) DropTable(name string, table string) (string, error) { func (adapter *MysqlAdapter) DropTable(name string, table string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
querystr := "DROP TABLE IF EXISTS `" + table + "`;" querystr := "DROP TABLE IF EXISTS `" + table + "`;"
// TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator
adapter.pushStatement(name, "drop-table", querystr) adapter.pushStatement(name, "drop-table", querystr)
return querystr, nil return querystr, nil
} }
func (adapter *MysqlAdapter) CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) { func (adapter *MysqlAdapter) CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -133,6 +128,7 @@ func (adapter *MysqlAdapter) CreateTable(name string, table string, charset stri
querystr += " COLLATE " + collation querystr += " COLLATE " + collation
} }
// TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator
adapter.pushStatement(name, "create-table", querystr+";") adapter.pushStatement(name, "create-table", querystr+";")
return querystr + ";", nil return querystr + ";", nil
} }
@ -178,23 +174,18 @@ func (adapter *MysqlAdapter) parseColumn(column DBTableColumn) (col DBTableColum
// TODO: Support AFTER column // TODO: Support AFTER column
// TODO: Test to make sure everything works here // TODO: Test to make sure everything works here
func (adapter *MysqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) { func (adapter *MysqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
column, size, end := adapter.parseColumn(column) column, size, end := adapter.parseColumn(column)
querystr := "ALTER TABLE `" + table + "` ADD COLUMN " + "`" + column.Name + "` " + column.Type + size + end + ";" querystr := "ALTER TABLE `" + table + "` ADD COLUMN " + "`" + column.Name + "` " + column.Type + size + end + ";"
// TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator
adapter.pushStatement(name, "add-column", querystr) adapter.pushStatement(name, "add-column", querystr)
return querystr, nil return querystr, nil
} }
func (adapter *MysqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) { func (adapter *MysqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -218,6 +209,7 @@ func (adapter *MysqlAdapter) SimpleInsert(name string, table string, columns str
} }
querystr += ")" querystr += ")"
// TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator
adapter.pushStatement(name, "insert", querystr) adapter.pushStatement(name, "insert", querystr)
return querystr, nil return querystr, nil
} }
@ -239,9 +231,6 @@ func (adapter *MysqlAdapter) buildColumns(columns string) (querystr string) {
// ! DEPRECATED // ! DEPRECATED
func (adapter *MysqlAdapter) SimpleReplace(name string, table string, columns string, fields string) (string, error) { func (adapter *MysqlAdapter) SimpleReplace(name string, table string, columns string, fields string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -258,14 +247,12 @@ func (adapter *MysqlAdapter) SimpleReplace(name string, table string, columns st
} }
querystr = querystr[0 : len(querystr)-1] querystr = querystr[0 : len(querystr)-1]
// TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator
adapter.pushStatement(name, "replace", querystr+")") adapter.pushStatement(name, "replace", querystr+")")
return querystr + ")", nil return querystr + ")", nil
} }
func (adapter *MysqlAdapter) SimpleUpsert(name string, table string, columns string, fields string, where string) (string, error) { func (adapter *MysqlAdapter) SimpleUpsert(name string, table string, columns string, fields string, where string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -305,23 +292,21 @@ func (adapter *MysqlAdapter) SimpleUpsert(name string, table string, columns str
querystr += insertColumns + setBit querystr += insertColumns + setBit
// TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator
adapter.pushStatement(name, "upsert", querystr) adapter.pushStatement(name, "upsert", querystr)
return querystr, nil return querystr, nil
} }
func (adapter *MysqlAdapter) SimpleUpdate(name string, table string, set string, where string) (string, error) { func (adapter *MysqlAdapter) SimpleUpdate(up *updatePrebuilder) (string, error) {
if name == "" { if up.table == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
if set == "" { if up.set == "" {
return "", errors.New("You need to set data in this update statement") return "", errors.New("You need to set data in this update statement")
} }
var querystr = "UPDATE `" + table + "` SET " var querystr = "UPDATE `" + up.table + "` SET "
for _, item := range processSet(set) { for _, item := range processSet(up.set) {
querystr += "`" + item.Column + "` =" querystr += "`" + item.Column + "` ="
for _, token := range item.Expr { for _, token := range item.Expr {
switch token.Type { switch token.Type {
@ -337,20 +322,18 @@ func (adapter *MysqlAdapter) SimpleUpdate(name string, table string, set string,
} }
querystr = querystr[0 : len(querystr)-1] querystr = querystr[0 : len(querystr)-1]
whereStr, err := adapter.buildWhere(where) whereStr, err := adapter.buildWhere(up.where)
if err != nil { if err != nil {
return querystr, err return querystr, err
} }
querystr += whereStr querystr += whereStr
adapter.pushStatement(name, "update", querystr) // TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator
adapter.pushStatement(up.name, "update", querystr)
return querystr, nil return querystr, nil
} }
func (adapter *MysqlAdapter) SimpleDelete(name string, table string, where string) (string, error) { func (adapter *MysqlAdapter) SimpleDelete(name string, table string, where string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -378,15 +361,13 @@ func (adapter *MysqlAdapter) SimpleDelete(name string, table string, where strin
} }
querystr = strings.TrimSpace(querystr[0 : len(querystr)-4]) querystr = strings.TrimSpace(querystr[0 : len(querystr)-4])
// TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator
adapter.pushStatement(name, "delete", querystr) adapter.pushStatement(name, "delete", querystr)
return querystr, nil return querystr, nil
} }
// We don't want to accidentally wipe tables, so we'll have a separate method for purging tables instead // We don't want to accidentally wipe tables, so we'll have a separate method for purging tables instead
func (adapter *MysqlAdapter) Purge(name string, table string) (string, error) { func (adapter *MysqlAdapter) Purge(name string, table string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -459,9 +440,6 @@ func (adapter *MysqlAdapter) buildOrderby(orderby string) (querystr string) {
} }
func (adapter *MysqlAdapter) SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error) { func (adapter *MysqlAdapter) SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -490,9 +468,6 @@ func (adapter *MysqlAdapter) SimpleSelect(name string, table string, columns str
} }
func (adapter *MysqlAdapter) ComplexSelect(preBuilder *selectPrebuilder) (out string, err error) { func (adapter *MysqlAdapter) ComplexSelect(preBuilder *selectPrebuilder) (out string, err error) {
if preBuilder.name == "" {
return "", errors.New("You need a name for this statement")
}
if preBuilder.table == "" { if preBuilder.table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -531,9 +506,6 @@ func (adapter *MysqlAdapter) ComplexSelect(preBuilder *selectPrebuilder) (out st
} }
func (adapter *MysqlAdapter) SimpleLeftJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) { func (adapter *MysqlAdapter) SimpleLeftJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table1 == "" { if table1 == "" {
return "", errors.New("You need a name for the left table") return "", errors.New("You need a name for the left table")
} }
@ -560,9 +532,6 @@ func (adapter *MysqlAdapter) SimpleLeftJoin(name string, table1 string, table2 s
} }
func (adapter *MysqlAdapter) SimpleInnerJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) { func (adapter *MysqlAdapter) SimpleInnerJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table1 == "" { if table1 == "" {
return "", errors.New("You need a name for the left table") return "", errors.New("You need a name for the left table")
} }
@ -588,6 +557,37 @@ func (adapter *MysqlAdapter) SimpleInnerJoin(name string, table1 string, table2
return querystr, nil return querystr, nil
} }
func (adapter *MysqlAdapter) SimpleUpdateSelect(up *updatePrebuilder) (string, error) {
sel := up.whereSubQuery
whereStr, err := adapter.buildWhere(sel.where)
if err != nil {
return "", err
}
var setter string
for _, item := range processSet(up.set) {
setter += "`" + item.Column + "` ="
for _, token := range item.Expr {
switch token.Type {
case "function", "operator", "number", "substitute", "or":
setter += " " + token.Contents
case "column":
setter += " `" + token.Contents + "`"
case "string":
setter += " '" + token.Contents + "'"
}
}
setter += ","
}
setter = setter[0 : len(setter)-1]
var querystr = "UPDATE `" + up.table + "` SET " + setter + " WHERE (SELECT" + adapter.buildJoinColumns(sel.columns) + " FROM `" + sel.table + "`" + whereStr + adapter.buildOrderby(sel.orderby) + adapter.buildLimit(sel.limit) + ")"
querystr = strings.TrimSpace(querystr)
adapter.pushStatement(up.name, "update", querystr)
return querystr, nil
}
func (adapter *MysqlAdapter) SimpleInsertSelect(name string, ins DBInsert, sel DBSelect) (string, error) { func (adapter *MysqlAdapter) SimpleInsertSelect(name string, ins DBInsert, sel DBSelect) (string, error) {
whereStr, err := adapter.buildWhere(sel.Where) whereStr, err := adapter.buildWhere(sel.Where)
if err != nil { if err != nil {
@ -692,9 +692,6 @@ func (adapter *MysqlAdapter) SimpleInsertInnerJoin(name string, ins DBInsert, se
} }
func (adapter *MysqlAdapter) SimpleCount(name string, table string, where string, limit string) (querystr string, err error) { func (adapter *MysqlAdapter) SimpleCount(name string, table string, where string, limit string) (querystr string, err error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -778,7 +775,7 @@ func _gen_mysql() (err error) {
// Internal methods, not exposed in the interface // Internal methods, not exposed in the interface
func (adapter *MysqlAdapter) pushStatement(name string, stype string, querystr string) { func (adapter *MysqlAdapter) pushStatement(name string, stype string, querystr string) {
if name[0] == '_' { if name == "" {
return return
} }
adapter.Buffer[name] = DBStmt{querystr, stype} adapter.Buffer[name] = DBStmt{querystr, stype}

View File

@ -43,9 +43,6 @@ func (adapter *PgsqlAdapter) DbVersion() string {
} }
func (adapter *PgsqlAdapter) DropTable(name string, table string) (string, error) { func (adapter *PgsqlAdapter) DropTable(name string, table string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -57,9 +54,6 @@ func (adapter *PgsqlAdapter) DropTable(name string, table string) (string, error
// TODO: Implement this // TODO: Implement this
// We may need to change the CreateTable API to better suit PGSQL and the other database drivers which are coming up // We may need to change the CreateTable API to better suit PGSQL and the other database drivers which are coming up
func (adapter *PgsqlAdapter) CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) { func (adapter *PgsqlAdapter) CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -120,9 +114,6 @@ func (adapter *PgsqlAdapter) CreateTable(name string, table string, charset stri
// TODO: Implement this // TODO: Implement this
func (adapter *PgsqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) { func (adapter *PgsqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -132,9 +123,6 @@ func (adapter *PgsqlAdapter) AddColumn(name string, table string, column DBTable
// TODO: Test this // TODO: Test this
// ! We need to get the last ID out of this somehow, maybe add returning to every query? Might require some sort of wrapper over the sql statements // ! We need to get the last ID out of this somehow, maybe add returning to every query? Might require some sort of wrapper over the sql statements
func (adapter *PgsqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) { func (adapter *PgsqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -179,9 +167,6 @@ func (adapter *PgsqlAdapter) buildColumns(columns string) (querystr string) {
// TODO: Implement this // TODO: Implement this
func (adapter *PgsqlAdapter) SimpleReplace(name string, table string, columns string, fields string) (string, error) { func (adapter *PgsqlAdapter) SimpleReplace(name string, table string, columns string, fields string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -196,9 +181,6 @@ func (adapter *PgsqlAdapter) SimpleReplace(name string, table string, columns st
// TODO: Implement this // TODO: Implement this
func (adapter *PgsqlAdapter) SimpleUpsert(name string, table string, columns string, fields string, where string) (string, error) { func (adapter *PgsqlAdapter) SimpleUpsert(name string, table string, columns string, fields string, where string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -212,19 +194,16 @@ func (adapter *PgsqlAdapter) SimpleUpsert(name string, table string, columns str
} }
// TODO: Implemented, but we need CreateTable and a better installer to *test* it // TODO: Implemented, but we need CreateTable and a better installer to *test* it
func (adapter *PgsqlAdapter) SimpleUpdate(name string, table string, set string, where string) (string, error) { func (adapter *PgsqlAdapter) SimpleUpdate(up *updatePrebuilder) (string, error) {
if name == "" { if up.table == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
if set == "" { if up.set == "" {
return "", errors.New("You need to set data in this update statement") return "", errors.New("You need to set data in this update statement")
} }
var querystr = "UPDATE \"" + table + "\" SET " var querystr = "UPDATE \"" + up.table + "\" SET "
for _, item := range processSet(set) { for _, item := range processSet(up.set) {
querystr += "`" + item.Column + "` =" querystr += "`" + item.Column + "` ="
for _, token := range item.Expr { for _, token := range item.Expr {
switch token.Type { switch token.Type {
@ -248,9 +227,9 @@ func (adapter *PgsqlAdapter) SimpleUpdate(name string, table string, set string,
querystr = querystr[0 : len(querystr)-1] querystr = querystr[0 : len(querystr)-1]
// Add support for BETWEEN x.x // Add support for BETWEEN x.x
if len(where) != 0 { if len(up.where) != 0 {
querystr += " WHERE" querystr += " WHERE"
for _, loc := range processWhere(where) { for _, loc := range processWhere(up.where) {
for _, token := range loc.Expr { for _, token := range loc.Expr {
switch token.Type { switch token.Type {
case "function": case "function":
@ -274,15 +253,17 @@ func (adapter *PgsqlAdapter) SimpleUpdate(name string, table string, set string,
querystr = querystr[0 : len(querystr)-4] querystr = querystr[0 : len(querystr)-4]
} }
adapter.pushStatement(name, "update", querystr) adapter.pushStatement(up.name, "update", querystr)
return querystr, nil return querystr, nil
} }
// TODO: Implement this
func (adapter *PgsqlAdapter) SimpleUpdateSelect(up *updatePrebuilder) (string, error) {
return "", errors.New("not implemented")
}
// TODO: Implement this // TODO: Implement this
func (adapter *PgsqlAdapter) SimpleDelete(name string, table string, where string) (string, error) { func (adapter *PgsqlAdapter) SimpleDelete(name string, table string, where string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -295,9 +276,6 @@ func (adapter *PgsqlAdapter) SimpleDelete(name string, table string, where strin
// TODO: Implement this // TODO: Implement this
// We don't want to accidentally wipe tables, so we'll have a separate method for purging tables instead // We don't want to accidentally wipe tables, so we'll have a separate method for purging tables instead
func (adapter *PgsqlAdapter) Purge(name string, table string) (string, error) { func (adapter *PgsqlAdapter) Purge(name string, table string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -306,9 +284,6 @@ func (adapter *PgsqlAdapter) Purge(name string, table string) (string, error) {
// TODO: Implement this // TODO: Implement this
func (adapter *PgsqlAdapter) SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error) { func (adapter *PgsqlAdapter) SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -320,9 +295,6 @@ func (adapter *PgsqlAdapter) SimpleSelect(name string, table string, columns str
// TODO: Implement this // TODO: Implement this
func (adapter *PgsqlAdapter) ComplexSelect(prebuilder *selectPrebuilder) (string, error) { func (adapter *PgsqlAdapter) ComplexSelect(prebuilder *selectPrebuilder) (string, error) {
if prebuilder.name == "" {
return "", errors.New("You need a name for this statement")
}
if prebuilder.table == "" { if prebuilder.table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -334,9 +306,6 @@ func (adapter *PgsqlAdapter) ComplexSelect(prebuilder *selectPrebuilder) (string
// TODO: Implement this // TODO: Implement this
func (adapter *PgsqlAdapter) SimpleLeftJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) { func (adapter *PgsqlAdapter) SimpleLeftJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table1 == "" { if table1 == "" {
return "", errors.New("You need a name for the left table") return "", errors.New("You need a name for the left table")
} }
@ -354,9 +323,6 @@ func (adapter *PgsqlAdapter) SimpleLeftJoin(name string, table1 string, table2 s
// TODO: Implement this // TODO: Implement this
func (adapter *PgsqlAdapter) SimpleInnerJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) { func (adapter *PgsqlAdapter) SimpleInnerJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table1 == "" { if table1 == "" {
return "", errors.New("You need a name for the left table") return "", errors.New("You need a name for the left table")
} }
@ -389,9 +355,6 @@ func (adapter *PgsqlAdapter) SimpleInsertInnerJoin(name string, ins DBInsert, se
// TODO: Implement this // TODO: Implement this
func (adapter *PgsqlAdapter) SimpleCount(name string, table string, where string, limit string) (string, error) { func (adapter *PgsqlAdapter) SimpleCount(name string, table string, where string, limit string) (string, error) {
if name == "" {
return "", errors.New("You need a name for this statement")
}
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -454,7 +417,7 @@ func _gen_pgsql() (err error) {
// Internal methods, not exposed in the interface // Internal methods, not exposed in the interface
func (adapter *PgsqlAdapter) pushStatement(name string, stype string, querystr string) { func (adapter *PgsqlAdapter) pushStatement(name string, stype string, querystr string) {
if name[0] == '_' { if name == "" {
return return
} }
adapter.Buffer[name] = DBStmt{querystr, stype} adapter.Buffer[name] = DBStmt{querystr, stype}

View File

@ -110,7 +110,8 @@ type Adapter interface {
// TODO: Test this // TODO: Test this
AddColumn(name string, table string, column DBTableColumn) (string, error) AddColumn(name string, table string, column DBTableColumn) (string, error)
SimpleInsert(name string, table string, columns string, fields string) (string, error) SimpleInsert(name string, table string, columns string, fields string) (string, error)
SimpleUpdate(name string, table string, set string, where string) (string, error) SimpleUpdate(up *updatePrebuilder) (string, error)
SimpleUpdateSelect(up *updatePrebuilder) (string, error) // ! Experimental
SimpleDelete(name string, table string, where string) (string, error) SimpleDelete(name string, table string, where string) (string, error)
Purge(name string, table string) (string, error) Purge(name string, table string) (string, error)
SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error) SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error)

View File

@ -25,7 +25,7 @@ type TransactionBuilder struct {
} }
func (build *TransactionBuilder) SimpleDelete(table string, where string) (stmt *sql.Stmt, err error) { func (build *TransactionBuilder) SimpleDelete(table string, where string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleDelete("_builder", table, where) res, err := build.adapter.SimpleDelete("", table, where)
if err != nil { if err != nil {
return stmt, err return stmt, err
} }
@ -34,7 +34,7 @@ func (build *TransactionBuilder) SimpleDelete(table string, where string) (stmt
// Quick* versions refer to it being quick to type not the performance. For performance critical transactions, you might want to use the Simple* methods or the *Tx methods on the main builder. Alternate suggestions for names are welcome :) // Quick* versions refer to it being quick to type not the performance. For performance critical transactions, you might want to use the Simple* methods or the *Tx methods on the main builder. Alternate suggestions for names are welcome :)
func (build *TransactionBuilder) QuickDelete(table string, where string) *transactionStmt { func (build *TransactionBuilder) QuickDelete(table string, where string) *transactionStmt {
res, err := build.adapter.SimpleDelete("_builder", table, where) res, err := build.adapter.SimpleDelete("", table, where)
if err != nil { if err != nil {
return newTransactionStmt(nil, err) return newTransactionStmt(nil, err)
} }
@ -49,7 +49,7 @@ func (build *TransactionBuilder) QuickDelete(table string, where string) *transa
} }
func (build *TransactionBuilder) SimpleInsert(table string, columns string, fields string) (stmt *sql.Stmt, err error) { func (build *TransactionBuilder) SimpleInsert(table string, columns string, fields string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleInsert("_builder", table, columns, fields) res, err := build.adapter.SimpleInsert("", table, columns, fields)
if err != nil { if err != nil {
return stmt, err return stmt, err
} }
@ -57,7 +57,7 @@ func (build *TransactionBuilder) SimpleInsert(table string, columns string, fiel
} }
func (build *TransactionBuilder) QuickInsert(table string, where string) *transactionStmt { func (build *TransactionBuilder) QuickInsert(table string, where string) *transactionStmt {
res, err := build.adapter.SimpleDelete("_builder", table, where) res, err := build.adapter.SimpleDelete("", table, where)
if err != nil { if err != nil {
return newTransactionStmt(nil, err) return newTransactionStmt(nil, err)
} }

View File

@ -1,14 +1,8 @@
echo "Updating Gosora" echo "Updating Gosora"
rm ./schema/lastSchema.json
cp ./schema/schema.json ./schema/lastSchema.json
git stash git stash
git pull origin master git pull origin master
git stash apply git stash apply
echo "Patching Gosora" echo "Patching Gosora"
cd ./patcher go build -o Patcher "./patcher"
go generate
go build -o Patcher
mv ./Patcher ..
cd ..
./Patcher ./Patcher

View File

@ -89,7 +89,9 @@ func topicRoutes() *RouteGroup {
Action("routes.LockTopicSubmit", "/topic/lock/submit/").LitBefore("req.URL.Path += extraData"), Action("routes.LockTopicSubmit", "/topic/lock/submit/").LitBefore("req.URL.Path += extraData"),
Action("routes.UnlockTopicSubmit", "/topic/unlock/submit/", "extraData"), Action("routes.UnlockTopicSubmit", "/topic/unlock/submit/", "extraData"),
Action("routes.MoveTopicSubmit", "/topic/move/submit/", "extraData"), Action("routes.MoveTopicSubmit", "/topic/move/submit/", "extraData"),
Action("routes.LikeTopicSubmit", "/topic/like/submit/", "extraData").Before("ParseForm"), Action("routes.LikeTopicSubmit", "/topic/like/submit/", "extraData"),
UploadAction("routes.AddAttachToTopicSubmit", "/topic/attach/add/submit/", "extraData").MaxSizeVar("int(common.Config.MaxRequestSize)"),
Action("routes.RemoveAttachFromTopicSubmit", "/topic/attach/remove/submit/", "extraData"),
) )
} }
@ -99,7 +101,7 @@ func replyRoutes() *RouteGroup {
UploadAction("routes.CreateReplySubmit", "/reply/create/").MaxSizeVar("int(common.Config.MaxRequestSize)"), // TODO: Rename the route so it's /reply/create/submit/ UploadAction("routes.CreateReplySubmit", "/reply/create/").MaxSizeVar("int(common.Config.MaxRequestSize)"), // TODO: Rename the route so it's /reply/create/submit/
Action("routes.ReplyEditSubmit", "/reply/edit/submit/", "extraData"), Action("routes.ReplyEditSubmit", "/reply/edit/submit/", "extraData"),
Action("routes.ReplyDeleteSubmit", "/reply/delete/submit/", "extraData"), Action("routes.ReplyDeleteSubmit", "/reply/delete/submit/", "extraData"),
Action("routes.ReplyLikeSubmit", "/reply/like/submit/", "extraData").Before("ParseForm"), Action("routes.ReplyLikeSubmit", "/reply/like/submit/", "extraData"),
//MemberView("routes.ReplyEdit","/reply/edit/","extraData"), // No js fallback //MemberView("routes.ReplyEdit","/reply/edit/","extraData"), // No js fallback
//MemberView("routes.ReplyDelete","/reply/delete/","extraData"), // No js confirmation page? We could have a confirmation modal for the JS case //MemberView("routes.ReplyDelete","/reply/delete/","extraData"), // No js confirmation page? We could have a confirmation modal for the JS case
) )

View File

@ -30,6 +30,7 @@ var successJSONBytes = []byte(`{"success":"1"}`)
var phraseLoginAlerts = []byte(`{"msgs":[{"msg":"Login to see your alerts","path":"/accounts/login"}],"msgCount":0}`) var phraseLoginAlerts = []byte(`{"msgs":[{"msg":"Login to see your alerts","path":"/accounts/login"}],"msgCount":0}`)
// TODO: Refactor this endpoint // TODO: Refactor this endpoint
// TODO: Move this into the routes package
func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
// TODO: Don't make this too JSON dependent so that we can swap in newer more efficient formats // TODO: Don't make this too JSON dependent so that we can swap in newer more efficient formats
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@ -44,6 +45,7 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.R
} }
switch r.FormValue("module") { switch r.FormValue("module") {
// TODO: Split this into it's own function
case "dismiss-alert": case "dismiss-alert":
asid, err := strconv.Atoi(r.FormValue("asid")) asid, err := strconv.Atoi(r.FormValue("asid"))
if err != nil { if err != nil {
@ -61,6 +63,7 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.R
if common.EnableWebsockets && count > 0 { if common.EnableWebsockets && count > 0 {
_ = common.WsHub.PushMessage(user.ID, `{"event":"dismiss-alert","asid":`+strconv.Itoa(asid)+`}`) _ = common.WsHub.PushMessage(user.ID, `{"event":"dismiss-alert","asid":`+strconv.Itoa(asid)+`}`)
} }
// TODO: Split this into it's own function
case "alerts": // A feed of events tailored for a specific user case "alerts": // A feed of events tailored for a specific user
if !user.Loggedin { if !user.Loggedin {
w.Write(phraseLoginAlerts) w.Write(phraseLoginAlerts)

View File

@ -21,7 +21,7 @@ var forumStmts ForumStmts
func init() { func init() {
common.DbInits.Add(func(acc *qgen.Accumulator) error { common.DbInits.Add(func(acc *qgen.Accumulator) error {
forumStmts = ForumStmts{ forumStmts = ForumStmts{
getTopics: acc.Select("topics").Columns("tid, title, content, createdBy, is_closed, sticky, createdAt, lastReplyAt, lastReplyBy, parentID, views, postCount, likeCount").Where("parentID = ?").Orderby("sticky DESC, lastReplyAt DESC, createdBy DESC").Limit("?,?").Prepare(), getTopics: acc.Select("topics").Columns("tid, title, content, createdBy, is_closed, sticky, createdAt, lastReplyAt, lastReplyBy, lastReplyID, parentID, views, postCount, likeCount").Where("parentID = ?").Orderby("sticky DESC, lastReplyAt DESC, createdBy DESC").Limit("?,?").Prepare(),
} }
return acc.FirstError() return acc.FirstError()
}) })
@ -68,13 +68,12 @@ func ViewForum(w http.ResponseWriter, r *http.Request, user common.User, header
var reqUserList = make(map[int]bool) var reqUserList = make(map[int]bool)
for rows.Next() { for rows.Next() {
var topicItem = common.TopicsRow{ID: 0} var topicItem = common.TopicsRow{ID: 0}
err := rows.Scan(&topicItem.ID, &topicItem.Title, &topicItem.Content, &topicItem.CreatedBy, &topicItem.IsClosed, &topicItem.Sticky, &topicItem.CreatedAt, &topicItem.LastReplyAt, &topicItem.LastReplyBy, &topicItem.ParentID, &topicItem.ViewCount, &topicItem.PostCount, &topicItem.LikeCount) err := rows.Scan(&topicItem.ID, &topicItem.Title, &topicItem.Content, &topicItem.CreatedBy, &topicItem.IsClosed, &topicItem.Sticky, &topicItem.CreatedAt, &topicItem.LastReplyAt, &topicItem.LastReplyBy, &topicItem.LastReplyID, &topicItem.ParentID, &topicItem.ViewCount, &topicItem.PostCount, &topicItem.LikeCount)
if err != nil { if err != nil {
return common.InternalError(err, w, r) return common.InternalError(err, w, r)
} }
topicItem.Link = common.BuildTopicURL(common.NameToSlug(topicItem.Title), topicItem.ID) topicItem.Link = common.BuildTopicURL(common.NameToSlug(topicItem.Title), topicItem.ID)
topicItem.RelativeLastReplyAt = common.RelativeTime(topicItem.LastReplyAt)
// TODO: Create a specialised function with a bit less overhead for getting the last page for a post count // TODO: Create a specialised function with a bit less overhead for getting the last page for a post count
_, _, lastPage := common.PageOffset(topicItem.PostCount, 1, common.Config.ItemsPerPage) _, _, lastPage := common.PageOffset(topicItem.PostCount, 1, common.Config.ItemsPerPage)
topicItem.LastPage = lastPage topicItem.LastPage = lastPage

View File

@ -34,7 +34,7 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user common.User, heade
var err error var err error
var replyCreatedAt time.Time var replyCreatedAt time.Time
var replyContent, replyCreatedByName, replyRelativeCreatedAt, replyAvatar, replyMicroAvatar, replyTag, replyClassName string var replyContent, replyCreatedByName, replyAvatar, replyMicroAvatar, replyTag, replyClassName string
var rid, replyCreatedBy, replyLastEdit, replyLastEditBy, replyLines, replyGroup int var rid, replyCreatedBy, replyLastEdit, replyLastEditBy, replyLines, replyGroup int
var replyList []common.ReplyUser var replyList []common.ReplyUser
@ -98,11 +98,9 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user common.User, heade
replyLiked := false replyLiked := false
replyLikeCount := 0 replyLikeCount := 0
replyRelativeCreatedAt = common.RelativeTime(replyCreatedAt)
// TODO: Add a hook here // TODO: Add a hook here
replyList = append(replyList, common.ReplyUser{rid, puser.ID, replyContent, common.ParseMessage(replyContent, 0, ""), replyCreatedBy, common.BuildProfileURL(common.NameToSlug(replyCreatedByName), replyCreatedBy), replyCreatedByName, replyGroup, replyCreatedAt, replyRelativeCreatedAt, replyLastEdit, replyLastEditBy, replyAvatar, replyMicroAvatar, replyClassName, replyLines, replyTag, "", "", "", 0, "", replyLiked, replyLikeCount, "", ""}) replyList = append(replyList, common.ReplyUser{rid, puser.ID, replyContent, common.ParseMessage(replyContent, 0, ""), replyCreatedBy, common.BuildProfileURL(common.NameToSlug(replyCreatedByName), replyCreatedBy), replyCreatedByName, replyGroup, replyCreatedAt, replyLastEdit, replyLastEditBy, replyAvatar, replyMicroAvatar, replyClassName, replyLines, replyTag, "", "", "", 0, "", replyLiked, replyLikeCount, "", ""})
} }
err = rows.Err() err = rows.Err()
if err != nil { if err != nil {

View File

@ -1,14 +1,8 @@
package routes package routes
import ( import (
"crypto/sha256"
"database/sql" "database/sql"
"encoding/hex"
"io"
"log"
"net/http" "net/http"
"os"
"regexp"
"strconv" "strconv"
"strings" "strings"
@ -16,7 +10,6 @@ import (
"github.com/Azareal/Gosora/common/counters" "github.com/Azareal/Gosora/common/counters"
) )
// TODO: De-duplicate the upload logic
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 {
@ -45,70 +38,9 @@ func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user common.User)
// Handle the file attachments // Handle the file attachments
// TODO: Stop duplicating this code // TODO: Stop duplicating this code
if user.Perms.UploadFiles { if user.Perms.UploadFiles {
files, ok := r.MultipartForm.File["upload_files"] _, rerr := uploadAttachment(w, r, user, topic.ParentID, "forums", tid, "replies")
if ok { if rerr != nil {
if len(files) > 5 { return rerr
return common.LocalError("You can't attach more than five files", w, r, user)
}
for _, file := range files {
if file.Filename == "" {
continue
}
log.Print("file.Filename ", file.Filename)
extarr := strings.Split(file.Filename, ".")
if len(extarr) < 2 {
return common.LocalError("Bad file", w, r, user)
}
ext := extarr[len(extarr)-1]
// TODO: Can we do this without a regex?
reg, err := regexp.Compile("[^A-Za-z0-9]+")
if err != nil {
return common.LocalError("Bad file extension", w, r, user)
}
ext = strings.ToLower(reg.ReplaceAllString(ext, ""))
if !common.AllowedFileExts.Contains(ext) {
return common.LocalError("You're not allowed to upload files with this extension", w, r, user)
}
infile, err := file.Open()
if err != nil {
return common.LocalError("Upload failed", w, r, user)
}
defer infile.Close()
hasher := sha256.New()
_, err = io.Copy(hasher, infile)
if err != nil {
return common.LocalError("Upload failed [Hashing Failed]", w, r, user)
}
infile.Close()
checksum := hex.EncodeToString(hasher.Sum(nil))
filename := checksum + "." + ext
outfile, err := os.Create("." + "/attachs/" + filename)
if err != nil {
return common.LocalError("Upload failed [File Creation Failed]", w, r, user)
}
defer outfile.Close()
infile, err = file.Open()
if err != nil {
return common.LocalError("Upload failed", w, r, user)
}
defer infile.Close()
_, err = io.Copy(outfile, infile)
if err != nil {
return common.LocalError("Upload failed [Copy Failed]", w, r, user)
}
err = common.Attachments.Add(topic.ParentID, "forums", tid, "replies", user.ID, filename)
if err != nil {
return common.InternalError(err, w, r)
}
}
} }
} }
@ -127,8 +59,8 @@ func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user common.User)
var maxPollOptions = 10 var maxPollOptions = 10
var pollInputItems = make(map[int]string) var pollInputItems = make(map[int]string)
for key, values := range r.Form { for key, values := range r.Form {
common.DebugDetail("key: ", key) //common.DebugDetail("key: ", key)
common.DebugDetailf("values: %+v\n", values) //common.DebugDetailf("values: %+v\n", values)
for _, value := range values { for _, value := range values {
if strings.HasPrefix(key, "pollinputitem[") { if strings.HasPrefix(key, "pollinputitem[") {
halves := strings.Split(key, "[") halves := strings.Split(key, "[")

View File

@ -5,6 +5,7 @@ import (
"database/sql" "database/sql"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors"
"io" "io"
"log" "log"
"net/http" "net/http"
@ -22,6 +23,7 @@ import (
type TopicStmts struct { type TopicStmts struct {
getReplies *sql.Stmt getReplies *sql.Stmt
getLikedTopic *sql.Stmt getLikedTopic *sql.Stmt
updateAttachs *sql.Stmt
} }
var topicStmts TopicStmts var topicStmts TopicStmts
@ -32,6 +34,8 @@ func init() {
topicStmts = TopicStmts{ topicStmts = TopicStmts{
getReplies: acc.SimpleLeftJoin("replies", "users", "replies.rid, replies.content, replies.createdBy, replies.createdAt, replies.lastEdit, replies.lastEditBy, users.avatar, users.name, users.group, users.url_prefix, users.url_name, users.level, replies.ipaddress, replies.likeCount, replies.actionType", "replies.createdBy = users.uid", "replies.tid = ?", "replies.rid ASC", "?,?"), getReplies: acc.SimpleLeftJoin("replies", "users", "replies.rid, replies.content, replies.createdBy, replies.createdAt, replies.lastEdit, replies.lastEditBy, users.avatar, users.name, users.group, users.url_prefix, users.url_name, users.level, replies.ipaddress, replies.likeCount, replies.actionType", "replies.createdBy = users.uid", "replies.tid = ?", "replies.rid ASC", "?,?"),
getLikedTopic: acc.Select("likes").Columns("targetItem").Where("sentBy = ? && targetItem = ? && targetType = 'topics'").Prepare(), getLikedTopic: acc.Select("likes").Columns("targetItem").Where("sentBy = ? && targetItem = ? && targetType = 'topics'").Prepare(),
// TODO: Less race-y attachment count updates
updateAttachs: acc.Update("topics").Set("attachCount = ?").Where("tid = ?").Prepare(),
} }
return acc.FirstError() return acc.FirstError()
}) })
@ -51,7 +55,6 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header
} else if err != nil { } else if err != nil {
return common.InternalError(err, w, r) return common.InternalError(err, w, r)
} }
topic.ClassName = ""
ferr := common.ForumUserCheck(header, w, r, &user, topic.ParentID) ferr := common.ForumUserCheck(header, w, r, &user, topic.ParentID)
if ferr != nil { if ferr != nil {
@ -64,6 +67,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header
header.Zone = "view_topic" header.Zone = "view_topic"
header.Path = common.BuildTopicURL(common.NameToSlug(topic.Title), topic.ID) header.Path = common.BuildTopicURL(common.NameToSlug(topic.Title), topic.ID)
// TODO: Cache ContentHTML when possible?
topic.ContentHTML = common.ParseMessage(topic.Content, topic.ParentID, "forums") topic.ContentHTML = common.ParseMessage(topic.Content, topic.ParentID, "forums")
topic.ContentLines = strings.Count(topic.Content, "\n") topic.ContentLines = strings.Count(topic.Content, "\n")
@ -76,7 +80,6 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header
if postGroup.IsMod { if postGroup.IsMod {
topic.ClassName = common.Config.StaffCSS topic.ClassName = common.Config.StaffCSS
} }
topic.RelativeCreatedAt = common.RelativeTime(topic.CreatedAt)
forum, err := common.Forums.Get(topic.ParentID) forum, err := common.Forums.Get(topic.ParentID)
if err != nil { if err != nil {
@ -105,6 +108,15 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header
} }
} }
if topic.AttachCount > 0 {
attachs, err := common.Attachments.MiniTopicGet(topic.ID)
if err != nil {
// TODO: We might want to be a little permissive here in-case of a desync?
return common.InternalError(err, w, r)
}
topic.Attachments = attachs
}
// Calculate the offset // Calculate the offset
offset, page, lastPage := common.PageOffset(topic.PostCount, page, common.Config.ItemsPerPage) offset, page, lastPage := common.PageOffset(topic.PostCount, page, common.Config.ItemsPerPage)
pageList := common.Paginate(topic.PostCount, common.Config.ItemsPerPage, 5) pageList := common.Paginate(topic.PostCount, common.Config.ItemsPerPage, 5)
@ -150,33 +162,37 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header
// TODO: Make a function for this? Build a more sophisticated noavatar handling system? Do bulk user loads and let the common.UserStore initialise this? // TODO: Make a function for this? Build a more sophisticated noavatar handling system? Do bulk user loads and let the common.UserStore initialise this?
replyItem.Avatar, replyItem.MicroAvatar = common.BuildAvatar(replyItem.CreatedBy, replyItem.Avatar) replyItem.Avatar, replyItem.MicroAvatar = common.BuildAvatar(replyItem.CreatedBy, replyItem.Avatar)
replyItem.Tag = postGroup.Tag replyItem.Tag = postGroup.Tag
replyItem.RelativeCreatedAt = common.RelativeTime(replyItem.CreatedAt)
// We really shouldn't have inline HTML, we should do something about this... // We really shouldn't have inline HTML, we should do something about this...
if replyItem.ActionType != "" { if replyItem.ActionType != "" {
var action string
switch replyItem.ActionType { switch replyItem.ActionType {
case "lock": case "lock":
replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_lock", replyItem.UserLink, replyItem.CreatedByName) action = "lock"
replyItem.ActionIcon = "&#x1F512;&#xFE0E" replyItem.ActionIcon = "&#x1F512;&#xFE0E"
case "unlock": case "unlock":
replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_unlock", replyItem.UserLink, replyItem.CreatedByName) action = "unlock"
replyItem.ActionIcon = "&#x1F513;&#xFE0E" replyItem.ActionIcon = "&#x1F513;&#xFE0E"
case "stick": case "stick":
replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_stick", replyItem.UserLink, replyItem.CreatedByName) action = "stick"
replyItem.ActionIcon = "&#x1F4CC;&#xFE0E" replyItem.ActionIcon = "&#x1F4CC;&#xFE0E"
case "unstick": case "unstick":
replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_unstick", replyItem.UserLink, replyItem.CreatedByName) action = "unstick"
replyItem.ActionIcon = "&#x1F4CC;&#xFE0E" replyItem.ActionIcon = "&#x1F4CC;&#xFE0E"
case "move": case "move":
replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_move", replyItem.UserLink, replyItem.CreatedByName) action = "move"
replyItem.ActionIcon = ""
}
if action != "" {
replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_"+action, replyItem.UserLink, replyItem.CreatedByName)
} else {
// TODO: Only fire this off if a corresponding phrase for the ActionType doesn't exist? Or maybe have some sort of action registry? // TODO: Only fire this off if a corresponding phrase for the ActionType doesn't exist? Or maybe have some sort of action registry?
default:
replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_default", replyItem.ActionType) replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_default", replyItem.ActionType)
replyItem.ActionIcon = "" replyItem.ActionIcon = ""
} }
} }
if replyItem.LikeCount > 0 { if replyItem.LikeCount > 0 && user.Liked > 0 {
likedMap[replyItem.ID] = len(tpage.ItemList) likedMap[replyItem.ID] = len(tpage.ItemList)
likedQueryList = append(likedQueryList, replyItem.ID) likedQueryList = append(likedQueryList, replyItem.ID)
} }
@ -192,6 +208,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header
// TODO: Add a config setting to disable the liked query for a burst of extra speed // TODO: Add a config setting to disable the liked query for a burst of extra speed
if user.Liked > 0 && len(likedQueryList) > 1 /*&& user.LastLiked <= time.Now()*/ { if user.Liked > 0 && len(likedQueryList) > 1 /*&& user.LastLiked <= time.Now()*/ {
// TODO: Abstract this
rows, err := qgen.NewAcc().Select("likes").Columns("targetItem").Where("sentBy = ? AND targetType = 'replies'").In("targetItem", likedQueryList[1:]).Query(user.ID) rows, err := qgen.NewAcc().Select("likes").Columns("targetItem").Where("sentBy = ? AND targetType = 'replies'").In("targetItem", likedQueryList[1:]).Query(user.ID)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
return common.InternalError(err, w, r) return common.InternalError(err, w, r)
@ -219,6 +236,89 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header
return rerr return rerr
} }
// TODO: Avoid uploading this again if the attachment already exists? They'll resolve to the same hash either way, but we could save on some IO / bandwidth here
// TODO: Enforce the max request limit on all of this topic's attachments
// TODO: Test this route
func AddAttachToTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, stid string) common.RouteError {
tid, err := strconv.Atoi(stid)
if err != nil {
return common.LocalErrorJS(phrases.GetErrorPhrase("id_must_be_integer"), w, r)
}
topic, err := common.Topics.Get(tid)
if err != nil {
return common.NotFoundJS(w, r)
}
_, ferr := common.SimpleForumUserCheck(w, r, &user, topic.ParentID)
if ferr != nil {
return ferr
}
if !user.Perms.ViewTopic || !user.Perms.EditTopic || !user.Perms.UploadFiles {
return common.NoPermissionsJS(w, r, user)
}
if topic.IsClosed && !user.Perms.CloseTopic {
return common.NoPermissionsJS(w, r, user)
}
// Handle the file attachments
pathMap, rerr := uploadAttachment(w, r, user, topic.ParentID, "forums", tid, "topics")
if rerr != nil {
// TODO: This needs to be a JS error...
return rerr
}
if len(pathMap) == 0 {
return common.InternalErrorJS(errors.New("no paths for attachment add"), w, r)
}
var elemStr string
for path, aids := range pathMap {
elemStr += "\"" + path + "\":\"" + aids + "\","
}
if len(elemStr) > 1 {
elemStr = elemStr[:len(elemStr)-1]
}
w.Write([]byte(`{"success":"1","elems":[{` + elemStr + `}]}`))
return nil
}
func RemoveAttachFromTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, stid string) common.RouteError {
tid, err := strconv.Atoi(stid)
if err != nil {
return common.LocalErrorJS(phrases.GetErrorPhrase("id_must_be_integer"), w, r)
}
topic, err := common.Topics.Get(tid)
if err != nil {
return common.NotFoundJS(w, r)
}
_, ferr := common.SimpleForumUserCheck(w, r, &user, topic.ParentID)
if ferr != nil {
return ferr
}
if !user.Perms.ViewTopic || !user.Perms.EditTopic {
return common.NoPermissionsJS(w, r, user)
}
if topic.IsClosed && !user.Perms.CloseTopic {
return common.NoPermissionsJS(w, r, user)
}
for _, said := range strings.Split(r.PostFormValue("aids"), ",") {
aid, err := strconv.Atoi(said)
if err != nil {
return common.LocalErrorJS(phrases.GetErrorPhrase("id_must_be_integer"), w, r)
}
rerr := deleteAttachment(w, r, user, aid, true)
if rerr != nil {
// TODO: This needs to be a JS error...
return rerr
}
}
w.Write(successJSONBytes)
return nil
}
// ? - Should we add a new permission or permission zone (like per-forum permissions) specifically for profile comment creation // ? - Should we add a new permission or permission zone (like per-forum permissions) specifically for profile comment creation
// ? - Should we allow banned users to make reports? How should we handle report abuse? // ? - Should we allow banned users to make reports? How should we handle report abuse?
// TODO: Add a permission to stop certain users from using custom avatars // TODO: Add a permission to stop certain users from using custom avatars
@ -337,8 +437,6 @@ func CreateTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User)
var maxPollOptions = 10 var maxPollOptions = 10
var pollInputItems = make(map[int]string) var pollInputItems = make(map[int]string)
for key, values := range r.Form { for key, values := range r.Form {
//common.DebugDetail("key: ", key)
//common.DebugDetailf("values: %+v\n", values)
for _, value := range values { for _, value := range values {
if strings.HasPrefix(key, "pollinputitem[") { if strings.HasPrefix(key, "pollinputitem[") {
halves := strings.Split(key, "[") halves := strings.Split(key, "[")
@ -389,72 +487,10 @@ func CreateTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User)
} }
// Handle the file attachments // Handle the file attachments
// TODO: Stop duplicating this code
if user.Perms.UploadFiles { if user.Perms.UploadFiles {
files, ok := r.MultipartForm.File["upload_files"] _, rerr := uploadAttachment(w, r, user, fid, "forums", tid, "topics")
if ok { if rerr != nil {
if len(files) > 5 { return rerr
return common.LocalError("You can't attach more than five files", w, r, user)
}
for _, file := range files {
if file.Filename == "" {
continue
}
common.DebugLog("file.Filename ", file.Filename)
extarr := strings.Split(file.Filename, ".")
if len(extarr) < 2 {
return common.LocalError("Bad file", w, r, user)
}
ext := extarr[len(extarr)-1]
// TODO: Can we do this without a regex?
reg, err := regexp.Compile("[^A-Za-z0-9]+")
if err != nil {
return common.LocalError("Bad file extension", w, r, user)
}
ext = strings.ToLower(reg.ReplaceAllString(ext, ""))
if !common.AllowedFileExts.Contains(ext) {
return common.LocalError("You're not allowed to upload files with this extension", w, r, user)
}
infile, err := file.Open()
if err != nil {
return common.LocalError("Upload failed", w, r, user)
}
defer infile.Close()
hasher := sha256.New()
_, err = io.Copy(hasher, infile)
if err != nil {
return common.LocalError("Upload failed [Hashing Failed]", w, r, user)
}
infile.Close()
checksum := hex.EncodeToString(hasher.Sum(nil))
filename := checksum + "." + ext
outfile, err := os.Create("." + "/attachs/" + filename)
if err != nil {
return common.LocalError("Upload failed [File Creation Failed]", w, r, user)
}
defer outfile.Close()
infile, err = file.Open()
if err != nil {
return common.LocalError("Upload failed", w, r, user)
}
defer infile.Close()
_, err = io.Copy(outfile, infile)
if err != nil {
return common.LocalError("Upload failed [Copy Failed]", w, r, user)
}
err = common.Attachments.Add(fid, "forums", tid, "topics", user.ID, filename)
if err != nil {
return common.InternalError(err, w, r)
}
}
} }
} }
@ -464,6 +500,141 @@ func CreateTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User)
return nil return nil
} }
func uploadFilesWithHash(w http.ResponseWriter, r *http.Request, user common.User, dir string) (filenames []string, rerr common.RouteError) {
files, ok := r.MultipartForm.File["upload_files"]
if !ok {
return nil, nil
}
if len(files) > 5 {
return nil, common.LocalError("You can't attach more than five files", w, r, user)
}
for _, file := range files {
if file.Filename == "" {
continue
}
//common.DebugLog("file.Filename ", file.Filename)
extarr := strings.Split(file.Filename, ".")
if len(extarr) < 2 {
return nil, common.LocalError("Bad file", w, r, user)
}
ext := extarr[len(extarr)-1]
// TODO: Can we do this without a regex?
reg, err := regexp.Compile("[^A-Za-z0-9]+")
if err != nil {
return nil, common.LocalError("Bad file extension", w, r, user)
}
ext = strings.ToLower(reg.ReplaceAllString(ext, ""))
if !common.AllowedFileExts.Contains(ext) {
return nil, common.LocalError("You're not allowed to upload files with this extension", w, r, user)
}
infile, err := file.Open()
if err != nil {
return nil, common.LocalError("Upload failed", w, r, user)
}
defer infile.Close()
hasher := sha256.New()
_, err = io.Copy(hasher, infile)
if err != nil {
return nil, common.LocalError("Upload failed [Hashing Failed]", w, r, user)
}
infile.Close()
checksum := hex.EncodeToString(hasher.Sum(nil))
filename := checksum + "." + ext
outfile, err := os.Create(dir + filename)
if err != nil {
return nil, common.LocalError("Upload failed [File Creation Failed]", w, r, user)
}
defer outfile.Close()
infile, err = file.Open()
if err != nil {
return nil, common.LocalError("Upload failed", w, r, user)
}
defer infile.Close()
_, err = io.Copy(outfile, infile)
if err != nil {
return nil, common.LocalError("Upload failed [Copy Failed]", w, r, user)
}
filenames = append(filenames, filename)
}
return filenames, nil
}
// TODO: Add a table for the files and lock the file row when performing tasks related to the file
func deleteAttachment(w http.ResponseWriter, r *http.Request, user common.User, aid int, js bool) common.RouteError {
attach, err := common.Attachments.Get(aid)
if err == sql.ErrNoRows {
return common.NotFoundJSQ(w, r, nil, js)
} else if err != nil {
return common.InternalErrorJSQ(err, w, r, js)
}
err = common.Attachments.Delete(aid)
if err != nil {
return common.InternalErrorJSQ(err, w, r, js)
}
count := common.Attachments.CountInPath(attach.Path)
if err != nil {
return common.InternalErrorJSQ(err, w, r, js)
}
if count == 0 {
err := os.Remove("./attachs/" + attach.Path)
if err != nil {
return common.InternalErrorJSQ(err, w, r, js)
}
}
return nil
}
// TODO: Stop duplicating this code
// TODO: Use a transaction here
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)
files, rerr := uploadFilesWithHash(w, r, user, "./attachs/")
if rerr != nil {
return nil, rerr
}
for _, filename := range files {
aid, err := common.Attachments.Add(sid, sectionTable, oid, originTable, user.ID, filename)
if err != nil {
return nil, common.InternalError(err, w, r)
}
_, ok := pathMap[filename]
if ok {
pathMap[filename] += "," + strconv.Itoa(aid)
} else {
pathMap[filename] = strconv.Itoa(aid)
}
switch sectionTable {
case "topics":
_, err = topicStmts.updateAttachs.Exec(common.Attachments.CountInTopic(oid), oid)
if err != nil {
return nil, common.InternalError(err, w, r)
}
err = common.Topics.Reload(oid)
if err != nil {
return nil, common.InternalError(err, w, r)
}
}
}
return pathMap, nil
}
// TODO: Update the stats after edits so that we don't under or over decrement stats during deletes // TODO: Update the stats after edits so that we don't under or over decrement stats during deletes
// TODO: Disable stat updates in posts handled by plugin_guilds // TODO: Disable stat updates in posts handled by plugin_guilds
func EditTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, stid string) common.RouteError { func EditTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, stid string) common.RouteError {

View File

@ -25,21 +25,8 @@
{{end}} {{end}}
</div> </div>
{{if .CurrentUser.Loggedin}} {{if .CurrentUser.Loggedin}}
<div class="mod_floater auto_hide"> {{template "topics_mod_floater.html"}}
<form method="post">
<div class="mod_floater_head">
<span></span>
</div>
<div class="mod_floater_body">
<select class="mod_floater_options">
<option val="delete">{{lang "topic_list.moderate_delete"}}</option>
<option val="lock">{{lang "topic_list.moderate_lock"}}</option>
<option val="move">{{lang "topic_list.moderate_move"}}</option>
</select>
<button>{{lang "topic_list.moderate_run"}}</button>
</div>
</form>
</div>
{{if .CurrentUser.Perms.CreateTopic}} {{if .CurrentUser.Perms.CreateTopic}}
<div id="forum_topic_create_form" class="rowblock topic_create_form quick_create_form" style="display: none;" aria-label="{{lang "quick_topic.aria"}}"> <div id="forum_topic_create_form" class="rowblock topic_create_form quick_create_form" style="display: none;" aria-label="{{lang "quick_topic.aria"}}">
<form id="quick_post_form" enctype="multipart/form-data" action="/topic/create/submit/?session={{.CurrentUser.Session}}" method="post"></form> <form id="quick_post_form" enctype="multipart/form-data" action="/topic/create/submit/?session={{.CurrentUser.Session}}" method="post"></form>
@ -106,7 +93,7 @@
<a href="{{.LastUser.Link}}"><img src="{{.LastUser.MicroAvatar}}" height="64" alt="{{.LastUser.Name}}'s Avatar" title="{{.LastUser.Name}}'s Avatar" /></a> <a href="{{.LastUser.Link}}"><img src="{{.LastUser.MicroAvatar}}" height="64" alt="{{.LastUser.Name}}'s Avatar" title="{{.LastUser.Name}}'s Avatar" /></a>
<span> <span>
<a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;" title="{{.LastUser.Name}}">{{.LastUser.Name}}</a><br> <a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;" title="{{.LastUser.Name}}">{{.LastUser.Name}}</a><br>
<a href="{{.Link}}?page={{.LastPage}}" class="rowsmall lastReplyAt" title="{{abstime .LastReplyAt}}">{{.RelativeLastReplyAt}}</a> <a href="{{.Link}}?page={{.LastPage}}{{if .LastReplyID}}#post-{{.LastReplyID}}{{end}}" class="rowsmall lastReplyAt" title="{{abstime .LastReplyAt}}">{{reltime .LastReplyAt}}</a>
</span> </span>
</div> </div>
</div> </div>

View File

@ -77,7 +77,26 @@
</div> </div>
<div class="content_container"> <div class="content_container">
<div class="hide_on_edit topic_content user_content" itemprop="text">{{.Topic.ContentHTML}}</div> <div class="hide_on_edit topic_content user_content" itemprop="text">{{.Topic.ContentHTML}}</div>
{{if .CurrentUser.Loggedin}}{{if .CurrentUser.Perms.EditTopic}}<textarea name="topic_content" class="show_on_edit topic_content_input">{{.Topic.Content}}</textarea>{{end}}{{end}} {{if .CurrentUser.Loggedin}}{{if .CurrentUser.Perms.EditTopic}}<textarea name="topic_content" class="show_on_edit topic_content_input">{{.Topic.Content}}</textarea>
{{if .Topic.Attachments}}<div class="show_on_edit attach_edit_bay" tid="{{.Topic.ID}}">
{{range .Topic.Attachments}}
<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}}
<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_copy">Copy</button>
</div>
{{end}}
<div class="attach_item attach_item_buttons">
{{if .CurrentUser.Perms.UploadFiles}}
<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}}
<button class="attach_item_delete">Delete</button>
</div>
</div>{{end}}
{{end}}{{end}}
<div class="controls button_container{{if .Topic.LikeCount}} has_likes{{end}}"> <div class="controls button_container{{if .Topic.LikeCount}} has_likes{{end}}">
<div class="action_button_left"> <div class="action_button_left">
{{if .CurrentUser.Loggedin}} {{if .CurrentUser.Loggedin}}
@ -97,7 +116,7 @@
</div> </div>
<div class="action_button_right"> <div class="action_button_right">
<a class="action_button like_count hide_on_micro" aria-label="{{lang "topic.like_count_aria"}}">{{.Topic.LikeCount}}</a> <a class="action_button like_count hide_on_micro" aria-label="{{lang "topic.like_count_aria"}}">{{.Topic.LikeCount}}</a>
<a class="action_button created_at hide_on_mobile" title="{{abstime .Topic.CreatedAt}}">{{.Topic.RelativeCreatedAt}}</a> <a class="action_button created_at hide_on_mobile" title="{{abstime .Topic.CreatedAt}}">{{reltime .Topic.CreatedAt}}</a>
{{if .CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.Topic.IPAddress}}" title="{{lang "topic.ip_full_tooltip"}}" class="action_button ip_item hide_on_mobile" aria-hidden="true">{{.Topic.IPAddress}}</a>{{end}} {{if .CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.Topic.IPAddress}}" title="{{lang "topic.ip_full_tooltip"}}" class="action_button ip_item hide_on_mobile" aria-hidden="true">{{.Topic.IPAddress}}</a>{{end}}
</div> </div>
</div> </div>

View File

@ -28,7 +28,7 @@
</div> </div>
<div class="action_button_right"> <div class="action_button_right">
<a class="action_button like_count hide_on_micro" aria-label="{{lang "topic.post_like_count_tooltip"}}">{{.LikeCount}}</a> <a class="action_button like_count hide_on_micro" aria-label="{{lang "topic.post_like_count_tooltip"}}">{{.LikeCount}}</a>
<a class="action_button created_at hide_on_mobile" title="{{abstime .CreatedAt}}">{{.RelativeCreatedAt}}</a> <a class="action_button created_at hide_on_mobile" title="{{abstime .CreatedAt}}">{{reltime .CreatedAt}}</a>
{{if $.CurrentUser.Loggedin}}{{if $.CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.IPAddress}}" title="IP Address" class="action_button ip_item hide_on_mobile" aria-hidden="true">{{.IPAddress}}</a>{{end}}{{end}} {{if $.CurrentUser.Loggedin}}{{if $.CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.IPAddress}}" title="IP Address" class="action_button ip_item hide_on_mobile" aria-hidden="true">{{.IPAddress}}</a>{{end}}{{end}}
</div> </div>
</div> </div>

View File

@ -32,22 +32,7 @@
</div> </div>
{{if .CurrentUser.Loggedin}} {{if .CurrentUser.Loggedin}}
{{/** TODO: Hide these from unauthorised users? **/}} {{template "topics_mod_floater.html"}}
<div class="mod_floater auto_hide">
<form method="post">
<div class="mod_floater_head">
<span></span>
</div>
<div class="mod_floater_body">
<select class="mod_floater_options">
<option val="delete">{{lang "topic_list.moderate_delete"}}</option>
<option val="lock">{{lang "topic_list.moderate_lock"}}</option>
<option val="move">{{lang "topic_list.moderate_move"}}</option>
</select>
<button class="mod_floater_submit">{{lang "topic_list.moderate_run"}}</button>
</div>
</form>
</div>
{{if .ForumList}} {{if .ForumList}}
{{/** TODO: Have a seperate forum list for moving topics? Maybe an AJAX forum search compatible with plugin_guilds? **/}} {{/** TODO: Have a seperate forum list for moving topics? Maybe an AJAX forum search compatible with plugin_guilds? **/}}

View File

@ -0,0 +1,16 @@
{{/** TODO: Hide these from unauthorised users? **/}}
<div class="mod_floater auto_hide">
<form method="post">
<div class="mod_floater_head">
<span></span>
</div>
<div class="mod_floater_body">
<select class="mod_floater_options">
<option val="delete">{{lang "topic_list.moderate_delete"}}</option>
<option val="lock">{{lang "topic_list.moderate_lock"}}</option>
<option val="move">{{lang "topic_list.moderate_move"}}</option>
</select>
<button class="mod_floater_submit">{{lang "topic_list.moderate_run"}}</button>
</div>
</form>
</div>

View File

@ -27,7 +27,7 @@
<a href="{{.LastUser.Link}}"><img src="{{.LastUser.MicroAvatar}}" height="64" alt="{{.LastUser.Name}}'s Avatar" title="{{.LastUser.Name}}'s Avatar" /></a> <a href="{{.LastUser.Link}}"><img src="{{.LastUser.MicroAvatar}}" height="64" alt="{{.LastUser.Name}}'s Avatar" title="{{.LastUser.Name}}'s Avatar" /></a>
<span> <span>
<a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;" title="{{.LastUser.Name}}">{{.LastUser.Name}}</a><br> <a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;" title="{{.LastUser.Name}}">{{.LastUser.Name}}</a><br>
<a href="{{.Link}}?page={{.LastPage}}" class="rowsmall lastReplyAt" title="{{abstime .LastReplyAt}}">{{.RelativeLastReplyAt}}</a> <a href="{{.Link}}?page={{.LastPage}}{{if .LastReplyID}}#post-{{.LastReplyID}}{{end}}" class="rowsmall lastReplyAt" title="{{abstime .LastReplyAt}}">{{reltime .LastReplyAt}}</a>
</span> </span>
</div> </div>
</div> </div>

View File

@ -1006,8 +1006,6 @@ textarea {
padding-right: 42px; padding-right: 42px;
padding-bottom: 18px; padding-bottom: 18px;
height: min-content; height: min-content;
/*overflow: hidden;
text-overflow: ellipsis;*/
} }
.user_meta { .user_meta {
display: flex; display: flex;
@ -1129,6 +1127,9 @@ textarea {
content: "{{lang "topic.report_button_text" .}}"; content: "{{lang "topic.report_button_text" .}}";
} }
.attach_edit_bay {
display: none;
}
.zone_view_topic .pageset { .zone_view_topic .pageset {
margin-bottom: 14px; margin-bottom: 14px;
} }

View File

@ -293,12 +293,17 @@ h2 {
.quick_create_form .topic_meta { .quick_create_form .topic_meta {
display: flex; display: flex;
} }
.quick_create_form input, .quick_create_form select {
margin-left: 0px;
margin-bottom: 0px;
}
.quick_create_form .topic_meta .topic_name_row { .quick_create_form .topic_meta .topic_name_row {
margin-bottom: 8px; margin-bottom: 8px;
width: 100%; width: 100%;
font-size: 14px;
} }
.quick_create_form .topic_meta .topic_name_row:not(:only-child) { .quick_create_form .topic_meta .topic_name_row:not(:only-child) {
margin-left: 8px; margin-left: 6px;
} }
.quick_create_form .topic_meta .topic_name_row:only-child input { .quick_create_form .topic_meta .topic_name_row:only-child input {
margin-left: 0px; margin-left: 0px;
@ -623,12 +628,24 @@ button, .formbutton, .panel_right_button:not(.has_inner_button) {
.topic_view_count:after { .topic_view_count:after {
content: "{{lang "topic.view_count_suffix" . }}"; content: "{{lang "topic.view_count_suffix" . }}";
} }
.edithead {
margin-left: 0px;
margin-bottom: 10px;
}
.topic_name_input { .topic_name_input {
width: 100%; width: 100%;
margin-right: 12px; margin-right: 10px;
margin-bottom: 0px;
margin-left: 0px;
margin-left: 0px;
} }
.topic_item .submit_edit { .topic_item .submit_edit {
margin-right: 16px; /*margin-right: 16px;*/
}
.zone_view_topic button, .zone_view_topic .formbutton {
padding: 5px;
padding-top: 4px;
padding-bottom: 4px;
} }
.postImage { .postImage {
width: 100%; width: 100%;
@ -688,7 +705,7 @@ button, .formbutton, .panel_right_button:not(.has_inner_button) {
flex-direction: column; flex-direction: column;
color: #bbbbbb; color: #bbbbbb;
} }
.action_item .content_container, .post_item .user_content { .action_item .content_container, .post_item .user_content, .post_item .button_container {
background-color: #444444; background-color: #444444;
border-radius: 3px; border-radius: 3px;
padding: 16px; padding: 16px;
@ -698,8 +715,6 @@ button, .formbutton, .panel_right_button:not(.has_inner_button) {
margin-top: 8px; margin-top: 8px;
margin-bottom: auto; margin-bottom: auto;
padding: 14px; padding: 14px;
background-color: #444444;
border-radius: 3px;
} }
.post_item .action_button { .post_item .action_button {
margin-right: 5px; margin-right: 5px;
@ -713,11 +728,7 @@ button, .formbutton, .panel_right_button:not(.has_inner_button) {
.post_item .action_button_right { .post_item .action_button_right {
margin-left: auto; margin-left: auto;
} }
.post_item .controls:not(.has_likes) .like_count { .post_item .controls:not(.has_likes) .like_count, .action_item .userinfo, .action_item .action_icon {
display: none;
}
.action_item .userinfo, .action_item .action_icon {
display: none; display: none;
} }
.action_item .content_container { .action_item .content_container {
@ -788,6 +799,49 @@ input[type=checkbox]:checked + label .sel {
content: "{{lang "topic.like_count_suffix" . }}"; content: "{{lang "topic.like_count_suffix" . }}";
} }
/*.attach_edit_bay {
display: flex;
flex-direction: row;
}*/
.attach_item {
display: flex;
background-color: #444444;
border-radius: 4px;
margin-top: 8px;
padding: 6px;
text-overflow: ellipsis;
overflow: hidden;
}
.attach_item_selected {
background-color: #446644
}
.attach_item img {
margin-right: 8px;
border-radius: 4px;
}
.attach_image_holder span {
margin-bottom: 4px;
}
.attach_edit_bay button {
margin-top: 8px;
margin-left: 8px;
}
/* New */
.attach_item {
padding: 8px;
width: 100%;
}
.attach_image_holder span {
margin-right: auto;
overflow: hidden;
text-overflow: ellipsis;
width: 300px;
}
.attach_item button {
margin-top: -1px;
}
.zone_view_topic .pageset { .zone_view_topic .pageset {
margin-bottom: 14px; margin-bottom: 14px;
} }

View File

@ -12,7 +12,9 @@
$(".alerts").html(alertCount + " new alerts"); $(".alerts").html(alertCount + " new alerts");
$(".user_box").addClass("has_alerts"); $(".user_box").addClass("has_alerts");
} }
}) });
addHook("open_edit", () => $('.topic_block').addClass("edithead"));
addHook("close_edit", () => $('.topic_block').removeClass("edithead"));
})(); })();
$(document).ready(() => { $(document).ready(() => {