2021-06-06 23:48:43 +00:00
package website
import (
2021-12-04 14:55:45 +00:00
"context"
2021-12-02 10:53:36 +00:00
"errors"
2021-07-08 07:40:30 +00:00
"fmt"
2021-12-02 10:53:36 +00:00
"image"
"io"
2021-06-06 23:48:43 +00:00
"math"
"math/rand"
"net/http"
2021-12-29 14:38:23 +00:00
"path"
2021-11-11 20:00:36 +00:00
"sort"
2021-12-02 10:53:36 +00:00
"strings"
2021-06-06 23:48:43 +00:00
"time"
2021-12-02 10:53:36 +00:00
"git.handmade.network/hmn/hmn/src/assets"
2021-06-06 23:48:43 +00:00
"git.handmade.network/hmn/hmn/src/db"
2021-12-09 02:04:15 +00:00
"git.handmade.network/hmn/hmn/src/hmndata"
2021-06-06 23:48:43 +00:00
"git.handmade.network/hmn/hmn/src/hmnurl"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
2021-12-02 10:53:36 +00:00
"git.handmade.network/hmn/hmn/src/parsing"
2021-06-06 23:48:43 +00:00
"git.handmade.network/hmn/hmn/src/templates"
2022-03-22 18:07:43 +00:00
"git.handmade.network/hmn/hmn/src/twitch"
2021-06-06 23:48:43 +00:00
"git.handmade.network/hmn/hmn/src/utils"
2021-12-02 10:53:36 +00:00
"github.com/google/uuid"
2021-12-08 03:37:52 +00:00
"github.com/jackc/pgx/v4"
2022-06-24 21:38:11 +00:00
"github.com/teacat/noire"
2021-06-06 23:48:43 +00:00
)
2021-12-09 04:02:11 +00:00
const maxPersonalProjects = 5
2021-12-09 04:23:20 +00:00
const maxProjectOwners = 5
2021-12-09 04:02:11 +00:00
2022-06-24 21:38:11 +00:00
func ProjectCSS ( c * RequestContext ) ResponseData {
color := c . URL ( ) . Query ( ) . Get ( "color" )
if color == "" {
return c . ErrorResponse ( http . StatusBadRequest , NewSafeError ( nil , "You must provide a 'color' parameter.\n" ) )
}
baseData := getBaseData ( c , "" , nil )
bgColor := noire . NewHex ( color )
h , s , l := bgColor . HSL ( )
if baseData . Theme == "dark" {
l = 15
} else {
l = 95
}
if s > 20 {
s = 20
}
bgColor = noire . NewHSL ( h , s , l )
templateData := struct {
templates . BaseData
Color string
PostBgColor string
} {
BaseData : baseData ,
Color : color ,
PostBgColor : bgColor . HTML ( ) ,
}
var res ResponseData
res . Header ( ) . Add ( "Content-Type" , "text/css" )
err := res . WriteTemplate ( "project.css" , templateData , c . Perf )
if err != nil {
return c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to generate project CSS" ) )
}
return res
}
2021-06-06 23:48:43 +00:00
type ProjectTemplateData struct {
templates . BaseData
Pagination templates . Pagination
CarouselProjects [ ] templates . Project
Projects [ ] templates . Project
2021-11-06 20:25:31 +00:00
PersonalProjects [ ] templates . Project
2021-06-06 23:48:43 +00:00
ProjectAtomFeedUrl string
2021-09-05 19:57:10 +00:00
WIPForumUrl string
2021-06-06 23:48:43 +00:00
}
func ProjectIndex ( c * RequestContext ) ResponseData {
const projectsPerPage = 20
const maxCarouselProjects = 10
2021-11-06 20:25:31 +00:00
const maxPersonalProjects = 10
2021-06-06 23:48:43 +00:00
2022-06-24 21:38:11 +00:00
officialProjects , err := hmndata . FetchProjects ( c , c . Conn , c . CurrentUser , hmndata . ProjectsQuery {
2021-12-09 02:04:15 +00:00
Types : hmndata . OfficialProjects ,
2021-11-06 20:25:31 +00:00
} )
2021-06-06 23:48:43 +00:00
if err != nil {
2021-08-28 12:21:03 +00:00
return c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to fetch projects" ) )
2021-06-06 23:48:43 +00:00
}
2021-11-06 20:25:31 +00:00
numPages := int ( math . Ceil ( float64 ( len ( officialProjects ) ) / projectsPerPage ) )
page , numPages , ok := getPageInfo ( c . PathParams [ "page" ] , len ( officialProjects ) , feedPostsPerPage )
if ! ok {
return c . Redirect ( hmnurl . BuildProjectIndex ( 1 ) , http . StatusSeeOther )
2021-06-06 23:48:43 +00:00
}
pagination := templates . Pagination {
Current : page ,
Total : numPages ,
FirstUrl : hmnurl . BuildProjectIndex ( 1 ) ,
LastUrl : hmnurl . BuildProjectIndex ( numPages ) ,
NextUrl : hmnurl . BuildProjectIndex ( utils . IntClamp ( 1 , page + 1 , numPages ) ) ,
PreviousUrl : hmnurl . BuildProjectIndex ( utils . IntClamp ( 1 , page - 1 , numPages ) ) ,
}
c . Perf . StartBlock ( "PROJECTS" , "Grouping and sorting" )
var handmadeHero * templates . Project
var featuredProjects [ ] templates . Project
var recentProjects [ ] templates . Project
var restProjects [ ] templates . Project
now := time . Now ( )
2021-11-06 20:25:31 +00:00
for _ , p := range officialProjects {
2021-12-29 14:38:23 +00:00
templateProject := templates . ProjectAndStuffToTemplate ( & p , hmndata . UrlContextForProject ( & p . Project ) . BuildHomepage ( ) , c . Theme )
2021-12-08 03:37:52 +00:00
2021-11-06 20:25:31 +00:00
if p . Project . Slug == "hero" {
2021-06-06 23:48:43 +00:00
// NOTE(asaf): Handmade Hero gets special treatment. Must always be first in the list.
handmadeHero = & templateProject
continue
}
2021-11-06 20:25:31 +00:00
if p . Project . Featured {
2021-06-06 23:48:43 +00:00
featuredProjects = append ( featuredProjects , templateProject )
2021-11-06 20:25:31 +00:00
} else if now . Sub ( p . Project . AllLastUpdated ) . Seconds ( ) < models . RecentProjectUpdateTimespanSec {
2021-06-06 23:48:43 +00:00
recentProjects = append ( recentProjects , templateProject )
} else {
restProjects = append ( restProjects , templateProject )
}
}
_ , randSeed := now . ISOWeek ( )
random := rand . New ( rand . NewSource ( int64 ( randSeed ) ) )
random . Shuffle ( len ( featuredProjects ) , func ( i , j int ) { featuredProjects [ i ] , featuredProjects [ j ] = featuredProjects [ j ] , featuredProjects [ i ] } )
random . Shuffle ( len ( recentProjects ) , func ( i , j int ) { recentProjects [ i ] , recentProjects [ j ] = recentProjects [ j ] , recentProjects [ i ] } )
random . Shuffle ( len ( restProjects ) , func ( i , j int ) { restProjects [ i ] , restProjects [ j ] = restProjects [ j ] , restProjects [ i ] } )
if handmadeHero != nil {
// NOTE(asaf): As mentioned above, inserting HMH first.
featuredProjects = append ( [ ] templates . Project { * handmadeHero } , featuredProjects ... )
}
orderedProjects := make ( [ ] templates . Project , 0 , len ( featuredProjects ) + len ( recentProjects ) + len ( restProjects ) )
orderedProjects = append ( orderedProjects , featuredProjects ... )
orderedProjects = append ( orderedProjects , recentProjects ... )
orderedProjects = append ( orderedProjects , restProjects ... )
firstProjectIndex := ( page - 1 ) * projectsPerPage
endIndex := utils . IntMin ( firstProjectIndex + projectsPerPage , len ( orderedProjects ) )
pageProjects := orderedProjects [ firstProjectIndex : endIndex ]
var carouselProjects [ ] templates . Project
if page == 1 {
carouselProjects = featuredProjects [ : utils . IntMin ( len ( featuredProjects ) , maxCarouselProjects ) ]
}
c . Perf . EndBlock ( )
2021-11-06 20:25:31 +00:00
// Fetch and highlight a random selection of personal projects
var personalProjects [ ] templates . Project
{
2022-06-24 21:38:11 +00:00
projects , err := hmndata . FetchProjects ( c , c . Conn , c . CurrentUser , hmndata . ProjectsQuery {
2021-12-09 02:04:15 +00:00
Types : hmndata . PersonalProjects ,
2021-11-06 20:25:31 +00:00
} )
if err != nil {
return c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to fetch personal projects" ) )
}
2021-11-12 00:35:00 +00:00
sort . Slice ( projects , func ( i , j int ) bool {
p1 := projects [ i ] . Project
p2 := projects [ j ] . Project
return p2 . AllLastUpdated . Before ( p1 . AllLastUpdated ) // sort backwards - recent first
} )
2021-11-06 20:25:31 +00:00
for i , p := range projects {
if i >= maxPersonalProjects {
break
}
2021-12-29 14:38:23 +00:00
templateProject := templates . ProjectAndStuffToTemplate ( & p , hmndata . UrlContextForProject ( & p . Project ) . BuildHomepage ( ) , c . Theme )
2021-12-08 03:37:52 +00:00
personalProjects = append ( personalProjects , templateProject )
2021-11-06 20:25:31 +00:00
}
}
2021-09-09 02:51:43 +00:00
baseData := getBaseDataAutocrumb ( c , "Projects" )
2021-06-06 23:48:43 +00:00
var res ResponseData
2021-07-17 15:19:17 +00:00
res . MustWriteTemplate ( "project_index.html" , ProjectTemplateData {
2021-06-06 23:48:43 +00:00
BaseData : baseData ,
Pagination : pagination ,
CarouselProjects : carouselProjects ,
Projects : pageProjects ,
2021-11-06 20:25:31 +00:00
PersonalProjects : personalProjects ,
2021-06-06 23:48:43 +00:00
ProjectAtomFeedUrl : hmnurl . BuildAtomFeedForProjects ( ) ,
2021-11-10 04:11:39 +00:00
WIPForumUrl : hmnurl . HMNProjectContext . BuildForum ( [ ] string { "wip" } , 1 ) ,
2021-06-06 23:48:43 +00:00
} , c . Perf )
return res
}
2021-07-08 07:40:30 +00:00
type ProjectHomepageData struct {
templates . BaseData
Project templates . Project
Owners [ ] templates . User
Screenshots [ ] string
ProjectLinks [ ] templates . Link
Licenses [ ] templates . Link
RecentActivity [ ] templates . TimelineItem
2022-08-05 04:03:45 +00:00
SnippetEdit templates . SnippetEdit
2021-07-08 07:40:30 +00:00
}
func ProjectHomepage ( c * RequestContext ) ResponseData {
maxRecentActivity := 15
2021-11-09 19:14:38 +00:00
if c . CurrentProject == nil {
2021-07-08 07:40:30 +00:00
return FourOhFour ( c )
}
2021-11-08 19:16:54 +00:00
// There are no further permission checks to do, because permissions are
// checked whatever way we fetch the project.
2022-06-24 21:38:11 +00:00
owners , err := hmndata . FetchProjectOwners ( c , c . Conn , c . CurrentProject . ID )
2021-07-08 07:40:30 +00:00
if err != nil {
2021-08-28 12:21:03 +00:00
return c . ErrorResponse ( http . StatusInternalServerError , err )
2021-07-08 07:40:30 +00:00
}
c . Perf . StartBlock ( "SQL" , "Fetching screenshots" )
2022-06-24 21:38:11 +00:00
screenshotFilenames , err := db . QueryScalar [ string ] ( c , c . Conn ,
2021-07-08 07:40:30 +00:00
`
2022-04-16 17:49:29 +00:00
SELECT screenshot . file
2021-07-08 07:40:30 +00:00
FROM
2022-05-07 13:11:05 +00:00
image_file AS screenshot
INNER JOIN project_screenshot ON screenshot . id = project_screenshot . imagefile_id
2021-07-08 07:40:30 +00:00
WHERE
2022-05-07 13:11:05 +00:00
project_screenshot . project_id = $ 1
2021-07-08 07:40:30 +00:00
` ,
2021-11-09 19:14:38 +00:00
c . CurrentProject . ID ,
2021-07-08 07:40:30 +00:00
)
if err != nil {
2021-08-28 12:21:03 +00:00
return c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to fetch screenshots for project" ) )
2021-07-08 07:40:30 +00:00
}
c . Perf . EndBlock ( )
c . Perf . StartBlock ( "SQL" , "Fetching project links" )
2022-06-24 21:38:11 +00:00
projectLinks , err := db . Query [ models . Link ] ( c , c . Conn ,
2021-07-08 07:40:30 +00:00
`
SELECT $ columns
FROM
2022-05-07 13:11:05 +00:00
link as link
2021-07-08 07:40:30 +00:00
WHERE
link . project_id = $ 1
ORDER BY link . ordering ASC
` ,
2021-11-09 19:14:38 +00:00
c . CurrentProject . ID ,
2021-07-08 07:40:30 +00:00
)
if err != nil {
2021-08-28 12:21:03 +00:00
return c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to fetch project links" ) )
2021-07-08 07:40:30 +00:00
}
c . Perf . EndBlock ( )
2021-07-30 03:40:47 +00:00
c . Perf . StartBlock ( "SQL" , "Fetch subforum tree" )
2022-06-24 21:38:11 +00:00
subforumTree := models . GetFullSubforumTree ( c , c . Conn )
2021-07-30 03:40:47 +00:00
lineageBuilder := models . MakeSubforumLineageBuilder ( subforumTree )
2021-07-08 07:40:30 +00:00
c . Perf . EndBlock ( )
c . Perf . StartBlock ( "SQL" , "Fetching project timeline" )
2022-06-24 21:38:11 +00:00
posts , err := hmndata . FetchPosts ( c , c . Conn , c . CurrentUser , hmndata . PostsQuery {
2022-06-14 16:14:38 +00:00
ProjectIDs : [ ] int { c . CurrentProject . ID } ,
Limit : maxRecentActivity ,
SortDescending : true ,
} )
2021-07-08 07:40:30 +00:00
c . Perf . EndBlock ( )
2021-11-11 20:00:36 +00:00
var templateData ProjectHomepageData
2021-07-08 07:40:30 +00:00
2021-11-11 20:00:36 +00:00
templateData . BaseData = getBaseData ( c , c . CurrentProject . Name , nil )
templateData . BaseData . OpenGraphItems = append ( templateData . BaseData . OpenGraphItems , templates . OpenGraphItem {
2021-09-09 02:51:43 +00:00
Property : "og:description" ,
2021-11-09 19:14:38 +00:00
Value : c . CurrentProject . Blurb ,
2021-09-09 02:51:43 +00:00
} )
2022-06-24 21:38:11 +00:00
p , err := hmndata . FetchProject ( c , c . Conn , c . CurrentUser , c . CurrentProject . ID , hmndata . ProjectsQuery {
2021-12-11 19:08:10 +00:00
Lifecycles : models . AllProjectLifecycles ,
IncludeHidden : true ,
} )
2021-12-08 03:37:52 +00:00
if err != nil {
return c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to fetch project details" ) )
}
2021-12-29 14:38:23 +00:00
templateData . Project = templates . ProjectAndStuffToTemplate ( & p , c . UrlContext . BuildHomepage ( ) , c . Theme )
2021-07-23 03:09:46 +00:00
for _ , owner := range owners {
2021-11-11 20:00:36 +00:00
templateData . Owners = append ( templateData . Owners , templates . UserToTemplate ( owner , c . Theme ) )
2021-07-08 07:40:30 +00:00
}
2021-11-09 19:14:38 +00:00
if c . CurrentProject . Hidden {
2021-11-11 20:00:36 +00:00
templateData . BaseData . AddImmediateNotice (
2021-08-17 05:18:04 +00:00
"hidden" ,
"NOTICE: This project is hidden. It is currently visible only to owners and site admins." ,
)
2021-07-08 07:40:30 +00:00
}
2021-11-09 19:14:38 +00:00
if c . CurrentProject . Lifecycle != models . ProjectLifecycleActive {
switch c . CurrentProject . Lifecycle {
2021-07-08 07:40:30 +00:00
case models . ProjectLifecycleUnapproved :
2021-11-11 20:00:36 +00:00
templateData . BaseData . AddImmediateNotice (
2021-08-17 05:18:04 +00:00
"unapproved" ,
fmt . Sprintf (
"NOTICE: This project has not yet been submitted for approval. It is only visible to owners. Please <a href=\"%s\">submit it for approval</a> when the project content is ready for review." ,
2021-11-10 04:11:39 +00:00
c . UrlContext . BuildProjectEdit ( "submit" ) ,
2021-08-17 05:18:04 +00:00
) ,
)
2021-07-08 07:40:30 +00:00
case models . ProjectLifecycleApprovalRequired :
2021-11-11 20:00:36 +00:00
templateData . BaseData . AddImmediateNotice (
2021-08-17 05:18:04 +00:00
"unapproved" ,
"NOTICE: This project is awaiting approval. It is only visible to owners and site admins." ,
)
2021-07-08 07:40:30 +00:00
case models . ProjectLifecycleHiatus :
2021-11-11 20:00:36 +00:00
templateData . BaseData . AddImmediateNotice (
2021-08-17 05:18:04 +00:00
"hiatus" ,
"NOTICE: This project is on hiatus and may not update for a while." ,
)
2021-07-08 07:40:30 +00:00
case models . ProjectLifecycleDead :
2021-11-11 20:00:36 +00:00
templateData . BaseData . AddImmediateNotice (
2021-08-17 05:18:04 +00:00
"dead" ,
2021-12-02 10:53:36 +00:00
"NOTICE: This project is has been marked dead and is only visible to owners and site admins." ,
2021-08-17 05:18:04 +00:00
)
2021-07-08 07:40:30 +00:00
case models . ProjectLifecycleLTSRequired :
2021-11-11 20:00:36 +00:00
templateData . BaseData . AddImmediateNotice (
2021-08-17 05:18:04 +00:00
"lts-reqd" ,
"NOTICE: This project is awaiting approval for maintenance-mode status." ,
)
2021-07-08 07:40:30 +00:00
}
}
2022-04-16 17:49:29 +00:00
for _ , screenshotFilename := range screenshotFilenames {
templateData . Screenshots = append ( templateData . Screenshots , hmnurl . BuildUserFile ( screenshotFilename ) )
2021-07-08 07:40:30 +00:00
}
2022-04-16 17:49:29 +00:00
for _ , link := range projectLinks {
templateData . ProjectLinks = append ( templateData . ProjectLinks , templates . LinkToTemplate ( link ) )
2021-07-08 07:40:30 +00:00
}
2022-04-16 17:49:29 +00:00
for _ , post := range posts {
2021-11-11 20:00:36 +00:00
templateData . RecentActivity = append ( templateData . RecentActivity , PostToTimelineItem (
2021-11-10 04:11:39 +00:00
c . UrlContext ,
2021-07-08 07:40:30 +00:00
lineageBuilder ,
2022-04-16 17:49:29 +00:00
& post . Post ,
& post . Thread ,
2022-06-14 16:14:38 +00:00
post . Author ,
2021-07-08 07:40:30 +00:00
c . Theme ,
) )
}
2022-06-24 21:38:11 +00:00
snippets , err := hmndata . FetchSnippets ( c , c . Conn , c . CurrentUser , hmndata . SnippetQuery {
2022-08-05 04:03:45 +00:00
ProjectIDs : [ ] int { c . CurrentProject . ID } ,
2021-11-11 20:00:36 +00:00
} )
if err != nil {
return c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to fetch project snippets" ) )
}
for _ , s := range snippets {
item := SnippetToTimelineItem (
& s . Snippet ,
s . Asset ,
s . DiscordMessage ,
2022-08-05 04:03:45 +00:00
s . Projects ,
2021-11-11 20:00:36 +00:00
s . Owner ,
c . Theme ,
2022-08-05 04:03:45 +00:00
( c . CurrentUser != nil && ( s . Owner . ID == c . CurrentUser . ID || c . CurrentUser . IsStaff ) ) ,
2021-11-11 20:00:36 +00:00
)
item . SmallInfo = true
templateData . RecentActivity = append ( templateData . RecentActivity , item )
}
c . Perf . StartBlock ( "PROFILE" , "Sort timeline" )
sort . Slice ( templateData . RecentActivity , func ( i , j int ) bool {
return templateData . RecentActivity [ j ] . Date . Before ( templateData . RecentActivity [ i ] . Date )
} )
c . Perf . EndBlock ( )
2022-08-05 04:03:45 +00:00
if c . CurrentUser != nil {
userProjects , err := hmndata . FetchProjects ( c , c . Conn , c . CurrentUser , hmndata . ProjectsQuery {
OwnerIDs : [ ] int { c . CurrentUser . ID } ,
} )
if err != nil {
return c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to fetch user projects" ) )
}
templateProjects := make ( [ ] templates . Project , 0 , len ( userProjects ) )
for _ , p := range userProjects {
templateProject := templates . ProjectAndStuffToTemplate ( & p , hmndata . UrlContextForProject ( & p . Project ) . BuildHomepage ( ) , c . Theme )
templateProjects = append ( templateProjects , templateProject )
}
templateData . SnippetEdit = templates . SnippetEdit {
AvailableProjectsJSON : templates . SnippetEditProjectsToJSON ( templateProjects ) ,
SubmitUrl : hmnurl . BuildSnippetSubmit ( ) ,
AssetMaxSize : AssetMaxSize ( c . CurrentUser ) ,
}
}
2021-07-08 07:40:30 +00:00
var res ResponseData
2021-11-11 20:00:36 +00:00
err = res . WriteTemplate ( "project_homepage.html" , templateData , c . Perf )
2021-07-08 07:40:30 +00:00
if err != nil {
2021-08-28 12:21:03 +00:00
return c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to render project homepage template" ) )
2021-07-08 07:40:30 +00:00
}
return res
}
2021-11-25 03:59:51 +00:00
var ProjectLogoMaxFileSize = 2 * 1024 * 1024
type ProjectEditData struct {
templates . BaseData
Editing bool
ProjectSettings templates . ProjectSettings
2021-12-09 04:23:20 +00:00
MaxOwners int
2021-11-25 03:59:51 +00:00
APICheckUsernameUrl string
LogoMaxFileSize int
2022-08-02 02:01:55 +00:00
MaxFileSize int
UploadUrl string
2021-11-25 03:59:51 +00:00
}
func ProjectNew ( c * RequestContext ) ResponseData {
2022-06-24 21:38:11 +00:00
numProjects , err := hmndata . CountProjects ( c , c . Conn , c . CurrentUser , hmndata . ProjectsQuery {
2021-12-09 04:02:11 +00:00
OwnerIDs : [ ] int { c . CurrentUser . ID } ,
Types : hmndata . PersonalProjects ,
} )
if err != nil {
return c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to check number of personal projects" ) )
}
if numProjects >= maxPersonalProjects {
2022-06-24 21:38:11 +00:00
return c . RejectRequest ( fmt . Sprintf ( "You have already reached the maximum of %d personal projects." , maxPersonalProjects ) )
2021-12-09 04:02:11 +00:00
}
2021-11-25 03:59:51 +00:00
var project templates . ProjectSettings
project . Owners = append ( project . Owners , templates . UserToTemplate ( c . CurrentUser , c . Theme ) )
project . Personal = true
2022-06-19 22:26:33 +00:00
var currentJam * hmndata . Jam
if c . Req . URL . Query ( ) . Has ( "jam" ) {
currentJam = hmndata . CurrentJam ( )
if currentJam != nil {
project . JamParticipation = [ ] templates . ProjectJamParticipation {
templates . ProjectJamParticipation {
JamName : currentJam . Name ,
JamSlug : currentJam . Slug ,
Participating : true ,
} ,
}
}
}
2021-11-25 03:59:51 +00:00
var res ResponseData
res . MustWriteTemplate ( "project_edit.html" , ProjectEditData {
BaseData : getBaseDataAutocrumb ( c , "New Project" ) ,
Editing : false ,
ProjectSettings : project ,
2021-12-09 04:23:20 +00:00
MaxOwners : maxProjectOwners ,
2021-11-25 03:59:51 +00:00
APICheckUsernameUrl : hmnurl . BuildAPICheckUsername ( ) ,
LogoMaxFileSize : ProjectLogoMaxFileSize ,
2022-08-02 02:01:55 +00:00
MaxFileSize : AssetMaxSize ( c . CurrentUser ) ,
UploadUrl : c . UrlContext . BuildAssetUpload ( ) ,
2021-11-25 03:59:51 +00:00
} , c . Perf )
return res
}
func ProjectNewSubmit ( c * RequestContext ) ResponseData {
2021-12-04 14:55:45 +00:00
formResult := ParseProjectEditForm ( c )
if formResult . Error != nil {
return c . ErrorResponse ( http . StatusInternalServerError , formResult . Error )
}
if len ( formResult . RejectionReason ) != 0 {
2022-06-24 21:38:11 +00:00
return c . RejectRequest ( formResult . RejectionReason )
2021-12-04 14:55:45 +00:00
}
2022-06-24 21:38:11 +00:00
tx , err := c . Conn . Begin ( c )
2021-12-04 14:55:45 +00:00
if err != nil {
return c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "Failed to start db transaction" ) )
}
2022-06-24 21:38:11 +00:00
defer tx . Rollback ( c )
2021-12-04 14:55:45 +00:00
2022-06-24 21:38:11 +00:00
numProjects , err := hmndata . CountProjects ( c , c . Conn , c . CurrentUser , hmndata . ProjectsQuery {
2021-12-09 04:02:11 +00:00
OwnerIDs : [ ] int { c . CurrentUser . ID } ,
Types : hmndata . PersonalProjects ,
} )
if err != nil {
return c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to check number of personal projects" ) )
}
if numProjects >= maxPersonalProjects {
2022-06-24 21:38:11 +00:00
return c . RejectRequest ( fmt . Sprintf ( "You have already reached the maximum of %d personal projects." , maxPersonalProjects ) )
2021-12-09 04:02:11 +00:00
}
2021-12-04 14:55:45 +00:00
var projectId int
2022-06-24 21:38:11 +00:00
err = tx . QueryRow ( c ,
2021-12-04 14:55:45 +00:00
`
2022-05-07 13:11:05 +00:00
INSERT INTO project
2021-12-04 14:55:45 +00:00
( name , blurb , description , descparsed , lifecycle , date_created , all_last_updated )
VALUES
( $ 1 , $ 2 , $ 3 , $ 4 , $ 5 , $ 6 , $ 6 )
RETURNING id
` ,
"" ,
"" ,
"" ,
"" ,
models . ProjectLifecycleUnapproved ,
time . Now ( ) , // NOTE(asaf): Using this param twice.
) . Scan ( & projectId )
if err != nil {
return c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "Failed to insert new project" ) )
}
formResult . Payload . ProjectID = projectId
2022-06-24 21:38:11 +00:00
err = updateProject ( c , tx , c . CurrentUser , & formResult . Payload )
2021-12-04 14:55:45 +00:00
if err != nil {
return c . ErrorResponse ( http . StatusInternalServerError , err )
}
2022-06-24 21:38:11 +00:00
tx . Commit ( c )
2021-12-04 14:55:45 +00:00
urlContext := & hmnurl . UrlContext {
PersonalProject : true ,
ProjectID : projectId ,
ProjectName : formResult . Payload . Name ,
}
return c . Redirect ( urlContext . BuildHomepage ( ) , http . StatusSeeOther )
}
func ProjectEdit ( c * RequestContext ) ResponseData {
if ! c . CurrentUserCanEditCurrentProject {
return FourOhFour ( c )
}
2021-12-09 02:04:15 +00:00
p , err := hmndata . FetchProject (
2022-06-24 21:38:11 +00:00
c , c . Conn ,
2021-12-08 03:37:52 +00:00
c . CurrentUser , c . CurrentProject . ID ,
2021-12-09 02:04:15 +00:00
hmndata . ProjectsQuery {
2021-12-11 19:08:10 +00:00
Lifecycles : models . AllProjectLifecycles ,
2021-12-08 03:37:52 +00:00
IncludeHidden : true ,
} ,
)
2021-12-04 14:55:45 +00:00
if err != nil {
2021-12-11 19:08:10 +00:00
return c . ErrorResponse ( http . StatusInternalServerError , err )
2021-12-04 14:55:45 +00:00
}
2021-12-26 10:03:25 +00:00
c . Perf . StartBlock ( "SQL" , "Fetching project links" )
2022-06-24 21:38:11 +00:00
projectLinks , err := db . Query [ models . Link ] ( c , c . Conn ,
2021-12-26 10:03:25 +00:00
`
SELECT $ columns
FROM
2022-05-07 13:11:05 +00:00
link as link
2021-12-26 10:03:25 +00:00
WHERE
link . project_id = $ 1
ORDER BY link . ordering ASC
` ,
p . Project . ID ,
)
if err != nil {
return c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to fetch project links" ) )
}
c . Perf . EndBlock ( )
2022-06-19 22:26:33 +00:00
c . Perf . StartBlock ( "SQL" , "Fetching project jams" )
2022-08-04 23:37:51 +00:00
projectJams , err := hmndata . FetchJamsForProject ( c , c . Conn , c . CurrentUser , p . Project . ID )
2022-06-19 22:26:33 +00:00
if err != nil {
return c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to fetch jams for project" ) )
}
c . Perf . EndBlock ( )
2021-12-29 14:38:23 +00:00
lightLogoUrl := templates . ProjectLogoUrl ( & p . Project , p . LogoLightAsset , p . LogoDarkAsset , "light" )
darkLogoUrl := templates . ProjectLogoUrl ( & p . Project , p . LogoLightAsset , p . LogoDarkAsset , "dark" )
2021-12-08 03:37:52 +00:00
projectSettings := templates . ProjectToProjectSettings (
& p . Project ,
2021-12-11 19:08:10 +00:00
p . Owners ,
2021-12-08 03:37:52 +00:00
p . TagText ( ) ,
2021-12-29 14:38:23 +00:00
lightLogoUrl , darkLogoUrl ,
2021-12-08 03:37:52 +00:00
c . Theme ,
)
2021-12-04 14:55:45 +00:00
2022-04-16 17:49:29 +00:00
projectSettings . LinksText = LinksToText ( projectLinks )
2021-12-26 10:03:25 +00:00
2022-06-19 22:26:33 +00:00
projectSettings . JamParticipation = make ( [ ] templates . ProjectJamParticipation , 0 , len ( projectJams ) )
for _ , jam := range projectJams {
projectSettings . JamParticipation = append ( projectSettings . JamParticipation , templates . ProjectJamParticipation {
JamName : jam . JamName ,
JamSlug : jam . JamSlug ,
Participating : jam . Participating ,
} )
}
2021-12-04 14:55:45 +00:00
var res ResponseData
res . MustWriteTemplate ( "project_edit.html" , ProjectEditData {
BaseData : getBaseDataAutocrumb ( c , "Edit Project" ) ,
Editing : true ,
ProjectSettings : projectSettings ,
2021-12-09 04:23:20 +00:00
MaxOwners : maxProjectOwners ,
2021-12-04 14:55:45 +00:00
APICheckUsernameUrl : hmnurl . BuildAPICheckUsername ( ) ,
LogoMaxFileSize : ProjectLogoMaxFileSize ,
2022-08-02 02:01:55 +00:00
MaxFileSize : AssetMaxSize ( c . CurrentUser ) ,
UploadUrl : c . UrlContext . BuildAssetUpload ( ) ,
2021-12-04 14:55:45 +00:00
} , c . Perf )
return res
}
func ProjectEditSubmit ( c * RequestContext ) ResponseData {
if ! c . CurrentUserCanEditCurrentProject {
return FourOhFour ( c )
}
formResult := ParseProjectEditForm ( c )
if formResult . Error != nil {
return c . ErrorResponse ( http . StatusInternalServerError , formResult . Error )
}
if len ( formResult . RejectionReason ) != 0 {
2022-06-24 21:38:11 +00:00
return c . RejectRequest ( formResult . RejectionReason )
2021-12-04 14:55:45 +00:00
}
2022-06-24 21:38:11 +00:00
tx , err := c . Conn . Begin ( c )
2021-12-04 14:55:45 +00:00
if err != nil {
return c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "Failed to start db transaction" ) )
}
2022-06-24 21:38:11 +00:00
defer tx . Rollback ( c )
2021-12-04 14:55:45 +00:00
formResult . Payload . ProjectID = c . CurrentProject . ID
2022-06-24 21:38:11 +00:00
err = updateProject ( c , tx , c . CurrentUser , & formResult . Payload )
2021-12-04 14:55:45 +00:00
if err != nil {
return c . ErrorResponse ( http . StatusInternalServerError , err )
}
2022-06-24 21:38:11 +00:00
tx . Commit ( c )
2021-12-04 14:55:45 +00:00
urlContext := & hmnurl . UrlContext {
PersonalProject : formResult . Payload . Personal ,
ProjectSlug : formResult . Payload . Slug ,
ProjectID : formResult . Payload . ProjectID ,
ProjectName : formResult . Payload . Name ,
}
return c . Redirect ( urlContext . BuildHomepage ( ) , http . StatusSeeOther )
}
type ProjectPayload struct {
2022-06-19 22:26:33 +00:00
ProjectID int
Name string
Blurb string
Links [ ] ParsedLink
Description string
ParsedDescription string
Lifecycle models . ProjectLifecycle
Hidden bool
OwnerUsernames [ ] string
LightLogo FormImage
DarkLogo FormImage
Tag string
JamParticipationSlugs [ ] string
2021-12-04 14:55:45 +00:00
Slug string
Featured bool
Personal bool
}
type ProjectEditFormResult struct {
Payload ProjectPayload
RejectionReason string
Error error
}
func ParseProjectEditForm ( c * RequestContext ) ProjectEditFormResult {
var res ProjectEditFormResult
2021-12-02 10:53:36 +00:00
maxBodySize := int64 ( ProjectLogoMaxFileSize * 2 + 1024 * 1024 )
c . Req . Body = http . MaxBytesReader ( c . Res , c . Req . Body , maxBodySize )
err := c . Req . ParseMultipartForm ( maxBodySize )
if err != nil {
// NOTE(asaf): The error for exceeding the max filesize doesn't have a special type, so we can't easily detect it here.
2021-12-04 14:55:45 +00:00
res . Error = oops . New ( err , "failed to parse form" )
return res
2021-12-02 10:53:36 +00:00
}
projectName := strings . TrimSpace ( c . Req . Form . Get ( "project_name" ) )
if len ( projectName ) == 0 {
2021-12-04 14:55:45 +00:00
res . RejectionReason = "Project name is empty"
return res
2021-12-02 10:53:36 +00:00
}
shortDesc := strings . TrimSpace ( c . Req . Form . Get ( "shortdesc" ) )
if len ( shortDesc ) == 0 {
2021-12-04 14:55:45 +00:00
res . RejectionReason = "Projects must have a short description"
return res
2021-12-02 10:53:36 +00:00
}
2021-12-26 10:03:25 +00:00
links := ParseLinks ( c . Req . Form . Get ( "links" ) )
2022-08-06 21:40:05 +00:00
description := c . Req . Form . Get ( "full_description" )
2021-12-02 10:53:36 +00:00
parsedDescription := parsing . ParseMarkdown ( description , parsing . ForumRealMarkdown )
lifecycleStr := c . Req . Form . Get ( "lifecycle" )
2021-12-04 14:55:45 +00:00
lifecycle , found := templates . ProjectLifecycleFromValue ( lifecycleStr )
if ! found {
res . RejectionReason = "Project status is invalid"
return res
2021-12-02 10:53:36 +00:00
}
2021-12-08 03:37:52 +00:00
tag := c . Req . Form . Get ( "tag" )
if ! models . ValidateTagText ( tag ) {
res . RejectionReason = "Project tag is invalid"
return res
}
2021-12-02 10:53:36 +00:00
hiddenStr := c . Req . Form . Get ( "hidden" )
hidden := len ( hiddenStr ) > 0
lightLogo , err := GetFormImage ( c , "light_logo" )
if err != nil {
2021-12-04 14:55:45 +00:00
res . Error = oops . New ( err , "Failed to read image from form" )
return res
2021-12-02 10:53:36 +00:00
}
darkLogo , err := GetFormImage ( c , "dark_logo" )
if err != nil {
2021-12-04 14:55:45 +00:00
res . Error = oops . New ( err , "Failed to read image from form" )
return res
2021-12-02 10:53:36 +00:00
}
owners := c . Req . Form [ "owners" ]
2021-12-09 04:23:20 +00:00
if len ( owners ) > maxProjectOwners {
res . RejectionReason = fmt . Sprintf ( "Projects can have at most %d owners" , maxProjectOwners )
return res
}
2021-12-02 10:53:36 +00:00
2021-12-04 14:55:45 +00:00
slug := strings . TrimSpace ( c . Req . Form . Get ( "slug" ) )
officialStr := c . Req . Form . Get ( "official" )
official := len ( officialStr ) > 0
featuredStr := c . Req . Form . Get ( "featured" )
featured := len ( featuredStr ) > 0
if official && len ( slug ) == 0 {
res . RejectionReason = "Official projects must have a slug"
return res
2021-12-02 10:53:36 +00:00
}
2022-06-19 22:26:33 +00:00
jamParticipationSlugs := c . Req . Form [ "jam_participation" ]
2021-12-04 14:55:45 +00:00
res . Payload = ProjectPayload {
2022-06-19 22:26:33 +00:00
Name : projectName ,
Blurb : shortDesc ,
Links : links ,
Description : description ,
ParsedDescription : parsedDescription ,
Lifecycle : lifecycle ,
Hidden : hidden ,
OwnerUsernames : owners ,
LightLogo : lightLogo ,
DarkLogo : darkLogo ,
Tag : tag ,
JamParticipationSlugs : jamParticipationSlugs ,
Slug : slug ,
Personal : ! official ,
Featured : featured ,
2021-12-04 14:55:45 +00:00
}
return res
}
2021-12-08 03:37:52 +00:00
func updateProject ( ctx context . Context , tx pgx . Tx , user * models . User , payload * ProjectPayload ) error {
2021-12-02 10:53:36 +00:00
var lightLogoUUID * uuid . UUID
2021-12-04 14:55:45 +00:00
if payload . LightLogo . Exists {
lightLogo := & payload . LightLogo
2021-12-08 03:37:52 +00:00
lightLogoAsset , err := assets . Create ( ctx , tx , assets . CreateInput {
2021-12-02 10:53:36 +00:00
Content : lightLogo . Content ,
Filename : lightLogo . Filename ,
ContentType : lightLogo . Mime ,
2021-12-04 14:55:45 +00:00
UploaderID : & user . ID ,
2021-12-02 10:53:36 +00:00
Width : lightLogo . Width ,
Height : lightLogo . Height ,
} )
if err != nil {
2021-12-04 14:55:45 +00:00
return oops . New ( err , "Failed to save asset" )
2021-12-02 10:53:36 +00:00
}
lightLogoUUID = & lightLogoAsset . ID
}
var darkLogoUUID * uuid . UUID
2021-12-04 14:55:45 +00:00
if payload . DarkLogo . Exists {
darkLogo := & payload . DarkLogo
2021-12-08 03:37:52 +00:00
darkLogoAsset , err := assets . Create ( ctx , tx , assets . CreateInput {
2021-12-02 10:53:36 +00:00
Content : darkLogo . Content ,
Filename : darkLogo . Filename ,
ContentType : darkLogo . Mime ,
2021-12-04 14:55:45 +00:00
UploaderID : & user . ID ,
2021-12-02 10:53:36 +00:00
Width : darkLogo . Width ,
Height : darkLogo . Height ,
} )
if err != nil {
2021-12-04 14:55:45 +00:00
return oops . New ( err , "Failed to save asset" )
2021-12-02 10:53:36 +00:00
}
darkLogoUUID = & darkLogoAsset . ID
}
hasSelf := false
2021-12-04 14:55:45 +00:00
selfUsername := strings . ToLower ( user . Username )
for i , _ := range payload . OwnerUsernames {
payload . OwnerUsernames [ i ] = strings . ToLower ( payload . OwnerUsernames [ i ] )
if payload . OwnerUsernames [ i ] == selfUsername {
2021-12-02 10:53:36 +00:00
hasSelf = true
}
}
2021-12-04 14:55:45 +00:00
if ! hasSelf && ! user . IsStaff {
payload . OwnerUsernames = append ( payload . OwnerUsernames , selfUsername )
2021-12-02 10:53:36 +00:00
}
2021-12-26 10:03:25 +00:00
_ , err := tx . Exec ( ctx ,
2021-12-04 14:55:45 +00:00
`
2022-05-07 13:11:05 +00:00
UPDATE project SET
2021-12-26 10:03:25 +00:00
name = $ 2 ,
blurb = $ 3 ,
description = $ 4 ,
descparsed = $ 5 ,
lifecycle = $ 6
WHERE id = $ 1
2021-12-04 14:55:45 +00:00
` ,
2021-12-26 10:03:25 +00:00
payload . ProjectID ,
2021-12-04 14:55:45 +00:00
payload . Name ,
payload . Blurb ,
payload . Description ,
payload . ParsedDescription ,
payload . Lifecycle ,
)
if err != nil {
return oops . New ( err , "Failed to update project" )
}
2022-02-13 00:36:12 +00:00
_ , err = hmndata . SetProjectTag ( ctx , tx , user , payload . ProjectID , payload . Tag )
2022-01-31 08:22:25 +00:00
if err != nil {
return err
}
2021-12-08 03:37:52 +00:00
2021-12-04 14:55:45 +00:00
if user . IsStaff {
2021-12-08 03:37:52 +00:00
_ , err = tx . Exec ( ctx ,
2021-12-04 14:55:45 +00:00
`
2022-05-07 13:11:05 +00:00
UPDATE project SET
2021-12-04 14:55:45 +00:00
slug = $ 2 ,
featured = $ 3 ,
2021-12-26 10:03:25 +00:00
personal = $ 4 ,
hidden = $ 5
2021-12-04 14:55:45 +00:00
WHERE
id = $ 1
` ,
payload . ProjectID ,
payload . Slug ,
payload . Featured ,
payload . Personal ,
2021-12-26 10:03:25 +00:00
payload . Hidden ,
2021-12-04 14:55:45 +00:00
)
if err != nil {
return oops . New ( err , "Failed to update project with admin fields" )
}
}
if payload . LightLogo . Exists || payload . LightLogo . Remove {
2021-12-08 03:37:52 +00:00
_ , err = tx . Exec ( ctx ,
2021-12-04 14:55:45 +00:00
`
2022-05-07 13:11:05 +00:00
UPDATE project
2021-12-04 14:55:45 +00:00
SET
logolight_asset_id = $ 2
WHERE
id = $ 1
` ,
payload . ProjectID ,
lightLogoUUID ,
)
if err != nil {
return oops . New ( err , "Failed to update project's light logo" )
}
}
if payload . DarkLogo . Exists || payload . DarkLogo . Remove {
2021-12-08 03:37:52 +00:00
_ , err = tx . Exec ( ctx ,
2021-12-04 14:55:45 +00:00
`
2022-05-07 13:11:05 +00:00
UPDATE project
2021-12-04 14:55:45 +00:00
SET
logodark_asset_id = $ 2
WHERE
id = $ 1
` ,
payload . ProjectID ,
darkLogoUUID ,
)
if err != nil {
return oops . New ( err , "Failed to update project's dark logo" )
}
}
2022-04-16 17:49:29 +00:00
owners , err := db . Query [ models . User ] ( ctx , tx ,
2021-12-02 10:53:36 +00:00
`
2022-05-07 18:58:00 +00:00
SELECT $ columns
FROM hmn_user
2021-12-02 10:53:36 +00:00
WHERE LOWER ( username ) = ANY ( $ 1 )
` ,
2021-12-04 14:55:45 +00:00
payload . OwnerUsernames ,
2021-12-02 10:53:36 +00:00
)
if err != nil {
2021-12-04 14:55:45 +00:00
return oops . New ( err , "Failed to query users" )
2021-12-02 10:53:36 +00:00
}
2021-12-08 03:37:52 +00:00
_ , err = tx . Exec ( ctx ,
2021-12-02 10:53:36 +00:00
`
2022-05-07 13:11:05 +00:00
DELETE FROM user_project
2021-12-04 14:55:45 +00:00
WHERE project_id = $ 1
2021-12-02 10:53:36 +00:00
` ,
2021-12-04 14:55:45 +00:00
payload . ProjectID ,
)
2021-12-02 10:53:36 +00:00
if err != nil {
2021-12-04 14:55:45 +00:00
return oops . New ( err , "Failed to delete project owners" )
2021-12-02 10:53:36 +00:00
}
2022-04-16 17:49:29 +00:00
for _ , owner := range owners {
2021-12-08 03:37:52 +00:00
_ , err = tx . Exec ( ctx ,
2021-12-02 10:53:36 +00:00
`
2022-05-07 13:11:05 +00:00
INSERT INTO user_project
2021-12-02 10:53:36 +00:00
( user_id , project_id )
VALUES
( $ 1 , $ 2 )
` ,
2022-04-16 17:49:29 +00:00
owner . ID ,
2021-12-04 14:55:45 +00:00
payload . ProjectID ,
2021-12-02 10:53:36 +00:00
)
if err != nil {
2021-12-04 14:55:45 +00:00
return oops . New ( err , "Failed to insert project owner" )
2021-12-02 10:53:36 +00:00
}
}
2022-03-22 18:07:43 +00:00
twitchLoginsPreChange , preErr := hmndata . FetchTwitchLoginsForUserOrProject ( ctx , tx , nil , & payload . ProjectID )
2022-05-07 13:11:05 +00:00
_ , err = tx . Exec ( ctx , ` DELETE FROM link WHERE project_id = $1 ` , payload . ProjectID )
2021-12-26 10:03:25 +00:00
if err != nil {
return oops . New ( err , "Failed to delete project links" )
}
for i , link := range payload . Links {
_ , err = tx . Exec ( ctx ,
`
2022-05-07 13:11:05 +00:00
INSERT INTO link ( name , url , ordering , project_id )
2021-12-26 10:03:25 +00:00
VALUES ( $ 1 , $ 2 , $ 3 , $ 4 )
` ,
link . Name ,
link . Url ,
i ,
payload . ProjectID ,
)
if err != nil {
return oops . New ( err , "Failed to insert new project link" )
}
}
2022-03-22 18:07:43 +00:00
twitchLoginsPostChange , postErr := hmndata . FetchTwitchLoginsForUserOrProject ( ctx , tx , nil , & payload . ProjectID )
if preErr == nil && postErr == nil {
twitch . UserOrProjectLinksUpdated ( twitchLoginsPreChange , twitchLoginsPostChange )
}
2021-12-26 10:03:25 +00:00
2022-06-19 22:26:33 +00:00
// NOTE(asaf): Regular users can only edit the jam participation status of the current jam or
// jams the project was previously a part of.
var possibleJamSlugs [ ] string
if user . IsStaff {
possibleJamSlugs = make ( [ ] string , 0 , len ( hmndata . AllJams ) )
for _ , jam := range hmndata . AllJams {
possibleJamSlugs = append ( possibleJamSlugs , jam . Slug )
}
} else {
possibleJamSlugs , err = db . QueryScalar [ string ] ( ctx , tx ,
`
SELECT jam_slug
FROM jam_project
WHERE project_id = $ 1
` ,
payload . ProjectID ,
)
if err != nil {
return oops . New ( err , "Failed to fetch jam participation for project" )
}
currentJam := hmndata . CurrentJam ( )
if currentJam != nil {
possibleJamSlugs = append ( possibleJamSlugs , currentJam . Slug )
}
}
_ , err = tx . Exec ( ctx ,
`
UPDATE jam_project
SET participating = FALSE
WHERE project_id = $ 1
` ,
payload . ProjectID ,
)
if err != nil {
return oops . New ( err , "Failed to remove jam participation for project" )
}
for _ , jamSlug := range payload . JamParticipationSlugs {
found := false
for _ , possibleSlug := range possibleJamSlugs {
if possibleSlug == jamSlug {
found = true
break
}
}
if found {
_ , err = tx . Exec ( ctx ,
`
INSERT INTO jam_project ( project_id , jam_slug , participating )
VALUES ( $ 1 , $ 2 , $ 3 )
ON CONFLICT ( project_id , jam_slug ) DO UPDATE SET
participating = EXCLUDED . participating
` ,
payload . ProjectID ,
jamSlug ,
true ,
)
if err != nil {
return oops . New ( err , "Failed to insert/update jam participation for project" )
}
}
}
2021-12-04 14:55:45 +00:00
return nil
2021-11-25 03:59:51 +00:00
}
2021-12-02 10:53:36 +00:00
type FormImage struct {
Exists bool
2021-12-04 14:55:45 +00:00
Remove bool
2021-12-02 10:53:36 +00:00
Filename string
Mime string
Content [ ] byte
Width int
Height int
Size int64
}
// NOTE(asaf): This assumes that you already called ParseMultipartForm (which is why there's no size limit here).
func GetFormImage ( c * RequestContext , fieldName string ) ( FormImage , error ) {
var res FormImage
res . Exists = false
2021-12-04 14:55:45 +00:00
removeStr := c . Req . Form . Get ( "remove_" + fieldName )
res . Remove = ( removeStr == "true" )
2021-12-02 10:53:36 +00:00
img , header , err := c . Req . FormFile ( fieldName )
if err != nil {
if errors . Is ( err , http . ErrMissingFile ) {
return res , nil
} else {
return FormImage { } , err
}
}
if header != nil {
res . Exists = true
res . Size = header . Size
res . Filename = header . Filename
res . Content = make ( [ ] byte , res . Size )
img . Read ( res . Content )
img . Seek ( 0 , io . SeekStart )
2021-12-29 14:38:23 +00:00
fileExtensionOverrides := [ ] string { ".svg" }
fileExt := strings . ToLower ( path . Ext ( res . Filename ) )
tryDecode := true
for _ , ext := range fileExtensionOverrides {
if fileExt == ext {
tryDecode = false
}
}
if tryDecode {
config , _ , err := image . DecodeConfig ( img )
if err != nil {
return FormImage { } , err
}
res . Width = config . Width
res . Height = config . Height
res . Mime = http . DetectContentType ( res . Content )
} else {
if fileExt == ".svg" {
res . Mime = "image/svg+xml"
}
2021-12-02 10:53:36 +00:00
}
}
return res , nil
}
2021-12-09 02:04:15 +00:00
2021-12-09 03:50:35 +00:00
func CanEditProject ( user * models . User , owners [ ] * models . User ) bool {
2021-12-09 02:04:15 +00:00
if user != nil {
if user . IsStaff {
2021-12-09 03:50:35 +00:00
return true
2021-12-09 02:04:15 +00:00
} else {
for _ , owner := range owners {
if owner . ID == user . ID {
2021-12-09 03:50:35 +00:00
return true
2021-12-09 02:04:15 +00:00
}
}
}
}
2021-12-09 03:50:35 +00:00
return false
2021-12-09 02:04:15 +00:00
}