add activities.
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
var ErrDriverNotSupported = errors.New("driver not found or supported")
|
||||
|
||||
type Database interface {
|
||||
Activities() repositories.ActivityRepository
|
||||
Issues() repositories.IssueRepository
|
||||
Items() repositories.ItemRepository
|
||||
Projects() repositories.ProjectRepository
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package mysqldriver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"git.aiterp.net/stufflog/server/internal/generate"
|
||||
"git.aiterp.net/stufflog/server/internal/xlerrors"
|
||||
"git.aiterp.net/stufflog/server/models"
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type activityRepository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func (r *activityRepository) Find(ctx context.Context, id string) (*models.Activity, error) {
|
||||
activity := models.Activity{}
|
||||
err := r.db.GetContext(ctx, &activity, "SELECT * FROM activity WHERE activity_id=?", id)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, xlerrors.NotFound("Activity")
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &activity, nil
|
||||
}
|
||||
|
||||
func (r *activityRepository) List(ctx context.Context, filter models.ActivityFilter) ([]*models.Activity, error) {
|
||||
q := sq.Select("*").From("activity")
|
||||
if len(filter.ActivityIDs) > 0 {
|
||||
q = q.Where(sq.Eq{"activity_id": filter.ActivityIDs})
|
||||
}
|
||||
if len(filter.ProjectIDs) > 0 {
|
||||
q = q.Where(sq.Eq{"project_id": filter.ProjectIDs})
|
||||
}
|
||||
|
||||
query, args, err := q.ToSql()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results := make([]*models.Activity, 0, 16)
|
||||
err = r.db.SelectContext(ctx, &results, query, args...)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return []*models.Activity{}, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *activityRepository) Insert(ctx context.Context, activity models.Activity) (*models.Activity, error) {
|
||||
if activity.ProjectID == "" {
|
||||
return nil, errors.New("missing project id")
|
||||
}
|
||||
|
||||
activity.ID = generate.ActivityID()
|
||||
|
||||
_, err := r.db.NamedExecContext(ctx, `
|
||||
INSERT INTO activity (
|
||||
activity_id, project_id, name, countable,
|
||||
unit_is_time, unit_name, unit_value, base_value
|
||||
) VALUES (
|
||||
:activity_id, :project_id, :name, :countable,
|
||||
:unit_is_time, :unit_name, :unit_value, :base_value
|
||||
)
|
||||
`, activity)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &activity, nil
|
||||
}
|
||||
|
||||
func (r *activityRepository) Save(ctx context.Context, activity models.Activity) error {
|
||||
_, err := r.db.NamedExecContext(ctx, `
|
||||
UPDATE activity SET
|
||||
name=:name,
|
||||
countable=:countable,
|
||||
unit_is_time=:unit_is_time,
|
||||
unit_name=:unit_name,
|
||||
unit_value=:unit_value,
|
||||
base_value=:base_value
|
||||
WHERE activity_id=:activity_id
|
||||
`, activity)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *activityRepository) Delete(ctx context.Context, activity models.Activity) error {
|
||||
_, err := r.db.ExecContext(ctx, "DELETE FROM activity WHERE activity_id=? LIMIT 1;", activity.ID)
|
||||
return err
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
type DB struct {
|
||||
db *sqlx.DB
|
||||
activities *activityRepository
|
||||
issues *issueRepository
|
||||
items *itemRepository
|
||||
projects *projectRepository
|
||||
@@ -22,6 +23,10 @@ type DB struct {
|
||||
projectStatuses *projectStatusRepository
|
||||
}
|
||||
|
||||
func (db *DB) Activities() repositories.ActivityRepository {
|
||||
return db.activities
|
||||
}
|
||||
|
||||
func (db *DB) Issues() repositories.IssueRepository {
|
||||
return db.issues
|
||||
}
|
||||
@@ -76,6 +81,7 @@ func Open(connectionString string) (*DB, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
activities := &activityRepository{db: db}
|
||||
issues := &issueRepository{db: db}
|
||||
items := &itemRepository{db: db}
|
||||
projects := &projectRepository{db: db}
|
||||
@@ -85,6 +91,7 @@ func Open(connectionString string) (*DB, error) {
|
||||
|
||||
return &DB{
|
||||
db: db,
|
||||
activities: activities,
|
||||
issues: issues,
|
||||
items: items,
|
||||
projects: projects,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/stufflog/server/models"
|
||||
)
|
||||
|
||||
type ActivityRepository interface {
|
||||
Find(ctx context.Context, id string) (*models.Activity, error)
|
||||
List(ctx context.Context, filter models.ActivityFilter) ([]*models.Activity, error)
|
||||
Insert(ctx context.Context, activity models.Activity) (*models.Activity, error)
|
||||
Save(ctx context.Context, activity models.Activity) error
|
||||
Delete(ctx context.Context, activity models.Activity) error
|
||||
}
|
||||
@@ -9,6 +9,12 @@ model:
|
||||
filename: graphcore/input_gen.go
|
||||
package: graphcore
|
||||
|
||||
models:
|
||||
Activity:
|
||||
fields:
|
||||
unitName:
|
||||
resolver: true
|
||||
|
||||
resolver:
|
||||
layout: follow-schema
|
||||
dir: resolvers
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package resolvers
|
||||
|
||||
// This file will be automatically regenerated based on the schema, any resolver implementations
|
||||
// will be copied through when generating and any unknown code will be moved to the end.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/stufflog/server/graph/graphcore"
|
||||
"git.aiterp.net/stufflog/server/models"
|
||||
)
|
||||
|
||||
func (r *activityResolver) UnitName(ctx context.Context, obj *models.Activity) (*string, error) {
|
||||
if !obj.Countable || obj.UnitIsTimeSpent {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &obj.UnitName, nil
|
||||
}
|
||||
|
||||
func (r *activityResolver) Project(ctx context.Context, obj *models.Activity) (*models.Project, error) {
|
||||
return r.Database.Projects().Find(ctx, obj.ProjectID)
|
||||
}
|
||||
|
||||
// Activity returns graphcore.ActivityResolver implementation.
|
||||
func (r *Resolver) Activity() graphcore.ActivityResolver { return &activityResolver{r} }
|
||||
|
||||
type activityResolver struct{ *Resolver }
|
||||
@@ -5,6 +5,7 @@ package resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/stufflog/server/graph/graphcore"
|
||||
"git.aiterp.net/stufflog/server/graph/graphutil"
|
||||
"git.aiterp.net/stufflog/server/graph/loaders"
|
||||
|
||||
@@ -6,6 +6,7 @@ package resolvers
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.aiterp.net/stufflog/server/graph/graphcore"
|
||||
"git.aiterp.net/stufflog/server/internal/xlerrors"
|
||||
"git.aiterp.net/stufflog/server/models"
|
||||
@@ -44,6 +45,83 @@ func (r *mutationResolver) CreateProject(ctx context.Context, input graphcore.Pr
|
||||
return project, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) CreateActivity(ctx context.Context, input graphcore.ActivityCreateInput) (*models.Activity, error) {
|
||||
user := r.Auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, xlerrors.PermissionDenied
|
||||
}
|
||||
|
||||
project, err := r.Database.Projects().Find(ctx, input.ProjectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if perm, err := r.Auth.ProjectPermission(ctx, *project); err != nil || !perm.CanManageActivities() {
|
||||
return nil, xlerrors.PermissionDenied
|
||||
}
|
||||
|
||||
activity := &models.Activity{
|
||||
ProjectID: project.ID,
|
||||
Name: input.Name,
|
||||
Countable: input.Countable != nil && *input.Countable,
|
||||
UnitIsTimeSpent: input.UnitIsTimeSpent != nil && *input.UnitIsTimeSpent,
|
||||
BaseValue: input.BaseValue,
|
||||
}
|
||||
if !activity.UnitIsTimeSpent && activity.Countable {
|
||||
if input.UnitName != nil {
|
||||
activity.UnitName = *input.UnitName
|
||||
} else {
|
||||
return nil, errors.New("unit name is required for countable non-time-spent activities")
|
||||
}
|
||||
}
|
||||
if activity.UnitIsTimeSpent || activity.Countable {
|
||||
if input.UnitValue != nil {
|
||||
activity.UnitValue = *input.UnitValue
|
||||
}
|
||||
}
|
||||
|
||||
return r.Database.Activities().Insert(ctx, *activity)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) EditActivity(ctx context.Context, input graphcore.ActivityEditInput) (*models.Activity, error) {
|
||||
user := r.Auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, xlerrors.PermissionDenied
|
||||
}
|
||||
|
||||
activity, err := r.Database.Activities().Find(ctx, input.ActivityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
project, err := r.Database.Projects().Find(ctx, activity.ProjectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if perm, err := r.Auth.ProjectPermission(ctx, *project); err != nil || !perm.CanManageActivities() {
|
||||
return nil, xlerrors.PermissionDenied
|
||||
}
|
||||
|
||||
if input.SetName != nil {
|
||||
activity.Name = *input.SetName
|
||||
}
|
||||
if input.SetBaseValue != nil {
|
||||
activity.BaseValue = *input.SetBaseValue
|
||||
}
|
||||
if input.SetUnitName != nil {
|
||||
activity.UnitName = *input.SetUnitName
|
||||
}
|
||||
if input.SetUnitValue != nil {
|
||||
activity.UnitValue = *input.SetUnitValue
|
||||
}
|
||||
|
||||
err = r.Database.Activities().Save(ctx, *activity)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return activity, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) CreateIssue(ctx context.Context, input graphcore.IssueCreateInput) (*models.Issue, error) {
|
||||
user := r.Auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
@@ -58,14 +136,19 @@ func (r *mutationResolver) CreateIssue(ctx context.Context, input graphcore.Issu
|
||||
return nil, xlerrors.PermissionDenied
|
||||
}
|
||||
|
||||
status, err := r.Database.ProjectStatuses().Find(ctx, project.ID, input.StatusName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
issue := &models.Issue{
|
||||
ProjectID: project.ID,
|
||||
OwnerID: user.ID,
|
||||
AssigneeID: "",
|
||||
StatusStage: input.StatusStage,
|
||||
StatusName: input.StatusName,
|
||||
StatusStage: status.Stage,
|
||||
StatusName: status.Name,
|
||||
Name: input.Name,
|
||||
Title: input.Name,
|
||||
Title: input.Name, // Title set below if it's in the input.
|
||||
Description: input.Description,
|
||||
}
|
||||
if input.Title != nil && *input.Title != "" {
|
||||
|
||||
@@ -48,6 +48,10 @@ func (r *projectResolver) Statuses(ctx context.Context, obj *models.Project, fil
|
||||
return r.Database.ProjectStatuses().List(ctx, *filter)
|
||||
}
|
||||
|
||||
func (r *projectResolver) Activities(ctx context.Context, obj *models.Project) ([]*models.Activity, error) {
|
||||
return r.Database.Activities().List(ctx, models.ActivityFilter{ProjectIDs: []string{obj.ID}})
|
||||
}
|
||||
|
||||
func (r *projectPermissionResolver) User(ctx context.Context, obj *models.ProjectPermission) (*models.User, error) {
|
||||
return loaders.UserLoaderFromContext(ctx).Load(obj.UserID)
|
||||
}
|
||||
|
||||
@@ -8,14 +8,14 @@ import (
|
||||
"errors"
|
||||
|
||||
"git.aiterp.net/stufflog/server/graph/graphcore"
|
||||
"git.aiterp.net/stufflog/server/internal/xlerrors"
|
||||
"git.aiterp.net/stufflog/server/models"
|
||||
"git.aiterp.net/stufflog/server/services"
|
||||
)
|
||||
|
||||
func (r *queryResolver) Issue(ctx context.Context, id string) (*models.Issue, error) {
|
||||
user := r.Auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, services.ErrPermissionDenied
|
||||
return nil, xlerrors.PermissionDenied
|
||||
}
|
||||
|
||||
issue, err := r.Database.Issues().Find(ctx, id)
|
||||
@@ -33,7 +33,7 @@ func (r *queryResolver) Issue(ctx context.Context, id string) (*models.Issue, er
|
||||
func (r *queryResolver) Issues(ctx context.Context, filter *models.IssueFilter) ([]*models.Issue, error) {
|
||||
user := r.Auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, services.ErrPermissionDenied
|
||||
return nil, xlerrors.PermissionDenied
|
||||
}
|
||||
|
||||
if filter == nil {
|
||||
@@ -61,7 +61,7 @@ func (r *queryResolver) Issues(ctx context.Context, filter *models.IssueFilter)
|
||||
func (r *queryResolver) Project(ctx context.Context, id string) (*models.Project, error) {
|
||||
user := r.Auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, services.ErrPermissionDenied
|
||||
return nil, xlerrors.PermissionDenied
|
||||
}
|
||||
|
||||
project, err := r.Database.Projects().Find(ctx, id)
|
||||
@@ -79,7 +79,7 @@ func (r *queryResolver) Project(ctx context.Context, id string) (*models.Project
|
||||
func (r *queryResolver) Projects(ctx context.Context, filter *models.ProjectFilter) ([]*models.Project, error) {
|
||||
user := r.Auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, services.ErrPermissionDenied
|
||||
return nil, xlerrors.PermissionDenied
|
||||
}
|
||||
|
||||
skipCheck := false
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
An activity is a measurable activity for a project. It can be measured.
|
||||
"""
|
||||
type Activity {
|
||||
"The activity ID."
|
||||
id: String!
|
||||
"The activity name."
|
||||
name: String!
|
||||
"Whether the activity is countable."
|
||||
countable: Boolean!
|
||||
"Whether the time spent is the unit."
|
||||
unitIsTimeSpent: Boolean!
|
||||
"The name of the unit. If unitIsTime or countable is true, this value should be ignored."
|
||||
unitName: String
|
||||
"The value per unit."
|
||||
unitValue: Float!
|
||||
"The base score value for any performed activity. For uncountables, this is the only points scored."
|
||||
baseValue: Float!
|
||||
|
||||
"Parent project."
|
||||
project: Project!
|
||||
}
|
||||
|
||||
"""
|
||||
Input for the createActivity mutation.
|
||||
"""
|
||||
input ActivityCreateInput {
|
||||
"Project ID to associate it with."
|
||||
projectId: String!
|
||||
"Proejct name."
|
||||
name: String!
|
||||
"The base value of any activity performed. If uncountable, this should be non-zero."
|
||||
baseValue: Float!
|
||||
"Whether the activity is countable. Default: false"
|
||||
countable: Boolean
|
||||
"Whether time spent should be the unit of this activity."
|
||||
unitIsTimeSpent: Boolean
|
||||
"The unit name of the activity."
|
||||
unitName: String
|
||||
"The per-unit value of the activity."
|
||||
unitValue: Float
|
||||
}
|
||||
|
||||
"""
|
||||
Input for the editActivity mutation. Not all changes are available as they could create
|
||||
some serious issues with past goals and logs.
|
||||
"""
|
||||
input ActivityEditInput {
|
||||
"The ID of the activity to edit."
|
||||
activityId: String!
|
||||
|
||||
"Update the name of the activity."
|
||||
setName: String
|
||||
"Set the base value of the activity. This has an effect on current and past goals!"
|
||||
setBaseValue: Float
|
||||
"Set the unit name of the activity. This is only for countable, non time spent acitivities."
|
||||
setUnitName: String
|
||||
"Set the unit value of the activity. This has an effect on current and past goals!"
|
||||
setUnitValue: Float
|
||||
}
|
||||
+23
-14
@@ -1,51 +1,60 @@
|
||||
type Issue {
|
||||
"The issue ID."
|
||||
id: String!
|
||||
"The time at which the issue is created."
|
||||
createdTime: Time!
|
||||
"The time at which the issue was updated."
|
||||
updatedTime: Time!
|
||||
"Optionally, when this issue is due."
|
||||
dueTime: Time
|
||||
"The name of the issue, used in lists."
|
||||
name: String!
|
||||
"The issue title. This can be longer than the name."
|
||||
title: String!
|
||||
"The description of the issue, in markdown."
|
||||
description: String!
|
||||
|
||||
"Parent project."
|
||||
project: Project
|
||||
"The issue's owner/creator."
|
||||
owner: User
|
||||
"The issue assignee."
|
||||
assignee: User
|
||||
"The issue status."
|
||||
status: ProjectStatus!
|
||||
}
|
||||
|
||||
input IssueFilter {
|
||||
"Filter by issue IDs (mostly used internally by data loaders)"
|
||||
"Filter by issue IDs. (mostly used internally by data loaders)"
|
||||
issueIds: [String!]
|
||||
"Filter by project IDs"
|
||||
"Filter by project IDs."
|
||||
projectIds: [String!]
|
||||
"Filter by owner IDs"
|
||||
"Filter by owner IDs."
|
||||
ownerIds: [String!]
|
||||
"Filter by assignee IDs"
|
||||
"Filter by assignee IDs."
|
||||
assigneeIds: [String!]
|
||||
"Text search"
|
||||
"Text search."
|
||||
search: String
|
||||
"Earliest stage (inclusive)"
|
||||
"Earliest stage. (inclusive)"
|
||||
minStage: Int
|
||||
"Latest stage (inclusive)"
|
||||
"Latest stage. (inclusive)"
|
||||
maxStage: Int
|
||||
"Limit the result set"
|
||||
"Limit the result set."
|
||||
limit: Int
|
||||
}
|
||||
|
||||
input IssueCreateInput {
|
||||
"Project ID"
|
||||
"Project ID."
|
||||
projectId: String!
|
||||
"Status stage"
|
||||
statusStage: Int!
|
||||
"Status name"
|
||||
"Status name."
|
||||
statusName: String!
|
||||
"A name for the issue."
|
||||
name: String!
|
||||
"Description of the issue."
|
||||
description: String!
|
||||
"Assign this issue, will default to unassigned"
|
||||
"Assign this issue, will default to unassigned."
|
||||
assigneeId: String
|
||||
"A date when this issue is due"
|
||||
"A date when this issue is due."
|
||||
dueTime: Time
|
||||
"Optional title to use instead of the name when not in a list."
|
||||
title: String
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
type Mutation {
|
||||
# PROJECT
|
||||
"Create a new project"
|
||||
"Create a new project."
|
||||
createProject(input: ProjectCreateInput!): Project!
|
||||
|
||||
# ACTIVITY
|
||||
"Create an activity."
|
||||
createActivity(input: ActivityCreateInput!): Activity!
|
||||
"Edit an activity."
|
||||
editActivity(input: ActivityEditInput!): Activity!
|
||||
|
||||
# ISSUE
|
||||
"Create a new issue"
|
||||
"Create a new issue."
|
||||
createIssue(input: IssueCreateInput!): Issue!
|
||||
|
||||
# USER
|
||||
"Log in"
|
||||
"Log in."
|
||||
loginUser(input: UserLoginInput!): User!
|
||||
"Log out"
|
||||
"Log out."
|
||||
logoutUser: User!
|
||||
"Create a new user. This can only be done by administrators."
|
||||
createUser(input: UserCreateInput!): User!
|
||||
"Edit an existing user. This can only be done by administrators"
|
||||
"Edit an existing user. This can only be done by administrators."
|
||||
editUser(input: UserEditInput!): User!
|
||||
}
|
||||
@@ -16,6 +16,8 @@ type Project {
|
||||
userPermissions: ProjectPermission!
|
||||
"List all project statuses."
|
||||
statuses(filter: ProjectStatusFilter): [ProjectStatus!]!
|
||||
"The activities in this project."
|
||||
activities: [Activity!]!
|
||||
}
|
||||
|
||||
"The permissions of a user within the project."
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package generate
|
||||
|
||||
func ActivityID() string {
|
||||
return Generate(16, "A")
|
||||
}
|
||||
|
||||
func ItemID() string {
|
||||
return Generate(32, "I")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE activity (
|
||||
activity_id CHAR(16) PRIMARY KEY,
|
||||
project_id CHAR(16) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
countable BOOLEAN NOT NULL,
|
||||
unit_is_time BOOLEAN NOT NULL,
|
||||
unit_name VARCHAR(255) NOT NULL,
|
||||
unit_value FLOAT NOT NULL,
|
||||
base_value FLOAT NOT NULL,
|
||||
|
||||
INDEX(Project_id)
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE activity;
|
||||
-- +goose StatementEnd
|
||||
+13
-6
@@ -3,10 +3,17 @@ package models
|
||||
// Activity is an activity within a project that can be measured and estimated, for example "writing" or "developing".
|
||||
// The points are for the "gamified" aspect of this.
|
||||
type Activity struct {
|
||||
ID string `db:"activity_id"`
|
||||
ProjectID string `db:"project_id"`
|
||||
Countable bool `db:"countable"`
|
||||
UnitName string `db:"unit_name"`
|
||||
UnitValue float64 `db:"unit_value"`
|
||||
BaseValue float64 `db:"base_value"`
|
||||
ID string `db:"activity_id"`
|
||||
ProjectID string `db:"project_id"`
|
||||
Name string `db:"name"`
|
||||
Countable bool `db:"countable"`
|
||||
UnitIsTimeSpent bool `db:"unit_is_time"`
|
||||
UnitName string `db:"unit_name"`
|
||||
UnitValue float64 `db:"unit_value"`
|
||||
BaseValue float64 `db:"base_value"`
|
||||
}
|
||||
|
||||
type ActivityFilter struct {
|
||||
ActivityIDs []string
|
||||
ProjectIDs []string
|
||||
}
|
||||
|
||||
@@ -34,3 +34,7 @@ func (permission *ProjectPermission) CanManageAnyIssue() bool {
|
||||
func (permission *ProjectPermission) CanManagePermissions() bool {
|
||||
return permission.Level >= ProjectPermissionLevelOwner
|
||||
}
|
||||
|
||||
func (permission *ProjectPermission) CanManageActivities() bool {
|
||||
return permission.Level >= ProjectPermissionLevelAdmin
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user