Skip to content

Commit

Permalink
Add a mechanism for adding new products and associating them with
Browse files Browse the repository at this point in the history
barcodes.
  • Loading branch information
caoimhechaos committed Dec 26, 2013
1 parent d951642 commit 769375e
Show file tree
Hide file tree
Showing 6 changed files with 321 additions and 2 deletions.
6 changes: 6 additions & 0 deletions cassandra-schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
create keyspace starstock;
use starstock;
create column family products with key_validation_class = 'LexicalUUIDType' and comparator = 'AsciiType' and column_metadata = [{column_name: name, validation_class: UTF8Type, index_type: KEYS}, {column_name: price, validation_class: DoubleType, index_type: KEYS}, {column_name: vendor, validation_class: LexicalUUIDType, index_type: KEYS}, {column_name: barcodes, validation_class: BytesType, index_type: 0}];
create column family vendors with key_validation_class = 'LexicalUUIDType' and comparator = 'AsciiType' and column_metadata = [{column_name: name, validation_class: UTF8Type, index_type: KEYS}, {column_name: address, validation_class: UTF8Type, index_type: 0}, {column_name: comments, validation_class: UTF8Type, index_type: 0}];
create column family products_byname with key_validation_class = 'UTF8Type' and comparator = 'AsciiType' and column_metadata = [{column_name: product, validation_class: LexicalUUIDType, index_type: KEYS}];
create column family products_bybarcode with key_validation_class = 'AsciiType' and comparator = 'AsciiType' and column_metadata = [{column_name: product, validation_class: LexicalUUIDType, index_type: KEYS}];
30 changes: 30 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@
package main

import (
"database/cassandra"
"flag"
"html/template"
"log"
"net/http"
"os"
"time"

"ancient-solutions.com/ancientauth"
"ancient-solutions.com/doozer/exportedservice"
Expand All @@ -48,10 +50,13 @@ func main() {
var lockserv, lockboot, servicename string
var ca, pub, priv, authserver string
var requested_scope string
var dbserver, keyspace string
var searchif_tmpl *template.Template
var permission_denied_tmpl *template.Template
var exporter *exportedservice.ServiceExporter
var authenticator *ancientauth.Authenticator
var client *cassandra.RetryCassandraClient
var ire *cassandra.InvalidRequestException
var err error

flag.BoolVar(&help, "help", false, "Display help")
Expand All @@ -78,6 +83,10 @@ func main() {
"", "Service name to publish as to the lock server")
flag.StringVar(&requested_scope, "scope",
"staff", "People need to be in this scope to use the application")
flag.StringVar(&dbserver, "cassandra-server", "localhost:9160",
"Cassandra database server to use")
flag.StringVar(&keyspace, "keyspace", "starstock",
"Cassandra keyspace to use for accessing stock data")
flag.Parse()

if help {
Expand Down Expand Up @@ -109,11 +118,32 @@ func main() {
log.Fatal("NewAuthenticator: ", err)
}

// Connect to the Cassandra server.
client, err = cassandra.NewRetryCassandraClientTimeout(dbserver,
10*time.Second)
if err != nil {
log.Fatal("Error opening connection to ", dbserver, ": ", err)
}

ire, err = client.SetKeyspace(keyspace)
if ire != nil {
log.Fatal("Error setting keyspace to ", keyspace, ": ", ire.Why)
}
if err != nil {
log.Fatal("Error setting keyspace to ", keyspace, ": ", err)
}

// Register the URL handler to be invoked.
http.Handle("/css/", http.FileServer(http.Dir(template_dir)))
http.Handle("/js/", http.FileServer(http.Dir(template_dir)))
http.Handle("/api/add-product", &ProductAddAPI{
authenticator: authenticator,
client: client,
scope: requested_scope,
})
http.Handle("/api/products", &ProductSearchAPI{
authenticator: authenticator,
client: client,
scope: requested_scope,
})
http.Handle("/", &ProductSearchForm{
Expand Down
217 changes: 217 additions & 0 deletions productedit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/*
* (c) 2013, Caoimhe Chaos <[email protected]>,
* Starship Factory. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* * Neither the name of the Starship Factory nor the name of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
* SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package main

import (
"bytes"
"crypto/rand"
"database/cassandra"
"encoding/binary"
"expvar"
"io"
"log"
"net/http"
"regexp"
"strings"
"time"

"ancient-solutions.com/ancientauth"
"code.google.com/p/goprotobuf/proto"
)

const NUM_100NS_INTERVALS_SINCE_UUID_EPOCH = 0x01b21dd213814000

// Number of errors which occurred adding products, mapped by type.
var productAddErrors *expvar.Map = expvar.NewMap("num-product-add-errors")

type ProductAddAPI struct {
authenticator *ancientauth.Authenticator
client *cassandra.RetryCassandraClient
scope string
}

func GenTimeUUID(when *time.Time) ([]byte, error) {
var uuid *bytes.Buffer = new(bytes.Buffer)
var stamp int64 = when.UnixNano()/100 + NUM_100NS_INTERVALS_SINCE_UUID_EPOCH
var stampLow int64 = stamp & 0xffffffff
var stampMid int64 = stamp & 0xffff00000000
var stampHi int64 = stamp & 0xfff000000000000
var err error

var upper int64 = (stampLow << 32) | (stampMid >> 16) | (1 << 12) |
(stampHi >> 48)

err = binary.Write(uuid, binary.LittleEndian, upper)
if err != nil {
return []byte{}, err
}
uuid.WriteByte(0xC0)
uuid.WriteByte(0x00)

_, err = io.CopyN(uuid, rand.Reader, 6)
if err != nil {
return []byte{}, err
}

return uuid.Bytes(), nil
}

func (self *ProductAddAPI) ServeHTTP(w http.ResponseWriter, req *http.Request) {
var uuid []byte
var mmap map[string]map[string][]*cassandra.Mutation
var mutations []*cassandra.Mutation
var mutation *cassandra.Mutation
var col *cassandra.Column
var ire *cassandra.InvalidRequestException
var ue *cassandra.UnavailableException
var te *cassandra.TimedOutException
var prodname, barcode string
var now time.Time = time.Now()
var err error
var match bool

numRequests.Add(1)
numAPIRequests.Add(1)

// Check the user is in the reqeuested scope.
if !self.authenticator.IsAuthenticatedScope(req, self.scope) {
numDisallowedScope.Add(1)
http.Error(w,
"You are not in the right group to access this resource",
http.StatusForbidden)
return
}

// Check if the product name has been specified.
prodname = req.PostFormValue("prodname")
if len(prodname) <= 0 {
http.Error(w, "Product name empty", http.StatusNotAcceptable)
return
}

// Check if the barcode has been given. If it was, it needs to be
// numeric (EAN-13). If we find different types of barcodes we can
// always revise this.
barcode = strings.Replace(req.PostFormValue("barcode"), " ", "", -1)
if len(barcode) > 0 {
match, err = regexp.MatchString("^[0-9]+$", barcode)
if err != nil {
productAddErrors.Add(err.Error(), 1)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if !match {
productAddErrors.Add("barcode-format-error", 1)
http.Error(w, "Barcode should only contain numbers",
http.StatusNotAcceptable)
return
}
}

col = cassandra.NewColumn()
col.Name = []byte("name")
col.Value = []byte(prodname)
col.Timestamp = now.Unix()
mutation = cassandra.NewMutation()
mutation.ColumnOrSupercolumn = cassandra.NewColumnOrSuperColumn()
mutation.ColumnOrSupercolumn.Column = col
mutations = append(mutations, mutation)

if len(barcode) > 0 {
var codes *Barcodes = new(Barcodes)
codes.Barcode = append(codes.Barcode, barcode)

col = cassandra.NewColumn()
col.Name = []byte("barcodes")
col.Value, err = proto.Marshal(codes)
if err != nil {
productAddErrors.Add(err.Error(), 1)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
col.Timestamp = now.Unix()
mutation = cassandra.NewMutation()
mutation.ColumnOrSupercolumn = cassandra.NewColumnOrSuperColumn()
mutation.ColumnOrSupercolumn.Column = col
mutations = append(mutations, mutation)
}

uuid, err = GenTimeUUID(&now)
if err != nil {
productAddErrors.Add(err.Error(), 1)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
mmap = make(map[string]map[string][]*cassandra.Mutation)
mmap[string(uuid)] = make(map[string][]*cassandra.Mutation)
mmap[string(uuid)]["products"] = mutations

mutations = make([]*cassandra.Mutation, 0)
col = cassandra.NewColumn()
col.Name = []byte("product")
col.Value = uuid
col.Timestamp = now.Unix()
mutation = cassandra.NewMutation()
mutation.ColumnOrSupercolumn = cassandra.NewColumnOrSuperColumn()
mutation.ColumnOrSupercolumn.Column = col
mutations = append(mutations, mutation)
mmap[prodname] = make(map[string][]*cassandra.Mutation)
mmap[prodname]["products_byname"] = mutations

if len(barcode) > 0 {
mmap[barcode] = make(map[string][]*cassandra.Mutation)
mmap[barcode]["products_bybarcode"] = mutations
}

ire, ue, te, err = self.client.BatchMutate(mmap,
cassandra.ConsistencyLevel_QUORUM)
if ire != nil {
log.Println("Invalid request: ", ire.Why)
productAddErrors.Add(ire.Why, 1)
return
}
if ue != nil {
log.Println("Unavailable")
productAddErrors.Add("unavailable", 1)
return
}
if te != nil {
log.Println("Request to database backend timed out")
productAddErrors.Add("timeout", 1)
return
}
if err != nil {
log.Println("Generic error: ", err)
productAddErrors.Add(err.Error(), 1)
return
}
}
12 changes: 12 additions & 0 deletions productsearch.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
package main

import (
"database/cassandra"
"encoding/json"
"expvar"
"html/template"
Expand Down Expand Up @@ -79,6 +80,7 @@ type ProductSearchForm struct {

type ProductSearchAPI struct {
authenticator *ancientauth.Authenticator
client *cassandra.RetryCassandraClient
scope string
}

Expand Down Expand Up @@ -116,6 +118,16 @@ func (self *ProductSearchAPI) ServeHTTP(w http.ResponseWriter, req *http.Request
var res CategorizedSearchResult

numRequests.Add(1)
numAPIRequests.Add(1)

// Check the user is in the reqeuested scope.
if !self.authenticator.IsAuthenticatedScope(req, self.scope) {
numDisallowedScope.Add(1)
http.Error(w,
"You are not in the right group to access this resource",
http.StatusForbidden)
return
}

if len(query) >= 3 {
var r SearchResult
Expand Down
53 changes: 51 additions & 2 deletions templates/search.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,51 @@

elem.className = "search-query input-xlarge";
elem.removeAttribute("disabled");

$('#add-prodname')[0].value = elem.value;
}
});
}

function AddProduct()
{
var prodname = $('#add-prodname')[0];
var barcode = $('#add-barcode')[0];

// TODO(caoimhe): notify the user about the error somehow.
if (prodname.value.length <= 0)
return;

new Ajax.Request('/api/add-product', {
parameters: {
prodname: prodname.value,
barcode: barcode.value
},
onFailure: function(response) {
var al = document.createElement("div");
var el = document.createElement("button");

al.className = "alert alert-block";
el.setAttribute("type", "button");
el.className = "close";
el.setAttribute("data-dismiss", "alert");
el.innerHTML = "&times;";
al.appendChild(el);

el = document.createElement("h4");
el.appendChild(document.createTextNode("Error adding product"));
al.appendChild(el);

al.appendChild(document.createTextNode("Error fetching data: "));
al.appendChild(document.createTextNode(response.statusText));
document.appendChild(al);
},
onSuccess: function(response) {
prevsearch = '';
prodname.value = '';
barcode.value = '';
$('#addItem').modal('hide');
KeyEvent();
}
});
}
Expand All @@ -238,11 +283,15 @@
<h3 id="addItemLabel">Modal header</h3>
</div>
<div class="modal-body">
asdf
<label for="add-prodname">Product name:</label>
<input type="text" id="add-prodname" title="Please enter a product name here. Make sure it's not a duplicate." />
<br/>
<label for="add-barcode">Example barcode:</label>
<input type="text" id="add-barcode" title="If you have an example barcode, add it here." />
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
<button class="btn btn-primary">Save changes</button>
<button class="btn btn-primary" onclick="AddProduct();">Save changes</button>
</div>
</div>
</body>
Expand Down
Loading

0 comments on commit 769375e

Please sign in to comment.