diff options
| author | bnewbold <bnewbold@robocracy.org> | 2012-09-19 17:07:54 +0200 | 
|---|---|---|
| committer | bnewbold <bnewbold@robocracy.org> | 2012-09-19 17:07:54 +0200 | 
| commit | cc783ca52451587a471d109c9d0229c3c21c29b8 (patch) | |
| tree | 27487ca55f942d95307d4bb69a32763799feac8c | |
| parent | 24a4873ded5020f99ccb8850c9efda25928b5710 (diff) | |
| download | bommom-cc783ca52451587a471d109c9d0229c3c21c29b8.tar.gz bommom-cc783ca52451587a471d109c9d0229c3c21c29b8.zip  | |
basic bom uploading (WIP)
| -rw-r--r-- | README | 7 | ||||
| -rw-r--r-- | TODO | 3 | ||||
| -rw-r--r-- | bommom.go | 2 | ||||
| -rw-r--r-- | core.go | 8 | ||||
| -rw-r--r-- | octopart.go | 129 | ||||
| -rw-r--r-- | octopart_test.go | 9 | ||||
| -rw-r--r-- | serve.go | 116 | ||||
| -rw-r--r-- | templates/bom_upload.html | 29 | 
8 files changed, 284 insertions, 19 deletions
@@ -10,13 +10,18 @@  BomMom: A web publishing/wiki system for electronics Bill of Materials (BOM)  -A work in progress as of April 2012. Written in golang. +VAPORWARE ALERT! A work in progress as of April 2012. + +Written in golang.  ### Instructions  Install golang compiler and run `go build` in this directory, then run the  `bommom` command to list available commands and options.  +Run ``./bommom -port 7777 serve`` to start a server on local port 7777; by +default listens on all interfaces. +  ### Basic Features   - command line tools for managing part list files @@ -0,0 +1,3 @@ +- crude octopart integration (prices, availability) +- basic view-only HTML5 skin, with charts etc +- deployment scheme @@ -175,7 +175,7 @@ func initCmd() {  		// dummy BomMeta already exists?  		return  	} -	b := makeTestBom() +	_, b := makeTestBom()  	b.Version = "v001"  	bm = &BomMeta{Name: "gizmo",  		Owner:        "common", @@ -30,6 +30,7 @@ type LineItem struct {  	Category     string   `json:"category"` // hierarchy as comma seperated list  	Elements     []string `json:"elements"`  	Offers       []Offer  `json:"offers"` +    AggregateInfo map[string]string  `json:"miscinfo"`  }  func (li *LineItem) Id() string { @@ -101,7 +102,7 @@ func (bm *BomMeta) Validate() error {  }  // ---------- testing -func makeTestBom() *Bom { +func makeTestBom() (*BomMeta, *Bom) {  	op1 := OfferPrice{Currency: "usd", Price: 1.0, MinQty: 1}  	op2 := OfferPrice{Currency: "usd", Price: 0.8, MinQty: 100}  	o := Offer{Sku: "A123", Distributor: "Acme", Prices: []OfferPrice{op1, op2}} @@ -114,5 +115,8 @@ func makeTestBom() *Bom {  	//li.AddOffer(o)  	b := NewBom("test01")  	b.AddLineItem(&li) -	return b +	b.AddLineItem(&li) +	b.AddLineItem(&li) +    bm := &BomMeta{Name: "Some Bom", Owner: "Some Owner", Description: "This is such a thing!", HeadVersion: b.Version, Homepage: "http://bommom.com", IsPublicView: true, IsPublicEdit: false} +	return bm, b  } diff --git a/octopart.go b/octopart.go index a1f14df..367bc99 100644 --- a/octopart.go +++ b/octopart.go @@ -2,6 +2,11 @@ package main  import (      "net/http" +    "net/url" +    "encoding/json" +    "bytes" +    "log" +    //"io/ioutil"  )  /* @@ -15,34 +20,140 @@ type OctopartClient struct {      ApiKey string      RemoteHost string      client *http.Client +    infoCache map[string]interface{}  }  func NewOctopartClient(apikey string) *OctopartClient {      oc := &OctopartClient{ApiKey: apikey, -                          RemoteHost: "https://www.octopart.com"} +                          RemoteHost: "https://octopart.com"}      oc.client = &http.Client{} +    oc.infoCache = make(map[string]interface{})      return oc  }  func openPricingSource() { +    // TODO: pass through octopart API key here      pricingSource = NewOctopartClient("")  } -func (*oc OctopartClient) apiCall(method string, params map[string]string) (map[string]interface, error) { +func (oc *OctopartClient) apiCall(method string, params map[string]string) (map[string]interface{}, error) {      paramString := "?apikey=" + oc.ApiKey +    // TODO: assert clean-ness of params +    // TODO: use url.Values instead...      for key := range params { -        paramString += "&" + key + "=" + params[key] +        paramString += "&" + url.QueryEscape(key) + "=" + url.QueryEscape(params[key])      } -    resp, err := oc.client.Get(oc.RemoteHost + "/api/v2/" + method) - -    // resp as json, or interpret as error -    return  +    paramStringUnescaped, _ := url.QueryUnescape(paramString) // TODO: err +    log.Println("Fetching: " + oc.RemoteHost + "/api/v2/" + method + paramStringUnescaped) +    resp, err := oc.client.Get(oc.RemoteHost + "/api/v2/" + method + paramString) +    if err != nil { +        return nil, err +    } +    if resp.StatusCode != 200 { +        return nil, Error("Octopart API call error: " + resp.Status) +    } +    result := make(map[string]interface{}) +    defer resp.Body.Close() +    //body, err := ioutil.ReadAll(resp.Body) +    //if err != nil { +    //    return nil, err +    //} +    //body = append(body, '\n') +    //dec := json.NewDecoder(bytes.NewReader(body)) +    dec := json.NewDecoder(resp.Body) +    if err = dec.Decode(&result); err != nil { +        return nil, err +    } +    return result, nil  } -func (*oc OctopartClient) GetMarketInfo(mpn, manufacturer string) (map[string]interface, error) { +// this method doesn't check internal cache, but it does append to it +func (oc *OctopartClient) bomApiCall(manufacturers, mpns []string) ([]map[string]interface{}, error) { +    // TODO: check len(mpns) == len(manufacturers)  +    queryList := make([]map[string]string, len(mpns)) +    listItem := make(map[string]string) +    for i, _ := range mpns { +        listItem = make(map[string]string) +        listItem["mpn_or_sku"] = mpns[i] +        listItem["manufacturer"] = manufacturers[i] +        listItem["limit"] = "1" +        listItem["reference"] = manufacturers[i] + "|" + mpns[i] +        queryList[i] = listItem +    } +    linesBuffer := new(bytes.Buffer) +    enc := json.NewEncoder(linesBuffer) +    if err := enc.Encode(queryList); err != nil { +        return nil, err +    } + +    response, err := oc.apiCall("bom/match", map[string]string{"lines": linesBuffer.String()}) +    if err != nil { +        return nil, err +    } +    // TODO: just grabbing first result for now; user can make better specification later +    ret := make([]map[string]interface{}, len(mpns)) +    for i, rawresult := range response["results"].([]interface{}) { +        result := rawresult.(map[string]interface{}) +        hits := int(result["hits"].(float64)) +        reference := result["reference"].(string) +        if hits == 0 { +            ret[i] = nil +            oc.infoCache[reference] = nil +        } else { +            ret[i] = result["items"].([]interface{})[0].(map[string]interface{}) +            oc.infoCache[reference] = ret[i] +        } +    } +    return ret, nil  } -func (*oc OctopartClient) GetPricing(method string, params map[string]string) (map[string]interface, error) { +func (oc *OctopartClient) GetMarketInfo(manufacturers, mpns []string) ([]interface{}, error) { +    if len(mpns) < 1 { +        return nil, Error("no mpns strings passed in") +    } +    if len(mpns) != len(manufacturers) { +        return nil, Error("number of mpns doesn't match number of manufacturers") +    } +    if len(mpns) > 100 { +        return nil, Error("can't handle more than 100 queries at a time (yet)") +    } +    mpnToQuery := make([]string, 0) +    manufacturersToQuery := make([]string, 0) +    queryHash := "" +    // check for queryHashes in internal cache +    for i, _ := range mpns { +        queryHash = manufacturers[i] + "|" + mpns[i] +        if _, hasKey := oc.infoCache[queryHash]; hasKey != true { +            manufacturersToQuery = append(manufacturersToQuery, manufacturers[i]) +            mpnToQuery = append(mpnToQuery, mpns[i]) +        } +    } +    // if necessary, fetch missing queryHashes to internal cache +    if len(mpnToQuery) > 0 { +        if _, err := oc.bomApiCall(manufacturersToQuery, mpnToQuery); err != nil { +            return nil, err +        } +    } +    // construct list of return info +    result := make([]interface{}, len(mpns)) +    for i, _ := range mpns { +        queryHash = manufacturers[i] + "|" + mpns[i] +        value, hasKey := oc.infoCache[queryHash] +        if hasKey != true { +            return nil, Error("key should be in cache, but isn't: " + queryHash) +        } +        result[i] = value +    } +    return result, nil +} +func (oc *OctopartClient) GetReducedPricing(mpn, manufacturer string) (interface{}, error) { +    marketInfo, err := oc.GetMarketInfo([]string{mpn}, []string{manufacturer}) +    if err != nil { +        return nil, err +    } +    // reduce marketInfo to pricing +    return marketInfo[0], nil  } + diff --git a/octopart_test.go b/octopart_test.go index 75953ee..2e0e295 100644 --- a/octopart_test.go +++ b/octopart_test.go @@ -42,4 +42,11 @@ func TestGetMarketInfo(t *testing.T) {      }  } -//TODO? TestGetPricing +func TestAttachInfo(t *testing.T) { +    b := makeTestBom() +    bm := &BomMeta{} +    oc := NewOctopartClient("") +    oc.AttachMarketInfo(oc) +    DumpBomAsText(bm, b,  +} + @@ -6,25 +6,31 @@ import (  	"log"  	"net/http"  	"regexp" +    "path/filepath" +    "time"  )  var ( -	tmplHome, tmplView, tmplUser, tmplBomView *template.Template +	tmplHome, tmplView, tmplUser, tmplBomView, tmplBomUpload *template.Template  )  func baseHandler(w http.ResponseWriter, r *http.Request) {  	var err error  	log.Printf("serving %s\n", r.URL.Path) -	bomUrlPattern := regexp.MustCompile("^/([a-zA-Z][a-zA-Z0-9_]*)/([a-zA-Z][a-zA-Z0-9_]*)(/.*)$") +	bomUrlPattern := regexp.MustCompile("^/([a-zA-Z][a-zA-Z0-9_]*)/([a-zA-Z][a-zA-Z0-9_]*)/$") +	bomUploadUrlPattern := regexp.MustCompile("^/([a-zA-Z][a-zA-Z0-9_]*)/([a-zA-Z][a-zA-Z0-9_]*)/_upload/$")  	userUrlPattern := regexp.MustCompile("^/([a-zA-Z][a-zA-Z0-9_]*)/$")  	switch {  	case r.URL.Path == "/":  		err = homeController(w, r) +	case bomUploadUrlPattern.MatchString(r.URL.Path): +		match := bomUploadUrlPattern.FindStringSubmatch(r.URL.Path) +		err = bomUploadController(w, r, match[1], match[2])  	case bomUrlPattern.MatchString(r.URL.Path):  		match := bomUrlPattern.FindStringSubmatch(r.URL.Path) -		err = bomController(w, r, match[1], match[2], match[3]) +		err = bomController(w, r, match[1], match[2])  	case userUrlPattern.MatchString(r.URL.Path):  		match := userUrlPattern.FindStringSubmatch(r.URL.Path)  		err = userController(w, r, match[1], "") @@ -74,7 +80,7 @@ func userController(w http.ResponseWriter, r *http.Request, user, extra string)  	return  } -func bomController(w http.ResponseWriter, r *http.Request, user, name, extra string) (err error) { +func bomController(w http.ResponseWriter, r *http.Request, user, name string) (err error) {  	if !isShortName(user) {  		http.Error(w, "invalid username: "+user, 400)  		return @@ -86,12 +92,111 @@ func bomController(w http.ResponseWriter, r *http.Request, user, name, extra str  	context := make(map[string]interface{})  	context["BomMeta"], context["Bom"], err = bomstore.GetHead(ShortName(user), ShortName(name))  	if err != nil { -		return +		http.Error(w, "404 couldn't open bom: " + user + "/" + name, 404) +		return nil  	}  	err = tmplBomView.Execute(w, context)  	return  } +func bomUploadController(w http.ResponseWriter, r *http.Request, user, name string) (err error) { + +	if !isShortName(user) { +		http.Error(w, "invalid username: "+user, 400) +		return +	} +	if !isShortName(name) { +		http.Error(w, "invalid bom name: "+name, 400) +		return +	} +	context := make(map[string]interface{}) +    context["user"] = ShortName(user) +    context["name"] = ShortName(name) +	context["BomMeta"], context["Bom"], err = bomstore.GetHead(ShortName(user), ShortName(name)) + +    switch r.Method { +	case "POST": + + +        err := r.ParseMultipartForm(1024*1024*2) +        if err != nil { +            log.Println(err) +            http.Error(w, err.Error(), 400) +            return nil +        } +        file, fileheader, err := r.FormFile("bomfile") +        if err != nil { +            log.Println(err) +            context["error"] = "bomfile was nil!" +            err = tmplBomUpload.Execute(w, context) +            return err +        } +        if file == nil { +            log.Println("bomfile was nil") +            context["error"] = "bomfile was nil!" +            err = tmplBomUpload.Execute(w, context) +            return err +        } +        versionStr := r.FormValue("version") +        if len(versionStr) == 0 || isShortName(versionStr) == false { +            context["error"] = "Version must be specified and a ShortName!" +            context["version"] = versionStr +            err = tmplBomUpload.Execute(w, context) +            return err +        } + +        //contentType := fileheader.Header["Content-Type"][0] +        var b *Bom +        var bm *BomMeta + +        switch filepath.Ext(fileheader.Filename) { +            case ".json": +                bm, b, err = LoadBomFromJSON(file) +                if err != nil { +                    context["error"] = "Problem loading JSON file" +                    err = tmplBomUpload.Execute(w, context) +                    return err +                } +            case ".csv": +                b, err = LoadBomFromCSV(file) +                bm = &BomMeta{} +                if err != nil { +                    context["error"] = "Problem loading XML file" +                    err = tmplBomUpload.Execute(w, context) +                    return err +                } +            case ".xml": +                bm, b, err = LoadBomFromXML(file) +                if err != nil { +                    context["error"] = "Problem loading XML file" +                    err = tmplBomUpload.Execute(w, context) +                    return err +                } +            default: +                context["error"] = "Unknown file type: " + string(fileheader.Filename) +                err = tmplBomUpload.Execute(w, context) +                return err +        } +        bm.Owner = user  +        bm.Name = name +        b.Progeny = "File uploaded from " + fileheader.Filename +        b.Created = time.Now() +        b.Version = string(versionStr) +        if err := bomstore.Persist(bm, b, ShortName(versionStr)); err != nil { +            context["error"] = "Problem saving to datastore: " + err.Error() +            err = tmplBomUpload.Execute(w, context) +        } +        http.Redirect(w, r, "//" + user + "/" + name + "/", 302) +	case "GET": +        err = tmplBomUpload.Execute(w, context) +    default: +        http.Error(w, "bad method", 405) +        return nil +    } +	return +} + +  func serveCmd() {  	var err error @@ -100,6 +205,7 @@ func serveCmd() {  	tmplHome = template.Must(template.ParseFiles(*templatePath+"/home.html", baseTmplPath))  	tmplUser = template.Must(template.ParseFiles(*templatePath+"/user.html", baseTmplPath))  	tmplBomView = template.Must(template.ParseFiles(*templatePath+"/bom_view.html", baseTmplPath)) +	tmplBomUpload = template.Must(template.ParseFiles(*templatePath+"/bom_upload.html", baseTmplPath))  	if err != nil {  		log.Fatal(err)  	} diff --git a/templates/bom_upload.html b/templates/bom_upload.html new file mode 100644 index 0000000..1d03963 --- /dev/null +++ b/templates/bom_upload.html @@ -0,0 +1,29 @@ +{{ template "HEADER" }} +{{ if .BomMeta }} + +<h1>{{ .BomMeta.Name }} is a bom.</h1> +<b>Owner: </b>{{ .BomMeta.Owner }}<br> +{{ if .BomMeta.Homepage }}<b>Homepage: </b>{{ .BomMeta.Homepage }}<br>{{ end }} +{{ if .BomMeta.Description }}<b>Description: </b>{{ .BomMeta.Description }}<br>{{ end }} +<b>Version: </b>{{ .BomMeta.HeadVersion }} (at head)<br> +<b>Created: </b>{{ .Bom.Created }}<br> +{{ if .Bom.Progeny}}<b>Source: </b>{{ .Bom.Progeny }}<br>{{ end }} +<hr> + +{{ else }} + +<h1>{{ .name }} could be a bom.</h1> +<b>Owner: </b>{{ .user }}<br> +<hr> + +{{ end }} + +{{ if .error }}<h3 style="color:red;">{{ .error }}</h3>{{ end }} +<form enctype="multipart/form-data" method="POST" target="."> +<b>Version: </b><input name="version" type="text" value="{{ .version }}"></input><br> +<b>Created: </b>now!<br> +<input type="file" name="bomfile" accept="application/json,application/xml,text/csv"></input><br> +<input type="submit" value="Upload"></input> +</form> + +{{ template "FOOTER" }}  | 
