Added projects to admin approval queue
This commit is contained in:
parent
f67429becd
commit
5c29f3f814
|
@ -76,6 +76,7 @@ type Project struct {
|
||||||
Hidden bool `db:"hidden"`
|
Hidden bool `db:"hidden"`
|
||||||
Featured bool `db:"featured"`
|
Featured bool `db:"featured"`
|
||||||
DateApproved time.Time `db:"date_approved"`
|
DateApproved time.Time `db:"date_approved"`
|
||||||
|
DateCreated time.Time `db:"date_created"`
|
||||||
AllLastUpdated time.Time `db:"all_last_updated"`
|
AllLastUpdated time.Time `db:"all_last_updated"`
|
||||||
ForumLastUpdated time.Time `db:"forum_last_updated"`
|
ForumLastUpdated time.Time `db:"forum_last_updated"`
|
||||||
BlogLastUpdated time.Time `db:"blog_last_updated"`
|
BlogLastUpdated time.Time `db:"blog_last_updated"`
|
||||||
|
|
|
@ -2,54 +2,88 @@
|
||||||
|
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div class="content-block">
|
<div class="content-block">
|
||||||
{{ range .Posts }}
|
{{ range .UnapprovedUsers }}
|
||||||
<div class="post background-even pa3">
|
<div class="flex flex-row bg--card mb3 pa2">
|
||||||
<div class="fl w-100 w-25-l pt3 pa3-l flex tc-l">
|
<div class="
|
||||||
<div class="fl w-20 mw3 dn-l w3">
|
sidebar flex-shrink-0
|
||||||
<!-- Mobile avatar -->
|
flex flex-column items-stretch-l
|
||||||
<div class="aspect-ratio--1x1 contain bg-center" style="background-image:url('{{ .Author.AvatarUrl }}');"></div>
|
overflow-hidden
|
||||||
|
" style="width: 200px;">
|
||||||
|
<a class="db" href="{{ .User.ProfileUrl }}">{{ .User.Username }}</a>
|
||||||
|
<div>{{ .User.Name }}</div>
|
||||||
|
<div class="w-100 flex-shrink-0 flex justify-center">
|
||||||
|
<img class="br3" alt="{{ .User.Name }}'s Avatar" src="{{ .User.AvatarUrl }}">
|
||||||
|
</div>
|
||||||
|
<div class="mt3 mt0-ns mt3-l ml3-ns ml0-l flex flex-column items-start overflow-hidden">
|
||||||
|
{{ with or .User.Bio .User.Blurb }}
|
||||||
|
<div class="mb3">{{ . }}</div>
|
||||||
|
{{ end }}
|
||||||
|
<div class="w-100 w-auto-ns w-100-l">
|
||||||
|
{{ if .User.Email }}
|
||||||
|
<div class="pair flex">
|
||||||
|
<div class="key flex-auto flex-shrink-0 mr2">Email</div>
|
||||||
|
<div class="value projectlink truncate">{{ .User.Email }}</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ range .UserLinks }}
|
||||||
|
<div class="pair flex">
|
||||||
|
<div class="key flex-auto flex-shrink-0 mr2">{{ .Name }}</div>
|
||||||
|
<div class="value projectlink truncate"><a class="external" href="{{ .Url }}" ><span class="icon-{{ .Icon }}"></span> {{ .LinkText }}</a></div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<div>{{ absoluteshortdate .User.DateJoined }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-100-l pl3 pl0-l flex flex-column items-center-l">
|
|
||||||
<div>
|
<div>
|
||||||
<a class="username" href="{{ .Author.ProfileUrl }}" target="_blank">{{ .Author.Username }}</a>
|
<form method="POST" class="mb2" action="{{ $.SubmitUrl }}">
|
||||||
</div>
|
|
||||||
{{ if and .Author.Name (ne .Author.Name .Author.Username) }}
|
|
||||||
<div class="c--dim f7"> {{ .Author.Name }} </div>
|
|
||||||
{{ end }}
|
|
||||||
<!-- Large avatar -->
|
|
||||||
<div class="dn db-l w-60 pv2">
|
|
||||||
<div class="aspect-ratio--1x1 contain bg-center" style="background-image:url('{{ .Author.AvatarUrl }}');"></div>
|
|
||||||
</div>
|
|
||||||
<div class="i c--dim f7">
|
|
||||||
{{ if .Author.Blurb }}
|
|
||||||
{{ .Author.Blurb }} {{/* TODO: Linebreaks? */}}
|
|
||||||
{{ else if .Author.Bio }}
|
|
||||||
{{ .Author.Bio }}
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="fl w-100 w-75-l pv3 pa3-l">
|
|
||||||
<div class="w-100 flex-l flex-row-reverse-l">
|
|
||||||
<div class="inline-flex flex-row-reverse pl3-l pb3 items-center">
|
|
||||||
<div class="postid">
|
|
||||||
<a name="{{ .ID }}" href="{{ .Url }}">#{{ .ID }}</a>
|
|
||||||
</div>
|
|
||||||
<div class="flex pr3">
|
|
||||||
<form method="POST" class="mr4" action="{{ $.SubmitUrl }}">
|
|
||||||
{{ csrftoken $.Session }}
|
{{ csrftoken $.Session }}
|
||||||
<input type="hidden" name="action" value="{{ $.ApprovalAction }}" />
|
<input type="hidden" name="action" value="{{ $.ApprovalAction }}" />
|
||||||
<input type="hidden" name="user_id" value="{{ .Author.ID }}" />
|
<input type="hidden" name="user_id" value="{{ .User.ID }}" />
|
||||||
<input type="submit" value="Approve User" />
|
<input type="submit" class="w-100" value="Approve User" />
|
||||||
</form>
|
</form>
|
||||||
<form method="POST" action="{{ $.SubmitUrl }}">
|
<form method="POST" action="{{ $.SubmitUrl }}">
|
||||||
{{ csrftoken $.Session }}
|
{{ csrftoken $.Session }}
|
||||||
<input type="hidden" name="action" value="{{ $.SpammerAction }}" />
|
<input type="hidden" name="action" value="{{ $.SpammerAction }}" />
|
||||||
<input type="hidden" name="user_id" value="{{ .Author.ID }}" />
|
<input type="hidden" name="user_id" value="{{ .User.ID }}" />
|
||||||
<input type="submit" value="Mark as spammer" />
|
<input type="submit" class="w-100" value="Mark as spammer" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-grow-1 flex flex-column ml3">
|
||||||
|
{{ range .ProjectsWithLinks }}
|
||||||
|
<div class="project-card flex br2 overflow-hidden items-center relative mv3 w-100">
|
||||||
|
{{ with .Project.Logo }}
|
||||||
|
<div class="image-container flex-shrink-0">
|
||||||
|
<div class="image bg-center cover" style="background-image:url({{ . }})"></div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
<div class="details pa3 flex-grow-1">
|
||||||
|
<h3 class="mb1"><a href="{{ .Project.Url }}">{{ .Project.Name }}</a></h3>
|
||||||
|
<div class="blurb">{{ .Project.Blurb }}</div>
|
||||||
|
<div class="badges mt2">
|
||||||
|
{{ if .Project.LifecycleString }}
|
||||||
|
<span class="badge {{ .Project.LifecycleBadgeClass }}">{{ .Project.LifecycleString }}</span>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ range .Links }}
|
||||||
|
<div class="pair flex">
|
||||||
|
<div class="key flex-auto flex-shrink-0 mr2">{{ .Name }}</div>
|
||||||
|
<div class="value projectlink truncate"><a class="external" href="{{ .Url }}" ><span class="icon-{{ .Icon }}"></span> {{ .LinkText }}</a></div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ range .Posts }}
|
||||||
|
<div class="post background-even pa3">
|
||||||
|
<div class="fl w-100 pv3 pa3-l">
|
||||||
|
<div class="w-100 flex-l flex-row-reverse-l">
|
||||||
|
<div class="inline-flex flex-row-reverse pl3-l pb3 items-center">
|
||||||
|
<div class="postid">
|
||||||
|
<a name="{{ .ID }}" href="{{ .Url }}">#{{ .ID }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="w-100 pb3">
|
<div class="w-100 pb3">
|
||||||
<div class="b" role="heading" aria-level="2">{{ .Title }}</div>
|
<div class="b" role="heading" aria-level="2">{{ .Title }}</div>
|
||||||
{{ timehtml (relativedate .PostDate) .PostDate }}
|
{{ timehtml (relativedate .PostDate) .PostDate }}
|
||||||
|
@ -61,13 +95,10 @@
|
||||||
<div class="post-content overflow-x-auto">
|
<div class="post-content overflow-x-auto">
|
||||||
{{ .Content }}
|
{{ .Content }}
|
||||||
</div>
|
</div>
|
||||||
{{/* {% if post.author.signature|length %}
|
|
||||||
<div class="signature"><hr />
|
|
||||||
{{ post.author.signature|bbdecode|safe }}
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %} */}}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="cb"></div>
|
{{ end }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -112,12 +113,25 @@ type postWithTitle struct {
|
||||||
type adminApprovalQueueData struct {
|
type adminApprovalQueueData struct {
|
||||||
templates.BaseData
|
templates.BaseData
|
||||||
|
|
||||||
Posts []postWithTitle
|
UnapprovedUsers []*unapprovedUserData
|
||||||
SubmitUrl string
|
SubmitUrl string
|
||||||
ApprovalAction string
|
ApprovalAction string
|
||||||
SpammerAction string
|
SpammerAction string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type projectWithLinks struct {
|
||||||
|
Project templates.Project
|
||||||
|
Links []templates.Link
|
||||||
|
}
|
||||||
|
|
||||||
|
type unapprovedUserData struct {
|
||||||
|
User templates.User
|
||||||
|
Date time.Time
|
||||||
|
UserLinks []templates.Link
|
||||||
|
Posts []postWithTitle
|
||||||
|
ProjectsWithLinks []projectWithLinks
|
||||||
|
}
|
||||||
|
|
||||||
func AdminApprovalQueue(c *RequestContext) ResponseData {
|
func AdminApprovalQueue(c *RequestContext) ResponseData {
|
||||||
c.Perf.StartBlock("SQL", "Fetch subforum tree")
|
c.Perf.StartBlock("SQL", "Fetch subforum tree")
|
||||||
subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn)
|
subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn)
|
||||||
|
@ -129,22 +143,103 @@ func AdminApprovalQueue(c *RequestContext) ResponseData {
|
||||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch unapproved posts"))
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch unapproved posts"))
|
||||||
}
|
}
|
||||||
|
|
||||||
data := adminApprovalQueueData{
|
projects, err := fetchUnapprovedProjects(c)
|
||||||
BaseData: getBaseDataAutocrumb(c, "Admin approval queue"),
|
if err != nil {
|
||||||
SubmitUrl: hmnurl.BuildAdminApprovalQueue(),
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch unapproved projects"))
|
||||||
ApprovalAction: ApprovalQueueActionApprove,
|
|
||||||
SpammerAction: ApprovalQueueActionSpammer,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unapprovedUsers := make([]*unapprovedUserData, 0)
|
||||||
|
userIDToDataIdx := make(map[int]int)
|
||||||
|
|
||||||
for _, p := range posts {
|
for _, p := range posts {
|
||||||
|
var userData *unapprovedUserData
|
||||||
|
if idx, ok := userIDToDataIdx[p.Author.ID]; ok {
|
||||||
|
userData = unapprovedUsers[idx]
|
||||||
|
} else {
|
||||||
|
userData = &unapprovedUserData{
|
||||||
|
User: templates.UserToTemplate(&p.Author, c.Theme),
|
||||||
|
UserLinks: make([]templates.Link, 0, 10),
|
||||||
|
}
|
||||||
|
unapprovedUsers = append(unapprovedUsers, userData)
|
||||||
|
userIDToDataIdx[p.Author.ID] = len(unapprovedUsers) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Post.PostDate.After(userData.Date) {
|
||||||
|
userData.Date = p.Post.PostDate
|
||||||
|
}
|
||||||
post := templates.PostToTemplate(&p.Post, &p.Author, c.Theme)
|
post := templates.PostToTemplate(&p.Post, &p.Author, c.Theme)
|
||||||
post.AddContentVersion(p.CurrentVersion, &p.Author) // NOTE(asaf): Don't care about editors here
|
post.AddContentVersion(p.CurrentVersion, &p.Author) // NOTE(asaf): Don't care about editors here
|
||||||
post.Url = UrlForGenericPost(hmndata.UrlContextForProject(&p.Project), &p.Thread, &p.Post, lineageBuilder)
|
post.Url = UrlForGenericPost(hmndata.UrlContextForProject(&p.Project), &p.Thread, &p.Post, lineageBuilder)
|
||||||
data.Posts = append(data.Posts, postWithTitle{
|
userData.Posts = append(userData.Posts, postWithTitle{
|
||||||
Post: post,
|
Post: post,
|
||||||
Title: p.Thread.Title,
|
Title: p.Thread.Title,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, p := range projects {
|
||||||
|
var userData *unapprovedUserData
|
||||||
|
if idx, ok := userIDToDataIdx[p.User.ID]; ok {
|
||||||
|
userData = unapprovedUsers[idx]
|
||||||
|
} else {
|
||||||
|
userData = &unapprovedUserData{
|
||||||
|
User: templates.UserToTemplate(p.User, c.Theme),
|
||||||
|
UserLinks: make([]templates.Link, 0, 10),
|
||||||
|
}
|
||||||
|
unapprovedUsers = append(unapprovedUsers, userData)
|
||||||
|
userIDToDataIdx[p.User.ID] = len(unapprovedUsers) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
projectLinks := make([]templates.Link, 0, len(p.ProjectLinks))
|
||||||
|
for _, l := range p.ProjectLinks {
|
||||||
|
projectLinks = append(projectLinks, templates.LinkToTemplate(l))
|
||||||
|
}
|
||||||
|
if p.ProjectAndStuff.Project.DateCreated.After(userData.Date) {
|
||||||
|
userData.Date = p.ProjectAndStuff.Project.DateCreated
|
||||||
|
}
|
||||||
|
userData.ProjectsWithLinks = append(userData.ProjectsWithLinks, projectWithLinks{
|
||||||
|
Project: templates.ProjectAndStuffToTemplate(p.ProjectAndStuff, hmndata.UrlContextForProject(&p.ProjectAndStuff.Project).BuildHomepage(), c.Theme),
|
||||||
|
Links: projectLinks,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
userIds := make([]int, 0, len(unapprovedUsers))
|
||||||
|
for _, u := range unapprovedUsers {
|
||||||
|
userIds = append(userIds, u.User.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
userLinks, err := db.Query(c.Context(), c.Conn, models.Link{},
|
||||||
|
`
|
||||||
|
SELECT $columns
|
||||||
|
FROM
|
||||||
|
handmade_links
|
||||||
|
WHERE
|
||||||
|
user_id = ANY($1)
|
||||||
|
ORDER BY ordering ASC
|
||||||
|
`,
|
||||||
|
userIds,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user links"))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ul := range userLinks {
|
||||||
|
link := ul.(*models.Link)
|
||||||
|
userData := unapprovedUsers[userIDToDataIdx[*link.UserID]]
|
||||||
|
userData.UserLinks = append(userData.UserLinks, templates.LinkToTemplate(link))
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(unapprovedUsers, func(a, b int) bool {
|
||||||
|
return unapprovedUsers[a].Date.After(unapprovedUsers[b].Date)
|
||||||
|
})
|
||||||
|
|
||||||
|
data := adminApprovalQueueData{
|
||||||
|
BaseData: getBaseDataAutocrumb(c, "Admin approval queue"),
|
||||||
|
UnapprovedUsers: unapprovedUsers,
|
||||||
|
SubmitUrl: hmnurl.BuildAdminApprovalQueue(),
|
||||||
|
ApprovalAction: ApprovalQueueActionApprove,
|
||||||
|
SpammerAction: ApprovalQueueActionSpammer,
|
||||||
|
}
|
||||||
|
|
||||||
var res ResponseData
|
var res ResponseData
|
||||||
res.MustWriteTemplate("admin_approval_queue.html", data, c.Perf)
|
res.MustWriteTemplate("admin_approval_queue.html", data, c.Perf)
|
||||||
return res
|
return res
|
||||||
|
@ -219,6 +314,10 @@ func AdminApprovalQueueSubmit(c *RequestContext) ResponseData {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete spammer's posts"))
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete spammer's posts"))
|
||||||
}
|
}
|
||||||
|
err = deleteAllProjectsForUser(c.Context(), c.Conn, user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete spammer's projects"))
|
||||||
|
}
|
||||||
whatHappened = fmt.Sprintf("%s banned successfully", user.Username)
|
whatHappened = fmt.Sprintf("%s banned successfully", user.Username)
|
||||||
} else {
|
} else {
|
||||||
whatHappened = fmt.Sprintf("Unrecognized action: %s", action)
|
whatHappened = fmt.Sprintf("Unrecognized action: %s", action)
|
||||||
|
@ -266,6 +365,86 @@ func fetchUnapprovedPosts(c *RequestContext) ([]*UnapprovedPost, error) {
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UnapprovedProject struct {
|
||||||
|
User *models.User
|
||||||
|
ProjectAndStuff *hmndata.ProjectAndStuff
|
||||||
|
ProjectLinks []*models.Link
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchUnapprovedProjects(c *RequestContext) ([]UnapprovedProject, error) {
|
||||||
|
type unapprovedUser struct {
|
||||||
|
ID int `db:"id"`
|
||||||
|
}
|
||||||
|
it, err := db.Query(c.Context(), c.Conn, unapprovedUser{},
|
||||||
|
`
|
||||||
|
SELECT $columns
|
||||||
|
FROM
|
||||||
|
auth_user AS u
|
||||||
|
WHERE
|
||||||
|
u.status = ANY($1)
|
||||||
|
`,
|
||||||
|
[]models.UserStatus{models.UserStatusConfirmed},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, oops.New(err, "failed to fetch unapproved users")
|
||||||
|
}
|
||||||
|
ownerIDs := make([]int, 0, len(it))
|
||||||
|
for _, uid := range it {
|
||||||
|
ownerIDs = append(ownerIDs, uid.(*unapprovedUser).ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
projects, err := hmndata.FetchProjects(c.Context(), c.Conn, c.CurrentUser, hmndata.ProjectsQuery{
|
||||||
|
OwnerIDs: ownerIDs,
|
||||||
|
IncludeHidden: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
projectIDs := make([]int, 0, len(projects))
|
||||||
|
for _, p := range projects {
|
||||||
|
projectIDs = append(projectIDs, p.Project.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
projectLinks, err := db.Query(c.Context(), c.Conn, models.Link{},
|
||||||
|
`
|
||||||
|
SELECT $columns
|
||||||
|
FROM
|
||||||
|
handmade_links AS link
|
||||||
|
WHERE
|
||||||
|
link.project_id = ANY($1)
|
||||||
|
ORDER BY link.ordering ASC
|
||||||
|
`,
|
||||||
|
projectIDs,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, oops.New(err, "failed to fetch links for projects")
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []UnapprovedProject
|
||||||
|
|
||||||
|
for idx, proj := range projects {
|
||||||
|
links := make([]*models.Link, 0, 10) // NOTE(asaf): 10 should be enough for most projects.
|
||||||
|
for _, l := range projectLinks {
|
||||||
|
link := l.(*models.Link)
|
||||||
|
if *link.ProjectID == proj.Project.ID {
|
||||||
|
links = append(links, link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, u := range proj.Owners {
|
||||||
|
if u.Status == models.UserStatusConfirmed {
|
||||||
|
result = append(result, UnapprovedProject{
|
||||||
|
User: u,
|
||||||
|
ProjectAndStuff: &projects[idx],
|
||||||
|
ProjectLinks: links,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func deleteAllPostsForUser(ctx context.Context, conn *pgxpool.Pool, userId int) error {
|
func deleteAllPostsForUser(ctx context.Context, conn *pgxpool.Pool, userId int) error {
|
||||||
tx, err := conn.Begin(ctx)
|
tx, err := conn.Begin(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -302,3 +481,50 @@ func deleteAllPostsForUser(ctx context.Context, conn *pgxpool.Pool, userId int)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func deleteAllProjectsForUser(ctx context.Context, conn *pgxpool.Pool, userId int) error {
|
||||||
|
tx, err := conn.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return oops.New(err, "failed to start transaction")
|
||||||
|
}
|
||||||
|
defer tx.Rollback(ctx)
|
||||||
|
|
||||||
|
toDelete, err := db.Query(ctx, tx, models.Project{},
|
||||||
|
`
|
||||||
|
SELECT $columns
|
||||||
|
FROM
|
||||||
|
handmade_project AS project
|
||||||
|
JOIN handmade_user_projects AS up ON up.project_id = project.id
|
||||||
|
WHERE
|
||||||
|
up.user_id = $1
|
||||||
|
`,
|
||||||
|
userId,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return oops.New(err, "failed to fetch user's projects")
|
||||||
|
}
|
||||||
|
|
||||||
|
var projectIds []int
|
||||||
|
for _, p := range toDelete {
|
||||||
|
projectIds = append(projectIds, p.(*models.Project).ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(projectIds) > 0 {
|
||||||
|
_, err = tx.Exec(ctx,
|
||||||
|
`
|
||||||
|
DELETE FROM handmade_project WHERE id = ANY($1)
|
||||||
|
`,
|
||||||
|
projectIds,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return oops.New(err, "failed to delete user's projects")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return oops.New(err, "failed to commit transaction")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue