2021-07-23 03:09:46 +00:00
|
|
|
package website
|
|
|
|
|
|
|
|
import (
|
2021-11-06 20:25:31 +00:00
|
|
|
"context"
|
2021-12-02 10:53:36 +00:00
|
|
|
"fmt"
|
2021-11-06 20:25:31 +00:00
|
|
|
|
2021-11-08 19:16:54 +00:00
|
|
|
"git.handmade.network/hmn/hmn/src/hmnurl"
|
2021-12-08 03:37:52 +00:00
|
|
|
"git.handmade.network/hmn/hmn/src/templates"
|
2021-11-10 04:11:39 +00:00
|
|
|
|
|
|
|
"git.handmade.network/hmn/hmn/src/db"
|
2021-07-23 03:09:46 +00:00
|
|
|
"git.handmade.network/hmn/hmn/src/models"
|
|
|
|
"git.handmade.network/hmn/hmn/src/oops"
|
|
|
|
)
|
|
|
|
|
2021-11-06 20:25:31 +00:00
|
|
|
type ProjectTypeQuery int
|
|
|
|
|
|
|
|
const (
|
|
|
|
PersonalProjects ProjectTypeQuery = 1 << iota
|
|
|
|
OfficialProjects
|
|
|
|
)
|
|
|
|
|
|
|
|
type ProjectsQuery struct {
|
2021-11-08 19:16:54 +00:00
|
|
|
// Available on all project queries
|
2021-12-08 03:37:52 +00:00
|
|
|
Lifecycles []models.ProjectLifecycle // if empty, defaults to models.VisibleProjectLifecycles
|
|
|
|
Types ProjectTypeQuery // bitfield
|
|
|
|
IncludeHidden bool
|
2021-11-08 19:16:54 +00:00
|
|
|
|
|
|
|
// Ignored when using FetchProject
|
|
|
|
ProjectIDs []int // if empty, all projects
|
|
|
|
Slugs []string // if empty, all projects
|
2021-12-02 10:53:36 +00:00
|
|
|
OwnerIDs []int // if empty, all projects
|
2021-11-06 20:25:31 +00:00
|
|
|
|
|
|
|
// Ignored when using CountProjects
|
|
|
|
Limit, Offset int // if empty, no pagination
|
2021-12-02 10:53:36 +00:00
|
|
|
OrderBy string
|
2021-11-06 20:25:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type ProjectAndStuff struct {
|
2021-12-08 03:37:52 +00:00
|
|
|
Project models.Project
|
|
|
|
LogoLightAsset *models.Asset `db:"logolight_asset"`
|
|
|
|
LogoDarkAsset *models.Asset `db:"logodark_asset"`
|
|
|
|
Owners []*models.User
|
|
|
|
Tag *models.Tag
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *ProjectAndStuff) TagText() string {
|
|
|
|
if p.Tag == nil {
|
|
|
|
return ""
|
|
|
|
} else {
|
|
|
|
return p.Tag.Text
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *ProjectAndStuff) LogoURL(theme string) string {
|
|
|
|
return templates.ProjectLogoUrl(&p.Project, p.LogoLightAsset, p.LogoDarkAsset, theme)
|
2021-11-06 20:25:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func FetchProjects(
|
|
|
|
ctx context.Context,
|
|
|
|
dbConn db.ConnOrTx,
|
|
|
|
currentUser *models.User,
|
|
|
|
q ProjectsQuery,
|
|
|
|
) ([]ProjectAndStuff, error) {
|
|
|
|
perf := ExtractPerf(ctx)
|
|
|
|
perf.StartBlock("SQL", "Fetch projects")
|
|
|
|
defer perf.EndBlock()
|
|
|
|
|
|
|
|
var currentUserID *int
|
|
|
|
if currentUser != nil {
|
|
|
|
currentUserID = ¤tUser.ID
|
|
|
|
}
|
|
|
|
|
|
|
|
tx, err := dbConn.Begin(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, oops.New(err, "failed to start transaction")
|
|
|
|
}
|
|
|
|
defer tx.Rollback(ctx)
|
|
|
|
|
2021-12-08 03:37:52 +00:00
|
|
|
type projectRow struct {
|
|
|
|
Project models.Project `db:"project"`
|
|
|
|
LogoLightAsset *models.Asset `db:"logolight_asset"`
|
|
|
|
LogoDarkAsset *models.Asset `db:"logodark_asset"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// If true, join against the project owners table and check visibility permissions
|
|
|
|
checkOwnerVisibility := q.IncludeHidden && currentUser != nil
|
|
|
|
|
2021-11-06 20:25:31 +00:00
|
|
|
// Fetch all valid projects (not yet subject to user permission checks)
|
|
|
|
var qb db.QueryBuilder
|
2021-12-02 10:53:36 +00:00
|
|
|
if len(q.OrderBy) > 0 {
|
|
|
|
qb.Add(`SELECT * FROM (`)
|
|
|
|
}
|
2021-11-06 20:25:31 +00:00
|
|
|
qb.Add(`
|
2021-12-02 10:53:36 +00:00
|
|
|
SELECT DISTINCT ON (project.id) $columns
|
2021-11-06 20:25:31 +00:00
|
|
|
FROM
|
|
|
|
handmade_project AS project
|
2021-12-02 10:53:36 +00:00
|
|
|
LEFT JOIN handmade_asset AS logolight_asset ON logolight_asset.id = project.logolight_asset_id
|
|
|
|
LEFT JOIN handmade_asset AS logodark_asset ON logodark_asset.id = project.logodark_asset_id
|
|
|
|
`)
|
|
|
|
if len(q.OwnerIDs) > 0 {
|
|
|
|
qb.Add(`
|
|
|
|
INNER JOIN handmade_user_projects AS owner_filter ON owner_filter.project_id = project.id
|
|
|
|
`)
|
|
|
|
}
|
2021-12-08 03:37:52 +00:00
|
|
|
if checkOwnerVisibility {
|
2021-12-02 10:53:36 +00:00
|
|
|
qb.Add(`
|
|
|
|
LEFT JOIN handmade_user_projects AS owner_visibility ON owner_visibility.project_id = project.id
|
|
|
|
`)
|
|
|
|
}
|
|
|
|
qb.Add(`
|
2021-11-06 20:25:31 +00:00
|
|
|
WHERE
|
2021-11-09 19:14:38 +00:00
|
|
|
TRUE
|
2021-11-06 20:25:31 +00:00
|
|
|
`)
|
2021-12-08 03:37:52 +00:00
|
|
|
|
2021-12-02 10:53:36 +00:00
|
|
|
// Filters
|
2021-11-08 19:16:54 +00:00
|
|
|
if len(q.ProjectIDs) > 0 {
|
|
|
|
qb.Add(`AND project.id = ANY ($?)`, q.ProjectIDs)
|
|
|
|
}
|
|
|
|
if len(q.Slugs) > 0 {
|
2021-11-11 23:59:05 +00:00
|
|
|
qb.Add(`AND (project.slug != '' AND project.slug = ANY ($?))`, q.Slugs)
|
2021-11-08 19:16:54 +00:00
|
|
|
}
|
2021-12-02 10:53:36 +00:00
|
|
|
if len(q.OwnerIDs) > 0 {
|
|
|
|
qb.Add(`AND (owner_filter.user_id = ANY ($?))`, q.OwnerIDs)
|
2021-11-06 20:25:31 +00:00
|
|
|
}
|
|
|
|
if q.Types != 0 {
|
|
|
|
qb.Add(`AND (FALSE`)
|
|
|
|
if q.Types&PersonalProjects != 0 {
|
|
|
|
qb.Add(`OR project.personal`)
|
|
|
|
}
|
|
|
|
if q.Types&OfficialProjects != 0 {
|
|
|
|
qb.Add(`OR NOT project.personal`)
|
|
|
|
}
|
|
|
|
qb.Add(`)`)
|
|
|
|
}
|
2021-12-02 10:53:36 +00:00
|
|
|
|
|
|
|
// Visibility
|
2021-12-08 03:37:52 +00:00
|
|
|
if checkOwnerVisibility {
|
|
|
|
qb.Add(`AND ($? OR owner_visibility.user_id = $? OR (TRUE`, currentUser.IsStaff, currentUser.ID)
|
2021-12-02 10:53:36 +00:00
|
|
|
}
|
|
|
|
if !q.IncludeHidden {
|
|
|
|
qb.Add(`AND NOT hidden`)
|
|
|
|
}
|
|
|
|
if len(q.Lifecycles) > 0 {
|
|
|
|
qb.Add(`AND project.lifecycle = ANY($?)`, q.Lifecycles)
|
|
|
|
} else {
|
|
|
|
qb.Add(`AND project.lifecycle = ANY($?)`, models.VisibleProjectLifecycles)
|
|
|
|
}
|
2021-12-08 03:37:52 +00:00
|
|
|
if checkOwnerVisibility {
|
2021-12-02 10:53:36 +00:00
|
|
|
qb.Add(`))`)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Output
|
2021-11-06 20:25:31 +00:00
|
|
|
if q.Limit > 0 {
|
|
|
|
qb.Add(`LIMIT $? OFFSET $?`, q.Limit, q.Offset)
|
|
|
|
}
|
2021-12-02 10:53:36 +00:00
|
|
|
if len(q.OrderBy) > 0 {
|
|
|
|
qb.Add(fmt.Sprintf(`) q ORDER BY %s`, q.OrderBy))
|
|
|
|
}
|
2021-12-08 03:37:52 +00:00
|
|
|
|
|
|
|
// Do the query
|
|
|
|
itProjects, err := db.Query(ctx, dbConn, projectRow{}, qb.String(), qb.Args()...)
|
2021-11-06 20:25:31 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, oops.New(err, "failed to fetch projects")
|
|
|
|
}
|
|
|
|
iprojects := itProjects.ToSlice()
|
|
|
|
|
2021-12-08 03:37:52 +00:00
|
|
|
// Fetch project tags
|
|
|
|
var tagIDs []int
|
|
|
|
for _, iproject := range iprojects {
|
|
|
|
tagID := iproject.(*projectRow).Project.TagID
|
|
|
|
if tagID != nil {
|
|
|
|
tagIDs = append(tagIDs, *tagID)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
tags, err := FetchTags(ctx, tx, TagQuery{
|
|
|
|
IDs: tagIDs,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-11-06 20:25:31 +00:00
|
|
|
// Fetch project owners to do permission checks
|
|
|
|
projectIds := make([]int, len(iprojects))
|
|
|
|
for i, iproject := range iprojects {
|
2021-12-08 03:37:52 +00:00
|
|
|
projectIds[i] = iproject.(*projectRow).Project.ID
|
2021-11-06 20:25:31 +00:00
|
|
|
}
|
|
|
|
projectOwners, err := FetchMultipleProjectsOwners(ctx, tx, projectIds)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var res []ProjectAndStuff
|
|
|
|
for i, iproject := range iprojects {
|
2021-12-08 03:37:52 +00:00
|
|
|
row := iproject.(*projectRow)
|
2021-11-06 20:25:31 +00:00
|
|
|
owners := projectOwners[i].Owners
|
|
|
|
|
|
|
|
/*
|
|
|
|
Per our spec, a user can see a project if:
|
|
|
|
- The project is official
|
|
|
|
- The project is personal and all of the project's owners are approved
|
|
|
|
- The project is personal and the current user is a collaborator (regardless of user status)
|
|
|
|
|
|
|
|
See https://www.notion.so/handmade-network/Technical-Plan-a11aaa9ea2f14d9a95f7d7780edd789c
|
|
|
|
*/
|
|
|
|
|
|
|
|
var projectVisible bool
|
2021-12-08 03:37:52 +00:00
|
|
|
if row.Project.Personal {
|
2021-11-06 20:25:31 +00:00
|
|
|
allOwnersApproved := true
|
|
|
|
for _, owner := range owners {
|
|
|
|
if owner.Status != models.UserStatusApproved {
|
|
|
|
allOwnersApproved = false
|
|
|
|
}
|
|
|
|
if currentUserID != nil && *currentUserID == owner.ID {
|
|
|
|
projectVisible = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if allOwnersApproved {
|
|
|
|
projectVisible = true
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
projectVisible = true
|
|
|
|
}
|
|
|
|
|
|
|
|
if projectVisible {
|
2021-12-08 03:37:52 +00:00
|
|
|
var projectTag *models.Tag
|
|
|
|
if row.Project.TagID != nil {
|
|
|
|
for _, tag := range tags {
|
|
|
|
if tag.ID == *row.Project.TagID {
|
|
|
|
projectTag = tag
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-06 20:25:31 +00:00
|
|
|
res = append(res, ProjectAndStuff{
|
2021-12-08 03:37:52 +00:00
|
|
|
Project: row.Project,
|
|
|
|
LogoLightAsset: row.LogoLightAsset,
|
|
|
|
LogoDarkAsset: row.LogoDarkAsset,
|
|
|
|
Owners: owners,
|
|
|
|
Tag: projectTag,
|
2021-11-06 20:25:31 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
err = tx.Commit(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, oops.New(err, "failed to commit transaction")
|
|
|
|
}
|
|
|
|
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
|
2021-11-08 19:16:54 +00:00
|
|
|
/*
|
|
|
|
Fetches a single project. A wrapper around FetchProjects.
|
|
|
|
|
|
|
|
Returns db.NotFound if no result is found.
|
|
|
|
*/
|
|
|
|
func FetchProject(
|
|
|
|
ctx context.Context,
|
|
|
|
dbConn db.ConnOrTx,
|
|
|
|
currentUser *models.User,
|
|
|
|
projectID int,
|
|
|
|
q ProjectsQuery,
|
|
|
|
) (ProjectAndStuff, error) {
|
|
|
|
q.ProjectIDs = []int{projectID}
|
|
|
|
q.Limit = 1
|
|
|
|
q.Offset = 0
|
|
|
|
|
|
|
|
res, err := FetchProjects(ctx, dbConn, currentUser, q)
|
|
|
|
if err != nil {
|
|
|
|
return ProjectAndStuff{}, oops.New(err, "failed to fetch project")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(res) == 0 {
|
|
|
|
return ProjectAndStuff{}, db.NotFound
|
|
|
|
}
|
|
|
|
|
|
|
|
return res[0], nil
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
Fetches a single project by slug. A wrapper around FetchProjects.
|
|
|
|
|
|
|
|
Returns db.NotFound if no result is found.
|
|
|
|
*/
|
|
|
|
func FetchProjectBySlug(
|
|
|
|
ctx context.Context,
|
|
|
|
dbConn db.ConnOrTx,
|
|
|
|
currentUser *models.User,
|
|
|
|
projectSlug string,
|
|
|
|
q ProjectsQuery,
|
|
|
|
) (ProjectAndStuff, error) {
|
|
|
|
q.Slugs = []string{projectSlug}
|
|
|
|
q.Limit = 1
|
|
|
|
q.Offset = 0
|
|
|
|
|
|
|
|
res, err := FetchProjects(ctx, dbConn, currentUser, q)
|
|
|
|
if err != nil {
|
|
|
|
return ProjectAndStuff{}, oops.New(err, "failed to fetch project")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(res) == 0 {
|
|
|
|
return ProjectAndStuff{}, db.NotFound
|
|
|
|
}
|
|
|
|
|
|
|
|
return res[0], nil
|
|
|
|
}
|
|
|
|
|
2021-11-06 20:25:31 +00:00
|
|
|
func CountProjects(
|
|
|
|
ctx context.Context,
|
|
|
|
dbConn db.ConnOrTx,
|
|
|
|
currentUser *models.User,
|
|
|
|
q ProjectsQuery,
|
|
|
|
) (int, error) {
|
|
|
|
perf := ExtractPerf(ctx)
|
|
|
|
perf.StartBlock("SQL", "Count projects")
|
|
|
|
defer perf.EndBlock()
|
|
|
|
|
|
|
|
q.Limit = 0
|
|
|
|
q.Offset = 0
|
|
|
|
|
|
|
|
// I'm lazy and there probably won't ever be that many projects.
|
|
|
|
projects, err := FetchProjects(ctx, dbConn, currentUser, q)
|
|
|
|
if err != nil {
|
|
|
|
return 0, oops.New(err, "failed to fetch projects")
|
|
|
|
}
|
|
|
|
|
|
|
|
return len(projects), nil
|
|
|
|
}
|
|
|
|
|
2021-07-23 03:09:46 +00:00
|
|
|
func CanEditProject(c *RequestContext, user *models.User, projectId int) (bool, error) {
|
|
|
|
if user != nil {
|
|
|
|
if user.IsStaff {
|
|
|
|
return true, nil
|
|
|
|
} else {
|
2021-11-06 20:25:31 +00:00
|
|
|
owners, err := FetchProjectOwners(c.Context(), c.Conn, projectId)
|
2021-07-23 03:09:46 +00:00
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
for _, owner := range owners {
|
|
|
|
if owner.ID == user.ID {
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
2021-11-06 20:25:31 +00:00
|
|
|
type ProjectOwners struct {
|
|
|
|
ProjectID int
|
|
|
|
Owners []*models.User
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fetches all owners for multiple projects. Does NOT check permissions on the
|
|
|
|
// project IDs, since the assumption is that you will check permissions on the
|
|
|
|
// projects themselves before using any of this data.
|
|
|
|
//
|
|
|
|
// The returned slice will always have one entry for each project ID given, in
|
|
|
|
// the same order as they were provided. If there are duplicate project IDs in
|
|
|
|
// projectIds, the results will be wrong, so don't do that.
|
|
|
|
//
|
|
|
|
// This function does not verify that the requested projects do in fact exist.
|
|
|
|
func FetchMultipleProjectsOwners(
|
|
|
|
ctx context.Context,
|
|
|
|
dbConn db.ConnOrTx,
|
|
|
|
projectIds []int,
|
|
|
|
) ([]ProjectOwners, error) {
|
|
|
|
perf := ExtractPerf(ctx)
|
|
|
|
perf.StartBlock("SQL", "Fetch owners for multiple projects")
|
|
|
|
defer perf.EndBlock()
|
|
|
|
|
|
|
|
tx, err := dbConn.Begin(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, oops.New(err, "failed to start transaction")
|
|
|
|
}
|
|
|
|
defer tx.Rollback(ctx)
|
|
|
|
|
|
|
|
// Fetch all user/project pairs for the given projects
|
|
|
|
type userProject struct {
|
|
|
|
UserID int `db:"user_id"`
|
|
|
|
ProjectID int `db:"project_id"`
|
2021-07-23 03:09:46 +00:00
|
|
|
}
|
2021-11-06 20:25:31 +00:00
|
|
|
it, err := db.Query(ctx, tx, userProject{},
|
2021-07-23 03:09:46 +00:00
|
|
|
`
|
|
|
|
SELECT $columns
|
2021-11-06 20:25:31 +00:00
|
|
|
FROM handmade_user_projects
|
|
|
|
WHERE project_id = ANY($1)
|
|
|
|
`,
|
|
|
|
projectIds,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, oops.New(err, "failed to fetch project IDs")
|
|
|
|
}
|
|
|
|
iuserprojects := it.ToSlice()
|
|
|
|
|
|
|
|
// Get the unique user IDs from this set and fetch the users from the db
|
|
|
|
var userIds []int
|
|
|
|
for _, iuserproject := range iuserprojects {
|
|
|
|
userProject := iuserproject.(*userProject)
|
|
|
|
|
|
|
|
addUserId := true
|
|
|
|
for _, uid := range userIds {
|
|
|
|
if uid == userProject.UserID {
|
|
|
|
addUserId = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if addUserId {
|
|
|
|
userIds = append(userIds, userProject.UserID)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
it, err = db.Query(ctx, tx, models.User{},
|
|
|
|
`
|
|
|
|
SELECT $columns
|
|
|
|
FROM auth_user
|
2021-07-23 03:09:46 +00:00
|
|
|
WHERE
|
2021-11-06 20:25:31 +00:00
|
|
|
id = ANY($1)
|
2021-07-23 03:09:46 +00:00
|
|
|
`,
|
2021-11-06 20:25:31 +00:00
|
|
|
userIds,
|
2021-07-23 03:09:46 +00:00
|
|
|
)
|
|
|
|
if err != nil {
|
2021-11-06 20:25:31 +00:00
|
|
|
return nil, oops.New(err, "failed to fetch users for projects")
|
2021-07-23 03:09:46 +00:00
|
|
|
}
|
2021-11-06 20:25:31 +00:00
|
|
|
iusers := it.ToSlice()
|
|
|
|
|
|
|
|
// Build the final result set with real user data
|
|
|
|
res := make([]ProjectOwners, len(projectIds))
|
|
|
|
for i, pid := range projectIds {
|
|
|
|
res[i] = ProjectOwners{ProjectID: pid}
|
2021-07-23 03:09:46 +00:00
|
|
|
}
|
2021-11-06 20:25:31 +00:00
|
|
|
for _, iuserproject := range iuserprojects {
|
|
|
|
userProject := iuserproject.(*userProject)
|
|
|
|
|
|
|
|
// Get a pointer to the existing record in the result
|
|
|
|
var projectOwners *ProjectOwners
|
|
|
|
for i := range res {
|
|
|
|
if res[i].ProjectID == userProject.ProjectID {
|
|
|
|
projectOwners = &res[i]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the full user record we fetched
|
|
|
|
var user *models.User
|
|
|
|
for _, iuser := range iusers {
|
|
|
|
u := iuser.(*models.User)
|
|
|
|
if u.ID == userProject.UserID {
|
|
|
|
user = u
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if user == nil {
|
|
|
|
panic("we apparently failed to fetch a project's owner")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Slam 'em together
|
|
|
|
projectOwners.Owners = append(projectOwners.Owners, user)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = tx.Commit(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, oops.New(err, "failed to commit transaction")
|
|
|
|
}
|
|
|
|
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fetches project owners for a single project. It is subject to all the same
|
|
|
|
// restrictions as FetchMultipleProjectsOwners.
|
|
|
|
func FetchProjectOwners(
|
|
|
|
ctx context.Context,
|
|
|
|
dbConn db.ConnOrTx,
|
|
|
|
projectId int,
|
|
|
|
) ([]*models.User, error) {
|
|
|
|
perf := ExtractPerf(ctx)
|
|
|
|
perf.StartBlock("SQL", "Fetch owners for project")
|
|
|
|
defer perf.EndBlock()
|
|
|
|
|
|
|
|
projectOwners, err := FetchMultipleProjectsOwners(ctx, dbConn, []int{projectId})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return projectOwners[0].Owners, nil
|
2021-07-23 03:09:46 +00:00
|
|
|
}
|
2021-11-08 19:16:54 +00:00
|
|
|
|
2021-11-10 04:11:39 +00:00
|
|
|
func UrlContextForProject(p *models.Project) *hmnurl.UrlContext {
|
|
|
|
return &hmnurl.UrlContext{
|
|
|
|
PersonalProject: p.Personal,
|
|
|
|
ProjectID: p.ID,
|
|
|
|
ProjectSlug: p.Slug,
|
|
|
|
ProjectName: p.Name,
|
2021-11-08 19:16:54 +00:00
|
|
|
}
|
|
|
|
}
|
2021-12-08 03:37:52 +00:00
|
|
|
|
|
|
|
func SetProjectTag(
|
|
|
|
ctx context.Context,
|
|
|
|
dbConn db.ConnOrTx,
|
|
|
|
projectID int,
|
|
|
|
tagText string,
|
|
|
|
) (*models.Tag, error) {
|
|
|
|
tx, err := dbConn.Begin(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, oops.New(err, "failed to start transaction")
|
|
|
|
}
|
|
|
|
defer tx.Rollback(ctx)
|
|
|
|
|
|
|
|
p, err := FetchProject(ctx, tx, nil, projectID, ProjectsQuery{
|
|
|
|
IncludeHidden: true,
|
|
|
|
Lifecycles: models.AllProjectLifecycles,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var resultTag *models.Tag
|
|
|
|
if tagText == "" {
|
|
|
|
// Once a project's tag is set, it cannot be unset. Return the existing tag.
|
|
|
|
resultTag = p.Tag
|
|
|
|
} else if p.Project.TagID == nil {
|
|
|
|
// Create a tag
|
|
|
|
itag, err := db.QueryOne(ctx, tx, models.Tag{},
|
|
|
|
`
|
|
|
|
INSERT INTO tags (text) VALUES ($1)
|
|
|
|
RETURNING $columns
|
|
|
|
`,
|
|
|
|
tagText,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, oops.New(err, "failed to create new tag for project")
|
|
|
|
}
|
|
|
|
resultTag = itag.(*models.Tag)
|
|
|
|
|
|
|
|
// Attach it to the project
|
|
|
|
_, err = tx.Exec(ctx,
|
|
|
|
`
|
|
|
|
UPDATE handmade_project
|
|
|
|
SET tag = $1
|
|
|
|
WHERE id = $2
|
|
|
|
`,
|
|
|
|
resultTag.ID, projectID,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, oops.New(err, "failed to attach new tag to project")
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Update the text of an existing one
|
|
|
|
itag, err := db.QueryOne(ctx, tx, models.Tag{},
|
|
|
|
`
|
|
|
|
UPDATE tags
|
|
|
|
SET text = $1
|
|
|
|
WHERE id = (SELECT tag FROM handmade_project WHERE id = $2)
|
|
|
|
RETURNING $columns
|
|
|
|
`,
|
|
|
|
tagText, projectID,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, oops.New(err, "failed to update existing tag")
|
|
|
|
}
|
|
|
|
resultTag = itag.(*models.Tag)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = tx.Commit(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, oops.New(err, "failed to commit transaction")
|
|
|
|
}
|
|
|
|
|
|
|
|
return resultTag, nil
|
|
|
|
}
|