Skip to content

Commit

Permalink
Add Paste view count (#32)
Browse files Browse the repository at this point in the history
- Added Update method to the PasteService
 - New API endpoint to update the view count
  • Loading branch information
iliafrenkel authored Aug 2, 2021
1 parent b323a7e commit cc5bcf2
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 4 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# Test data folders
testdata
test.db
test.log

# Output of the go coverage tool, specifically when used with LiteIDE
*.out
Expand Down
3 changes: 3 additions & 0 deletions src/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Paste struct {
Syntax string `json:"syntax"`
UserID int64 `json:"user_id" gorm:"index default:null"`
User User `json:"-"`
Views int64 `json:"views"`
}

// URL generates a base62 encoded string from the paste ID. This string is
Expand Down Expand Up @@ -120,6 +121,8 @@ type PasteService interface {
Delete(id int64) error
// List returns a list of pastes for a user specified by ID.
List(uid int64) ([]Paste, error)
// Update updates existing paste
Update(p Paste) error
}

// PasteStore is the interface that defines methods required to persist and
Expand Down
52 changes: 52 additions & 0 deletions src/api/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ type APIServer struct {
// The routes are:
// GET /paste/{id} - get paste by ID
// POST /paste/{id} - get password protected paste by ID
// PATCH /paste/{id}/view - update existing paste view count
// POST /paste - create new paste
// DELETE /paste/{id} - delete paste by ID
// GET /paste/list/{id} - get a list of pastes by UserID
Expand Down Expand Up @@ -115,6 +116,7 @@ func New(pSvc api.PasteService, uSvc api.UserService, opts APIServerOptions) *AP
{
paste.GET("/:id", handler.handlePasteGet)
paste.POST("/:id", handler.verifyJSONMiddleware(new(api.PastePassword)), handler.handlePasteGetWithPassword)
paste.PATCH("/:id/view", handler.handlePasteUpdateViewCount)
paste.POST("", handler.verifyJSONMiddleware(new(api.PasteForm)), handler.handlePasteCreate)
paste.DELETE("/:id", handler.handlePasteDelete)
paste.GET("/list/:id", handler.handlePasteList)
Expand Down Expand Up @@ -480,6 +482,56 @@ func (h *APIServer) handlePasteList(c *gin.Context) {
c.JSON(http.StatusOK, pastes)
}

// handlePasteUpdateViewCount is an HTTP handler for PATCH /paste/:id route.
// It updates the paste view count.
func (h *APIServer) handlePasteUpdateViewCount(c *gin.Context) {
// We expect the id parameter as base62 encoded string, we try to decode
// it into a int64 paste id and return 404 if we can't.
id, err := base62.Decode(c.Param("id"))
if err != nil {
c.JSON(http.StatusNotFound, api.HTTPError{
Code: http.StatusNotFound,
Message: "Paste not found",
})
return
}

p, err := h.PasteService.Get(id)
// Service returned an error and we don't know what to do here. We log the
// error and send 500 InternalServerError back to the caller.
if err != nil {
log.Println("handlePasteUpdateViewCount: unexpected error: ", err.Error())
c.JSON(http.StatusInternalServerError, api.HTTPError{
Code: http.StatusInternalServerError,
Message: fmt.Sprintf("%s: %s", http.StatusText(http.StatusInternalServerError), err.Error()),
})
return
}
// Service call was successful but returned nil. Respond with 404 NoFound.
if p == nil {
c.JSON(http.StatusNotFound, api.HTTPError{
Code: http.StatusNotFound,
Message: "Paste not found",
})
return
}

// Increase the view count and save the paste.
p.Views += 1
err = h.PasteService.Update(*p)
if err != nil {
log.Println("handlePasteUpdateViewCount: unexpected error: ", err.Error())
c.JSON(http.StatusInternalServerError, api.HTTPError{
Code: http.StatusInternalServerError,
Message: fmt.Sprintf("%s: %s", http.StatusText(http.StatusInternalServerError), err.Error()),
})
return
}

c.Header("Content-Type", "application/json")
c.Status(http.StatusOK)
}

// handleUserLogin is an HTTP handler for POST /user/login route. It returns
// auth.UserInfo with the username and JWT token on success.
func (h *APIServer) handleUserLogin(c *gin.Context) {
Expand Down
47 changes: 47 additions & 0 deletions src/api/http/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,53 @@ func Test_ListPastesWrongID(t *testing.T) {
t.Errorf("Response should be [%s], got [%s]", want, got)
}
}

func Test_UpdatePasteViewCount(t *testing.T) {
var p = createTestPaste()
paste, err := pasteSvc.Create(*p)
if err != nil {
t.Fatal(err)
}
// Update view count
client := &http.Client{}
req, _ := http.NewRequest(http.MethodPatch, mckSrv.URL+"/paste/"+paste.URL()+"/view", nil)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
_, err = client.Do(req)
if err != nil {
t.Errorf("Failed to update view count: %v", err)
return
}
paste.Views += 1
// Get the paste back
resp, err := http.Get(mckSrv.URL + "/paste/" + paste.URL())

// Handle any unexpected error
if err != nil {
t.Fatal(err)
}

// Check status
if resp.StatusCode != http.StatusOK {
t.Errorf("Status should be OK, got %d", resp.StatusCode)
}

// Check body
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
got := string(b)
want, err := json.Marshal(paste)
// Handle any unexpected error
if err != nil {
t.Fatal(err)
}
if got != string(want) {
t.Errorf("Response should be [%s], got [%s]", want, got)
}
}
func Test_DeletePasteNotFound(t *testing.T) {
client := &http.Client{}
req, err := http.NewRequest(http.MethodDelete, mckSrv.URL+"/paste/qweasd", nil)
Expand Down
5 changes: 5 additions & 0 deletions src/api/paste/paste.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,8 @@ func (s *PasteService) List(uid int64) ([]api.Paste, error) {
}
return pastes, nil
}

// Update updates existing paste
func (s *PasteService) Update(p api.Paste) error {
return s.PasteStore.Store(p)
}
29 changes: 29 additions & 0 deletions src/api/paste/paste_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,3 +286,32 @@ func Test_List(t *testing.T) {
t.Errorf("wrong title, want %s got %s", p.Title, list[0].Title)
}
}

func Test_Update(t *testing.T) {
t.Parallel()

var p = createTestPaste()
var paste *api.Paste
paste, err := service.Create(*p)
if err != nil {
t.Errorf("failed to create a paste: %v", err)
return
}
// update the paste
paste.Views += 1
err = service.Update(*paste)
if err != nil {
t.Errorf("failed to update the paste: %v", err)
return
}
// check that the update was saved
updPaste, err := service.Get(paste.ID)
if err != nil {
t.Errorf("failed to get updated paste: %v", err)
return
}
if updPaste.Views != 1 {
t.Errorf("wrong view count, want 1 got %d", updPaste.Views)
}

}
4 changes: 2 additions & 2 deletions src/api/paste/sqldb/sqldb.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ type DBPasteStore struct {
func (s DBPasteStore) Store(paste api.Paste) error {
var err error
if paste.UserID == 0 {
err = s.db.Omit("user_id").Create(&paste).Error
err = s.db.Omit("user_id").Save(&paste).Error
} else {
err = s.db.Create(&paste).Error
err = s.db.Save(&paste).Error
}
if err != nil {
return err
Expand Down
18 changes: 18 additions & 0 deletions src/web/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,24 @@ func (h *WebServer) handleGetPaste(c *gin.Context) {
sort.Slice(pastes, func(i, j int) bool { return pastes[i].Created.After(pastes[j].Created) })
}

session := sessions.Default(c)
val := session.Get("view_counted")
if val == nil {
_, code, err := h.makeAPICall(
"/paste/"+id+"/view",
"PATCH",
nil,
map[int]struct{}{
http.StatusOK: {},
})
if err != nil {
log.Println("handleGetPaste: error talking to API: ", err)
} else if code != http.StatusOK {
log.Println("handleGetPaste: API returned: ", code)
}
session.Set("view_counted", true)
session.Save()
}
// Send HTML
username, _ := c.Get("username")
c.HTML(
Expand Down
2 changes: 1 addition & 1 deletion src/web/templates/paste.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
</svg>
0
{{ .Views }}
</span>
{{if eq .Privacy "private" }}
<span class="badge bg-transparent text-danger fw-light text-uppercase border" title="Private">
Expand Down
2 changes: 1 addition & 1 deletion src/web/templates/view.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ <h6 class="card-subtitle mb-2 text-muted" style="font-size: 90%;">
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
</svg>
0
{{ .Views }}
</span>
{{if eq .Privacy "private" }}
<span class="badge bg-transparent text-danger fw-light text-uppercase border shadow-sm" title="Private">
Expand Down

0 comments on commit cc5bcf2

Please sign in to comment.