Move pagination to helper function and implement filters+search in contacts

This commit is contained in:
William Bouzourène 2025-01-23 12:04:09 +01:00
parent f2afffd818
commit 5cb7d7ce32
Signed by: bouzoure
SSH key fingerprint: SHA256:19MbXpLua4rUtk8tunMesD8KUKb91LXLHg8E/qTooww
5 changed files with 470 additions and 61 deletions

View file

@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"strconv"
"strings"
"time"
"git.readonly.ch/bouzoure/pop-camarades/helpers"
@ -45,26 +46,374 @@ func Contacts(c *fiber.Ctx) error {
return err
}
filterSection := c.Query("se")
filterArchive := c.Query("a")
filterSearch := c.Query("s")
sqlFilterSections := allowedSections
sqlFilterAppend := "IS NULL"
if filterArchive == "1" {
sqlFilterAppend = "IS NOT NULL"
sqlFilterSections = allowedSectionsArchived
}
var filterSectionUint uint
if len(filterSection) > 0 {
filterSectionUint64, err := strconv.ParseUint(filterSection, 10, 0)
if err == nil {
for _, s := range sqlFilterSections {
if s == uint(filterSectionUint64) {
filterSectionUint = uint(filterSectionUint64)
}
}
}
}
if filterSectionUint > 0 {
sqlFilterAppend = fmt.Sprintf(
"%s AND section_id = %d",
sqlFilterAppend,
filterSectionUint,
)
}
var sqlFilterSearch string
if len(filterSearch) > 0 {
sqlFilterSearch = fmt.Sprintf(
"%%%s%%", filterSearch,
)
}
var count int64
if len(filterSearch) > 0 {
db.Model(&models.Person{}).Where(
fmt.Sprintf(
"is_contact = ? AND section_id IN ? AND (first_name LIKE ? OR last_name LIKE ?) AND deleted_at %s",
sqlFilterAppend,
),
true, allowedSections, sqlFilterSearch, sqlFilterSearch,
).Count(&count)
} else {
db.Model(&models.Person{}).Where(
fmt.Sprintf(
"is_contact = ? AND section_id IN ? AND deleted_at %s",
sqlFilterAppend,
),
true, allowedSections,
).Count(&count)
}
page, _ := strconv.Atoi(c.Query("p"))
pagination := helpers.Paginate(50, int(count), page)
var people []models.Person
result := db.Order(
if len(filterSearch) > 0 {
result := db.Unscoped().Offset(
pagination.Offset,
).Limit(
pagination.PageSize,
).Order(
"last_name collate nocase asc, first_name collate nocase asc",
).Preload("Section").Find(
&people, "is_contact = ? AND section_id IN ?", true, allowedSections,
).Preload(
"Section",
).Find(
&people,
fmt.Sprintf(
"is_contact = ? AND section_id IN ? AND (first_name LIKE ? OR last_name LIKE ?) AND deleted_at %s",
sqlFilterAppend,
),
true, sqlFilterSections, sqlFilterSearch, sqlFilterSearch,
)
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
return err
}
} else {
result := db.Unscoped().Offset(
pagination.Offset,
).Limit(
pagination.PageSize,
).Order(
"last_name collate nocase asc, first_name collate nocase asc",
).Preload(
"Section",
).Find(
&people,
fmt.Sprintf(
"is_contact = ? AND section_id IN ? AND deleted_at %s",
sqlFilterAppend,
),
true, sqlFilterSections,
)
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
return err
}
}
var sections []models.Section
db.Order(
"name collate nocase asc",
).Find(
&sections, sqlFilterSections,
)
return c.Render("people", fiber.Map{
"PageTitle": "Contacts",
"MembersPage": false,
"People": people,
"Pagination": pagination,
"PermShow": permShow,
"PermShowArchived": permShowArchived,
"Sections": sections,
"FilterArchive": filterArchive,
"FilterSection": filterSectionUint,
"FilterSearch": filterSearch,
})
}
func ContactsExport(c *fiber.Ctx) error {
userid, err := helpers.GetSessionUserId(c)
if err != nil {
return err
}
allowedSections, err := helpers.PermissionsGetSections(
userid, "show_contact",
)
if err != nil {
return err
}
allowedSectionsArchived, err := helpers.PermissionsGetSections(
userid, "show_archived_contact",
)
if err != nil {
return err
}
permShow := (len(allowedSections) > 0)
permShowArchived := (len(allowedSectionsArchived) > 0)
if !permShow && !permShowArchived {
return fiber.NewError(fiber.StatusForbidden, "Forbidden")
}
db, err := helpers.GetDatabase()
if err != nil {
return err
}
filterSection := c.Query("se")
filterArchive := c.Query("a")
filterSearch := c.Query("s")
sqlFilterSections := allowedSections
sqlFilterAppend := "IS NULL"
if filterArchive == "1" {
sqlFilterAppend = "IS NOT NULL"
sqlFilterSections = allowedSectionsArchived
}
var filterSectionUint uint
if len(filterSection) > 0 {
filterSectionUint64, err := strconv.ParseUint(filterSection, 10, 0)
if err == nil {
for _, s := range sqlFilterSections {
if s == uint(filterSectionUint64) {
filterSectionUint = uint(filterSectionUint64)
}
}
}
}
if filterSectionUint > 0 {
sqlFilterAppend = fmt.Sprintf(
"%s AND section_id = %d",
sqlFilterAppend,
filterSectionUint,
)
}
var sqlFilterSearch string
if len(filterSearch) > 0 {
sqlFilterSearch = fmt.Sprintf(
"%%%s%%", filterSearch,
)
}
var people []models.Person
if len(filterSearch) > 0 {
result := db.Unscoped().Order(
"last_name collate nocase asc, first_name collate nocase asc",
).Preload("Section").Find(
&people,
fmt.Sprintf(
"is_contact = ? AND section_id IN ? AND (first_name LIKE ? OR last_name LIKE ?) AND deleted_at %s",
sqlFilterAppend,
),
true, sqlFilterSections, sqlFilterSearch, sqlFilterSearch,
)
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
return err
}
} else {
result := db.Unscoped().Order(
"last_name collate nocase asc, first_name collate nocase asc",
).Preload("Section").Find(
&people,
fmt.Sprintf(
"is_contact = ? AND section_id IN ? AND deleted_at %s",
sqlFilterAppend,
),
true, sqlFilterSections,
)
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
return err
}
}
var fields []models.Field
db.Order("name collate nocase asc").Preload(
"List",
).Find(
&fields, "person_type = ?", "contact",
)
var fieldValues []models.FieldValue
db.Preload("ListItem").Find(&fieldValues)
c.Set("Content-Type", "text/csv")
c.Set("Content-Disposition", fmt.Sprintf(
"attachment;filename=contacts_%s.csv",
time.Now().Format(time.RFC3339),
))
csvFields := []string{
"first_name",
"last_name",
"email",
"phone",
"mobile",
"address1",
"address2",
"postal_code",
"city",
"section_id",
"section_name",
}
for _, field := range fields {
csvFields = append(csvFields, field.Name)
}
for key, csvField := range csvFields {
csvField = strings.ReplaceAll(csvField, "\r", "")
csvField = strings.ReplaceAll(csvField, "\n", " ")
csvField = strings.ReplaceAll(csvField, ",", "_")
csvField = strings.ReplaceAll(csvField, ";", "_")
if key+1 == len(csvFields) {
c.Writef("\"%s\"\n", csvField)
} else {
c.Writef("\"%s\";", csvField)
}
}
for _, person := range people {
c.Writef("\"%s\";", person.FirstName)
c.Writef("\"%s\";", person.LastName)
c.Writef("\"%s\";", person.Email)
c.Writef("\"%s\";", person.Phone)
c.Writef("\"%s\";", person.Mobile)
c.Writef("\"%s\";", person.Address1)
c.Writef("\"%s\";", person.Address2)
c.Writef("\"%s\";", person.PostalCode)
c.Writef("\"%s\";", person.City)
c.Writef("\"%d\";", person.SectionID)
if len(fields) > 0 {
c.Writef("\"%s\";", person.Section.Name)
} else {
c.Writef("\"%s\"\n", person.Section.Name)
}
for key, field := range fields {
endLine := ";"
if key+1 == len(fields) {
endLine = "\n"
}
countMulti := 0
found := false
for _, value := range fieldValues {
if value.FieldID == field.ID && value.PersonID == person.ID {
found = true
if field.FieldType == "text" || field.FieldType == "longtext" {
text := value.ValueString.String
text = strings.ReplaceAll(text, "\r", "")
text = strings.ReplaceAll(text, "\n", " ")
text = strings.ReplaceAll(text, ",", "_")
text = strings.ReplaceAll(text, ";", "_")
c.Writef("\"%s\"%s", text, endLine)
} else if field.FieldType == "number" {
if value.ValueInt.Valid {
c.Writef("\"%d\"%s", value.ValueInt.Int64, endLine)
} else {
c.Writef("\"\"%s", endLine)
}
} else if field.FieldType == "date" {
if value.ValueDate.Valid {
date := value.ValueDate.Time.Format("2006-01-02")
c.Writef("\"%s\"%s", date, endLine)
} else {
c.Writef("\"\"%s", endLine)
}
} else if field.FieldType == "list" {
if field.List.Multi {
if countMulti == 0 {
c.Writef("\"")
} else {
c.Writef(",")
}
c.Writef("%s", value.ListItem.Value)
countMulti++
} else {
c.Writef("\"%s\"%s", value.ListItem.Value, endLine)
}
}
}
}
if countMulti > 0 {
c.Writef("\"%s", endLine)
}
if !found {
c.Writef("\"\"%s", endLine)
}
}
}
return nil
}
func ContactShow(c *fiber.Ctx) error {
id := c.Params("id")
@ -78,14 +427,14 @@ func ContactShow(c *fiber.Ctx) error {
&person, "id = ? AND is_contact", id, true,
)
if result.RowsAffected < 1 {
return fiber.NewError(fiber.StatusNotFound, "Not found")
}
if result.Error != nil {
return result.Error
}
if result.RowsAffected < 1 {
return fiber.NewError(fiber.StatusNotFound, "Not found")
}
userid, err := helpers.GetSessionUserId(c)
if err != nil {
return err

View file

@ -116,45 +116,20 @@ func Members(c *fiber.Ctx) error {
).Count(&count)
}
pageQuery := c.Query("p")
page, _ := strconv.Atoi(pageQuery)
if page <= 0 {
page = 1
}
pageSize := 50
maxPages := 1
if count > int64(pageSize) {
maxPages = int(count) / pageSize
}
if page > maxPages {
page = 1
}
offset := (page - 1) * pageSize
var pages []int
for i := 1; i <= maxPages; i++ {
if i == page {
pages = append(pages, i)
} else if i >= (page-2) && i < page {
pages = append(pages, i)
} else if i <= (page+2) && i > page {
pages = append(pages, i)
} else if page <= 2 && i <= 5 {
pages = append(pages, i)
} else if page >= (maxPages-2) && i >= (maxPages-5) {
pages = append(pages, i)
}
}
page, _ := strconv.Atoi(c.Query("p"))
pagination := helpers.Paginate(50, int(count), page)
var people []models.Person
if len(filterSearch) > 0 {
result := db.Unscoped().Offset(offset).Limit(pageSize).Order(
result := db.Unscoped().Offset(
pagination.Offset,
).Limit(
pagination.PageSize,
).Order(
"last_name collate nocase asc, first_name collate nocase asc",
).Preload("Section").Find(
).Preload(
"Section",
).Find(
&people,
fmt.Sprintf(
"is_member = ? AND section_id IN ? AND (first_name LIKE ? OR last_name LIKE ?) AND deleted_at %s",
@ -167,9 +142,15 @@ func Members(c *fiber.Ctx) error {
return err
}
} else {
result := db.Unscoped().Offset(offset).Limit(pageSize).Order(
result := db.Unscoped().Offset(
pagination.Offset,
).Limit(
pagination.PageSize,
).Order(
"last_name collate nocase asc, first_name collate nocase asc",
).Preload("Section").Find(
).Preload(
"Section",
).Find(
&people,
fmt.Sprintf(
"is_member = ? AND section_id IN ? AND deleted_at %s",
@ -194,9 +175,7 @@ func Members(c *fiber.Ctx) error {
"PageTitle": "Membres",
"MembersPage": true,
"People": people,
"Page": page,
"Pages": pages,
"MaxPages": maxPages,
"Pagination": pagination,
"PermShow": permShow,
"PermShowArchived": permShowArchived,
"Sections": sections,

68
helpers/pagination.go Normal file
View file

@ -0,0 +1,68 @@
package helpers
type Pagination struct {
PageSize int
MaxPages int
CurrentPage int
Offset int
Count int
Pages []int
}
func Paginate(size int, count int, page int) Pagination {
if size < 1 {
size = 1
}
if page < 1 {
page = 1
}
pagination := Pagination{
PageSize: size,
CurrentPage: page,
Count: count,
MaxPages: 1,
}
if pagination.Count > pagination.PageSize {
pagination.MaxPages = pagination.Count / pagination.PageSize
}
if pagination.CurrentPage > pagination.MaxPages {
pagination.CurrentPage = 1
}
pagination.Offset = (pagination.CurrentPage - 1) / pagination.PageSize
for i := 1; i <= pagination.MaxPages; i++ {
if i == pagination.CurrentPage {
pagination.Pages = append(pagination.Pages, i)
continue
}
if i >= (pagination.CurrentPage-2) && i < pagination.CurrentPage {
pagination.Pages = append(pagination.Pages, i)
continue
}
if i <= (pagination.CurrentPage+2) && i > pagination.CurrentPage {
pagination.Pages = append(pagination.Pages, i)
continue
}
if pagination.CurrentPage <= 2 && i <= 5 {
pagination.Pages = append(pagination.Pages, i)
continue
}
if pagination.CurrentPage >= (pagination.MaxPages-2) && i >= (pagination.MaxPages-5) {
pagination.Pages = append(pagination.Pages, i)
continue
}
}
return pagination
}

View file

@ -136,6 +136,7 @@ func main() {
// Contacts
app.Get("/contacts", controllers.Contacts)
app.Get("/contacts/export", controllers.ContactsExport)
app.Get("/contacts/:id<int;min(0)>", controllers.ContactShow)
app.Get("/contacts/add", controllers.ContactAdd)
app.Post("/contacts/add", controllers.ContactAdd)

View file

@ -45,7 +45,7 @@
</div>
<form id="filters" method="get" class="my-3">
<input type="hidden" name="p" value="{{ Page }}">
<input type="hidden" name="p" value="{{ Pagination.CurrentPage }}">
<div class="row">
<div class="col-6 col-lg-3 mb-2">
@ -181,7 +181,7 @@
<nav class="mt-3 mb-5">
<ul class="pagination justify-content-center">
{% if Page <= 1 %}
{% if Pagination.CurrentPage <= 1 %}
<li class="page-item disabled">
<a class="page-link" href="javascript:;">
<i class="bi-rewind"></i>
@ -194,21 +194,27 @@
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?se={{ FilterSection }}&s={{ FilterSearch|urlencode }}&a={{ FilterArchive }}&p=1">
<a
class="page-link"
href="?se={{ FilterSection }}&s={{ FilterSearch|urlencode }}&a={{ FilterArchive }}&p=1"
>
<i class="bi-rewind"></i>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?se={{ FilterSection }}&s={{ FilterSearch|urlencode }}&a={{ FilterArchive }}&p={{ Page - 1 }}">
<a
class="page-link"
href="?se={{ FilterSection }}&s={{ FilterSearch|urlencode }}&a={{ FilterArchive }}&p={{ Pagination.CurrentPage - 1 }}"
>
<i class="bi-caret-left"></i>
</a>
</li>
{% endif %}
{% for i in Pages %}
{% for i in Pagination.Pages %}
<li class="page-item">
<a
{% if i == Page %}
{% if i == Pagination.CurrentPage %}
class="page-link active"
{% else %}
class="page-link"
@ -221,7 +227,7 @@
</li>
{% endfor %}
{% if Page >= MaxPages %}
{% if Pagination.CurrentPage >= Pagination.MaxPages %}
<li class="page-item disabled">
<a class="page-link" href="javascript:;">
<i class="bi-caret-right"></i>
@ -234,12 +240,18 @@
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?se={{ FilterSection }}&s={{ FilterSearch|urlencode }}&a={{ FilterArchive }}&p={{ Page + 1 }}">
<a
class="page-link"
href="?se={{ FilterSection }}&s={{ FilterSearch|urlencode }}&a={{ FilterArchive }}&p={{ Pagination.CurrentPage + 1 }}"
>
<i class="bi-caret-right"></i>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?se={{ FilterSection }}&s={{ FilterSearch|urlencode }}&a={{ FilterArchive }}&p={{ MaxPages }}">
<a
class="page-link"
href="?se={{ FilterSection }}&s={{ FilterSearch|urlencode }}&a={{ FilterArchive }}&p={{ Pagination.MaxPages }}"
>
<i class="bi-fast-forward"></i>
</a>
</li>