• 07-Profile Page and Avatar
    • Profile Page
    • Avatar
    • More Info
    • Edit Profile
    • Links

    07-Profile Page and Avatar

    本章将致力于为应用添加个人主页。个人主页用来展示用户的相关信息,其个人信息由本人录入。 我将为你展示如何动态地生成每个用户的主页,并提供一个编辑页面给他们来更新个人信息。

    本章的GitHub链接为: Source, Diff,
    Zip

    Profile Page

    作为创建个人主页的第一步,让我们为其URL /user/ 新建一个对应的视图函数。

    我们还是老套路 一个 vm 加一个 page 另外再加一个 controllermodel 暂时不涉及新的model)

    vm/profile.go

    1. package vm
    2. import "github.com/bonfy/go-mega-code/model"
    3. // ProfileViewModel struct
    4. type ProfileViewModel struct {
    5. BaseViewModel
    6. Posts []model.Post
    7. ProfileUser model.User
    8. }
    9. // ProfileViewModelOp struct
    10. type ProfileViewModelOp struct{}
    11. // GetVM func
    12. func (ProfileViewModelOp) GetVM(sUser, pUser string) (ProfileViewModel, error) {
    13. v := ProfileViewModel{}
    14. v.SetTitle("Profile")
    15. u1, err := model.GetUserByUsername(pUser)
    16. if err != nil {
    17. return v, err
    18. }
    19. posts, _ := model.GetPostsByUserID(u1.ID)
    20. v.ProfileUser = *u1
    21. v.Posts = *posts
    22. v.SetCurrentUser(sUser)
    23. return v, nil
    24. }

    _base.html 在登陆情况下 加入 Profile 的链接

    templates/_base.html

    1. ...
    2. <div>
    3. Blog:
    4. <a href="/">Home</a>
    5. {{if .CurrentUser}}
    6. <a href="/user/{{.CurrentUser}}">Profile</a>
    7. <a href="/logout">Logout</a>
    8. {{else}}
    9. <a href="/login">Login</a>
    10. {{end}}
    11. </div>
    12. ...

    templates/content/profile.html

    1. {{define "content"}}
    2. <h1>User: {{.ProfileUser.Username}}</h1>
    3. <hr/>
    4. {{range .Posts}}
    5. <p>
    6. {{ .User.Username }} says: <b>{{ .Body }}</b>
    7. </p>
    8. {{end}}
    9. {{end}}

    加入 profileController

    controller/home.go

    1. package controller
    2. import (
    3. "fmt"
    4. "log"
    5. "net/http"
    6. "github.com/bonfy/go-mega-code/vm"
    7. "github.com/gorilla/mux"
    8. )
    9. type home struct{}
    10. func (h home) registerRoutes() {
    11. r := mux.NewRouter()
    12. r.HandleFunc("/logout", middleAuth(logoutHandler))
    13. r.HandleFunc("/login", loginHandler)
    14. r.HandleFunc("/register", registerHandler)
    15. r.HandleFunc("/user/{username}", middleAuth(profileHandler))
    16. r.HandleFunc("/", middleAuth(indexHandler))
    17. http.Handle("/", r)
    18. }
    19. ...
    20. func profileHandler(w http.ResponseWriter, r *http.Request) {
    21. tpName := "profile.html"
    22. vars := mux.Vars(r)
    23. pUser := vars["username"]
    24. sUser, _ := getSessionUser(r)
    25. vop := vm.ProfileViewModelOp{}
    26. v, err := vop.GetVM(sUser, pUser)
    27. if err != nil {
    28. msg := fmt.Sprintf("user ( %s ) does not exist", pUser)
    29. w.Write([]byte(msg))
    30. return
    31. }
    32. templates[tpName].Execute(w, &v)
    33. }
    34. ...

    这里面由于要实现 flask 那样的 /user/username 的效果,快速的方法是引入 gorilla/mux 的第三方package

    1. $ go get -v github.com/gorilla/mux

    然后注意 registerRoutes 函数,里面原来的 http.handleFunc 全部替换成 r.handleFunc 交给第三方的 gorilla/mux来处理,这样我们就可以在 handler里面使用 mux.Vars 来解析URL里面的{username}

    另外 严谨起见,这里在 profileHandler 前面加了 auth check,其实不加的话也是OK的,区别就是在登陆之前能不能查看特定 user 的 Profile

    我们运行程序,登陆后点击 Profile的链接,就能查看到结果了。

    当然你在地址栏里面直接输入 http://127.0.0.1/user/username如果存在,就显示 User 的 Profile,不存在就提示 user (username) does not exist

    07-01

    本小节 Diff

    Avatar

    我相信你也觉得我刚刚建立的个人主页非常枯燥乏味。为了使它们更加有趣,我将添加用户头像。与其在服务器上处理大量的上传图片,我将使用Gravatar为所有用户提供图片服务。

    Gravatar服务使用起来非常简单。 要请求给定用户的图片,使用格式为https://www.gravatar.com/avatar/ 的URL即可,其中 hash 是用户的电子邮件地址的MD5哈希值。 在下面,你可以看到如何生成电子邮件为john@example.com的用户的Gravatar URL:

    如果你想看一个实际的例子,我自己的Gravatar URL是https://www.gravatar.com/avatar/c60f4fa4bf54012b80bc140aab0fc2bc。Gravatar返回的图片如下:

    avatar

    默认情况下 是 80*80 , 但可以通过向URL的查询字符串添加s参数来请求不同大小的图片。如: https://www.gravatar.com/avatar/c60f4fa4bf54012b80bc140aab0fc2bc?s=128

    另一个可传递给Gravatar的有趣参数是d,它让Gravatar为没有向服务注册头像的用户提供的随机头像。 我最喜欢的随机头像类型是“identicon”,它为每个邮箱都返回一个漂亮且不重复的几何设计图片。 如下:

    07-Profile-Page-And-Avatar - 图3

    请注意,一些Web浏览器插件(如Ghostery)会屏蔽Gravatar图像,因为它们认为Automattic(Gravatar服务的所有者)可以根据你发送的获取头像的请求来判断你正在访问的网站。 如果在浏览器中看不到头像,你在排查问题的时候可以考虑以下是否在浏览器中安装了此类插件。

    由于头像与用户相关联,所以将生成头像URL的逻辑添加到用户模型是有道理的。

    如果你对Gravatar服务很有兴趣,可以学习他们的文档。

    理论说好了,我们来写代码,由于 Go 原生的Template 不像 Jinja2 那么好支持函数( Go Template 是支持自定义函数的,只是要在 template.New().Funcs() 中预先传入,与我们已有的 PopulateTemplates函数集成上有点难度)

    Notice: Go Template 支持类的 Func,不用预先传入,这里 Avatar 字段不是特别的必要了,特此说明,可以参见 12-Dates-And-Times的用法

    所以这里发挥主观能动性,直接将 Avatar 作为字段放入数据库中,等于冗余了Avatar数据,但是减少了我们coding的难度(我们也乘此机会,fix下上次提到的Gorm format问题)

    虽然我们 GeneratePasswordHash 就是 MD5 方法,不过为了逻辑的清晰,我们可以像下面代码这样处理。

    model/utils.go

    1. package model
    2. import (
    3. "crypto/md5"
    4. "encoding/hex"
    5. )
    6. // GeneratePasswordHash : Use MD5
    7. func GeneratePasswordHash(pwd string) string {
    8. return Md5(pwd)
    9. }
    10. // Md5 func
    11. func Md5(origin string) string {
    12. hasher := md5.New()
    13. hasher.Write([]byte(origin))
    14. return hex.EncodeToString(hasher.Sum(nil))
    15. }

    model/user.go

    1. ...
    2. // 说明: User 加入 Avatar、AboutMe、LastSeen 字段
    3. type User struct {
    4. ID int `gorm:"primary_key"`
    5. Username string `gorm:"type:varchar(64)"`
    6. Email string `gorm:"type:varchar(120)"`
    7. PasswordHash string `gorm:"type:varchar(128)"`
    8. LastSeen *time.Time
    9. AboutMe string `gorm:"type:varchar(140)"`
    10. Avatar string `gorm:"type:varchar(200)"`
    11. Posts []Post
    12. Followers []*User `gorm:"many2many:follower;association_jointable_foreignkey:follower_id"`
    13. }
    14. ...
    15. // 说明: 在增加User 的时候,直接设置Avatar
    16. // SetAvatar func
    17. func (u *User) SetAvatar(email string) {
    18. u.Avatar = fmt.Sprintf("https://www.gravatar.com/avatar/%s?d=identicon", Md5(email))
    19. }
    20. // AddUser func
    21. func AddUser(username, password, email string) error {
    22. user := User{Username: username, Email: email}
    23. user.SetPassword(password)
    24. user.SetAvatar(email)
    25. return db.Create(&user).Error
    26. }
    27. ...

    cmd/db_init/main.go

    1. package main
    2. import (
    3. "fmt"
    4. "log"
    5. "github.com/bonfy/go-mega-code/model"
    6. _ "github.com/jinzhu/gorm/dialects/mysql"
    7. )
    8. func main() {
    9. log.Println("DB Init ...")
    10. db := model.ConnectToDB()
    11. defer db.Close()
    12. model.SetDB(db)
    13. db.DropTableIfExists(model.User{}, model.Post{})
    14. db.CreateTable(model.User{}, model.Post{})
    15. users := []model.User{
    16. {
    17. Username: "bonfy",
    18. PasswordHash: model.GeneratePasswordHash("abc123"),
    19. Email: "i@bonfy.im",
    20. Avatar: fmt.Sprintf("https://www.gravatar.com/avatar/%s?d=identicon", model.Md5("i@bonfy.im")),
    21. Posts: []model.Post{
    22. {Body: "Beautiful day in Portland!"},
    23. },
    24. },
    25. {
    26. Username: "rene",
    27. PasswordHash: model.GeneratePasswordHash("abc123"),
    28. Email: "rene@test.com",
    29. Avatar: fmt.Sprintf("https://www.gravatar.com/avatar/%s?d=identicon", model.Md5("rene@test.com")),
    30. Posts: []model.Post{
    31. {Body: "The Avengers movie was so cool!"},
    32. {Body: "Sun shine is beautiful"},
    33. },
    34. },
    35. }
    36. for _, u := range users {
    37. db.Debug().Create(&u)
    38. }
    39. }

    这样再运行

    1. $ go run cmd/db_init/main.go

    数据库中的数据就有了 Avatar 这个字段

    07-02

    现在我们再在 profile.html加入Avatar

    这样我的个人主页的顶部有一个不错的大头像,不止如此,底下的所有用户动态都会有一个小头像。

    templates/content/profile.html

    1. {{define "content"}}
    2. <table>
    3. <tr valign="top">
    4. <td><img src="{{.ProfileUser.Avatar}}&s=128"></td>
    5. <td><h1>User: {{.ProfileUser.Username}}</h1></td>
    6. </tr>
    7. </table>
    8. <hr/>
    9. {{range .Posts}}
    10. <table>
    11. <tr valign="top">
    12. <td><img src="{{.User.Avatar}}&s=36"></td>
    13. <td>{{ .User.Username }} says:<br>{{ .Body }}</td>
    14. </tr>
    15. </table>
    16. {{end}}
    17. {{end}}

    运行程序

    1. $ go run main.go

    07-03

    本小节 Diff

    More Info

    新增的个人主页存在的一个问题是,真正显示的内容不够丰富。 用户喜欢在个人主页上展示他们的相关信息,所以我会让他们写一些自我介绍并在这里展示。 我也将跟踪每个用户最后一次访问该网站的时间,并显示在他们的个人主页上。

    新字段 last_seenabout_me 已经在前面一起创建过了,我们只要在页面中加入就行了。

    现在 model/user.go 中加入 UpdateLastSeen 函数

    model/user.go

    1. ...
    2. // UpdateUserByUsername func
    3. func UpdateUserByUsername(username string, contents map[string]interface{}) error {
    4. item, err := GetUserByUsername(username)
    5. if err != nil {
    6. return err
    7. }
    8. return db.Model(item).Updates(contents).Error
    9. }
    10. // UpdateLastSeen func
    11. func UpdateLastSeen(username string) error {
    12. contents := map[string]interface{}{"last_seen": time.Now()}
    13. return UpdateUserByUsername(username, contents)
    14. }

    在 middleAuth 中,如果判断用户登陆了,就更新他的 LastSeen 时间 (middleAuth的用法类似于Python 里面的装饰器用法)

    Tip: 我们一般是不会在 controller 层直接和 model 层打交道,一般会通过 vm 层去处理,但是由于 middle 层不具有具体的view,我们这里破例直接调用 model 中的方法。 当然考究点,你可以建立一个 middle 的vm,在里面新建一个 UpdateLastSeen 再在 controller/middle 中调用,也是可以的

    controller/middle.go

    1. package controller
    2. import (
    3. "log"
    4. "net/http"
    5. "github.com/bonfy/go-mega-code/model"
    6. )
    7. func middleAuth(next http.HandlerFunc) http.HandlerFunc {
    8. return func(w http.ResponseWriter, r *http.Request) {
    9. username, err := getSessionUser(r)
    10. log.Println("middle:", username)
    11. if username != "" {
    12. log.Println("Last seen:", username)
    13. model.UpdateLastSeen(username)
    14. }
    15. if err != nil {
    16. log.Println("middle get session err and redirect to login")
    17. http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
    18. } else {
    19. next.ServeHTTP(w, r)
    20. }
    21. }
    22. }
    23. ...

    profile.html 中加入 AboutMe 以及 LastSeen

    templates/content/profile.html

    1. {{define "content"}}
    2. <table>
    3. <tr valign="top">
    4. <td><img src="{{.ProfileUser.Avatar}}&s=128"></td>
    5. <td>
    6. <h1>User: {{.ProfileUser.Username}}</h1>
    7. {{if .ProfileUser.AboutMe}}
    8. <p>{{ .ProfileUser.AboutMe }}</p>
    9. {{end}}
    10. {{if .ProfileUser.LastSeen}}
    11. <p>Last seen on: {{ .ProfileUser.LastSeen }}</p>
    12. {{end}}
    13. </td>
    14. </tr>
    15. </table>
    16. <hr/>
    17. {{range .Posts}}
    18. <table>
    19. <tr valign="top">
    20. <td><img src="{{.User.Avatar}}&s=36"></td>
    21. <td>{{ .User.Username }} says:<br>{{ .Body }}</td>
    22. </tr>
    23. </table>
    24. {{end}}
    25. {{end}}

    07-04

    本小节 Diff

    Edit Profile

    我还需要给用户一个表单,让他们输入一些个人资料。 表单将允许用户更改他们的用户名,并且写一些个人介绍,以存储在新的about_me字段中。

    其实细分的话分两步:

    • Profile页面加入 Edit 的链接
    • 增加一个 profile_edit 的页面

    加入 Edit 链接: vm 增加 Editable 字段, 然后在 profile.html 中加入链接

    model/profile.go

    1. package vm
    2. import "github.com/bonfy/go-mega-code/model"
    3. // ProfileViewModel struct
    4. type ProfileViewModel struct {
    5. BaseViewModel
    6. Posts []model.Post
    7. Editable bool
    8. ProfileUser model.User
    9. }
    10. // ProfileViewModelOp struct
    11. type ProfileViewModelOp struct{}
    12. // GetVM func
    13. func (ProfileViewModelOp) GetVM(sUser, pUser string) (ProfileViewModel, error) {
    14. v := ProfileViewModel{}
    15. v.SetTitle("Profile")
    16. u1, err := model.GetUserByUsername(pUser)
    17. if err != nil {
    18. return v, err
    19. }
    20. posts, _ := model.GetPostsByUserID(u1.ID)
    21. v.ProfileUser = *u1
    22. v.Editable = (sUser == pUser)
    23. v.Posts = *posts
    24. v.SetCurrentUser(sUser)
    25. return v, nil
    26. }

    templates/content/profile.html

    1. {{define "content"}}
    2. <table>
    3. <tr valign="top">
    4. <td><img src="{{.ProfileUser.Avatar}}&s=128"></td>
    5. <td>
    6. <h1>User: {{.ProfileUser.Username}}</h1>
    7. {{if .ProfileUser.AboutMe}}
    8. <p><pre>{{ .ProfileUser.AboutMe }}</pre></p>
    9. {{end}}
    10. {{if .ProfileUser.LastSeen}}
    11. <p>Last seen on: {{ .ProfileUser.LastSeen }}</p>
    12. {{end}}
    13. {{if .Editable}}
    14. <p><a href="/profile_edit">Edit your profile</a></p>
    15. {{end}}
    16. </td>
    17. </tr>
    18. </table>
    19. <hr/>
    20. ...

    增加 profile_edit ,这个老套路

    model/user.go

    1. ...
    2. // UpdateAboutMe func
    3. func UpdateAboutMe(username, text string) error {
    4. contents := map[string]interface{}{"about_me": text}
    5. return UpdateUserByUsername(username, contents)
    6. }

    vm/profile_edit.go

    1. package vm
    2. import "github.com/bonfy/go-mega-code/model"
    3. // ProfileEditViewModel struct
    4. type ProfileEditViewModel struct {
    5. LoginViewModel
    6. ProfileUser model.User
    7. }
    8. // ProfileEditViewModelOp struct
    9. type ProfileEditViewModelOp struct{}
    10. // GetVM func
    11. func (ProfileEditViewModelOp) GetVM(username string) ProfileEditViewModel {
    12. v := ProfileEditViewModel{}
    13. u, _ := model.GetUserByUsername(username)
    14. v.SetTitle("Profile Edit")
    15. v.SetCurrentUser(username)
    16. v.ProfileUser = *u
    17. return v
    18. }
    19. // UpdateAboutMe func
    20. func UpdateAboutMe(username, text string) error {
    21. return model.UpdateAboutMe(username, text)
    22. }

    templates/content/profile_edit.html

    1. {{define "content"}}
    2. <h1>Profile Edit</h1>
    3. <p>Username: {{.ProfileUser.Username}}</p>
    4. <form action="/profile_edit" method="post" name="profile_edit">
    5. <p>About Me</p>
    6. <p><textarea name="aboutme" rows="5" cols="80" value="" placeholder="about me">{{.ProfileUser.AboutMe}}</textarea></p>
    7. <p><input type="submit" name="submit" value="Save"></p>
    8. </form>
    9. {{if .Errs}}
    10. <ul>
    11. {{range .Errs}}
    12. <li>{{.}}</li>
    13. {{end}}
    14. </ul>
    15. {{end}}
    16. {{end}}

    最后在 controller 里面加入 profileEditHandler

    controller/home.go

    1. ...
    2. func (h home) registerRoutes() {
    3. r := mux.NewRouter()
    4. r.HandleFunc("/logout", middleAuth(logoutHandler))
    5. r.HandleFunc("/login", loginHandler)
    6. r.HandleFunc("/register", registerHandler)
    7. r.HandleFunc("/user/{username}", middleAuth(profileHandler))
    8. r.HandleFunc("/profile_edit", middleAuth(profileEditHandler))
    9. r.HandleFunc("/", middleAuth(indexHandler))
    10. http.Handle("/", r)
    11. }
    12. ...
    13. func profileEditHandler(w http.ResponseWriter, r *http.Request) {
    14. tpName := "profile_edit.html"
    15. username, _ := getSessionUser(r)
    16. vop := vm.ProfileEditViewModelOp{}
    17. v := vop.GetVM(username)
    18. if r.Method == http.MethodGet {
    19. err := templates[tpName].Execute(w, &v)
    20. if err != nil {
    21. log.Println(err)
    22. }
    23. }
    24. if r.Method == http.MethodPost {
    25. r.ParseForm()
    26. aboutme := r.Form.Get("aboutme")
    27. log.Println(aboutme)
    28. if err := vm.UpdateAboutMe(username, aboutme); err != nil {
    29. log.Println("update Aboutme error:", err)
    30. w.Write([]byte("Error update aboutme"))
    31. return
    32. }
    33. http.Redirect(w, r, fmt.Sprintf("/user/%s", username), http.StatusSeeOther)
    34. }
    35. }

    07-05

    07-06

    本小节 Diff

    • 目录
    • 上一节: 06-User-Login
    • 下一节: 08-Follower