Rework project visibility
This commit is contained in:
		
							parent
							
								
									7cb6869fcb
								
							
						
					
					
						commit
						415ce8db43
					
				|  | @ -47,8 +47,8 @@ func addCreateProjectCommand(projectCommand *cobra.Command) { | ||||||
| 			defer tx.Rollback(ctx) | 			defer tx.Rollback(ctx) | ||||||
| 
 | 
 | ||||||
| 			p, err := hmndata.FetchProject(ctx, tx, nil, models.HMNProjectID, hmndata.ProjectsQuery{ | 			p, err := hmndata.FetchProject(ctx, tx, nil, models.HMNProjectID, hmndata.ProjectsQuery{ | ||||||
| 				IncludeHidden: true, |  | ||||||
| 				Lifecycles:    models.AllProjectLifecycles, | 				Lifecycles:    models.AllProjectLifecycles, | ||||||
|  | 				IncludeHidden: true, | ||||||
| 			}) | 			}) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				panic(err) | 				panic(err) | ||||||
|  |  | ||||||
|  | @ -20,8 +20,9 @@ const ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type ProjectsQuery struct { | type ProjectsQuery struct { | ||||||
| 	// Available on all project queries
 | 	// Available on all project queries. By default, you will get projects that
 | ||||||
| 	Lifecycles    []models.ProjectLifecycle // if empty, defaults to models.VisibleProjectLifecycles
 | 	// are generally visible to all users.
 | ||||||
|  | 	Lifecycles    []models.ProjectLifecycle // If empty, defaults to visible lifecycles. Do not conflate this with permissions; those are checked separately.
 | ||||||
| 	Types         ProjectTypeQuery          // bitfield
 | 	Types         ProjectTypeQuery          // bitfield
 | ||||||
| 	IncludeHidden bool | 	IncludeHidden bool | ||||||
| 
 | 
 | ||||||
|  | @ -65,11 +66,6 @@ func FetchProjects( | ||||||
| 	perf.StartBlock("SQL", "Fetch projects") | 	perf.StartBlock("SQL", "Fetch projects") | ||||||
| 	defer perf.EndBlock() | 	defer perf.EndBlock() | ||||||
| 
 | 
 | ||||||
| 	var currentUserID *int |  | ||||||
| 	if currentUser != nil { |  | ||||||
| 		currentUserID = ¤tUser.ID |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	tx, err := dbConn.Begin(ctx) | 	tx, err := dbConn.Begin(ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, oops.New(err, "failed to start transaction") | 		return nil, oops.New(err, "failed to start transaction") | ||||||
|  | @ -80,11 +76,9 @@ func FetchProjects( | ||||||
| 		Project        models.Project `db:"project"` | 		Project        models.Project `db:"project"` | ||||||
| 		LogoLightAsset *models.Asset  `db:"logolight_asset"` | 		LogoLightAsset *models.Asset  `db:"logolight_asset"` | ||||||
| 		LogoDarkAsset  *models.Asset  `db:"logodark_asset"` | 		LogoDarkAsset  *models.Asset  `db:"logodark_asset"` | ||||||
|  | 		Tag            *models.Tag    `db:"tags"` | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// If true, join against the project owners table and check visibility permissions
 |  | ||||||
| 	checkOwnerVisibility := q.IncludeHidden && currentUser != nil |  | ||||||
| 
 |  | ||||||
| 	// Fetch all valid projects (not yet subject to user permission checks)
 | 	// Fetch all valid projects (not yet subject to user permission checks)
 | ||||||
| 	var qb db.QueryBuilder | 	var qb db.QueryBuilder | ||||||
| 	if len(q.OrderBy) > 0 { | 	if len(q.OrderBy) > 0 { | ||||||
|  | @ -96,31 +90,31 @@ func FetchProjects( | ||||||
| 			handmade_project AS project | 			handmade_project AS project | ||||||
| 			LEFT JOIN handmade_asset AS logolight_asset ON logolight_asset.id = project.logolight_asset_id | 			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 | 			LEFT JOIN handmade_asset AS logodark_asset ON logodark_asset.id = project.logodark_asset_id | ||||||
|  | 			LEFT JOIN tags ON project.tag = tags.id | ||||||
| 	`) | 	`) | ||||||
| 	if len(q.OwnerIDs) > 0 { | 	if len(q.OwnerIDs) > 0 { | ||||||
| 		qb.Add(` | 		qb.Add( | ||||||
| 			INNER JOIN handmade_user_projects AS owner_filter ON owner_filter.project_id = project.id | 			` | ||||||
| 		`) | 			JOIN ( | ||||||
| 	} | 				SELECT project_id, array_agg(user_id) AS owner_ids | ||||||
| 	if checkOwnerVisibility { | 				FROM handmade_user_projects | ||||||
| 		qb.Add(` | 				WHERE user_id = ANY ($?) | ||||||
| 			LEFT JOIN handmade_user_projects AS owner_visibility ON owner_visibility.project_id = project.id | 				GROUP BY project_id | ||||||
| 		`) | 			) AS owner_filter ON project.id = owner_filter.project_id | ||||||
|  | 			`, | ||||||
|  | 			q.OwnerIDs, | ||||||
|  | 		) | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	// Filters (permissions are checked after the query, in Go)
 | ||||||
| 	qb.Add(` | 	qb.Add(` | ||||||
| 		WHERE | 		WHERE | ||||||
| 			TRUE | 			TRUE | ||||||
| 	`) | 	`) | ||||||
| 
 | 	if len(q.Lifecycles) > 0 { | ||||||
| 	// Filters
 | 		qb.Add(`AND project.lifecycle = ANY ($?)`, q.Lifecycles) | ||||||
| 	if len(q.ProjectIDs) > 0 { | 	} else { | ||||||
| 		qb.Add(`AND project.id = ANY ($?)`, q.ProjectIDs) | 		qb.Add(`AND project.lifecycle = ANY ($?)`, models.VisibleProjectLifecycles) | ||||||
| 	} |  | ||||||
| 	if len(q.Slugs) > 0 { |  | ||||||
| 		qb.Add(`AND (project.slug != '' AND project.slug = ANY ($?))`, q.Slugs) |  | ||||||
| 	} |  | ||||||
| 	if len(q.OwnerIDs) > 0 { |  | ||||||
| 		qb.Add(`AND (owner_filter.user_id = ANY ($?))`, q.OwnerIDs) |  | ||||||
| 	} | 	} | ||||||
| 	if q.Types != 0 { | 	if q.Types != 0 { | ||||||
| 		qb.Add(`AND (FALSE`) | 		qb.Add(`AND (FALSE`) | ||||||
|  | @ -132,21 +126,14 @@ func FetchProjects( | ||||||
| 		} | 		} | ||||||
| 		qb.Add(`)`) | 		qb.Add(`)`) | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	// Visibility
 |  | ||||||
| 	if checkOwnerVisibility { |  | ||||||
| 		qb.Add(`AND ($? OR owner_visibility.user_id = $? OR (TRUE`, currentUser.IsStaff, currentUser.ID) |  | ||||||
| 	} |  | ||||||
| 	if !q.IncludeHidden { | 	if !q.IncludeHidden { | ||||||
| 		qb.Add(`AND NOT hidden`) | 		qb.Add(`AND NOT project.hidden`) | ||||||
| 	} | 	} | ||||||
| 	if len(q.Lifecycles) > 0 { | 	if len(q.ProjectIDs) > 0 { | ||||||
| 		qb.Add(`AND project.lifecycle = ANY($?)`, q.Lifecycles) | 		qb.Add(`AND project.id = ANY ($?)`, q.ProjectIDs) | ||||||
| 	} else { |  | ||||||
| 		qb.Add(`AND project.lifecycle = ANY($?)`, models.VisibleProjectLifecycles) |  | ||||||
| 	} | 	} | ||||||
| 	if checkOwnerVisibility { | 	if len(q.Slugs) > 0 { | ||||||
| 		qb.Add(`))`) | 		qb.Add(`AND (project.slug != '' AND project.slug = ANY ($?))`, q.Slugs) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Output
 | 	// Output
 | ||||||
|  | @ -164,21 +151,6 @@ func FetchProjects( | ||||||
| 	} | 	} | ||||||
| 	iprojects := itProjects.ToSlice() | 	iprojects := itProjects.ToSlice() | ||||||
| 
 | 
 | ||||||
| 	// 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 |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Fetch project owners to do permission checks
 | 	// Fetch project owners to do permission checks
 | ||||||
| 	projectIds := make([]int, len(iprojects)) | 	projectIds := make([]int, len(iprojects)) | ||||||
| 	for i, iproject := range iprojects { | 	for i, iproject := range iprojects { | ||||||
|  | @ -195,49 +167,55 @@ func FetchProjects( | ||||||
| 		owners := projectOwners[i].Owners | 		owners := projectOwners[i].Owners | ||||||
| 
 | 
 | ||||||
| 		/* | 		/* | ||||||
| 			Per our spec, a user can see a project if: | 			Here's the rundown on project permissions: | ||||||
|  | 
 | ||||||
|  | 			- In general, users can only see projects that are Generally Visible. | ||||||
|  | 			- As an exception, users can always see projects that they own. | ||||||
|  | 			- As an exception, staff can always see every project. | ||||||
|  | 
 | ||||||
|  | 			A project is Generally Visible if all the following conditions are true: | ||||||
|  | 			- The project has a "visible" lifecycle (per models.VisibleProjectLifecycles) | ||||||
|  | 			- The project is not hidden | ||||||
|  | 			- One of the following is true: | ||||||
| 				- The project is official | 				- The project is official | ||||||
| 				- The project is personal and all of the project's owners are approved | 				- 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) | 
 | ||||||
|  | 			As an exception, the HMN project is always generally visible. | ||||||
| 
 | 
 | ||||||
| 			See https://www.notion.so/handmade-network/Technical-Plan-a11aaa9ea2f14d9a95f7d7780edd789c
 | 			See https://www.notion.so/handmade-network/Technical-Plan-a11aaa9ea2f14d9a95f7d7780edd789c
 | ||||||
| 		*/ | 		*/ | ||||||
| 
 | 
 | ||||||
| 		var projectVisible bool | 		currentUserIsOwner := false | ||||||
| 		if row.Project.Personal { |  | ||||||
| 		allOwnersApproved := true | 		allOwnersApproved := true | ||||||
| 		for _, owner := range owners { | 		for _, owner := range owners { | ||||||
| 			if owner.Status != models.UserStatusApproved { | 			if owner.Status != models.UserStatusApproved { | ||||||
| 				allOwnersApproved = false | 				allOwnersApproved = false | ||||||
| 			} | 			} | ||||||
| 				if currentUserID != nil && *currentUserID == owner.ID { | 			if currentUser != nil && owner.ID == currentUser.ID { | ||||||
| 					projectVisible = true | 				currentUserIsOwner = true | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 			if allOwnersApproved { | 
 | ||||||
| 				projectVisible = true | 		projectGenerallyVisible := true && | ||||||
| 			} | 			row.Project.Lifecycle.In(models.VisibleProjectLifecycles) && | ||||||
| 		} else { | 			!row.Project.Hidden && | ||||||
| 			projectVisible = true | 			(!row.Project.Personal || allOwnersApproved || row.Project.IsHMN()) | ||||||
|  | 		if row.Project.IsHMN() { | ||||||
|  | 			projectGenerallyVisible = true // hard override
 | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		projectVisible := false || | ||||||
|  | 			projectGenerallyVisible || | ||||||
|  | 			currentUserIsOwner || | ||||||
|  | 			(currentUser != nil && currentUser.IsStaff) | ||||||
|  | 
 | ||||||
| 		if projectVisible { | 		if projectVisible { | ||||||
| 			var projectTag *models.Tag |  | ||||||
| 			if row.Project.TagID != nil { |  | ||||||
| 				for _, tag := range tags { |  | ||||||
| 					if tag.ID == *row.Project.TagID { |  | ||||||
| 						projectTag = tag |  | ||||||
| 						break |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			res = append(res, ProjectAndStuff{ | 			res = append(res, ProjectAndStuff{ | ||||||
| 				Project:        row.Project, | 				Project:        row.Project, | ||||||
| 				LogoLightAsset: row.LogoLightAsset, | 				LogoLightAsset: row.LogoLightAsset, | ||||||
| 				LogoDarkAsset:  row.LogoDarkAsset, | 				LogoDarkAsset:  row.LogoDarkAsset, | ||||||
| 				Owners:         owners, | 				Owners:         owners, | ||||||
| 				Tag:            projectTag, | 				Tag:            row.Tag, | ||||||
| 			}) | 			}) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | @ -485,8 +463,8 @@ func SetProjectTag( | ||||||
| 	defer tx.Rollback(ctx) | 	defer tx.Rollback(ctx) | ||||||
| 
 | 
 | ||||||
| 	p, err := FetchProject(ctx, tx, nil, projectID, ProjectsQuery{ | 	p, err := FetchProject(ctx, tx, nil, projectID, ProjectsQuery{ | ||||||
| 		IncludeHidden: true, |  | ||||||
| 		Lifecycles:    models.AllProjectLifecycles, | 		Lifecycles:    models.AllProjectLifecycles, | ||||||
|  | 		IncludeHidden: true, | ||||||
| 	}) | 	}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
|  |  | ||||||
|  | @ -18,7 +18,7 @@ type TagQuery struct { | ||||||
| 
 | 
 | ||||||
| func FetchTags(ctx context.Context, dbConn db.ConnOrTx, q TagQuery) ([]*models.Tag, error) { | func FetchTags(ctx context.Context, dbConn db.ConnOrTx, q TagQuery) ([]*models.Tag, error) { | ||||||
| 	perf := perf.ExtractPerf(ctx) | 	perf := perf.ExtractPerf(ctx) | ||||||
| 	perf.StartBlock("SQL", "Fetch snippets") | 	perf.StartBlock("SQL", "Fetch tags") | ||||||
| 	defer perf.EndBlock() | 	defer perf.EndBlock() | ||||||
| 
 | 
 | ||||||
| 	var qb db.QueryBuilder | 	var qb db.QueryBuilder | ||||||
|  |  | ||||||
|  | @ -36,7 +36,7 @@ var AllProjectLifecycles = []ProjectLifecycle{ | ||||||
| 	ProjectLifecycleLTS, | 	ProjectLifecycleLTS, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NOTE(asaf): Just checking the lifecycle is not sufficient. Visible projects also must have flags = 0.
 | // NOTE(asaf): Just checking the lifecycle is not sufficient. Visible projects also must not be hidden.
 | ||||||
| var VisibleProjectLifecycles = []ProjectLifecycle{ | var VisibleProjectLifecycles = []ProjectLifecycle{ | ||||||
| 	ProjectLifecycleActive, | 	ProjectLifecycleActive, | ||||||
| 	ProjectLifecycleHiatus, | 	ProjectLifecycleHiatus, | ||||||
|  | @ -44,6 +44,15 @@ var VisibleProjectLifecycles = []ProjectLifecycle{ | ||||||
| 	ProjectLifecycleLTS, | 	ProjectLifecycleLTS, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (lc ProjectLifecycle) In(lcs []ProjectLifecycle) bool { | ||||||
|  | 	for _, v := range lcs { | ||||||
|  | 		if lc == v { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  | 
 | ||||||
| const RecentProjectUpdateTimespanSec = 60 * 60 * 24 * 28 // NOTE(asaf): Four weeks
 | const RecentProjectUpdateTimespanSec = 60 * 60 * 24 * 28 // NOTE(asaf): Four weeks
 | ||||||
| 
 | 
 | ||||||
| type Project struct { | type Project struct { | ||||||
|  |  | ||||||
|  | @ -157,7 +157,6 @@ func AtomFeed(c *RequestContext) ResponseData { | ||||||
| 				itemsPerFeed = 100000 | 				itemsPerFeed = 100000 | ||||||
| 			} | 			} | ||||||
| 			projectsAndStuff, err := hmndata.FetchProjects(c.Context(), c.Conn, nil, hmndata.ProjectsQuery{ | 			projectsAndStuff, err := hmndata.FetchProjects(c.Context(), c.Conn, nil, hmndata.ProjectsQuery{ | ||||||
| 				Lifecycles: models.VisibleProjectLifecycles, |  | ||||||
| 				Limit:   itemsPerFeed, | 				Limit:   itemsPerFeed, | ||||||
| 				Types:   hmndata.OfficialProjects, | 				Types:   hmndata.OfficialProjects, | ||||||
| 				OrderBy: "date_approved DESC", | 				OrderBy: "date_approved DESC", | ||||||
|  |  | ||||||
|  | @ -265,7 +265,10 @@ func ProjectHomepage(c *RequestContext) ResponseData { | ||||||
| 		Value:    c.CurrentProject.Blurb, | 		Value:    c.CurrentProject.Blurb, | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	p, err := hmndata.FetchProject(c.Context(), c.Conn, c.CurrentUser, c.CurrentProject.ID, hmndata.ProjectsQuery{}) | 	p, err := hmndata.FetchProject(c.Context(), c.Conn, c.CurrentUser, c.CurrentProject.ID, hmndata.ProjectsQuery{ | ||||||
|  | 		Lifecycles:    models.AllProjectLifecycles, | ||||||
|  | 		IncludeHidden: true, | ||||||
|  | 	}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch project details")) | 		return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch project details")) | ||||||
| 	} | 	} | ||||||
|  | @ -491,16 +494,16 @@ func ProjectEdit(c *RequestContext) ResponseData { | ||||||
| 		c.Context(), c.Conn, | 		c.Context(), c.Conn, | ||||||
| 		c.CurrentUser, c.CurrentProject.ID, | 		c.CurrentUser, c.CurrentProject.ID, | ||||||
| 		hmndata.ProjectsQuery{ | 		hmndata.ProjectsQuery{ | ||||||
|  | 			Lifecycles:    models.AllProjectLifecycles, | ||||||
| 			IncludeHidden: true, | 			IncludeHidden: true, | ||||||
| 		}, | 		}, | ||||||
| 	) | 	) | ||||||
| 	owners, err := hmndata.FetchProjectOwners(c.Context(), c.Conn, p.Project.ID) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to fetch project owners")) | 		return c.ErrorResponse(http.StatusInternalServerError, err) | ||||||
| 	} | 	} | ||||||
| 	projectSettings := templates.ProjectToProjectSettings( | 	projectSettings := templates.ProjectToProjectSettings( | ||||||
| 		&p.Project, | 		&p.Project, | ||||||
| 		owners, | 		p.Owners, | ||||||
| 		p.TagText(), | 		p.TagText(), | ||||||
| 		p.LogoURL("light"), p.LogoURL("dark"), | 		p.LogoURL("light"), p.LogoURL("dark"), | ||||||
| 		c.Theme, | 		c.Theme, | ||||||
|  |  | ||||||
|  | @ -300,7 +300,10 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool) ht | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					panic(oops.New(err, "project id was not numeric (bad regex in routing)")) | 					panic(oops.New(err, "project id was not numeric (bad regex in routing)")) | ||||||
| 				} | 				} | ||||||
| 				p, err := hmndata.FetchProject(c.Context(), c.Conn, c.CurrentUser, id, hmndata.ProjectsQuery{}) | 				p, err := hmndata.FetchProject(c.Context(), c.Conn, c.CurrentUser, id, hmndata.ProjectsQuery{ | ||||||
|  | 					Lifecycles:    models.AllProjectLifecycles, | ||||||
|  | 					IncludeHidden: true, | ||||||
|  | 				}) | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					if errors.Is(err, db.NotFound) { | 					if errors.Is(err, db.NotFound) { | ||||||
| 						return FourOhFour(c) | 						return FourOhFour(c) | ||||||
|  | @ -465,7 +468,10 @@ func LoadCommonWebsiteData(c *RequestContext) (bool, ResponseData) { | ||||||
| 		var owners []*models.User | 		var owners []*models.User | ||||||
| 
 | 
 | ||||||
| 		if len(slug) > 0 { | 		if len(slug) > 0 { | ||||||
| 			dbProject, err := hmndata.FetchProjectBySlug(c.Context(), c.Conn, c.CurrentUser, slug, hmndata.ProjectsQuery{}) | 			dbProject, err := hmndata.FetchProjectBySlug(c.Context(), c.Conn, c.CurrentUser, slug, hmndata.ProjectsQuery{ | ||||||
|  | 				Lifecycles:    models.AllProjectLifecycles, | ||||||
|  | 				IncludeHidden: true, | ||||||
|  | 			}) | ||||||
| 			if err == nil { | 			if err == nil { | ||||||
| 				c.CurrentProject = &dbProject.Project | 				c.CurrentProject = &dbProject.Project | ||||||
| 				owners = dbProject.Owners | 				owners = dbProject.Owners | ||||||
|  | @ -480,6 +486,7 @@ func LoadCommonWebsiteData(c *RequestContext) (bool, ResponseData) { | ||||||
| 
 | 
 | ||||||
| 		if c.CurrentProject == nil { | 		if c.CurrentProject == nil { | ||||||
| 			dbProject, err := hmndata.FetchProject(c.Context(), c.Conn, c.CurrentUser, models.HMNProjectID, hmndata.ProjectsQuery{ | 			dbProject, err := hmndata.FetchProject(c.Context(), c.Conn, c.CurrentUser, models.HMNProjectID, hmndata.ProjectsQuery{ | ||||||
|  | 				Lifecycles:    models.AllProjectLifecycles, | ||||||
| 				IncludeHidden: true, | 				IncludeHidden: true, | ||||||
| 			}) | 			}) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
|  |  | ||||||
|  | @ -102,13 +102,12 @@ func UserProfile(c *RequestContext) ResponseData { | ||||||
| 	} | 	} | ||||||
| 	c.Perf.EndBlock() | 	c.Perf.EndBlock() | ||||||
| 
 | 
 | ||||||
| 	projectsQuery := hmndata.ProjectsQuery{ | 	projectsAndStuff, err := hmndata.FetchProjects(c.Context(), c.Conn, c.CurrentUser, hmndata.ProjectsQuery{ | ||||||
| 		OwnerIDs:      []int{profileUser.ID}, | 		OwnerIDs:      []int{profileUser.ID}, | ||||||
| 		Lifecycles: models.VisibleProjectLifecycles, | 		Lifecycles:    models.AllProjectLifecycles, | ||||||
|  | 		IncludeHidden: true, | ||||||
| 		OrderBy:       "all_last_updated DESC", | 		OrderBy:       "all_last_updated DESC", | ||||||
| 	} | 	}) | ||||||
| 
 |  | ||||||
| 	projectsAndStuff, err := hmndata.FetchProjects(c.Context(), c.Conn, c.CurrentUser, projectsQuery) |  | ||||||
| 	templateProjects := make([]templates.Project, 0, len(projectsAndStuff)) | 	templateProjects := make([]templates.Project, 0, len(projectsAndStuff)) | ||||||
| 	numPersonalProjects := 0 | 	numPersonalProjects := 0 | ||||||
| 	for _, p := range projectsAndStuff { | 	for _, p := range projectsAndStuff { | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue