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:
parent
548227104c
commit
3465e4c08f
|
@ -21,7 +21,6 @@ out/*
|
|||
*.log
|
||||
.DS_Store
|
||||
.vscode/launch.json
|
||||
schema/lastSchema.json
|
||||
config/config.go
|
||||
QueryGen
|
||||
RouterGen
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -2,28 +2,128 @@ package common
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/Azareal/Gosora/query_gen"
|
||||
)
|
||||
|
||||
var Attachments AttachmentStore
|
||||
|
||||
type MiniAttachment struct {
|
||||
ID int
|
||||
SectionID int
|
||||
OriginID int
|
||||
UploadedBy int
|
||||
Path string
|
||||
|
||||
Image bool
|
||||
Ext string
|
||||
}
|
||||
|
||||
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 {
|
||||
add *sql.Stmt
|
||||
get *sql.Stmt
|
||||
getByTopic *sql.Stmt
|
||||
add *sql.Stmt
|
||||
count *sql.Stmt
|
||||
countInTopic *sql.Stmt
|
||||
countInPath *sql.Stmt
|
||||
delete *sql.Stmt
|
||||
}
|
||||
|
||||
func NewDefaultAttachmentStore() (*DefaultAttachmentStore, error) {
|
||||
acc := qgen.NewAcc()
|
||||
return &DefaultAttachmentStore{
|
||||
add: acc.Insert("attachments").Columns("sectionID, sectionTable, originID, originTable, uploadedBy, path").Fields("?,?,?,?,?,?").Prepare(),
|
||||
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(),
|
||||
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()
|
||||
}
|
||||
|
||||
func (store *DefaultAttachmentStore) Add(sectionID int, sectionTable string, originID int, originTable string, uploadedBy int, path string) error {
|
||||
_, err := store.add.Exec(sectionID, sectionTable, originID, originTable, uploadedBy, path)
|
||||
// TODO: Make this more generic so we can use it for reply attachments too
|
||||
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
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"log"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/Azareal/Gosora/query_gen"
|
||||
)
|
||||
|
||||
|
@ -33,7 +34,7 @@ var TmplPtrMap = make(map[string]interface{})
|
|||
|
||||
// Anti-spam token with rotated key
|
||||
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 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
|
||||
|
||||
|
|
|
@ -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
|
||||
// ? - Add a JSQ and JS version of this?
|
||||
// ? - Add a JSQ version of this?
|
||||
// ? - Add a user parameter?
|
||||
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)
|
||||
}
|
||||
|
||||
// ? - 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
|
||||
func CustomError(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, header *Header, user User) RouteError {
|
||||
if header == nil {
|
||||
|
|
|
@ -94,7 +94,7 @@ func (list SFileList) JSTmplInit() error {
|
|||
|
||||
preLen := len(data)
|
||||
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
|
||||
fmt.Println("new endBrace: ", endBrace)
|
||||
fmt.Println("data: ", string(data))
|
||||
|
@ -130,58 +130,38 @@ func (list SFileList) JSTmplInit() error {
|
|||
}
|
||||
}
|
||||
each("strconv.Itoa(", 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
|
||||
braceAt, hasEndBrace := skipUntilIfExistsOrLine(data, index, ')')
|
||||
if hasEndBrace {
|
||||
data[braceAt] = ' ' // Blank it
|
||||
}
|
||||
})
|
||||
each("w.Write([]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 {
|
||||
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
|
||||
each("[]byte(", func(index int) {
|
||||
braceAt, hasEndBrace := skipUntilIfExistsOrLine(data, index, ')')
|
||||
if hasEndBrace {
|
||||
data[braceAt] = ' ' // Blank it
|
||||
}
|
||||
})
|
||||
each("w.Write(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 {
|
||||
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
|
||||
each("StringToBytes(", func(index int) {
|
||||
braceAt, hasEndBrace := skipUntilIfExistsOrLine(data, index, ')')
|
||||
if hasEndBrace {
|
||||
data[braceAt] = ' ' // Blank it
|
||||
}
|
||||
})
|
||||
each("w.Write(", 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
|
||||
braceAt, hasEndBrace := skipUntilIfExistsOrLine(data, index, ')')
|
||||
if hasEndBrace {
|
||||
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) {
|
||||
//fmt.Println("if index: ", index)
|
||||
braceAt, hasBrace := skipUntilIfExists(data, index, '{')
|
||||
braceAt, hasBrace := skipUntilIfExistsOrLine(data, index, '{')
|
||||
if hasBrace {
|
||||
if data[braceAt-1] != ' ' {
|
||||
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, shortName+"_tmpl_phrase_id = RegisterTmplPhraseNames([]string{", "[")
|
||||
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, " = []byte(", " = ")
|
||||
data = replace(data, " = StringToBytes(", " = ")
|
||||
data = replace(data, "[]byte(", "")
|
||||
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, "return nil", "return out")
|
||||
data = replace(data, " )", ")")
|
||||
|
|
|
@ -190,6 +190,18 @@ func skipUntilIfExists(tmplData []byte, i int, expects byte) (newI int, hasIt bo
|
|||
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) {
|
||||
j := i
|
||||
expectIndex := 0
|
||||
|
|
|
@ -11,17 +11,16 @@ import (
|
|||
var profileReplyStmts ProfileReplyStmts
|
||||
|
||||
type ProfileReply struct {
|
||||
ID int
|
||||
ParentID int
|
||||
Content string
|
||||
CreatedBy int
|
||||
Group int
|
||||
CreatedAt time.Time
|
||||
RelativeCreatedAt string
|
||||
LastEdit int
|
||||
LastEditBy int
|
||||
ContentLines int
|
||||
IPAddress string
|
||||
ID int
|
||||
ParentID int
|
||||
Content string
|
||||
CreatedBy int
|
||||
Group int
|
||||
CreatedAt time.Time
|
||||
LastEdit int
|
||||
LastEditBy int
|
||||
ContentLines int
|
||||
IPAddress string
|
||||
}
|
||||
|
||||
type ProfileReplyStmts struct {
|
||||
|
|
|
@ -16,48 +16,46 @@ import (
|
|||
)
|
||||
|
||||
type ReplyUser struct {
|
||||
ID int
|
||||
ParentID int
|
||||
Content string
|
||||
ContentHtml string
|
||||
CreatedBy int
|
||||
UserLink string
|
||||
CreatedByName string
|
||||
Group int
|
||||
CreatedAt time.Time
|
||||
RelativeCreatedAt string
|
||||
LastEdit int
|
||||
LastEditBy int
|
||||
Avatar string
|
||||
MicroAvatar string
|
||||
ClassName string
|
||||
ContentLines int
|
||||
Tag string
|
||||
URL string
|
||||
URLPrefix string
|
||||
URLName string
|
||||
Level int
|
||||
IPAddress string
|
||||
Liked bool
|
||||
LikeCount int
|
||||
ActionType string
|
||||
ActionIcon string
|
||||
ID int
|
||||
ParentID int
|
||||
Content string
|
||||
ContentHtml string
|
||||
CreatedBy int
|
||||
UserLink string
|
||||
CreatedByName string
|
||||
Group int
|
||||
CreatedAt time.Time
|
||||
LastEdit int
|
||||
LastEditBy int
|
||||
Avatar string
|
||||
MicroAvatar string
|
||||
ClassName string
|
||||
ContentLines int
|
||||
Tag string
|
||||
URL string
|
||||
URLPrefix string
|
||||
URLName string
|
||||
Level int
|
||||
IPAddress string
|
||||
Liked bool
|
||||
LikeCount int
|
||||
ActionType string
|
||||
ActionIcon string
|
||||
}
|
||||
|
||||
type Reply struct {
|
||||
ID int
|
||||
ParentID int
|
||||
Content string
|
||||
CreatedBy int
|
||||
Group int
|
||||
CreatedAt time.Time
|
||||
RelativeCreatedAt string
|
||||
LastEdit int
|
||||
LastEditBy int
|
||||
ContentLines int
|
||||
IPAddress string
|
||||
Liked bool
|
||||
LikeCount int
|
||||
ID int
|
||||
ParentID int
|
||||
Content string
|
||||
CreatedBy int
|
||||
Group int
|
||||
CreatedAt time.Time
|
||||
LastEdit int
|
||||
LastEditBy int
|
||||
ContentLines int
|
||||
IPAddress string
|
||||
Liked bool
|
||||
LikeCount int
|
||||
}
|
||||
|
||||
var ErrAlreadyLiked = errors.New("You already liked this!")
|
||||
|
|
|
@ -40,5 +40,5 @@ func (store *SQLReplyStore) Create(topic *Topic, content string, ipaddress strin
|
|||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(lastID), topic.AddReply(uid)
|
||||
return int(lastID), topic.AddReply(int(lastID), uid)
|
||||
}
|
||||
|
|
|
@ -341,7 +341,7 @@ func HandleUploadRoute(w http.ResponseWriter, r *http.Request, user User, maxFil
|
|||
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)
|
||||
}
|
||||
r.Body = http.MaxBytesReader(w, r.Body, int64(maxFileSize))
|
||||
r.Body = http.MaxBytesReader(w, r.Body, r.ContentLength)
|
||||
|
||||
err := r.ParseMultipartForm(int64(Megabyte))
|
||||
if err != nil {
|
||||
|
|
|
@ -226,11 +226,12 @@ func CompileTemplates() error {
|
|||
PollOption{1, "Something"},
|
||||
}, VoteCount: 7}
|
||||
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
|
||||
// TODO: Do we want the UID on this to be 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 compile = func(name string, expects string, expectsInt interface{}) (tmpl string, err error) {
|
||||
|
@ -285,7 +286,7 @@ func CompileTemplates() error {
|
|||
}
|
||||
|
||||
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"
|
||||
topicListPage := TopicListPage{header, topicsList, forumList, Config.DefaultForum, TopicListSort{"lastupdated", false}, Paginator{[]int{1}, 1, 1}}
|
||||
/*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
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -450,11 +451,12 @@ func CompileJSTemplates() error {
|
|||
PollOption{1, "Something"},
|
||||
}, VoteCount: 7}
|
||||
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
|
||||
// TODO: Do we really want the UID here to be zero?
|
||||
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)
|
||||
header.Title = "Topic Name"
|
||||
|
@ -639,10 +641,17 @@ func InitTemplates() error {
|
|||
if !ok {
|
||||
panic("timeInt is not a time.Time")
|
||||
}
|
||||
//return time.String()
|
||||
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{} {
|
||||
return ""
|
||||
}
|
||||
|
|
|
@ -91,6 +91,7 @@ func NewCTemplateSet() *CTemplateSet {
|
|||
//"langf":true,
|
||||
"level": true,
|
||||
"abstime": true,
|
||||
"reltime": true,
|
||||
"scope": true,
|
||||
"dyntmpl": true,
|
||||
},
|
||||
|
@ -959,6 +960,16 @@ ArgLoop:
|
|||
// TODO: Refactor this
|
||||
litString(leftParam+".Format(\"2006-01-02 15:04:05\")", false)
|
||||
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":
|
||||
literal = true
|
||||
break ArgLoop
|
||||
|
|
219
common/topic.go
219
common/topic.go
|
@ -22,51 +22,51 @@ import (
|
|||
// ? - Add a TopicMeta struct for *Forums?
|
||||
|
||||
type Topic struct {
|
||||
ID int
|
||||
Link string
|
||||
Title string
|
||||
Content string
|
||||
CreatedBy int
|
||||
IsClosed bool
|
||||
Sticky bool
|
||||
CreatedAt time.Time
|
||||
RelativeCreatedAt string
|
||||
LastReplyAt time.Time
|
||||
RelativeLastReplyAt string
|
||||
//LastReplyBy int
|
||||
ParentID int
|
||||
Status string // Deprecated. Marked for removal.
|
||||
IPAddress string
|
||||
ViewCount int64
|
||||
PostCount int
|
||||
LikeCount int
|
||||
ClassName string // CSS Class Name
|
||||
Poll int
|
||||
Data string // Used for report metadata
|
||||
ID int
|
||||
Link string
|
||||
Title string
|
||||
Content string
|
||||
CreatedBy int
|
||||
IsClosed bool
|
||||
Sticky bool
|
||||
CreatedAt time.Time
|
||||
LastReplyAt time.Time
|
||||
LastReplyBy int
|
||||
LastReplyID int
|
||||
ParentID int
|
||||
Status string // Deprecated. Marked for removal.
|
||||
IPAddress string
|
||||
ViewCount int64
|
||||
PostCount int
|
||||
LikeCount int
|
||||
AttachCount int
|
||||
ClassName string // CSS Class Name
|
||||
Poll int
|
||||
Data string // Used for report metadata
|
||||
}
|
||||
|
||||
type TopicUser struct {
|
||||
ID int
|
||||
Link string
|
||||
Title string
|
||||
Content string // TODO: Avoid converting this to bytes in templates, particularly if it's long
|
||||
CreatedBy int
|
||||
IsClosed bool
|
||||
Sticky bool
|
||||
CreatedAt time.Time
|
||||
RelativeCreatedAt string
|
||||
LastReplyAt time.Time
|
||||
RelativeLastReplyAt string
|
||||
//LastReplyBy int
|
||||
ParentID int
|
||||
Status string // Deprecated. Marked for removal.
|
||||
IPAddress string
|
||||
ViewCount int64
|
||||
PostCount int
|
||||
LikeCount int
|
||||
ClassName string
|
||||
Poll int
|
||||
Data string // Used for report metadata
|
||||
ID int
|
||||
Link string
|
||||
Title string
|
||||
Content string // TODO: Avoid converting this to bytes in templates, particularly if it's long
|
||||
CreatedBy int
|
||||
IsClosed bool
|
||||
Sticky bool
|
||||
CreatedAt time.Time
|
||||
LastReplyAt time.Time
|
||||
LastReplyBy int
|
||||
LastReplyID int
|
||||
ParentID int
|
||||
Status string // Deprecated. Marked for removal.
|
||||
IPAddress string
|
||||
ViewCount int64
|
||||
PostCount int
|
||||
LikeCount int
|
||||
AttachCount int
|
||||
ClassName string
|
||||
Poll int
|
||||
Data string // Used for report metadata
|
||||
|
||||
UserLink string
|
||||
CreatedByName string
|
||||
|
@ -81,30 +81,32 @@ type TopicUser struct {
|
|||
URLName string
|
||||
Level int
|
||||
Liked bool
|
||||
|
||||
Attachments []*MiniAttachment
|
||||
}
|
||||
|
||||
type TopicsRow struct {
|
||||
ID int
|
||||
Link string
|
||||
Title string
|
||||
Content string
|
||||
CreatedBy int
|
||||
IsClosed bool
|
||||
Sticky bool
|
||||
CreatedAt time.Time
|
||||
//RelativeCreatedAt string
|
||||
LastReplyAt time.Time
|
||||
RelativeLastReplyAt string
|
||||
LastReplyBy int
|
||||
ParentID int
|
||||
Status string // Deprecated. Marked for removal. -Is there anything we could use it for?
|
||||
IPAddress string
|
||||
ViewCount int64
|
||||
PostCount int
|
||||
LikeCount int
|
||||
LastPage int
|
||||
ClassName string
|
||||
Data string // Used for report metadata
|
||||
ID int
|
||||
Link string
|
||||
Title string
|
||||
Content string
|
||||
CreatedBy int
|
||||
IsClosed bool
|
||||
Sticky bool
|
||||
CreatedAt time.Time
|
||||
LastReplyAt time.Time
|
||||
LastReplyBy int
|
||||
LastReplyID int
|
||||
ParentID int
|
||||
Status string // Deprecated. Marked for removal. -Is there anything we could use it for?
|
||||
IPAddress string
|
||||
ViewCount int64
|
||||
PostCount int
|
||||
LikeCount int
|
||||
AttachCount int
|
||||
LastPage int
|
||||
ClassName string
|
||||
Data string // Used for report metadata
|
||||
|
||||
Creator *User
|
||||
CSS template.CSS
|
||||
|
@ -126,10 +128,12 @@ type WsTopicsRow struct {
|
|||
LastReplyAt time.Time
|
||||
RelativeLastReplyAt string
|
||||
LastReplyBy int
|
||||
LastReplyID int
|
||||
ParentID int
|
||||
ViewCount int64
|
||||
PostCount int
|
||||
LikeCount int
|
||||
AttachCount int
|
||||
ClassName string
|
||||
Creator *WsJSONUser
|
||||
LastUser *WsJSONUser
|
||||
|
@ -137,24 +141,26 @@ type WsTopicsRow struct {
|
|||
ForumLink string
|
||||
}
|
||||
|
||||
// TODO: Can we get the client side to render the relative times instead?
|
||||
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 {
|
||||
addRepliesToTopic *sql.Stmt
|
||||
lock *sql.Stmt
|
||||
unlock *sql.Stmt
|
||||
moveTo *sql.Stmt
|
||||
stick *sql.Stmt
|
||||
unstick *sql.Stmt
|
||||
hasLikedTopic *sql.Stmt
|
||||
createLike *sql.Stmt
|
||||
addLikesToTopic *sql.Stmt
|
||||
delete *sql.Stmt
|
||||
edit *sql.Stmt
|
||||
setPoll *sql.Stmt
|
||||
createActionReply *sql.Stmt
|
||||
addReplies *sql.Stmt
|
||||
updateLastReply *sql.Stmt
|
||||
lock *sql.Stmt
|
||||
unlock *sql.Stmt
|
||||
moveTo *sql.Stmt
|
||||
stick *sql.Stmt
|
||||
unstick *sql.Stmt
|
||||
hasLikedTopic *sql.Stmt
|
||||
createLike *sql.Stmt
|
||||
addLikesToTopic *sql.Stmt
|
||||
delete *sql.Stmt
|
||||
edit *sql.Stmt
|
||||
setPoll *sql.Stmt
|
||||
createAction *sql.Stmt
|
||||
|
||||
getTopicUser *sql.Stmt // TODO: Can we get rid of this?
|
||||
getByReplyID *sql.Stmt
|
||||
|
@ -165,21 +171,22 @@ var topicStmts TopicStmts
|
|||
func init() {
|
||||
DbInits.Add(func(acc *qgen.Accumulator) error {
|
||||
topicStmts = TopicStmts{
|
||||
addRepliesToTopic: acc.Update("topics").Set("postCount = postCount + ?, lastReplyBy = ?, lastReplyAt = UTC_TIMESTAMP()").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(),
|
||||
moveTo: acc.Update("topics").Set("parentID = ?").Where("tid = ?").Prepare(),
|
||||
stick: acc.Update("topics").Set("sticky = 1").Where("tid = ?").Prepare(),
|
||||
unstick: acc.Update("topics").Set("sticky = 0").Where("tid = ?").Prepare(),
|
||||
hasLikedTopic: acc.Select("likes").Columns("targetItem").Where("sentBy = ? and targetItem = ? and targetType = 'topics'").Prepare(),
|
||||
createLike: acc.Insert("likes").Columns("weight, targetItem, targetType, sentBy, createdAt").Fields("?,?,?,?,UTC_TIMESTAMP()").Prepare(),
|
||||
addLikesToTopic: acc.Update("topics").Set("likeCount = likeCount + ?").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?
|
||||
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(),
|
||||
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(),
|
||||
unlock: acc.Update("topics").Set("is_closed = 0").Where("tid = ?").Prepare(),
|
||||
moveTo: acc.Update("topics").Set("parentID = ?").Where("tid = ?").Prepare(),
|
||||
stick: acc.Update("topics").Set("sticky = 1").Where("tid = ?").Prepare(),
|
||||
unstick: acc.Update("topics").Set("sticky = 0").Where("tid = ?").Prepare(),
|
||||
hasLikedTopic: acc.Select("likes").Columns("targetItem").Where("sentBy = ? and targetItem = ? and targetType = 'topics'").Prepare(),
|
||||
createLike: acc.Insert("likes").Columns("weight, targetItem, targetType, sentBy, createdAt").Fields("?,?,?,?,UTC_TIMESTAMP()").Prepare(),
|
||||
addLikesToTopic: acc.Update("topics").Set("likeCount = likeCount + ?").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?
|
||||
setPoll: acc.Update("topics").Set("content = '', parsed_content = '', poll = ?").Where("tid = ? AND poll = 0").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 = ?", "", ""),
|
||||
}
|
||||
return acc.FirstError()
|
||||
|
@ -197,8 +204,12 @@ func (topic *Topic) cacheRemove() {
|
|||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
func (topic *Topic) AddReply(uid int) (err error) {
|
||||
_, err = topicStmts.addRepliesToTopic.Exec(1, uid, topic.ID)
|
||||
func (topic *Topic) AddReply(rid int, uid int) (err error) {
|
||||
_, err = topicStmts.addReplies.Exec(1, uid, topic.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = topicStmts.updateLastReply.Exec(rid, rid, topic.ID)
|
||||
topic.cacheRemove()
|
||||
return err
|
||||
}
|
||||
|
@ -314,11 +325,20 @@ func (topic *Topic) SetPoll(pollID int) error {
|
|||
|
||||
// TODO: Have this go through the ReplyStore?
|
||||
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 {
|
||||
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()
|
||||
// ? - Update the last topic cache for the parent forum?
|
||||
return err
|
||||
|
@ -336,7 +356,7 @@ func (topic *Topic) Copy() Topic {
|
|||
return *topic
|
||||
}
|
||||
|
||||
// TODO: Load LastReplyAt?
|
||||
// TODO: Load LastReplyAt and LastReplyID?
|
||||
func TopicByReplyID(rid int) (*Topic, error) {
|
||||
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)
|
||||
|
@ -376,14 +396,15 @@ func GetTopicUser(user *User, tid int) (tu TopicUser, err error) {
|
|||
}
|
||||
|
||||
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.Link = BuildTopicURL(NameToSlug(tu.Title), tu.ID)
|
||||
tu.UserLink = BuildProfileURL(NameToSlug(tu.CreatedByName), tu.CreatedBy)
|
||||
tu.Tag = Groups.DirtyGet(tu.Group).Tag
|
||||
|
||||
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)
|
||||
_ = tcache.Add(&theTopic)
|
||||
}
|
||||
|
@ -409,11 +430,13 @@ func copyTopicToTopicUser(topic *Topic, user *User) (tu TopicUser) {
|
|||
tu.Sticky = topic.Sticky
|
||||
tu.CreatedAt = topic.CreatedAt
|
||||
tu.LastReplyAt = topic.LastReplyAt
|
||||
tu.LastReplyBy = topic.LastReplyBy
|
||||
tu.ParentID = topic.ParentID
|
||||
tu.IPAddress = topic.IPAddress
|
||||
tu.ViewCount = topic.ViewCount
|
||||
tu.PostCount = topic.PostCount
|
||||
tu.LikeCount = topic.LikeCount
|
||||
tu.AttachCount = topic.AttachCount
|
||||
tu.Poll = topic.Poll
|
||||
tu.Data = topic.Data
|
||||
|
||||
|
|
|
@ -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
|
||||
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 {
|
||||
return nil, Paginator{nil, 1, 1}, err
|
||||
}
|
||||
|
@ -230,7 +230,7 @@ func (tList *DefaultTopicList) getList(page int, orderby string, argList []inter
|
|||
for rows.Next() {
|
||||
// TODO: Embed Topic structs in TopicsRow to make it easier for us to reuse this work in the topic cache
|
||||
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 {
|
||||
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.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
|
||||
_, _, lastPage := PageOffset(topicItem.PostCount, 1, Config.ItemsPerPage)
|
||||
topicItem.LastPage = lastPage
|
||||
|
|
|
@ -57,7 +57,7 @@ func NewDefaultTopicStore(cache TopicCache) (*DefaultTopicStore, error) {
|
|||
}
|
||||
return &DefaultTopicStore{
|
||||
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(),
|
||||
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(),
|
||||
|
@ -71,7 +71,7 @@ func (mts *DefaultTopicStore) DirtyGet(id int) *Topic {
|
|||
}
|
||||
|
||||
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 {
|
||||
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
|
||||
_ = mts.cache.Add(topic)
|
||||
|
@ -88,7 +88,7 @@ func (mts *DefaultTopicStore) Get(id int) (topic *Topic, err error) {
|
|||
}
|
||||
|
||||
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 {
|
||||
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
|
||||
_ = 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
|
||||
func (mts *DefaultTopicStore) BypassGet(id int) (*Topic, error) {
|
||||
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)
|
||||
return topic, err
|
||||
}
|
||||
|
||||
func (mts *DefaultTopicStore) Reload(id int) error {
|
||||
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 {
|
||||
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
|
||||
_ = mts.cache.Set(topic)
|
||||
|
|
|
@ -202,6 +202,7 @@ func ConvertByteInUnit(bytes float64, unit string) (count float64) {
|
|||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
// TODO: Localise this?
|
||||
func FriendlyUnitToBytes(quantity int, unit string) (bytes int, err error) {
|
||||
switch unit {
|
||||
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")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ func (hub *WsHubImpl) Start() {
|
|||
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 {
|
||||
// Don't waste CPU time if nothing has happened
|
||||
// TODO: Get a topic list method which strips stickies?
|
||||
|
|
|
@ -2,8 +2,6 @@ echo "Updating the dependencies"
|
|||
go get
|
||||
|
||||
echo "Updating Gosora"
|
||||
rm ./schema/lastSchema.json
|
||||
cp ./schema/schema.json ./schema/lastSchema.json
|
||||
git stash
|
||||
git pull origin master
|
||||
git stash apply
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
echo "Building the patcher"
|
||||
cp ./schema/schema.json ./schema/lastSchema.json
|
||||
go generate
|
||||
go build -o Patcher "./patcher"
|
|
@ -8,10 +8,6 @@ if %errorlevel% neq 0 (
|
|||
)
|
||||
|
||||
echo Updating Gosora
|
||||
cd schema
|
||||
del /Q lastSchema.json
|
||||
copy schema.json lastSchema.json
|
||||
cd ..
|
||||
git stash
|
||||
if %errorlevel% neq 0 (
|
||||
pause
|
||||
|
|
|
@ -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 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`.
|
||||
|
||||
|
@ -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 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:
|
||||
```
|
||||
cd ./patcher
|
||||
go build -o Patcher
|
||||
mv ./Patcher ..
|
||||
```
|
||||
After that, you'll need to run `go build -o Patcher "./patcher"`
|
||||
|
||||
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 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:
|
||||
```
|
||||
cd ./patcher
|
||||
go build -o Patcher
|
||||
mv ./Patcher ..
|
||||
```
|
||||
After that, you'll need to run `go build -o Patcher "./patcher"`
|
||||
|
||||
Once you've done that, you just need to run `./Patcher` to apply the latest patches to the database, etc.
|
||||
|
||||
|
|
180
gen_router.go
180
gen_router.go
|
@ -128,6 +128,8 @@ var RouteMap = map[string]interface{}{
|
|||
"routes.UnlockTopicSubmit": routes.UnlockTopicSubmit,
|
||||
"routes.MoveTopicSubmit": routes.MoveTopicSubmit,
|
||||
"routes.LikeTopicSubmit": routes.LikeTopicSubmit,
|
||||
"routes.AddAttachToTopicSubmit": routes.AddAttachToTopicSubmit,
|
||||
"routes.RemoveAttachFromTopicSubmit": routes.RemoveAttachFromTopicSubmit,
|
||||
"routes.ViewTopic": routes.ViewTopic,
|
||||
"routes.CreateReplySubmit": routes.CreateReplySubmit,
|
||||
"routes.ReplyEditSubmit": routes.ReplyEditSubmit,
|
||||
|
@ -261,29 +263,31 @@ var routeMapEnum = map[string]int{
|
|||
"routes.UnlockTopicSubmit": 103,
|
||||
"routes.MoveTopicSubmit": 104,
|
||||
"routes.LikeTopicSubmit": 105,
|
||||
"routes.ViewTopic": 106,
|
||||
"routes.CreateReplySubmit": 107,
|
||||
"routes.ReplyEditSubmit": 108,
|
||||
"routes.ReplyDeleteSubmit": 109,
|
||||
"routes.ReplyLikeSubmit": 110,
|
||||
"routes.ProfileReplyCreateSubmit": 111,
|
||||
"routes.ProfileReplyEditSubmit": 112,
|
||||
"routes.ProfileReplyDeleteSubmit": 113,
|
||||
"routes.PollVote": 114,
|
||||
"routes.PollResults": 115,
|
||||
"routes.AccountLogin": 116,
|
||||
"routes.AccountRegister": 117,
|
||||
"routes.AccountLogout": 118,
|
||||
"routes.AccountLoginSubmit": 119,
|
||||
"routes.AccountLoginMFAVerify": 120,
|
||||
"routes.AccountLoginMFAVerifySubmit": 121,
|
||||
"routes.AccountRegisterSubmit": 122,
|
||||
"routes.DynamicRoute": 123,
|
||||
"routes.UploadedFile": 124,
|
||||
"routes.StaticFile": 125,
|
||||
"routes.RobotsTxt": 126,
|
||||
"routes.SitemapXml": 127,
|
||||
"routes.BadRoute": 128,
|
||||
"routes.AddAttachToTopicSubmit": 106,
|
||||
"routes.RemoveAttachFromTopicSubmit": 107,
|
||||
"routes.ViewTopic": 108,
|
||||
"routes.CreateReplySubmit": 109,
|
||||
"routes.ReplyEditSubmit": 110,
|
||||
"routes.ReplyDeleteSubmit": 111,
|
||||
"routes.ReplyLikeSubmit": 112,
|
||||
"routes.ProfileReplyCreateSubmit": 113,
|
||||
"routes.ProfileReplyEditSubmit": 114,
|
||||
"routes.ProfileReplyDeleteSubmit": 115,
|
||||
"routes.PollVote": 116,
|
||||
"routes.PollResults": 117,
|
||||
"routes.AccountLogin": 118,
|
||||
"routes.AccountRegister": 119,
|
||||
"routes.AccountLogout": 120,
|
||||
"routes.AccountLoginSubmit": 121,
|
||||
"routes.AccountLoginMFAVerify": 122,
|
||||
"routes.AccountLoginMFAVerifySubmit": 123,
|
||||
"routes.AccountRegisterSubmit": 124,
|
||||
"routes.DynamicRoute": 125,
|
||||
"routes.UploadedFile": 126,
|
||||
"routes.StaticFile": 127,
|
||||
"routes.RobotsTxt": 128,
|
||||
"routes.SitemapXml": 129,
|
||||
"routes.BadRoute": 130,
|
||||
}
|
||||
var reverseRouteMapEnum = map[int]string{
|
||||
0: "routes.Overview",
|
||||
|
@ -392,29 +396,31 @@ var reverseRouteMapEnum = map[int]string{
|
|||
103: "routes.UnlockTopicSubmit",
|
||||
104: "routes.MoveTopicSubmit",
|
||||
105: "routes.LikeTopicSubmit",
|
||||
106: "routes.ViewTopic",
|
||||
107: "routes.CreateReplySubmit",
|
||||
108: "routes.ReplyEditSubmit",
|
||||
109: "routes.ReplyDeleteSubmit",
|
||||
110: "routes.ReplyLikeSubmit",
|
||||
111: "routes.ProfileReplyCreateSubmit",
|
||||
112: "routes.ProfileReplyEditSubmit",
|
||||
113: "routes.ProfileReplyDeleteSubmit",
|
||||
114: "routes.PollVote",
|
||||
115: "routes.PollResults",
|
||||
116: "routes.AccountLogin",
|
||||
117: "routes.AccountRegister",
|
||||
118: "routes.AccountLogout",
|
||||
119: "routes.AccountLoginSubmit",
|
||||
120: "routes.AccountLoginMFAVerify",
|
||||
121: "routes.AccountLoginMFAVerifySubmit",
|
||||
122: "routes.AccountRegisterSubmit",
|
||||
123: "routes.DynamicRoute",
|
||||
124: "routes.UploadedFile",
|
||||
125: "routes.StaticFile",
|
||||
126: "routes.RobotsTxt",
|
||||
127: "routes.SitemapXml",
|
||||
128: "routes.BadRoute",
|
||||
106: "routes.AddAttachToTopicSubmit",
|
||||
107: "routes.RemoveAttachFromTopicSubmit",
|
||||
108: "routes.ViewTopic",
|
||||
109: "routes.CreateReplySubmit",
|
||||
110: "routes.ReplyEditSubmit",
|
||||
111: "routes.ReplyDeleteSubmit",
|
||||
112: "routes.ReplyLikeSubmit",
|
||||
113: "routes.ProfileReplyCreateSubmit",
|
||||
114: "routes.ProfileReplyEditSubmit",
|
||||
115: "routes.ProfileReplyDeleteSubmit",
|
||||
116: "routes.PollVote",
|
||||
117: "routes.PollResults",
|
||||
118: "routes.AccountLogin",
|
||||
119: "routes.AccountRegister",
|
||||
120: "routes.AccountLogout",
|
||||
121: "routes.AccountLoginSubmit",
|
||||
122: "routes.AccountLoginMFAVerify",
|
||||
123: "routes.AccountLoginMFAVerifySubmit",
|
||||
124: "routes.AccountRegisterSubmit",
|
||||
125: "routes.DynamicRoute",
|
||||
126: "routes.UploadedFile",
|
||||
127: "routes.StaticFile",
|
||||
128: "routes.RobotsTxt",
|
||||
129: "routes.SitemapXml",
|
||||
130: "routes.BadRoute",
|
||||
}
|
||||
var osMapEnum = map[string]int{
|
||||
"unknown": 0,
|
||||
|
@ -705,7 +711,7 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||
counters.GlobalViewCounter.Bump()
|
||||
|
||||
if prefix == "/static" {
|
||||
counters.RouteViewCounter.Bump(125)
|
||||
counters.RouteViewCounter.Bump(127)
|
||||
req.URL.Path += extraData
|
||||
routes.StaticFile(w, req)
|
||||
return
|
||||
|
@ -1780,15 +1786,40 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
|||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
counters.RouteViewCounter.Bump(105)
|
||||
err = routes.LikeTopicSubmit(w,req,user,extraData)
|
||||
default:
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -1812,7 +1843,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
|||
return err
|
||||
}
|
||||
|
||||
counters.RouteViewCounter.Bump(107)
|
||||
counters.RouteViewCounter.Bump(109)
|
||||
err = routes.CreateReplySubmit(w,req,user)
|
||||
case "/reply/edit/submit/":
|
||||
err = common.NoSessionMismatch(w,req,user)
|
||||
|
@ -1825,7 +1856,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
|||
return err
|
||||
}
|
||||
|
||||
counters.RouteViewCounter.Bump(108)
|
||||
counters.RouteViewCounter.Bump(110)
|
||||
err = routes.ReplyEditSubmit(w,req,user,extraData)
|
||||
case "/reply/delete/submit/":
|
||||
err = common.NoSessionMismatch(w,req,user)
|
||||
|
@ -1838,7 +1869,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
|||
return err
|
||||
}
|
||||
|
||||
counters.RouteViewCounter.Bump(109)
|
||||
counters.RouteViewCounter.Bump(111)
|
||||
err = routes.ReplyDeleteSubmit(w,req,user,extraData)
|
||||
case "/reply/like/submit/":
|
||||
err = common.NoSessionMismatch(w,req,user)
|
||||
|
@ -1851,12 +1882,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
|||
return err
|
||||
}
|
||||
|
||||
err = common.ParseForm(w,req,user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
counters.RouteViewCounter.Bump(110)
|
||||
counters.RouteViewCounter.Bump(112)
|
||||
err = routes.ReplyLikeSubmit(w,req,user,extraData)
|
||||
}
|
||||
case "/profile":
|
||||
|
@ -1872,7 +1898,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
|||
return err
|
||||
}
|
||||
|
||||
counters.RouteViewCounter.Bump(111)
|
||||
counters.RouteViewCounter.Bump(113)
|
||||
err = routes.ProfileReplyCreateSubmit(w,req,user)
|
||||
case "/profile/reply/edit/submit/":
|
||||
err = common.NoSessionMismatch(w,req,user)
|
||||
|
@ -1885,7 +1911,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
|||
return err
|
||||
}
|
||||
|
||||
counters.RouteViewCounter.Bump(112)
|
||||
counters.RouteViewCounter.Bump(114)
|
||||
err = routes.ProfileReplyEditSubmit(w,req,user,extraData)
|
||||
case "/profile/reply/delete/submit/":
|
||||
err = common.NoSessionMismatch(w,req,user)
|
||||
|
@ -1898,7 +1924,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
|||
return err
|
||||
}
|
||||
|
||||
counters.RouteViewCounter.Bump(113)
|
||||
counters.RouteViewCounter.Bump(115)
|
||||
err = routes.ProfileReplyDeleteSubmit(w,req,user,extraData)
|
||||
}
|
||||
case "/poll":
|
||||
|
@ -1914,23 +1940,23 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
|||
return err
|
||||
}
|
||||
|
||||
counters.RouteViewCounter.Bump(114)
|
||||
counters.RouteViewCounter.Bump(116)
|
||||
err = routes.PollVote(w,req,user,extraData)
|
||||
case "/poll/results/":
|
||||
counters.RouteViewCounter.Bump(115)
|
||||
counters.RouteViewCounter.Bump(117)
|
||||
err = routes.PollResults(w,req,user,extraData)
|
||||
}
|
||||
case "/accounts":
|
||||
switch(req.URL.Path) {
|
||||
case "/accounts/login/":
|
||||
counters.RouteViewCounter.Bump(116)
|
||||
counters.RouteViewCounter.Bump(118)
|
||||
head, err := common.UserCheck(w,req,&user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = routes.AccountLogin(w,req,user,head)
|
||||
case "/accounts/create/":
|
||||
counters.RouteViewCounter.Bump(117)
|
||||
counters.RouteViewCounter.Bump(119)
|
||||
head, err := common.UserCheck(w,req,&user)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -1947,7 +1973,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
|||
return err
|
||||
}
|
||||
|
||||
counters.RouteViewCounter.Bump(118)
|
||||
counters.RouteViewCounter.Bump(120)
|
||||
err = routes.AccountLogout(w,req,user)
|
||||
case "/accounts/login/submit/":
|
||||
err = common.ParseForm(w,req,user)
|
||||
|
@ -1955,10 +1981,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
|||
return err
|
||||
}
|
||||
|
||||
counters.RouteViewCounter.Bump(119)
|
||||
counters.RouteViewCounter.Bump(121)
|
||||
err = routes.AccountLoginSubmit(w,req,user)
|
||||
case "/accounts/mfa_verify/":
|
||||
counters.RouteViewCounter.Bump(120)
|
||||
counters.RouteViewCounter.Bump(122)
|
||||
head, err := common.UserCheck(w,req,&user)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -1970,7 +1996,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
|||
return err
|
||||
}
|
||||
|
||||
counters.RouteViewCounter.Bump(121)
|
||||
counters.RouteViewCounter.Bump(123)
|
||||
err = routes.AccountLoginMFAVerifySubmit(w,req,user)
|
||||
case "/accounts/create/submit/":
|
||||
err = common.ParseForm(w,req,user)
|
||||
|
@ -1978,7 +2004,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
|||
return err
|
||||
}
|
||||
|
||||
counters.RouteViewCounter.Bump(122)
|
||||
counters.RouteViewCounter.Bump(124)
|
||||
err = routes.AccountRegisterSubmit(w,req,user)
|
||||
}
|
||||
/*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-Encoding")
|
||||
}
|
||||
counters.RouteViewCounter.Bump(124)
|
||||
counters.RouteViewCounter.Bump(126)
|
||||
req.URL.Path += extraData
|
||||
// TODO: Find a way to propagate errors up from this?
|
||||
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
|
||||
switch(extraData) {
|
||||
case "robots.txt":
|
||||
counters.RouteViewCounter.Bump(126)
|
||||
counters.RouteViewCounter.Bump(128)
|
||||
return routes.RobotsTxt(w,req)
|
||||
/*case "sitemap.xml":
|
||||
counters.RouteViewCounter.Bump(127)
|
||||
counters.RouteViewCounter.Bump(129)
|
||||
return routes.SitemapXml(w,req)*/
|
||||
}
|
||||
return common.NotFound(w,req,nil)
|
||||
|
@ -2018,7 +2044,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
|||
r.RUnlock()
|
||||
|
||||
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
|
||||
return handle(w,req,user)
|
||||
}
|
||||
|
@ -2029,7 +2055,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
|||
} else {
|
||||
r.DumpRequest(req,"Bad Route")
|
||||
}
|
||||
counters.RouteViewCounter.Bump(128)
|
||||
counters.RouteViewCounter.Bump(130)
|
||||
return common.NotFound(w,req,nil)
|
||||
}
|
||||
return err
|
||||
|
|
|
@ -92,7 +92,7 @@ func userStoreTest(t *testing.T, newUserID int) {
|
|||
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) {
|
||||
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))
|
||||
|
|
|
@ -2,6 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
@ -89,20 +90,29 @@ 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
|
||||
}
|
||||
|
||||
func patcher(scanner *bufio.Scanner) error {
|
||||
func loadSchema() (schemaFile SchemaFile, err error) {
|
||||
fmt.Println("Loading the schema file")
|
||||
data, err := ioutil.ReadFile("./schema/lastSchema.json")
|
||||
if err != nil {
|
||||
return err
|
||||
return schemaFile, err
|
||||
}
|
||||
|
||||
var schemaFile SchemaFile
|
||||
err = json.Unmarshal(data, &schemaFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbVersion, err := strconv.Atoi(schemaFile.DBVersion)
|
||||
if err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
dbVersion, err = strconv.Atoi(schemaFile.DBVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -113,6 +123,7 @@ func patcher(scanner *bufio.Scanner) error {
|
|||
}
|
||||
|
||||
// Run the queued up patches
|
||||
var patched int
|
||||
for index, patch := range pslice {
|
||||
if dbVersion > index {
|
||||
continue
|
||||
|
@ -121,6 +132,14 @@ func patcher(scanner *bufio.Scanner) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
patched++
|
||||
}
|
||||
|
||||
if patched > 0 {
|
||||
_, err := qgen.NewAcc().Update("updates").Set("dbVersion = ?").Exec(len(pslice))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -7,6 +7,9 @@ import (
|
|||
"github.com/Azareal/Gosora/query_gen"
|
||||
)
|
||||
|
||||
type tblColumn = qgen.DBTableColumn
|
||||
type tblKey = qgen.DBTableKey
|
||||
|
||||
func init() {
|
||||
addPatch(0, patch0)
|
||||
addPatch(1, patch1)
|
||||
|
@ -18,6 +21,7 @@ func init() {
|
|||
addPatch(7, patch7)
|
||||
addPatch(8, patch8)
|
||||
addPatch(9, patch9)
|
||||
addPatch(10, patch10)
|
||||
}
|
||||
|
||||
func patch0(scanner *bufio.Scanner) (err error) {
|
||||
|
@ -32,11 +36,11 @@ func patch0(scanner *bufio.Scanner) (err error) {
|
|||
}
|
||||
|
||||
err = execStmt(qgen.Builder.CreateTable("menus", "", "",
|
||||
[]qgen.DBTableColumn{
|
||||
qgen.DBTableColumn{"mid", "int", 0, false, true, ""},
|
||||
[]tblColumn{
|
||||
tblColumn{"mid", "int", 0, false, true, ""},
|
||||
},
|
||||
[]qgen.DBTableKey{
|
||||
qgen.DBTableKey{"mid", "primary"},
|
||||
[]tblKey{
|
||||
tblKey{"mid", "primary"},
|
||||
},
|
||||
))
|
||||
if err != nil {
|
||||
|
@ -44,26 +48,26 @@ func patch0(scanner *bufio.Scanner) (err error) {
|
|||
}
|
||||
|
||||
err = execStmt(qgen.Builder.CreateTable("menu_items", "", "",
|
||||
[]qgen.DBTableColumn{
|
||||
qgen.DBTableColumn{"miid", "int", 0, false, true, ""},
|
||||
qgen.DBTableColumn{"mid", "int", 0, false, false, ""},
|
||||
qgen.DBTableColumn{"name", "varchar", 200, false, false, ""},
|
||||
qgen.DBTableColumn{"htmlID", "varchar", 200, false, false, "''"},
|
||||
qgen.DBTableColumn{"cssClass", "varchar", 200, false, false, "''"},
|
||||
qgen.DBTableColumn{"position", "varchar", 100, false, false, ""},
|
||||
qgen.DBTableColumn{"path", "varchar", 200, false, false, "''"},
|
||||
qgen.DBTableColumn{"aria", "varchar", 200, false, false, "''"},
|
||||
qgen.DBTableColumn{"tooltip", "varchar", 200, false, false, "''"},
|
||||
qgen.DBTableColumn{"tmplName", "varchar", 200, false, false, "''"},
|
||||
qgen.DBTableColumn{"order", "int", 0, false, false, "0"},
|
||||
[]tblColumn{
|
||||
tblColumn{"miid", "int", 0, false, true, ""},
|
||||
tblColumn{"mid", "int", 0, false, false, ""},
|
||||
tblColumn{"name", "varchar", 200, false, false, ""},
|
||||
tblColumn{"htmlID", "varchar", 200, false, false, "''"},
|
||||
tblColumn{"cssClass", "varchar", 200, false, false, "''"},
|
||||
tblColumn{"position", "varchar", 100, false, false, ""},
|
||||
tblColumn{"path", "varchar", 200, false, false, "''"},
|
||||
tblColumn{"aria", "varchar", 200, false, false, "''"},
|
||||
tblColumn{"tooltip", "varchar", 200, false, false, "''"},
|
||||
tblColumn{"tmplName", "varchar", 200, false, false, "''"},
|
||||
tblColumn{"order", "int", 0, false, false, "0"},
|
||||
|
||||
qgen.DBTableColumn{"guestOnly", "boolean", 0, false, false, "0"},
|
||||
qgen.DBTableColumn{"memberOnly", "boolean", 0, false, false, "0"},
|
||||
qgen.DBTableColumn{"staffOnly", "boolean", 0, false, false, "0"},
|
||||
qgen.DBTableColumn{"adminOnly", "boolean", 0, false, false, "0"},
|
||||
tblColumn{"guestOnly", "boolean", 0, false, false, "0"},
|
||||
tblColumn{"memberOnly", "boolean", 0, false, false, "0"},
|
||||
tblColumn{"staffOnly", "boolean", 0, false, false, "0"},
|
||||
tblColumn{"adminOnly", "boolean", 0, false, false, "0"},
|
||||
},
|
||||
[]qgen.DBTableKey{
|
||||
qgen.DBTableKey{"miid", "primary"},
|
||||
[]tblKey{
|
||||
tblKey{"miid", "primary"},
|
||||
},
|
||||
))
|
||||
if err != nil {
|
||||
|
@ -159,25 +163,20 @@ func patch2(scanner *bufio.Scanner) error {
|
|||
}
|
||||
|
||||
func patch3(scanner *bufio.Scanner) error {
|
||||
err := execStmt(qgen.Builder.CreateTable("registration_logs", "", "",
|
||||
[]qgen.DBTableColumn{
|
||||
qgen.DBTableColumn{"rlid", "int", 0, false, true, ""},
|
||||
qgen.DBTableColumn{"username", "varchar", 100, false, false, ""},
|
||||
qgen.DBTableColumn{"email", "varchar", 100, false, false, ""},
|
||||
qgen.DBTableColumn{"failureReason", "varchar", 100, false, false, ""},
|
||||
qgen.DBTableColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed?
|
||||
qgen.DBTableColumn{"ipaddress", "varchar", 200, false, false, ""},
|
||||
qgen.DBTableColumn{"doneAt", "createdAt", 0, false, false, ""},
|
||||
return execStmt(qgen.Builder.CreateTable("registration_logs", "", "",
|
||||
[]tblColumn{
|
||||
tblColumn{"rlid", "int", 0, false, true, ""},
|
||||
tblColumn{"username", "varchar", 100, false, false, ""},
|
||||
tblColumn{"email", "varchar", 100, false, false, ""},
|
||||
tblColumn{"failureReason", "varchar", 100, false, false, ""},
|
||||
tblColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed?
|
||||
tblColumn{"ipaddress", "varchar", 200, false, false, ""},
|
||||
tblColumn{"doneAt", "createdAt", 0, false, false, ""},
|
||||
},
|
||||
[]qgen.DBTableKey{
|
||||
qgen.DBTableKey{"rlid", "primary"},
|
||||
[]tblKey{
|
||||
tblKey{"rlid", "primary"},
|
||||
},
|
||||
))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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",
|
||||
[]qgen.DBTableColumn{
|
||||
qgen.DBTableColumn{"pid", "int", 0, false, true, ""},
|
||||
qgen.DBTableColumn{"name", "varchar", 200, false, false, ""},
|
||||
qgen.DBTableColumn{"title", "varchar", 200, false, false, ""},
|
||||
qgen.DBTableColumn{"body", "text", 0, false, false, ""},
|
||||
qgen.DBTableColumn{"allowedGroups", "text", 0, false, false, ""},
|
||||
qgen.DBTableColumn{"menuID", "int", 0, false, false, "-1"},
|
||||
[]tblColumn{
|
||||
tblColumn{"pid", "int", 0, false, true, ""},
|
||||
tblColumn{"name", "varchar", 200, false, false, ""},
|
||||
tblColumn{"title", "varchar", 200, false, false, ""},
|
||||
tblColumn{"body", "text", 0, false, false, ""},
|
||||
tblColumn{"allowedGroups", "text", 0, false, false, ""},
|
||||
tblColumn{"menuID", "int", 0, false, false, "-1"},
|
||||
},
|
||||
[]qgen.DBTableKey{
|
||||
qgen.DBTableKey{"pid", "primary"},
|
||||
[]tblKey{
|
||||
tblKey{"pid", "primary"},
|
||||
},
|
||||
))
|
||||
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",
|
||||
[]qgen.DBTableColumn{
|
||||
qgen.DBTableColumn{"uid", "int", 0, false, false, ""},
|
||||
qgen.DBTableColumn{"secret", "varchar", 100, false, false, ""},
|
||||
qgen.DBTableColumn{"scratch1", "varchar", 50, false, false, ""},
|
||||
qgen.DBTableColumn{"scratch2", "varchar", 50, false, false, ""},
|
||||
qgen.DBTableColumn{"scratch3", "varchar", 50, false, false, ""},
|
||||
qgen.DBTableColumn{"scratch4", "varchar", 50, false, false, ""},
|
||||
qgen.DBTableColumn{"scratch5", "varchar", 50, false, false, ""},
|
||||
qgen.DBTableColumn{"scratch6", "varchar", 50, false, false, ""},
|
||||
qgen.DBTableColumn{"scratch7", "varchar", 50, false, false, ""},
|
||||
qgen.DBTableColumn{"scratch8", "varchar", 50, false, false, ""},
|
||||
qgen.DBTableColumn{"createdAt", "createdAt", 0, false, false, ""},
|
||||
[]tblColumn{
|
||||
tblColumn{"uid", "int", 0, false, false, ""},
|
||||
tblColumn{"secret", "varchar", 100, false, false, ""},
|
||||
tblColumn{"scratch1", "varchar", 50, false, false, ""},
|
||||
tblColumn{"scratch2", "varchar", 50, false, false, ""},
|
||||
tblColumn{"scratch3", "varchar", 50, false, false, ""},
|
||||
tblColumn{"scratch4", "varchar", 50, false, false, ""},
|
||||
tblColumn{"scratch5", "varchar", 50, false, false, ""},
|
||||
tblColumn{"scratch6", "varchar", 50, false, false, ""},
|
||||
tblColumn{"scratch7", "varchar", 50, false, false, ""},
|
||||
tblColumn{"scratch8", "varchar", 50, false, false, ""},
|
||||
tblColumn{"createdAt", "createdAt", 0, false, false, ""},
|
||||
},
|
||||
[]qgen.DBTableKey{
|
||||
qgen.DBTableKey{"uid", "primary"},
|
||||
[]tblKey{
|
||||
tblKey{"uid", "primary"},
|
||||
},
|
||||
))
|
||||
if err != nil {
|
||||
|
@ -292,28 +291,18 @@ func patch5(scanner *bufio.Scanner) error {
|
|||
}
|
||||
|
||||
func patch6(scanner *bufio.Scanner) error {
|
||||
err := execStmt(qgen.Builder.SimpleInsert("settings", "name, content, type", "'rapid_loading','1','bool'"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return execStmt(qgen.Builder.SimpleInsert("settings", "name, content, type", "'rapid_loading','1','bool'"))
|
||||
}
|
||||
|
||||
func patch7(scanner *bufio.Scanner) error {
|
||||
err := execStmt(qgen.Builder.CreateTable("users_avatar_queue", "", "",
|
||||
[]qgen.DBTableColumn{
|
||||
qgen.DBTableColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key
|
||||
return execStmt(qgen.Builder.CreateTable("users_avatar_queue", "", "",
|
||||
[]tblColumn{
|
||||
tblColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key
|
||||
},
|
||||
[]qgen.DBTableKey{
|
||||
qgen.DBTableKey{"uid", "primary"},
|
||||
[]tblKey{
|
||||
tblKey{"uid", "primary"},
|
||||
},
|
||||
))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func renameRoutes(routes map[string]string) error {
|
||||
|
@ -369,17 +358,12 @@ func patch8(scanner *bufio.Scanner) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = execStmt(qgen.Builder.CreateTable("updates", "", "",
|
||||
[]qgen.DBTableColumn{
|
||||
qgen.DBTableColumn{"dbVersion", "int", 0, false, false, "0"},
|
||||
return execStmt(qgen.Builder.CreateTable("updates", "", "",
|
||||
[]tblColumn{
|
||||
tblColumn{"dbVersion", "int", 0, false, false, "0"},
|
||||
},
|
||||
[]qgen.DBTableKey{},
|
||||
[]tblKey{},
|
||||
))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func patch9(scanner *bufio.Scanner) error {
|
||||
|
@ -389,21 +373,60 @@ func patch9(scanner *bufio.Scanner) error {
|
|||
return err
|
||||
}
|
||||
|
||||
err = execStmt(qgen.Builder.CreateTable("login_logs", "", "",
|
||||
[]qgen.DBTableColumn{
|
||||
qgen.DBTableColumn{"lid", "int", 0, false, true, ""},
|
||||
qgen.DBTableColumn{"uid", "int", 0, false, false, ""},
|
||||
qgen.DBTableColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed?
|
||||
qgen.DBTableColumn{"ipaddress", "varchar", 200, false, false, ""},
|
||||
qgen.DBTableColumn{"doneAt", "createdAt", 0, false, false, ""},
|
||||
return execStmt(qgen.Builder.CreateTable("login_logs", "", "",
|
||||
[]tblColumn{
|
||||
tblColumn{"lid", "int", 0, false, true, ""},
|
||||
tblColumn{"uid", "int", 0, false, false, ""},
|
||||
tblColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed?
|
||||
tblColumn{"ipaddress", "varchar", 200, false, false, ""},
|
||||
tblColumn{"doneAt", "createdAt", 0, false, false, ""},
|
||||
},
|
||||
[]qgen.DBTableKey{
|
||||
qgen.DBTableKey{"lid", "primary"},
|
||||
[]tblKey{
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
|
208
public/global.js
208
public/global.js
|
@ -224,11 +224,8 @@ function runWebSockets() {
|
|||
|
||||
// TODO: Add support for other alert feeds like PM Alerts
|
||||
var generalAlerts = document.getElementById("general_alerts");
|
||||
if(alertList.length < 8) {
|
||||
loadAlerts(generalAlerts);
|
||||
} else {
|
||||
updateAlertList(generalAlerts);
|
||||
}
|
||||
if(alertList.length < 8) loadAlerts(generalAlerts);
|
||||
else updateAlertList(generalAlerts);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -374,6 +371,7 @@ function mainInit(){
|
|||
event.preventDefault();
|
||||
$('.hide_on_edit').addClass("edit_opened");
|
||||
$('.show_on_edit').addClass("edit_opened");
|
||||
runHook("open_edit");
|
||||
});
|
||||
|
||||
$(".topic_item .submit_edit").click(function(event){
|
||||
|
@ -388,6 +386,7 @@ function mainInit(){
|
|||
|
||||
$('.hide_on_edit').removeClass("edit_opened");
|
||||
$('.show_on_edit').removeClass("edit_opened");
|
||||
runHook("close_edit");
|
||||
|
||||
let formAction = this.form.getAttribute("action");
|
||||
$.ajax({
|
||||
|
@ -566,74 +565,185 @@ function mainInit(){
|
|||
$(".topic_create_form").hide();
|
||||
});
|
||||
|
||||
function uploadFileHandler() {
|
||||
var fileList = this.files;
|
||||
// Truncate the number of files to 5
|
||||
function uploadFileHandler(fileList,maxFiles = 5, step1 = () => {}, step2 = () => {}) {
|
||||
let files = [];
|
||||
for(var i = 0; i < fileList.length && i < 5; i++) {
|
||||
files[i] = fileList[i];
|
||||
}
|
||||
|
||||
// Iterate over the files
|
||||
let totalSize = 0;
|
||||
for(let i = 0; i < files.length; i++) {
|
||||
console.log("files[" + i + "]",files[i]);
|
||||
totalSize += files[i]["size"];
|
||||
}
|
||||
if(totalSize > me.Site.MaxRequestSize) {
|
||||
throw("You can't upload this much at once, max: " + me.Site.MaxRequestSize);
|
||||
}
|
||||
|
||||
for(let i = 0; i < files.length; i++) {
|
||||
let reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
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();
|
||||
fileItem.innerText = "." + ext;
|
||||
fileItem.className = "formbutton uploadItem";
|
||||
fileItem.style.backgroundImage = "url("+e.target.result+")";
|
||||
|
||||
fileDock.appendChild(fileItem);
|
||||
reader.onload = (e) => {
|
||||
let filename = files[i]["name"];
|
||||
step1(e,filename)
|
||||
|
||||
let reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
crypto.subtle.digest('SHA-256',e.target.result)
|
||||
.then(function(hash) {
|
||||
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(function(hash) {
|
||||
console.log("hash",hash);
|
||||
let content = document.getElementById("input_content")
|
||||
console.log("content.value", content.value);
|
||||
|
||||
let attachItem;
|
||||
if(content.value == "") attachItem = "//" + window.location.host + "/attachs/" + hash + "." + ext;
|
||||
else attachItem = "\r\n//" + window.location.host + "/attachs/" + hash + "." + ext;
|
||||
content.value = content.value + attachItem;
|
||||
console.log("content.value", content.value);
|
||||
|
||||
// For custom / third party text editors
|
||||
attachItemCallback(attachItem);
|
||||
});
|
||||
}).then(hash => step2(e,hash,filename));
|
||||
}
|
||||
reader.readAsArrayBuffer(files[i]);
|
||||
}
|
||||
reader.readAsDataURL(files[i]);
|
||||
}
|
||||
if(totalSize > me.Site.MaxRequestSize) {
|
||||
}
|
||||
|
||||
// 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("You can't upload this much data at once, max: " + me.Site.MaxRequestSize);
|
||||
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.className = "formbutton uploadItem";
|
||||
// TODO: Check if this is actually an image
|
||||
fileItem.style.backgroundImage = "url("+e.target.result+")";
|
||||
|
||||
fileDock.appendChild(fileItem);
|
||||
},(e,hash, filename) => {
|
||||
console.log("hash",hash);
|
||||
let ext = getExt(filename)
|
||||
let content = document.getElementById("input_content")
|
||||
console.log("content.value", content.value);
|
||||
|
||||
let attachItem;
|
||||
if(content.value == "") attachItem = "//" + window.location.host + "/attachs/" + hash + "." + ext;
|
||||
else attachItem = "\r\n//" + window.location.host + "/attachs/" + hash + "." + ext;
|
||||
content.value = content.value + attachItem;
|
||||
console.log("content.value", content.value);
|
||||
|
||||
// For custom / third party text editors
|
||||
attachItemCallback(attachItem);
|
||||
});
|
||||
} catch(e) {
|
||||
// TODO: Use a notice instead
|
||||
alert(e);
|
||||
}
|
||||
}
|
||||
|
||||
var uploadFiles = document.getElementById("upload_files");
|
||||
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) => {
|
||||
event.preventDefault();
|
||||
|
@ -643,10 +753,11 @@ function mainInit(){
|
|||
$(this).click(function(){
|
||||
selectedTopics.push(parseInt($(this).attr("data-tid"),10));
|
||||
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 {
|
||||
$(".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");
|
||||
$(".mod_floater").removeClass("auto_hide");
|
||||
});
|
||||
|
@ -670,7 +781,6 @@ function mainInit(){
|
|||
let selectNode = this.form.querySelector(".mod_floater_options");
|
||||
let optionNode = selectNode.options[selectNode.selectedIndex];
|
||||
let action = optionNode.getAttribute("val");
|
||||
//console.log("action", action);
|
||||
|
||||
// Handle these specially
|
||||
switch(action) {
|
||||
|
|
|
@ -12,6 +12,8 @@ var hooks = {
|
|||
"after_phrases":[],
|
||||
"after_add_alert":[],
|
||||
"after_update_alert_list":[],
|
||||
"open_edit":[],
|
||||
"close_edit":[],
|
||||
};
|
||||
var ranInitHooks = {}
|
||||
|
||||
|
@ -130,7 +132,7 @@ function fetchPhrases() {
|
|||
(() => {
|
||||
runInitHook("pre_iife");
|
||||
let loggedIn = document.head.querySelector("[property='x-loggedin']").content;
|
||||
if(loggedIn) {
|
||||
if(loggedIn=="true") {
|
||||
fetch("/api/me/")
|
||||
.then((resp) => resp.json())
|
||||
.then((data) => {
|
||||
|
|
|
@ -40,28 +40,41 @@ func (builder *accDeleteBuilder) Run(args ...interface{}) (int, error) {
|
|||
}
|
||||
|
||||
type accUpdateBuilder struct {
|
||||
table string
|
||||
set string
|
||||
where string
|
||||
|
||||
up *updatePrebuilder
|
||||
build *Accumulator
|
||||
}
|
||||
|
||||
func (update *accUpdateBuilder) Set(set string) *accUpdateBuilder {
|
||||
update.set = set
|
||||
update.up.set = set
|
||||
return update
|
||||
}
|
||||
|
||||
func (update *accUpdateBuilder) Where(where string) *accUpdateBuilder {
|
||||
if update.where != "" {
|
||||
update.where += " AND "
|
||||
if update.up.where != "" {
|
||||
update.up.where += " AND "
|
||||
}
|
||||
update.where += where
|
||||
update.up.where += where
|
||||
return update
|
||||
}
|
||||
|
||||
func (update *accUpdateBuilder) Prepare() *sql.Stmt {
|
||||
return update.build.SimpleUpdate(update.table, update.set, update.where)
|
||||
func (update *accUpdateBuilder) WhereQ(sel *selectPrebuilder) *accUpdateBuilder {
|
||||
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 {
|
||||
|
@ -77,17 +90,22 @@ type AccSelectBuilder struct {
|
|||
build *Accumulator
|
||||
}
|
||||
|
||||
func (selectItem *AccSelectBuilder) Columns(columns string) *AccSelectBuilder {
|
||||
selectItem.columns = columns
|
||||
return selectItem
|
||||
func (builder *AccSelectBuilder) Columns(columns string) *AccSelectBuilder {
|
||||
builder.columns = columns
|
||||
return builder
|
||||
}
|
||||
|
||||
func (selectItem *AccSelectBuilder) Where(where string) *AccSelectBuilder {
|
||||
if selectItem.where != "" {
|
||||
selectItem.where += " AND "
|
||||
func (builder *AccSelectBuilder) Cols(columns string) *AccSelectBuilder {
|
||||
builder.columns = columns
|
||||
return builder
|
||||
}
|
||||
|
||||
func (builder *AccSelectBuilder) Where(where string) *AccSelectBuilder {
|
||||
if builder.where != "" {
|
||||
builder.where += " AND "
|
||||
}
|
||||
selectItem.where += where
|
||||
return selectItem
|
||||
builder.where += where
|
||||
return builder
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (selectItem *AccSelectBuilder) DateCutoff(column string, quantity int, unit string) *AccSelectBuilder {
|
||||
selectItem.dateCutoff = &dateCutoff{column, quantity, unit}
|
||||
return selectItem
|
||||
func (builder *AccSelectBuilder) DateCutoff(column string, quantity int, unit string) *AccSelectBuilder {
|
||||
builder.dateCutoff = &dateCutoff{column, quantity, unit}
|
||||
return builder
|
||||
}
|
||||
|
||||
func (selectItem *AccSelectBuilder) Orderby(orderby string) *AccSelectBuilder {
|
||||
selectItem.orderby = orderby
|
||||
return selectItem
|
||||
func (builder *AccSelectBuilder) Orderby(orderby string) *AccSelectBuilder {
|
||||
builder.orderby = orderby
|
||||
return builder
|
||||
}
|
||||
|
||||
func (selectItem *AccSelectBuilder) Limit(limit string) *AccSelectBuilder {
|
||||
selectItem.limit = limit
|
||||
return selectItem
|
||||
func (builder *AccSelectBuilder) Limit(limit string) *AccSelectBuilder {
|
||||
builder.limit = limit
|
||||
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.
|
||||
if selectItem.dateCutoff != nil || selectItem.inChain != nil {
|
||||
selectBuilder := selectItem.build.GetAdapter().Builder().Select().FromAcc(selectItem)
|
||||
return selectItem.build.prepare(selectItem.build.GetAdapter().ComplexSelect(selectBuilder))
|
||||
if builder.dateCutoff != nil || builder.inChain != nil {
|
||||
selectBuilder := builder.build.GetAdapter().Builder().Select().FromAcc(builder)
|
||||
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) {
|
||||
|
@ -145,15 +163,15 @@ func (builder *AccSelectBuilder) query() (string, error) {
|
|||
selectBuilder := builder.build.GetAdapter().Builder().Select().FromAcc(builder)
|
||||
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) {
|
||||
stmt := selectItem.Prepare()
|
||||
func (builder *AccSelectBuilder) Query(args ...interface{}) (*sql.Rows, error) {
|
||||
stmt := builder.Prepare()
|
||||
if stmt != nil {
|
||||
return stmt.Query(args...)
|
||||
}
|
||||
return nil, selectItem.build.FirstError()
|
||||
return nil, builder.build.FirstError()
|
||||
}
|
||||
|
||||
type AccRowWrap struct {
|
||||
|
@ -245,7 +263,7 @@ func (insert *accInsertBuilder) Prepare() *sql.Stmt {
|
|||
}
|
||||
|
||||
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 {
|
||||
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) {
|
||||
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 {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -292,4 +310,13 @@ func (count *accCountBuilder) Prepare() *sql.Stmt {
|
|||
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?
|
||||
|
|
|
@ -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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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
|
||||
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) {
|
||||
|
@ -155,63 +159,63 @@ func (build *Accumulator) prepareTx(tx *sql.Tx, res string, err error) (stmt *sq
|
|||
|
||||
// These ones support transactions
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
res, err := build.adapter.Purge("_builder", table)
|
||||
res, err := build.adapter.Purge("", table)
|
||||
return build.prepareTx(tx, res, err)
|
||||
}
|
||||
|
||||
|
@ -220,7 +224,7 @@ func (build *Accumulator) Delete(table string) *accDeleteBuilder {
|
|||
}
|
||||
|
||||
func (build *Accumulator) Update(table string) *accUpdateBuilder {
|
||||
return &accUpdateBuilder{table, "", "", build}
|
||||
return &accUpdateBuilder{qUpdate(table, "", ""), build}
|
||||
}
|
||||
|
||||
func (build *Accumulator) Select(table string) *AccSelectBuilder {
|
||||
|
|
|
@ -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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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
|
||||
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) {
|
||||
|
@ -150,62 +150,62 @@ func (build *builder) prepareTx(tx *sql.Tx, res string, err error) (*sql.Stmt, e
|
|||
|
||||
// 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) {
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
res, err := build.adapter.Purge("_builder", table)
|
||||
res, err := build.adapter.Purge("", table)
|
||||
return build.prepareTx(tx, res, err)
|
||||
}
|
||||
|
|
|
@ -11,22 +11,22 @@ type prebuilder struct {
|
|||
}
|
||||
|
||||
func (build *prebuilder) Select(nlist ...string) *selectPrebuilder {
|
||||
name := optString(nlist, "_builder")
|
||||
name := optString(nlist, "")
|
||||
return &selectPrebuilder{name, "", "", "", "", "", nil, nil, "", build.adapter}
|
||||
}
|
||||
|
||||
func (build *prebuilder) Insert(nlist ...string) *insertPrebuilder {
|
||||
name := optString(nlist, "_builder")
|
||||
name := optString(nlist, "")
|
||||
return &insertPrebuilder{name, "", "", "", build.adapter}
|
||||
}
|
||||
|
||||
func (build *prebuilder) Update(nlist ...string) *updatePrebuilder {
|
||||
name := optString(nlist, "_builder")
|
||||
return &updatePrebuilder{name, "", "", "", build.adapter}
|
||||
name := optString(nlist, "")
|
||||
return &updatePrebuilder{name, "", "", "", nil, build.adapter}
|
||||
}
|
||||
|
||||
func (build *prebuilder) Delete(nlist ...string) *deletePrebuilder {
|
||||
name := optString(nlist, "_builder")
|
||||
name := optString(nlist, "")
|
||||
return &deletePrebuilder{name, "", "", build.adapter}
|
||||
}
|
||||
|
||||
|
@ -60,14 +60,19 @@ func (delete *deletePrebuilder) Parse() {
|
|||
}
|
||||
|
||||
type updatePrebuilder struct {
|
||||
name string
|
||||
table string
|
||||
set string
|
||||
where string
|
||||
name string
|
||||
table string
|
||||
set string
|
||||
where string
|
||||
whereSubQuery *selectPrebuilder
|
||||
|
||||
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 {
|
||||
update.table = table
|
||||
return update
|
||||
|
@ -86,12 +91,17 @@ func (update *updatePrebuilder) Where(where string) *updatePrebuilder {
|
|||
return update
|
||||
}
|
||||
|
||||
func (update *updatePrebuilder) WhereQ(sel *selectPrebuilder) *updatePrebuilder {
|
||||
update.whereSubQuery = sel
|
||||
return update
|
||||
}
|
||||
|
||||
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() {
|
||||
update.build.SimpleUpdate(update.name, update.table, update.set, update.where)
|
||||
update.build.SimpleUpdate(update)
|
||||
}
|
||||
|
||||
type selectPrebuilder struct {
|
||||
|
@ -151,7 +161,7 @@ func (selectItem *selectPrebuilder) FromAcc(accBuilder *AccSelectBuilder) *selec
|
|||
|
||||
selectItem.dateCutoff = accBuilder.dateCutoff
|
||||
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
|
||||
}
|
||||
return selectItem
|
||||
|
|
|
@ -45,9 +45,6 @@ func (adapter *MssqlAdapter) DbVersion() string {
|
|||
}
|
||||
|
||||
func (adapter *MssqlAdapter) DropTable(name string, table string) (string, error) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if 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
|
||||
// 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) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if 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
|
||||
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 == "" {
|
||||
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) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if 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) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if 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
|
||||
}
|
||||
|
||||
func (adapter *MssqlAdapter) SimpleUpdate(name string, table string, set string, where string) (string, error) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if table == "" {
|
||||
func (adapter *MssqlAdapter) SimpleUpdate(up *updatePrebuilder) (string, error) {
|
||||
if up.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")
|
||||
}
|
||||
|
||||
var querystr = "UPDATE [" + table + "] SET "
|
||||
for _, item := range processSet(set) {
|
||||
var querystr = "UPDATE [" + up.table + "] SET "
|
||||
for _, item := range processSet(up.set) {
|
||||
querystr += "[" + item.Column + "] ="
|
||||
for _, token := range item.Expr {
|
||||
switch token.Type {
|
||||
|
@ -370,9 +352,9 @@ func (adapter *MssqlAdapter) SimpleUpdate(name string, table string, set string,
|
|||
querystr = querystr[0 : len(querystr)-1]
|
||||
|
||||
// Add support for BETWEEN x.x
|
||||
if len(where) != 0 {
|
||||
if len(up.where) != 0 {
|
||||
querystr += " WHERE"
|
||||
for _, loc := range processWhere(where) {
|
||||
for _, loc := range processWhere(up.where) {
|
||||
for _, token := range loc.Expr {
|
||||
switch token.Type {
|
||||
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]
|
||||
}
|
||||
|
||||
adapter.pushStatement(name, "update", querystr)
|
||||
adapter.pushStatement(up.name, "update", querystr)
|
||||
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) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if 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
|
||||
func (adapter *MssqlAdapter) Purge(name string, table string) (string, error) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if 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) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if 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) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if table1 == "" {
|
||||
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) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if table1 == "" {
|
||||
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) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if 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 {
|
||||
var stmts, body string
|
||||
for _, name := range adapter.BufferOrder {
|
||||
if name[0] == '_' {
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
stmt := adapter.Buffer[name]
|
||||
|
|
|
@ -83,21 +83,16 @@ func (adapter *MysqlAdapter) DbVersion() string {
|
|||
}
|
||||
|
||||
func (adapter *MysqlAdapter) DropTable(name string, table string) (string, error) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if table == "" {
|
||||
return "", errors.New("You need a name for this 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)
|
||||
return querystr, nil
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
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
|
||||
}
|
||||
|
||||
// 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+";")
|
||||
return querystr + ";", nil
|
||||
}
|
||||
|
@ -178,23 +174,18 @@ func (adapter *MysqlAdapter) parseColumn(column DBTableColumn) (col DBTableColum
|
|||
// TODO: Support AFTER column
|
||||
// TODO: Test to make sure everything works here
|
||||
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 == "" {
|
||||
return "", errors.New("You need a name for this table")
|
||||
}
|
||||
|
||||
column, size, end := adapter.parseColumn(column)
|
||||
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)
|
||||
return querystr, nil
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
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 += ")"
|
||||
|
||||
// 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)
|
||||
return querystr, nil
|
||||
}
|
||||
|
@ -239,9 +231,6 @@ func (adapter *MysqlAdapter) buildColumns(columns string) (querystr string) {
|
|||
|
||||
// ! DEPRECATED
|
||||
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 == "" {
|
||||
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]
|
||||
|
||||
// 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+")")
|
||||
return querystr + ")", nil
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
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
|
||||
|
||||
// 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)
|
||||
return querystr, nil
|
||||
}
|
||||
|
||||
func (adapter *MysqlAdapter) SimpleUpdate(name string, table string, set string, where string) (string, error) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if table == "" {
|
||||
func (adapter *MysqlAdapter) SimpleUpdate(up *updatePrebuilder) (string, error) {
|
||||
if up.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")
|
||||
}
|
||||
|
||||
var querystr = "UPDATE `" + table + "` SET "
|
||||
for _, item := range processSet(set) {
|
||||
var querystr = "UPDATE `" + up.table + "` SET "
|
||||
for _, item := range processSet(up.set) {
|
||||
querystr += "`" + item.Column + "` ="
|
||||
for _, token := range item.Expr {
|
||||
switch token.Type {
|
||||
|
@ -337,20 +322,18 @@ func (adapter *MysqlAdapter) SimpleUpdate(name string, table string, set string,
|
|||
}
|
||||
querystr = querystr[0 : len(querystr)-1]
|
||||
|
||||
whereStr, err := adapter.buildWhere(where)
|
||||
whereStr, err := adapter.buildWhere(up.where)
|
||||
if err != nil {
|
||||
return querystr, err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
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])
|
||||
// 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)
|
||||
return querystr, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if 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) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if 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) {
|
||||
if preBuilder.name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if preBuilder.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) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if table1 == "" {
|
||||
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) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if table1 == "" {
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
whereStr, err := adapter.buildWhere(sel.Where)
|
||||
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) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if 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
|
||||
func (adapter *MysqlAdapter) pushStatement(name string, stype string, querystr string) {
|
||||
if name[0] == '_' {
|
||||
if name == "" {
|
||||
return
|
||||
}
|
||||
adapter.Buffer[name] = DBStmt{querystr, stype}
|
||||
|
|
|
@ -43,9 +43,6 @@ func (adapter *PgsqlAdapter) DbVersion() string {
|
|||
}
|
||||
|
||||
func (adapter *PgsqlAdapter) DropTable(name string, table string) (string, error) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if 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
|
||||
// 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) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if 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
|
||||
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 == "" {
|
||||
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
|
||||
// ! 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) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if 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
|
||||
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 == "" {
|
||||
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
|
||||
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 == "" {
|
||||
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
|
||||
func (adapter *PgsqlAdapter) SimpleUpdate(name string, table string, set string, where string) (string, error) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if table == "" {
|
||||
func (adapter *PgsqlAdapter) SimpleUpdate(up *updatePrebuilder) (string, error) {
|
||||
if up.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")
|
||||
}
|
||||
|
||||
var querystr = "UPDATE \"" + table + "\" SET "
|
||||
for _, item := range processSet(set) {
|
||||
var querystr = "UPDATE \"" + up.table + "\" SET "
|
||||
for _, item := range processSet(up.set) {
|
||||
querystr += "`" + item.Column + "` ="
|
||||
for _, token := range item.Expr {
|
||||
switch token.Type {
|
||||
|
@ -248,9 +227,9 @@ func (adapter *PgsqlAdapter) SimpleUpdate(name string, table string, set string,
|
|||
querystr = querystr[0 : len(querystr)-1]
|
||||
|
||||
// Add support for BETWEEN x.x
|
||||
if len(where) != 0 {
|
||||
if len(up.where) != 0 {
|
||||
querystr += " WHERE"
|
||||
for _, loc := range processWhere(where) {
|
||||
for _, loc := range processWhere(up.where) {
|
||||
for _, token := range loc.Expr {
|
||||
switch token.Type {
|
||||
case "function":
|
||||
|
@ -274,15 +253,17 @@ func (adapter *PgsqlAdapter) SimpleUpdate(name string, table string, set string,
|
|||
querystr = querystr[0 : len(querystr)-4]
|
||||
}
|
||||
|
||||
adapter.pushStatement(name, "update", querystr)
|
||||
adapter.pushStatement(up.name, "update", querystr)
|
||||
return querystr, nil
|
||||
}
|
||||
|
||||
// TODO: Implement this
|
||||
func (adapter *PgsqlAdapter) SimpleUpdateSelect(up *updatePrebuilder) (string, error) {
|
||||
return "", errors.New("not implemented")
|
||||
}
|
||||
|
||||
// TODO: Implement this
|
||||
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 == "" {
|
||||
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
|
||||
// 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) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if 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
|
||||
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 == "" {
|
||||
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
|
||||
func (adapter *PgsqlAdapter) ComplexSelect(prebuilder *selectPrebuilder) (string, error) {
|
||||
if prebuilder.name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if prebuilder.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
|
||||
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 == "" {
|
||||
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
|
||||
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 == "" {
|
||||
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
|
||||
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 == "" {
|
||||
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
|
||||
func (adapter *PgsqlAdapter) pushStatement(name string, stype string, querystr string) {
|
||||
if name[0] == '_' {
|
||||
if name == "" {
|
||||
return
|
||||
}
|
||||
adapter.Buffer[name] = DBStmt{querystr, stype}
|
||||
|
|
|
@ -110,7 +110,8 @@ type Adapter interface {
|
|||
// TODO: Test this
|
||||
AddColumn(name string, table string, column DBTableColumn) (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)
|
||||
Purge(name string, table string) (string, error)
|
||||
SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error)
|
||||
|
|
|
@ -25,7 +25,7 @@ type TransactionBuilder struct {
|
|||
}
|
||||
|
||||
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 {
|
||||
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 :)
|
||||
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 {
|
||||
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) {
|
||||
res, err := build.adapter.SimpleInsert("_builder", table, columns, fields)
|
||||
res, err := build.adapter.SimpleInsert("", table, columns, fields)
|
||||
if err != nil {
|
||||
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 {
|
||||
res, err := build.adapter.SimpleDelete("_builder", table, where)
|
||||
res, err := build.adapter.SimpleDelete("", table, where)
|
||||
if err != nil {
|
||||
return newTransactionStmt(nil, err)
|
||||
}
|
||||
|
|
|
@ -1,14 +1,8 @@
|
|||
echo "Updating Gosora"
|
||||
rm ./schema/lastSchema.json
|
||||
cp ./schema/schema.json ./schema/lastSchema.json
|
||||
git stash
|
||||
git pull origin master
|
||||
git stash apply
|
||||
|
||||
echo "Patching Gosora"
|
||||
cd ./patcher
|
||||
go generate
|
||||
go build -o Patcher
|
||||
mv ./Patcher ..
|
||||
cd ..
|
||||
go build -o Patcher "./patcher"
|
||||
./Patcher
|
|
@ -89,7 +89,9 @@ func topicRoutes() *RouteGroup {
|
|||
Action("routes.LockTopicSubmit", "/topic/lock/submit/").LitBefore("req.URL.Path += extraData"),
|
||||
Action("routes.UnlockTopicSubmit", "/topic/unlock/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/
|
||||
Action("routes.ReplyEditSubmit", "/reply/edit/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.ReplyDelete","/reply/delete/","extraData"), // No js confirmation page? We could have a confirmation modal for the JS case
|
||||
)
|
||||
|
|
|
@ -30,6 +30,7 @@ var successJSONBytes = []byte(`{"success":"1"}`)
|
|||
var phraseLoginAlerts = []byte(`{"msgs":[{"msg":"Login to see your alerts","path":"/accounts/login"}],"msgCount":0}`)
|
||||
|
||||
// TODO: Refactor this endpoint
|
||||
// TODO: Move this into the routes package
|
||||
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
|
||||
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") {
|
||||
// TODO: Split this into it's own function
|
||||
case "dismiss-alert":
|
||||
asid, err := strconv.Atoi(r.FormValue("asid"))
|
||||
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 {
|
||||
_ = 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
|
||||
if !user.Loggedin {
|
||||
w.Write(phraseLoginAlerts)
|
||||
|
|
|
@ -21,7 +21,7 @@ var forumStmts ForumStmts
|
|||
func init() {
|
||||
common.DbInits.Add(func(acc *qgen.Accumulator) error {
|
||||
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()
|
||||
})
|
||||
|
@ -68,13 +68,12 @@ func ViewForum(w http.ResponseWriter, r *http.Request, user common.User, header
|
|||
var reqUserList = make(map[int]bool)
|
||||
for rows.Next() {
|
||||
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 {
|
||||
return common.InternalError(err, w, r)
|
||||
}
|
||||
|
||||
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
|
||||
_, _, lastPage := common.PageOffset(topicItem.PostCount, 1, common.Config.ItemsPerPage)
|
||||
topicItem.LastPage = lastPage
|
||||
|
|
|
@ -34,7 +34,7 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user common.User, heade
|
|||
|
||||
var err error
|
||||
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 replyList []common.ReplyUser
|
||||
|
||||
|
@ -98,11 +98,9 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user common.User, heade
|
|||
|
||||
replyLiked := false
|
||||
replyLikeCount := 0
|
||||
replyRelativeCreatedAt = common.RelativeTime(replyCreatedAt)
|
||||
|
||||
// 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()
|
||||
if err != nil {
|
||||
|
|
|
@ -1,14 +1,8 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -16,7 +10,6 @@ import (
|
|||
"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 {
|
||||
tid, err := strconv.Atoi(r.PostFormValue("tid"))
|
||||
if err != nil {
|
||||
|
@ -45,70 +38,9 @@ func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user common.User)
|
|||
// Handle the file attachments
|
||||
// TODO: Stop duplicating this code
|
||||
if user.Perms.UploadFiles {
|
||||
files, ok := r.MultipartForm.File["upload_files"]
|
||||
if ok {
|
||||
if len(files) > 5 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
_, rerr := uploadAttachment(w, r, user, topic.ParentID, "forums", tid, "replies")
|
||||
if rerr != nil {
|
||||
return rerr
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,8 +59,8 @@ func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user common.User)
|
|||
var maxPollOptions = 10
|
||||
var pollInputItems = make(map[int]string)
|
||||
for key, values := range r.Form {
|
||||
common.DebugDetail("key: ", key)
|
||||
common.DebugDetailf("values: %+v\n", values)
|
||||
//common.DebugDetail("key: ", key)
|
||||
//common.DebugDetailf("values: %+v\n", values)
|
||||
for _, value := range values {
|
||||
if strings.HasPrefix(key, "pollinputitem[") {
|
||||
halves := strings.Split(key, "[")
|
||||
|
|
327
routes/topic.go
327
routes/topic.go
|
@ -5,6 +5,7 @@ import (
|
|||
"database/sql"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
|
@ -22,6 +23,7 @@ import (
|
|||
type TopicStmts struct {
|
||||
getReplies *sql.Stmt
|
||||
getLikedTopic *sql.Stmt
|
||||
updateAttachs *sql.Stmt
|
||||
}
|
||||
|
||||
var topicStmts TopicStmts
|
||||
|
@ -32,6 +34,8 @@ func init() {
|
|||
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", "?,?"),
|
||||
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()
|
||||
})
|
||||
|
@ -51,7 +55,6 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header
|
|||
} else if err != nil {
|
||||
return common.InternalError(err, w, r)
|
||||
}
|
||||
topic.ClassName = ""
|
||||
|
||||
ferr := common.ForumUserCheck(header, w, r, &user, topic.ParentID)
|
||||
if ferr != nil {
|
||||
|
@ -64,6 +67,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header
|
|||
header.Zone = "view_topic"
|
||||
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.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 {
|
||||
topic.ClassName = common.Config.StaffCSS
|
||||
}
|
||||
topic.RelativeCreatedAt = common.RelativeTime(topic.CreatedAt)
|
||||
|
||||
forum, err := common.Forums.Get(topic.ParentID)
|
||||
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
|
||||
offset, page, lastPage := common.PageOffset(topic.PostCount, page, common.Config.ItemsPerPage)
|
||||
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?
|
||||
replyItem.Avatar, replyItem.MicroAvatar = common.BuildAvatar(replyItem.CreatedBy, replyItem.Avatar)
|
||||
replyItem.Tag = postGroup.Tag
|
||||
replyItem.RelativeCreatedAt = common.RelativeTime(replyItem.CreatedAt)
|
||||
|
||||
// We really shouldn't have inline HTML, we should do something about this...
|
||||
if replyItem.ActionType != "" {
|
||||
var action string
|
||||
switch replyItem.ActionType {
|
||||
case "lock":
|
||||
replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_lock", replyItem.UserLink, replyItem.CreatedByName)
|
||||
action = "lock"
|
||||
replyItem.ActionIcon = "🔒︎"
|
||||
case "unlock":
|
||||
replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_unlock", replyItem.UserLink, replyItem.CreatedByName)
|
||||
action = "unlock"
|
||||
replyItem.ActionIcon = "🔓︎"
|
||||
case "stick":
|
||||
replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_stick", replyItem.UserLink, replyItem.CreatedByName)
|
||||
action = "stick"
|
||||
replyItem.ActionIcon = "📌︎"
|
||||
case "unstick":
|
||||
replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_unstick", replyItem.UserLink, replyItem.CreatedByName)
|
||||
action = "unstick"
|
||||
replyItem.ActionIcon = "📌︎"
|
||||
case "move":
|
||||
replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_move", replyItem.UserLink, replyItem.CreatedByName)
|
||||
// TODO: Only fire this off if a corresponding phrase for the ActionType doesn't exist? Or maybe have some sort of action registry?
|
||||
default:
|
||||
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?
|
||||
replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_default", replyItem.ActionType)
|
||||
replyItem.ActionIcon = ""
|
||||
}
|
||||
}
|
||||
|
||||
if replyItem.LikeCount > 0 {
|
||||
if replyItem.LikeCount > 0 && user.Liked > 0 {
|
||||
likedMap[replyItem.ID] = len(tpage.ItemList)
|
||||
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
|
||||
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)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return common.InternalError(err, w, r)
|
||||
|
@ -219,6 +236,89 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header
|
|||
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 allow banned users to make reports? How should we handle report abuse?
|
||||
// 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 pollInputItems = make(map[int]string)
|
||||
for key, values := range r.Form {
|
||||
//common.DebugDetail("key: ", key)
|
||||
//common.DebugDetailf("values: %+v\n", values)
|
||||
for _, value := range values {
|
||||
if strings.HasPrefix(key, "pollinputitem[") {
|
||||
halves := strings.Split(key, "[")
|
||||
|
@ -389,72 +487,10 @@ func CreateTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User)
|
|||
}
|
||||
|
||||
// Handle the file attachments
|
||||
// TODO: Stop duplicating this code
|
||||
if user.Perms.UploadFiles {
|
||||
files, ok := r.MultipartForm.File["upload_files"]
|
||||
if ok {
|
||||
if len(files) > 5 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
_, rerr := uploadAttachment(w, r, user, fid, "forums", tid, "topics")
|
||||
if rerr != nil {
|
||||
return rerr
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -464,6 +500,141 @@ func CreateTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User)
|
|||
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: Disable stat updates in posts handled by plugin_guilds
|
||||
func EditTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, stid string) common.RouteError {
|
||||
|
|
|
@ -25,21 +25,8 @@
|
|||
{{end}}
|
||||
</div>
|
||||
{{if .CurrentUser.Loggedin}}
|
||||
<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>{{lang "topic_list.moderate_run"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{{template "topics_mod_floater.html"}}
|
||||
|
||||
{{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"}}">
|
||||
<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>
|
||||
<span>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -77,7 +77,26 @@
|
|||
</div>
|
||||
<div class="content_container">
|
||||
<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}}§ionType=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="action_button_left">
|
||||
{{if .CurrentUser.Loggedin}}
|
||||
|
@ -97,7 +116,7 @@
|
|||
</div>
|
||||
<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 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}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
</div>
|
||||
<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 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}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -32,22 +32,7 @@
|
|||
</div>
|
||||
|
||||
{{if .CurrentUser.Loggedin}}
|
||||
{{/** 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>
|
||||
{{template "topics_mod_floater.html"}}
|
||||
|
||||
{{if .ForumList}}
|
||||
{{/** TODO: Have a seperate forum list for moving topics? Maybe an AJAX forum search compatible with plugin_guilds? **/}}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
<span>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1006,8 +1006,6 @@ textarea {
|
|||
padding-right: 42px;
|
||||
padding-bottom: 18px;
|
||||
height: min-content;
|
||||
/*overflow: hidden;
|
||||
text-overflow: ellipsis;*/
|
||||
}
|
||||
.user_meta {
|
||||
display: flex;
|
||||
|
@ -1129,6 +1127,9 @@ textarea {
|
|||
content: "{{lang "topic.report_button_text" .}}";
|
||||
}
|
||||
|
||||
.attach_edit_bay {
|
||||
display: none;
|
||||
}
|
||||
.zone_view_topic .pageset {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
|
|
@ -293,12 +293,17 @@ h2 {
|
|||
.quick_create_form .topic_meta {
|
||||
display: flex;
|
||||
}
|
||||
.quick_create_form input, .quick_create_form select {
|
||||
margin-left: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.quick_create_form .topic_meta .topic_name_row {
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
}
|
||||
.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 {
|
||||
margin-left: 0px;
|
||||
|
@ -623,12 +628,24 @@ button, .formbutton, .panel_right_button:not(.has_inner_button) {
|
|||
.topic_view_count:after {
|
||||
content: "{{lang "topic.view_count_suffix" . }}";
|
||||
}
|
||||
.edithead {
|
||||
margin-left: 0px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.topic_name_input {
|
||||
width: 100%;
|
||||
margin-right: 12px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 0px;
|
||||
margin-left: 0px;
|
||||
margin-left: 0px;
|
||||
}
|
||||
.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 {
|
||||
width: 100%;
|
||||
|
@ -688,7 +705,7 @@ button, .formbutton, .panel_right_button:not(.has_inner_button) {
|
|||
flex-direction: column;
|
||||
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;
|
||||
border-radius: 3px;
|
||||
padding: 16px;
|
||||
|
@ -698,8 +715,6 @@ button, .formbutton, .panel_right_button:not(.has_inner_button) {
|
|||
margin-top: 8px;
|
||||
margin-bottom: auto;
|
||||
padding: 14px;
|
||||
background-color: #444444;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.post_item .action_button {
|
||||
margin-right: 5px;
|
||||
|
@ -713,11 +728,7 @@ button, .formbutton, .panel_right_button:not(.has_inner_button) {
|
|||
.post_item .action_button_right {
|
||||
margin-left: auto;
|
||||
}
|
||||
.post_item .controls:not(.has_likes) .like_count {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.action_item .userinfo, .action_item .action_icon {
|
||||
.post_item .controls:not(.has_likes) .like_count, .action_item .userinfo, .action_item .action_icon {
|
||||
display: none;
|
||||
}
|
||||
.action_item .content_container {
|
||||
|
@ -788,6 +799,49 @@ input[type=checkbox]:checked + label .sel {
|
|||
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 {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
|
|
@ -12,7 +12,9 @@
|
|||
$(".alerts").html(alertCount + " new alerts");
|
||||
$(".user_box").addClass("has_alerts");
|
||||
}
|
||||
})
|
||||
});
|
||||
addHook("open_edit", () => $('.topic_block').addClass("edithead"));
|
||||
addHook("close_edit", () => $('.topic_block').removeClass("edithead"));
|
||||
})();
|
||||
|
||||
$(document).ready(() => {
|
||||
|
|
Loading…
Reference in New Issue