Skip to content

Instantly share code, notes, and snippets.

@montanaflynn
Forked from anonymous/enter_info.html
Created March 20, 2016 10:24
Show Gist options
  • Save montanaflynn/0610d5d5ea088491daf4 to your computer and use it in GitHub Desktop.
Save montanaflynn/0610d5d5ea088491daf4 to your computer and use it in GitHub Desktop.
anonymously share salary
<!DOCTYPE html>
<head>
<title>salary: submit info</title>
<link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.6.0/base-min.css">
<link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.6.0/pure-min.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
padding: 1em;
}
</style>
</head>
<body>
<h1>Enter Information</h1>
<h2>{{.Name}}</h2>
<p>Please enter your information. Once you have entered your information and a total of {{.MinSize}} people have entered theirs also, visting this page will show you the results.</p>
<form action="/pool/salary" method="post" class="pure-form pure-form-stacked">
<legend>Salary Info</legend>
<fieldset>
<label for="amount">Salary in USD</label>
<input type="number" name="amount" id="amount" min="1" placeholder="12345" required>
<label for="title">Title</label>
<input type="text" name="title" id="title" value="Software Engineer">
<label for="exp">Years of experience</label>
<input type="number" name="yearsexperience" id="exp" min="0" max="123" placeholder="1">
<label for="hours">Hours per week</label>
<input type="number" name="hourswk" id="hours" min="1" max="168" placeholder="40">
Mandatory overtime
<label for="o1">
<input id="o1" type="radio" name="overtime" value="never" checked>
Never
</label>
<label for="o2">
<input id="o2" type="radio" name="overtime" value="rarely">
Rarely
</label>
<label for="o3">
<input id="o3" type="radio" name="overtime" value="sometimes">
Sometimes
</label>
<label for="o4">
<input id="o4" type="radio" name="overtime" value="often">
Often
</label>
Paid overtime
<label for="po1">
<input id="po1" type="radio" name="overtimepaid" value="unpaid" checked>
Unpaid
</label>
<label for="po2">
<input id="po2" type="radio" name="overtimepaid" value="paid">
Paid
</label>
Remote / Telecommuting
<label for="r1">
<input id="r1" type="radio" name="remote" value="no" checked>
No
</label>
<label for="r2">
<input id="r2" type="radio" name="remote" value="special">
Special (infrequently for things such as bad weather)
</label>
<label for="r3">
<input id="r3" type="radio" name="remote" value="partial">
Partial Telecommute
</label>
<label for="r4">
<input id="r4" type="radio" name="remote" value="yes">
Full Telecommute
</label>
Travel
<label for="t1">
<input id="t1" type="radio" name="travel" value="never" checked>
Never
</label>
<label for="t2">
<input id="t2" type="radio" name="travel" value="rarely">
Rarely
</label>
<label for="t3">
<input id="t3" type="radio" name="travel" value="sometimes">
Sometimes
</label>
<label for="t4">
<input id="t4" type="radio" name="travel" value="often">
Often
</label>
<input type="hidden" name="id" value="{{.UUID}}">
<input type="submit" value="Submit" class="pure-button pure-button-primary">
</fieldset>
</form>
</body>
</html>
<!DOCTYPE html>
<head>
<title>salary</title>
<link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.6.0/base-min.css">
<link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.6.0/pure-min.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
padding: 1em;
}
</style>
</head>
<body>
<h1>anonsalary</h1>
<h2>Share salary information anonymously with friends.</h2>
<form action="/pool" method="post" class="pure-form pure-form-stacked">
<fieldset>
<legend>New Share</legend>
<label for="poolName">Pool name</label>
<input type="text" name="poolName" id="poolName" required>
<label for="minSize">Minimum #/submissions until results are displayed</label>
<input type="number" name="minSize" id="minSize" min="1" value="5" required></p>
<input type="submit" value="Share" class="pure-button pure-button-primary">
</fieldset>
</form>
</body>
</html>
<!DOCTYPE html>
<head>
<title>salary</title>
<link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.6.0/base-min.css">
<link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.6.0/pure-min.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
padding: 1em;
}
</style>
</head>
<body>
<h1>anonsalary</h1>
<h2>{{.Name}}</h2>
<p>This pool will not be visible until {{.MinSize}} users have submitted their information.</p>
<p>To share this pool, give your friends the current URL in your browser.</p>
<p>Please check back again later.</p>
</body>
</html>
<!DOCTYPE html>
<head>
<title>salary: view pool</title>
<link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.6.0/base-min.css">
<link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.6.0/pure-min.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
padding: 1em;
}
</style>
</head>
<body>
<h1>Salary Information</h1>
<h2>{{.PoolName}}</h2>
<table class="pure-table pure-table-striped">
<thead>
<th>Title</th>
<th>Salary</th>
<th>Hours</th>
<th>Exp</th>
<th>OT</th>
<th>Paid OT</th>
<th>Remote</th>
<th>Travel</th>
</thead>
<tbody>
{{range .Salaries}}
<tr>
<td>{{.Title}}</td>
<td>{{.Amount}}</td>
<td>{{.HoursWk}}</td>
<td>{{.YearsExperience}}</td>
<td>{{.Overtime}}</td>
<td>{{.OvertimePaid}}</td>
<td>{{.Remote}}</td>
<td>{{.Travel}}</td>
</tr>
{{end}}
</tbody>
</body>
</html>
// salary
//
// This is a web application for anonymously sharing salary information among a
// pool of participants. Each pool is identified by a UUID. A user cannot view
// the results of the pool unless they first share their information; the pool
// must also meet a mininum number of contributors set at pool creation time.
//
// It requires PostgreSQL. Configure the database connection via the following
// environment variables: SUSER, SPASS, SDB (database name). Commands to
// create the two necessary tables are below.
//
// I meant to clean this up a bit and write tests, but after two months I
// haven't gotten around to it. Sorry for any bad code here.
//
// A good change would be to make hours/wk an enum like the overtime field, so
// that there's less potential for inadvertently knowing who submitted a
// salary ("I know $foo works 42 hours, and this entry is for 42 hours").
//
// Copyright notice:
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"database/sql"
"errors"
"fmt"
_ "github.com/lib/pq"
"github.com/satori/go.uuid"
"html/template"
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"time"
)
// create table pool (
// pool_id serial primary key,
// uuid uuid not null,
// submit uuid not null,
// name varchar(140) not null,
// minsize smallint not null
// )
type pool struct {
Id int32
UUID uuid.UUID
Submit uuid.UUID
Name string
MinSize int16
}
// create table salary (
// salary_id serial primary key,
// amount int not null,
// hourswk smallint not null,
// overtime varchar(9) check (overtime in ('never', 'rarely', 'sometimes', 'often')) not null,
// overtimepaid bool not null,
// remote varchar(7) check (remote in ('no', 'special', 'partial', 'yes')) not null,
// title varchar (100) not null,
// yearsexperience smallint not null,
// travel varchar(9) check (travel in ('never', 'rarely', 'sometimes', 'often')) not null,
// pool_id integer not null,
// constraint pool_id foreign key (pool_id)
// references pool (pool_id) match simple
// on update cascade on delete cascade
// );
type salary struct {
Amount int32
HoursWk int16
Overtime string
OvertimePaid bool
Remote string
Title string
Travel string
YearsExperience int16
}
var (
db *sql.DB
indexPage []byte
enterInfoTemplate = template.Must(template.ParseFiles("enter_info.html"))
poolTemplate = template.Must(template.ParseFiles("pool.html"))
notEnoughTemplate = template.Must(template.ParseFiles("notenough.html"))
)
type poolTemplateData struct {
PoolName string
Salaries []salary
}
func index(w http.ResponseWriter, r *http.Request) {
w.Write(indexPage)
}
func submitPool(w http.ResponseWriter, r *http.Request) {
minSize, err := strconv.ParseUint(r.FormValue("minSize"), 10, 0)
if err != nil || minSize < 1 {
log.Println(err)
http.Error(w, "Invalid minimum share size", http.StatusBadRequest)
return
}
name := r.FormValue("poolName")
if name == "" {
log.Println(err)
http.Error(w, "Invalid pool name", http.StatusBadRequest)
return
}
u := uuid.NewV4().String()
s := uuid.NewV4().String()
stmt := "insert into pool(uuid, submit, name, minsize) values($1,$2,$3,$4)"
if _, err := db.Exec(stmt, u, s, name, minSize); err != nil {
log.Println(err)
http.Error(w, "error creating pool; please try again", http.StatusInternalServerError)
return
}
poolUrl := fmt.Sprintf("/pool?id=%s", u)
http.Redirect(w, r, poolUrl, 303)
}
func poolHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
submitPool(w, r)
return
}
id := r.FormValue("id")
if id == "" {
http.Error(w, "missing pool id", http.StatusBadRequest)
return
}
submitted, no_submitted := r.Cookie(fmt.Sprintf("salary_%s", id))
// Retrieve Pool
p, err := getPool(id, w)
if err != nil {
return
}
// Enter salary if no cookie or cookie doesn't match submit key
if no_submitted != nil || submitted.Value != p.Submit.String() {
enterSalary(w, r, p)
return
}
// Get count
var count int16
stmt := `select count(*) from salary where pool_id=$1`
err = db.QueryRow(stmt, p.Id).Scan(&count)
switch {
case err == sql.ErrNoRows:
http.Error(w, "requested pool does not exist", http.StatusNotFound)
return
case err != nil:
log.Println(err)
http.Error(w, "error retrieving pool info", http.StatusInternalServerError)
return
}
if count >= p.MinSize {
displayPool(w, r, p)
return
}
if err := notEnoughTemplate.Execute(w, p); err != nil {
log.Println(err.Error())
http.Error(w, "error rendering template", http.StatusInternalServerError)
return
}
return
}
func getPool(id string, w http.ResponseWriter) (pool, error) {
gpError := func(e string, status int) (pool, error) {
log.Println(e)
http.Error(w, e, status)
return pool{}, errors.New(e)
}
p := pool{}
var u, s string
stmt := `select pool_id, uuid, submit, name, minsize from pool where uuid=$1`
err := db.QueryRow(stmt, id).Scan(&p.Id, &u, &s, &p.Name, &p.MinSize)
switch {
case err == sql.ErrNoRows:
return gpError("requested pool does not exist", http.StatusNotFound)
case err != nil:
log.Println(err)
return gpError("error retrieving pool info", http.StatusInternalServerError)
}
p.UUID = uuid.FromStringOrNil(u)
p.Submit = uuid.FromStringOrNil(s)
return p, nil
}
func enterSalary(w http.ResponseWriter, r *http.Request, p pool) {
if err := enterInfoTemplate.Execute(w, p); err != nil {
log.Println(err.Error())
http.Error(w, "error rendering template", http.StatusInternalServerError)
return
}
}
func submitSalary(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "you must POST a salary; GET not supported", http.StatusNotImplemented)
return
}
isNeg := func(v string) bool {
n, err := strconv.ParseInt(r.FormValue(v), 10, 32)
if err != nil {
// Failure to convert to int can just return true, since we end up
// erroring out anyways
return true
}
return n < 0
}
if isNeg("amount") || isNeg("yearsexperience") || isNeg("hourswk") {
http.Error(w, "cannot use negative values", http.StatusBadRequest)
return
}
id := r.FormValue("id")
submitted, err := r.Cookie(fmt.Sprintf("salary_%s", id))
// For checking if already submitted, don't actually need to compare with
// the submit key from the pool table
if submitted.String() != "" {
http.Error(w, "you have already submitted your salary", http.StatusBadRequest)
return
}
p, err := getPool(id, w)
if err != nil {
return
}
ins := `insert into salary(amount, hourswk, overtime, overtimepaid, remote, title, yearsexperience, travel, pool_id) values($1,$2,$3,$4,$5,$6,$7,$8,$9)`
_, err = db.Exec(ins, r.FormValue("amount"), r.FormValue("hourswk"), r.FormValue("overtime"), r.FormValue("overtimepaid") == "paid", r.FormValue("remote"), r.FormValue("title"), r.FormValue("yearsexperience"), r.FormValue("travel"), p.Id)
if err != nil {
log.Println(err)
http.Error(w, "All fields are required. Sorry, no fancy helpful message yet. :)", http.StatusInternalServerError)
return
}
expiration := time.Now().Add(365 * 24 * time.Hour)
cookie := http.Cookie{Name: fmt.Sprintf("salary_%s", id), Value: p.Submit.String(), Expires: expiration}
http.SetCookie(w, &cookie)
poolUrl := fmt.Sprintf("/pool?id=%s", p.UUID.String())
http.Redirect(w, r, poolUrl, 303)
}
func displayPool(w http.ResponseWriter, r *http.Request, p pool) {
stmt := `select amount, hourswk, overtimepaid, remote, title, yearsexperience, travel, overtime from salary where pool_id=$1 order by title asc, amount desc`
rows, err := db.Query(stmt, p.Id)
switch {
case err == sql.ErrNoRows:
http.Error(w, "no salaries for pool", http.StatusNotFound)
return
case err != nil:
log.Println(err)
http.Error(w, "error retrieving group salary info", http.StatusInternalServerError)
return
}
salaries := make([]salary, 0)
for rows.Next() {
s := salary{}
err = rows.Scan(&s.Amount, &s.HoursWk, &s.OvertimePaid, &s.Remote, &s.Title, &s.YearsExperience, &s.Travel, &s.Overtime)
if err != nil {
log.Println(err)
http.Error(w, "error retrieving individual salary info", http.StatusInternalServerError)
return
}
salaries = append(salaries, s)
}
data := poolTemplateData{
PoolName: p.Name,
Salaries: salaries,
}
if err := poolTemplate.Execute(w, data); err != nil {
log.Println(err.Error())
http.Error(w, "error rendering template", http.StatusInternalServerError)
return
}
}
func main() {
ip, err := ioutil.ReadFile("index.html")
if err != nil {
panic(err)
}
indexPage = ip
dbinfo := fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable", os.Getenv("SUSER"), os.Getenv("SPASS"), os.Getenv("SDB"))
db, err = sql.Open("postgres", dbinfo)
if err != nil {
log.Fatal("failed to open database", err)
}
defer db.Close()
http.HandleFunc("/", index)
http.HandleFunc("/pool", poolHandler)
http.HandleFunc("/pool/salary", submitSalary)
log.Fatal(http.ListenAndServe(":9001", nil))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment