| @@ -464,3 +464,12 @@ func CanDelAttachment(isSigned bool, user *User, attach *Attachment) bool { | |||
| } | |||
| return false | |||
| } | |||
| func GetAttachmentSizeByDatasetID(datasetID int64) (int64, error) { | |||
| total, err := x.Where("dataset_id = ?", datasetID).SumInt(&Attachment{}, "size") | |||
| if err != nil { | |||
| return 0, err | |||
| } | |||
| return total, nil | |||
| } | |||
| @@ -687,7 +687,7 @@ func CanDelJob(isSigned bool, user *User, job *CloudbrainInfo) bool { | |||
| return false | |||
| } | |||
| if user.ID == job.UserID || user.IsAdmin || permission.AccessMode >= AccessModeAdmin { | |||
| if (user.ID == job.UserID && permission.AccessMode >= AccessModeWrite) || user.IsAdmin || permission.AccessMode >= AccessModeAdmin { | |||
| return true | |||
| } | |||
| return false | |||
| @@ -1016,3 +1016,19 @@ func UpdateCommentsMigrationsByType(tp structs.GitServiceType, originalAuthorID | |||
| }) | |||
| return err | |||
| } | |||
| func GetCommentCountByRepoID(repoID int64) (int64, error) { | |||
| //sql := fmt.Sprintf("select count(1) from comment where issue_id in (select id from issue where repo_id = %d) and type = %d;", repoID, CommentTypeComment) | |||
| //res, err := x.Query(sql) | |||
| //if err != nil { | |||
| // return 0, err | |||
| //} | |||
| //return int64(binary.BigEndian.Uint64(res[0]["count"])), nil | |||
| total, err := x.Where("issue_id in (select id from issue where repo_id = ?) and type = ?", repoID, CommentTypeComment).Count(&Comment{}) | |||
| if err != nil { | |||
| return 0, err | |||
| } | |||
| return total, nil | |||
| } | |||
| @@ -136,7 +136,7 @@ func init() { | |||
| ) | |||
| tablesStatistic = append(tablesStatistic, | |||
| new(FileChunk), | |||
| new(RepoStatistic), | |||
| new(UserBusinessAnalysis), | |||
| ) | |||
| @@ -331,7 +331,7 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { | |||
| } | |||
| if opts.TopicName != "" { | |||
| var subQueryCond = builder.NewCond() | |||
| subQueryCond = subQueryCond.Or(builder.Eq{"topic.name": opts.TopicName}) | |||
| subQueryCond = subQueryCond.Or(builder.Eq{"topic.name": strings.ToLower(opts.TopicName)}) | |||
| subQuery := builder.Select("repo_topic.repo_id").From("repo_topic"). | |||
| Join("INNER", "topic", "topic.id = repo_topic.topic_id"). | |||
| Where(subQueryCond). | |||
| @@ -0,0 +1,60 @@ | |||
| package models | |||
| import ( | |||
| "code.gitea.io/gitea/modules/timeutil" | |||
| "fmt" | |||
| ) | |||
| // RepoStatistic statistic info of all repository | |||
| type RepoStatistic struct { | |||
| ID int64 `xorm:"pk autoincr"` | |||
| RepoID int64 `xorm:"unique(s) NOT NULL"` | |||
| Date string `xorm:"unique(s) NOT NULL"` | |||
| NumWatches int64 `xorm:"NOT NULL DEFAULT 0"` | |||
| NumStars int64 `xorm:"NOT NULL DEFAULT 0"` | |||
| NumForks int64 `xorm:"NOT NULL DEFAULT 0"` | |||
| NumDownloads int64 `xorm:"NOT NULL DEFAULT 0"` | |||
| NumComments int64 `xorm:"NOT NULL DEFAULT 0"` | |||
| NumVisits int64 `xorm:"NOT NULL DEFAULT 0"` | |||
| NumClosedIssues int64 `xorm:"NOT NULL DEFAULT 0"` | |||
| NumVersions int64 `xorm:"NOT NULL DEFAULT 0"` | |||
| //develop months | |||
| NumDevMonths int64 `xorm:"NOT NULL DEFAULT 0"` | |||
| RepoSize int64 `xorm:"NOT NULL DEFAULT 0"` | |||
| DatasetSize int64 `xorm:"NOT NULL DEFAULT 0"` | |||
| NumModels int64 `xorm:"NOT NULL DEFAULT 0"` | |||
| NumWikiViews int64 `xorm:"NOT NULL DEFAULT 0"` | |||
| NumCommits int64 `xorm:"NOT NULL DEFAULT 0"` | |||
| NumIssues int64 `xorm:"NOT NULL DEFAULT 0"` | |||
| NumPulls int64 `xorm:"NOT NULL DEFAULT 0"` | |||
| IssueFixedRate float32 `xorm:"NOT NULL"` | |||
| NumContributor int64 `xorm:"NOT NULL DEFAULT 0"` | |||
| NumKeyContributor int64 `xorm:"NOT NULL DEFAULT 0"` | |||
| CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` | |||
| UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` | |||
| } | |||
| func DeleteRepoStatDaily(date string) error { | |||
| sess := xStatistic.NewSession() | |||
| defer sess.Close() | |||
| if err := sess.Begin(); err != nil { | |||
| return fmt.Errorf("Begin: %v", err) | |||
| } | |||
| if _, err := sess.Where("date = ?", date).Delete(&RepoStatistic{}); err != nil { | |||
| return fmt.Errorf("Delete: %v", err) | |||
| } | |||
| if err := sess.Commit(); err != nil { | |||
| sess.Close() | |||
| return fmt.Errorf("Commit: %v", err) | |||
| } | |||
| sess.Close() | |||
| return nil | |||
| } | |||
| func InsertRepoStat(repoStat *RepoStatistic) (int64, error) { | |||
| return xStatistic.Insert(repoStat) | |||
| } | |||
| @@ -163,6 +163,28 @@ func registerHandleBlockChainUnSuccessCommits() { | |||
| }) | |||
| } | |||
| func registerHandleRepoStatistic() { | |||
| RegisterTaskFatal("handle_repo_statistic", &BaseConfig{ | |||
| Enabled: true, | |||
| RunAtStart: false, | |||
| Schedule: "@daily", | |||
| }, func(ctx context.Context, _ *models.User, _ Config) error { | |||
| repo.RepoStatisticAuto() | |||
| return nil | |||
| }) | |||
| } | |||
| func registerHandleUserStatistic() { | |||
| RegisterTaskFatal("handle_user_statistic", &BaseConfig{ | |||
| Enabled: true, | |||
| RunAtStart: false, | |||
| Schedule: "@daily", | |||
| }, func(ctx context.Context, _ *models.User, _ Config) error { | |||
| repo.TimingCountData() | |||
| return nil | |||
| }) | |||
| } | |||
| func initBasicTasks() { | |||
| registerUpdateMirrorTask() | |||
| registerRepoHealthCheck() | |||
| @@ -177,4 +199,7 @@ func initBasicTasks() { | |||
| registerHandleBlockChainUnSuccessRepos() | |||
| registerHandleBlockChainMergedPulls() | |||
| registerHandleBlockChainUnSuccessCommits() | |||
| registerHandleRepoStatistic() | |||
| registerHandleUserStatistic() | |||
| } | |||
| @@ -42,7 +42,8 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
| m.Post("/manager/shutdown", Shutdown) | |||
| m.Post("/manager/restart", Restart) | |||
| m.Post("/manager/flush-queues", bind(private.FlushOptions{}), FlushQueues) | |||
| m.Post("/cmd/update_all_repo_commit_cnt", UpdateAllRepoCommitCnt) | |||
| m.Post("/tool/update_all_repo_commit_cnt", UpdateAllRepoCommitCnt) | |||
| m.Post("/tool/repo_stat", RepoStatisticManually) | |||
| }, CheckInternalToken) | |||
| } | |||
| @@ -5,11 +5,13 @@ | |||
| package private | |||
| import ( | |||
| "gitea.com/macaron/macaron" | |||
| "net/http" | |||
| "code.gitea.io/gitea/models" | |||
| "code.gitea.io/gitea/modules/log" | |||
| "code.gitea.io/gitea/routers/repo" | |||
| "gitea.com/macaron/macaron" | |||
| ) | |||
| func UpdateAllRepoCommitCnt(ctx *macaron.Context) { | |||
| @@ -35,3 +37,8 @@ func UpdateAllRepoCommitCnt(ctx *macaron.Context) { | |||
| "error_msg": "", | |||
| }) | |||
| } | |||
| func RepoStatisticManually(ctx *macaron.Context) { | |||
| date := ctx.Query("date") | |||
| repo.RepoStatisticDaily(date) | |||
| } | |||
| @@ -0,0 +1,122 @@ | |||
| package repo | |||
| import ( | |||
| "time" | |||
| "code.gitea.io/gitea/models" | |||
| "code.gitea.io/gitea/modules/log" | |||
| ) | |||
| //auto daily or manually | |||
| func RepoStatisticAuto() { | |||
| log.Info("", time.Now()) | |||
| yesterday := time.Now().AddDate(0, 0, -1).Format("2006-01-02") | |||
| RepoStatisticDaily(yesterday) | |||
| } | |||
| func RepoStatisticDaily(date string) { | |||
| log.Info("%s", date) | |||
| if err := models.DeleteRepoStatDaily(date); err != nil { | |||
| log.Error("DeleteRepoStatDaily failed: %v", err.Error()) | |||
| return | |||
| } | |||
| repos, err := models.GetAllRepositories() | |||
| if err != nil { | |||
| log.Error("GetAllRepositories failed: %v", err.Error()) | |||
| return | |||
| } | |||
| for _, repo := range repos { | |||
| log.Info("start statistic: %s", repo.Name) | |||
| repoGitStat, err := models.GetRepoKPIStats(repo) | |||
| if err != nil { | |||
| log.Error("GetRepoKPIStats failed: %s", repo.Name) | |||
| log.Error("failed statistic: %s", repo.Name) | |||
| continue | |||
| } | |||
| var issueFixedRate float32 | |||
| if repo.NumIssues != 0 { | |||
| issueFixedRate = float32(repo.NumClosedIssues) / float32(repo.NumIssues) | |||
| } | |||
| numVersions, err := models.GetReleaseCountByRepoID(repo.ID, models.FindReleasesOptions{}) | |||
| if err != nil { | |||
| log.Error("GetReleaseCountByRepoID failed: %s", repo.Name) | |||
| log.Error("failed statistic: %s", repo.Name) | |||
| continue | |||
| } | |||
| datasetSize, err := getDatasetSize(repo) | |||
| if err != nil { | |||
| log.Error("getDatasetSize failed: %s", repo.Name) | |||
| log.Error("failed statistic: %s", repo.Name) | |||
| continue | |||
| } | |||
| numComments, err := models.GetCommentCountByRepoID(repo.ID) | |||
| if err != nil { | |||
| log.Error("GetCommentCountByRepoID failed: %s", repo.Name) | |||
| log.Error("failed statistic: %s", repo.Name) | |||
| continue | |||
| } | |||
| //beginTime, endTime := getStatTime(date) | |||
| //numVisits := repository.AppointProjectView(repo.OwnerName, repo.Name, beginTime, endTime) | |||
| numVisits := 0 | |||
| repoStat := models.RepoStatistic{ | |||
| RepoID: repo.ID, | |||
| Date: date, | |||
| NumWatches: int64(repo.NumWatches), | |||
| NumStars: int64(repo.NumStars), | |||
| NumDownloads: repo.CloneCnt, | |||
| NumComments: numComments, | |||
| NumVisits: int64(numVisits), | |||
| NumClosedIssues: int64(repo.NumClosedIssues), | |||
| NumVersions: numVersions, | |||
| NumDevMonths: repoGitStat.DevelopAge, | |||
| RepoSize: repo.Size, | |||
| DatasetSize: datasetSize, | |||
| NumModels: 0, | |||
| NumWikiViews: repoGitStat.WikiPages, | |||
| NumCommits: repo.NumCommit, | |||
| NumIssues: int64(repo.NumIssues), | |||
| NumPulls: int64(repo.NumPulls), | |||
| IssueFixedRate: issueFixedRate, | |||
| NumContributor: repoGitStat.Contributors, | |||
| NumKeyContributor: repoGitStat.KeyContributors, | |||
| } | |||
| if _, err = models.InsertRepoStat(&repoStat); err != nil { | |||
| log.Error("InsertRepoStat failed: %s", repo.Name) | |||
| log.Error("failed statistic: %s", repo.Name) | |||
| continue | |||
| } | |||
| log.Info("finish statistic: %s", repo.Name) | |||
| } | |||
| } | |||
| func getDatasetSize(repo *models.Repository) (int64, error) { | |||
| dataset, err := models.GetDatasetByRepo(repo) | |||
| if err != nil { | |||
| return 0, err | |||
| } | |||
| return models.GetAttachmentSizeByDatasetID(dataset.ID) | |||
| } | |||
| func getStatTime(timeStr string) (string, string) { | |||
| t, _ := time.Parse("2006-01-02", timeStr) | |||
| timeNumber := t.Unix() | |||
| beginTimeNumber := timeNumber - 8*60*60 | |||
| endTimeNumber := timeNumber + 16*60*60 | |||
| beginTime := time.Unix(beginTimeNumber, 0).Format(time.RFC3339) | |||
| endTime := time.Unix(endTimeNumber, 0).Format(time.RFC3339) | |||
| log.Info("%s, %s", beginTime, endTime) | |||
| return beginTime, endTime | |||
| } | |||
| @@ -8,7 +8,7 @@ import ( | |||
| "code.gitea.io/gitea/modules/log" | |||
| ) | |||
| func TimeingCountData() { | |||
| func TimingCountData() { | |||
| //query wiki data | |||
| log.Info("start to time count data") | |||
| wikiMap := make(map[string]int) | |||
| @@ -318,7 +318,11 @@ | |||
| </div> | |||
| <div class="one wide column"> | |||
| <a href="{{AppSubUrl}}/{{.User.Name}}" title="{{.User.Name}}"><img class="ui avatar image" src="{{.User.RelAvatarLink}}"></a> | |||
| {{if .User.Name}} | |||
| <a href="{{AppSubUrl}}/{{.User.Name}}" title="{{.User.Name}}"><img class="ui avatar image" src="{{.User.RelAvatarLink}}"></a> | |||
| {{else}} | |||
| <a title="Ghost"><img class="ui avatar image" src="{{AppSubUrl}}/user/avatar/Ghost/-1"></a> | |||
| {{end}} | |||
| </div> | |||
| <div class="seven wide column text right"> | |||
| <div class="ui compact buttons"> | |||
| @@ -5,7 +5,7 @@ | |||
| {{range .Attachments}} | |||
| <div class="ui grid item" id="{{.UUID}}"> | |||
| <div class="row"> | |||
| <div class="eight wide column"> | |||
| <div class="eight wide column" data-tooltip="{{.Name}}"> | |||
| <span class="ui right">{{.Size | FileSize}}</span> | |||
| <a class="title" href="{{.DownloadURL}}?type={{$.Type}}"> | |||
| {{svg "octicon-cloud-download" 16}} {{.Name}} | |||
| @@ -13,7 +13,7 @@ | |||
| </div> | |||
| <div class="eight wide column right aligned"> | |||
| <div class="ui left mini icon buttons"> | |||
| <span class="ui basic button" data-tooltip='{{$.i18n.Tr "dataset.download_count"}}' data-position="bottom right">{{svg "octicon-flame" 16}} {{(.DownloadCount | PrettyNumber)}}</span> | |||
| <span class="ui basic button text left" data-tooltip='{{$.i18n.Tr "dataset.download_count"}}' data-position="bottom right" style="width: 60px; padding-left: 0;">{{svg "octicon-flame" 16}} {{(.DownloadCount | PrettyNumber)}}</span> | |||
| <span class="ui basic basic button clipboard" data-clipboard-text="{{.DownloadURL}}" data-tooltip='{{$.i18n.Tr "dataset.copy_url"}}' data-clipboard-action="copy"{{if ne $.Type 0}} style="display:none;"{{end}}>{{svg "octicon-file" 16}}</span> | |||
| <span class="ui basic basic button clipboard" data-clipboard-text="{{.FileChunk.Md5}}" data-tooltip='{{$.i18n.Tr "dataset.copy_md5"}}' data-clipboard-action="copy">{{svg "octicon-file-binary" 16}}</span> | |||
| </div> | |||
| @@ -291,7 +291,7 @@ | |||
| {{end}} --> | |||
| {{range .Topics}} | |||
| <a class="ui repo-topic small label topic" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a> | |||
| <a class="ui repo-topic small label topic" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=">{{.Name}}</a> | |||
| {{end}} | |||
| @@ -292,7 +292,11 @@ | |||
| </div> | |||
| <div class="one wide column"> | |||
| {{if .User.Name}} | |||
| <a href="{{AppSubUrl}}/{{.User.Name}}" title="{{.User.Name}}"><img class="ui avatar image" src="{{.User.RelAvatarLink}}"></a> | |||
| {{else}} | |||
| <a title="Ghost"><img class="ui avatar image" src="{{AppSubUrl}}/user/avatar/Ghost/-1"></a> | |||
| {{end}} | |||
| </div> | |||
| <div class="seven wide column text right"> | |||
| @@ -148,29 +148,37 @@ export default { | |||
| params:this.params | |||
| }).then((res)=>{ | |||
| this.array = res.data.topics | |||
| this.array.forEach((element,index) => { | |||
| if (this.arrayTopics.indexOf(element.topic_name)>-1){ | |||
| this.showInitTopic.push(true) | |||
| }else{ | |||
| } | |||
| else{ | |||
| this.showInitTopic.push(false) | |||
| } | |||
| this.showInputValue = true | |||
| }); | |||
| let findelement = this.array.some((item)=>{ | |||
| return item.topic_name===this.input | |||
| }) | |||
| this.showInputValue = !findelement | |||
| }); | |||
| }) | |||
| this.showInputValue = true | |||
| this.showSearchTopic = true | |||
| } | |||
| this.showAddTopic = false | |||
| }, | |||
| @@ -201,6 +209,9 @@ export default { | |||
| }) | |||
| }, | |||
| postTopic(){ | |||
| if(!this.showInputValue){ | |||
| return | |||
| } | |||
| const patter = /^[\u4e00-\u9fa5a-zA-Z0-9][\u4e00-\u9fa5a-zA-Z0-9-]{0,34}$/ | |||
| let regexp = patter.test(this.input) | |||
| if(!regexp){ | |||
| @@ -249,7 +260,7 @@ export default { | |||
| this.Post(data,topics) | |||
| if(this.arrayTopics.length===0){ | |||
| console.log("add postTopic") | |||
| $('#repo-topics1').append('<span class="no-description text-italic">暂无标签</span>') | |||
| }else{ | |||
| $('#repo-topics1').children('span').remove() | |||
| @@ -282,7 +293,9 @@ export default { | |||
| this.input = '' | |||
| if (this.input === ''){ | |||
| this.array = this.arrayTopics | |||
| let data = [] | |||
| this.showInitTopic = [] | |||
| this.array.forEach((element,index) => { | |||
| @@ -301,6 +314,7 @@ export default { | |||
| this.showInputValue = false | |||
| this.showSearchTopic = true | |||
| this.showAddTopic = false | |||
| } | |||
| stopPropagation(e); | |||
| @@ -332,30 +346,30 @@ computed:{ | |||
| }, | |||
| watch: { | |||
| input(newValue){ | |||
| // input(newValue){ | |||
| if (newValue === ''){ | |||
| this.array = this.arrayTopics | |||
| let data = [] | |||
| this.showInitTopic = [] | |||
| this.array.forEach((element,index) => { | |||
| // if (newValue === ''){ | |||
| // this.array = this.arrayTopics | |||
| // let data = [] | |||
| // this.showInitTopic = [] | |||
| // this.array.forEach((element,index) => { | |||
| let item = {} | |||
| item.topic_name = element | |||
| // let item = {} | |||
| // item.topic_name = element | |||
| data.push(item) | |||
| this.showInitTopic.push(true) | |||
| // data.push(item) | |||
| // this.showInitTopic.push(true) | |||
| }); | |||
| // }); | |||
| this.array = data | |||
| // this.array = data | |||
| this.showInputValue = false | |||
| this.showSearchTopic = true | |||
| } | |||
| } | |||
| // this.showInputValue = false | |||
| // this.showSearchTopic = true | |||
| // } | |||
| // } | |||
| }, | |||
| mounted() { | |||
| const context = this | |||