feat(ui+backend): base of data persistence

This commit is contained in:
2020-09-11 09:19:18 +02:00
parent c7cea6e46b
commit 7fc1a7f3af
37 changed files with 1298 additions and 195 deletions

21
internal/model/base.go Normal file
View File

@ -0,0 +1,21 @@
package model
import "time"
type Base struct {
ID int64 `gorm:"primary_key,AUTO_INCREMENT" json:"id"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (b *Base) BeforeSave() (err error) {
now := time.Now()
if b.CreatedAt.IsZero() {
b.CreatedAt = now
}
b.UpdatedAt = now
return nil
}

123
internal/model/project.go Normal file
View File

@ -0,0 +1,123 @@
package model
import (
"encoding/json"
"github.com/jinzhu/gorm/dialects/postgres"
"github.com/pkg/errors"
)
type Project struct {
Base
Title *string `json:"title"`
Tasks []*Task `json:"tasks"`
Params *ProjectParams `gorm:"-" json:"params"`
ParamsJSON postgres.Jsonb `json:"-" gorm:"column:params;"`
TaskCategories []*TaskCategory `json:"taskCategories"`
ACL []*Access `json:"acl"`
}
func (p *Project) BeforeSave() error {
data, err := json.Marshal(p.Params)
if err != nil {
return errors.WithStack(err)
}
p.ParamsJSON = postgres.Jsonb{RawMessage: data}
return nil
}
func (p *Project) AfterFind() error {
params := &ProjectParams{}
if err := json.Unmarshal(p.ParamsJSON.RawMessage, params); err != nil {
return errors.WithStack(err)
}
p.Params = params
return nil
}
type ProjectParams struct {
TimeUnit *TimeUnit `json:"timeUnit"`
Currency string `json:"currency"`
RoundUpEstimations bool `json:"roundUpEstimations"`
HideFinancialPreviewOnPrint bool `json:"hideFinancialPreviewOnPrint"`
}
func NewDefaultProjectParams() *ProjectParams {
return &ProjectParams{
Currency: "€ H.T.",
TimeUnit: &TimeUnit{
Acronym: "J/H",
Label: "Jour/Homme",
},
RoundUpEstimations: false,
HideFinancialPreviewOnPrint: false,
}
}
func NewDefaultTaskCategories() []*TaskCategory {
return []*TaskCategory{
&TaskCategory{
Label: "Développement",
CostPerTimeUnit: 500,
},
&TaskCategory{
Label: "Conduite de projet",
CostPerTimeUnit: 500,
},
&TaskCategory{
Label: "Recette",
CostPerTimeUnit: 500,
},
}
}
const (
LevelOwner = "owner"
LevelReadOnly = "readonly"
LevelContributor = "contributor"
)
type Access struct {
Base
ProjectID int64
Project *Project `json:"-"`
UserID int64
User *User `json:"user"`
Level string `json:"level"`
}
const (
EstimationPessimistic = "pessimistic"
EstimationLikely = "likely"
EstimationOptimistic = "optimistic"
)
type Task struct {
Base
ProjectID int64 `json:"-"`
Project *Project `json:"-"`
Label *string `json:"label"`
CategoryID int64 `json:"-"`
Category *TaskCategory `json:"category"`
Estimations postgres.Hstore `json:"estimations"`
}
type TaskCategory struct {
Base
ProjectID int64 `json:"-"`
Project *Project `json:"-"`
Label string `json:"label"`
CostPerTimeUnit float64 `json:"costPerTimeUnit"`
}
type ProjectsFilter struct {
Ids []*int64 `json:"ids"`
Limit *int `json:"limit"`
Offset *int `json:"offset"`
Search *string `json:"search"`
OwnerIds []int64 `json:"-"`
}

View File

@ -0,0 +1,261 @@
package model
import (
"context"
"fmt"
"strconv"
"github.com/jinzhu/gorm/dialects/postgres"
"forge.cadoles.com/Cadoles/guesstimate/internal/orm"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
)
type ProjectRepository struct {
db *gorm.DB
}
func (r *ProjectRepository) Create(ctx context.Context, title string, ownerID int64) (*Project, error) {
project := &Project{
Title: &title,
Tasks: make([]*Task, 0),
TaskCategories: NewDefaultTaskCategories(),
Params: NewDefaultProjectParams(),
}
err := orm.WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error {
if err := tx.Save(project).Error; err != nil {
return errors.WithStack(err)
}
err := tx.Model(project).Association("ACL").Append(&Access{
Level: LevelOwner,
UserID: ownerID,
}).Error
if err != nil {
return errors.WithStack(err)
}
err = tx.Model(project).
Preload("ACL").
Preload("ACL.User").
Preload("Tasks").
Preload("Tasks.Category").
Preload("TaskCategories").
Find(project).
Error
if err != nil {
return errors.WithStack(err)
}
return nil
})
if err != nil {
return nil, errors.Wrap(err, "could not create user")
}
return project, nil
}
func (r *ProjectRepository) UpdateTitle(ctx context.Context, projectID int64, title string) (*Project, error) {
project := &Project{}
project.ID = projectID
err := r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(project).Update("title", title).Error; err != nil {
return errors.WithStack(err)
}
err := tx.Model(project).
Preload("ACL").
Preload("ACL.User").
Preload("Tasks").
Preload("Tasks.Category").
Preload("TaskCategories").
First(project, "id = ?", projectID).
Error
if err != nil {
return errors.WithStack(err)
}
return nil
})
if err != nil {
return nil, errors.WithStack(err)
}
return project, nil
}
func (r *ProjectRepository) Search(ctx context.Context, filter *ProjectsFilter) ([]*Project, error) {
projects := make([]*Project, 0)
projectTableName := r.db.NewScope(&Project{}).TableName()
query := r.db.Table(projectTableName).
Preload("ACL").
Preload("ACL.User").
Preload("Tasks").
Preload("Tasks.Category").
Preload("TaskCategories")
if filter != nil {
if len(filter.Ids) > 0 {
query = query.Where(fmt.Sprintf("%s.id in (?)", projectTableName), filter.Ids)
}
if len(filter.OwnerIds) > 0 {
accessTableName := r.db.NewScope(&Access{}).TableName()
query = query.
Joins(
fmt.Sprintf(
"left join %s on %s.project_id = %s.id",
accessTableName, accessTableName, projectTableName,
),
).
Where(fmt.Sprintf("%s.level = ?", accessTableName), LevelOwner).
Where(fmt.Sprintf("%s.user_id IN (?)", accessTableName), filter.OwnerIds)
}
if filter.Limit != nil {
query = query.Limit(*filter.Limit)
}
if filter.Offset != nil {
query = query.Offset(*filter.Offset)
}
}
if err := query.Find(&projects).Error; err != nil {
return nil, errors.WithStack(err)
}
return projects, nil
}
func (r *ProjectRepository) AddTask(ctx context.Context, projectID int64, changes *ProjectTaskChanges) (*Task, error) {
project := &Project{}
project.ID = projectID
task := &Task{}
if changes == nil {
return nil, errors.Errorf("changes should not be nil")
}
err := r.db.Transaction(func(tx *gorm.DB) error {
if changes.Label != nil {
task.Label = changes.Label
}
if changes.CategoryID != nil {
taskCategory := &TaskCategory{}
taskCategory.ID = *changes.CategoryID
task.Category = taskCategory
}
if changes.Estimations != nil {
if task.Estimations == nil {
task.Estimations = postgres.Hstore{}
}
if changes.Estimations.Pessimistic != nil {
pessimistic := strconv.FormatFloat(*changes.Estimations.Pessimistic, 'f', 12, 64)
task.Estimations[EstimationPessimistic] = &pessimistic
}
if changes.Estimations.Likely != nil {
likely := strconv.FormatFloat(*changes.Estimations.Likely, 'f', 12, 64)
task.Estimations[EstimationLikely] = &likely
}
if changes.Estimations.Optimistic != nil {
optimistic := strconv.FormatFloat(*changes.Estimations.Optimistic, 'f', 12, 64)
task.Estimations[EstimationOptimistic] = &optimistic
}
if changes.CategoryID != nil {
taskCategory := &TaskCategory{}
if err := tx.Find(taskCategory, "id = ?", *changes.CategoryID).Error; err != nil {
return errors.Wrap(err, "could not find task category")
}
task.Category = taskCategory
}
}
if err := tx.Save(task).Error; err != nil {
return errors.Wrap(err, "could not create task")
}
err := tx.Model(project).Association("Tasks").Append(task).Error
if err != nil {
return errors.Wrap(err, "could not add task")
}
return nil
})
if err != nil {
return nil, errors.Wrap(err, "could not add task")
}
return task, nil
}
func (r *ProjectRepository) RemoveTask(ctx context.Context, projectID int64, taskID int64) error {
project := &Project{}
project.ID = projectID
err := r.db.Transaction(func(tx *gorm.DB) error {
task := &Task{}
task.ID = taskID
err := tx.Model(project).Association("Tasks").Delete(task).Error
if err != nil {
return errors.Wrap(err, "could not remove task relationship")
}
err = tx.Delete(task, "id = ?", taskID).Error
if err != nil {
return errors.Wrap(err, "could not delete task")
}
return nil
})
if err != nil {
return errors.Wrap(err, "could not remove task")
}
return nil
}
func (r *ProjectRepository) UpdateTaskEstimation(ctx context.Context, projectID, taskID int64, estimation string, value float64) (*Task, error) {
err := r.db.Transaction(func(tx *gorm.DB) error {
task := &Task{}
if err := tx.First(task, "id = ?", taskID).Error; err != nil {
return errors.WithStack(err)
}
strValue := strconv.FormatFloat(value, 'f', 12, 64)
task.Estimations[estimation] = &strValue
if err := tx.Save(task).Error; err != nil {
return errors.WithStack(err)
}
return nil
})
if err != nil {
return nil, errors.Wrap(err, "could not update task")
}
return nil, nil
}
func NewProjectRepository(db *gorm.DB) *ProjectRepository {
return &ProjectRepository{db}
}

View File

@ -5,11 +5,10 @@ import (
)
type User struct {
ID string `json:"id"`
Base
Name *string `json:"name"`
Email string `json:"email"`
ConnectedAt time.Time `json:"connectedAt"`
CreatedAt time.Time `json:"createdAt"`
}
type UserChanges struct {