Skip to content
Open
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
18 changes: 13 additions & 5 deletions pkg/data/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Job struct {
Description sql.NullString `db:"description" json:"-"`
DescriptionJSON *string `db:"-" json:"description"`
Email string `db:"email" json:"email"`
LookingForWork bool `db:"looking_for_work" json:"looking_for_work"`
PublishedAt time.Time `db:"published_at" json:"published_at"`
}

Expand All @@ -47,6 +48,9 @@ func (job *Job) Update(newParams NewJob) {

job.Description.String = newParams.Description
job.Description.Valid = newParams.Description != ""

// Update post type
job.LookingForWork = newParams.PostType == "candidate"
}

func (job *Job) RenderDescription() (string, error) {
Expand Down Expand Up @@ -75,8 +79,8 @@ func (job *Job) RenderDescription() (string, error) {

func (job *Job) Save(db *sqlx.DB) (sql.Result, error) {
return db.Exec(
"UPDATE jobs SET position = $1, organization = $2, url = $3, description = $4 WHERE id = $5",
job.Position, job.Organization, job.Url, job.Description, job.ID,
"UPDATE jobs SET position = $1, organization = $2, url = $3, description = $4, looking_for_work = $5 WHERE id = $6",
job.Position, job.Organization, job.Url, job.Description, job.LookingForWork, job.ID,
)
}

Expand Down Expand Up @@ -153,6 +157,8 @@ type NewJob struct {
Url string `form:"url"`
Description string `form:"description"`
Email string `form:"email"`
// PostType can be "job" (default) or "candidate" for individuals looking for work
PostType string `form:"post_type"`
}

func (newJob *NewJob) Validate(update bool) map[string]string {
Expand All @@ -162,7 +168,8 @@ func (newJob *NewJob) Validate(update bool) map[string]string {
errs["position"] = ErrNoPosition
}

if newJob.Organization == "" {
// Organization is only required for job postings
if newJob.PostType != "candidate" && newJob.Organization == "" {
errs["organization"] = ErrNoOrganization
}

Expand All @@ -188,8 +195,8 @@ func (newJob *NewJob) Validate(update bool) map[string]string {

func (newJob *NewJob) SaveToDB(db *sqlx.DB) (Job, error) {
query := `INSERT INTO jobs
(position, organization, url, description, email)
VALUES ($1, $2, $3, $4, $5)
(position, organization, url, description, email, looking_for_work)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *`

params := []interface{}{
Expand All @@ -204,6 +211,7 @@ func (newJob *NewJob) SaveToDB(db *sqlx.DB) (Job, error) {
Valid: newJob.Description != "",
},
newJob.Email,
newJob.PostType == "candidate",
}

var job Job
Expand Down
28 changes: 27 additions & 1 deletion pkg/server/routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func TestNewJob(t *testing.T) {
required bool
textArea bool
}{
{"post_type", false, false},
{"position", true, false},
{"organization", true, false},
{"description", false, true},
Expand Down Expand Up @@ -106,6 +107,19 @@ func TestCreateJob(t *testing.T) {
"description": {"Super rad place to work"},
"url": {""},
"email": {"test@example.com"},
"post_type": {"job"},
},
expectSuccess: true,
},
{
// Candidate looking for work: organization not required
values: map[string][]string{
"position": {"Senior Go Developer"},
"organization": {""},
"description": {"Available for contracts"},
"url": {"https://example.com/portfolio"},
"email": {"candidate@example.com"},
"post_type": {"candidate"},
},
expectSuccess: true,
},
Expand Down Expand Up @@ -152,7 +166,17 @@ func TestCreateJob(t *testing.T) {
expectSelectJobsQuery(dbmock, []data.Job{newJob})
}

// default to job post if not specified
if _, ok := tt.values["post_type"]; !ok {
tt.values["post_type"] = []string{"job"}
}
reqBody := url.Values(tt.values).Encode()
if _, ok := tt.values["post_type"]; !ok {
// Keep default as job for existing tests
v := url.Values(tt.values)
v.Set("post_type", "job")
reqBody = v.Encode()
}
respBody, resp := sendRequest(t, fmt.Sprintf("%s/jobs", s.URL), []byte(reqBody))

// Should follow the redirect and result in a 200 regardless of success/failure
Expand Down Expand Up @@ -379,6 +403,7 @@ func TestUpdateJobAuthorized(t *testing.T) {
tt.values["organization"][0],
sql.NullString{String: urlVal, Valid: urlVal != ""},
sql.NullString{String: desc, Valid: desc != ""},
false, // looking_for_work defaults to false unless specified
job.ID,
).WillReturnResult(sqlmock.NewResult(0, 1))

Expand Down Expand Up @@ -527,6 +552,7 @@ func mockJobRow(job data.Job) []driver.Value {
sql.NullString{String: "https://devict.org", Valid: true},
sql.NullString{},
"example@example.com",
false,
time.Now(),
}

Expand Down Expand Up @@ -555,7 +581,7 @@ func mockJobRow(job data.Job) []driver.Value {
}

if !job.PublishedAt.IsZero() {
vals[6] = job.PublishedAt
vals[7] = job.PublishedAt
}

return vals
Expand Down
24 changes: 17 additions & 7 deletions pkg/services/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,22 @@ func (svc *SlackService) PostToSlack(job data.Job) error {
}

func slackMessageFromJob(job data.Job, c *config.Config) SlackMessage {
text := fmt.Sprintf(
"A new job was posted!\n> *<%s/jobs/%s|%s @ %s>*",
c.URL,
job.ID,
job.Position,
job.Organization,
)
var text string
if job.LookingForWork {
text = fmt.Sprintf(
"A candidate is looking for work!\n> *<%s/jobs/%s|%s>*",
c.URL,
job.ID,
job.Position,
)
} else {
text = fmt.Sprintf(
"A new job was posted!\n> *<%s/jobs/%s|%s @ %s>*",
c.URL,
job.ID,
job.Position,
job.Organization,
)
}
return SlackMessage{Text: text}
}
8 changes: 8 additions & 0 deletions pkg/services/twitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ func (svc *TwitterService) PostToTwitter(job data.Job) error {
}

func tweetFromJob(job data.Job, c *config.Config) string {
if job.LookingForWork {
return fmt.Sprintf(
"A candidate is looking for work! -- %s\n\nMore info at %s/jobs/%s",
job.Position,
c.URL,
job.ID,
)
}
return fmt.Sprintf(
"A job was posted! -- %s at %s\n\nMore info at %s/jobs/%s",
job.Position,
Expand Down
2 changes: 2 additions & 0 deletions sql/20251009000000_add_looking_for_work.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Remove the looking_for_work column if present
ALTER TABLE jobs DROP COLUMN IF EXISTS looking_for_work;
2 changes: 2 additions & 0 deletions sql/20251009000000_add_looking_for_work.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Add a boolean column to indicate if the post is from an individual looking for work
ALTER TABLE jobs ADD COLUMN IF NOT EXISTS looking_for_work BOOLEAN NOT NULL DEFAULT FALSE;
2 changes: 2 additions & 0 deletions sql/20251009000000_add_looking_for_work.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Add a boolean column to indicate if the post is from an individual looking for work
ALTER TABLE jobs ADD COLUMN IF NOT EXISTS looking_for_work BOOLEAN NOT NULL DEFAULT FALSE;
11 changes: 11 additions & 0 deletions templates/edit.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
{{ define "content" }}
<form method="post" action="/jobs/{{ .job.ID }}?token={{ .token }}">
<!-- TODO: csrf -->
<fieldset class="mb-4">
<legend class="form-label mb-2">What are you posting?</legend>
<label class="inline-flex items-center mr-4">
<input type="radio" name="post_type" value="job" class="mr-2" {{ if not .job.LookingForWork }}checked{{ end }}>
Job opening
</label>
<label class="inline-flex items-center">
<input type="radio" name="post_type" value="candidate" class="mr-2" {{ if .job.LookingForWork }}checked{{ end }}>
I'm looking for work
</label>
</fieldset>
<label class="block">
<span class="form-label">Position</span>
<span class="align-top text-sm text-gray-500">*</span>
Expand Down
12 changes: 8 additions & 4 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
<li class="flex mb-2 p-4 relative border-b sm:border-b-0 last:border-b-0 hover:bg-blue-100 group sm:rounded-lg">
<div class="w-full sm:pr-16">
<h2 class="m-0 font-bold text-lg">{{ .Position }}</h2>
<div>{{ .Organization }}</div>
{{ if .LookingForWork }}
<div class="text-sm text-gray-600">Candidate looking for work</div>
{{ else }}
<div>{{ .Organization }}</div>
{{ end }}
<a
href="/jobs/{{ .ID }}"
class="relative z-10 text-gray-500 hover:underline focus:underline"
Expand All @@ -14,12 +18,12 @@ <h2 class="m-0 font-bold text-lg">{{ .Position }}</h2>
</time>
</a>
</div>
{{ if .Url.Valid }}
{{ if .Url.Valid }}
<a
href="{{ .Url.String }}"
target="_blank"
class="opacity-0 text-sm font-bold text-orange-500 uppercase absolute inset-0 flex items-center justify-end p-4 sm:group-hover:opacity-100 sm:focus:opacity-100"
>Apply</a>
class="opacity-0 text-sm font-bold text-orange-500 uppercase absolute inset-0 flex items-center justify-end p-4 sm:group-hover:opacity-100 sm:focus:opacity-100"
>{{ if .LookingForWork }}Contact{{ else }}Apply{{ end }}</a>
{{ else }}
<a
href="/jobs/{{ .ID }}"
Expand Down
12 changes: 12 additions & 0 deletions templates/new.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
{{ define "content" }}
<form method="post" action="/jobs">
<!-- TODO: csrf -->
<fieldset class="mb-4">
<legend class="form-label mb-2">What are you posting?</legend>
<label class="inline-flex items-center mr-4">
<input type="radio" name="post_type" value="job" class="mr-2" checked>
Job opening
</label>
<label class="inline-flex items-center">
<input type="radio" name="post_type" value="candidate" class="mr-2">
I'm looking for work
</label>
</fieldset>
<label class="block">
<span class="form-label">Position</span>
<span class="align-top text-sm text-gray-500">*</span>
Expand All @@ -15,6 +26,7 @@
<label class="block">
<span class="form-label">Organization</span>
<span class="align-top text-sm text-gray-500">*</span>
<span class="form-description">For candidates, you can put your name or "N/A".</span>
{{ if .organization_err }}
{{ range .organization_err }}
<span class="form-error">{{ . }}</span>
Expand Down
8 changes: 6 additions & 2 deletions templates/view.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
{{ define "content" }}
<h2 class="m-0 font-bold text-lg">{{ .job.Position }}</h2>
<div class="mb-6">{{ .job.Organization }}</div>
{{ if .job.LookingForWork }}
<div class="mb-6 text-sm text-gray-600">Candidate looking for work</div>
{{ else }}
<div class="mb-6">{{ .job.Organization }}</div>
{{ end }}
{{ if.job.Description.Valid }}
<hr>
<div class="mb-6">{{ .description }}</div>
{{ end }}
{{ if .job.Url.Valid }}
<div class="mb-6">
<a href="{{ .job.Url.String }}" target="_blank" class="btn btn-primary">
Apply
{{ if .job.LookingForWork }}Contact{{ else }}Apply{{ end }}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="20" height="20" fill="currentColor" class="inline-block ml-1"><path d="M0 3c0-1.1.9-2 2-2h16a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3zm2 2v12h16V5H2zm8 3l4 5H6l4-5z"/></svg>
</a>
</div>
Expand Down