Implement guestbook settings #20
| @ -147,7 +147,7 @@ func (app *application) putUserSettings(w http.ResponseWriter, r *http.Request) | |||||||
| 	} | 	} | ||||||
| 	form.CheckField(validator.PermittedValue(form.LocalTimezone, app.timezones...), "timezone", "Invalid value") | 	form.CheckField(validator.PermittedValue(form.LocalTimezone, app.timezones...), "timezone", "Invalid value") | ||||||
| 	if !form.Valid() { | 	if !form.Valid() { | ||||||
| 		// rerender template with errors | 		// TODO: rerender template with errors | ||||||
| 		app.clientError(w, http.StatusUnprocessableEntity) | 		app.clientError(w, http.StatusUnprocessableEntity) | ||||||
| 	} | 	} | ||||||
| 	err = app.users.SetLocalTimezone(userId, form.LocalTimezone) | 	err = app.users.SetLocalTimezone(userId, form.LocalTimezone) | ||||||
|  | |||||||
| @ -38,7 +38,7 @@ CREATE TABLE user_settings ( | |||||||
|     Id integer primary key autoincrement, |     Id integer primary key autoincrement, | ||||||
|     UserId integer NOT NULL, |     UserId integer NOT NULL, | ||||||
|     SettingId integer NOT NULL, |     SettingId integer NOT NULL, | ||||||
|     AllowedSettingValueId integer NOT NULL, |     AllowedSettingValueId integer, | ||||||
|     UnconstrainedValue varchar(256), |     UnconstrainedValue varchar(256), | ||||||
|     FOREIGN KEY (UserId) REFERENCES users(Id) |     FOREIGN KEY (UserId) REFERENCES users(Id) | ||||||
|         ON DELETE RESTRICT |         ON DELETE RESTRICT | ||||||
| @ -55,7 +55,7 @@ CREATE TABLE guestbook_settings ( | |||||||
|     Id integer primary key autoincrement, |     Id integer primary key autoincrement, | ||||||
|     GuestbookId integer NOT NULL, |     GuestbookId integer NOT NULL, | ||||||
|     SettingId integer NOT NULL, |     SettingId integer NOT NULL, | ||||||
|     AllowedSettingValueId integer NOT NULL, |     AllowedSettingValueId integer, | ||||||
|     UnconstrainedValue varchar(256), |     UnconstrainedValue varchar(256), | ||||||
|     FOREIGN KEY (GuestbookId) REFERENCES guestbooks(Id) |     FOREIGN KEY (GuestbookId) REFERENCES guestbooks(Id) | ||||||
|         ON DELETE RESTRICT |         ON DELETE RESTRICT | ||||||
|  | |||||||
| @ -8,4 +8,6 @@ var ( | |||||||
| 	ErrInvalidCredentials = errors.New("models: invalid credentials") | 	ErrInvalidCredentials = errors.New("models: invalid credentials") | ||||||
| 
 | 
 | ||||||
| 	ErrDuplicateEmail = errors.New("models: duplicate email") | 	ErrDuplicateEmail = errors.New("models: duplicate email") | ||||||
|  | 
 | ||||||
|  | 	ErrInvalidSettingValue = errors.New("models: invalid setting value") | ||||||
| ) | ) | ||||||
|  | |||||||
							
								
								
									
										202
									
								
								internal/models/settings.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								internal/models/settings.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,202 @@ | |||||||
|  | package models | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"database/sql" | ||||||
|  | 	"maps" | ||||||
|  | 	"strconv" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type SettingGroup int | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	SettingGroupUser      SettingGroup = 1 | ||||||
|  | 	SettingGroupGuestbook SettingGroup = 2 | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type SettingDataType int | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	SettingTypeInt      SettingDataType = 1 | ||||||
|  | 	SettingTypeDate     SettingDataType = 2 | ||||||
|  | 	SettingTypeAlphanum SettingDataType = 3 | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type SettingValue struct { | ||||||
|  | 	Id                 int | ||||||
|  | 	SettingId          int | ||||||
|  | 	AllowedSettingId   int | ||||||
|  | 	UnconstrainedValue string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type AllowedSettingValue struct { | ||||||
|  | 	id        int | ||||||
|  | 	itemValue string | ||||||
|  | 	caption   string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *AllowedSettingValue) Id() int { | ||||||
|  | 	return s.id | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *AllowedSettingValue) ItemValue() string { | ||||||
|  | 	return s.itemValue | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *AllowedSettingValue) Caption() string { | ||||||
|  | 	return s.caption | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type Setting struct { | ||||||
|  | 	id            int | ||||||
|  | 	description   string | ||||||
|  | 	constrained   bool | ||||||
|  | 	dataType      SettingDataType | ||||||
|  | 	settingGroup  SettingGroup | ||||||
|  | 	minValue      string // TODO: Maybe should be int? | ||||||
|  | 	maxValue      string | ||||||
|  | 	allowedValues map[int]AllowedSettingValue | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *Setting) Id() int { | ||||||
|  | 	return s.id | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *Setting) Description() string { | ||||||
|  | 	return s.description | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *Setting) Constrained() bool { | ||||||
|  | 	return s.constrained | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *Setting) DataType() SettingDataType { | ||||||
|  | 	return s.dataType | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *Setting) SettingGroup() SettingGroup { | ||||||
|  | 	return s.settingGroup | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *Setting) MinValue() string { | ||||||
|  | 	return s.minValue | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *Setting) MaxValue() string { | ||||||
|  | 	return s.maxValue | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *Setting) AllowedValues() map[int]AllowedSettingValue { | ||||||
|  | 	result := make(map[int]AllowedSettingValue, 50) | ||||||
|  | 	maps.Copy(result, s.allowedValues) | ||||||
|  | 	return result | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *Setting) ValidateUnconstrained(value string) bool { | ||||||
|  | 	switch s.dataType { | ||||||
|  | 	case SettingTypeInt: | ||||||
|  | 		return s.validateInt(value) | ||||||
|  | 	case SettingTypeAlphanum: | ||||||
|  | 		return s.validateAlphanum(value) | ||||||
|  | 	case SettingTypeDate: | ||||||
|  | 		return s.validateDatetime(value) | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *Setting) ValidateConstrained(value *AllowedSettingValue) bool { | ||||||
|  | 	_, exists := s.allowedValues[value.id] | ||||||
|  | 	if s.constrained && exists { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *Setting) validateInt(value string) bool { | ||||||
|  | 	v, err := strconv.ParseInt(value, 10, 0) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	var min int64 | ||||||
|  | 	var max int64 | ||||||
|  | 	if len(s.minValue) > 0 { | ||||||
|  | 		min, err = strconv.ParseInt(s.minValue, 10, 0) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 		if v < min { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if len(s.maxValue) > 0 { | ||||||
|  | 		max, err = strconv.ParseInt(s.maxValue, 10, 0) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 		if v < max { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *Setting) validateDatetime(value string) bool { | ||||||
|  | 	v, err := time.Parse(time.DateTime, value) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	var min time.Time | ||||||
|  | 	var max time.Time | ||||||
|  | 
 | ||||||
|  | 	if len(s.minValue) > 0 { | ||||||
|  | 		min, err = time.Parse(time.DateTime, s.minValue) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 		if v.Before(min) { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if len(s.maxValue) > 0 { | ||||||
|  | 		max, err = time.Parse(time.DateTime, s.maxValue) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 		if v.After(max) { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *Setting) validateAlphanum(value string) bool { | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func validateSetting(db *sql.DB, settingId int, value string) (bool, error) { | ||||||
|  | 	stmt := `SELECT s.Id, Description, Constrained, DataType, SettingGroup, MinValue, MaxValue, a.Id  | ||||||
|  | 	FROM settings AS s LEFT JOIN allowed_setting_values AS a ON s.Id = a.SettingId AND a.ItemValue = ? | ||||||
|  | 	WHERE s.Id = ?` | ||||||
|  | 	result := db.QueryRow(stmt, value, settingId) | ||||||
|  | 
 | ||||||
|  | 	var s Setting | ||||||
|  | 	var minval sql.NullString | ||||||
|  | 	var maxval sql.NullString | ||||||
|  | 	var allowedId sql.NullInt64 | ||||||
|  | 	err := result.Scan(&s.id, &s.description, &s.constrained, &s.dataType, &s.settingGroup, &minval, &maxval, &allowedId) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return false, err | ||||||
|  | 	} | ||||||
|  | 	if s.constrained && allowedId.Valid { | ||||||
|  | 		return true, nil | ||||||
|  | 	} | ||||||
|  | 	switch s.dataType { | ||||||
|  | 	case SettingTypeInt: | ||||||
|  | 		return s.validateInt(value), nil | ||||||
|  | 	case SettingTypeAlphanum: | ||||||
|  | 		return s.validateAlphanum(value), nil | ||||||
|  | 	case SettingTypeDate: | ||||||
|  | 		return s.validateDatetime(value), nil | ||||||
|  | 	} | ||||||
|  | 	return false, nil | ||||||
|  | } | ||||||
| @ -54,13 +54,7 @@ func (m *UserModel) Insert(shortId uint64, username string, email string, passwo | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	settingsStmt := `INSERT INTO user_settings  | 	err = m.initializeUserSettings(id, settings) | ||||||
| 		(UserId, SettingId, AllowedSettingValueId, UnconstrainedValue) |  | ||||||
| 		VALUES (?, ?, ?, ?)` |  | ||||||
| 	_, err = m.DB.Exec(settingsStmt, id, u_timezone, nil, settings.LocalTimezone.String()) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -184,9 +178,25 @@ func (m *UserModel) GetSettings(userId int64) (UserSettings, error) { | |||||||
| 	return settings, err | 	return settings, err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (m *UserModel) SetLocalTimezone(userId int64, timezone string) error { | func (m *UserModel) initializeUserSettings(userId int64, settings UserSettings) error { | ||||||
| 	stmt := `UPDATE user_settings SET UnconstrainedValue = ? WHERE UserId = ?` | 	stmt := `INSERT INTO user_settings (UserId, SettingId, AllowedSettingValueId, UnconstrainedValue) VALUES (?, ?, ?, ?)` | ||||||
| 	_, err := m.DB.Exec(stmt, timezone, userId) | 	_, err := m.DB.Exec(stmt, userId, u_timezone, nil, settings.LocalTimezone.String()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *UserModel) SetLocalTimezone(userId int64, timezone string) error { | ||||||
|  | 	valid, err := validateSetting(m.DB, u_timezone, timezone) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if !valid { | ||||||
|  | 		return ErrInvalidSettingValue | ||||||
|  | 	} | ||||||
|  | 	stmt := `UPDATE user_settings SET UnconstrainedValue = ? WHERE UserId = ?` | ||||||
|  | 	_, err = m.DB.Exec(stmt, timezone, userId) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user