Commit ca412c06a9
Verified · cmc
e2e/profileabout_test.go +38
| @@ -43,6 +43,19 @@ func TestProfileAboutFromRepo(t *testing.T) { | ||
| 43 | 43 | if !strings.Contains(body, "hello from a file") { |
| 44 | 44 | t.Error("web profile does not render the about") |
| 45 | 45 | } |
| 46 | ||
| 47 | // The extension drives the renderer: an .org about renders as org. | |
| 48 | if _, _, code := inst.ssh(t, bobKey, "", "repo", "create", "bob/.gitbay"); code != 0 { | |
| 49 | t.Fatal("creating bob/.gitbay failed") | |
| 50 | } | |
| 51 | if _, errOut, code := inst.ssh(t, bobKey, "a /note/ in org\n", | |
| 52 | "repo", "commit-file", "bob/.gitbay", "profile/README.org", | |
| 53 | "--ref", "main", "--file", "-"); code != 0 { | |
| 54 | t.Fatalf("committing bob's org about: %s", errOut) | |
| 55 | } | |
| 56 | if _, page := inst.get(t, "/bob"); !strings.Contains(page, "<em>note</em>") { | |
| 57 | t.Errorf("about did not render as org:\n%s", page) | |
| 58 | } | |
| 46 | 59 | } |
| 47 | 60 | |
| 48 | 61 | // The extension picks the format, .md wins the resolution order, and a |
| @@ -87,3 +100,28 @@ func TestProfileAboutFormatAndPrivacy(t *testing.T) { | ||
| 87 | 100 | t.Errorf(".md did not win resolution: %s", out) |
| 88 | 101 | } |
| 89 | 102 | } |
| 103 | ||
| 104 | // The about is not settable through profile set any more: it is a file. | |
| 105 | func TestProfileSetHasNoAbout(t *testing.T) { | |
| 106 | inst := startInstance(t) | |
| 107 | aliceKey := inst.newKey(t, "alice") | |
| 108 | inst.admin(t, "admin", "user", "create", "alice", | |
| 109 | "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified") | |
| 110 | ||
| 111 | if _, _, code := inst.ssh(t, aliceKey, "", "profile", "set", "--about", "'inline text'"); code == 0 { | |
| 112 | t.Error("profile set --about still accepted") | |
| 113 | } | |
| 114 | if _, _, code := inst.ssh(t, aliceKey, "x", "profile", "set", "--file", "-"); code == 0 { | |
| 115 | t.Error("profile set --file still accepted") | |
| 116 | } | |
| 117 | ||
| 118 | // The flags that stay still work. | |
| 119 | if _, errOut, code := inst.ssh(t, aliceKey, "", | |
| 120 | "profile", "set", "--description", "'a line'", "--link", "'site|https://example.org'"); code != 0 { | |
| 121 | t.Fatalf("profile set --description --link: %s", errOut) | |
| 122 | } | |
| 123 | out, _, _ := inst.ssh(t, aliceKey, "", "profile", "show", "alice", "--json") | |
| 124 | if !strings.Contains(out, "a line") || !strings.Contains(out, "https://example.org") { | |
| 125 | t.Errorf("description or link not saved: %s", out) | |
| 126 | } | |
| 127 | } | |
e2e/profileweb_test.go +48 −22
| @@ -7,8 +7,9 @@ import ( | ||
| 7 | 7 | ) |
| 8 | 8 | |
| 9 | 9 | // TestProfileSettingsWeb covers profile set from the account settings page |
| 10 | // (#161): description, website, links and about round-trip through the | |
| 11 | // form, and emptying a field actually clears it rather than being skipped. | |
| 10 | // (#161): description, website and links round-trip through the form, and | |
| 11 | // emptying a field actually clears it rather than being skipped. The about | |
| 12 | // text is not on this form — it is a file, covered below. | |
| 12 | 13 | func TestProfileSettingsWeb(t *testing.T) { |
| 13 | 14 | inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\n") |
| 14 | 15 | aliceKey := inst.newKey(t, "alice") |
| @@ -21,7 +22,7 @@ func TestProfileSettingsWeb(t *testing.T) { | ||
| 21 | 22 | _, body := browserGet(t, alice, settingsURL) |
| 22 | 23 | for _, want := range []string{ |
| 23 | 24 | `<label for="p-description">`, `<label for="p-website">`, |
| 24 | `<label for="p-links">`, `<label for="p-about">`, `<label for="format">`, | |
| 25 | `<label for="p-links">`, | |
| 25 | 26 | } { |
| 26 | 27 | if !strings.Contains(body, want) { |
| 27 | 28 | t.Fatalf("profile form missing %q:\n%s", want, body) |
| @@ -34,8 +35,6 @@ func TestProfileSettingsWeb(t *testing.T) { | ||
| 34 | 35 | "description": {"builds small tools"}, |
| 35 | 36 | "website": {"https://alice.example"}, |
| 36 | 37 | "links": {"Mastodon|https://fosstodon.example/@alice\nhttps://alice.example/now"}, |
| 37 | "about": {"hello there"}, | |
| 38 | "format": {"md"}, | |
| 39 | 38 | }) |
| 40 | 39 | if status != 200 && status != 303 { |
| 41 | 40 | t.Fatalf("profile post: %d", status) |
| @@ -44,7 +43,7 @@ func TestProfileSettingsWeb(t *testing.T) { | ||
| 44 | 43 | for _, want := range []string{ |
| 45 | 44 | `"description":"builds small tools"`, `"website":"https://alice.example"`, |
| 46 | 45 | `"label":"Mastodon"`, `"url":"https://fosstodon.example/@alice"`, |
| 47 | `"url":"https://alice.example/now"`, `"about":"hello there"`, | |
| 46 | `"url":"https://alice.example/now"`, | |
| 48 | 47 | } { |
| 49 | 48 | if !strings.Contains(out, want) { |
| 50 | 49 | t.Fatalf("profile set missing %q: %s", want, out) |
| @@ -55,7 +54,7 @@ func TestProfileSettingsWeb(t *testing.T) { | ||
| 55 | 54 | _, body = browserGet(t, alice, settingsURL) |
| 56 | 55 | for _, want := range []string{ |
| 57 | 56 | "builds small tools", "https://alice.example", "Mastodon|https://fosstodon.example/@alice", |
| 58 | "https://alice.example/now", "hello there", | |
| 57 | "https://alice.example/now", | |
| 59 | 58 | } { |
| 60 | 59 | if !strings.Contains(body, want) { |
| 61 | 60 | t.Fatalf("settings page did not round-trip %q:\n%s", want, body) |
| @@ -66,22 +65,10 @@ func TestProfileSettingsWeb(t *testing.T) { | ||
| 66 | 65 | // each step below carries the fields already in place and changes one. |
| 67 | 66 | links := "Mastodon|https://fosstodon.example/@alice\nhttps://alice.example/now" |
| 68 | 67 | |
| 69 | // The org choice is honoured on the profile page. | |
| 70 | if status, _ := browserPost(t, alice, settingsURL, url.Values{ | |
| 71 | "field": {"profile"}, "description": {"builds small tools"}, | |
| 72 | "website": {"https://alice.example"}, "links": {links}, | |
| 73 | "about": {"a /note/ in org"}, "format": {"org"}, | |
| 74 | }); status != 200 && status != 303 { | |
| 75 | t.Fatalf("profile post (org): %d", status) | |
| 76 | } | |
| 77 | if _, page := browserGet(t, alice, inst.base()+"/alice"); !strings.Contains(page, "<em>note</em>") { | |
| 78 | t.Fatalf("about did not render as org:\n%s", page) | |
| 79 | } | |
| 80 | ||
| 81 | 68 | // Emptying the website clears it, not leaves it alone. |
| 82 | 69 | if status, _ := browserPost(t, alice, settingsURL, url.Values{ |
| 83 | 70 | "field": {"profile"}, "description": {"builds small tools"}, |
| 84 | "website": {""}, "links": {links}, "about": {"a /note/ in org"}, "format": {"org"}, | |
| 71 | "website": {""}, "links": {links}, | |
| 85 | 72 | }); status != 200 && status != 303 { |
| 86 | 73 | t.Fatalf("profile post (clear website): %d", status) |
| 87 | 74 | } |
| @@ -98,8 +85,7 @@ func TestProfileSettingsWeb(t *testing.T) { | ||
| 98 | 85 | |
| 99 | 86 | // Emptying the links field clears the whole list. |
| 100 | 87 | if status, _ := browserPost(t, alice, settingsURL, url.Values{ |
| 101 | "field": {"profile"}, "description": {"builds small tools"}, | |
| 102 | "links": {""}, "about": {"a /note/ in org"}, "format": {"org"}, | |
| 88 | "field": {"profile"}, "description": {"builds small tools"}, "links": {""}, | |
| 103 | 89 | }); status != 200 && status != 303 { |
| 104 | 90 | t.Fatalf("profile post (clear links): %d", status) |
| 105 | 91 | } |
| @@ -108,3 +94,43 @@ func TestProfileSettingsWeb(t *testing.T) { | ||
| 108 | 94 | t.Fatalf("links not cleared: %s", out) |
| 109 | 95 | } |
| 110 | 96 | } |
| 97 | ||
| 98 | // The settings page does not edit the about text; it creates the | |
| 99 | // repository that holds it and points at the file editor. | |
| 100 | func TestProfileAboutRepoFromWeb(t *testing.T) { | |
| 101 | inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\n") | |
| 102 | aliceKey := inst.newKey(t, "alice") | |
| 103 | inst.admin(t, "admin", "user", "create", "alice", | |
| 104 | "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified") | |
| 105 | alice := inst.login(t, aliceKey) | |
| 106 | settingsURL := inst.base() + "/settings" | |
| 107 | ||
| 108 | // With no repository yet, the page offers to create one. | |
| 109 | _, body := browserGet(t, alice, settingsURL) | |
| 110 | if !strings.Contains(body, "Create alice/.gitbay") { | |
| 111 | t.Fatalf("settings page does not offer the profile repository:\n%s", body) | |
| 112 | } | |
| 113 | ||
| 114 | if status, _ := browserPost(t, alice, settingsURL, url.Values{ | |
| 115 | "field": {"profile-repo"}, | |
| 116 | }); status != 200 && status != 303 { | |
| 117 | t.Fatalf("profile-repo post: %d", status) | |
| 118 | } | |
| 119 | ||
| 120 | // The repository exists with a starter file, and the page now links to | |
| 121 | // the editor instead of offering to create it again. | |
| 122 | out, _, code := inst.ssh(t, aliceKey, "", "profile", "show", "alice", "--json") | |
| 123 | if code != 0 { | |
| 124 | t.Fatalf("profile show: %d", code) | |
| 125 | } | |
| 126 | if !strings.Contains(out, `"about_path":"profile/README.md"`) { | |
| 127 | t.Fatalf("starter about not committed: %s", out) | |
| 128 | } | |
| 129 | _, body = browserGet(t, alice, settingsURL) | |
| 130 | if !strings.Contains(body, "/alice/.gitbay/edit/main/profile/README.md") { | |
| 131 | t.Fatalf("settings page does not link to the about file:\n%s", body) | |
| 132 | } | |
| 133 | if strings.Contains(body, "Create alice/.gitbay") { | |
| 134 | t.Error("settings page still offers to create an existing repository") | |
| 135 | } | |
| 136 | } | |
internal/control/profile.go +18 −48
| @@ -20,10 +20,10 @@ func init() { | ||
| 20 | 20 | Usage: "profile show [name]", ReadOnly: true, Run: runProfileShow}) |
| 21 | 21 | register(Command{Path: []string{"profile", "set"}, |
| 22 | 22 | Summary: "set your profile", |
| 23 | Usage: "profile set [--description <d>] [--website <url>] [--about <text>|--file -] [--about-format md|org] [--link <label|url>]... ('' clears)", ReadsStdin: true, Run: runProfileSet}) | |
| 23 | Usage: "profile set [--description <d>] [--website <url>] [--link <label|url>]... ('' clears)", Run: runProfileSet}) | |
| 24 | 24 | register(Command{Path: []string{"org", "profile"}, |
| 25 | 25 | Summary: "show or set an org's profile", |
| 26 | Usage: "org profile <org> [--description <d>] [--website <url>] [--about <text>|--file -] [--about-format md|org] [--link <label|url>]...", ReadsStdin: true, Run: runOrgProfile}) | |
| 26 | Usage: "org profile <org> [--description <d>] [--website <url>] [--link <label|url>]...", Run: runOrgProfile}) | |
| 27 | 27 | } |
| 28 | 28 | |
| 29 | 29 | // maxProfileLinks caps the free-form link list. A profile is a header, |
| @@ -78,29 +78,24 @@ func ownerAbout(c *Ctx, owner string) (text, format, path string) { | ||
| 78 | 78 | type profileEdit struct { |
| 79 | 79 | Description *string |
| 80 | 80 | Website *string |
| 81 | About *string | |
| 82 | AboutFormat *string | |
| 83 | 81 | Links *[]store.ProfileLink |
| 84 | 82 | } |
| 85 | 83 | |
| 86 | 84 | func (e profileEdit) empty() bool { |
| 87 | return e.Description == nil && e.Website == nil && e.About == nil && | |
| 88 | e.AboutFormat == nil && e.Links == nil | |
| 85 | return e.Description == nil && e.Website == nil && e.Links == nil | |
| 89 | 86 | } |
| 90 | 87 | |
| 91 | // parseProfileFlags pulls the profile flags out of args. --about takes | |
| 92 | // inline text or reads stdin via --file -; --link repeats, and a single | |
| 93 | // empty --link clears the list. | |
| 94 | func parseProfileFlags(c *Ctx, args []string) (rest []string, e profileEdit, err error) { | |
| 95 | about, file := "", "" | |
| 96 | sawAbout := false | |
| 88 | // parseProfileFlags pulls the profile flags out of args. --link repeats, | |
| 89 | // and a single empty --link clears the list. The about text is not here: | |
| 90 | // it is a file in <owner>/.gitbay, written like any other file. | |
| 91 | func parseProfileFlags(args []string) (rest []string, e profileEdit, err error) { | |
| 97 | 92 | var links []store.ProfileLink |
| 98 | f, err := parseFlags(args, flagSpec{Values: []string{"--description", "--website", "--about", "--about-format", "--file"}, Multi: []string{"--link"}, MaxPos: -1}) | |
| 93 | f, err := parseFlags(args, flagSpec{Values: []string{"--description", "--website"}, Multi: []string{"--link"}, MaxPos: -1}) | |
| 99 | 94 | if err != nil { |
| 100 | 95 | return nil, e, err |
| 101 | 96 | } |
| 102 | 97 | rest = f.Pos |
| 103 | for _, name := range []string{"--description", "--website", "--about-format"} { | |
| 98 | for _, name := range []string{"--description", "--website"} { | |
| 104 | 99 | if !f.Has(name) { |
| 105 | 100 | continue |
| 106 | 101 | } |
| @@ -110,16 +105,8 @@ func parseProfileFlags(c *Ctx, args []string) (rest []string, e profileEdit, err | ||
| 110 | 105 | e.Description = &v |
| 111 | 106 | case "--website": |
| 112 | 107 | e.Website = &v |
| 113 | case "--about-format": | |
| 114 | e.AboutFormat = &v | |
| 115 | 108 | } |
| 116 | 109 | } |
| 117 | if f.Has("--about") { | |
| 118 | about, sawAbout = f.Value("--about"), true | |
| 119 | } | |
| 120 | if f.Has("--file") { | |
| 121 | file, sawAbout = f.Value("--file"), true | |
| 122 | } | |
| 123 | 110 | for _, v := range f.List("--link") { |
| 124 | 111 | if v == "" { |
| 125 | 112 | links = nil |
| @@ -133,13 +120,6 @@ func parseProfileFlags(c *Ctx, args []string) (rest []string, e profileEdit, err | ||
| 133 | 120 | links = append(links, l) |
| 134 | 121 | e.Links = &links |
| 135 | 122 | } |
| 136 | if sawAbout { | |
| 137 | body, berr := bodyFrom(c, about, file) | |
| 138 | if berr != nil { | |
| 139 | return nil, e, berr | |
| 140 | } | |
| 141 | e.About = &body | |
| 142 | } | |
| 143 | 123 | if len(links) > maxProfileLinks { |
| 144 | 124 | return nil, e, fmt.Errorf("at most %d links", maxProfileLinks) |
| 145 | 125 | } |
| @@ -189,16 +169,6 @@ func applyProfile(p store.Profile, e profileEdit) (store.Profile, error) { | ||
| 189 | 169 | } |
| 190 | 170 | p.Website = s |
| 191 | 171 | } |
| 192 | if e.About != nil { | |
| 193 | p.About = strings.TrimSpace(*e.About) | |
| 194 | } | |
| 195 | if e.AboutFormat != nil { | |
| 196 | f := strings.TrimSpace(*e.AboutFormat) | |
| 197 | if f != "md" && f != "org" { | |
| 198 | return p, errors.New("about format must be md or org") | |
| 199 | } | |
| 200 | p.AboutFormat = f | |
| 201 | } | |
| 202 | 172 | if e.Links != nil { |
| 203 | 173 | p.Links = *e.Links |
| 204 | 174 | } |
| @@ -210,8 +180,8 @@ type ProfileOut struct { | ||
| 210 | 180 | Kind string `json:"kind"` |
| 211 | 181 | Description string `json:"description,omitempty"` |
| 212 | 182 | Website string `json:"website,omitempty"` |
| 213 | // About is long-form markdown, rendered by the web between the | |
| 214 | // header and the activity graph. | |
| 183 | // About is the long-form text from <owner>/.gitbay, rendered by the | |
| 184 | // web between the header and the activity graph. | |
| 215 | 185 | About string `json:"about,omitempty"` |
| 216 | 186 | AboutFormat string `json:"about_format,omitempty"` |
| 217 | 187 | // AboutPath is where the about was read from in <owner>/.gitbay, so a |
| @@ -403,7 +373,7 @@ func runProfileShow(c *Ctx, args []string) int { | ||
| 403 | 373 | } |
| 404 | 374 | |
| 405 | 375 | func runProfileSet(c *Ctx, args []string) int { |
| 406 | rest, e, err := parseProfileFlags(c, args) | |
| 376 | rest, e, err := parseProfileFlags(args) | |
| 407 | 377 | if err != nil { |
| 408 | 378 | return c.failInput(err) |
| 409 | 379 | } |
| @@ -411,7 +381,7 @@ func runProfileSet(c *Ctx, args []string) int { | ||
| 411 | 381 | return c.usage() |
| 412 | 382 | } |
| 413 | 383 | if e.empty() { |
| 414 | return c.fail(protocol.ExitUsage, "nothing to set: pass --description, --website, --about and/or --link") | |
| 384 | return c.fail(protocol.ExitUsage, "nothing to set: pass --description, --website and/or --link") | |
| 415 | 385 | } |
| 416 | 386 | p, err := c.Store.OwnerProfile("user", c.User.ID) |
| 417 | 387 | if err != nil { |
| @@ -425,12 +395,12 @@ func runProfileSet(c *Ctx, args []string) int { | ||
| 425 | 395 | return c.fail(protocol.ExitFailure, "%v", err) |
| 426 | 396 | } |
| 427 | 397 | return emitProfile(c, ProfileOut{Name: c.User.Username, Kind: "user", |
| 428 | Description: p.Description, Website: p.Website, About: p.About, | |
| 429 | AboutFormat: p.AboutFormat, Links: p.Links, Repos: []ProfileRepo{}}) | |
| 398 | Description: p.Description, Website: p.Website, Links: p.Links, | |
| 399 | Repos: []ProfileRepo{}}) | |
| 430 | 400 | } |
| 431 | 401 | |
| 432 | 402 | func runOrgProfile(c *Ctx, args []string) int { |
| 433 | rest, e, err := parseProfileFlags(c, args) | |
| 403 | rest, e, err := parseProfileFlags(args) | |
| 434 | 404 | if err != nil { |
| 435 | 405 | return c.failInput(err) |
| 436 | 406 | } |
| @@ -457,6 +427,6 @@ func runOrgProfile(c *Ctx, args []string) int { | ||
| 457 | 427 | return c.fail(protocol.ExitFailure, "%v", err) |
| 458 | 428 | } |
| 459 | 429 | return emitProfile(c, ProfileOut{Name: org.Name, Kind: "org", |
| 460 | Description: p.Description, Website: p.Website, About: p.About, | |
| 461 | AboutFormat: p.AboutFormat, Links: p.Links, Repos: []ProfileRepo{}}) | |
| 430 | Description: p.Description, Website: p.Website, Links: p.Links, | |
| 431 | Repos: []ProfileRepo{}}) | |
| 462 | 432 | } |
internal/httpd/account.go +33 −15
| @@ -34,12 +34,11 @@ type accountPGP struct { | ||
| 34 | 34 | // accountForm renders the account's own settings: keys, addresses, and the |
| 35 | 35 | // commands for everything that stays on SSH. |
| 36 | 36 | func (s *Server) accountForm(w http.ResponseWriter, r *http.Request, u store.User) { |
| 37 | s.accountPage(w, r, u, nil) | |
| 37 | s.accountPage(w, r, u) | |
| 38 | 38 | } |
| 39 | 39 | |
| 40 | // accountPage renders the settings page. d is non-nil when the profile | |
| 41 | // form asked to see its about text rather than save it (#235). | |
| 42 | func (s *Server) accountPage(w http.ResponseWriter, r *http.Request, u store.User, d *draft) { | |
| 40 | // accountPage renders the settings page. | |
| 41 | func (s *Server) accountPage(w http.ResponseWriter, r *http.Request, u store.User) { | |
| 43 | 42 | var keys []accountKey |
| 44 | 43 | if list, err := s.st.ListSSHKeys(u.ID); err == nil { |
| 45 | 44 | for _, k := range list { |
| @@ -67,6 +66,14 @@ func (s *Server) accountPage(w http.ResponseWriter, r *http.Request, u store.Use | ||
| 67 | 66 | watchOn, _ := s.st.WatchEnabled(u.ID) |
| 68 | 67 | theme, _ := s.st.Theme(u.ID) |
| 69 | 68 | |
| 69 | // The about text is a file. The page points at it rather than editing | |
| 70 | // it: the repository's own editor already does that job. | |
| 71 | aboutRepo := u.Username + "/" + control.ProfileRepoName | |
| 72 | aboutEdit := "" | |
| 73 | if profile.AboutPath != "" { | |
| 74 | aboutEdit = "/" + aboutRepo + "/edit/main/" + profile.AboutPath | |
| 75 | } | |
| 76 | ||
| 70 | 77 | s.render(w, "account.html", struct { |
| 71 | 78 | basePage |
| 72 | 79 | Tab string // marks the rail's Settings row as current |
| @@ -75,15 +82,17 @@ func (s *Server) accountPage(w http.ResponseWriter, r *http.Request, u store.Use | ||
| 75 | 82 | Emails []store.Email |
| 76 | 83 | Profile control.ProfileOut |
| 77 | 84 | LinksText string |
| 85 | AboutRepo string // <user>/.gitbay, which holds the about text | |
| 86 | AboutEdit string // the file editor's URL, empty when there is no file yet | |
| 78 | 87 | Host string |
| 79 | 88 | Notice string |
| 80 | 89 | Message string |
| 81 | 90 | MailOn bool |
| 82 | 91 | WatchOn bool |
| 83 | 92 | ThemeSetting string // system, light or dark: the form's selected option |
| 84 | Draft *draft | |
| 85 | }{s.baseFor(u), "account", keys, pgp, emails, profile, profileLinksText(profile.Links), s.cfg.SiteHost(), | |
| 86 | s.takeFlash(w, r), r.URL.Query().Get("m"), mailOn, watchOn, theme, d}) | |
| 93 | }{s.baseFor(u), "account", keys, pgp, emails, profile, profileLinksText(profile.Links), | |
| 94 | aboutRepo, aboutEdit, s.cfg.SiteHost(), | |
| 95 | s.takeFlash(w, r), r.URL.Query().Get("m"), mailOn, watchOn, theme}) | |
| 87 | 96 | } |
| 88 | 97 | |
| 89 | 98 | // accountExport hands the browser the same bundle `account export` |
| @@ -246,25 +255,34 @@ func (s *Server) accountSubmit(w http.ResponseWriter, r *http.Request, u store.U | ||
| 246 | 255 | } |
| 247 | 256 | back("", "notification preferences saved") |
| 248 | 257 | case "profile": |
| 249 | format := bodyFormat(r) | |
| 250 | if wantsPreview(r) { | |
| 251 | s.accountPage(w, r, u, s.draftWith(r, "about", format, r.FormValue("about"), ugcHTML)) | |
| 252 | return | |
| 253 | } | |
| 254 | 258 | argv := []string{"profile", "set", |
| 255 | 259 | "--description", r.FormValue("description"), |
| 256 | 260 | "--website", r.FormValue("website"), |
| 257 | "--about-format", format, | |
| 258 | "--file", "-", | |
| 259 | 261 | } |
| 260 | 262 | for _, link := range profileLinkArgs(r.FormValue("links")) { |
| 261 | 263 | argv = append(argv, "--link", link) |
| 262 | 264 | } |
| 263 | if msg, ok := s.runControlStdin(u, argv, r.FormValue("about")); !ok { | |
| 265 | if _, msg, ok := s.runControl(u, argv); !ok { | |
| 264 | 266 | back(msg, "") |
| 265 | 267 | return |
| 266 | 268 | } |
| 267 | 269 | back("", "profile updated") |
| 270 | case "profile-repo": | |
| 271 | // The about text is a file. Create the repository that holds it and | |
| 272 | // commit a starter README, so the file editor has a branch to open. | |
| 273 | path := u.Username + "/" + control.ProfileRepoName | |
| 274 | if _, msg, ok := s.runControl(u, []string{"repo", "create", path}); !ok { | |
| 275 | back(msg, "") | |
| 276 | return | |
| 277 | } | |
| 278 | starter := "# " + u.Username + "\n\nThis is the about text on your profile.\n" | |
| 279 | if msg, ok := s.runControlStdin(u, []string{"repo", "commit-file", path, | |
| 280 | control.AboutBase + ".md", "--ref", "main", | |
| 281 | "--message", "add profile about", "--file", "-"}, starter); !ok { | |
| 282 | back(msg, "") | |
| 283 | return | |
| 284 | } | |
| 285 | back("", "profile repository created") | |
| 268 | 286 | default: |
| 269 | 287 | back("unknown form", "") |
| 270 | 288 | } |
internal/web/templates/account.html +13 −8
| @@ -22,20 +22,25 @@ | ||
| 22 | 22 | <div class="colmain"> |
| 23 | 23 | <section id="profile"><h2>Profile</h2> |
| 24 | 24 | <p class="meta">Shown on your profile page, <a href="/{{.Viewer}}">/{{.Viewer}}</a>.</p> |
| 25 | {{if .Draft.Is "about"}}{{template "previewblock" .Draft.HTML}}{{end}} | |
| 26 | 25 | <form method="post" action="/settings" class="setform stack"> |
| 27 | 26 | <input type="hidden" name="field" value="profile"> |
| 28 | 27 | <label for="p-description">Description</label> |
| 29 | <input type="text" id="p-description" name="description" value="{{.Draft.Or "about" "description" .Profile.Description}}" placeholder="one line, shown in listings"> | |
| 28 | <input type="text" id="p-description" name="description" value="{{.Profile.Description}}" placeholder="one line, shown in listings"> | |
| 30 | 29 | <label for="p-website">Website</label> |
| 31 | <input type="text" id="p-website" name="website" value="{{.Draft.Or "about" "website" .Profile.Website}}" placeholder="https://example.org"> | |
| 30 | <input type="text" id="p-website" name="website" value="{{.Profile.Website}}" placeholder="https://example.org"> | |
| 32 | 31 | <label for="p-links">Links</label> |
| 33 | <textarea id="p-links" name="links" rows="3" placeholder="one per line: label|https://... or a bare https://... (at most 5)">{{.Draft.Or "about" "links" .LinksText}}</textarea> | |
| 34 | <label for="p-about">About</label> | |
| 35 | <textarea id="p-about" name="about" rows="8" placeholder="longer, shown below your repositories">{{.Draft.Or "about" "about" .Profile.About}}</textarea> | |
| 36 | {{template "formatpicker" (.Draft.Or "about" "format" .Profile.AboutFormat)}} | |
| 37 | <span class="btngroup"><button type="submit" class="btn">Save profile</button>{{template "previewbtn"}}</span> | |
| 32 | <textarea id="p-links" name="links" rows="3" placeholder="one per line: label|https://... or a bare https://... (at most 5)">{{.LinksText}}</textarea> | |
| 33 | <span class="btngroup"><button type="submit" class="btn">Save profile</button></span> | |
| 38 | 34 | </form> |
| 35 | <h3>About</h3> | |
| 36 | <p class="meta">The longer text below your repositories is a file: | |
| 37 | <code>profile/README.md</code> in <a href="/{{.AboutRepo}}">{{.AboutRepo}}</a>. | |
| 38 | Write it with a push, or edit it here.</p> | |
| 39 | {{if .AboutEdit}}<p><a class="btn" href="{{.AboutEdit}}">Edit {{.Profile.AboutPath}}</a></p> | |
| 40 | {{else}}<form method="post" action="/settings" class="setform"> | |
| 41 | <input type="hidden" name="field" value="profile-repo"> | |
| 42 | <button type="submit" class="btn">Create {{.AboutRepo}}</button> | |
| 43 | </form>{{end}} | |
| 39 | 44 | </section> |
| 40 | 45 | |
| 41 | 46 | <section id="keys"><h2>SSH keys</h2> |