| @@ -140,6 +140,7 @@ func NewRepoContext() { | |||||
| // RepositoryStatus defines the status of repository | // RepositoryStatus defines the status of repository | ||||
| type RepositoryStatus int | type RepositoryStatus int | ||||
| type RepoBlockChainStatus int | type RepoBlockChainStatus int | ||||
| type RepoType int | |||||
| // all kinds of RepositoryStatus | // all kinds of RepositoryStatus | ||||
| const ( | const ( | ||||
| @@ -153,6 +154,11 @@ const ( | |||||
| RepoBlockChainFailed | RepoBlockChainFailed | ||||
| ) | ) | ||||
| const ( | |||||
| RepoNormal RepoType = iota | |||||
| RepoCourse | |||||
| ) | |||||
| // Repository represents a git repository. | // Repository represents a git repository. | ||||
| type Repository struct { | type Repository struct { | ||||
| ID int64 `xorm:"pk autoincr"` | ID int64 `xorm:"pk autoincr"` | ||||
| @@ -166,7 +172,8 @@ type Repository struct { | |||||
| OriginalServiceType api.GitServiceType `xorm:"index"` | OriginalServiceType api.GitServiceType `xorm:"index"` | ||||
| OriginalURL string `xorm:"VARCHAR(2048)"` | OriginalURL string `xorm:"VARCHAR(2048)"` | ||||
| DefaultBranch string | DefaultBranch string | ||||
| CreatorID int64 `xorm:"INDEX NOT NULL DEFAULT 0"` | |||||
| Creator *User `xorm:"-"` | |||||
| NumWatches int | NumWatches int | ||||
| NumStars int | NumStars int | ||||
| NumForks int | NumForks int | ||||
| @@ -175,11 +182,12 @@ type Repository struct { | |||||
| NumOpenIssues int `xorm:"-"` | NumOpenIssues int `xorm:"-"` | ||||
| NumPulls int | NumPulls int | ||||
| NumClosedPulls int | NumClosedPulls int | ||||
| NumOpenPulls int `xorm:"-"` | |||||
| NumMilestones int `xorm:"NOT NULL DEFAULT 0"` | |||||
| NumClosedMilestones int `xorm:"NOT NULL DEFAULT 0"` | |||||
| NumOpenMilestones int `xorm:"-"` | |||||
| NumCommit int64 `xorm:"NOT NULL DEFAULT 0"` | |||||
| NumOpenPulls int `xorm:"-"` | |||||
| NumMilestones int `xorm:"NOT NULL DEFAULT 0"` | |||||
| NumClosedMilestones int `xorm:"NOT NULL DEFAULT 0"` | |||||
| NumOpenMilestones int `xorm:"-"` | |||||
| NumCommit int64 `xorm:"NOT NULL DEFAULT 0"` | |||||
| RepoType RepoType `xorm:"NOT NULL DEFAULT 0"` | |||||
| IsPrivate bool `xorm:"INDEX"` | IsPrivate bool `xorm:"INDEX"` | ||||
| IsEmpty bool `xorm:"INDEX"` | IsEmpty bool `xorm:"INDEX"` | ||||
| @@ -1035,6 +1043,8 @@ type CreateRepoOptions struct { | |||||
| IsMirror bool | IsMirror bool | ||||
| AutoInit bool | AutoInit bool | ||||
| Status RepositoryStatus | Status RepositoryStatus | ||||
| IsCourse bool | |||||
| Topics []string | |||||
| } | } | ||||
| // GetRepoInitFile returns repository init files | // GetRepoInitFile returns repository init files | ||||
| @@ -1080,7 +1090,7 @@ func IsUsableRepoAlias(name string) error { | |||||
| } | } | ||||
| // CreateRepository creates a repository for the user/organization. | // CreateRepository creates a repository for the user/organization. | ||||
| func CreateRepository(ctx DBContext, doer, u *User, repo *Repository) (err error) { | |||||
| func CreateRepository(ctx DBContext, doer, u *User, repo *Repository, opts ...CreateRepoOptions) (err error) { | |||||
| if err = IsUsableRepoName(repo.Name); err != nil { | if err = IsUsableRepoName(repo.Name); err != nil { | ||||
| return err | return err | ||||
| } | } | ||||
| @@ -1094,7 +1104,10 @@ func CreateRepository(ctx DBContext, doer, u *User, repo *Repository) (err error | |||||
| } else if has { | } else if has { | ||||
| return ErrRepoAlreadyExist{u.Name, repo.Name} | return ErrRepoAlreadyExist{u.Name, repo.Name} | ||||
| } | } | ||||
| isCourse := isCourse(opts) | |||||
| if isCourse { | |||||
| repo.CreatorID = doer.ID | |||||
| } | |||||
| if _, err = ctx.e.Insert(repo); err != nil { | if _, err = ctx.e.Insert(repo); err != nil { | ||||
| return err | return err | ||||
| } | } | ||||
| @@ -1128,17 +1141,23 @@ func CreateRepository(ctx DBContext, doer, u *User, repo *Repository) (err error | |||||
| Config: &PullRequestsConfig{AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true}, | Config: &PullRequestsConfig{AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true}, | ||||
| }) | }) | ||||
| } else if tp == UnitTypeDatasets { | } else if tp == UnitTypeDatasets { | ||||
| units = append(units, RepoUnit{ | |||||
| RepoID: repo.ID, | |||||
| Type: tp, | |||||
| Config: &DatasetConfig{EnableDataset: true}, | |||||
| }) | |||||
| if !isCourse { | |||||
| units = append(units, RepoUnit{ | |||||
| RepoID: repo.ID, | |||||
| Type: tp, | |||||
| Config: &DatasetConfig{EnableDataset: true}, | |||||
| }) | |||||
| } | |||||
| } else if tp == UnitTypeCloudBrain { | } else if tp == UnitTypeCloudBrain { | ||||
| units = append(units, RepoUnit{ | |||||
| RepoID: repo.ID, | |||||
| Type: tp, | |||||
| Config: &CloudBrainConfig{EnableCloudBrain: true}, | |||||
| }) | |||||
| if !isCourse { | |||||
| units = append(units, RepoUnit{ | |||||
| RepoID: repo.ID, | |||||
| Type: tp, | |||||
| Config: &CloudBrainConfig{EnableCloudBrain: true}, | |||||
| }) | |||||
| } | |||||
| } else if tp == UnitTypeBlockChain { | } else if tp == UnitTypeBlockChain { | ||||
| units = append(units, RepoUnit{ | units = append(units, RepoUnit{ | ||||
| RepoID: repo.ID, | RepoID: repo.ID, | ||||
| @@ -1146,11 +1165,13 @@ func CreateRepository(ctx DBContext, doer, u *User, repo *Repository) (err error | |||||
| Config: &BlockChainConfig{EnableBlockChain: true}, | Config: &BlockChainConfig{EnableBlockChain: true}, | ||||
| }) | }) | ||||
| } else if tp == UnitTypeModelManage { | } else if tp == UnitTypeModelManage { | ||||
| units = append(units, RepoUnit{ | |||||
| RepoID: repo.ID, | |||||
| Type: tp, | |||||
| Config: &ModelManageConfig{EnableModelManage: true}, | |||||
| }) | |||||
| if !isCourse { | |||||
| units = append(units, RepoUnit{ | |||||
| RepoID: repo.ID, | |||||
| Type: tp, | |||||
| Config: &ModelManageConfig{EnableModelManage: true}, | |||||
| }) | |||||
| } | |||||
| } else { | } else { | ||||
| units = append(units, RepoUnit{ | units = append(units, RepoUnit{ | ||||
| RepoID: repo.ID, | RepoID: repo.ID, | ||||
| @@ -1220,6 +1241,14 @@ func CreateRepository(ctx DBContext, doer, u *User, repo *Repository) (err error | |||||
| return nil | return nil | ||||
| } | } | ||||
| func isCourse(opts []CreateRepoOptions) bool { | |||||
| var isCourse = false | |||||
| if len(opts) > 0 { | |||||
| isCourse = opts[0].IsCourse | |||||
| } | |||||
| return isCourse | |||||
| } | |||||
| func countRepositories(userID int64, private bool) int64 { | func countRepositories(userID int64, private bool) int64 { | ||||
| sess := x.Where("id > 0") | sess := x.Where("id > 0") | ||||
| @@ -48,9 +48,12 @@ func (repos RepositoryList) loadAttributes(e Engine) error { | |||||
| set := make(map[int64]struct{}) | set := make(map[int64]struct{}) | ||||
| repoIDs := make([]int64, len(repos)) | repoIDs := make([]int64, len(repos)) | ||||
| setCreator := make(map[int64]struct{}) | |||||
| for i := range repos { | for i := range repos { | ||||
| set[repos[i].OwnerID] = struct{}{} | set[repos[i].OwnerID] = struct{}{} | ||||
| repoIDs[i] = repos[i].ID | repoIDs[i] = repos[i].ID | ||||
| setCreator[repos[i].CreatorID] = struct{}{} | |||||
| } | } | ||||
| // Load owners. | // Load owners. | ||||
| @@ -61,8 +64,18 @@ func (repos RepositoryList) loadAttributes(e Engine) error { | |||||
| Find(&users); err != nil { | Find(&users); err != nil { | ||||
| return fmt.Errorf("find users: %v", err) | return fmt.Errorf("find users: %v", err) | ||||
| } | } | ||||
| //Load creator | |||||
| creators := make(map[int64]*User, len(set)) | |||||
| if err := e. | |||||
| Where("id > 0"). | |||||
| In("id", keysInt64(setCreator)). | |||||
| Find(&creators); err != nil { | |||||
| return fmt.Errorf("find create repo users: %v", err) | |||||
| } | |||||
| for i := range repos { | for i := range repos { | ||||
| repos[i].Owner = users[repos[i].OwnerID] | repos[i].Owner = users[repos[i].OwnerID] | ||||
| repos[i].Creator = creators[repos[i].CreatorID] | |||||
| } | } | ||||
| // Load primary language. | // Load primary language. | ||||
| @@ -174,6 +187,10 @@ type SearchRepoOptions struct { | |||||
| // True -> include just has milestones | // True -> include just has milestones | ||||
| // False -> include just has no milestone | // False -> include just has no milestone | ||||
| HasMilestones util.OptionalBool | HasMilestones util.OptionalBool | ||||
| // None -> include all repos | |||||
| // True -> include just courses | |||||
| // False -> include just no courses | |||||
| Course util.OptionalBool | |||||
| } | } | ||||
| //SearchOrderBy is used to sort the result | //SearchOrderBy is used to sort the result | ||||
| @@ -351,6 +368,10 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { | |||||
| cond = cond.And(builder.Eq{"is_mirror": opts.Mirror == util.OptionalBoolTrue}) | cond = cond.And(builder.Eq{"is_mirror": opts.Mirror == util.OptionalBoolTrue}) | ||||
| } | } | ||||
| if opts.Course == util.OptionalBoolTrue { | |||||
| cond = cond.And(builder.Eq{"repo_type": RepoCourse}) | |||||
| } | |||||
| if opts.Actor != nil && opts.Actor.IsRestricted { | if opts.Actor != nil && opts.Actor.IsRestricted { | ||||
| cond = cond.And(accessibleRepositoryCondition(opts.Actor)) | cond = cond.And(accessibleRepositoryCondition(opts.Actor)) | ||||
| } | } | ||||
| @@ -728,3 +728,15 @@ type DeadlineForm struct { | |||||
| func (f *DeadlineForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { | func (f *DeadlineForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { | ||||
| return validate(errs, ctx.Data, f, ctx.Locale) | return validate(errs, ctx.Data, f, ctx.Locale) | ||||
| } | } | ||||
| type CreateCourseForm struct { | |||||
| RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"` | |||||
| Alias string `binding:"Required;MaxSize(100);AlphaDashDotChinese"` | |||||
| Topics string | |||||
| Description string `binding:"MaxSize(1024)"` | |||||
| } | |||||
| // Validate validates the fields | |||||
| func (f *CreateCourseForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { | |||||
| return validate(errs, ctx.Data, f, ctx.Locale) | |||||
| } | |||||
| @@ -22,6 +22,10 @@ func CreateRepository(doer, u *models.User, opts models.CreateRepoOptions) (_ *m | |||||
| Limit: u.MaxRepoCreation, | Limit: u.MaxRepoCreation, | ||||
| } | } | ||||
| } | } | ||||
| var RepoType = models.RepoNormal | |||||
| if opts.IsCourse { | |||||
| RepoType = models.RepoCourse | |||||
| } | |||||
| repo := &models.Repository{ | repo := &models.Repository{ | ||||
| OwnerID: u.ID, | OwnerID: u.ID, | ||||
| @@ -38,10 +42,14 @@ func CreateRepository(doer, u *models.User, opts models.CreateRepoOptions) (_ *m | |||||
| CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch, | CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch, | ||||
| Status: opts.Status, | Status: opts.Status, | ||||
| IsEmpty: !opts.AutoInit, | IsEmpty: !opts.AutoInit, | ||||
| RepoType: RepoType, | |||||
| } | } | ||||
| err = models.WithTx(func(ctx models.DBContext) error { | err = models.WithTx(func(ctx models.DBContext) error { | ||||
| if err = models.CreateRepository(ctx, doer, u, repo); err != nil { | |||||
| if err = models.CreateRepository(ctx, doer, u, repo, opts); err != nil { | |||||
| return err | |||||
| } | |||||
| if err = models.SaveTopics(repo.ID, opts.Topics...); err != nil { | |||||
| return err | return err | ||||
| } | } | ||||
| @@ -50,6 +50,7 @@ repository = Repository | |||||
| organization = Organization | organization = Organization | ||||
| mirror = Mirror | mirror = Mirror | ||||
| new_repo = New Repository | new_repo = New Repository | ||||
| new_course=Publish Course | |||||
| new_migrate = New Migration | new_migrate = New Migration | ||||
| new_dataset = New Dataset | new_dataset = New Dataset | ||||
| edit_dataset = Edit Dataset | edit_dataset = Edit Dataset | ||||
| @@ -390,6 +391,7 @@ lang_select_error = Select a language from the list. | |||||
| username_been_taken = The username is already taken. | username_been_taken = The username is already taken. | ||||
| repo_name_been_taken = The repository name or path is already used. | repo_name_been_taken = The repository name or path is already used. | ||||
| course_name_been_taken=The course path is already used. | |||||
| visit_rate_limit = Remote visit addressed rate limitation. | visit_rate_limit = Remote visit addressed rate limitation. | ||||
| 2fa_auth_required = Remote visit required two factors authentication. | 2fa_auth_required = Remote visit required two factors authentication. | ||||
| org_name_been_taken = The organization name is already taken. | org_name_been_taken = The organization name is already taken. | ||||
| @@ -802,6 +804,8 @@ readme = README | |||||
| readme_helper = Select a README file template. | readme_helper = Select a README file template. | ||||
| auto_init = Initialize Repository (Adds .gitignore, License and README) | auto_init = Initialize Repository (Adds .gitignore, License and README) | ||||
| create_repo = Create Repository | create_repo = Create Repository | ||||
| create_course = Publish Course | |||||
| failed_to_create_course=Fail to publish course, please try again later. | |||||
| default_branch = Default Branch | default_branch = Default Branch | ||||
| mirror_prune = Prune | mirror_prune = Prune | ||||
| mirror_prune_desc = Remove obsolete remote-tracking references | mirror_prune_desc = Remove obsolete remote-tracking references | ||||
| @@ -968,8 +972,12 @@ archive.issue.nocomment = This repo is archived. You cannot comment on issues. | |||||
| archive.pull.nocomment = This repo is archived. You cannot comment on pull requests. | archive.pull.nocomment = This repo is archived. You cannot comment on pull requests. | ||||
| form.reach_limit_of_creation = You have already reached your limit of %d repositories. | form.reach_limit_of_creation = You have already reached your limit of %d repositories. | ||||
| form.reach_limit_of_course_creation=You have already reached your limit of %d courses or repositories. | |||||
| form.name_reserved = The repository name '%s' is reserved. | form.name_reserved = The repository name '%s' is reserved. | ||||
| form.course_name_reserved=The course name '%s' is reserved. | |||||
| form.name_pattern_not_allowed = The pattern '%s' is not allowed in a repository name. | form.name_pattern_not_allowed = The pattern '%s' is not allowed in a repository name. | ||||
| form.course_name_pattern_not_allowed=The pattern '%s' is not allowed in a course name. | |||||
| add_course_org_fail=Fail to add organization, please try again later. | |||||
| need_auth = Clone Authorization | need_auth = Clone Authorization | ||||
| migrate_type = Migration Type | migrate_type = Migration Type | ||||
| @@ -50,6 +50,7 @@ repository=项目 | |||||
| organization=组织 | organization=组织 | ||||
| mirror=镜像 | mirror=镜像 | ||||
| new_repo=创建项目 | new_repo=创建项目 | ||||
| new_course=发布课程 | |||||
| new_dataset=创建数据集 | new_dataset=创建数据集 | ||||
| new_migrate=迁移外部项目 | new_migrate=迁移外部项目 | ||||
| edit_dataset = Edit Dataset | edit_dataset = Edit Dataset | ||||
| @@ -395,6 +396,7 @@ lang_select_error=从列表中选出语言 | |||||
| username_been_taken=用户名已被使用。 | username_been_taken=用户名已被使用。 | ||||
| repo_name_been_taken=项目名称或项目路径已被使用。 | repo_name_been_taken=项目名称或项目路径已被使用。 | ||||
| course_name_been_taken=课程名称或路径已被使用。 | |||||
| visit_rate_limit=远程访问达到速度限制。 | visit_rate_limit=远程访问达到速度限制。 | ||||
| 2fa_auth_required=远程访问需要双重验证。 | 2fa_auth_required=远程访问需要双重验证。 | ||||
| org_name_been_taken=组织名称已被使用。 | org_name_been_taken=组织名称已被使用。 | ||||
| @@ -808,6 +810,8 @@ readme=自述 | |||||
| readme_helper=选择自述文件模板。 | readme_helper=选择自述文件模板。 | ||||
| auto_init=初始化存储库 (添加. gitignore、许可证和自述文件) | auto_init=初始化存储库 (添加. gitignore、许可证和自述文件) | ||||
| create_repo=创建项目 | create_repo=创建项目 | ||||
| create_course=发布课程 | |||||
| failed_to_create_course=发布课程失败,请稍后再试。 | |||||
| default_branch=默认分支 | default_branch=默认分支 | ||||
| mirror_prune=修剪 | mirror_prune=修剪 | ||||
| mirror_prune_desc=删除过时的远程跟踪引用 | mirror_prune_desc=删除过时的远程跟踪引用 | ||||
| @@ -980,8 +984,12 @@ archive.issue.nocomment=此项目已存档,您不能在此任务添加评论 | |||||
| archive.pull.nocomment=此项目已存档,您不能在此合并请求添加评论。 | archive.pull.nocomment=此项目已存档,您不能在此合并请求添加评论。 | ||||
| form.reach_limit_of_creation=你已经达到了您的 %d 项目的限制。 | form.reach_limit_of_creation=你已经达到了您的 %d 项目的限制。 | ||||
| form.reach_limit_of_course_creation=你已经达到了您的 %d 课程的限制。 | |||||
| form.name_reserved=项目名称 '%s' 是被保留的。 | form.name_reserved=项目名称 '%s' 是被保留的。 | ||||
| form.course_name_reserved=课程名称 '%s' 是被保留的。 | |||||
| form.name_pattern_not_allowed=项目名称中不允许使用模式 "%s"。 | form.name_pattern_not_allowed=项目名称中不允许使用模式 "%s"。 | ||||
| form.course_name_pattern_not_allowed=课程名称中不允许使用模式 "%s"。 | |||||
| add_course_org_fail=加入组织失败,请稍后重试。 | |||||
| need_auth=需要授权验证 | need_auth=需要授权验证 | ||||
| migrate_type=迁移类型 | migrate_type=迁移类型 | ||||
| @@ -7,11 +7,11 @@ package routers | |||||
| import ( | import ( | ||||
| "bytes" | "bytes" | ||||
| "fmt" | |||||
| "io/ioutil" | |||||
| "net/http" | "net/http" | ||||
| "strings" | "strings" | ||||
| "code.gitea.io/gitea/services/repository" | |||||
| "code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
| "code.gitea.io/gitea/modules/base" | "code.gitea.io/gitea/modules/base" | ||||
| "code.gitea.io/gitea/modules/context" | "code.gitea.io/gitea/modules/context" | ||||
| @@ -133,6 +133,7 @@ type RepoSearchOptions struct { | |||||
| Restricted bool | Restricted bool | ||||
| PageSize int | PageSize int | ||||
| TplName base.TplName | TplName base.TplName | ||||
| Course util.OptionalBool | |||||
| } | } | ||||
| var ( | var ( | ||||
| @@ -211,6 +212,7 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) { | |||||
| AllLimited: true, | AllLimited: true, | ||||
| TopicName: topic, | TopicName: topic, | ||||
| IncludeDescription: setting.UI.SearchRepoDescription, | IncludeDescription: setting.UI.SearchRepoDescription, | ||||
| Course: opts.Course, | |||||
| }) | }) | ||||
| if err != nil { | if err != nil { | ||||
| ctx.ServerError("SearchRepository", err) | ctx.ServerError("SearchRepository", err) | ||||
| @@ -559,7 +561,7 @@ func NotFound(ctx *context.Context) { | |||||
| func RecommendOrgFromPromote(ctx *context.Context) { | func RecommendOrgFromPromote(ctx *context.Context) { | ||||
| url := setting.RecommentRepoAddr + "organizations" | url := setting.RecommentRepoAddr + "organizations" | ||||
| result, err := recommendFromPromote(url) | |||||
| result, err := repository.RecommendFromPromote(url) | |||||
| if err != nil { | if err != nil { | ||||
| ctx.ServerError("500", err) | ctx.ServerError("500", err) | ||||
| return | return | ||||
| @@ -586,62 +588,11 @@ func RecommendOrgFromPromote(ctx *context.Context) { | |||||
| ctx.JSON(200, resultOrg) | ctx.JSON(200, resultOrg) | ||||
| } | } | ||||
| func recommendFromPromote(url string) ([]string, error) { | |||||
| resp, err := http.Get(url) | |||||
| if err != nil || resp.StatusCode != 200 { | |||||
| log.Info("Get organizations url error=" + err.Error()) | |||||
| return nil, err | |||||
| } | |||||
| bytes, err := ioutil.ReadAll(resp.Body) | |||||
| resp.Body.Close() | |||||
| if err != nil { | |||||
| log.Info("Get organizations url error=" + err.Error()) | |||||
| return nil, err | |||||
| } | |||||
| allLineStr := string(bytes) | |||||
| lines := strings.Split(allLineStr, "\n") | |||||
| result := make([]string, len(lines)) | |||||
| for i, line := range lines { | |||||
| log.Info("i=" + fmt.Sprint(i) + " line=" + line) | |||||
| result[i] = strings.Trim(line, " ") | |||||
| } | |||||
| return result, nil | |||||
| } | |||||
| func RecommendRepoFromPromote(ctx *context.Context) { | func RecommendRepoFromPromote(ctx *context.Context) { | ||||
| url := setting.RecommentRepoAddr + "projects" | |||||
| result, err := recommendFromPromote(url) | |||||
| result, err := repository.GetRecommendRepoFromPromote("projects") | |||||
| if err != nil { | if err != nil { | ||||
| ctx.ServerError("500", err) | ctx.ServerError("500", err) | ||||
| return | |||||
| } | |||||
| resultRepo := make([]map[string]interface{}, 0) | |||||
| //resultRepo := make([]*models.Repository, 0) | |||||
| for _, repoName := range result { | |||||
| tmpIndex := strings.Index(repoName, "/") | |||||
| if tmpIndex == -1 { | |||||
| log.Info("error repo name format.") | |||||
| } else { | |||||
| ownerName := strings.Trim(repoName[0:tmpIndex], " ") | |||||
| repoName := strings.Trim(repoName[tmpIndex+1:], " ") | |||||
| repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName) | |||||
| if err == nil { | |||||
| repoMap := make(map[string]interface{}) | |||||
| repoMap["ID"] = fmt.Sprint(repo.ID) | |||||
| repoMap["Name"] = repo.Name | |||||
| repoMap["OwnerName"] = repo.OwnerName | |||||
| repoMap["NumStars"] = repo.NumStars | |||||
| repoMap["NumForks"] = repo.NumForks | |||||
| repoMap["Description"] = repo.Description | |||||
| repoMap["NumWatchs"] = repo.NumWatches | |||||
| repoMap["Topics"] = repo.Topics | |||||
| repoMap["Avatar"] = repo.RelAvatarLink() | |||||
| resultRepo = append(resultRepo, repoMap) | |||||
| } else { | |||||
| log.Info("query repo error," + err.Error()) | |||||
| } | |||||
| } | |||||
| } else { | |||||
| ctx.JSON(200, result) | |||||
| } | } | ||||
| ctx.JSON(200, resultRepo) | |||||
| } | } | ||||
| @@ -7,6 +7,10 @@ package org | |||||
| import ( | import ( | ||||
| "strings" | "strings" | ||||
| "code.gitea.io/gitea/services/repository" | |||||
| "code.gitea.io/gitea/modules/util" | |||||
| "code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
| "code.gitea.io/gitea/modules/base" | "code.gitea.io/gitea/modules/base" | ||||
| "code.gitea.io/gitea/modules/context" | "code.gitea.io/gitea/modules/context" | ||||
| @@ -14,7 +18,8 @@ import ( | |||||
| ) | ) | ||||
| const ( | const ( | ||||
| tplOrgHome base.TplName = "org/home" | |||||
| tplOrgHome base.TplName = "org/home" | |||||
| tplOrgCourseHome base.TplName = "org/home_courses" | |||||
| ) | ) | ||||
| // Home show organization home page | // Home show organization home page | ||||
| @@ -59,10 +64,16 @@ func Home(ctx *context.Context) { | |||||
| case "fewestforks": | case "fewestforks": | ||||
| orderBy = models.SearchOrderByForks | orderBy = models.SearchOrderByForks | ||||
| default: | default: | ||||
| ctx.Data["SortType"] = "recentupdate" | |||||
| orderBy = models.SearchOrderByRecentUpdated | |||||
| } | |||||
| if setting.Course.OrgName == org.Name { | |||||
| ctx.Data["SortType"] = "newest" | |||||
| orderBy = models.SearchOrderByNewest | |||||
| } else { | |||||
| ctx.Data["SortType"] = "recentupdate" | |||||
| orderBy = models.SearchOrderByRecentUpdated | |||||
| } | |||||
| } | |||||
| orderBy = orderBy + ",id" | |||||
| keyword := strings.Trim(ctx.Query("q"), " ") | keyword := strings.Trim(ctx.Query("q"), " ") | ||||
| ctx.Data["Keyword"] = keyword | ctx.Data["Keyword"] = keyword | ||||
| @@ -76,9 +87,21 @@ func Home(ctx *context.Context) { | |||||
| count int64 | count int64 | ||||
| err error | err error | ||||
| ) | ) | ||||
| pageSize := setting.UI.User.RepoPagingNum | |||||
| var CourseOptional util.OptionalBool = util.OptionalBoolNone | |||||
| if setting.Course.OrgName == org.Name { | |||||
| pageSize = 15 | |||||
| CourseOptional = util.OptionalBoolTrue | |||||
| recommendCourses, _ := repository.GetRecommendRepoFromPromote("courses") | |||||
| ctx.Data["RecommendCourses"] = recommendCourses | |||||
| recommendCourseKeyWords, _ := repository.GetRecommendCourseKeyWords() | |||||
| ctx.Data["CoursesKeywords"] = recommendCourseKeyWords | |||||
| } | |||||
| repos, count, err = models.SearchRepository(&models.SearchRepoOptions{ | repos, count, err = models.SearchRepository(&models.SearchRepoOptions{ | ||||
| ListOptions: models.ListOptions{ | ListOptions: models.ListOptions{ | ||||
| PageSize: setting.UI.User.RepoPagingNum, | |||||
| PageSize: pageSize, | |||||
| Page: page, | Page: page, | ||||
| }, | }, | ||||
| Keyword: keyword, | Keyword: keyword, | ||||
| @@ -87,6 +110,7 @@ func Home(ctx *context.Context) { | |||||
| Private: ctx.IsSigned, | Private: ctx.IsSigned, | ||||
| Actor: ctx.User, | Actor: ctx.User, | ||||
| IncludeDescription: setting.UI.SearchRepoDescription, | IncludeDescription: setting.UI.SearchRepoDescription, | ||||
| Course: CourseOptional, | |||||
| }) | }) | ||||
| if err != nil { | if err != nil { | ||||
| ctx.ServerError("SearchRepository", err) | ctx.ServerError("SearchRepository", err) | ||||
| @@ -138,5 +162,10 @@ func Home(ctx *context.Context) { | |||||
| } | } | ||||
| ctx.Data["tags"] = tags | ctx.Data["tags"] = tags | ||||
| ctx.HTML(200, tplOrgHome) | |||||
| if setting.Course.OrgName == org.Name { | |||||
| ctx.HTML(200, tplOrgCourseHome) | |||||
| } else { | |||||
| ctx.HTML(200, tplOrgHome) | |||||
| } | |||||
| } | } | ||||
| @@ -17,7 +17,8 @@ import ( | |||||
| const ( | const ( | ||||
| // tplMembers template for organization members page | // tplMembers template for organization members page | ||||
| tplMembers base.TplName = "org/member/members" | |||||
| tplMembers base.TplName = "org/member/members" | |||||
| tplCourseMembers base.TplName = "org/member/course_members" | |||||
| ) | ) | ||||
| // Members render organization users page | // Members render organization users page | ||||
| @@ -64,8 +65,11 @@ func Members(ctx *context.Context) { | |||||
| ctx.Data["MembersIsPublicMember"] = membersIsPublic | ctx.Data["MembersIsPublicMember"] = membersIsPublic | ||||
| ctx.Data["MembersIsUserOrgOwner"] = members.IsUserOrgOwner(org.ID) | ctx.Data["MembersIsUserOrgOwner"] = members.IsUserOrgOwner(org.ID) | ||||
| ctx.Data["MembersTwoFaStatus"] = members.GetTwoFaStatus() | ctx.Data["MembersTwoFaStatus"] = members.GetTwoFaStatus() | ||||
| ctx.HTML(200, tplMembers) | |||||
| if setting.Course.OrgName == org.Name { | |||||
| ctx.HTML(200, tplCourseMembers) | |||||
| } else { | |||||
| ctx.HTML(200, tplMembers) | |||||
| } | |||||
| } | } | ||||
| // MembersAction response for operation to a member of organization | // MembersAction response for operation to a member of organization | ||||
| @@ -10,6 +10,8 @@ import ( | |||||
| "path" | "path" | ||||
| "strings" | "strings" | ||||
| "code.gitea.io/gitea/modules/setting" | |||||
| "code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
| "code.gitea.io/gitea/modules/auth" | "code.gitea.io/gitea/modules/auth" | ||||
| "code.gitea.io/gitea/modules/base" | "code.gitea.io/gitea/modules/base" | ||||
| @@ -22,7 +24,8 @@ import ( | |||||
| const ( | const ( | ||||
| // tplTeams template path for teams list page | // tplTeams template path for teams list page | ||||
| tplTeams base.TplName = "org/team/teams" | |||||
| tplTeams base.TplName = "org/team/teams" | |||||
| tplCourseTeams base.TplName = "org/team/courseTeams" | |||||
| // tplTeamNew template path for create new team page | // tplTeamNew template path for create new team page | ||||
| tplTeamNew base.TplName = "org/team/new" | tplTeamNew base.TplName = "org/team/new" | ||||
| // tplTeamMembers template path for showing team members page | // tplTeamMembers template path for showing team members page | ||||
| @@ -44,8 +47,12 @@ func Teams(ctx *context.Context) { | |||||
| } | } | ||||
| } | } | ||||
| ctx.Data["Teams"] = org.Teams | ctx.Data["Teams"] = org.Teams | ||||
| if setting.Course.OrgName == org.Name { | |||||
| ctx.HTML(200, tplCourseTeams) | |||||
| } else { | |||||
| ctx.HTML(200, tplTeams) | |||||
| } | |||||
| ctx.HTML(200, tplTeams) | |||||
| } | } | ||||
| // TeamsAction response for join, leave, remove, add operations to team | // TeamsAction response for join, leave, remove, add operations to team | ||||
| @@ -0,0 +1,189 @@ | |||||
| package repo | |||||
| import ( | |||||
| "net/http" | |||||
| "strings" | |||||
| "code.gitea.io/gitea/models" | |||||
| "code.gitea.io/gitea/modules/auth" | |||||
| "code.gitea.io/gitea/modules/base" | |||||
| "code.gitea.io/gitea/modules/context" | |||||
| "code.gitea.io/gitea/modules/log" | |||||
| "code.gitea.io/gitea/modules/setting" | |||||
| repo_service "code.gitea.io/gitea/services/repository" | |||||
| ) | |||||
| const ( | |||||
| tplCreateCourse base.TplName = "repo/createCourse" | |||||
| ) | |||||
| func CreateCourse(ctx *context.Context) { | |||||
| ctx.Data["Title"] = ctx.Tr("new_course") | |||||
| ctx.Data["Owner"] = setting.Course.OrgName | |||||
| ctx.HTML(200, tplCreateCourse) | |||||
| } | |||||
| func CreateCoursePost(ctx *context.Context, form auth.CreateCourseForm) { | |||||
| ctx.Data["Title"] = ctx.Tr("new_course") | |||||
| if ctx.Written() { | |||||
| return | |||||
| } | |||||
| var topics = make([]string, 0) | |||||
| var topicsStr = strings.TrimSpace(form.Topics) | |||||
| if len(topicsStr) > 0 { | |||||
| topics = strings.Split(topicsStr, ",") | |||||
| } | |||||
| validTopics, invalidTopics := models.SanitizeAndValidateTopics(topics) | |||||
| if len(validTopics) > 25 { | |||||
| ctx.RenderWithErr(ctx.Tr("repo.topic.count_prompt"), tplCreateCourse, form) | |||||
| return | |||||
| } | |||||
| if len(invalidTopics) > 0 { | |||||
| ctx.RenderWithErr(ctx.Tr("repo.topic.format_prompt"), tplCreateCourse, form) | |||||
| return | |||||
| } | |||||
| var repo *models.Repository | |||||
| var err error | |||||
| if setting.Course.OrgName == "" || setting.Course.TeamName == "" { | |||||
| log.Error("then organization name or team name of course is empty.") | |||||
| ctx.RenderWithErr(ctx.Tr("repo.failed_to_create_course"), tplCreateCourse, form) | |||||
| return | |||||
| } | |||||
| org, team, err := getOrgAndTeam() | |||||
| if err != nil { | |||||
| log.Error("Failed to get team from db.", err) | |||||
| ctx.RenderWithErr(ctx.Tr("repo.failed_to_create_course"), tplCreateCourse, form) | |||||
| return | |||||
| } | |||||
| isInTeam, err := models.IsUserInTeams(ctx.User.ID, []int64{team.ID}) | |||||
| if err != nil { | |||||
| log.Error("Failed to get user in team from db.") | |||||
| ctx.RenderWithErr(ctx.Tr("repo.failed_to_create_course"), tplCreateCourse, form) | |||||
| return | |||||
| } | |||||
| if !isInTeam { | |||||
| err = models.AddTeamMember(team, ctx.User.ID) | |||||
| if err != nil { | |||||
| log.Error("Failed to add user to team.") | |||||
| ctx.RenderWithErr(ctx.Tr("repo.failed_to_create_course"), tplCreateCourse, form) | |||||
| return | |||||
| } | |||||
| } | |||||
| if ctx.HasError() { | |||||
| ctx.HTML(200, tplCreateCourse) | |||||
| return | |||||
| } | |||||
| repo, err = repo_service.CreateRepository(ctx.User, org, models.CreateRepoOptions{ | |||||
| Name: form.RepoName, | |||||
| Alias: form.Alias, | |||||
| Description: form.Description, | |||||
| Gitignores: "", | |||||
| IssueLabels: "", | |||||
| License: "", | |||||
| Readme: "Default", | |||||
| IsPrivate: false, | |||||
| DefaultBranch: "master", | |||||
| AutoInit: true, | |||||
| IsCourse: true, | |||||
| Topics: validTopics, | |||||
| }) | |||||
| if err == nil { | |||||
| log.Trace("Repository created [%d]: %s/%s", repo.ID, org.Name, repo.Name) | |||||
| ctx.Redirect(setting.AppSubURL + "/" + org.Name + "/" + repo.Name) | |||||
| return | |||||
| } | |||||
| handleCreateCourseError(ctx, org, err, "CreateCoursePost", tplCreateCourse, &form) | |||||
| } | |||||
| func AddCourseOrg(ctx *context.Context) { | |||||
| _, team, err := getOrgAndTeam() | |||||
| if err != nil { | |||||
| log.Error("Failed to get team from db.", err) | |||||
| ctx.JSON(http.StatusOK, map[string]interface{}{ | |||||
| "code": 1, | |||||
| "message": ctx.Tr("repo.addCourseOrgFail"), | |||||
| }) | |||||
| return | |||||
| } | |||||
| isInTeam, err := models.IsUserInTeams(ctx.User.ID, []int64{team.ID}) | |||||
| if err != nil { | |||||
| log.Error("Failed to get user in team from db.", err) | |||||
| ctx.JSON(http.StatusOK, map[string]interface{}{ | |||||
| "code": 1, | |||||
| "message": ctx.Tr("repo.add_course_org_fail"), | |||||
| }) | |||||
| return | |||||
| } | |||||
| if !isInTeam { | |||||
| err = models.AddTeamMember(team, ctx.User.ID) | |||||
| if err != nil { | |||||
| log.Error("Failed to add user to team.", err) | |||||
| ctx.JSON(http.StatusOK, map[string]interface{}{ | |||||
| "code": 1, | |||||
| "message": ctx.Tr("repo.add_course_org_fail"), | |||||
| }) | |||||
| return | |||||
| } | |||||
| } | |||||
| ctx.JSON(http.StatusOK, map[string]interface{}{ | |||||
| "code": 0, | |||||
| "message": "", | |||||
| }) | |||||
| } | |||||
| func getOrgAndTeam() (*models.User, *models.Team, error) { | |||||
| org, err := models.GetUserByName(setting.Course.OrgName) | |||||
| if err != nil { | |||||
| log.Error("Failed to get organization from db.", err) | |||||
| return nil, nil, err | |||||
| } | |||||
| team, err := models.GetTeam(org.ID, setting.Course.TeamName) | |||||
| if err != nil { | |||||
| log.Error("Failed to get team from db.", err) | |||||
| return nil, nil, err | |||||
| } | |||||
| return org, team, nil | |||||
| } | |||||
| func handleCreateCourseError(ctx *context.Context, owner *models.User, err error, name string, tpl base.TplName, form interface{}) { | |||||
| switch { | |||||
| case models.IsErrReachLimitOfRepo(err): | |||||
| ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_course_creation", owner.MaxCreationLimit()), tpl, form) | |||||
| case models.IsErrRepoAlreadyExist(err): | |||||
| ctx.Data["Err_RepoName"] = true | |||||
| ctx.RenderWithErr(ctx.Tr("form.course_name_been_taken"), tpl, form) | |||||
| case models.IsErrNameReserved(err): | |||||
| ctx.Data["Err_RepoName"] = true | |||||
| ctx.RenderWithErr(ctx.Tr("repo.form.course_name_reserved", err.(models.ErrNameReserved).Name), tpl, form) | |||||
| case models.IsErrNamePatternNotAllowed(err): | |||||
| ctx.Data["Err_RepoName"] = true | |||||
| ctx.RenderWithErr(ctx.Tr("repo.form.course_name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tpl, form) | |||||
| default: | |||||
| ctx.ServerError(name, err) | |||||
| } | |||||
| } | |||||
| @@ -708,6 +708,13 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| }, reqSignIn) | }, reqSignIn) | ||||
| // ***** END: Organization ***** | // ***** END: Organization ***** | ||||
| m.Group("/course", func() { | |||||
| m.Get("/create", repo.CreateCourse) | |||||
| m.Post("/create", bindIgnErr(auth.CreateCourseForm{}), repo.CreateCoursePost) | |||||
| m.Get("/addOrg", repo.AddCourseOrg) | |||||
| }, reqSignIn) | |||||
| // ***** START: Repository ***** | // ***** START: Repository ***** | ||||
| m.Group("/repo", func() { | m.Group("/repo", func() { | ||||
| m.Get("/create", repo.Create) | m.Get("/create", repo.Create) | ||||
| @@ -5,12 +5,17 @@ | |||||
| package repository | package repository | ||||
| import ( | import ( | ||||
| "fmt" | |||||
| "io/ioutil" | |||||
| "net/http" | |||||
| "strings" | |||||
| "code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
| "code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
| "code.gitea.io/gitea/modules/notification" | "code.gitea.io/gitea/modules/notification" | ||||
| repo_module "code.gitea.io/gitea/modules/repository" | repo_module "code.gitea.io/gitea/modules/repository" | ||||
| "code.gitea.io/gitea/modules/setting" | |||||
| pull_service "code.gitea.io/gitea/services/pull" | pull_service "code.gitea.io/gitea/services/pull" | ||||
| "fmt" | |||||
| ) | ) | ||||
| // CreateRepository creates a repository for the user/organization. | // CreateRepository creates a repository for the user/organization. | ||||
| @@ -86,3 +91,79 @@ func PushCreateRepo(authUser, owner *models.User, repoName string) (*models.Repo | |||||
| return repo, nil | return repo, nil | ||||
| } | } | ||||
| func GetRecommendCourseKeyWords() ([]string, error) { | |||||
| url := setting.RecommentRepoAddr + "course_keywords" | |||||
| result, err := RecommendFromPromote(url) | |||||
| if err != nil { | |||||
| return []string{}, err | |||||
| } | |||||
| return result, err | |||||
| } | |||||
| func GetRecommendRepoFromPromote(filename string) ([]map[string]interface{}, error) { | |||||
| resultRepo := make([]map[string]interface{}, 0) | |||||
| url := setting.RecommentRepoAddr + filename | |||||
| result, err := RecommendFromPromote(url) | |||||
| if err != nil { | |||||
| return resultRepo, err | |||||
| } | |||||
| //resultRepo := make([]*models.Repository, 0) | |||||
| for _, repoName := range result { | |||||
| tmpIndex := strings.Index(repoName, "/") | |||||
| if tmpIndex == -1 { | |||||
| log.Info("error repo name format.") | |||||
| } else { | |||||
| ownerName := strings.Trim(repoName[0:tmpIndex], " ") | |||||
| repoName := strings.Trim(repoName[tmpIndex+1:], " ") | |||||
| repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName) | |||||
| if err == nil { | |||||
| repoMap := make(map[string]interface{}) | |||||
| repoMap["ID"] = fmt.Sprint(repo.ID) | |||||
| repoMap["Name"] = repo.Name | |||||
| repoMap["Alias"] = repo.Alias | |||||
| repoMap["Creator"] = repo.Creator | |||||
| repoMap["OwnerName"] = repo.OwnerName | |||||
| repoMap["NumStars"] = repo.NumStars | |||||
| repoMap["NumForks"] = repo.NumForks | |||||
| repoMap["Description"] = repo.Description | |||||
| repoMap["NumWatchs"] = repo.NumWatches | |||||
| repoMap["Topics"] = repo.Topics | |||||
| repoMap["Avatar"] = repo.RelAvatarLink() | |||||
| resultRepo = append(resultRepo, repoMap) | |||||
| } else { | |||||
| log.Info("query repo error," + err.Error()) | |||||
| } | |||||
| } | |||||
| } | |||||
| return resultRepo, nil | |||||
| } | |||||
| func RecommendFromPromote(url string) ([]string, error) { | |||||
| resp, err := http.Get(url) | |||||
| if err != nil || resp.StatusCode != 200 { | |||||
| log.Info("Get organizations url error=" + err.Error()) | |||||
| return nil, err | |||||
| } | |||||
| bytes, err := ioutil.ReadAll(resp.Body) | |||||
| resp.Body.Close() | |||||
| if err != nil { | |||||
| log.Info("Get organizations url error=" + err.Error()) | |||||
| return nil, err | |||||
| } | |||||
| allLineStr := string(bytes) | |||||
| lines := strings.Split(allLineStr, "\n") | |||||
| result := make([]string, len(lines)) | |||||
| for i, line := range lines { | |||||
| log.Info("i=" + fmt.Sprint(i) + " line=" + line) | |||||
| result[i] = strings.Trim(line, " ") | |||||
| } | |||||
| return result, nil | |||||
| } | |||||