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{"lastActiveAt", "datetime", 0, false, false, ""},
tC{"session", "varchar", 200, false, false, "''"}, tC{"session", "varchar", 200, false, false, "''"},
//tC{"authToken", "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{"enable_embeds", "int", 0, false, false, "-1"},
tC{"email", "varchar", 200, false, false, "''"}, tC{"email", "varchar", 200, false, false, "''"},
tC{"avatar", "varchar", 100, false, false, "''"}, tC{"avatar", "varchar", 100, false, false, "''"},
@ -90,13 +90,28 @@ func createTables(adapter qgen.Adapter) (err error) {
// Requirements // Requirements
tC{"level", "int", 0, false, false, ""}, tC{"level", "int", 0, false, false, ""},
tC{"posts", "int", 0, false, false, "0"}, 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{
tblKey{"pid", "primary", "", false}, 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, createTable("users_2fa_keys", mysqlPre, mysqlCol,
[]tC{ []tC{
tC{"uid", "int", 0, false, false, ""}, tC{"uid", "int", 0, false, false, ""},

View File

@ -2,6 +2,8 @@ package common
import ( import (
"database/sql" "database/sql"
//"log"
"time"
qgen "github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
) )
@ -14,17 +16,18 @@ type GroupPromotion struct {
To int To int
TwoWay bool TwoWay bool
Level int Level int
Posts int Posts int
MinTime int MinTime int
RegisteredFor int
} }
type GroupPromotionStore interface { type GroupPromotionStore interface {
GetByGroup(gid int) (gps []*GroupPromotion, err error) GetByGroup(gid int) (gps []*GroupPromotion, err error)
Get(id int) (*GroupPromotion, 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 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 { type DefaultGroupPromotionStore struct {
@ -33,21 +36,33 @@ type DefaultGroupPromotionStore struct {
delete *sql.Stmt delete *sql.Stmt
create *sql.Stmt create *sql.Stmt
getByUser *sql.Stmt getByUser *sql.Stmt
updateUser *sql.Stmt getByUserMins *sql.Stmt
updateUser *sql.Stmt
updateGeneric *sql.Stmt
} }
func NewDefaultGroupPromotionStore(acc *qgen.Accumulator) (*DefaultGroupPromotionStore, error) { func NewDefaultGroupPromotionStore(acc *qgen.Accumulator) (*DefaultGroupPromotionStore, error) {
ugp := "users_groups_promotions" ugp := "users_groups_promotions"
return &DefaultGroupPromotionStore{ prs := &DefaultGroupPromotionStore{
getByGroup: acc.Select(ugp).Columns("pid, from_gid, to_gid, two_way, level, posts, minTime").Where("from_gid=? OR to_gid=?").Prepare(), 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").Where("pid = ?").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(), delete: acc.Delete(ugp).Where("pid=?").Prepare(),
create: acc.Insert(ugp).Columns("from_gid, to_gid, two_way, level, posts, minTime").Fields("?,?,?,?,?,?").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(), //err := s.getByUser.QueryRow(u.Group, level, posts, mins).Scan(&g.ID, &g.To, &g.TwoWay, &g.Level, &g.Posts, &g.MinTime, &g.RegisteredFor)
updateUser: acc.Update("users").Set("group = ?").Where("level >= ? AND posts >= ?").Prepare(), //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(),
}, acc.FirstError() 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) { 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() { for rows.Next() {
g := &GroupPromotion{} 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 { if err != nil {
return nil, err return nil, err
} }
@ -76,22 +91,31 @@ func (s *DefaultGroupPromotionStore) Get(id int) (*GroupPromotion, error) {
}*/ }*/
g := &GroupPromotion{ID: id} 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 { if err == nil {
//s.cache.Set(u) //s.cache.Set(u)
} }
return g, err 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} 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 { if err == sql.ErrNoRows {
//log.Print("no matches found")
return nil return nil
} else if err != nil { } else if err != nil {
return err 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 return err
} }
@ -100,8 +124,8 @@ func (s *DefaultGroupPromotionStore) Delete(id int) error {
return err return err
} }
func (s *DefaultGroupPromotionStore) Create(from int, to int, twoWay bool, level int, posts int) (int, error) { 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) res, err := s.create.Exec(from, to, twoWay, level, posts, 0, registeredFor)
if err != nil { if err != nil {
return 0, err return 0, err
} }

View File

@ -93,14 +93,14 @@ var Template_account_handle = genIntTmpl("account")
func tmplInitUsers() (User, User, User) { func tmplInitUsers() (User, User, User) {
avatar, microAvatar := BuildAvatar(62, "") 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? // TODO: Do a more accurate level calculation for this?
avatar, microAvatar = BuildAvatar(1, "") 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, "") 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 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? // 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 // 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") var ErrNoTempGroup = errors.New("We couldn't find a temporary group for this user")
type User struct { type User struct {
@ -56,6 +56,7 @@ type User struct {
Score int Score int
Posts int Posts int
Liked int Liked int
CreatedAt time.Time
LastIP string // ! This part of the UserCache data might fall out of date LastIP string // ! This part of the UserCache data might fall out of date
LastAgent string // ! Temporary hack, don't use LastAgent string // ! Temporary hack, don't use
TempGroup int TempGroup int
@ -592,7 +593,7 @@ func (u *User) IncreasePostStats(wcount int, topic bool) (err error) {
if err != nil { if err != nil {
return err return err
} }
err = GroupPromotions.PromoteIfEligible(u, level, u.Posts+1) err = GroupPromotions.PromoteIfEligible(u, level, u.Posts+1, u.CreatedAt)
u.CacheRemove() u.CacheRemove()
return err return err
} }

View File

@ -39,7 +39,7 @@ type DefaultUserStore struct {
get *sql.Stmt get *sql.Stmt
getByName *sql.Stmt getByName *sql.Stmt
getOffset *sql.Stmt getOffset *sql.Stmt
getAll *sql.Stmt getAll *sql.Stmt
exists *sql.Stmt exists *sql.Stmt
register *sql.Stmt register *sql.Stmt
nameExists *sql.Stmt nameExists *sql.Stmt
@ -53,13 +53,14 @@ func NewDefaultUserStore(cache UserCache) (*DefaultUserStore, error) {
cache = NewNullUserCache() cache = NewNullUserCache()
} }
u := "users" 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? // TODO: Add an admin version of registerStmt with more flexibility?
return &DefaultUserStore{ return &DefaultUserStore{
cache: cache, 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(), 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("uid, name, group, active, is_super_admin, session, email, avatar, message, level, score, posts, liked, last_ip, temp_group, enable_embeds").Where("name = ?").Prepare(), getByName: acc.Select(u).Columns(allCols).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(), getOffset: acc.Select(u).Columns(allCols).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(), getAll: acc.Select(u).Columns(allCols).Prepare(),
exists: acc.Exists(u, "uid").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 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(), nameExists: acc.Exists(u, "name").Prepare(),
@ -91,7 +92,7 @@ func (s *DefaultUserStore) Get(id int) (*User, error) {
u = &User{ID: id, Loggedin: true} u = &User{ID: id, Loggedin: true}
var embeds int 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 err == nil {
if embeds != -1 { if embeds != -1 {
u.ParseSettings = DefaultParseSettings.CopyPtr() u.ParseSettings = DefaultParseSettings.CopyPtr()
@ -108,7 +109,7 @@ func (s *DefaultUserStore) Get(id int) (*User, error) {
func (s *DefaultUserStore) GetByName(name string) (*User, error) { func (s *DefaultUserStore) GetByName(name string) (*User, error) {
u := &User{Loggedin: true} u := &User{Loggedin: true}
var embeds int 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 err == nil {
if embeds != -1 { if embeds != -1 {
u.ParseSettings = DefaultParseSettings.CopyPtr() u.ParseSettings = DefaultParseSettings.CopyPtr()
@ -132,7 +133,7 @@ func (s *DefaultUserStore) GetOffset(offset, perPage int) (users []*User, err er
var embeds int var embeds int
for rows.Next() { for rows.Next() {
u := &User{Loggedin: true} 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 { if err != nil {
return nil, err return nil, err
} }
@ -155,7 +156,7 @@ func (s *DefaultUserStore) Each(f func(*User) error) error {
var embeds int var embeds int
for rows.Next() { for rows.Next() {
u := new(User) 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 return err
} }
if embeds != -1 { 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] 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 { if err != nil {
return list, err return list, err
} }
@ -222,7 +223,7 @@ func (s *DefaultUserStore) BulkGetMap(ids []int) (list map[int]*User, err error)
var embeds int var embeds int
for rows.Next() { for rows.Next() {
u := &User{Loggedin: true} 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 { if err != nil {
return list, err 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) { func (s *DefaultUserStore) BypassGet(id int) (*User, error) {
u := &User{ID: id, Loggedin: true} u := &User{ID: id, Loggedin: true}
var embeds int 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 err == nil {
if embeds != -1 { if embeds != -1 {
u.ParseSettings = DefaultParseSettings.CopyPtr() 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}) 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 // 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_two_way":"Two Way",
"panel_group_promotions_level":"Level", "panel_group_promotions_level":"Level",
"panel_group_promotions_posts":"Posts", "panel_group_promotions_posts":"Posts",
"panel_group_promotion_registered_for":"Registered For",
"panel_group_promotions_create_button":"Add Promotion", "panel_group_promotions_create_button":"Add Promotion",
"panel_word_filters_head":"Word Filters", "panel_word_filters_head":"Word Filters",

View File

@ -499,7 +499,7 @@ func topicStoreTest(t *testing.T, newID int, ip string) {
return "" 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) topic, err = c.Topics.Get(tid)
recordMustExist(t, err, fmt.Sprintf("Couldn't find TID #%d", 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)) 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() 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) fp, err := c.FPStore.Get(fid, gid)
expectNilErr(t, err) expectNilErr(t, err)
vt := fp.ViewTopic vt := fp.ViewTopic
@ -900,6 +900,64 @@ func TestGroupStore(t *testing.T) {
// TODO: Test group cache set // 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) { func TestReplyStore(t *testing.T) {
miscinit(t) miscinit(t)
if !c.PluginsInited { if !c.PluginsInited {
@ -1073,21 +1131,21 @@ func TestActivityStream(t *testing.T) {
func TestLogs(t *testing.T) { func TestLogs(t *testing.T) {
miscinit(t) miscinit(t)
gTests := func(store c.LogStore, phrase string) { gTests := func(s c.LogStore, phrase string) {
expect(t, store.Count() == 0, "There shouldn't be any "+phrase) expect(t, s.Count() == 0, "There shouldn't be any "+phrase)
logs, err := store.GetOffset(0, 25) logs, err := s.GetOffset(0, 25)
expectNilErr(t, err) expectNilErr(t, err)
expect(t, len(logs) == 0, "The log slice should be empty") expect(t, len(logs) == 0, "The log slice should be empty")
} }
gTests(c.ModLogs, "modlogs") gTests(c.ModLogs, "modlogs")
gTests(c.AdminLogs, "adminlogs") gTests(c.AdminLogs, "adminlogs")
gTests2 := func(store c.LogStore, phrase string) { gTests2 := func(s c.LogStore, phrase string) {
err := store.Create("something", 0, "bumblefly", "::1", 1) err := s.Create("something", 0, "bumblefly", "::1", 1)
expectNilErr(t, err) expectNilErr(t, err)
count := store.Count() count := s.Count()
expect(t, count == 1, fmt.Sprintf("store.Count should return one, not %d", 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) recordMustExist(t, err, "We should have at-least one "+phrase)
expect(t, len(logs) == 1, "The length of the log slice should be one") 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"] _, ok := c.Plugins["fairy-dust"]
expect(t, !ok, "Plugin fairy-dust shouldn't exist") 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, ok, "Plugin bbcode should exist")
expect(t, !plugin.Installable, "Plugin bbcode shouldn't be installable") expect(t, !pl.Installable, "Plugin bbcode shouldn't be installable")
expect(t, !plugin.Installed, "Plugin bbcode shouldn't be 'installed'") expect(t, !pl.Installed, "Plugin bbcode shouldn't be 'installed'")
expect(t, !plugin.Active, "Plugin bbcode shouldn't be active") expect(t, !pl.Active, "Plugin bbcode shouldn't be active")
active, err := plugin.BypassActive() active, err := pl.BypassActive()
expectNilErr(t, err) expectNilErr(t, err)
expect(t, !active, "Plugin bbcode shouldn't be active in the database either") expect(t, !active, "Plugin bbcode shouldn't be active in the database either")
hasPlugin, err := plugin.InDatabase() hasPlugin, err := pl.InDatabase()
expectNilErr(t, err) expectNilErr(t, err)
expect(t, !hasPlugin, "Plugin bbcode shouldn't exist in the database") expect(t, !hasPlugin, "Plugin bbcode shouldn't exist in the database")
// TODO: Add some test cases for SetActive and SetInstalled before calling AddToDatabase // TODO: Add some test cases for SetActive and SetInstalled before calling AddToDatabase
expectNilErr(t, plugin.AddToDatabase(true, false)) expectNilErr(t, pl.AddToDatabase(true, false))
expect(t, !plugin.Installable, "Plugin bbcode shouldn't be installable") expect(t, !pl.Installable, "Plugin bbcode shouldn't be installable")
expect(t, !plugin.Installed, "Plugin bbcode shouldn't be 'installed'") expect(t, !pl.Installed, "Plugin bbcode shouldn't be 'installed'")
expect(t, plugin.Active, "Plugin bbcode should be active") expect(t, pl.Active, "Plugin bbcode should be active")
active, err = plugin.BypassActive() active, err = pl.BypassActive()
expectNilErr(t, err) expectNilErr(t, err)
expect(t, active, "Plugin bbcode should be active in the database too") expect(t, active, "Plugin bbcode should be active in the database too")
hasPlugin, err = plugin.InDatabase() hasPlugin, err = pl.InDatabase()
expectNilErr(t, err) expectNilErr(t, err)
expect(t, hasPlugin, "Plugin bbcode should exist in the database") expect(t, hasPlugin, "Plugin bbcode should exist in the database")
expect(t, plugin.Init != nil, "Plugin bbcode should have an init function") expect(t, pl.Init != nil, "Plugin bbcode should have an init function")
expectNilErr(t, plugin.Init(plugin)) expectNilErr(t, pl.Init(pl))
expectNilErr(t, plugin.SetActive(true)) expectNilErr(t, pl.SetActive(true))
expect(t, !plugin.Installable, "Plugin bbcode shouldn't be installable") expect(t, !pl.Installable, "Plugin bbcode shouldn't be installable")
expect(t, !plugin.Installed, "Plugin bbcode shouldn't be 'installed'") expect(t, !pl.Installed, "Plugin bbcode shouldn't be 'installed'")
expect(t, plugin.Active, "Plugin bbcode should still be active") expect(t, pl.Active, "Plugin bbcode should still be active")
active, err = plugin.BypassActive() active, err = pl.BypassActive()
expectNilErr(t, err) expectNilErr(t, err)
expect(t, active, "Plugin bbcode should still be active in the database too") expect(t, active, "Plugin bbcode should still be active in the database too")
hasPlugin, err = plugin.InDatabase() hasPlugin, err = pl.InDatabase()
expectNilErr(t, err) expectNilErr(t, err)
expect(t, hasPlugin, "Plugin bbcode should still exist in the database") expect(t, hasPlugin, "Plugin bbcode should still exist in the database")
expectNilErr(t, plugin.SetActive(false)) expectNilErr(t, pl.SetActive(false))
expect(t, !plugin.Installable, "Plugin bbcode shouldn't be installable") expect(t, !pl.Installable, "Plugin bbcode shouldn't be installable")
expect(t, !plugin.Installed, "Plugin bbcode shouldn't be 'installed'") expect(t, !pl.Installed, "Plugin bbcode shouldn't be 'installed'")
expect(t, !plugin.Active, "Plugin bbcode shouldn't be active") expect(t, !pl.Active, "Plugin bbcode shouldn't be active")
active, err = plugin.BypassActive() active, err = pl.BypassActive()
expectNilErr(t, err) expectNilErr(t, err)
expect(t, !active, "Plugin bbcode shouldn't be active in the database") expect(t, !active, "Plugin bbcode shouldn't be active in the database")
hasPlugin, err = plugin.InDatabase() hasPlugin, err = pl.InDatabase()
expectNilErr(t, err) expectNilErr(t, err)
expect(t, hasPlugin, "Plugin bbcode should still exist in the database") expect(t, hasPlugin, "Plugin bbcode should still exist in the database")
expect(t, plugin.Deactivate != nil, "Plugin bbcode should have an init function") expect(t, pl.Deactivate != nil, "Plugin bbcode should have an init function")
plugin.Deactivate(plugin) // Returns nothing pl.Deactivate(pl) // Returns nothing
// Not installable, should not be mutated // Not installable, should not be mutated
expect(t, plugin.SetInstalled(true) == c.ErrPluginNotInstallable, "Plugin was set as installed despite not being installable") expect(t, pl.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, !pl.Installable, "Plugin bbcode shouldn't be installable")
expect(t, !plugin.Installed, "Plugin bbcode shouldn't be 'installed'") expect(t, !pl.Installed, "Plugin bbcode shouldn't be 'installed'")
expect(t, !plugin.Active, "Plugin bbcode shouldn't be active") expect(t, !pl.Active, "Plugin bbcode shouldn't be active")
active, err = plugin.BypassActive() active, err = pl.BypassActive()
expectNilErr(t, err) expectNilErr(t, err)
expect(t, !active, "Plugin bbcode shouldn't be active in the database either") expect(t, !active, "Plugin bbcode shouldn't be active in the database either")
hasPlugin, err = plugin.InDatabase() hasPlugin, err = pl.InDatabase()
expectNilErr(t, err) expectNilErr(t, err)
expect(t, hasPlugin, "Plugin bbcode should still exist in the database") 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, pl.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, !pl.Installable, "Plugin bbcode shouldn't be installable")
expect(t, !plugin.Installed, "Plugin bbcode shouldn't be 'installed'") expect(t, !pl.Installed, "Plugin bbcode shouldn't be 'installed'")
expect(t, !plugin.Active, "Plugin bbcode shouldn't be active") expect(t, !pl.Active, "Plugin bbcode shouldn't be active")
active, err = plugin.BypassActive() active, err = pl.BypassActive()
expectNilErr(t, err) expectNilErr(t, err)
expect(t, !active, "Plugin bbcode shouldn't be active in the database either") expect(t, !active, "Plugin bbcode shouldn't be active in the database either")
hasPlugin, err = plugin.InDatabase() hasPlugin, err = pl.InDatabase()
expectNilErr(t, err) expectNilErr(t, err)
expect(t, hasPlugin, "Plugin bbcode should still exist in the database") 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 // This isn't really installable, but we want to get a few tests done before getting plugins which are stateful
plugin.Installable = true pl.Installable = true
expectNilErr(t, plugin.SetInstalled(true)) expectNilErr(t, pl.SetInstalled(true))
expect(t, plugin.Installable, "Plugin bbcode should be installable") expect(t, pl.Installable, "Plugin bbcode should be installable")
expect(t, plugin.Installed, "Plugin bbcode should be 'installed'") expect(t, pl.Installed, "Plugin bbcode should be 'installed'")
expect(t, !plugin.Active, "Plugin bbcode shouldn't be active") expect(t, !pl.Active, "Plugin bbcode shouldn't be active")
active, err = plugin.BypassActive() active, err = pl.BypassActive()
expectNilErr(t, err) expectNilErr(t, err)
expect(t, !active, "Plugin bbcode shouldn't be active in the database either") expect(t, !active, "Plugin bbcode shouldn't be active in the database either")
hasPlugin, err = plugin.InDatabase() hasPlugin, err = pl.InDatabase()
expectNilErr(t, err) expectNilErr(t, err)
expect(t, hasPlugin, "Plugin bbcode should still exist in the database") expect(t, hasPlugin, "Plugin bbcode should still exist in the database")
expectNilErr(t, plugin.SetInstalled(false)) expectNilErr(t, pl.SetInstalled(false))
expect(t, plugin.Installable, "Plugin bbcode should be installable") expect(t, pl.Installable, "Plugin bbcode should be installable")
expect(t, !plugin.Installed, "Plugin bbcode shouldn't be 'installed'") expect(t, !pl.Installed, "Plugin bbcode shouldn't be 'installed'")
expect(t, !plugin.Active, "Plugin bbcode shouldn't be active") expect(t, !pl.Active, "Plugin bbcode shouldn't be active")
active, err = plugin.BypassActive() active, err = pl.BypassActive()
expectNilErr(t, err) expectNilErr(t, err)
expect(t, !active, "Plugin bbcode shouldn't be active in the database either") expect(t, !active, "Plugin bbcode shouldn't be active in the database either")
hasPlugin, err = plugin.InDatabase() hasPlugin, err = pl.InDatabase()
expectNilErr(t, err) expectNilErr(t, err)
expect(t, hasPlugin, "Plugin bbcode should still exist in the database") 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 // 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 // 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, ok, "Plugin markdown should exist")
expect(t, !plugin2.Installable, "Plugin markdown shouldn't be installable") expect(t, !pl2.Installable, "Plugin markdown shouldn't be installable")
expect(t, !plugin2.Installed, "Plugin markdown shouldn't be 'installed'") expect(t, !pl2.Installed, "Plugin markdown shouldn't be 'installed'")
expect(t, !plugin2.Active, "Plugin markdown shouldn't be active") expect(t, !pl2.Active, "Plugin markdown shouldn't be active")
active, err = plugin2.BypassActive() active, err = pl2.BypassActive()
expectNilErr(t, err) expectNilErr(t, err)
expect(t, !active, "Plugin markdown shouldn't be active in the database either") expect(t, !active, "Plugin markdown shouldn't be active in the database either")
hasPlugin, err = plugin2.InDatabase() hasPlugin, err = pl2.InDatabase()
expectNilErr(t, err) expectNilErr(t, err)
expect(t, !hasPlugin, "Plugin markdown shouldn't exist in the database") expect(t, !hasPlugin, "Plugin markdown shouldn't exist in the database")
expectNilErr(t, plugin2.AddToDatabase(true, false)) expectNilErr(t, pl2.AddToDatabase(true, false))
expectNilErr(t, plugin2.Init(plugin2)) expectNilErr(t, pl2.Init(pl2))
expectNilErr(t, plugin.SetActive(true)) expectNilErr(t, pl.SetActive(true))
expectNilErr(t, plugin.Init(plugin)) expectNilErr(t, pl.Init(pl))
plugin2.Deactivate(plugin2) pl2.Deactivate(pl2)
expectNilErr(t, plugin2.SetActive(false)) expectNilErr(t, pl2.SetActive(false))
plugin.Deactivate(plugin) pl.Deactivate(pl)
expectNilErr(t, plugin.SetActive(false)) expectNilErr(t, pl.SetActive(false))
// Hook tests // Hook tests
ht := func() *c.HookTable { ht := func() *c.HookTable {
@ -1241,18 +1299,18 @@ func TestPluginManager(t *testing.T) {
handle := func(in string) (out string) { handle := func(in string) (out string) {
return in + "hi" return in + "hi"
} }
plugin.AddHook("haha", handle) pl.AddHook("haha", handle)
expect(t, ht().Sshook("haha", "ho") == "hohi", "Sshook didn't give hohi") 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().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") expect(t, ht().Hook("haha", "ho") == "ho", "Hook shouldn't have anything bound to it yet")
handle2 := func(inI interface{}) (out interface{}) { handle2 := func(inI interface{}) (out interface{}) {
return inI.(string) + "hi" 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") 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") expect(t, ht().Hook("hehe", "ho").(string) == "ho", "Hook shouldn't have anything bound to it anymore")
// TODO: Add tests for more hook types // TODO: Add tests for more hook types
@ -1260,7 +1318,7 @@ func TestPluginManager(t *testing.T) {
func TestPhrases(t *testing.T) { func TestPhrases(t *testing.T) {
getPhrase := phrases.GetPermPhrase getPhrase := phrases.GetPermPhrase
tp := func(name string, expects string) { tp := func(name, expects string) {
res := getPhrase(name) res := getPhrase(name)
expect(t, res == expects, "Not the expected phrase, got '"+res+"' instead") 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 // 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 { if len(hashedPassword) < 10 {
t.Error("Hash too short") t.Error("Hash too short")
} }
@ -1638,7 +1696,7 @@ type CountTestList struct {
Items []CountTest 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}) l.Items = append(l.Items, CountTest{name, msg, expects})
} }

View File

@ -47,6 +47,7 @@ func init() {
addPatch(27, patch27) addPatch(27, patch27)
addPatch(28, patch28) addPatch(28, patch28)
addPatch(29, patch29) addPatch(29, patch29)
addPatch(30, patch30)
} }
func patch0(scanner *bufio.Scanner) (err error) { func patch0(scanner *bufio.Scanner) (err error) {
@ -833,10 +834,6 @@ func patch29(scanner *bufio.Scanner) error {
if err != nil { if err != nil {
return err 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")) err = execStmt(qgen.Builder.SetDefaultColumn("replies", "lastEdit", "int", "0"))
if err != nil { 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)) 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: Stop duplicating this code
// TODO: Use a transaction here // TODO: Use a transaction here
// TODO: Move this function to neutral ground // 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) pathMap = make(map[string]string)
files, rerr := uploadFilesWithHash(w, r, user, "./attachs/") files, rerr := uploadFilesWithHash(w, r, user, "./attachs/")
if rerr != nil { if rerr != nil {

View File

@ -7,8 +7,8 @@ import (
"github.com/Azareal/Gosora/common/phrases" "github.com/Azareal/Gosora/common/phrases"
) )
func IPSearch(w http.ResponseWriter, r *http.Request, user c.User, header *c.Header) c.RouteError { func IPSearch(w http.ResponseWriter, r *http.Request, user c.User, h *c.Header) c.RouteError {
header.Title = phrases.GetTitlePhrase("ip_search") h.Title = phrases.GetTitlePhrase("ip_search")
// TODO: How should we handle the permissions if we extend this into an alt detector of sorts? // TODO: How should we handle the permissions if we extend this into an alt detector of sorts?
if !user.Perms.ViewIPs { if !user.Perms.ViewIPs {
return c.NoPermissions(w, r, user) 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 { if err != nil {
return c.InternalError(err, w, r) 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 { if count == perPage {
break break
} }
var rank string var rank, rankClass string
var rankClass string
canDelete := false canDelete := false
// TODO: Localise this // 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) 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) g, err := c.Groups.Get(from)
ferr := groupCheck(w, r, user, g, err) ferr := groupCheck(w, r, user, g, err)
if err != nil { if err != nil {
@ -225,7 +238,7 @@ func GroupsPromotionsCreateSubmit(w http.ResponseWriter, r *http.Request, user c
if err != nil { if err != nil {
return ferr 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 { if err != nil {
return c.InternalError(err, w, r) 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) return c.LocalError("Only administrators can edit the account of other administrators.", w, r, user)
} }
newname := c.SanitiseSingleLine(r.PostFormValue("name")) newName := c.SanitiseSingleLine(r.PostFormValue("name"))
if newname == "" { if newName == "" {
return c.LocalError("You didn't put in a name.", w, r, user) 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 activation factor into admin set emails?
// TODO: How should we handle secondary emails? Do we even have secondary emails implemented? // TODO: How should we handle secondary emails? Do we even have secondary emails implemented?
newemail := c.SanitiseSingleLine(r.PostFormValue("email")) newEmail := c.SanitiseSingleLine(r.PostFormValue("email"))
if newemail == "" && targetUser.Email != "" { if newEmail == "" && targetUser.Email != "" {
return c.LocalError("You didn't put in an email address.", w, r, user) return c.LocalError("You didn't put in an email address.", w, r, user)
} }
if newemail == "-1" { if newEmail == "-1" {
newemail = targetUser.Email 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) return c.LocalError("You need the EditUserEmail permission to edit the email address of a user.", w, r, user)
} }
newpassword := r.PostFormValue("password") newPassword := r.PostFormValue("password")
if newpassword != "" && !user.Perms.EditUserPassword { if newPassword != "" && !user.Perms.EditUserPassword {
return c.LocalError("You need the EditUserPassword permission to edit the password of a user.", w, r, user) 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 { if err != nil {
return c.LocalError("You need to provide a whole number for the group ID", w, r, user) 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 { if err == sql.ErrNoRows {
return c.LocalError("The group you're trying to place this user in doesn't exist.", w, r, user) return c.LocalError("The group you're trying to place this user in doesn't exist.", w, r, user)
} else if err != nil { } 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) 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 { if err != nil {
return c.InternalError(err, w, r) return c.InternalError(err, w, r)
} }
red := false red := false
if newpassword != "" { if newPassword != "" {
c.SetPassword(targetUser.ID, newpassword) c.SetPassword(targetUser.ID, newPassword)
// Log the user out as a safety precaution // Log the user out as a safety precaution
c.Auth.ForceLogout(targetUser.ID) c.Auth.ForceLogout(targetUser.ID)
red = true red = true
} }
targetUser.CacheRemove() 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) err = c.AdminLogs.Create("edit", targetUser.ID, "user", user.GetIP(), user.ID)
if err != nil { if err != nil {
return c.InternalError(err, w, r) 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 { if err != nil {
return c.PreError("The provided PollID is not a valid number.", w, r) return c.PreError("The provided PollID is not a valid number.", w, r)
} }
poll, err := c.Polls.Get(pollID) poll, err := c.Polls.Get(pollID)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return c.PreError("The poll you tried to vote for doesn't exist.", w, r) 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 { if err != nil {
return c.PreError("The provided PollID is not a valid number.", w, r) return c.PreError("The provided PollID is not a valid number.", w, r)
} }
poll, err := c.Polls.Get(pollID) poll, err := c.Polls.Get(pollID)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return c.PreError("The poll you tried to vote for doesn't exist.", w, r) 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 // 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 { if err != nil {
return c.InternalError(err, w, r) return c.InternalError(err, w, r)
} }

View File

@ -9,14 +9,14 @@ import (
"github.com/Azareal/Gosora/common/counters" "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) headerLite, ferr := c.SimpleUserCheck(w, r, &user)
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
js := r.PostFormValue("js") == "1" js := r.PostFormValue("js") == "1"
itemID, err := strconv.Atoi(sitemID) itemID, err := strconv.Atoi(sItemID)
if err != nil { if err != nil {
return c.LocalError("Bad ID", w, r, user) return c.LocalError("Bad ID", w, r, user)
} }

View File

@ -39,9 +39,9 @@ var topicStmts TopicStmts
func init() { func init() {
c.DbInits.Add(func(acc *qgen.Accumulator) error { c.DbInits.Add(func(acc *qgen.Accumulator) error {
topicStmts = TopicStmts{ 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 // 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() 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 { if topic.ContentHTML == topic.Content {
topic.ContentHTML = topic.Content topic.ContentHTML = topic.Content
} }
topic.Tag = postGroup.Tag topic.Tag = postGroup.Tag
if postGroup.IsMod { if postGroup.IsMod {
topic.ClassName = c.Config.StaffCSS 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? // Do a bulk forum fetch, just in case it's the SqlForumStore?
forum := c.Forums.DirtyGet(ffid) f := c.Forums.DirtyGet(ffid)
if forum.Name != "" && forum.Active { if f.Name != "" && f.Active {
fcopy := forum.Copy() fcopy := f.Copy()
// TODO: Abstract this // TODO: Abstract this
if header.Hooks.HookSkippable("topic_create_frow_assign", &fcopy) { if header.Hooks.HookSkippable("topic_create_frow_assign", &fcopy) {
continue 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) 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) tid, err := strconv.Atoi(stid)
if err != nil { if err != nil {
return nil, nil, c.PreError(phrases.GetErrorPhrase("id_must_be_integer"), w, r) 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 // TODO: Implement search
func TopicListCommon(w http.ResponseWriter, r *http.Request, user c.User, header *c.Header, torder string, tsorder string) c.RouteError { func TopicListCommon(w http.ResponseWriter, r *http.Request, user c.User, h *c.Header, torder, tsorder string) c.RouteError {
header.Title = phrases.GetTitlePhrase("topics") h.Title = phrases.GetTitlePhrase("topics")
header.Zone = "topics" h.Zone = "topics"
header.Path = "/topics/" h.Path = "/topics/"
header.MetaDesc = header.Settings["meta_desc"].(string) h.MetaDesc = h.Settings["meta_desc"].(string)
group, err := c.Groups.Get(user.Group) group, err := c.Groups.Get(user.Group)
if err != nil { if err != nil {
@ -61,8 +61,8 @@ func TopicListCommon(w http.ResponseWriter, r *http.Request, user c.User, header
if err != nil { if err != nil {
return c.LocalError("Invalid fid forum", w, r, user) return c.LocalError("Invalid fid forum", w, r, user)
} }
header.Title = forum.Name h.Title = forum.Name
header.ZoneID = forum.ID h.ZoneID = forum.ID
} }
} }
@ -95,8 +95,8 @@ func TopicListCommon(w http.ResponseWriter, r *http.Request, user c.User, header
} }
for _, fid := range fids { for _, fid := range fids {
if inSlice(canSee, fid) { if inSlice(canSee, fid) {
forum := c.Forums.DirtyGet(fid) f := c.Forums.DirtyGet(fid)
if forum.Name != "" && forum.Active && (forum.ParentType == "" || forum.ParentType == "forum") { if f.Name != "" && f.Active && (f.ParentType == "" || f.ParentType == "forum") {
// TODO: Add a hook here for plugin_guilds? // TODO: Add a hook here for plugin_guilds?
cfids = append(cfids, fid) 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) return c.InternalError(err, w, r)
} }
reqUserList := make(map[int]bool) reqUserList := make(map[int]bool)
for _, topic := range tMap { for _, t := range tMap {
reqUserList[topic.CreatedBy] = true reqUserList[t.CreatedBy] = true
reqUserList[topic.LastReplyBy] = true reqUserList[t.LastReplyBy] = true
topicList = append(topicList, topic.TopicsRow()) topicList = append(topicList, t.TopicsRow())
} }
//fmt.Printf("reqUserList %+v\n", reqUserList) //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? // TODO: De-dupe this logic in common/topic_list.go?
for _, topic := range topicList { for _, t := range topicList {
topic.Link = c.BuildTopicURL(c.NameToSlug(topic.Title), topic.ID) 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. // 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) forum := c.Forums.DirtyGet(t.ParentID)
topic.ForumName = forum.Name t.ForumName = forum.Name
topic.ForumLink = forum.Link t.ForumLink = forum.Link
// TODO: Create a specialised function with a bit less overhead for getting the last page for a post count // TODO: Create a specialised function with a bit less overhead for getting the last page for a post count
_, _, lastPage := c.PageOffset(topic.PostCount, 1, c.Config.ItemsPerPage) _, _, lastPage := c.PageOffset(t.PostCount, 1, c.Config.ItemsPerPage)
topic.LastPage = lastPage t.LastPage = lastPage
topic.Creator = userList[topic.CreatedBy] t.Creator = userList[t.CreatedBy]
topic.LastUser = userList[topic.LastReplyBy] t.LastUser = userList[t.LastReplyBy]
} }
// TODO: Reduce the amount of boilerplate here // 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 return nil
} }
header.Title = phrases.GetTitlePhrase("topics_search") h.Title = phrases.GetTitlePhrase("topics_search")
pi := c.TopicListPage{header, topicList, forumList, c.Config.DefaultForum, c.TopicListSort{torder, false}, paginator} pi := c.TopicListPage{h, topicList, forumList, c.Config.DefaultForum, c.TopicListSort{torder, false}, paginator}
return renderTemplate("topics", w, r, header, pi) return renderTemplate("topics", w, r, h, pi)
} }
// TODO: Pass a struct back rather than passing back so many variables // 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 return nil
} }
pi := c.TopicListPage{header, topicList, forumList, c.Config.DefaultForum, c.TopicListSort{torder, false}, paginator} pi := c.TopicListPage{h, topicList, forumList, c.Config.DefaultForum, c.TopicListSort{torder, false}, paginator}
return renderTemplate("topics", w, r, header, pi) 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) 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) err = c.ModLogs.Create("activate", targetUser.ID, "user", user.GetIP(), user.ID)
if err != nil { if err != nil {
return c.InternalError(err, w, r) return c.InternalError(err, w, r)

View File

@ -9,7 +9,7 @@ CREATE TABLE [users] (
[createdAt] datetime not null, [createdAt] datetime not null,
[lastActiveAt] datetime not null, [lastActiveAt] datetime not null,
[session] nvarchar (200) DEFAULT '' 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, [enable_embeds] int DEFAULT -1 not null,
[email] nvarchar (200) DEFAULT '' not null, [email] nvarchar (200) DEFAULT '' not null,
[avatar] nvarchar (100) DEFAULT '' not null, [avatar] nvarchar (100) DEFAULT '' not null,

View File

@ -6,5 +6,6 @@ CREATE TABLE [users_groups_promotions] (
[level] int not null, [level] int not null,
[posts] int DEFAULT 0 not null, [posts] int DEFAULT 0 not null,
[minTime] int not null, [minTime] int not null,
[registeredFor] int DEFAULT 0 not null,
primary key([pid]) primary key([pid])
); );

View File

@ -9,7 +9,7 @@ CREATE TABLE `users` (
`createdAt` datetime not null, `createdAt` datetime not null,
`lastActiveAt` datetime not null, `lastActiveAt` datetime not null,
`session` varchar(200) DEFAULT '' 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, `enable_embeds` int DEFAULT -1 not null,
`email` varchar(200) DEFAULT '' not null, `email` varchar(200) DEFAULT '' not null,
`avatar` varchar(100) DEFAULT '' not null, `avatar` varchar(100) DEFAULT '' not null,

View File

@ -6,5 +6,6 @@ CREATE TABLE `users_groups_promotions` (
`level` int not null, `level` int not null,
`posts` int DEFAULT 0 not null, `posts` int DEFAULT 0 not null,
`minTime` int not null, `minTime` int not null,
`registeredFor` int DEFAULT 0 not null,
primary key(`pid`) primary key(`pid`)
) CHARSET=utf8mb4 COLLATE utf8mb4_general_ci; ) CHARSET=utf8mb4 COLLATE utf8mb4_general_ci;

View File

@ -9,7 +9,7 @@ CREATE TABLE "users" (
`createdAt` timestamp not null, `createdAt` timestamp not null,
`lastActiveAt` timestamp not null, `lastActiveAt` timestamp not null,
`session` varchar (200) DEFAULT '' 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, `enable_embeds` int DEFAULT -1 not null,
`email` varchar (200) DEFAULT '' not null, `email` varchar (200) DEFAULT '' not null,
`avatar` varchar (100) DEFAULT '' not null, `avatar` varchar (100) DEFAULT '' not null,

View File

@ -6,5 +6,6 @@ CREATE TABLE "users_groups_promotions" (
`level` int not null, `level` int not null,
`posts` int DEFAULT 0 not null, `posts` int DEFAULT 0 not null,
`minTime` int not null, `minTime` int not null,
`registeredFor` int DEFAULT 0 not null,
primary key(`pid`) primary key(`pid`)
); );

View File

@ -13,6 +13,7 @@
<a href="#p-{{.ID}}">{{.FromGroup.Name}} -> {{.ToGroup.Name}}{{if .TwoWay}} (two way){{end}}</a> <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 .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 .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"> <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> <a href="/panel/groups/promotions/delete/submit/{{$.ID}}-{{.ID}}?s={{$.CurrentUser.Session}}"><button form="nn">{{lang "panel_group_promotions_delete_button"}}</button></a>
</div> </div>
@ -56,11 +57,19 @@
</div> </div>
<div class="formrow"> <div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_group_promotions_level"}}</a></div> <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>
<div class="formrow"> <div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_group_promotions_posts"}}</a></div> <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>
<div class="formrow form_button_row"> <div class="formrow form_button_row">
<div class="formitem"><button name="panel-button" class="formbutton">{{lang "panel_group_promotions_create_button"}}</button></div> <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"> <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>
<div class="rowblock filter_list widget_filter"> <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> {{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>