diff --git a/pkg/data/data.go b/pkg/data/data.go index c13b8c2..4e619d3 100644 --- a/pkg/data/data.go +++ b/pkg/data/data.go @@ -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"` } @@ -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) { @@ -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, ) } @@ -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 { @@ -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 } @@ -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{}{ @@ -204,6 +211,7 @@ func (newJob *NewJob) SaveToDB(db *sqlx.DB) (Job, error) { Valid: newJob.Description != "", }, newJob.Email, + newJob.PostType == "candidate", } var job Job diff --git a/pkg/server/routes_test.go b/pkg/server/routes_test.go index e94c8d9..dea7519 100644 --- a/pkg/server/routes_test.go +++ b/pkg/server/routes_test.go @@ -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}, @@ -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, }, @@ -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 @@ -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)) @@ -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(), } @@ -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 diff --git a/pkg/services/slack.go b/pkg/services/slack.go index 05ae234..0633e2a 100644 --- a/pkg/services/slack.go +++ b/pkg/services/slack.go @@ -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} } diff --git a/pkg/services/twitter.go b/pkg/services/twitter.go index 1d3c137..eda7472 100644 --- a/pkg/services/twitter.go +++ b/pkg/services/twitter.go @@ -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, diff --git a/sql/20251009000000_add_looking_for_work.down.sql b/sql/20251009000000_add_looking_for_work.down.sql new file mode 100644 index 0000000..da7b24e --- /dev/null +++ b/sql/20251009000000_add_looking_for_work.down.sql @@ -0,0 +1,2 @@ +-- Remove the looking_for_work column if present +ALTER TABLE jobs DROP COLUMN IF EXISTS looking_for_work; diff --git a/sql/20251009000000_add_looking_for_work.sql b/sql/20251009000000_add_looking_for_work.sql new file mode 100644 index 0000000..00b35a7 --- /dev/null +++ b/sql/20251009000000_add_looking_for_work.sql @@ -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; diff --git a/sql/20251009000000_add_looking_for_work.up.sql b/sql/20251009000000_add_looking_for_work.up.sql new file mode 100644 index 0000000..00b35a7 --- /dev/null +++ b/sql/20251009000000_add_looking_for_work.up.sql @@ -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; diff --git a/templates/edit.html b/templates/edit.html index 2a38d9d..8bb2871 100644 --- a/templates/edit.html +++ b/templates/edit.html @@ -1,6 +1,17 @@ {{ define "content" }}
+
+ What are you posting? + + +