@@ -9,6 +9,12 @@ import ( | |||||
"xorm.io/builder" | "xorm.io/builder" | ||||
) | ) | ||||
const ( | |||||
DatasetStatusPrivate int32 = iota | |||||
DatasetStatusPublic | |||||
DatasetStatusDeleted | |||||
) | |||||
type Dataset struct { | type Dataset struct { | ||||
ID int64 `xorm:"pk autoincr"` | ID int64 `xorm:"pk autoincr"` | ||||
Title string `xorm:"INDEX NOT NULL"` | Title string `xorm:"INDEX NOT NULL"` | ||||
@@ -23,6 +29,7 @@ type Dataset struct { | |||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` | CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` | ||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` | UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` | ||||
User *User `xorm:"-"` | |||||
Attachments []*Attachment `xorm:"-"` | Attachments []*Attachment `xorm:"-"` | ||||
} | } | ||||
@@ -41,14 +48,36 @@ func (d *Dataset) IsPrivate() bool { | |||||
type DatasetList []*Dataset | type DatasetList []*Dataset | ||||
const ( | |||||
DatasetStatusPrivate int32 = iota | |||||
DatasetStatusPublic | |||||
DatasetStatusDeleted | |||||
) | |||||
func (datasets DatasetList) loadAttributes(e Engine) error { | |||||
if len(datasets) == 0 { | |||||
return nil | |||||
} | |||||
set := make(map[int64]struct{}) | |||||
datasetIDs := make([]int64, len(datasets)) | |||||
for i := range datasets { | |||||
set[datasets[i].UserID] = struct{}{} | |||||
datasetIDs[i] = datasets[i].ID | |||||
} | |||||
// Load owners. | |||||
users := make(map[int64]*User, len(set)) | |||||
if err := e. | |||||
Where("id > 0"). | |||||
In("id", keysInt64(set)). | |||||
Find(&users); err != nil { | |||||
return fmt.Errorf("find users: %v", err) | |||||
} | |||||
for i := range datasets { | |||||
datasets[i].User = users[datasets[i].UserID] | |||||
} | |||||
return nil | |||||
} | |||||
type SearchDatasetOptions struct { | type SearchDatasetOptions struct { | ||||
Keyword string | Keyword string | ||||
doerID int64 | |||||
OwnerID int64 | OwnerID int64 | ||||
IsPublic bool | IsPublic bool | ||||
ListOptions | ListOptions | ||||
@@ -71,20 +100,30 @@ func SearchDataset(opts *SearchDatasetOptions) (DatasetList, int64, error) { | |||||
func SearchDatasetCondition(opts *SearchDatasetOptions) builder.Cond { | func SearchDatasetCondition(opts *SearchDatasetOptions) builder.Cond { | ||||
var cond = builder.NewCond() | var cond = builder.NewCond() | ||||
cond = cond.And(builder.Neq{"status": DatasetStatusDeleted}) | cond = cond.And(builder.Neq{"status": DatasetStatusDeleted}) | ||||
if len(opts.Keyword) > 0 { | if len(opts.Keyword) > 0 { | ||||
cond = cond.And(builder.Like{"title", opts.Keyword}) | cond = cond.And(builder.Like{"title", opts.Keyword}) | ||||
} | } | ||||
if opts.IsPublic { | |||||
cond = cond.And(builder.Eq{"status": DatasetStatusPublic}) | |||||
if opts.OwnerID > 0 { | |||||
cond = cond.Or(builder.Eq{"user_id": opts.OwnerID}) | |||||
if opts.doerID > 0 { | |||||
doer, err := GetUserByID(opts.doerID) | |||||
if err != nil { | |||||
return nil | |||||
} | |||||
if doer.IsAdmin { | |||||
} | |||||
} else { | |||||
if opts.IsPublic { | |||||
cond = cond.And(builder.Eq{"status": DatasetStatusPublic}) | |||||
if opts.OwnerID > 0 { | |||||
cond = cond.Or(builder.Eq{"user_id": opts.OwnerID}) | |||||
} | |||||
} | } | ||||
} | |||||
if opts.OwnerID > 0 { | |||||
cond = cond.And(builder.Eq{"user_id": opts.OwnerID}) | |||||
if opts.OwnerID > 0 { | |||||
cond = cond.And(builder.Eq{"user_id": opts.OwnerID}) | |||||
} | |||||
} | } | ||||
return cond | return cond | ||||
@@ -114,6 +153,10 @@ func SearchDatasetByCondition(opts *SearchDatasetOptions, cond builder.Cond) (Da | |||||
return nil, 0, fmt.Errorf("Dataset: %v", err) | return nil, 0, fmt.Errorf("Dataset: %v", err) | ||||
} | } | ||||
if err = datasets.loadAttributes(sess); err != nil { | |||||
return nil, 0, fmt.Errorf("LoadAttributes: %v", err) | |||||
} | |||||
return datasets, count, nil | return datasets, count, nil | ||||
} | } | ||||
@@ -216,6 +259,34 @@ func GetDatasetByID(id int64) (*Dataset, error) { | |||||
return rel, nil | return rel, nil | ||||
} | } | ||||
func DeleteDataset(datasetID int64, uid int64) error { | |||||
var err error | |||||
sess := x.NewSession() | |||||
defer sess.Close() | |||||
if err = sess.Begin(); err != nil { | |||||
return err | |||||
} | |||||
dataset := &Dataset{ID: datasetID, UserID: uid} | |||||
has, err := sess.Get(dataset) | |||||
if err != nil { | |||||
return err | |||||
} else if !has { | |||||
return errors.New("not found") | |||||
} | |||||
if cnt, err := sess.ID(datasetID).Delete(new(Dataset)); err != nil { | |||||
return err | |||||
} else if cnt != 1 { | |||||
return errors.New("not found") | |||||
} | |||||
if err = sess.Commit(); err != nil { | |||||
sess.Close() | |||||
return fmt.Errorf("Commit: %v", err) | |||||
} | |||||
return nil | |||||
} | |||||
func GetOwnerDatasetByID(id int64, user *User) (*Dataset, error) { | func GetOwnerDatasetByID(id int64, user *User) (*Dataset, error) { | ||||
dataset, err := GetDatasetByID(id) | dataset, err := GetDatasetByID(id) | ||||
if err != nil { | if err != nil { | ||||
@@ -641,6 +641,12 @@ visibility_description=只有组织所有人或拥有权利的组织成员才能 | |||||
visibility_helper=将数据集设为私有 | visibility_helper=将数据集设为私有 | ||||
visibility_helper_forced=站点管理员强制要求新数据集为私有。 | visibility_helper_forced=站点管理员强制要求新数据集为私有。 | ||||
visibility_fork_helper=(修改该值将会影响到所有派生数据集) | visibility_fork_helper=(修改该值将会影响到所有派生数据集) | ||||
settings.delete=删除本据集 | |||||
settings.delete_desc=删除据集是永久性的, 无法撤消。 | |||||
settings.delete_notices_1=- 此操作 <strong>不可以</strong> 被回滚。 | |||||
settings.delete_notices_2=- 此操作将永久删除据集 <strong>%s</strong>。 | |||||
settings.delete_notices_fork_1=- 在此仓库删除后,它的派生据集将变成独立据集。 | |||||
settings.deletion_success=据集已被删除。 | |||||
[repo] | [repo] | ||||
owner=拥有者 | owner=拥有者 | ||||
@@ -1846,6 +1852,7 @@ teams.all_repositories_admin_permission_desc=该团队拥有 <strong>管理</str | |||||
dashboard=管理面板 | dashboard=管理面板 | ||||
users=帐户管理 | users=帐户管理 | ||||
organizations=组织管理 | organizations=组织管理 | ||||
datasets=数据集 | |||||
repositories=仓库管理 | repositories=仓库管理 | ||||
hooks=默认Web钩子 | hooks=默认Web钩子 | ||||
systemhooks=系统 Web 钩子 | systemhooks=系统 Web 钩子 | ||||
@@ -1992,6 +1999,11 @@ repos.forks=派生数 | |||||
repos.issues=工单数 | repos.issues=工单数 | ||||
repos.size=大小 | repos.size=大小 | ||||
datasets.dataset_manage_panel=数据集管理 | |||||
datasets.owner=所有者 | |||||
datasets.name=名称 | |||||
datasets.private=私有 | |||||
hooks.desc=当某些 Gitea 事件触发时, Web 钩子会自动向服务器发出 HTTP POST 请求。此处定义的 Web 钩子是默认值, 将复制到所有新建仓库中。参阅 <a target="_blank" rel="noopener" href="https://docs.gitea.io/en-us/webhooks/">Web钩子指南</a> 获取更多内容。 | hooks.desc=当某些 Gitea 事件触发时, Web 钩子会自动向服务器发出 HTTP POST 请求。此处定义的 Web 钩子是默认值, 将复制到所有新建仓库中。参阅 <a target="_blank" rel="noopener" href="https://docs.gitea.io/en-us/webhooks/">Web钩子指南</a> 获取更多内容。 | ||||
hooks.add_webhook=新增默认Web钩子 | hooks.add_webhook=新增默认Web钩子 | ||||
hooks.update_webhook=更新默认Web钩子 | hooks.update_webhook=更新默认Web钩子 | ||||
@@ -0,0 +1,115 @@ | |||||
package admin | |||||
import ( | |||||
"strings" | |||||
"code.gitea.io/gitea/models" | |||||
"code.gitea.io/gitea/modules/base" | |||||
"code.gitea.io/gitea/modules/context" | |||||
"code.gitea.io/gitea/modules/log" | |||||
"code.gitea.io/gitea/modules/setting" | |||||
) | |||||
const ( | |||||
tplDatasets base.TplName = "admin/dataset/list" | |||||
) | |||||
func Datasets(ctx *context.Context) { | |||||
ctx.Data["Title"] = ctx.Tr("admin.datasets") | |||||
ctx.Data["PageIsAdmin"] = true | |||||
ctx.Data["PageIsAdminDatasets"] = true | |||||
page := ctx.QueryInt("page") | |||||
if page <= 0 { | |||||
page = 1 | |||||
} | |||||
var ( | |||||
datasets []*models.Dataset | |||||
count int64 | |||||
err error | |||||
orderBy models.SearchOrderBy | |||||
) | |||||
ctx.Data["SortType"] = ctx.Query("sort") | |||||
switch ctx.Query("sort") { | |||||
case "newest": | |||||
orderBy = models.SearchOrderByNewest | |||||
case "oldest": | |||||
orderBy = models.SearchOrderByOldest | |||||
case "recentupdate": | |||||
orderBy = models.SearchOrderByRecentUpdated | |||||
case "leastupdate": | |||||
orderBy = models.SearchOrderByLeastUpdated | |||||
case "reversealphabetically": | |||||
orderBy = models.SearchOrderByAlphabeticallyReverse | |||||
case "alphabetically": | |||||
orderBy = models.SearchOrderByAlphabetically | |||||
case "reversesize": | |||||
orderBy = models.SearchOrderBySizeReverse | |||||
case "size": | |||||
orderBy = models.SearchOrderBySize | |||||
case "moststars": | |||||
orderBy = models.SearchOrderByStarsReverse | |||||
case "feweststars": | |||||
orderBy = models.SearchOrderByStars | |||||
case "mostforks": | |||||
orderBy = models.SearchOrderByForksReverse | |||||
case "fewestforks": | |||||
orderBy = models.SearchOrderByForks | |||||
default: | |||||
ctx.Data["SortType"] = "recentupdate" | |||||
orderBy = models.SearchOrderByRecentUpdated | |||||
} | |||||
keyword := strings.Trim(ctx.Query("q"), " ") | |||||
var uid int64 | |||||
if ctx.User != nil { | |||||
uid = ctx.User.ID | |||||
} else { | |||||
uid = 0 | |||||
} | |||||
datasets, count, err = models.SearchDataset(&models.SearchDatasetOptions{ | |||||
ListOptions: models.ListOptions{ | |||||
Page: page, | |||||
PageSize: setting.UI.ExplorePagingNum, | |||||
}, | |||||
Keyword: keyword, | |||||
OwnerID: uid, | |||||
SearchOrderBy: orderBy, | |||||
}) | |||||
if err != nil { | |||||
ctx.ServerError("SearchDataset", err) | |||||
return | |||||
} | |||||
ctx.Data["Keyword"] = keyword | |||||
ctx.Data["Total"] = count | |||||
ctx.Data["Datasets"] = datasets | |||||
pager := context.NewPagination(int(count), setting.UI.ExplorePagingNum, page, 5) | |||||
pager.SetDefaultParams(ctx) | |||||
ctx.Data["Page"] = pager | |||||
ctx.HTML(200, tplDatasets) | |||||
} | |||||
func DeleteDataset(ctx *context.Context) { | |||||
dataset, err := models.GetDatasetByID(ctx.QueryInt64("id")) | |||||
if err != nil { | |||||
ctx.ServerError("GetDatasetByID", err) | |||||
return | |||||
} | |||||
if err := models.DeleteDataset(dataset.ID, ctx.User.ID); err != nil { | |||||
ctx.ServerError("DeleteDataset", err) | |||||
return | |||||
} | |||||
log.Trace("DeleteDataset deleted: %s", dataset.ID) | |||||
ctx.Flash.Success(ctx.Tr("dataset.settings.deletion_success")) | |||||
ctx.JSON(200, map[string]interface{}{ | |||||
"redirect": setting.AppSubURL + "/admin/datasets?page=" + ctx.Query("page") + "&sort=" + ctx.Query("sort"), | |||||
}) | |||||
} |
@@ -464,6 +464,11 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
m.Post("/delete", admin.DeleteRepo) | m.Post("/delete", admin.DeleteRepo) | ||||
}) | }) | ||||
m.Group("/datasets", func() { | |||||
m.Get("", admin.Datasets) | |||||
m.Post("/delete", admin.DeleteDataset) | |||||
}) | |||||
m.Group("/^:configType(hooks|system-hooks)$", func() { | m.Group("/^:configType(hooks|system-hooks)$", func() { | ||||
m.Get("", admin.DefaultOrSystemWebhooks) | m.Get("", admin.DefaultOrSystemWebhooks) | ||||
m.Post("/delete", admin.DeleteDefaultOrSystemWebhook) | m.Post("/delete", admin.DeleteDefaultOrSystemWebhook) | ||||
@@ -0,0 +1,59 @@ | |||||
{{template "base/head" .}} | |||||
<div class="admin user"> | |||||
{{template "admin/navbar" .}} | |||||
<div class="ui container"> | |||||
{{template "base/alert" .}} | |||||
<h4 class="ui top attached header"> | |||||
{{.i18n.Tr "admin.datasets.dataset_manage_panel"}} ({{.i18n.Tr "admin.total" .Total}}) | |||||
</h4> | |||||
<div class="ui attached segment"> | |||||
{{template "admin/dataset/search" .}} | |||||
</div> | |||||
<div class="ui attached table segment"> | |||||
<table class="ui very basic striped table"> | |||||
<thead> | |||||
<tr> | |||||
<th>ID</th> | |||||
<th>{{.i18n.Tr "admin.datasets.owner"}}</th> | |||||
<th>{{.i18n.Tr "admin.datasets.name"}}</th> | |||||
<th>{{.i18n.Tr "admin.datasets.private"}}</th> | |||||
<th>{{.i18n.Tr "admin.users.created"}}</th> | |||||
<th>{{.i18n.Tr "admin.notices.op"}}</th> | |||||
</tr> | |||||
</thead> | |||||
<tbody> | |||||
{{range .Datasets}} | |||||
<tr> | |||||
<td>{{.ID}}</td> | |||||
<td> | |||||
<a href="{{AppSubUrl}}/{{.User.Name}}">{{.User.Name}}</a> | |||||
{{if .User.Visibility.IsPrivate}} | |||||
<span class="text gold">{{svg "octicon-lock" 16}}</span> | |||||
{{end}} | |||||
</td> | |||||
<td><a href="{{AppSubUrl}}/">{{.Title}}</a></td> | |||||
<td><i class="fa fa{{if .IsPrivate}}-check{{end}}-square-o"></i></td> | |||||
<td><span title="{{.CreatedUnix.FormatLong}}">{{.CreatedUnix.FormatShort}}</span></td> | |||||
<td><a class="delete-button" href="" data-url="{{$.Link}}/delete?page={{$.Page.Paginater.Current}}&sort={{$.SortType}}" data-id="{{.ID}}" data-name="{{.Title}}"><i class="trash icon text red"></i></a></td> | |||||
</tr> | |||||
{{end}} | |||||
</tbody> | |||||
</table> | |||||
</div> | |||||
{{template "base/paginate" .}} | |||||
</div> | |||||
</div> | |||||
<div class="ui small basic delete modal"> | |||||
<div class="ui icon header"> | |||||
<i class="trash icon"></i> | |||||
{{.i18n.Tr "dataset.settings.delete"}} | |||||
</div> | |||||
<div class="content"> | |||||
<p>{{.i18n.Tr "dataset.settings.delete_desc"}}</p> | |||||
{{.i18n.Tr "dataset.settings.delete_notices_2" `<span class="name"></span>` | Safe}}<br> | |||||
</div> | |||||
{{template "base/delete_modal_actions" .}} | |||||
</div> | |||||
{{template "base/footer" .}} |
@@ -0,0 +1,29 @@ | |||||
<div class="ui right floated secondary filter menu"> | |||||
<!-- Sort --> | |||||
<div class="ui dropdown type jump item"> | |||||
<span class="text"> | |||||
{{.i18n.Tr "repo.issues.filter_sort"}} | |||||
<i class="dropdown icon"></i> | |||||
</span> | |||||
<div class="menu"> | |||||
<a class='{{if or (eq .SortType "oldest") (not .SortType)}}active{{end}} item" href="{{$.Link}}?sort=oldest&q={{$.Keyword}}'>{{.i18n.Tr "repo.issues.filter_sort.oldest"}}</a> | |||||
<a class='{{if eq .SortType "newest"}}active{{end}} item" href="{{$.Link}}?sort=newest&q={{$.Keyword}}'>{{.i18n.Tr "repo.issues.filter_sort.latest"}}</a> | |||||
<a class="{{if eq .SortType "alphabetically"}}active{{end}} item" href="{{$.Link}}?sort=alphabetically&q={{$.Keyword}}">{{.i18n.Tr "repo.issues.label.filter_sort.alphabetically"}}</a> | |||||
<a class="{{if eq .SortType "reversealphabetically"}}active{{end}} item" href="{{$.Link}}?sort=reversealphabetically&q={{$.Keyword}}">{{.i18n.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a> | |||||
<a class="{{if eq .SortType "recentupdate"}}active{{end}} item" href="{{$.Link}}?sort=recentupdate&q={{$.Keyword}}">{{.i18n.Tr "repo.issues.filter_sort.recentupdate"}}</a> | |||||
<a class="{{if eq .SortType "leastupdate"}}active{{end}} item" href="{{$.Link}}?sort=leastupdate&q={{$.Keyword}}">{{.i18n.Tr "repo.issues.filter_sort.leastupdate"}}</a> | |||||
<a class="{{if eq .SortType "moststars"}}active{{end}} item" href="{{$.Link}}?sort=moststars&q={{$.Keyword}}&tab={{$.TabName}}">{{.i18n.Tr "repo.issues.filter_sort.moststars"}}</a> | |||||
<a class="{{if eq .SortType "feweststars"}}active{{end}} item" href="{{$.Link}}?sort=feweststars&q={{$.Keyword}}&tab={{$.TabName}}">{{.i18n.Tr "repo.issues.filter_sort.feweststars"}}</a> | |||||
<a class="{{if eq .SortType "mostforks"}}active{{end}} item" href="{{$.Link}}?sort=mostforks&q={{$.Keyword}}&tab={{$.TabName}}">{{.i18n.Tr "repo.issues.filter_sort.mostforks"}}</a> | |||||
<a class="{{if eq .SortType "fewestforks"}}active{{end}} item" href="{{$.Link}}?sort=fewestforks&q={{$.Keyword}}&tab={{$.TabName}}">{{.i18n.Tr "repo.issues.filter_sort.fewestforks"}}</a> | |||||
<a class="{{if eq .SortType "size"}}active{{end}} item" href="{{$.Link}}?sort=size&q={{$.Keyword}}">{{.i18n.Tr "repo.issues.label.filter_sort.by_size"}}</a> | |||||
<a class="{{if eq .SortType "reversesize"}}active{{end}} item" href="{{$.Link}}?sort=reversesize&q={{$.Keyword}}">{{.i18n.Tr "repo.issues.label.filter_sort.reverse_by_size"}}</a> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<form class="ui form ignore-dirty" style="max-width: 90%"> | |||||
<div class="ui fluid action input"> | |||||
<input name="q" value="{{.Keyword}}" placeholder="{{.i18n.Tr "explore.search"}}..." autofocus> | |||||
<button class="ui blue button">{{.i18n.Tr "explore.search"}}</button> | |||||
</div> | |||||
</form> |
@@ -11,6 +11,9 @@ | |||||
<a class="{{if .PageIsAdminRepositories}}active{{end}} item" href="{{AppSubUrl}}/admin/repos"> | <a class="{{if .PageIsAdminRepositories}}active{{end}} item" href="{{AppSubUrl}}/admin/repos"> | ||||
{{.i18n.Tr "admin.repositories"}} | {{.i18n.Tr "admin.repositories"}} | ||||
</a> | </a> | ||||
<a class="{{if .PageIsAdminDatasets}}active{{end}} item" href="{{AppSubUrl}}/admin/datasets"> | |||||
{{.i18n.Tr "admin.datasets"}} | |||||
</a> | |||||
<a class="{{if .PageIsAdminHooks}}active{{end}} item" href="{{AppSubUrl}}/admin/hooks"> | <a class="{{if .PageIsAdminHooks}}active{{end}} item" href="{{AppSubUrl}}/admin/hooks"> | ||||
{{.i18n.Tr "admin.hooks"}} | {{.i18n.Tr "admin.hooks"}} | ||||
</a> | </a> | ||||