First commit
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/gisle/stufflog/internal/generate"
|
||||
"github.com/gisle/stufflog/slerrors"
|
||||
)
|
||||
|
||||
// An Activity is a general hobby or group of related related sub-activities.
|
||||
type Activity struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"userId"`
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
DailyBonus int `json:"dailyBonus"`
|
||||
|
||||
SubActivities []SubActivity `json:"subActivities"`
|
||||
}
|
||||
|
||||
func (activity *Activity) GenerateIDs() {
|
||||
activity.ID = generate.ID("A", 12)
|
||||
for i := range activity.SubActivities {
|
||||
activity.SubActivities[i].ID = generate.ID("AS", 16)
|
||||
}
|
||||
}
|
||||
|
||||
func (activity *Activity) SubActivity(id string) *SubActivity {
|
||||
for i := range activity.SubActivities {
|
||||
if activity.SubActivities[i].ID == id {
|
||||
return &activity.SubActivities[i]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (activity *Activity) ApplyUpdate(update ActivityUpdate) (changed bool, err error) {
|
||||
if update.SetName != nil {
|
||||
activity.Name = *update.SetName
|
||||
changed = true
|
||||
}
|
||||
if update.SetIcon != nil {
|
||||
activity.Icon = *update.SetIcon
|
||||
changed = true
|
||||
}
|
||||
if update.SetDailyBonus != nil {
|
||||
activity.DailyBonus = *update.SetDailyBonus
|
||||
changed = true
|
||||
}
|
||||
|
||||
if update.AddSub != nil {
|
||||
sub := *update.AddSub
|
||||
sub.ID = generate.ID("SA", 16)
|
||||
|
||||
activity.SubActivities = append(activity.SubActivities, sub)
|
||||
changed = true
|
||||
}
|
||||
if update.EditSub != nil {
|
||||
sub := activity.SubActivity(update.EditSub.ID)
|
||||
if sub == nil {
|
||||
err = slerrors.NotFound("Sub-activity you're trying to edit")
|
||||
return
|
||||
}
|
||||
|
||||
*sub = *update.EditSub
|
||||
changed = true
|
||||
}
|
||||
if update.RemoveSub != nil {
|
||||
found := false
|
||||
for i, sub := range activity.SubActivities {
|
||||
if sub.ID == *update.RemoveSub {
|
||||
activity.SubActivities = append(activity.SubActivities[:i], activity.SubActivities[i+1:]...)
|
||||
|
||||
found = true
|
||||
changed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
err = slerrors.NotFound("Sub-activity you're trying to remove")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// A SubActivity is a part of the activity, i.e. tutorial, writing, practice. This is what determines
|
||||
// how many points should be awarded. The baseline multiplier should amount to 1000 points per hour,
|
||||
// while activities that take longer (i.e. a published piece of writing vs writing for your eyes only).
|
||||
type SubActivity struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
UnitName string `json:"unitName"`
|
||||
Value float64 `json:"value"`
|
||||
}
|
||||
|
||||
// ActivityUpdate describes a change to an activity
|
||||
type ActivityUpdate struct {
|
||||
AddSub *SubActivity `json:"addSub"`
|
||||
EditSub *SubActivity `json:"editSub"`
|
||||
RemoveSub *string `json:"removeSub"`
|
||||
SetName *string `json:"setName"`
|
||||
SetIcon *string `json:"setIcon"`
|
||||
SetDailyBonus *int `json:"setDailyBonus"`
|
||||
}
|
||||
|
||||
type ActivitiesByName []*Activity
|
||||
|
||||
func (activities ActivitiesByName) Len() int {
|
||||
return len(activities)
|
||||
}
|
||||
|
||||
func (activities ActivitiesByName) Less(i, j int) bool {
|
||||
return activities[i].Name < activities[j].Name
|
||||
}
|
||||
|
||||
func (activities ActivitiesByName) Swap(i, j int) {
|
||||
activities[i], activities[j] = activities[j], activities[i]
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/gisle/stufflog/internal/generate"
|
||||
"github.com/gisle/stufflog/slerrors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A Period is a chunk of time in which activities should be performed. They should always be created manually.
|
||||
type Period struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"userId"`
|
||||
From time.Time `json:"from"`
|
||||
To time.Time `json:"to"`
|
||||
Name string `json:"name"`
|
||||
|
||||
ShouldReScore bool `json:"-" msgpack:"-"`
|
||||
|
||||
Tags []string `json:"tags"`
|
||||
Goals []PeriodGoal `json:"goals"`
|
||||
Logs []PeriodLog `json:"logs"`
|
||||
}
|
||||
|
||||
// GenerateIDs generates IDs. It should only be used initially.
|
||||
func (period *Period) GenerateIDs() {
|
||||
period.ID = generate.ID("P", 12)
|
||||
for i := range period.Goals {
|
||||
period.Goals[i].ID = generate.ID("PG", 16)
|
||||
|
||||
for j := range period.Goals[i].SubGoals {
|
||||
period.Goals[i].SubGoals[j].ID = generate.ID("PGS", 20)
|
||||
}
|
||||
}
|
||||
for i := range period.Logs {
|
||||
period.Logs[i].ID = generate.ID("PL", 16)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear clears the period, but doesn't re-alloc the slices if they're set.
|
||||
func (period *Period) Clear() {
|
||||
period.ID = ""
|
||||
period.UserID = ""
|
||||
period.From = time.Time{}
|
||||
period.To = time.Time{}
|
||||
period.Name = ""
|
||||
|
||||
period.ShouldReScore = false
|
||||
|
||||
period.Tags = period.Tags[:0]
|
||||
period.Goals = period.Goals[:0]
|
||||
period.Logs = period.Logs[:0]
|
||||
}
|
||||
|
||||
// Copy makes a deep copy of the period.
|
||||
func (period *Period) Copy() *Period {
|
||||
periodCopy := *period
|
||||
|
||||
periodCopy.Tags = make([]string, len(period.Tags))
|
||||
copy(periodCopy.Tags, period.Tags)
|
||||
periodCopy.Goals = make([]PeriodGoal, len(period.Goals))
|
||||
copy(periodCopy.Goals, period.Goals)
|
||||
periodCopy.Logs = make([]PeriodLog, len(period.Logs))
|
||||
copy(periodCopy.Logs, period.Logs)
|
||||
|
||||
return &periodCopy
|
||||
}
|
||||
|
||||
// IncludesDate returns true if the date is within the interval of SetFrom..SetTo.
|
||||
func (period *Period) IncludesDate(date time.Time) bool {
|
||||
return (date == period.From || date == period.To) || (date.After(period.From) && date.Before(period.To))
|
||||
}
|
||||
|
||||
// ApplyUpdate applies an update. It does not calculate/validate the scores or remote IDs.
|
||||
//
|
||||
// It may still have been changed even if it errors.
|
||||
func (period *Period) ApplyUpdate(update PeriodUpdate) (changed bool, err error) {
|
||||
if update.SetFrom != nil {
|
||||
changed = true
|
||||
period.From = *update.SetFrom
|
||||
}
|
||||
if update.SetTo != nil {
|
||||
changed = true
|
||||
period.To = *update.SetTo
|
||||
}
|
||||
if update.SetName != nil {
|
||||
changed = true
|
||||
period.Name = *update.SetName
|
||||
}
|
||||
|
||||
if update.RemoveTag != nil {
|
||||
for i, existingTag := range period.Tags {
|
||||
if *update.AddTag == existingTag {
|
||||
period.Tags = append(period.Tags[:i], period.Tags[i+1:]...)
|
||||
changed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if update.AddTag != nil {
|
||||
found := false
|
||||
for _, existingTag := range period.Tags {
|
||||
if *update.AddTag == existingTag {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
changed = true
|
||||
period.Tags = append(period.Tags, *update.AddTag)
|
||||
}
|
||||
}
|
||||
|
||||
if update.AddGoal != nil {
|
||||
goal := *update.AddGoal
|
||||
goal.ID = generate.ID("PG", 16)
|
||||
|
||||
for _, existingGoal := range period.Goals {
|
||||
if existingGoal.ActivityID == goal.ActivityID {
|
||||
return false, slerrors.PreconditionFailed("Activity already exists in another goal.")
|
||||
}
|
||||
}
|
||||
|
||||
period.Goals = append(period.Goals, goal)
|
||||
|
||||
if update.AddLog != nil && update.AddLog.GoalID == "NEW" {
|
||||
update.AddLog.GoalID = goal.ID
|
||||
}
|
||||
for i := range goal.SubGoals {
|
||||
goal.SubGoals[i].ID = generate.ID("PGS", 20)
|
||||
}
|
||||
|
||||
changed = true
|
||||
}
|
||||
if update.ReplaceGoal != nil {
|
||||
goal := period.Goal(update.ReplaceGoal.ID)
|
||||
if goal == nil {
|
||||
err = slerrors.NotFound("Goal")
|
||||
return
|
||||
}
|
||||
|
||||
goal.PointCount = update.ReplaceGoal.PointCount
|
||||
goal.SubGoals = update.ReplaceGoal.SubGoals
|
||||
|
||||
changed = true
|
||||
period.ShouldReScore = true
|
||||
}
|
||||
if update.RemoveGoal != nil {
|
||||
found := false
|
||||
for i, goal := range period.Goals {
|
||||
if goal.ID == *update.RemoveGoal {
|
||||
period.Goals = append(period.Goals[:i], period.Goals[i+1:]...)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
err = slerrors.NotFound("Goal you're trying to remove")
|
||||
return
|
||||
}
|
||||
|
||||
changed = true
|
||||
period.ShouldReScore = true
|
||||
}
|
||||
|
||||
if update.AddLog != nil {
|
||||
log := *update.AddLog
|
||||
log.ID = generate.ID("L", 16)
|
||||
log.SubmitTime = time.Now()
|
||||
|
||||
goal := period.Goal(log.GoalID)
|
||||
if goal == nil {
|
||||
err = slerrors.NotFound("Goal")
|
||||
return
|
||||
}
|
||||
|
||||
period.Logs = append(period.Logs, log)
|
||||
changed = true
|
||||
}
|
||||
if update.RemoveLog != nil {
|
||||
found := false
|
||||
for i, log := range period.Logs {
|
||||
if log.ID == *update.RemoveLog {
|
||||
found = true
|
||||
changed = true
|
||||
period.Logs = append(period.Logs[:i], period.Logs[i+1:]...)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
err = slerrors.NotFound("Log you're trying to remove")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (period *Period) RemoveBrokenLogs() {
|
||||
goodLogs := make([]PeriodLog, 0, len(period.Logs))
|
||||
|
||||
for _, log := range period.Logs {
|
||||
goal := period.Goal(log.GoalID)
|
||||
if goal == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if log.SubGoalID != "" {
|
||||
if goal.SubGoal(log.SubGoalID) == nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
goodLogs = append(goodLogs, log)
|
||||
}
|
||||
|
||||
period.Logs = goodLogs
|
||||
}
|
||||
|
||||
func (period *Period) RemoveDuplicateTags() {
|
||||
deleteList := make([]int, 0, 2)
|
||||
|
||||
found := make(map[string]bool)
|
||||
for i, tag := range period.Tags {
|
||||
if found[tag] {
|
||||
deleteList = append(deleteList, i-len(deleteList))
|
||||
}
|
||||
found[tag] = true
|
||||
}
|
||||
for _, index := range deleteList {
|
||||
period.Tags = append(period.Tags[:index], period.Tags[index+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
func (period *Period) Goal(id string) *PeriodGoal {
|
||||
for i, goal := range period.Goals {
|
||||
if goal.ID == id {
|
||||
return &period.Goals[i]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (period *Period) DayHadActivity(date time.Time, activityID string) bool {
|
||||
date = date.UTC().Truncate(time.Hour * 24)
|
||||
|
||||
if !period.IncludesDate(date) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, log := range period.Logs {
|
||||
goal := period.Goal(log.GoalID)
|
||||
if goal == nil || goal.ActivityID != activityID {
|
||||
continue
|
||||
}
|
||||
|
||||
if date.Equal(log.Date.UTC().Truncate(time.Hour * 24)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// A PeriodGoal is a declared investment into the goal.
|
||||
type PeriodGoal struct {
|
||||
ID string `json:"id"`
|
||||
ActivityID string `json:"activityId"`
|
||||
PointCount int `json:"pointCount"`
|
||||
|
||||
SubGoals []PeriodSubGoal `json:"subGoals"`
|
||||
}
|
||||
|
||||
func (goal *PeriodGoal) SubGoal(id string) *PeriodSubGoal {
|
||||
if goal == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i, subGoal := range goal.SubGoals {
|
||||
if subGoal.ID == id {
|
||||
return &goal.SubGoals[i]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// A PeriodSubGoal is a specific sub-goal that should either punish or boost performance. For example, it could represent
|
||||
// a project or technique that should take priority, or a guilty pleasure that should definitely not be.
|
||||
type PeriodSubGoal struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Multiplier float64 `json:"multiplier"`
|
||||
}
|
||||
|
||||
// PeriodLog is a logged performance of an activity during the period. SubGoalID is optional, but GoalID is not. The
|
||||
// points is the points calculated on the time of submission and does not need to reflect the latest.
|
||||
type PeriodLog struct {
|
||||
Date time.Time `json:"date"`
|
||||
ID string `json:"id"`
|
||||
SubActivityID string `json:"subActivityId"`
|
||||
GoalID string `json:"goalId"`
|
||||
SubGoalID string `json:"subGoalId"`
|
||||
Description string `json:"description"`
|
||||
SubmitTime time.Time `json:"submitTime"`
|
||||
Amount int `json:"amount"`
|
||||
Score *Score `json:"score"`
|
||||
}
|
||||
|
||||
// PeriodUpdate describes a change to a period
|
||||
type PeriodUpdate struct {
|
||||
SetFrom *time.Time `json:"setFrom"`
|
||||
SetTo *time.Time `json:"setTo"`
|
||||
SetName *string `json:"setName"`
|
||||
|
||||
AddLog *PeriodLog `json:"addLog"`
|
||||
RemoveLog *string `json:"removeLog"`
|
||||
AddGoal *PeriodGoal `json:"addGoal"`
|
||||
ReplaceGoal *PeriodGoal `json:"replaceGoal"`
|
||||
RemoveGoal *string `json:"removeGoal"`
|
||||
AddTag *string `json:"addTag"`
|
||||
RemoveTag *string `json:"removeTag"`
|
||||
}
|
||||
|
||||
type Score struct {
|
||||
ActivityScore float64 `json:"activityScore"`
|
||||
Amount int `json:"amount"`
|
||||
SubGoalMultiplier *float64 `json:"subGoalMultiplier"`
|
||||
DailyBonus int `json:"dailyBonus"`
|
||||
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
func (s *Score) Calc() {
|
||||
subGoalMultiplier := 1.0
|
||||
if s.SubGoalMultiplier != nil {
|
||||
subGoalMultiplier = *s.SubGoalMultiplier
|
||||
}
|
||||
|
||||
s.Total = int64(s.DailyBonus) + int64(s.ActivityScore*float64(s.Amount)*subGoalMultiplier)
|
||||
}
|
||||
|
||||
type PeriodsByFrom []*Period
|
||||
|
||||
func (periods PeriodsByFrom) Len() int {
|
||||
return len(periods)
|
||||
}
|
||||
|
||||
func (periods PeriodsByFrom) Less(i, j int) bool {
|
||||
return periods[i].From.Before(periods[j].From)
|
||||
}
|
||||
|
||||
func (periods PeriodsByFrom) Swap(i, j int) {
|
||||
periods[i], periods[j] = periods[j], periods[i]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/gisle/stufflog/slerrors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A User is a user.
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Hash []byte `json:"-"`
|
||||
}
|
||||
|
||||
func (user *User) SetPassword(password string) error {
|
||||
if len(password) < 6 {
|
||||
return &slerrors.SLError{Code: http.StatusBadRequest, Text: "Password is too short"}
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.Hash = hash
|
||||
return nil
|
||||
}
|
||||
|
||||
func (user *User) CheckPassword(password string) error {
|
||||
err := bcrypt.CompareHashAndPassword(user.Hash, []byte(password))
|
||||
if err != nil {
|
||||
return slerrors.LoginFailed()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type UserSession struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"userId"`
|
||||
Expires time.Time `json:"expires"`
|
||||
}
|
||||
Reference in New Issue
Block a user