Render organization logos by 'slug'
This avoids leaking org IDs in the URL, and makes it possible to have a district-specific root mock that works in both dev and prod.
This commit is contained in:
parent
45868e4bde
commit
f549243c10
10 changed files with 259 additions and 17 deletions
|
|
@ -54,21 +54,29 @@ func apiGetDistrict(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func apiGetDistrictLogo(w http.ResponseWriter, r *http.Request) {
|
||||
id_str := chi.URLParam(r, "id")
|
||||
org_id, err := strconv.ParseInt(id_str, 10, 32)
|
||||
slug := chi.URLParam(r, "slug")
|
||||
ctx := r.Context()
|
||||
rows, err := models.Organizations.Query(
|
||||
models.SelectWhere.Organizations.Slug.EQ(slug),
|
||||
).All(ctx, db.PGInstance.BobDB)
|
||||
if err != nil {
|
||||
render.Render(w, r, errRender(fmt.Errorf("%s is not a recognized organization ID: %w", id_str, err)))
|
||||
http.Error(w, "Failed to query", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
ctx := r.Context()
|
||||
org, err := models.FindOrganization(ctx, db.PGInstance.BobDB, int32(org_id))
|
||||
if err != nil {
|
||||
switch len(rows) {
|
||||
case 0:
|
||||
http.Error(w, "Organization not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if org.LogoUUID.IsNull() {
|
||||
http.Error(w, "Logo not found", http.StatusNotFound)
|
||||
case 1:
|
||||
org := rows[0]
|
||||
if org.LogoUUID.IsNull() {
|
||||
http.Error(w, "Logo not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
userfile.ImageFileContentWriteLogo(w, org.LogoUUID.MustGet())
|
||||
return
|
||||
default:
|
||||
http.Error(w, "Too many organizations, this is a programmer error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
userfile.ImageFileContentWriteLogo(w, org.LogoUUID.MustGet())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ func AddRoutes(r chi.Router) {
|
|||
|
||||
// Unauthenticated endpoints
|
||||
r.Get("/district", apiGetDistrict)
|
||||
r.Get("/district/{id}/logo", apiGetDistrictLogo)
|
||||
r.Get("/district/{slug}/logo", apiGetDistrictLogo)
|
||||
r.Post("/twilio/message", twilioMessagePost)
|
||||
r.Post("/twilio/status", twilioStatusPost)
|
||||
r.Post("/twilio/text", twilioTextPost)
|
||||
|
|
|
|||
|
|
@ -87,6 +87,15 @@ var Organizations = Table[
|
|||
Generated: false,
|
||||
AutoIncr: false,
|
||||
},
|
||||
Slug: column{
|
||||
Name: "slug",
|
||||
DBType: "character varying",
|
||||
Default: "NULL",
|
||||
Comment: "",
|
||||
Nullable: true,
|
||||
Generated: false,
|
||||
AutoIncr: false,
|
||||
},
|
||||
},
|
||||
Indexes: organizationIndexes{
|
||||
OrganizationPkey: index{
|
||||
|
|
@ -182,11 +191,12 @@ type organizationColumns struct {
|
|||
ImportDistrictGid column
|
||||
Website column
|
||||
LogoUUID column
|
||||
Slug column
|
||||
}
|
||||
|
||||
func (c organizationColumns) AsSlice() []column {
|
||||
return []column{
|
||||
c.ID, c.Name, c.ArcgisID, c.ArcgisName, c.FieldseekerURL, c.ImportDistrictGid, c.Website, c.LogoUUID,
|
||||
c.ID, c.Name, c.ArcgisID, c.ArcgisName, c.FieldseekerURL, c.ImportDistrictGid, c.Website, c.LogoUUID, c.Slug,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2594,6 +2594,7 @@ func (f *Factory) FromExistingOrganization(m *models.Organization) *Organization
|
|||
o.ImportDistrictGid = func() null.Val[int32] { return m.ImportDistrictGid }
|
||||
o.Website = func() null.Val[string] { return m.Website }
|
||||
o.LogoUUID = func() null.Val[uuid.UUID] { return m.LogoUUID }
|
||||
o.Slug = func() null.Val[string] { return m.Slug }
|
||||
|
||||
ctx := context.Background()
|
||||
if len(m.R.Containerrelates) > 0 {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ type OrganizationTemplate struct {
|
|||
ImportDistrictGid func() null.Val[int32]
|
||||
Website func() null.Val[string]
|
||||
LogoUUID func() null.Val[uuid.UUID]
|
||||
Slug func() null.Val[string]
|
||||
|
||||
r organizationR
|
||||
f *Factory
|
||||
|
|
@ -745,6 +746,10 @@ func (o OrganizationTemplate) BuildSetter() *models.OrganizationSetter {
|
|||
val := o.LogoUUID()
|
||||
m.LogoUUID = omitnull.FromNull(val)
|
||||
}
|
||||
if o.Slug != nil {
|
||||
val := o.Slug()
|
||||
m.Slug = omitnull.FromNull(val)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
|
@ -791,6 +796,9 @@ func (o OrganizationTemplate) Build() *models.Organization {
|
|||
if o.LogoUUID != nil {
|
||||
m.LogoUUID = o.LogoUUID()
|
||||
}
|
||||
if o.Slug != nil {
|
||||
m.Slug = o.Slug()
|
||||
}
|
||||
|
||||
o.setModelRels(m)
|
||||
|
||||
|
|
@ -1642,6 +1650,7 @@ func (m organizationMods) RandomizeAllColumns(f *faker.Faker) OrganizationMod {
|
|||
OrganizationMods.RandomImportDistrictGid(f),
|
||||
OrganizationMods.RandomWebsite(f),
|
||||
OrganizationMods.RandomLogoUUID(f),
|
||||
OrganizationMods.RandomSlug(f),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2025,6 +2034,59 @@ func (m organizationMods) RandomLogoUUIDNotNull(f *faker.Faker) OrganizationMod
|
|||
})
|
||||
}
|
||||
|
||||
// Set the model columns to this value
|
||||
func (m organizationMods) Slug(val null.Val[string]) OrganizationMod {
|
||||
return OrganizationModFunc(func(_ context.Context, o *OrganizationTemplate) {
|
||||
o.Slug = func() null.Val[string] { return val }
|
||||
})
|
||||
}
|
||||
|
||||
// Set the Column from the function
|
||||
func (m organizationMods) SlugFunc(f func() null.Val[string]) OrganizationMod {
|
||||
return OrganizationModFunc(func(_ context.Context, o *OrganizationTemplate) {
|
||||
o.Slug = f
|
||||
})
|
||||
}
|
||||
|
||||
// Clear any values for the column
|
||||
func (m organizationMods) UnsetSlug() OrganizationMod {
|
||||
return OrganizationModFunc(func(_ context.Context, o *OrganizationTemplate) {
|
||||
o.Slug = nil
|
||||
})
|
||||
}
|
||||
|
||||
// Generates a random value for the column using the given faker
|
||||
// if faker is nil, a default faker is used
|
||||
// The generated value is sometimes null
|
||||
func (m organizationMods) RandomSlug(f *faker.Faker) OrganizationMod {
|
||||
return OrganizationModFunc(func(_ context.Context, o *OrganizationTemplate) {
|
||||
o.Slug = func() null.Val[string] {
|
||||
if f == nil {
|
||||
f = &defaultFaker
|
||||
}
|
||||
|
||||
val := random_string(f, "24")
|
||||
return null.From(val)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Generates a random value for the column using the given faker
|
||||
// if faker is nil, a default faker is used
|
||||
// The generated value is never null
|
||||
func (m organizationMods) RandomSlugNotNull(f *faker.Faker) OrganizationMod {
|
||||
return OrganizationModFunc(func(_ context.Context, o *OrganizationTemplate) {
|
||||
o.Slug = func() null.Val[string] {
|
||||
if f == nil {
|
||||
f = &defaultFaker
|
||||
}
|
||||
|
||||
val := random_string(f, "24")
|
||||
return null.From(val)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (m organizationMods) WithParentsCascading() OrganizationMod {
|
||||
return OrganizationModFunc(func(ctx context.Context, o *OrganizationTemplate) {
|
||||
if isDone, _ := organizationWithParentsCascadingCtx.Value(ctx); isDone {
|
||||
|
|
|
|||
4
db/migrations/00040_organization_slug.sql
Normal file
4
db/migrations/00040_organization_slug.sql
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
-- +goose Up
|
||||
ALTER TABLE organization ADD COLUMN slug VARCHAR(24) UNIQUE;
|
||||
-- +goose Down
|
||||
ALTER TABLE organization DROP COLUMN slug;
|
||||
|
|
@ -34,6 +34,7 @@ type Organization struct {
|
|||
ImportDistrictGid null.Val[int32] `db:"import_district_gid" `
|
||||
Website null.Val[string] `db:"website" `
|
||||
LogoUUID null.Val[uuid.UUID] `db:"logo_uuid" `
|
||||
Slug null.Val[string] `db:"slug" `
|
||||
|
||||
R organizationR `db:"-" `
|
||||
|
||||
|
|
@ -93,7 +94,7 @@ type organizationR struct {
|
|||
func buildOrganizationColumns(alias string) organizationColumns {
|
||||
return organizationColumns{
|
||||
ColumnsExpr: expr.NewColumnsExpr(
|
||||
"id", "name", "arcgis_id", "arcgis_name", "fieldseeker_url", "import_district_gid", "website", "logo_uuid",
|
||||
"id", "name", "arcgis_id", "arcgis_name", "fieldseeker_url", "import_district_gid", "website", "logo_uuid", "slug",
|
||||
).WithParent("organization"),
|
||||
tableAlias: alias,
|
||||
ID: psql.Quote(alias, "id"),
|
||||
|
|
@ -104,6 +105,7 @@ func buildOrganizationColumns(alias string) organizationColumns {
|
|||
ImportDistrictGid: psql.Quote(alias, "import_district_gid"),
|
||||
Website: psql.Quote(alias, "website"),
|
||||
LogoUUID: psql.Quote(alias, "logo_uuid"),
|
||||
Slug: psql.Quote(alias, "slug"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -118,6 +120,7 @@ type organizationColumns struct {
|
|||
ImportDistrictGid psql.Expression
|
||||
Website psql.Expression
|
||||
LogoUUID psql.Expression
|
||||
Slug psql.Expression
|
||||
}
|
||||
|
||||
func (c organizationColumns) Alias() string {
|
||||
|
|
@ -140,10 +143,11 @@ type OrganizationSetter struct {
|
|||
ImportDistrictGid omitnull.Val[int32] `db:"import_district_gid" `
|
||||
Website omitnull.Val[string] `db:"website" `
|
||||
LogoUUID omitnull.Val[uuid.UUID] `db:"logo_uuid" `
|
||||
Slug omitnull.Val[string] `db:"slug" `
|
||||
}
|
||||
|
||||
func (s OrganizationSetter) SetColumns() []string {
|
||||
vals := make([]string, 0, 8)
|
||||
vals := make([]string, 0, 9)
|
||||
if s.ID.IsValue() {
|
||||
vals = append(vals, "id")
|
||||
}
|
||||
|
|
@ -168,6 +172,9 @@ func (s OrganizationSetter) SetColumns() []string {
|
|||
if !s.LogoUUID.IsUnset() {
|
||||
vals = append(vals, "logo_uuid")
|
||||
}
|
||||
if !s.Slug.IsUnset() {
|
||||
vals = append(vals, "slug")
|
||||
}
|
||||
return vals
|
||||
}
|
||||
|
||||
|
|
@ -196,6 +203,9 @@ func (s OrganizationSetter) Overwrite(t *Organization) {
|
|||
if !s.LogoUUID.IsUnset() {
|
||||
t.LogoUUID = s.LogoUUID.MustGetNull()
|
||||
}
|
||||
if !s.Slug.IsUnset() {
|
||||
t.Slug = s.Slug.MustGetNull()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *OrganizationSetter) Apply(q *dialect.InsertQuery) {
|
||||
|
|
@ -204,7 +214,7 @@ func (s *OrganizationSetter) Apply(q *dialect.InsertQuery) {
|
|||
})
|
||||
|
||||
q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) {
|
||||
vals := make([]bob.Expression, 8)
|
||||
vals := make([]bob.Expression, 9)
|
||||
if s.ID.IsValue() {
|
||||
vals[0] = psql.Arg(s.ID.MustGet())
|
||||
} else {
|
||||
|
|
@ -253,6 +263,12 @@ func (s *OrganizationSetter) Apply(q *dialect.InsertQuery) {
|
|||
vals[7] = psql.Raw("DEFAULT")
|
||||
}
|
||||
|
||||
if !s.Slug.IsUnset() {
|
||||
vals[8] = psql.Arg(s.Slug.MustGetNull())
|
||||
} else {
|
||||
vals[8] = psql.Raw("DEFAULT")
|
||||
}
|
||||
|
||||
return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "")
|
||||
}))
|
||||
}
|
||||
|
|
@ -262,7 +278,7 @@ func (s OrganizationSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] {
|
|||
}
|
||||
|
||||
func (s OrganizationSetter) Expressions(prefix ...string) []bob.Expression {
|
||||
exprs := make([]bob.Expression, 0, 8)
|
||||
exprs := make([]bob.Expression, 0, 9)
|
||||
|
||||
if s.ID.IsValue() {
|
||||
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
|
||||
|
|
@ -320,6 +336,13 @@ func (s OrganizationSetter) Expressions(prefix ...string) []bob.Expression {
|
|||
}})
|
||||
}
|
||||
|
||||
if !s.Slug.IsUnset() {
|
||||
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
|
||||
psql.Quote(append(prefix, "slug")...),
|
||||
psql.Arg(s.Slug),
|
||||
}})
|
||||
}
|
||||
|
||||
return exprs
|
||||
}
|
||||
|
||||
|
|
@ -3847,6 +3870,7 @@ type organizationWhere[Q psql.Filterable] struct {
|
|||
ImportDistrictGid psql.WhereNullMod[Q, int32]
|
||||
Website psql.WhereNullMod[Q, string]
|
||||
LogoUUID psql.WhereNullMod[Q, uuid.UUID]
|
||||
Slug psql.WhereNullMod[Q, string]
|
||||
}
|
||||
|
||||
func (organizationWhere[Q]) AliasedAs(alias string) organizationWhere[Q] {
|
||||
|
|
@ -3863,6 +3887,7 @@ func buildOrganizationWhere[Q psql.Filterable](cols organizationColumns) organiz
|
|||
ImportDistrictGid: psql.WhereNull[Q, int32](cols.ImportDistrictGid),
|
||||
Website: psql.WhereNull[Q, string](cols.Website),
|
||||
LogoUUID: psql.WhereNull[Q, uuid.UUID](cols.LogoUUID),
|
||||
Slug: psql.WhereNull[Q, string](cols.Slug),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
43
public-report/mock.go
Normal file
43
public-report/mock.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package publicreport
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/Gleipnir-Technology/nidus-sync/config"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/htmlpage"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
var (
|
||||
mockRootT = buildTemplate("mock/root", "base")
|
||||
mockDistrictRootT = buildTemplate("mock/district-root", "base")
|
||||
)
|
||||
|
||||
type ContentDistrict struct {
|
||||
LogoURL string
|
||||
Name string
|
||||
}
|
||||
type ContentMock struct {
|
||||
District ContentDistrict
|
||||
}
|
||||
|
||||
func addMockRoutes(r chi.Router) {
|
||||
r.Get("/", renderMock(mockRootT))
|
||||
r.Get("/district/{slug}", renderMock(mockDistrictRootT))
|
||||
}
|
||||
|
||||
func renderMock(t *htmlpage.BuiltTemplate) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
slug := chi.URLParam(r, "slug")
|
||||
htmlpage.RenderOrError(
|
||||
w,
|
||||
t,
|
||||
ContentMock{
|
||||
District: ContentDistrict{
|
||||
LogoURL: config.MakeURLNidus("/api/district/%s/logo", slug),
|
||||
Name: "Delta MCD",
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -75,7 +75,7 @@ func getQuickSubmitComplete(w http.ResponseWriter, r *http.Request) {
|
|||
log.Debug().Int32("org_id", org.ID).Int32("d_gid", d.Gid).Msg("Getting district")
|
||||
if d != nil {
|
||||
district = &District{
|
||||
LogoURL: config.MakeURLNidus("/api/district/%s/logo", strconv.Itoa(int(org_id))),
|
||||
LogoURL: config.MakeURLNidus("/api/district/%s/logo", org.Slug.GetOr("placeholder")),
|
||||
Name: d.Agency.GetOr("Unknown"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
89
public-report/template/mock/district-root.html
Normal file
89
public-report/template/mock/district-root.html
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
{{template "base.html" .}}
|
||||
|
||||
{{define "title"}}Main{{end}}
|
||||
{{define "extraheader"}}
|
||||
<style>
|
||||
.service-card {
|
||||
transition: transform 0.3s;
|
||||
height: 100%;
|
||||
}
|
||||
.service-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
||||
}
|
||||
.district-logo {
|
||||
max-height: 80px;
|
||||
width: auto;
|
||||
}
|
||||
.quick-report-mobile {
|
||||
background-color: #ff9800;
|
||||
}
|
||||
.quick-report-desktop {
|
||||
background-color: #ffefd5;
|
||||
border-left: 4px solid #ff9800;
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
{{define "content"}}
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
<!-- Introduction Section -->
|
||||
<section class="py-5 bg-primary text-white">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10">
|
||||
<h2 class="text-center mb-4">Report Mosquitoes for {{.District.Name}}</h2>
|
||||
<img src="{{.District.LogoURL}}" width="256"/>
|
||||
<p class="lead text-center">This is the reporting page for mosquito problems in your area.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Services Section -->
|
||||
<section class="py-5">
|
||||
<div class="container">
|
||||
<h3 class="text-center mb-4">How Can We Help You Today?</h3>
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card service-card h-100">
|
||||
<div class="card-body text-center">
|
||||
<div class="mb-3">
|
||||
{{ template "svg/mosquito" }}
|
||||
</div>
|
||||
<h4 class="card-title">Report Mosquito Nuisance</h4>
|
||||
<p class="card-text">Report areas with high adult mosquito activity causing discomfort or concern.</p>
|
||||
<a href="/nuisance" class="btn btn-primary mt-3">Report Problem</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card service-card h-100">
|
||||
<div class="card-body text-center">
|
||||
<div class="mb-3">
|
||||
{{ template "svg/pond" }}
|
||||
</div>
|
||||
<h4 class="card-title">Report Standing Water</h4>
|
||||
<p class="card-text">Report any water that has been sitting for several days, where mosquitoes can live.</p>
|
||||
<a href="/pool" class="btn btn-primary mt-3">Report Source</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card service-card h-100">
|
||||
<div class="card-body text-center">
|
||||
<div class="mb-3">
|
||||
{{ template "svg/check-report" }}
|
||||
</div>
|
||||
<h4 class="card-title">Follow-up or Check Status</h4>
|
||||
<p class="card-text">Check on a previous request or view current mosquito activity in your area.</p>
|
||||
<a href="/status" class="btn btn-primary mt-3">Get Status</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{{end}}
|
||||
Loading…
Add table
Add a link
Reference in a new issue