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.
This commit is contained in:
Azareal 2020-02-09 20:00:08 +10:00
parent e37c98eaa1
commit b6931fe16a
26 changed files with 349 additions and 197 deletions

View File

@ -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, ""},

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()

View File

@ -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) {}

View File

@ -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",

View File

@ -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})
}

View File

@ -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", ""))
}

View File

@ -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 {

View File

@ -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})
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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)

View File

@ -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,

View File

@ -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])
);

View File

@ -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,

View File

@ -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;

View File

@ -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,

View File

@ -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`)
);

View File

@ -13,6 +13,7 @@
<a href="#p-{{.ID}}">{{.FromGroup.Name}} -> {{.ToGroup.Name}}{{if .TwoWay}} (two way){{end}}</a>
{{if .Level}}<span>&nbsp;-&nbsp;{{lang "panel_group_promotions_level_prefix"}}{{.Level}}</span>{{end}}
{{if .Posts}}<span>&nbsp;-&nbsp;{{lang "panel_group_promotions_posts_prefix"}}{{.Posts}}</span>{{end}}
{{if .RegisteredFor}}<span>&nbsp;-&nbsp;registered for {{.RegisteredFor}} minutes</span>{{end}}
<div class="to_right">
<a href="/panel/groups/promotions/delete/submit/{{$.ID}}-{{.ID}}?s={{$.CurrentUser.Session}}"><button form="nn">{{lang "panel_group_promotions_delete_button"}}</button></a>
</div>
@ -56,11 +57,19 @@
</div>
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_group_promotions_level"}}</a></div>
<div class="formitem"><input name="level" type="number" value=0 /></div>
<div class="formitem"><input name="level" type="number" value="0"/></div>
</div>
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_group_promotions_posts"}}</a></div>
<div class="formitem"><input name="posts" type="number" value=0 /></div>
<div class="formitem"><input name="posts" type="number" value="0"/></div>
</div>
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_group_promotion_registered_for"}}</a></div>
<div class="formitem">
<input name="registered_months" type="number" value="0"/> months<br>
<input name="registered_days" type="number" value="0"/> days<br>
<input name="registered_hours" type="number" value="0"/> hours
</div>
</div>
<div class="formrow form_button_row">
<div class="formitem"><button name="panel-button" class="formbutton">{{lang "panel_group_promotions_create_button"}}</button></div>

View File

@ -1,5 +1,5 @@
<div class="search widget_search">
<input class="widget_search_input" name="widget_search" placeholder="Search" type="search" />
<input class="widget_search_input" name="widget_search" placeholder="Search" type="search"/>
</div>
<div class="rowblock filter_list widget_filter">
{{range .Forums}} <div class="rowitem filter_item{{if .Selected}} filter_selected{{end}}" data-fid={{.ID}}><a href="/topics/?fids={{.ID}}" rel="nofollow">{{.Name}} ({{.TopicCount}})</a></div>