From b6931fe16a13768f9c903e752c2510fe8348ca7f Mon Sep 17 00:00:00 2001 From: Azareal Date: Sun, 9 Feb 2020 20:00:08 +1000 Subject: [PATCH] Add registered time as a parameter for group promotions. Run group promotions on group change. Run group promotions on registration. Load the CreatedAt field when users are loaded. Set the default for last_ip properly. Fix the default values in the group promotion form. Add initial group promotion tests. Add panel_group_promotion_registered_for phrase. Add the registeredFor column to the users_groups_promotions table. You will need to run the updater / patcher for this commit. --- cmd/query_gen/tables.go | 19 +- common/promotions.go | 68 ++++-- common/template_init.go | 6 +- common/user.go | 5 +- common/user_store.go | 25 +- extend/plugin_skeleton.go | 6 +- langs/english.json | 1 + misc_test.go | 224 +++++++++++------- patcher/patches.go | 13 +- routes/attachments.go | 2 +- routes/moderate.go | 6 +- routes/panel/groups.go | 19 +- routes/panel/users.go | 40 ++-- routes/poll.go | 4 +- routes/reports.go | 4 +- routes/topic.go | 14 +- routes/topic_list.go | 54 ++--- routes/user.go | 12 + schema/mssql/query_users.sql | 2 +- .../mssql/query_users_groups_promotions.sql | 1 + schema/mysql/query_users.sql | 2 +- .../mysql/query_users_groups_promotions.sql | 1 + schema/pgsql/query_users.sql | 2 +- .../pgsql/query_users_groups_promotions.sql | 1 + templates/panel_group_edit_promotions.html | 13 +- templates/widget_search_and_filter.html | 2 +- 26 files changed, 349 insertions(+), 197 deletions(-) diff --git a/cmd/query_gen/tables.go b/cmd/query_gen/tables.go index 29938a30..b4027652 100644 --- a/cmd/query_gen/tables.go +++ b/cmd/query_gen/tables.go @@ -30,7 +30,7 @@ func createTables(adapter qgen.Adapter) (err error) { tC{"lastActiveAt", "datetime", 0, false, false, ""}, tC{"session", "varchar", 200, false, false, "''"}, //tC{"authToken", "varchar", 200, false, false, "''"}, - tC{"last_ip", "varchar", 200, false, false, "0.0.0.0.0"}, + tC{"last_ip", "varchar", 200, false, false, "''"}, tC{"enable_embeds", "int", 0, false, false, "-1"}, tC{"email", "varchar", 200, false, false, "''"}, tC{"avatar", "varchar", 100, false, false, "''"}, @@ -90,13 +90,28 @@ func createTables(adapter qgen.Adapter) (err error) { // Requirements tC{"level", "int", 0, false, false, ""}, tC{"posts", "int", 0, false, false, "0"}, - tC{"minTime", "int", 0, false, false, ""}, // How long someone needs to have been in their current group before being promoted + tC{"minTime", "int", 0, false, false, ""}, // How long someone needs to have been in their current group before being promoted + tC{"registeredFor", "int", 0, false, false, "0"}, // minutes }, []tblKey{ tblKey{"pid", "primary", "", false}, }, ) + /* + createTable("users_groups_promotions_scheduled","","", + []tC{ + tC{"prid","int",0,false,false,""}, + tC{"uid","int",0,false,false,""}, + tC{"runAt","datetime",0,false,false,""}, + }, + []tblKey{ + // TODO: Test to see that the compound primary key works + tblKey{"prid,uid", "primary", "", false}, + }, + ) + */ + createTable("users_2fa_keys", mysqlPre, mysqlCol, []tC{ tC{"uid", "int", 0, false, false, ""}, diff --git a/common/promotions.go b/common/promotions.go index 94b3465b..0192a0a1 100644 --- a/common/promotions.go +++ b/common/promotions.go @@ -2,6 +2,8 @@ package common import ( "database/sql" + //"log" + "time" qgen "github.com/Azareal/Gosora/query_gen" ) @@ -14,17 +16,18 @@ type GroupPromotion struct { To int TwoWay bool - Level int - Posts int - MinTime int + Level int + Posts int + MinTime int + RegisteredFor int } type GroupPromotionStore interface { GetByGroup(gid int) (gps []*GroupPromotion, err error) Get(id int) (*GroupPromotion, error) - PromoteIfEligible(u *User, level int, posts int) error + PromoteIfEligible(u *User, level, posts int, registeredAt time.Time) error Delete(id int) error - Create(from int, to int, twoWay bool, level int, posts int) (int, error) + Create(from, to int, twoWay bool, level, posts, registeredFor int) (int, error) } type DefaultGroupPromotionStore struct { @@ -33,21 +36,33 @@ type DefaultGroupPromotionStore struct { delete *sql.Stmt create *sql.Stmt - getByUser *sql.Stmt - updateUser *sql.Stmt + getByUser *sql.Stmt + getByUserMins *sql.Stmt + updateUser *sql.Stmt + updateGeneric *sql.Stmt } func NewDefaultGroupPromotionStore(acc *qgen.Accumulator) (*DefaultGroupPromotionStore, error) { ugp := "users_groups_promotions" - return &DefaultGroupPromotionStore{ - getByGroup: acc.Select(ugp).Columns("pid, from_gid, to_gid, two_way, level, posts, minTime").Where("from_gid=? OR to_gid=?").Prepare(), - get: acc.Select(ugp).Columns("from_gid, to_gid, two_way, level, posts, minTime").Where("pid = ?").Prepare(), - delete: acc.Delete(ugp).Where("pid = ?").Prepare(), - create: acc.Insert(ugp).Columns("from_gid, to_gid, two_way, level, posts, minTime").Fields("?,?,?,?,?,?").Prepare(), + prs := &DefaultGroupPromotionStore{ + getByGroup: acc.Select(ugp).Columns("pid, from_gid, to_gid, two_way, level, posts, minTime, registeredFor").Where("from_gid=? OR to_gid=?").Prepare(), + get: acc.Select(ugp).Columns("from_gid, to_gid, two_way, level, posts, minTime, registeredFor").Where("pid=?").Prepare(), + delete: acc.Delete(ugp).Where("pid=?").Prepare(), + create: acc.Insert(ugp).Columns("from_gid, to_gid, two_way, level, posts, minTime, registeredFor").Fields("?,?,?,?,?,?,?").Prepare(), - getByUser: acc.Select(ugp).Columns("pid, to_gid, two_way, level, posts, minTime").Where("from_gid=? AND level<=? AND posts<=?").Orderby("level DESC").Limit("1").Prepare(), - updateUser: acc.Update("users").Set("group = ?").Where("level >= ? AND posts >= ?").Prepare(), - }, acc.FirstError() + //err := s.getByUser.QueryRow(u.Group, level, posts, mins).Scan(&g.ID, &g.To, &g.TwoWay, &g.Level, &g.Posts, &g.MinTime, &g.RegisteredFor) + //getByUserMins: acc.Select(ugp).Columns("pid, to_gid, two_way, level, posts, minTime, registeredFor").Where("from_gid=? AND level<=? AND posts<=?").DateOlderThanQ("registeredFor", "minute").Orderby("level DESC").Limit("1").Prepare(), + getByUserMins: acc.Select(ugp).Columns("pid, to_gid, two_way, level, posts, minTime, registeredFor").Where("from_gid=? AND level<=? AND posts<=? AND registeredFor<=?").Orderby("level DESC").Limit("1").Prepare(), + getByUser: acc.Select(ugp).Columns("pid, to_gid, two_way, level, posts, minTime, registeredFor").Where("from_gid=? AND level<=? AND posts<=?").Orderby("level DESC").Limit("1").Prepare(), + updateUser: acc.Update("users").Set("group=?").Where("group=? AND uid=?").Prepare(), + updateGeneric: acc.Update("users").Set("group=?").Where("group=? AND level>=? AND posts>=?").Prepare(), + } + AddScheduledFifteenMinuteTask(prs.Tick) + return prs, acc.FirstError() +} + +func (s *DefaultGroupPromotionStore) Tick() error { + return nil } func (s *DefaultGroupPromotionStore) GetByGroup(gid int) (gps []*GroupPromotion, err error) { @@ -59,7 +74,7 @@ func (s *DefaultGroupPromotionStore) GetByGroup(gid int) (gps []*GroupPromotion, for rows.Next() { g := &GroupPromotion{} - err := rows.Scan(&g.ID, &g.From, &g.To, &g.TwoWay, &g.Level, &g.Posts, &g.MinTime) + err := rows.Scan(&g.ID, &g.From, &g.To, &g.TwoWay, &g.Level, &g.Posts, &g.MinTime, &g.RegisteredFor) if err != nil { return nil, err } @@ -76,22 +91,31 @@ func (s *DefaultGroupPromotionStore) Get(id int) (*GroupPromotion, error) { }*/ g := &GroupPromotion{ID: id} - err := s.get.QueryRow(id).Scan(&g.From, &g.To, &g.TwoWay, &g.Level, &g.Posts, &g.MinTime) + err := s.get.QueryRow(id).Scan(&g.From, &g.To, &g.TwoWay, &g.Level, &g.Posts, &g.MinTime, &g.RegisteredFor) if err == nil { //s.cache.Set(u) } return g, err } -func (s *DefaultGroupPromotionStore) PromoteIfEligible(u *User, level int, posts int) error { +// TODO: Optimise this to avoid the query +func (s *DefaultGroupPromotionStore) PromoteIfEligible(u *User, level, posts int, registeredAt time.Time) error { + mins := time.Since(registeredAt).Minutes() g := &GroupPromotion{From: u.Group} - err := s.getByUser.QueryRow(u.Group, level, posts).Scan(&g.ID, &g.To, &g.TwoWay, &g.Level, &g.Posts, &g.MinTime) + //log.Printf("pre getByUserMins: %+v\n", u) + err := s.getByUserMins.QueryRow(u.Group, level, posts, mins).Scan(&g.ID, &g.To, &g.TwoWay, &g.Level, &g.Posts, &g.MinTime, &g.RegisteredFor) if err == sql.ErrNoRows { + //log.Print("no matches found") return nil } else if err != nil { return err } - _, err = s.updateUser.Exec(g.To, g.Level, g.Posts) + //log.Printf("g: %+v\n", g) + if g.RegisteredFor == 0 { + _, err = s.updateGeneric.Exec(g.To, g.From, g.Level, g.Posts) + } else { + _, err = s.updateUser.Exec(g.To, g.From, u.ID) + } return err } @@ -100,8 +124,8 @@ func (s *DefaultGroupPromotionStore) Delete(id int) error { return err } -func (s *DefaultGroupPromotionStore) Create(from int, to int, twoWay bool, level int, posts int) (int, error) { - res, err := s.create.Exec(from, to, twoWay, level, posts, 0) +func (s *DefaultGroupPromotionStore) Create(from, to int, twoWay bool, level, posts, registeredFor int) (int, error) { + res, err := s.create.Exec(from, to, twoWay, level, posts, 0, registeredFor) if err != nil { return 0, err } diff --git a/common/template_init.go b/common/template_init.go index e97862ee..4490139d 100644 --- a/common/template_init.go +++ b/common/template_init.go @@ -93,14 +93,14 @@ var Template_account_handle = genIntTmpl("account") func tmplInitUsers() (User, User, User) { avatar, microAvatar := BuildAvatar(62, "") - user := User{62, BuildProfileURL("fake-user", 62), "Fake User", "compiler@localhost", 0, false, false, false, false, false, false, GuestPerms, make(map[string]bool), "", false, "", avatar, microAvatar, "", "", 0, 0, 0, 0, "0.0.0.0.0", "", 0, nil} + user := User{62, BuildProfileURL("fake-user", 62), "Fake User", "compiler@localhost", 0, false, false, false, false, false, false, GuestPerms, make(map[string]bool), "", false, "", avatar, microAvatar, "", "", 0, 0, 0, 0, StartTime,"0.0.0.0.0", "", 0, nil} // TODO: Do a more accurate level calculation for this? avatar, microAvatar = BuildAvatar(1, "") - user2 := User{1, BuildProfileURL("admin-alice", 1), "Admin Alice", "alice@localhost", 1, true, true, true, true, false, false, AllPerms, make(map[string]bool), "", true, "", avatar, microAvatar, "", "", 58, 1000, 0, 1000, "127.0.0.1", "", 0, nil} + user2 := User{1, BuildProfileURL("admin-alice", 1), "Admin Alice", "alice@localhost", 1, true, true, true, true, false, false, AllPerms, make(map[string]bool), "", true, "", avatar, microAvatar, "", "", 58, 1000, 0, 1000, StartTime, "127.0.0.1", "", 0, nil} avatar, microAvatar = BuildAvatar(2, "") - user3 := User{2, BuildProfileURL("admin-fred", 62), "Admin Fred", "fred@localhost", 1, true, true, true, true, false, false, AllPerms, make(map[string]bool), "", true, "", avatar, microAvatar, "", "", 42, 900, 0, 900, "::1", "", 0, nil} + user3 := User{2, BuildProfileURL("admin-fred", 62), "Admin Fred", "fred@localhost", 1, true, true, true, true, false, false, AllPerms, make(map[string]bool), "", true, "", avatar, microAvatar, "", "", 42, 900, 0, 900, StartTime, "::1", "", 0, nil} return user, user2, user3 } diff --git a/common/user.go b/common/user.go index 76fde0ee..16861c3c 100644 --- a/common/user.go +++ b/common/user.go @@ -24,7 +24,7 @@ var BanGroup = 4 // TODO: Use something else as the guest avatar, maybe a question mark of some sort? // GuestUser is an instance of user which holds guest data to avoid having to initialise a guest every time -var GuestUser = User{ID: 0, Name: "Guest", Link: "#", Group: 6, Perms: GuestPerms} // BuildAvatar is done in site.go to make sure it's done after init +var GuestUser = User{ID: 0, Name: "Guest", Link: "#", Group: 6, Perms: GuestPerms, CreatedAt: StartTime} // BuildAvatar is done in site.go to make sure it's done after init var ErrNoTempGroup = errors.New("We couldn't find a temporary group for this user") type User struct { @@ -56,6 +56,7 @@ type User struct { Score int Posts int Liked int + CreatedAt time.Time LastIP string // ! This part of the UserCache data might fall out of date LastAgent string // ! Temporary hack, don't use TempGroup int @@ -592,7 +593,7 @@ func (u *User) IncreasePostStats(wcount int, topic bool) (err error) { if err != nil { return err } - err = GroupPromotions.PromoteIfEligible(u, level, u.Posts+1) + err = GroupPromotions.PromoteIfEligible(u, level, u.Posts+1, u.CreatedAt) u.CacheRemove() return err } diff --git a/common/user_store.go b/common/user_store.go index a30b587f..5eb1f363 100644 --- a/common/user_store.go +++ b/common/user_store.go @@ -39,7 +39,7 @@ type DefaultUserStore struct { get *sql.Stmt getByName *sql.Stmt getOffset *sql.Stmt - getAll *sql.Stmt + getAll *sql.Stmt exists *sql.Stmt register *sql.Stmt nameExists *sql.Stmt @@ -53,13 +53,14 @@ func NewDefaultUserStore(cache UserCache) (*DefaultUserStore, error) { cache = NewNullUserCache() } u := "users" + allCols := "uid, name, group, active, is_super_admin, session, email, avatar, message, level, score, posts, liked, last_ip, temp_group, createdAt, enable_embeds" // TODO: Add an admin version of registerStmt with more flexibility? return &DefaultUserStore{ cache: cache, - get: acc.Select(u).Columns("name, group, active, is_super_admin, session, email, avatar, message, level, score, posts, liked, last_ip, temp_group, enable_embeds").Where("uid=?").Prepare(), - getByName: acc.Select(u).Columns("uid, name, group, active, is_super_admin, session, email, avatar, message, level, score, posts, liked, last_ip, temp_group, enable_embeds").Where("name = ?").Prepare(), - getOffset: acc.Select(u).Columns("uid, name, group, active, is_super_admin, session, email, avatar, message, level, score, posts, liked, last_ip, temp_group, enable_embeds").Orderby("uid ASC").Limit("?,?").Prepare(), - getAll: acc.Select(u).Columns("uid, name, group, active, is_super_admin, session, email, avatar, message, level, score, posts, liked, last_ip, temp_group, enable_embeds").Prepare(), + get: acc.Select(u).Columns("name, group, active, is_super_admin, session, email, avatar, message, level, score, posts, liked, last_ip, temp_group, createdAt, enable_embeds").Where("uid=?").Prepare(), + getByName: acc.Select(u).Columns(allCols).Where("name=?").Prepare(), + getOffset: acc.Select(u).Columns(allCols).Orderby("uid ASC").Limit("?,?").Prepare(), + getAll: acc.Select(u).Columns(allCols).Prepare(), exists: acc.Exists(u, "uid").Prepare(), register: acc.Insert(u).Columns("name, email, password, salt, group, is_super_admin, session, active, message, createdAt, lastActiveAt, lastLiked, oldestItemLikedCreatedAt").Fields("?,?,?,?,?,0,'',?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP(),UTC_TIMESTAMP(),UTC_TIMESTAMP()").Prepare(), // TODO: Implement user_count on users_groups here nameExists: acc.Exists(u, "name").Prepare(), @@ -91,7 +92,7 @@ func (s *DefaultUserStore) Get(id int) (*User, error) { u = &User{ID: id, Loggedin: true} var embeds int - err = s.get.QueryRow(id).Scan(&u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &embeds) + err = s.get.QueryRow(id).Scan(&u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &u.CreatedAt, &embeds) if err == nil { if embeds != -1 { u.ParseSettings = DefaultParseSettings.CopyPtr() @@ -108,7 +109,7 @@ func (s *DefaultUserStore) Get(id int) (*User, error) { func (s *DefaultUserStore) GetByName(name string) (*User, error) { u := &User{Loggedin: true} var embeds int - err := s.getByName.QueryRow(name).Scan(&u.ID, &u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &embeds) + err := s.getByName.QueryRow(name).Scan(&u.ID, &u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &u.CreatedAt, &embeds) if err == nil { if embeds != -1 { u.ParseSettings = DefaultParseSettings.CopyPtr() @@ -132,7 +133,7 @@ func (s *DefaultUserStore) GetOffset(offset, perPage int) (users []*User, err er var embeds int for rows.Next() { u := &User{Loggedin: true} - err := rows.Scan(&u.ID, &u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &embeds) + err := rows.Scan(&u.ID, &u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &u.CreatedAt, &embeds) if err != nil { return nil, err } @@ -155,7 +156,7 @@ func (s *DefaultUserStore) Each(f func(*User) error) error { var embeds int for rows.Next() { u := new(User) - if err := rows.Scan(&u.ID, &u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &embeds); err != nil { + if err := rows.Scan(&u.ID, &u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &u.CreatedAt, &embeds); err != nil { return err } if embeds != -1 { @@ -213,7 +214,7 @@ func (s *DefaultUserStore) BulkGetMap(ids []int) (list map[int]*User, err error) } q = q[0 : len(q)-1] - rows, err := qgen.NewAcc().Select("users").Columns("uid,name,group,active,is_super_admin,session,email,avatar,message,level,score,posts,liked,last_ip,temp_group,enable_embeds").Where("uid IN(" + q + ")").Query(idList...) + rows, err := qgen.NewAcc().Select("users").Columns("uid,name,group,active,is_super_admin,session,email,avatar,message,level,score,posts,liked,last_ip,temp_group,createdAt,enable_embeds").Where("uid IN(" + q + ")").Query(idList...) if err != nil { return list, err } @@ -222,7 +223,7 @@ func (s *DefaultUserStore) BulkGetMap(ids []int) (list map[int]*User, err error) var embeds int for rows.Next() { u := &User{Loggedin: true} - err := rows.Scan(&u.ID, &u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &embeds) + err := rows.Scan(&u.ID, &u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &u.CreatedAt, &embeds) if err != nil { return list, err } @@ -259,7 +260,7 @@ func (s *DefaultUserStore) BulkGetMap(ids []int) (list map[int]*User, err error) func (s *DefaultUserStore) BypassGet(id int) (*User, error) { u := &User{ID: id, Loggedin: true} var embeds int - err := s.get.QueryRow(id).Scan(&u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &embeds) + err := s.get.QueryRow(id).Scan(&u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &u.CreatedAt, &embeds) if err == nil { if embeds != -1 { u.ParseSettings = DefaultParseSettings.CopyPtr() diff --git a/extend/plugin_skeleton.go b/extend/plugin_skeleton.go index bee024b0..c5dfff8f 100644 --- a/extend/plugin_skeleton.go +++ b/extend/plugin_skeleton.go @@ -31,9 +31,9 @@ func init() { c.Plugins.Add(&c.Plugin{UName: "skeleton", Name: "Skeleton", Author: "Azareal", Init: initSkeleton, Activate: activateSkeleton, Deactivate: deactivateSkeleton}) } -func initSkeleton(plugin *c.Plugin) error { return nil } +func initSkeleton(pl *c.Plugin) error { return nil } // Any errors encountered while trying to activate the plugin are reported back to the admin and the activation is aborted -func activateSkeleton(plugin *c.Plugin) error { return nil } +func activateSkeleton(pl *c.Plugin) error { return nil } -func deactivateSkeleton(plugin *c.Plugin) {} +func deactivateSkeleton(pl *c.Plugin) {} diff --git a/langs/english.json b/langs/english.json index e1424deb..84985a73 100644 --- a/langs/english.json +++ b/langs/english.json @@ -932,6 +932,7 @@ "panel_group_promotions_two_way":"Two Way", "panel_group_promotions_level":"Level", "panel_group_promotions_posts":"Posts", + "panel_group_promotion_registered_for":"Registered For", "panel_group_promotions_create_button":"Add Promotion", "panel_word_filters_head":"Word Filters", diff --git a/misc_test.go b/misc_test.go index ad3d9e7d..0c0d2acd 100644 --- a/misc_test.go +++ b/misc_test.go @@ -499,7 +499,7 @@ func topicStoreTest(t *testing.T, newID int, ip string) { return "" } - testTopic := func(tid int, title string, content string, createdBy int, ip string, parentID int, isClosed bool, sticky bool) { + testTopic := func(tid int, title, content string, createdBy int, ip string, parentID int, isClosed, sticky bool) { topic, err = c.Topics.Get(tid) recordMustExist(t, err, fmt.Sprintf("Couldn't find TID #%d", tid)) expect(t, topic.ID == tid, fmt.Sprintf("topic.ID does not match the requested TID. Got '%d' instead.", topic.ID)) @@ -734,7 +734,7 @@ func TestForumPermsStore(t *testing.T) { c.InitPlugins() } - f := func(fid int, gid int, msg string, inv ...bool) { + f := func(fid, gid int, msg string, inv ...bool) { fp, err := c.FPStore.Get(fid, gid) expectNilErr(t, err) vt := fp.ViewTopic @@ -900,6 +900,64 @@ func TestGroupStore(t *testing.T) { // TODO: Test group cache set } +func TestGroupPromotions(t *testing.T) { + miscinit(t) + if !c.PluginsInited { + c.InitPlugins() + } + + _, err := c.GroupPromotions.Get(-1) + recordMustNotExist(t, err, "GP #-1 shouldn't exist") + _, err = c.GroupPromotions.Get(0) + recordMustNotExist(t, err, "GP #0 shouldn't exist") + _, err = c.GroupPromotions.Get(1) + recordMustNotExist(t, err, "GP #1 shouldn't exist") + expectNilErr(t, c.GroupPromotions.Delete(1)) + + //GetByGroup(gid int) (gps []*GroupPromotion, err error) + + testPromo := func(exid, from, to, level, posts, registeredFor int, shouldFail bool) { + gpid, err := c.GroupPromotions.Create(from, to, false, level, posts, registeredFor) + expect(t, gpid == exid, fmt.Sprintf("gpid should be %d not %d", exid, gpid)) + //fmt.Println("gpid:", gpid) + gp, err := c.GroupPromotions.Get(gpid) + expectNilErr(t, err) + expect(t, gp.ID == gpid, fmt.Sprintf("gp.ID should be %d not %d", gpid, gp.ID)) + expect(t, gp.From == from, fmt.Sprintf("gp.From should be %d not %d", from, gp.From)) + expect(t, gp.To == to, fmt.Sprintf("gp.To should be %d not %d", to, gp.To)) + expect(t, !gp.TwoWay, "gp.TwoWay should be false not true") + expect(t, gp.Level == level, fmt.Sprintf("gp.Level should be %d not %d", level, gp.Level)) + expect(t, gp.Posts == posts, fmt.Sprintf("gp.Posts should be %d not %d", posts, gp.Posts)) + expect(t, gp.MinTime == 0, fmt.Sprintf("gp.MinTime should be %d not %d", 0, gp.MinTime)) + expect(t, gp.RegisteredFor == registeredFor, fmt.Sprintf("gp.RegisteredFor should be %d not %d", registeredFor, gp.RegisteredFor)) + + uid, err := c.Users.Create("Lord_"+strconv.Itoa(gpid), "I_Rule", "", from, false) + expectNilErr(t, err) + u, err := c.Users.Get(uid) + expectNilErr(t, err) + expect(t, u.ID == uid, fmt.Sprintf("u.ID should be %d not %d", uid, u.ID)) + expect(t, u.Group == from, fmt.Sprintf("u.Group should be %d not %d", from, u.Group)) + err = c.GroupPromotions.PromoteIfEligible(u, u.Level, u.Posts, u.CreatedAt) + expectNilErr(t, err) + u.CacheRemove() + u, err = c.Users.Get(uid) + expectNilErr(t, err) + expect(t, u.ID == uid, fmt.Sprintf("u.ID should be %d not %d", uid, u.ID)) + if shouldFail { + expect(t, u.Group == from, fmt.Sprintf("u.Group should be (from-group) %d not %d", from, u.Group)) + } else { + expect(t, u.Group == to, fmt.Sprintf("u.Group should be (to-group)%d not %d", to, u.Group)) + } + + expectNilErr(t, c.GroupPromotions.Delete(gpid)) + _, err = c.GroupPromotions.Get(gpid) + recordMustNotExist(t, err, fmt.Sprintf("GP #%d should no longer exist", gpid)) + } + testPromo(1, 1, 2, 0, 0, 0, false) + testPromo(2, 1, 2, 5, 5, 0, true) + testPromo(3, 1, 2, 0, 0, 1, true) +} + func TestReplyStore(t *testing.T) { miscinit(t) if !c.PluginsInited { @@ -1073,21 +1131,21 @@ func TestActivityStream(t *testing.T) { func TestLogs(t *testing.T) { miscinit(t) - gTests := func(store c.LogStore, phrase string) { - expect(t, store.Count() == 0, "There shouldn't be any "+phrase) - logs, err := store.GetOffset(0, 25) + gTests := func(s c.LogStore, phrase string) { + expect(t, s.Count() == 0, "There shouldn't be any "+phrase) + logs, err := s.GetOffset(0, 25) expectNilErr(t, err) expect(t, len(logs) == 0, "The log slice should be empty") } gTests(c.ModLogs, "modlogs") gTests(c.AdminLogs, "adminlogs") - gTests2 := func(store c.LogStore, phrase string) { - err := store.Create("something", 0, "bumblefly", "::1", 1) + gTests2 := func(s c.LogStore, phrase string) { + err := s.Create("something", 0, "bumblefly", "::1", 1) expectNilErr(t, err) - count := store.Count() + count := s.Count() expect(t, count == 1, fmt.Sprintf("store.Count should return one, not %d", count)) - logs, err := store.GetOffset(0, 25) + logs, err := s.GetOffset(0, 25) recordMustExist(t, err, "We should have at-least one "+phrase) expect(t, len(logs) == 1, "The length of the log slice should be one") @@ -1113,125 +1171,125 @@ func TestPluginManager(t *testing.T) { _, ok := c.Plugins["fairy-dust"] expect(t, !ok, "Plugin fairy-dust shouldn't exist") - plugin, ok := c.Plugins["bbcode"] + pl, ok := c.Plugins["bbcode"] expect(t, ok, "Plugin bbcode should exist") - expect(t, !plugin.Installable, "Plugin bbcode shouldn't be installable") - expect(t, !plugin.Installed, "Plugin bbcode shouldn't be 'installed'") - expect(t, !plugin.Active, "Plugin bbcode shouldn't be active") - active, err := plugin.BypassActive() + expect(t, !pl.Installable, "Plugin bbcode shouldn't be installable") + expect(t, !pl.Installed, "Plugin bbcode shouldn't be 'installed'") + expect(t, !pl.Active, "Plugin bbcode shouldn't be active") + active, err := pl.BypassActive() expectNilErr(t, err) expect(t, !active, "Plugin bbcode shouldn't be active in the database either") - hasPlugin, err := plugin.InDatabase() + hasPlugin, err := pl.InDatabase() expectNilErr(t, err) expect(t, !hasPlugin, "Plugin bbcode shouldn't exist in the database") // TODO: Add some test cases for SetActive and SetInstalled before calling AddToDatabase - expectNilErr(t, plugin.AddToDatabase(true, false)) - expect(t, !plugin.Installable, "Plugin bbcode shouldn't be installable") - expect(t, !plugin.Installed, "Plugin bbcode shouldn't be 'installed'") - expect(t, plugin.Active, "Plugin bbcode should be active") - active, err = plugin.BypassActive() + expectNilErr(t, pl.AddToDatabase(true, false)) + expect(t, !pl.Installable, "Plugin bbcode shouldn't be installable") + expect(t, !pl.Installed, "Plugin bbcode shouldn't be 'installed'") + expect(t, pl.Active, "Plugin bbcode should be active") + active, err = pl.BypassActive() expectNilErr(t, err) expect(t, active, "Plugin bbcode should be active in the database too") - hasPlugin, err = plugin.InDatabase() + hasPlugin, err = pl.InDatabase() expectNilErr(t, err) expect(t, hasPlugin, "Plugin bbcode should exist in the database") - expect(t, plugin.Init != nil, "Plugin bbcode should have an init function") - expectNilErr(t, plugin.Init(plugin)) + expect(t, pl.Init != nil, "Plugin bbcode should have an init function") + expectNilErr(t, pl.Init(pl)) - expectNilErr(t, plugin.SetActive(true)) - expect(t, !plugin.Installable, "Plugin bbcode shouldn't be installable") - expect(t, !plugin.Installed, "Plugin bbcode shouldn't be 'installed'") - expect(t, plugin.Active, "Plugin bbcode should still be active") - active, err = plugin.BypassActive() + expectNilErr(t, pl.SetActive(true)) + expect(t, !pl.Installable, "Plugin bbcode shouldn't be installable") + expect(t, !pl.Installed, "Plugin bbcode shouldn't be 'installed'") + expect(t, pl.Active, "Plugin bbcode should still be active") + active, err = pl.BypassActive() expectNilErr(t, err) expect(t, active, "Plugin bbcode should still be active in the database too") - hasPlugin, err = plugin.InDatabase() + hasPlugin, err = pl.InDatabase() expectNilErr(t, err) expect(t, hasPlugin, "Plugin bbcode should still exist in the database") - expectNilErr(t, plugin.SetActive(false)) - expect(t, !plugin.Installable, "Plugin bbcode shouldn't be installable") - expect(t, !plugin.Installed, "Plugin bbcode shouldn't be 'installed'") - expect(t, !plugin.Active, "Plugin bbcode shouldn't be active") - active, err = plugin.BypassActive() + expectNilErr(t, pl.SetActive(false)) + expect(t, !pl.Installable, "Plugin bbcode shouldn't be installable") + expect(t, !pl.Installed, "Plugin bbcode shouldn't be 'installed'") + expect(t, !pl.Active, "Plugin bbcode shouldn't be active") + active, err = pl.BypassActive() expectNilErr(t, err) expect(t, !active, "Plugin bbcode shouldn't be active in the database") - hasPlugin, err = plugin.InDatabase() + hasPlugin, err = pl.InDatabase() expectNilErr(t, err) expect(t, hasPlugin, "Plugin bbcode should still exist in the database") - expect(t, plugin.Deactivate != nil, "Plugin bbcode should have an init function") - plugin.Deactivate(plugin) // Returns nothing + expect(t, pl.Deactivate != nil, "Plugin bbcode should have an init function") + pl.Deactivate(pl) // Returns nothing // Not installable, should not be mutated - expect(t, plugin.SetInstalled(true) == c.ErrPluginNotInstallable, "Plugin was set as installed despite not being installable") - expect(t, !plugin.Installable, "Plugin bbcode shouldn't be installable") - expect(t, !plugin.Installed, "Plugin bbcode shouldn't be 'installed'") - expect(t, !plugin.Active, "Plugin bbcode shouldn't be active") - active, err = plugin.BypassActive() + expect(t, pl.SetInstalled(true) == c.ErrPluginNotInstallable, "Plugin was set as installed despite not being installable") + expect(t, !pl.Installable, "Plugin bbcode shouldn't be installable") + expect(t, !pl.Installed, "Plugin bbcode shouldn't be 'installed'") + expect(t, !pl.Active, "Plugin bbcode shouldn't be active") + active, err = pl.BypassActive() expectNilErr(t, err) expect(t, !active, "Plugin bbcode shouldn't be active in the database either") - hasPlugin, err = plugin.InDatabase() + hasPlugin, err = pl.InDatabase() expectNilErr(t, err) expect(t, hasPlugin, "Plugin bbcode should still exist in the database") - expect(t, plugin.SetInstalled(false) == c.ErrPluginNotInstallable, "Plugin was set as not installed despite not being installable") - expect(t, !plugin.Installable, "Plugin bbcode shouldn't be installable") - expect(t, !plugin.Installed, "Plugin bbcode shouldn't be 'installed'") - expect(t, !plugin.Active, "Plugin bbcode shouldn't be active") - active, err = plugin.BypassActive() + expect(t, pl.SetInstalled(false) == c.ErrPluginNotInstallable, "Plugin was set as not installed despite not being installable") + expect(t, !pl.Installable, "Plugin bbcode shouldn't be installable") + expect(t, !pl.Installed, "Plugin bbcode shouldn't be 'installed'") + expect(t, !pl.Active, "Plugin bbcode shouldn't be active") + active, err = pl.BypassActive() expectNilErr(t, err) expect(t, !active, "Plugin bbcode shouldn't be active in the database either") - hasPlugin, err = plugin.InDatabase() + hasPlugin, err = pl.InDatabase() expectNilErr(t, err) expect(t, hasPlugin, "Plugin bbcode should still exist in the database") // This isn't really installable, but we want to get a few tests done before getting plugins which are stateful - plugin.Installable = true - expectNilErr(t, plugin.SetInstalled(true)) - expect(t, plugin.Installable, "Plugin bbcode should be installable") - expect(t, plugin.Installed, "Plugin bbcode should be 'installed'") - expect(t, !plugin.Active, "Plugin bbcode shouldn't be active") - active, err = plugin.BypassActive() + pl.Installable = true + expectNilErr(t, pl.SetInstalled(true)) + expect(t, pl.Installable, "Plugin bbcode should be installable") + expect(t, pl.Installed, "Plugin bbcode should be 'installed'") + expect(t, !pl.Active, "Plugin bbcode shouldn't be active") + active, err = pl.BypassActive() expectNilErr(t, err) expect(t, !active, "Plugin bbcode shouldn't be active in the database either") - hasPlugin, err = plugin.InDatabase() + hasPlugin, err = pl.InDatabase() expectNilErr(t, err) expect(t, hasPlugin, "Plugin bbcode should still exist in the database") - expectNilErr(t, plugin.SetInstalled(false)) - expect(t, plugin.Installable, "Plugin bbcode should be installable") - expect(t, !plugin.Installed, "Plugin bbcode shouldn't be 'installed'") - expect(t, !plugin.Active, "Plugin bbcode shouldn't be active") - active, err = plugin.BypassActive() + expectNilErr(t, pl.SetInstalled(false)) + expect(t, pl.Installable, "Plugin bbcode should be installable") + expect(t, !pl.Installed, "Plugin bbcode shouldn't be 'installed'") + expect(t, !pl.Active, "Plugin bbcode shouldn't be active") + active, err = pl.BypassActive() expectNilErr(t, err) expect(t, !active, "Plugin bbcode shouldn't be active in the database either") - hasPlugin, err = plugin.InDatabase() + hasPlugin, err = pl.InDatabase() expectNilErr(t, err) expect(t, hasPlugin, "Plugin bbcode should still exist in the database") // Bugs sometimes arise when we try to delete a hook when there are multiple, so test for that // TODO: Do a finer grained test for that case...? A bigger test might catch more odd cases with multiple plugins - plugin2, ok := c.Plugins["markdown"] + pl2, ok := c.Plugins["markdown"] expect(t, ok, "Plugin markdown should exist") - expect(t, !plugin2.Installable, "Plugin markdown shouldn't be installable") - expect(t, !plugin2.Installed, "Plugin markdown shouldn't be 'installed'") - expect(t, !plugin2.Active, "Plugin markdown shouldn't be active") - active, err = plugin2.BypassActive() + expect(t, !pl2.Installable, "Plugin markdown shouldn't be installable") + expect(t, !pl2.Installed, "Plugin markdown shouldn't be 'installed'") + expect(t, !pl2.Active, "Plugin markdown shouldn't be active") + active, err = pl2.BypassActive() expectNilErr(t, err) expect(t, !active, "Plugin markdown shouldn't be active in the database either") - hasPlugin, err = plugin2.InDatabase() + hasPlugin, err = pl2.InDatabase() expectNilErr(t, err) expect(t, !hasPlugin, "Plugin markdown shouldn't exist in the database") - expectNilErr(t, plugin2.AddToDatabase(true, false)) - expectNilErr(t, plugin2.Init(plugin2)) - expectNilErr(t, plugin.SetActive(true)) - expectNilErr(t, plugin.Init(plugin)) - plugin2.Deactivate(plugin2) - expectNilErr(t, plugin2.SetActive(false)) - plugin.Deactivate(plugin) - expectNilErr(t, plugin.SetActive(false)) + expectNilErr(t, pl2.AddToDatabase(true, false)) + expectNilErr(t, pl2.Init(pl2)) + expectNilErr(t, pl.SetActive(true)) + expectNilErr(t, pl.Init(pl)) + pl2.Deactivate(pl2) + expectNilErr(t, pl2.SetActive(false)) + pl.Deactivate(pl) + expectNilErr(t, pl.SetActive(false)) // Hook tests ht := func() *c.HookTable { @@ -1241,18 +1299,18 @@ func TestPluginManager(t *testing.T) { handle := func(in string) (out string) { return in + "hi" } - plugin.AddHook("haha", handle) + pl.AddHook("haha", handle) expect(t, ht().Sshook("haha", "ho") == "hohi", "Sshook didn't give hohi") - plugin.RemoveHook("haha", handle) + pl.RemoveHook("haha", handle) expect(t, ht().Sshook("haha", "ho") == "ho", "Sshook shouldn't have anything bound to it anymore") expect(t, ht().Hook("haha", "ho") == "ho", "Hook shouldn't have anything bound to it yet") handle2 := func(inI interface{}) (out interface{}) { return inI.(string) + "hi" } - plugin.AddHook("hehe", handle2) + pl.AddHook("hehe", handle2) expect(t, ht().Hook("hehe", "ho").(string) == "hohi", "Hook didn't give hohi") - plugin.RemoveHook("hehe", handle2) + pl.RemoveHook("hehe", handle2) expect(t, ht().Hook("hehe", "ho").(string) == "ho", "Hook shouldn't have anything bound to it anymore") // TODO: Add tests for more hook types @@ -1260,7 +1318,7 @@ func TestPluginManager(t *testing.T) { func TestPhrases(t *testing.T) { getPhrase := phrases.GetPermPhrase - tp := func(name string, expects string) { + tp := func(name, expects string) { res := getPhrase(name) expect(t, res == expects, "Not the expected phrase, got '"+res+"' instead") } @@ -1572,7 +1630,7 @@ func TestAuth(t *testing.T) { } // TODO: Vary the salts? Keep in mind that some algorithms store the salt in the hash therefore the salt string may be blank -func passwordTest(t *testing.T, realPassword string, hashedPassword string) { +func passwordTest(t *testing.T, realPassword, hashedPassword string) { if len(hashedPassword) < 10 { t.Error("Hash too short") } @@ -1638,7 +1696,7 @@ type CountTestList struct { Items []CountTest } -func (l *CountTestList) Add(name string, msg string, expects int) { +func (l *CountTestList) Add(name, msg string, expects int) { l.Items = append(l.Items, CountTest{name, msg, expects}) } diff --git a/patcher/patches.go b/patcher/patches.go index 9018da2e..6cc1b53a 100644 --- a/patcher/patches.go +++ b/patcher/patches.go @@ -47,6 +47,7 @@ func init() { addPatch(27, patch27) addPatch(28, patch28) addPatch(29, patch29) + addPatch(30, patch30) } func patch0(scanner *bufio.Scanner) (err error) { @@ -833,10 +834,6 @@ func patch29(scanner *bufio.Scanner) error { if err != nil { return err } - err = execStmt(qgen.Builder.SetDefaultColumn("users", "last_ip", "varchar", "")) - if err != nil { - return err - } err = execStmt(qgen.Builder.SetDefaultColumn("replies", "lastEdit", "int", "0")) if err != nil { @@ -858,3 +855,11 @@ func patch29(scanner *bufio.Scanner) error { return execStmt(qgen.Builder.AddColumn("activity_stream", tC{"extra", "varchar", 200, false, false, "''"}, nil)) } + +func patch30(scanner *bufio.Scanner) error { + err := execStmt(qgen.Builder.AddColumn("users_groups_promotions", tC{"registeredFor", "int", 0, false, false, "0"}, nil)) + if err != nil { + return err + } + return execStmt(qgen.Builder.SetDefaultColumn("users", "last_ip", "varchar", "")) +} diff --git a/routes/attachments.go b/routes/attachments.go index c28c293a..02c48e0a 100644 --- a/routes/attachments.go +++ b/routes/attachments.go @@ -99,7 +99,7 @@ func deleteAttachment(w http.ResponseWriter, r *http.Request, user c.User, aid i // TODO: Stop duplicating this code // TODO: Use a transaction here // TODO: Move this function to neutral ground -func uploadAttachment(w http.ResponseWriter, r *http.Request, user c.User, sid int, sectionTable string, oid int, originTable string, extra string) (pathMap map[string]string, rerr c.RouteError) { +func uploadAttachment(w http.ResponseWriter, r *http.Request, user c.User, sid int, sectionTable string, oid int, originTable, extra string) (pathMap map[string]string, rerr c.RouteError) { pathMap = make(map[string]string) files, rerr := uploadFilesWithHash(w, r, user, "./attachs/") if rerr != nil { diff --git a/routes/moderate.go b/routes/moderate.go index bef8bb94..f3b2d9e6 100644 --- a/routes/moderate.go +++ b/routes/moderate.go @@ -7,8 +7,8 @@ import ( "github.com/Azareal/Gosora/common/phrases" ) -func IPSearch(w http.ResponseWriter, r *http.Request, user c.User, header *c.Header) c.RouteError { - header.Title = phrases.GetTitlePhrase("ip_search") +func IPSearch(w http.ResponseWriter, r *http.Request, user c.User, h *c.Header) c.RouteError { + h.Title = phrases.GetTitlePhrase("ip_search") // TODO: How should we handle the permissions if we extend this into an alt detector of sorts? if !user.Perms.ViewIPs { return c.NoPermissions(w, r, user) @@ -26,5 +26,5 @@ func IPSearch(w http.ResponseWriter, r *http.Request, user c.User, header *c.Hea if err != nil { return c.InternalError(err, w, r) } - return renderTemplate("ip_search", w, r, header, c.IPSearchPage{header, userList, ip}) + return renderTemplate("ip_search", w, r, h, c.IPSearchPage{h, userList, ip}) } diff --git a/routes/panel/groups.go b/routes/panel/groups.go index fa684a3c..8d7d548e 100644 --- a/routes/panel/groups.go +++ b/routes/panel/groups.go @@ -29,8 +29,7 @@ func Groups(w http.ResponseWriter, r *http.Request, u c.User) c.RouteError { if count == perPage { break } - var rank string - var rankClass string + var rank, rankClass string canDelete := false // TODO: Localise this @@ -215,6 +214,20 @@ func GroupsPromotionsCreateSubmit(w http.ResponseWriter, r *http.Request, user c return c.LocalError("posts must be integer", w, r, user) } + registeredHours, err := strconv.Atoi(r.FormValue("registered_hours")) + if err != nil { + return c.LocalError("registered_hours must be integer", w, r, user) + } + registeredDays, err := strconv.Atoi(r.FormValue("registered_days")) + if err != nil { + return c.LocalError("registered_days must be integer", w, r, user) + } + registeredMonths, err := strconv.Atoi(r.FormValue("registered_months")) + if err != nil { + return c.LocalError("registered_months must be integer", w, r, user) + } + registeredMinutes := (registeredHours * 60) + (registeredDays * 24 * 60) + (registeredMonths * 30 * 24 * 60) + g, err := c.Groups.Get(from) ferr := groupCheck(w, r, user, g, err) if err != nil { @@ -225,7 +238,7 @@ func GroupsPromotionsCreateSubmit(w http.ResponseWriter, r *http.Request, user c if err != nil { return ferr } - pid, err := c.GroupPromotions.Create(from, to, twoWay, level, posts) + pid, err := c.GroupPromotions.Create(from, to, twoWay, level, posts, registeredMinutes) if err != nil { return c.InternalError(err, w, r) } diff --git a/routes/panel/users.go b/routes/panel/users.go index 1eeb6019..66028e75 100644 --- a/routes/panel/users.go +++ b/routes/panel/users.go @@ -99,34 +99,34 @@ func UsersEditSubmit(w http.ResponseWriter, r *http.Request, user c.User, suid s return c.LocalError("Only administrators can edit the account of other administrators.", w, r, user) } - newname := c.SanitiseSingleLine(r.PostFormValue("name")) - if newname == "" { + newName := c.SanitiseSingleLine(r.PostFormValue("name")) + if newName == "" { return c.LocalError("You didn't put in a name.", w, r, user) } // TODO: How should activation factor into admin set emails? // TODO: How should we handle secondary emails? Do we even have secondary emails implemented? - newemail := c.SanitiseSingleLine(r.PostFormValue("email")) - if newemail == "" && targetUser.Email != "" { + newEmail := c.SanitiseSingleLine(r.PostFormValue("email")) + if newEmail == "" && targetUser.Email != "" { return c.LocalError("You didn't put in an email address.", w, r, user) } - if newemail == "-1" { - newemail = targetUser.Email + if newEmail == "-1" { + newEmail = targetUser.Email } - if (newemail != targetUser.Email) && !user.Perms.EditUserEmail { + if (newEmail != targetUser.Email) && !user.Perms.EditUserEmail { return c.LocalError("You need the EditUserEmail permission to edit the email address of a user.", w, r, user) } - newpassword := r.PostFormValue("password") - if newpassword != "" && !user.Perms.EditUserPassword { + newPassword := r.PostFormValue("password") + if newPassword != "" && !user.Perms.EditUserPassword { return c.LocalError("You need the EditUserPassword permission to edit the password of a user.", w, r, user) } - newgroup, err := strconv.Atoi(r.PostFormValue("group")) + newGroup, err := strconv.Atoi(r.PostFormValue("group")) if err != nil { return c.LocalError("You need to provide a whole number for the group ID", w, r, user) } - group, err := c.Groups.Get(newgroup) + group, err := c.Groups.Get(newGroup) if err == sql.ErrNoRows { return c.LocalError("The group you're trying to place this user in doesn't exist.", w, r, user) } else if err != nil { @@ -139,20 +139,32 @@ func UsersEditSubmit(w http.ResponseWriter, r *http.Request, user c.User, suid s return c.LocalError("You need the EditUserGroupSuperMod permission to assign someone to a super mod group.", w, r, user) } - err = targetUser.Update(newname, newemail, newgroup) + err = targetUser.Update(newName, newEmail, newGroup) if err != nil { return c.InternalError(err, w, r) } red := false - if newpassword != "" { - c.SetPassword(targetUser.ID, newpassword) + if newPassword != "" { + c.SetPassword(targetUser.ID, newPassword) // Log the user out as a safety precaution c.Auth.ForceLogout(targetUser.ID) red = true } targetUser.CacheRemove() + targetUser, err = c.Users.Get(uid) + if err == sql.ErrNoRows { + return c.LocalError("The user you're trying to edit doesn't exist.", w, r, user) + } else if err != nil { + return c.InternalError(err, w, r) + } + err = c.GroupPromotions.PromoteIfEligible(targetUser, targetUser.Level, targetUser.Posts, targetUser.CreatedAt) + if err != nil { + return c.InternalError(err, w, r) + } + targetUser.CacheRemove() + err = c.AdminLogs.Create("edit", targetUser.ID, "user", user.GetIP(), user.ID) if err != nil { return c.InternalError(err, w, r) diff --git a/routes/poll.go b/routes/poll.go index 54090ddd..0eee6cc6 100644 --- a/routes/poll.go +++ b/routes/poll.go @@ -15,7 +15,6 @@ func PollVote(w http.ResponseWriter, r *http.Request, user c.User, sPollID strin if err != nil { return c.PreError("The provided PollID is not a valid number.", w, r) } - poll, err := c.Polls.Get(pollID) if err == sql.ErrNoRows { return c.PreError("The poll you tried to vote for doesn't exist.", w, r) @@ -72,7 +71,6 @@ func PollResults(w http.ResponseWriter, r *http.Request, user c.User, sPollID st if err != nil { return c.PreError("The provided PollID is not a valid number.", w, r) } - poll, err := c.Polls.Get(pollID) if err == sql.ErrNoRows { return c.PreError("The poll you tried to vote for doesn't exist.", w, r) @@ -81,7 +79,7 @@ func PollResults(w http.ResponseWriter, r *http.Request, user c.User, sPollID st } // TODO: Abstract this - rows, err := qgen.NewAcc().Select("polls_options").Columns("votes").Where("pollID = ?").Orderby("option ASC").Query(poll.ID) + rows, err := qgen.NewAcc().Select("polls_options").Columns("votes").Where("pollID=?").Orderby("option ASC").Query(poll.ID) if err != nil { return c.InternalError(err, w, r) } diff --git a/routes/reports.go b/routes/reports.go index ed3d0dd4..cf2c5069 100644 --- a/routes/reports.go +++ b/routes/reports.go @@ -9,14 +9,14 @@ import ( "github.com/Azareal/Gosora/common/counters" ) -func ReportSubmit(w http.ResponseWriter, r *http.Request, user c.User, sitemID string) c.RouteError { +func ReportSubmit(w http.ResponseWriter, r *http.Request, user c.User, sItemID string) c.RouteError { headerLite, ferr := c.SimpleUserCheck(w, r, &user) if ferr != nil { return ferr } js := r.PostFormValue("js") == "1" - itemID, err := strconv.Atoi(sitemID) + itemID, err := strconv.Atoi(sItemID) if err != nil { return c.LocalError("Bad ID", w, r, user) } diff --git a/routes/topic.go b/routes/topic.go index 07a411b8..25ba9c26 100644 --- a/routes/topic.go +++ b/routes/topic.go @@ -39,9 +39,9 @@ var topicStmts TopicStmts func init() { c.DbInits.Add(func(acc *qgen.Accumulator) error { topicStmts = TopicStmts{ - getLikedTopic: acc.Select("likes").Columns("targetItem").Where("sentBy = ? && targetItem = ? && targetType = 'topics'").Prepare(), + getLikedTopic: acc.Select("likes").Columns("targetItem").Where("sentBy=? && targetItem=? && targetType='topics'").Prepare(), // TODO: Less race-y attachment count updates - updateAttachs: acc.Update("topics").Set("attachCount = ?").Where("tid = ?").Prepare(), + updateAttachs: acc.Update("topics").Set("attachCount=?").Where("tid=?").Prepare(), } return acc.FirstError() }) @@ -98,7 +98,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user c.User, header *c.He if topic.ContentHTML == topic.Content { topic.ContentHTML = topic.Content } - + topic.Tag = postGroup.Tag if postGroup.IsMod { topic.ClassName = c.Config.StaffCSS @@ -327,9 +327,9 @@ func CreateTopic(w http.ResponseWriter, r *http.Request, user c.User, header *c. } // Do a bulk forum fetch, just in case it's the SqlForumStore? - forum := c.Forums.DirtyGet(ffid) - if forum.Name != "" && forum.Active { - fcopy := forum.Copy() + f := c.Forums.DirtyGet(ffid) + if f.Name != "" && f.Active { + fcopy := f.Copy() // TODO: Abstract this if header.Hooks.HookSkippable("topic_create_frow_assign", &fcopy) { continue @@ -707,7 +707,7 @@ func StickTopicSubmit(w http.ResponseWriter, r *http.Request, user c.User, stid return topicActionPost(topic.Stick(), "stick", w, r, lite, topic, user) } -func topicActionPre(stid string, action string, w http.ResponseWriter, r *http.Request, user c.User) (*c.Topic, *c.HeaderLite, c.RouteError) { +func topicActionPre(stid, action string, w http.ResponseWriter, r *http.Request, user c.User) (*c.Topic, *c.HeaderLite, c.RouteError) { tid, err := strconv.Atoi(stid) if err != nil { return nil, nil, c.PreError(phrases.GetErrorPhrase("id_must_be_integer"), w, r) diff --git a/routes/topic_list.go b/routes/topic_list.go index ee58f650..a3929e33 100644 --- a/routes/topic_list.go +++ b/routes/topic_list.go @@ -32,11 +32,11 @@ func TopicListMostViewed(w http.ResponseWriter, r *http.Request, user c.User, h } // TODO: Implement search -func TopicListCommon(w http.ResponseWriter, r *http.Request, user c.User, header *c.Header, torder string, tsorder string) c.RouteError { - header.Title = phrases.GetTitlePhrase("topics") - header.Zone = "topics" - header.Path = "/topics/" - header.MetaDesc = header.Settings["meta_desc"].(string) +func TopicListCommon(w http.ResponseWriter, r *http.Request, user c.User, h *c.Header, torder, tsorder string) c.RouteError { + h.Title = phrases.GetTitlePhrase("topics") + h.Zone = "topics" + h.Path = "/topics/" + h.MetaDesc = h.Settings["meta_desc"].(string) group, err := c.Groups.Get(user.Group) if err != nil { @@ -61,8 +61,8 @@ func TopicListCommon(w http.ResponseWriter, r *http.Request, user c.User, header if err != nil { return c.LocalError("Invalid fid forum", w, r, user) } - header.Title = forum.Name - header.ZoneID = forum.ID + h.Title = forum.Name + h.ZoneID = forum.ID } } @@ -95,8 +95,8 @@ func TopicListCommon(w http.ResponseWriter, r *http.Request, user c.User, header } for _, fid := range fids { if inSlice(canSee, fid) { - forum := c.Forums.DirtyGet(fid) - if forum.Name != "" && forum.Active && (forum.ParentType == "" || forum.ParentType == "forum") { + f := c.Forums.DirtyGet(fid) + if f.Name != "" && f.Active && (f.ParentType == "" || f.ParentType == "forum") { // TODO: Add a hook here for plugin_guilds? cfids = append(cfids, fid) } @@ -118,10 +118,10 @@ func TopicListCommon(w http.ResponseWriter, r *http.Request, user c.User, header return c.InternalError(err, w, r) } reqUserList := make(map[int]bool) - for _, topic := range tMap { - reqUserList[topic.CreatedBy] = true - reqUserList[topic.LastReplyBy] = true - topicList = append(topicList, topic.TopicsRow()) + for _, t := range tMap { + reqUserList[t.CreatedBy] = true + reqUserList[t.LastReplyBy] = true + topicList = append(topicList, t.TopicsRow()) } //fmt.Printf("reqUserList %+v\n", reqUserList) @@ -141,18 +141,18 @@ func TopicListCommon(w http.ResponseWriter, r *http.Request, user c.User, header } // TODO: De-dupe this logic in common/topic_list.go? - for _, topic := range topicList { - topic.Link = c.BuildTopicURL(c.NameToSlug(topic.Title), topic.ID) + for _, t := range topicList { + t.Link = c.BuildTopicURL(c.NameToSlug(t.Title), t.ID) // TODO: Pass forum to something like topic.Forum and use that instead of these two properties? Could be more flexible. - forum := c.Forums.DirtyGet(topic.ParentID) - topic.ForumName = forum.Name - topic.ForumLink = forum.Link + forum := c.Forums.DirtyGet(t.ParentID) + t.ForumName = forum.Name + t.ForumLink = forum.Link // TODO: Create a specialised function with a bit less overhead for getting the last page for a post count - _, _, lastPage := c.PageOffset(topic.PostCount, 1, c.Config.ItemsPerPage) - topic.LastPage = lastPage - topic.Creator = userList[topic.CreatedBy] - topic.LastUser = userList[topic.LastReplyBy] + _, _, lastPage := c.PageOffset(t.PostCount, 1, c.Config.ItemsPerPage) + t.LastPage = lastPage + t.Creator = userList[t.CreatedBy] + t.LastUser = userList[t.LastReplyBy] } // TODO: Reduce the amount of boilerplate here @@ -165,9 +165,9 @@ func TopicListCommon(w http.ResponseWriter, r *http.Request, user c.User, header return nil } - header.Title = phrases.GetTitlePhrase("topics_search") - pi := c.TopicListPage{header, topicList, forumList, c.Config.DefaultForum, c.TopicListSort{torder, false}, paginator} - return renderTemplate("topics", w, r, header, pi) + h.Title = phrases.GetTitlePhrase("topics_search") + pi := c.TopicListPage{h, topicList, forumList, c.Config.DefaultForum, c.TopicListSort{torder, false}, paginator} + return renderTemplate("topics", w, r, h, pi) } // TODO: Pass a struct back rather than passing back so many variables @@ -190,6 +190,6 @@ func TopicListCommon(w http.ResponseWriter, r *http.Request, user c.User, header return nil } - pi := c.TopicListPage{header, topicList, forumList, c.Config.DefaultForum, c.TopicListSort{torder, false}, paginator} - return renderTemplate("topics", w, r, header, pi) + pi := c.TopicListPage{h, topicList, forumList, c.Config.DefaultForum, c.TopicListSort{torder, false}, paginator} + return renderTemplate("topics", w, r, h, pi) } diff --git a/routes/user.go b/routes/user.go index dfc9ffca..659c8f93 100644 --- a/routes/user.go +++ b/routes/user.go @@ -171,6 +171,18 @@ func ActivateUser(w http.ResponseWriter, r *http.Request, user c.User, suid stri return c.InternalError(err, w, r) } + targetUser, err = c.Users.Get(uid) + if err == sql.ErrNoRows { + return c.LocalError("The account you're trying to activate no longer exists.", w, r, user) + } else if err != nil { + return c.InternalError(err, w, r) + } + err = c.GroupPromotions.PromoteIfEligible(targetUser, targetUser.Level, targetUser.Posts, targetUser.CreatedAt) + if err != nil { + return c.InternalError(err, w, r) + } + targetUser.CacheRemove() + err = c.ModLogs.Create("activate", targetUser.ID, "user", user.GetIP(), user.ID) if err != nil { return c.InternalError(err, w, r) diff --git a/schema/mssql/query_users.sql b/schema/mssql/query_users.sql index bf3f9899..ab2b7f43 100644 --- a/schema/mssql/query_users.sql +++ b/schema/mssql/query_users.sql @@ -9,7 +9,7 @@ CREATE TABLE [users] ( [createdAt] datetime not null, [lastActiveAt] datetime not null, [session] nvarchar (200) DEFAULT '' not null, - [last_ip] nvarchar (200) DEFAULT '0.0.0.0.0' not null, + [last_ip] nvarchar (200) DEFAULT '' not null, [enable_embeds] int DEFAULT -1 not null, [email] nvarchar (200) DEFAULT '' not null, [avatar] nvarchar (100) DEFAULT '' not null, diff --git a/schema/mssql/query_users_groups_promotions.sql b/schema/mssql/query_users_groups_promotions.sql index 3e722449..dca4c7c7 100644 --- a/schema/mssql/query_users_groups_promotions.sql +++ b/schema/mssql/query_users_groups_promotions.sql @@ -6,5 +6,6 @@ CREATE TABLE [users_groups_promotions] ( [level] int not null, [posts] int DEFAULT 0 not null, [minTime] int not null, + [registeredFor] int DEFAULT 0 not null, primary key([pid]) ); \ No newline at end of file diff --git a/schema/mysql/query_users.sql b/schema/mysql/query_users.sql index a21a1d1f..59fd9e0e 100644 --- a/schema/mysql/query_users.sql +++ b/schema/mysql/query_users.sql @@ -9,7 +9,7 @@ CREATE TABLE `users` ( `createdAt` datetime not null, `lastActiveAt` datetime not null, `session` varchar(200) DEFAULT '' not null, - `last_ip` varchar(200) DEFAULT '0.0.0.0.0' not null, + `last_ip` varchar(200) DEFAULT '' not null, `enable_embeds` int DEFAULT -1 not null, `email` varchar(200) DEFAULT '' not null, `avatar` varchar(100) DEFAULT '' not null, diff --git a/schema/mysql/query_users_groups_promotions.sql b/schema/mysql/query_users_groups_promotions.sql index 8c1dec3e..9252c58b 100644 --- a/schema/mysql/query_users_groups_promotions.sql +++ b/schema/mysql/query_users_groups_promotions.sql @@ -6,5 +6,6 @@ CREATE TABLE `users_groups_promotions` ( `level` int not null, `posts` int DEFAULT 0 not null, `minTime` int not null, + `registeredFor` int DEFAULT 0 not null, primary key(`pid`) ) CHARSET=utf8mb4 COLLATE utf8mb4_general_ci; \ No newline at end of file diff --git a/schema/pgsql/query_users.sql b/schema/pgsql/query_users.sql index 525983da..bf3c913c 100644 --- a/schema/pgsql/query_users.sql +++ b/schema/pgsql/query_users.sql @@ -9,7 +9,7 @@ CREATE TABLE "users" ( `createdAt` timestamp not null, `lastActiveAt` timestamp not null, `session` varchar (200) DEFAULT '' not null, - `last_ip` varchar (200) DEFAULT '0.0.0.0.0' not null, + `last_ip` varchar (200) DEFAULT '' not null, `enable_embeds` int DEFAULT -1 not null, `email` varchar (200) DEFAULT '' not null, `avatar` varchar (100) DEFAULT '' not null, diff --git a/schema/pgsql/query_users_groups_promotions.sql b/schema/pgsql/query_users_groups_promotions.sql index 5bec0a3e..319c0104 100644 --- a/schema/pgsql/query_users_groups_promotions.sql +++ b/schema/pgsql/query_users_groups_promotions.sql @@ -6,5 +6,6 @@ CREATE TABLE "users_groups_promotions" ( `level` int not null, `posts` int DEFAULT 0 not null, `minTime` int not null, + `registeredFor` int DEFAULT 0 not null, primary key(`pid`) ); \ No newline at end of file diff --git a/templates/panel_group_edit_promotions.html b/templates/panel_group_edit_promotions.html index 7135dd16..8457bddf 100644 --- a/templates/panel_group_edit_promotions.html +++ b/templates/panel_group_edit_promotions.html @@ -13,6 +13,7 @@ {{.FromGroup.Name}} -> {{.ToGroup.Name}}{{if .TwoWay}} (two way){{end}} {{if .Level}} - {{lang "panel_group_promotions_level_prefix"}}{{.Level}}{{end}} {{if .Posts}} - {{lang "panel_group_promotions_posts_prefix"}}{{.Posts}}{{end}} + {{if .RegisteredFor}} - registered for {{.RegisteredFor}} minutes{{end}}
@@ -56,11 +57,19 @@
-
+
-
+
+
+
+ +
+ months
+ days
+ hours +
diff --git a/templates/widget_search_and_filter.html b/templates/widget_search_and_filter.html index a0e93bc4..a394c6ad 100644 --- a/templates/widget_search_and_filter.html +++ b/templates/widget_search_and_filter.html @@ -1,5 +1,5 @@
{{range .Forums}}