Skip to content
This repository has been archived by the owner on Dec 27, 2022. It is now read-only.

[API #4] GetListingUsingFilters #70

Merged
merged 7 commits into from
Oct 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions GoServer/constant/listings.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package constant

const (
LISTING_CONSTANT_TYPE_ITEM_STATUS = 0
LISTING_CONSTANT_TYPE_ITEM_CATEGORY = 1
LISTING_CONSTANT_TYPE_SHIPPING_TYPE = 2
LISTING_CONSTANT_TYPE_PAYMENT_TYPE = 3

ITEM_STATUS_ALL = 0
ITEM_STATUS_NORMAL = 1
ITEM_STATUS_SOLDOUT = 2
Expand All @@ -19,3 +24,39 @@ const (
PAYMENR_TYPE_INSTALMENT = 3
PAYMENT_TYPE_CASH = 4
)

func CheckListingConstant(field int, param uint32) bool {

ItemStatusList := [4]int{0, 1, 2, 3}
ItemCategoryList := [2]int{0, 1}
ShippingTypeList := [3]int{0, 1, 2}
PaymentTypeList := [5]int{0, 1, 2, 3, 4}

switch field {
case 1:
for _, elem := range ItemStatusList {
if param == uint32(elem) {
return true
}
}
case 2:
for _, elem := range ItemCategoryList {
if param == uint32(elem) {
return true
}
}
case 3:
for _, elem := range ShippingTypeList {
if param == uint32(elem) {
return true
}
}
case 4:
for _, elem := range PaymentTypeList {
if param == uint32(elem) {
return true
}
}
}
return false
}
137 changes: 137 additions & 0 deletions GoServer/controllers/listings/get_listings_using_filters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package listings

import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"

"github.com/aaronangxz/TIC2601/constant"
"github.com/aaronangxz/TIC2601/models"
"github.com/aaronangxz/TIC2601/utils"
"github.com/gin-gonic/gin"
)

func ValidateGetListingsUsingFiltersRequest(c *gin.Context, input *models.GetListingsUsingFiltersRequest) error {
//allow nil and empty
if err := c.ShouldBindJSON(&input); err != nil {

if input.CategoryFilter.ItemCategory != nil && !utils.ValidateUint(input.CategoryFilter.ItemCategory) {
c.JSON(http.StatusBadRequest, gin.H{"Respmeta": models.NewParamErrorsResponse("item_category must be uint type.")})
errormsg := fmt.Sprintf("item_category must be uint. input: %v", input.CategoryFilter.GetItemCategory())
return errors.New(errormsg)
}

if input.LocationFilter.Location != nil && !utils.ValidateString(input.LocationFilter.Location) {
c.JSON(http.StatusBadRequest, gin.H{"Respmeta": models.NewParamErrorsResponse("location must be string type.")})
errormsg := fmt.Sprintf("location must be string. input: %v", input.LocationFilter.GetLocation())
return errors.New(errormsg)
}

if input.PriceFilter.MinPrice != nil && !utils.ValidateUint(input.PriceFilter.MinPrice) {
c.JSON(http.StatusBadRequest, gin.H{"Respmeta": models.NewParamErrorsResponse("min_price must be uint type.")})
errormsg := fmt.Sprintf("min_price must be uint. input: %v", input.PriceFilter.GetMinPrice())
return errors.New(errormsg)
}

if input.PriceFilter.MaxPrice != nil && !utils.ValidateUint(input.PriceFilter.MaxPrice) {
c.JSON(http.StatusBadRequest, gin.H{"Respmeta": models.NewParamErrorsResponse("max_price must be uint type.")})
errormsg := fmt.Sprintf("max_price must be uint. input: %v", input.PriceFilter.GetMaxPrice())
return errors.New(errormsg)
}

c.JSON(http.StatusBadRequest, gin.H{"Respmeta": models.NewJSONErrorResponse(err)})
errormsg := fmt.Sprint("JSON error: &v", err.Error())
return errors.New(errormsg)
}
return nil
}

func ValidateGetListingsUsingFiltersInput(c *gin.Context, input *models.GetListingsUsingFiltersRequest) error {
//check if exists
if input.CategoryFilter.ItemCategory != nil && !constant.CheckListingConstant(constant.LISTING_CONSTANT_TYPE_ITEM_CATEGORY, input.GetItemCategory()) {
c.JSON(http.StatusBadRequest, gin.H{"Respmeta": models.NewParamErrorsResponse("unknown item_category.")})
errormsg := fmt.Sprintf("unknown item_category. input: %v", input.CategoryFilter.GetItemCategory())
return errors.New(errormsg)
}

if input.PriceFilter.MinPrice != nil && input.PriceFilter.MaxPrice != nil {
if input.PriceFilter.GetMaxPrice() < input.PriceFilter.GetMinPrice() {
c.JSON(http.StatusBadRequest, gin.H{"Respmeta": models.NewParamErrorsResponse("min_price cannot > max_price.")})
errormsg := fmt.Sprintf("min_price cannot > max_price. input: min_price: %v, max_price: %v", input.PriceFilter.GetMinPrice(), input.PriceFilter.GetMaxPrice())
return errors.New(errormsg)
}
}
return nil
}

func GetListingsUsingFilters(c *gin.Context) {
var (
listings []models.Listing
input models.GetListingsUsingFiltersRequest
categoryCondition = ""
locationCondition = ""
priceCondition = ""
)

if err := ValidateGetListingsUsingFiltersRequest(c, &input); err != nil {
log.Printf("Error during ValidateGetListingsUsingFiltersRequest: %v", err.Error())
return
}

if err := ValidateGetListingsUsingFiltersInput(c, &input); err != nil {
log.Printf("Error during ValidateGetListingsUsingFiltersInput: %v", err.Error())
return
}

//build SQL queries
//category filter
if input.CategoryFilter.ItemCategory != nil {
//else concat into query
categoryCondition += " WHERE item_category = " + fmt.Sprint(input.CategoryFilter.GetItemCategory())
}

if input.LocationFilter.Location != nil {
locationCondition += " item_location = '" + fmt.Sprint(input.LocationFilter.GetLocation()) + "'"
}

if categoryCondition != "" && locationCondition != "" {
categoryCondition += " AND"
} else if categoryCondition == "" {
categoryCondition = " WHERE"
}

if input.PriceFilter.MinPrice != nil && input.PriceFilter.MaxPrice == nil {
priceCondition += " item_price >= " + fmt.Sprint(input.PriceFilter.GetMinPrice())
} else if input.PriceFilter.MinPrice == nil && input.PriceFilter.MaxPrice != nil {
priceCondition += " item_price <= " + fmt.Sprint(input.PriceFilter.GetMaxPrice())
} else if input.PriceFilter.MinPrice != nil && input.PriceFilter.MaxPrice != nil {
priceCondition += " item_price >= " + fmt.Sprint(input.PriceFilter.GetMinPrice()) + " AND item_price <= " + fmt.Sprint(input.PriceFilter.GetMaxPrice())
}

if categoryCondition == " WHERE" && locationCondition == "" && priceCondition == "" {
categoryCondition = ""
}

if priceCondition != "" && locationCondition != "" {
locationCondition += " AND"
}

orderCondition := " ORDER BY listing_ctime DESC"
query := "SELECT * FROM listing_tab" + categoryCondition + locationCondition + priceCondition + orderCondition
log.Printf("Executing DB query: %v", query)

if err := models.DB.Raw(query).Scan(&listings).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"Respmeta": models.NewDBErrorResponse(err)})
log.Printf("Error during DB query: %v", err.Error())
return
}

c.JSON(http.StatusOK, gin.H{"Respmeta": utils.ValidateGetListingsUsingFiltersResult(listings), "Data": listings})
data, err := json.Marshal(listings)
if err != nil {
log.Printf("Failed to marshal JSON results: %v", err.Error())
}
log.Printf("Successful: GetListingsUsingFilters. Returned: %s", data)
}
1 change: 1 addition & 0 deletions GoServer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ func main() {
r.DELETE("/delete_single_listing", listings.DeleteListing)
r.GET("/get_user_listings", listings.GetUserListings)
r.GET("/get_latest_listings", listings.GetLatestListings)
r.GET("/get_listings_using_filters", listings.GetListingsUsingFilters)

r.Run()
}
35 changes: 35 additions & 0 deletions GoServer/models/listings.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,38 @@ func (r GetLatestListingsRequest) GetItemStatus() uint32 {
func (r GetLatestListingsRequest) GetLimit() uint32 {
return *r.Limit
}

type CategoryFilter struct {
ItemCategory *uint32 `json:"item_category"`
}

type LocationFilter struct {
Location *string `json:"location"`
}

type PriceFilter struct {
MinPrice *uint32 `json:"min_price"`
MaxPrice *uint32 `json:"max_price"`
}

type GetListingsUsingFiltersRequest struct {
CategoryFilter `json:"category_filter"`
LocationFilter `json:"location_filter"`
PriceFilter `json:"price_filter"`
}

func (r CategoryFilter) GetItemCategory() uint32 {
return *r.ItemCategory
}

func (r LocationFilter) GetLocation() string {
return *r.Location
}

func (r PriceFilter) GetMinPrice() uint32 {
return *r.MinPrice
}

func (r PriceFilter) GetMaxPrice() uint32 {
return *r.MaxPrice
}
7 changes: 7 additions & 0 deletions GoServer/utils/validate_result.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,10 @@ func ValidateGetLatestListingsResult(results []models.GetLatestListingsResponse)
}
return models.NewSuccessResponse()
}

func ValidateGetListingsUsingFiltersResult(results []models.Listing) models.ResponseMeta {
if len(results) == 0 {
return models.NewNotFoundResponse()
}
return models.NewSuccessResponse()
}
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
| 1.4 | 8f52518e400f | 30/09/2021 | Good | |
| 1.5 | 700032e186ac | 03/10/2021 | FAIL | [#44](https://github.com/aaronangxz/TIC2601/pull/44) |
| 1.6 | d85840c3a753 | 03/10/2021 | Good | [#50](https://github.com/aaronangxz/TIC2601/pull/50) |
| 1.7 | 725c67b4ab0f | 13/10/2021 | Good | [#52](https://github.com/aaronangxz/TIC2601/pull/52) |

<h2>Architecture</h2>

Expand All @@ -35,7 +36,7 @@
<h2>Docker Deployment</h2>

1. Write `Dockerfile`
2. To build: `docker build --tag tic2601 .`
2. To build: `docker build --tag tic2601 .` (single dockerfile) / `docker build -f Dockerfile.server .` (multi dockerfiles)
3. Tag docker image `docker tag <imageid> tic2601:<version>`
4. `docker run tic2601` will run container isolated from network.
5. Use `docker run --publish 8080:8080 tic2601` to expose container to network and port. ([host_port]:[container_port])
Expand Down Expand Up @@ -68,6 +69,10 @@
- Password:`f25c7f6b`
- Default Schema:`heroku_bdc39d4687a85d4`

<h2>Test API endpoints</h2>

Postman Workspace: https://www.postman.com/science-specialist-94927587/workspace/tic2601

<h2>Test Branch</h2>

Refer to https://github.com/aaronangxz/TIC2601/tree/test