This commit is contained in:
Carlos Felgueiras 2024-05-19 08:42:21 +01:00 committed by GitHub
commit fe403ce815
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 574 additions and 7 deletions

61
models/repo/pin.go Normal file
View File

@ -0,0 +1,61 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/timeutil"
)
type Pin struct {
ID int64 `xorm:"pk autoincr"`
UID int64 `xorm:"UNIQUE(s)"`
RepoID int64 `xorm:"UNIQUE(s)"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
}
// TableName sets the table name for the pin struct
func (s *Pin) TableName() string {
return "repository_pin"
}
func init() {
db.RegisterModel(new(Pin))
}
func IsPinned(ctx context.Context, userID, repoID int64) bool {
exists, _ := db.GetEngine(ctx).Get(&Pin{UID: userID, RepoID: repoID})
return exists
}
func PinRepo(ctx context.Context, doer *user_model.User, repo *Repository, pin bool) error {
return db.WithTx(ctx, func(ctx context.Context) error {
pinned := IsPinned(ctx, doer.ID, repo.ID)
if pin {
// Already pinned, nothing to do
if pinned {
return nil
}
if err := db.Insert(ctx, &Pin{UID: doer.ID, RepoID: repo.ID}); err != nil {
return err
}
} else {
// Not pinned, nothing to do
if !pinned {
return nil
}
if _, err := db.DeleteByBean(ctx, &Pin{UID: doer.ID, RepoID: repo.ID}); err != nil {
return err
}
}
return nil
})
}

48
models/repo/pin_test.go Normal file
View File

@ -0,0 +1,48 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo_test
import (
"testing"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
)
func TestPinRepoFunctionality(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
unittest.AssertNotExistsBean(t, &repo_model.Pin{UID: user.ID, RepoID: repo.ID})
assert.NoError(t, repo_model.PinRepo(db.DefaultContext, user, repo, true))
unittest.AssertExistsAndLoadBean(t, &repo_model.Pin{UID: user.ID, RepoID: repo.ID})
assert.NoError(t, repo_model.PinRepo(db.DefaultContext, user, repo, true))
unittest.AssertExistsAndLoadBean(t, &repo_model.Pin{UID: user.ID, RepoID: repo.ID})
assert.NoError(t, repo_model.PinRepo(db.DefaultContext, user, repo, false))
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
}
func TestIsPinned(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
unittest.AssertNotExistsBean(t, &repo_model.Pin{UID: user.ID, RepoID: repo.ID})
assert.NoError(t, repo_model.PinRepo(db.DefaultContext, user, repo, true))
unittest.AssertExistsAndLoadBean(t, &repo_model.Pin{UID: user.ID, RepoID: repo.ID})
assert.True(t, repo_model.IsPinned(db.DefaultContext, user.ID, repo.ID))
assert.NoError(t, repo_model.PinRepo(db.DefaultContext, user, repo, false))
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
assert.False(t, repo_model.IsPinned(db.DefaultContext, user.ID, repo.ID))
}

View File

@ -54,6 +54,41 @@ func GetStarredRepos(ctx context.Context, opts *StarredReposOptions) ([]*Reposit
return db.Find[Repository](ctx, opts)
}
type PinnedReposOptions struct {
db.ListOptions
PinnerID int64
RepoOwnerID int64
}
func (opts *PinnedReposOptions) ToConds() builder.Cond {
var cond builder.Cond = builder.Eq{
"repository_pin.uid": opts.PinnerID,
}
if opts.RepoOwnerID != 0 {
cond = cond.And(builder.Eq{
"repository.owner_id": opts.RepoOwnerID,
})
}
return cond
}
func (opts *PinnedReposOptions) ToJoins() []db.JoinFunc {
return []db.JoinFunc{
func(e db.Engine) error {
e.Join("INNER", "repository_pin", "`repository`.id=`repository_pin`.repo_id")
return nil
},
}
}
func GetPinnedRepos(ctx context.Context, opts *PinnedReposOptions) (RepositoryList, error) {
return db.Find[Repository](ctx, opts)
}
func CountPinnedRepos(ctx context.Context, opts *PinnedReposOptions) (int64, error) {
return db.Count[Repository](ctx, opts)
}
type WatchedReposOptions struct {
db.ListOptions
WatcherID int64

View File

@ -1102,6 +1102,7 @@ transfer.no_permission_to_reject = You do not have permission to reject this tra
desc.private = Private
desc.public = Public
desc.template = Template
desc.private_template = Private Template
desc.internal = Internal
desc.archived = Archived
desc.sha256 = SHA256
@ -1187,10 +1188,15 @@ fork_from_self = You cannot fork a repository you own.
fork_guest_user = Sign in to fork this repository.
watch_guest_user = Sign in to watch this repository.
star_guest_user = Sign in to star this repository.
pin_guest_user = Sign in to pin this repository.
unwatch = Unwatch
watch = Watch
unstar = Unstar
star = Star
pin = Pin
unpin = Unpin
pin-org = Pin to %s
unpin-org = Unpin from %s
fork = Fork
action.blocked_user = Cannot perform action because you are blocked by the repository owner.
download_archive = Download Repository

View File

@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/util"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
"code.gitea.io/gitea/services/context"
user_service "code.gitea.io/gitea/services/user"
)
const (
@ -101,9 +102,11 @@ func Home(ctx *context.Context) {
ctx.Data["IsPrivate"] = private
var (
repos []*repo_model.Repository
count int64
err error
repos []*repo_model.Repository
count int64
pinnedRepos []*repo_model.Repository
pinnedCount int64
err error
)
repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
@ -139,8 +142,19 @@ func Home(ctx *context.Context) {
return
}
// Get pinned repos
pinnedRepos, err = user_service.GetUserPinnedRepos(ctx, org.AsUser(), ctx.Doer)
if err != nil {
ctx.ServerError("GetUserPinnedRepos", err)
return
}
pinnedCount = int64(len(pinnedRepos))
ctx.Data["Repos"] = repos
ctx.Data["Total"] = count
ctx.Data["PinnedRepos"] = pinnedRepos
ctx.Data["PinnedTotal"] = pinnedCount
ctx.Data["Members"] = members
ctx.Data["Teams"] = ctx.Org.Teams
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull

View File

@ -36,6 +36,7 @@ import (
repo_service "code.gitea.io/gitea/services/repository"
archiver_service "code.gitea.io/gitea/services/repository/archiver"
commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
user_service "code.gitea.io/gitea/services/user"
)
const (
@ -321,6 +322,14 @@ func Action(ctx *context.Context) {
err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
case "unstar":
err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
case "pin":
err = user_service.PinRepo(ctx, ctx.Doer, ctx.Repo.Repository, true, false)
case "unpin":
err = user_service.PinRepo(ctx, ctx.Doer, ctx.Repo.Repository, false, false)
case "pin-org":
err = user_service.PinRepo(ctx, ctx.Doer, ctx.Repo.Repository, true, true)
case "unpin-org":
err = user_service.PinRepo(ctx, ctx.Doer, ctx.Repo.Repository, false, true)
case "accept_transfer":
err = acceptOrRejectRepoTransfer(ctx, true)
case "reject_transfer":

View File

@ -29,6 +29,7 @@ import (
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issue_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
@ -50,6 +51,7 @@ import (
"code.gitea.io/gitea/services/context"
issue_service "code.gitea.io/gitea/services/issue"
files_service "code.gitea.io/gitea/services/repository/files"
user_service "code.gitea.io/gitea/services/user"
"github.com/nektos/act/pkg/model"
@ -791,6 +793,14 @@ func Home(ctx *context.Context) {
return
}
if ctx.IsSigned {
err := loadPinData(ctx)
if err != nil {
ctx.ServerError("loadPinData", err)
return
}
}
renderHomeCode(ctx)
}
@ -1168,3 +1178,37 @@ func Forks(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplForks)
}
func loadPinData(ctx *context.Context) error {
// First, cleanup any pins that are no longer valid
err := user_service.CleanupPins(ctx, ctx.Doer)
if err != nil {
return err
}
ctx.Data["IsPinningRepo"] = repo_model.IsPinned(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
ctx.Data["CanPinRepo"], err = user_service.CanPin(ctx, ctx.Doer, ctx.Repo.Repository)
if err != nil {
return err
}
if ctx.Repo.Repository.Owner.IsOrganization() {
org := organization.OrgFromUser(ctx.Repo.Repository.Owner)
isAdmin, err := org.IsOrgAdmin(ctx, ctx.Doer.ID)
if err != nil {
return err
}
if isAdmin {
ctx.Data["CanUserPinToOrg"] = true
ctx.Data["IsOrgPinningRepo"] = repo_model.IsPinned(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID)
ctx.Data["CanOrgPinRepo"], err = user_service.CanPin(ctx, ctx.Repo.Repository.Owner, ctx.Repo.Repository)
if err != nil {
return err
}
}
}
return nil
}

View File

@ -26,6 +26,7 @@ import (
"code.gitea.io/gitea/routers/web/org"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
"code.gitea.io/gitea/services/context"
user_service "code.gitea.io/gitea/services/user"
)
const (
@ -104,10 +105,12 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
pagingNum := setting.UI.User.RepoPagingNum
topicOnly := ctx.FormBool("topic")
var (
repos []*repo_model.Repository
count int64
total int
orderBy db.SearchOrderBy
repos []*repo_model.Repository
pinnedRepos []*repo_model.Repository
count int64
pinnedCount int64
total int
orderBy db.SearchOrderBy
)
ctx.Data["SortType"] = ctx.FormString("sort")
@ -312,9 +315,19 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
}
total = int(count)
pinnedRepos, err = user_service.GetUserPinnedRepos(ctx, ctx.ContextUser, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserPinnedRepos", err)
return
}
pinnedCount = int64(len(pinnedRepos))
}
ctx.Data["Repos"] = repos
ctx.Data["Total"] = total
ctx.Data["PinnedRepos"] = pinnedRepos
ctx.Data["PinnedCount"] = pinnedCount
err = shared_user.LoadHeaderCount(ctx)
if err != nil {

View File

@ -151,6 +151,7 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
&repo_model.Redirect{RedirectRepoID: repoID},
&repo_model.RepoUnit{RepoID: repoID},
&repo_model.Star{RepoID: repoID},
&repo_model.Pin{RepoID: repoID},
&admin_model.Task{RepoID: repoID},
&repo_model.Watch{RepoID: repoID},
&webhook.Webhook{RepoID: repoID},

169
services/user/pin.go Normal file
View File

@ -0,0 +1,169 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
import (
"errors"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/services/context"
)
const maxPins = 6
// Check if a user have a new pinned repo in it's profile, meaning that it
// has permissions to pin said repo and also has enough space on the pinned list.
func CanPin(ctx *context.Context, u *user_model.User, r *repo_model.Repository) (bool, error) {
count, err := repo_model.CountPinnedRepos(*ctx, &repo_model.PinnedReposOptions{
ListOptions: db.ListOptions{
ListAll: true,
},
PinnerID: u.ID,
})
if err != nil {
ctx.ServerError("CountPinnedRepos", err)
return false, err
}
if count >= maxPins {
return false, nil
}
return HasPermsToPin(ctx, u, r), nil
}
// Checks if the user has permission to have the repo pinned in it's profile.
func HasPermsToPin(ctx *context.Context, u *user_model.User, r *repo_model.Repository) bool {
// If user is an organization, it can only pin its own repos
if u.IsOrganization() {
return r.OwnerID == u.ID
}
// For normal users, anyone that has read access to the repo can pin it
return canSeePin(ctx, u, r)
}
// Check if a user can see a pin
// A user can see a pin if he has read access to the repo
func canSeePin(ctx *context.Context, u *user_model.User, r *repo_model.Repository) bool {
perm, err := access_model.GetUserRepoPermission(ctx, r, u)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return false
}
return perm.HasAnyUnitAccess()
}
// CleanupPins iterates over the repos pinned by a user and removes
// the invalid pins. (Needs to be called everytime before we read/write a pin)
func CleanupPins(ctx *context.Context, u *user_model.User) error {
pinnedRepos, err := repo_model.GetPinnedRepos(*ctx, &repo_model.PinnedReposOptions{
ListOptions: db.ListOptions{
ListAll: true,
},
PinnerID: u.ID,
})
if err != nil {
return err
}
for _, repo := range pinnedRepos {
if !HasPermsToPin(ctx, u, repo) {
if err := repo_model.PinRepo(*ctx, u, repo, false); err != nil {
return err
}
}
}
return nil
}
// Returns the pinned repos of a user that the viewer can see
func GetUserPinnedRepos(ctx *context.Context, user, viewer *user_model.User) ([]*repo_model.Repository, error) {
// Start by cleaning up the invalid pins
err := CleanupPins(ctx, user)
if err != nil {
return nil, err
}
// Get all of the user's pinned repos
pinnedRepos, err := repo_model.GetPinnedRepos(*ctx, &repo_model.PinnedReposOptions{
ListOptions: db.ListOptions{
ListAll: true,
},
PinnerID: user.ID,
})
if err != nil {
return nil, err
}
var repos []*repo_model.Repository
// Only include the repos that the viewer can see
for _, repo := range pinnedRepos {
if canSeePin(ctx, viewer, repo) {
repos = append(repos, repo)
}
}
return repos, nil
}
func PinRepo(ctx *context.Context, doer *user_model.User, repo *repo_model.Repository, pin, toOrg bool) error {
// Determine the user which profile is the target for the pin
var targetUser *user_model.User
if toOrg {
targetUser = repo.Owner
} else {
targetUser = doer
}
// Start by cleaning up the invalid pins
err := CleanupPins(ctx, targetUser)
if err != nil {
return err
}
// If target is org profile, need to check if the doer can pin the repo
// on said org profile
if toOrg {
err = assertUserOrgPerms(ctx, doer, repo)
if err != nil {
return err
}
}
if pin {
canPin, err := CanPin(ctx, targetUser, repo)
if err != nil {
return err
}
if !canPin {
return errors.New("user cannot pin this repository")
}
}
return repo_model.PinRepo(*ctx, targetUser, repo, pin)
}
func assertUserOrgPerms(ctx *context.Context, doer *user_model.User, repo *repo_model.Repository) error {
if !ctx.Repo.Owner.IsOrganization() {
return errors.New("owner is not an organization")
}
isAdmin, err := organization.OrgFromUser(repo.Owner).IsOrgAdmin(ctx, doer.ID)
if err != nil {
return err
}
if !isAdmin {
return errors.New("user is not an admin of this organization")
}
return nil
}

View File

@ -8,6 +8,9 @@
{{if .ProfileReadme}}
<div id="readme_profile" class="markup">{{.ProfileReadme}}</div>
{{end}}
{{if .PinnedRepos}}
{{template "shared/pinned_repo_cards" .}}
{{end}}
{{template "shared/repo_search" .}}
{{template "explore/repo_list" .}}
{{template "base/paginate" .}}

View File

@ -60,6 +60,7 @@
{{svg "octicon-rss" 16}}
</a>
{{end}}
{{template "repo/pin_unpin" $}}
{{template "repo/watch_unwatch" $}}
{{if not $.DisableStars}}
{{template "repo/star_unstar" $}}

View File

@ -0,0 +1,30 @@
<div class="ui buttons">
<form hx-boost="true" hx-target="this" method="post" action="{{$.RepoLink}}/action/{{if .IsPinningRepo}}unpin{{else}}pin{{end}}">
<div class="ui labeled item" {{if not $.IsSigned}}data-tooltip-content="{{ctx.Locale.Tr "repo.pin_guest_user"}}"{{end}}>
{{$buttonText := ctx.Locale.Tr "repo.pin"}}
{{if $.IsPinningRepo}}{{$buttonText = ctx.Locale.Tr "repo.unpin"}}{{end}}
<button type="submit" class="ui compact small basic button"{{if or (not $.IsSigned) (and (not $.IsPinningRepo) (not .CanPinRepo))}} disabled{{end}} aria-label="{{$buttonText}}">
{{if $.IsPinningRepo}}{{svg "octicon-pin-slash"}}{{else}}{{svg "octicon-pin"}}{{end}}
<span class="not-mobile" aria-hidden="true">{{$buttonText}}</span>
</button>
</div>
</form>
{{if .CanUserPinToOrg}}
<div class="ui floating dropdown icon button">
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="compact menu">
<form hx-boost="true" hx-target="this" method="post" action="{{$.RepoLink}}/action/{{if .IsOrgPinningRepo}}unpin{{else}}pin{{end}}-org">
<div class="ui labeled button item" {{if not $.IsSigned}}data-tooltip-content="{{ctx.Locale.Tr "repo.pin_guest_user"}}"{{end}}>
{{$buttonText = ctx.Locale.Tr "repo.pin-org" .Owner.Name}}
{{if $.IsOrgPinningRepo}}{{$buttonText = ctx.Locale.Tr "repo.unpin-org" .Owner.Name}}{{end}}
<button type="submit" class="ui compact small basic button"{{if or (not $.IsSigned) (and (not $.IsOrgPinningRepo) (not .CanOrgPinRepo))}} disabled{{end}} aria-label="{{$buttonText}}">
{{if $.IsOrgPinningRepo}}{{svg "octicon-pin-slash"}}{{else}}{{svg "octicon-pin"}}{{end}}
<span class="not-mobile" aria-hidden="true">{{$buttonText}}</span>
</button>
</div>
</form>
</div>
</div>
{{end}}
</div>

View File

@ -0,0 +1,62 @@
<div class="ui three stackable cards">
{{range .PinnedRepos}}
<div class="ui card pin-repo-item">
<div class="content">
<div class="header">
<div class="flex-item tw-items-center">
<div class="flex-item-main">
<a class="name" href="{{.Link}}">
{{if or $.PageIsExplore $.PageIsProfileStarList}}{{if .Owner}}{{.Owner.Name}} / {{end}}{{end}}{{.Name}}
</a>
</div>
<div class="flex-item-trailing">
{{if .IsArchived}}
<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.archived"}}</span>
{{end}}
{{if .IsTemplate}}
{{if .IsPrivate}}
<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.private_template"}}</span>
{{end}}
{{else}}
{{if .IsPrivate}}
<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.private"}}</span>
{{end}}
{{end}}
{{if .IsFork}}
{{svg "octicon-repo-forked"}}
{{else if .IsMirror}}
{{svg "octicon-mirror"}}
{{end}}
</div>
</div>
</div>
<div class="extra content">
<div class="metas df ac">
{{if .PrimaryLanguage}}
<span class="text grey df ac mr-3"><i class="color-icon mr-3" style="background-color: {{.PrimaryLanguage.Color}}"></i>{{.PrimaryLanguage.Language}}</span>
{{end}}
{{if not $.DisableStars}}
<span class="text grey df ac mr-3">{{svg "octicon-star" 16 "mr-3"}}{{.NumStars}}</span>
{{end}}
<span class="text grey df ac mr-3">{{svg "octicon-git-branch" 16 "mr-3"}}{{.NumForks}}</span>
</div>
</div>
<div class="description">
{{$description := .DescriptionHTML $.Context}}
{{if $description}}<p>{{$description}}</p>{{end}}
{{if .Topics}}
<div class="ui tags">
{{range .Topics}}
{{if ne . ""}}
<a href="{{AppSubUrl}}/explore/repos?q={{.}}&topic=1">
<div class="ui small label topic">{{.}}</div>
</a>
{{end}}
{{end}}
</div>
{{end}}
</div>
</div>
</div>
{{end}}
</div>

View File

@ -28,6 +28,9 @@
{{else if eq .TabName "overview"}}
<div id="readme_profile" class="markup">{{.ProfileReadme}}</div>
{{else}}
{{if .PinnedRepos}}
{{template "shared/pinned_repo_cards" .}}
{{end}}
{{template "shared/repo_search" .}}
{{template "explore/repo_list" .}}
{{template "base/paginate" .}}

View File

@ -0,0 +1,63 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"net/url"
"path"
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
)
func TestUserRepoPin(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
session := loginUser(t, "user2")
unittest.AssertNotExistsBean(t, &repo_model.Pin{UID: 2, RepoID: 3})
testUserPinRepo(t, session, "org3", "repo3", true, false)
unittest.AssertExistsAndLoadBean(t, &repo_model.Pin{UID: 2, RepoID: 3})
testUserPinRepo(t, session, "org3", "repo3", false, false)
unittest.AssertNotExistsBean(t, &repo_model.Pin{UID: 2, RepoID: 3})
})
}
func TestOrgRepoPin(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
session := loginUser(t, "user2")
unittest.AssertNotExistsBean(t, &repo_model.Pin{UID: 3, RepoID: 3})
testUserPinRepo(t, session, "org3", "repo3", true, true)
unittest.AssertExistsAndLoadBean(t, &repo_model.Pin{UID: 3, RepoID: 3})
testUserPinRepo(t, session, "org3", "repo3", false, true)
unittest.AssertNotExistsBean(t, &repo_model.Pin{UID: 3, RepoID: 3})
})
}
func testUserPinRepo(t *testing.T, session *TestSession, user, repo string, pin, org bool) error {
var action string
if pin {
action = "pin"
} else {
action = "unpin"
}
if org {
action += "-org"
}
// Get repo page to get the CSRF token
reqPage := NewRequest(t, "GET", path.Join(user, repo))
respPage := session.MakeRequest(t, reqPage, http.StatusOK)
htmlDoc := NewHTMLParser(t, respPage.Body)
reqPath := path.Join(user, repo, "action", action)
req := NewRequestWithValues(t, "POST", reqPath, map[string]string{
"_csrf": htmlDoc.GetCSRF(),
})
session.MakeRequest(t, req, http.StatusSeeOther)
return nil
}

View File

@ -138,3 +138,8 @@
.notifications-item:hover .notifications-updated {
display: none;
}
.pin-repo-item {
width: calc(33.33333333333333% - 1em) !important;
margin: 0.4375em 0.5em !important;
}