diff --git a/common/poll_store.go b/common/poll_store.go index e0c7d72f..0275d878 100644 --- a/common/poll_store.go +++ b/common/poll_store.go @@ -57,12 +57,14 @@ type PollStore interface { type DefaultPollStore struct { cache PollCache - get *sql.Stmt - exists *sql.Stmt - create *sql.Stmt - addVote *sql.Stmt - incrementVoteCount *sql.Stmt - delete *sql.Stmt + get *sql.Stmt + exists *sql.Stmt + createPoll *sql.Stmt + createPollOption *sql.Stmt + addVote *sql.Stmt + incrementVoteCount *sql.Stmt + incrementVoteCountForOption *sql.Stmt + delete *sql.Stmt //pollCount *sql.Stmt } @@ -73,12 +75,14 @@ func NewDefaultPollStore(cache PollCache) (*DefaultPollStore, error) { } // TODO: Add an admin version of registerStmt with more flexibility? return &DefaultPollStore{ - cache: cache, - get: acc.Select("polls").Columns("parentID, parentTable, type, options, votes").Where("pollID = ?").Prepare(), - exists: acc.Select("polls").Columns("pollID").Where("pollID = ?").Prepare(), - create: acc.Insert("polls").Columns("parentID, parentTable, type, options").Fields("?,?,?,?").Prepare(), - addVote: acc.Insert("polls_votes").Columns("pollID, uid, option, castAt, ipaddress").Fields("?,?,?,UTC_TIMESTAMP(),?").Prepare(), - incrementVoteCount: acc.Update("polls").Set("votes = votes + 1").Where("pollID = ?").Prepare(), + cache: cache, + get: acc.Select("polls").Columns("parentID, parentTable, type, options, votes").Where("pollID = ?").Prepare(), + exists: acc.Select("polls").Columns("pollID").Where("pollID = ?").Prepare(), + createPoll: acc.Insert("polls").Columns("parentID, parentTable, type, options").Fields("?,?,?,?").Prepare(), + createPollOption: acc.Insert("polls_options").Columns("pollID, option, votes").Fields("?,?,0").Prepare(), + addVote: acc.Insert("polls_votes").Columns("pollID, uid, option, castAt, ipaddress").Fields("?,?,?,UTC_TIMESTAMP(),?").Prepare(), + incrementVoteCount: acc.Update("polls").Set("votes = votes + 1").Where("pollID = ?").Prepare(), + incrementVoteCountForOption: acc.Update("polls_options").Set("votes = votes + 1").Where("option = ? AND pollID = ?").Prepare(), //pollCount: acc.SimpleCount("polls", "", ""), }, acc.FirstError() } @@ -147,16 +151,21 @@ func (store *DefaultPollStore) CastVote(optionIndex int, pollID int, uid int, ip return err } _, err = store.incrementVoteCount.Exec(pollID) + if err != nil { + return err + } + _, err = store.incrementVoteCountForOption.Exec(optionIndex, pollID) return err } +// TODO: Use a transaction for this func (store *DefaultPollStore) Create(parent Pollable, pollType int, pollOptions map[int]string) (id int, err error) { pollOptionsTxt, err := json.Marshal(pollOptions) if err != nil { return 0, err } - res, err := store.create.Exec(parent.GetID(), parent.GetTable(), pollType, pollOptionsTxt) + res, err := store.createPoll.Exec(parent.GetID(), parent.GetTable(), pollType, pollOptionsTxt) if err != nil { return 0, err } @@ -165,7 +174,14 @@ func (store *DefaultPollStore) Create(parent Pollable, pollType int, pollOptions if err != nil { return 0, err } - return int(lastID), parent.SetPoll(int(lastID)) // TODO: Delete the poll if SetPoll fails + + for i := 0; i < len(pollOptions); i++ { + _, err := store.createPollOption.Exec(lastID, i) + if err != nil { + return 0, err + } + } + return int(lastID), parent.SetPoll(int(lastID)) // TODO: Delete the poll (and options) if SetPoll fails } func (store *DefaultPollStore) SetCache(cache PollCache) { diff --git a/gen_router.go b/gen_router.go index b26d7365..2ca26219 100644 --- a/gen_router.go +++ b/gen_router.go @@ -101,6 +101,7 @@ var RouteMap = map[string]interface{}{ "routes.ProfileReplyEditSubmit": routes.ProfileReplyEditSubmit, "routes.ProfileReplyDeleteSubmit": routes.ProfileReplyDeleteSubmit, "routes.PollVote": routes.PollVote, + "routes.PollResults": routes.PollResults, "routeLogin": routeLogin, "routeRegister": routeRegister, "routeLogout": routeLogout, @@ -198,14 +199,15 @@ var routeMapEnum = map[string]int{ "routes.ProfileReplyEditSubmit": 82, "routes.ProfileReplyDeleteSubmit": 83, "routes.PollVote": 84, - "routeLogin": 85, - "routeRegister": 86, - "routeLogout": 87, - "routeLoginSubmit": 88, - "routeRegisterSubmit": 89, - "routeDynamic": 90, - "routeUploads": 91, - "BadRoute": 92, + "routes.PollResults": 85, + "routeLogin": 86, + "routeRegister": 87, + "routeLogout": 88, + "routeLoginSubmit": 89, + "routeRegisterSubmit": 90, + "routeDynamic": 91, + "routeUploads": 92, + "BadRoute": 93, } var reverseRouteMapEnum = map[int]string{ 0: "routeAPI", @@ -293,14 +295,15 @@ var reverseRouteMapEnum = map[int]string{ 82: "routes.ProfileReplyEditSubmit", 83: "routes.ProfileReplyDeleteSubmit", 84: "routes.PollVote", - 85: "routeLogin", - 86: "routeRegister", - 87: "routeLogout", - 88: "routeLoginSubmit", - 89: "routeRegisterSubmit", - 90: "routeDynamic", - 91: "routeUploads", - 92: "BadRoute", + 85: "routes.PollResults", + 86: "routeLogin", + 87: "routeRegister", + 88: "routeLogout", + 89: "routeLoginSubmit", + 90: "routeRegisterSubmit", + 91: "routeDynamic", + 92: "routeUploads", + 93: "BadRoute", } var agentMapEnum = map[string]int{ "unknown": 0, @@ -415,7 +418,7 @@ func (router *GenRouter) DumpRequest(req *http.Request) { } func (router *GenRouter) SuspiciousRequest(req *http.Request) { - log.Print("Supicious Request") + log.Print("Suspicious Request") router.DumpRequest(req) common.AgentViewCounter.Bump(18) } @@ -424,6 +427,22 @@ func (router *GenRouter) SuspiciousRequest(req *http.Request) { // TODO: SetDefaultRoute // TODO: GetDefaultRoute func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { + // Redirect www. requests to the right place + if req.Host == "www." + common.Site.Host { + w.Header().Set("Connection", "close") + var s string + if common.Site.EnableSsl { + s = "s" + } + dest := "http"+s+"://" + req.Host + req.URL.Path + if len(req.URL.RawQuery) > 0 { + dest += "?" + req.URL.RawQuery + } + http.Redirect(w, req, dest, http.StatusMovedPermanently) + return + } + + // Deflect malformed requests if len(req.URL.Path) == 0 || req.URL.Path[0] != '/' || req.Host != common.Site.Host { w.WriteHeader(200) // 400 w.Write([]byte("")) @@ -1403,6 +1422,9 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { common.RouteViewCounter.Bump(84) err = routes.PollVote(w,req,user,extraData) + case "/poll/results/": + common.RouteViewCounter.Bump(85) + err = routes.PollResults(w,req,user,extraData) } if err != nil { router.handleError(err,w,req,user) @@ -1410,10 +1432,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { case "/accounts": switch(req.URL.Path) { case "/accounts/login/": - common.RouteViewCounter.Bump(85) + common.RouteViewCounter.Bump(86) err = routeLogin(w,req,user) case "/accounts/create/": - common.RouteViewCounter.Bump(86) + common.RouteViewCounter.Bump(87) err = routeRegister(w,req,user) case "/accounts/logout/": err = common.NoSessionMismatch(w,req,user) @@ -1428,7 +1450,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(87) + common.RouteViewCounter.Bump(88) err = routeLogout(w,req,user) case "/accounts/login/submit/": err = common.ParseForm(w,req,user) @@ -1437,7 +1459,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(88) + common.RouteViewCounter.Bump(89) err = routeLoginSubmit(w,req,user) case "/accounts/create/submit/": err = common.ParseForm(w,req,user) @@ -1446,7 +1468,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(89) + common.RouteViewCounter.Bump(90) err = routeRegisterSubmit(w,req,user) } if err != nil { @@ -1463,7 +1485,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { common.NotFound(w,req) return } - common.RouteViewCounter.Bump(91) + common.RouteViewCounter.Bump(92) req.URL.Path += extraData // TODO: Find a way to propagate errors up from this? router.UploadHandler(w,req) // TODO: Count these views @@ -1506,7 +1528,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { router.RUnlock() if ok { - common.RouteViewCounter.Bump(90) // TODO: Be more specific about *which* dynamic route it is + common.RouteViewCounter.Bump(91) // TODO: Be more specific about *which* dynamic route it is req.URL.Path += extraData err = handle(w,req,user) if err != nil { @@ -1519,7 +1541,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { if strings.Contains(lowerPath,"admin") || strings.Contains(lowerPath,"sql") || strings.Contains(lowerPath,"manage") { router.SuspiciousRequest(req) } - common.RouteViewCounter.Bump(92) + common.RouteViewCounter.Bump(93) common.NotFound(w,req) } } diff --git a/public/global.js b/public/global.js index 82a91569..21b73d53 100644 --- a/public/global.js +++ b/public/global.js @@ -245,8 +245,7 @@ $(document).ready(function(){ }); }); - $(".delete_item").click(function(event) - { + $(".delete_item").click(function(event) { postLink(event); $(this).closest('.deletable_block').remove(); }); @@ -271,12 +270,6 @@ $(document).ready(function(){ }); }); - $("#forum_quick_perms").click(function(){ - $(".submit_edit").click(function(event){ - - }); - }); - $(".edit_field").click(function(event) { event.preventDefault(); @@ -316,8 +309,7 @@ $(document).ready(function(){ blockParent.find('.editable_block').each(function(){ var fieldName = this.getAttribute("data-field"); var fieldType = this.getAttribute("data-type"); - if(fieldType=="list") - { + if(fieldType=="list") { var fieldValue = this.getAttribute("data-value"); if(fieldName in form_vars) var it = form_vars[fieldName]; else var it = ['No','Yes']; @@ -612,4 +604,22 @@ $(document).ready(function(){ $("#has_poll_input").val("1"); $(".pollinputinput").click(addPollInput); }); + + $(".poll_results_button").click(function(){ + let pollID = $(this).attr("data-poll-id"); + $("#poll_results_" + pollID + " .user_content").html("
"); + fetch("/poll/results/" + pollID, { + credentials: 'same-origin' + }).then((response) => response.text()).catch((error) => console.error("Error:",error)).then((rawData) => { + // TODO: Make sure the received data is actually a list of integers + let data = JSON.parse(rawData); + console.log("rawData: ", rawData); + console.log("series: ", data); + Chartist.Pie('#poll_results_chart_' + pollID, { + series: data, + }, { + height: '100%', + }); + }) + }); }); diff --git a/query_gen/tables.go b/query_gen/tables.go index a910d0ae..4b191fb3 100644 --- a/query_gen/tables.go +++ b/query_gen/tables.go @@ -232,6 +232,15 @@ func createTables(adapter qgen.Adapter) error { }, ) + qgen.Install.CreateTable("polls_options", "", "", + []qgen.DBTableColumn{ + qgen.DBTableColumn{"pollID", "int", 0, false, false, ""}, + qgen.DBTableColumn{"option", "int", 0, false, false, "0"}, + qgen.DBTableColumn{"votes", "int", 0, false, false, "0"}, + }, + []qgen.DBTableKey{}, + ) + qgen.Install.CreateTable("polls_votes", "utf8mb4", "utf8mb4_general_ci", []qgen.DBTableColumn{ qgen.DBTableColumn{"pollID", "int", 0, false, false, ""}, diff --git a/router_gen/main.go b/router_gen/main.go index e1432cb0..dc134e7f 100644 --- a/router_gen/main.go +++ b/router_gen/main.go @@ -291,7 +291,7 @@ func (router *GenRouter) DumpRequest(req *http.Request) { } func (router *GenRouter) SuspiciousRequest(req *http.Request) { - log.Print("Supicious Request") + log.Print("Suspicious Request") router.DumpRequest(req) common.AgentViewCounter.Bump({{.AllAgentMap.suspicious}}) } @@ -300,6 +300,22 @@ func (router *GenRouter) SuspiciousRequest(req *http.Request) { // TODO: SetDefaultRoute // TODO: GetDefaultRoute func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { + // Redirect www. requests to the right place + if req.Host == "www." + common.Site.Host { + w.Header().Set("Connection", "close") + var s string + if common.Site.EnableSsl { + s = "s" + } + dest := "http"+s+"://" + req.Host + req.URL.Path + if len(req.URL.RawQuery) > 0 { + dest += "?" + req.URL.RawQuery + } + http.Redirect(w, req, dest, http.StatusMovedPermanently) + return + } + + // Deflect malformed requests if len(req.URL.Path) == 0 || req.URL.Path[0] != '/' || req.Host != common.Site.Host { w.WriteHeader(200) // 400 w.Write([]byte("")) diff --git a/router_gen/routes.go b/router_gen/routes.go index e5c9aa92..53d81d90 100644 --- a/router_gen/routes.go +++ b/router_gen/routes.go @@ -109,6 +109,7 @@ func buildPollRoutes() { pollGroup := newRouteGroup("/poll/") pollGroup.Routes( Action("routes.PollVote", "/poll/vote/", "extraData"), + View("routes.PollResults", "/poll/results/", "extraData"), ) addRouteGroup(pollGroup) } diff --git a/routes.go b/routes.go index b52666f7..017b0440 100644 --- a/routes.go +++ b/routes.go @@ -457,6 +457,9 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user common.User, urlB return common.NoPermissions(w, r, user) } headerVars.Zone = "view_topic" + // TODO: Only include these on pages with polls + headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css") + headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js") topic.ContentHTML = common.ParseMessage(topic.Content, topic.ParentID, "forums") topic.ContentLines = strings.Count(topic.Content, "\n") diff --git a/routes/poll.go b/routes/poll.go index 7eba777f..36ae7ca5 100644 --- a/routes/poll.go +++ b/routes/poll.go @@ -3,10 +3,12 @@ package routes import ( "database/sql" "errors" + "log" "net/http" "strconv" "../common" + "../query_gen/lib" ) func PollVote(w http.ResponseWriter, r *http.Request, user common.User, sPollID string) common.RouteError { @@ -65,3 +67,47 @@ func PollVote(w http.ResponseWriter, r *http.Request, user common.User, sPollID http.Redirect(w, r, "/topic/"+strconv.Itoa(topic.ID), http.StatusSeeOther) return nil } + +func PollResults(w http.ResponseWriter, r *http.Request, user common.User, sPollID string) common.RouteError { + log.Print("in PollResults") + pollID, err := strconv.Atoi(sPollID) + if err != nil { + return common.PreError("The provided PollID is not a valid number.", w, r) + } + + poll, err := common.Polls.Get(pollID) + if err == sql.ErrNoRows { + return common.PreError("The poll you tried to vote for doesn't exist.", w, r) + } else if err != nil { + return common.InternalError(err, w, r) + } + + // TODO: Abstract this + acc := qgen.Builder.Accumulator() + rows, err := acc.Select("polls_options").Columns("votes").Where("pollID = ?").Orderby("option ASC").Query(poll.ID) + if err != nil { + return common.InternalError(err, w, r) + } + defer rows.Close() + + var optionList = "" + for rows.Next() { + var votes int + err := rows.Scan(&votes) + if err != nil { + return common.InternalError(err, w, r) + } + optionList += strconv.Itoa(votes) + "," + } + err = rows.Err() + if err != nil { + return common.InternalError(err, w, r) + } + + // TODO: Implement a version of this which doesn't rely so much on sequential order + if len(optionList) > 0 { + optionList = optionList[:len(optionList)-1] + } + w.Write([]byte("[" + optionList + "]")) + return nil +} diff --git a/schema/mssql/query_polls_options.sql b/schema/mssql/query_polls_options.sql new file mode 100644 index 00000000..ea3028f5 --- /dev/null +++ b/schema/mssql/query_polls_options.sql @@ -0,0 +1,5 @@ +CREATE TABLE [polls_options] ( + [pollID] int not null, + [option] int DEFAULT 0 not null, + [votes] int DEFAULT 0 not null +); \ No newline at end of file diff --git a/schema/mysql/query_polls_options.sql b/schema/mysql/query_polls_options.sql new file mode 100644 index 00000000..95bf84c3 --- /dev/null +++ b/schema/mysql/query_polls_options.sql @@ -0,0 +1,5 @@ +CREATE TABLE `polls_options` ( + `pollID` int not null, + `option` int DEFAULT 0 not null, + `votes` int DEFAULT 0 not null +); \ No newline at end of file diff --git a/schema/pgsql/query_polls_options.sql b/schema/pgsql/query_polls_options.sql new file mode 100644 index 00000000..95bf84c3 --- /dev/null +++ b/schema/pgsql/query_polls_options.sql @@ -0,0 +1,5 @@ +CREATE TABLE `polls_options` ( + `pollID` int not null, + `option` int DEFAULT 0 not null, + `votes` int DEFAULT 0 not null +); \ No newline at end of file diff --git a/template_list.go b/template_list.go index 6f2b04bf..d351bd28 100644 --- a/template_list.go +++ b/template_list.go @@ -16,6 +16,7 @@ var header_5 = []byte(`" rel="stylesheet" type="text/css"> `) var header_6 = []byte(` + `) var header_7 = []byte(` var topic_alt_38 = []byte(`"> - `) -var topic_alt_39 = []byte(` + `) +var topic_alt_40 = []byte(` `) -var topic_alt_40 = []byte(` +var topic_alt_41 = []byte(`