finally, my first actual project / contract / job built on the app engine! Pretty basic, but the one hurdle I had to face was custom authentication and user management. the client is uncomfortable with using google accounts, and wants a traditional account creation mechanism.
I spent the last 2 days looking at various ways of accomplishing this, spent more than a few hours trying to get nonrel to work to no avail (which is the google endorsed answer to users / sessions). I checked out the older app engine patch, plus a few others, nothing easily implemented for someone coming from 3 weeks or so of python and django. but FINALLY, after 2 days of tinkering and trying, I stumbled pretty blindly into tipfy from a stackoverflow article, which does everything i need and nothing more. Easy to configure and figure out, doesn’t take over the appengine functionality, still lets you use the tools you’re familiar with – appcfg, dev_appserver (yes, I’m sure django nonrel is awesome and I’m missing out enormously and I do intend to learn and hopefully love django… just not today) – plus I had it running in 3 minutes, after a bootstrap and a build. pretty freakin easy, immediate gratification, and off i go…
I have decided that python is awesome. I’ve been writing c# for a living the last 7 years or so, tons of javascript as well, and a large amount of php for personal endeavors, and I have a feeling python is going to be my favorite. If anyone actually reads this, and if by even greater chance you have any insight or suggestions in mastering python and all the little tips and tricks (Middleware? omfg, rad) – please do be a doll and pass them along.
exciting! the Intention Engine is live and available for download, the device app was created with titanium and google app engine runs everything behind the scenes. so far so good, maybe 15 downloads in the last couple hours and no app engine hiccups yet. The app seems to hang on first run, but thats a titanium thing with the 1.4 -> 1.5 upgrade so not much I can do there except beg not to be judged.
a while back I wrote an app i always intended to put into the market, but (heh) kept rewriting the backend to use various storage devices. I’m rewriting it one more time to use the app engine. third times a charm right?
One thing I wanted to do that I didn’t do previously is upload an image from the device to the server, store it, and serve it. I was a little surprised at the nuances needed to accomplish this! nothing too complicated, yet not exactly straightforward, so hear you go.
(titanium dev 1.2.2 + sdk 1.5) + appengine 1.5 (and aptana 3.0 of course)
in titanium:
// you *might* have to wrap this all in a setTimeout(..., 500)
renderedImage = touchView.toImage(); // the view you want to create as an image
var saveImageView = Titanium.UI.createImageView({
top: 0,
left: 0,
width: 'auto',
height: 'auto',
image: renderedImage,
});
mainWin.add(saveImageView);
imageToUpload = saveImageView.toBlob(); // toBlob, not toImage!! important...
var xhr = Titanium.Network.createHTTPClient();
xhr.onload = function() {
mainWin.remove(saveImageView); // remove the temp view...
}
xhr.open('POST', 'http://10.1.10.15:8888/addimage');
xhr.send({
image : imageToUpload
});
in the app engine python:
#the addimage endpoint
class AddImage(webapp.RequestHandler):
def post(self):
image = self.request.get('image')
i = Image()
i.picture = db.Blob(image)
i.put()
self.response.out.write('done');
#the Image object:
class Image(db.Model):
picture = db.BlobProperty();
#to get the image : /getimage?key=sdfsadfsf...
class GetImage(webapp.RequestHandler):
def get(self):
images_query = Image.get(self.request.get('key'))
if (images_query and images_query.picture):
self.response.headers['Content-Type'] = "image/jpeg"
self.response.out.write(images_query.picture)
#to draw the images out to the main page:
class MainPage(webapp.RequestHandler):
def get(self):
images = db.Query(Image)
keys = [];
for image in images:
keys.append(str(image.key()))
template_values = {'images' : keys}
path = os.path.join(os.path.dirname(__file__), 'index.html')
self.response.out.write(template.render(path, template_values))
in the index.html:
<div>
{% for i in images %}
<img src="/getimage?key={{i}}" />
{% endfor %}
</div>
Previously in our adventures, we talked about using various google services for tasks common to sharepoint. While this was a fun little undertaking on my part, I realize that I’m doing you all a huge disservice. What I need to show you is what you CANNOT do with sharepoint, things that you didn’t even imagine you would be able to achieve, while having fun doing it.
Thank you google, for bringing the joy back to development.
Here is the completely free suite of tools you need to have set up and configured to do everything I’m about to do here.
Python is easy, don’t be scared. I hadn’t written python before last week, so there you go. The only thing you really need to know going into this is javascript.
Bi-Directional communication in a nutshell: html5 websockets that enable client <-> server communication. The client does not have to initiate the call to retrieve data, check for changes, etc. The server instead can “push” data to the client. Something happens on the server (workflow) and all clients are notified without having to ask if something happened.
So… what should we build? I want to meet a real business need. I want it to be mobile ready. I want it to be rad and fun to use (which should be a requirement in all applications, but i digress… )
Since I don’t have anyone clamoring for a solution to their specific problem, I’ll have to draw from my own life experience for a real world example. Specifically my non-technical experience. Have I ever mentioned I was once a hooters girl? only for a day. just to be able to say that. But I did bartend for quite a long time, before university… the service industry was very good to me.
In some Utopian world I have yet to find and intend to create, small businesses like pubs and clubs wouldn’t see eachother as competition… to the contrary! they would see eachother as co-conspirators with the common goal of providing the customer with a memorable and pleasant experience. They would collaborate on ways to ensure a good time was had, no matter the location, down to delivery of the blissful person and interested parties to their destination safely.
Here is what we will build.
An app engine powered application which will:
allow for clubs to sign up to be part of the network. They provide their name and address and are provided with a dashboard page that will be their own view of the application.
allow customers to “check in” at any of the locations, and send messages to the locations dashboard
send messages using bi-directional communication channels to all users or to specific users, from the server or from other users.
a little video to see it in action. whats mind boggling is now ridiculously FAST this application is.
#each establishment being added, to be displayed on the map
class Location(db.Model):
name = db.StringProperty()
address = db.PostalAddressProperty()
latitude = db.FloatProperty()
longitude = db.FloatProperty()
person = db.UserProperty()
placekey = db.StringProperty()
#each connection to the application
class Connection(db.Model):
person = db.UserProperty()
channelKey = db.StringProperty()
latitude = db.FloatProperty()
longitude = db.FloatProperty()
to store the locations and connections:
def addLocation(name, address, lat, lon):
user = users.get_current_user()
if(user):
placekey = str(uuid1())
location = Location(key_name = placekey)
location.person = user
location.name = name
location.placekey = placekey
location.address = address
location.latitude = lat
location.longitude = lon
location.put()
return placekey
def addConnection():
user = users.get_current_user()
if user:
userkey = user.user_id()
connection = Connection(key_name = userkey);
connection.person = user
connection.channelKey = userkey;
connection.put();
return userkey;
when the page initializes, the channel is created so we can listen for server events and act upon them. the ‘onMessage’ function will be the entry point for all server messages. depending on the action supplied with the message, different things are done with the data sent through.
openChannel = function() {
var token = '{{ token }}'; // this is a template variable defined in the app.py MainPage endpoint response
var channel = new goog.appengine.Channel(token);
var handler = {
'onopen': onOpened,
'onmessage': onMessage,
'onerror': function() {},
'onclose': onClose
};
var socket = channel.open(handler);
socket.onopen = onOpened;
socket.onmessage = onMessage;
}
when the user clicks “add me”, a little javascript is called –
$('.addMe').unbind().click(function(){
if($('input.name').val()!='' && $('input.address').val()!=''){
// this will add an address to the locations storage and send via socket the new location to draw on the map to ALL the connected users.
sendMessage('/lookupaddress', "address=" +
$('input.address').val() + "&name=" + $('input.name').val());
}
});
// calls back to the app engine listening for /addme
sendMessage = function(path, opt_param) {
path += '?g=' + state.key; // I'm setting this as the userid, but could be anything you want it to be.
if (opt_param) {
path += '&' + opt_param;
}
var xhr = new XMLHttpRequest();
xhr.open('POST', path, true);
xhr.send();
};
the app engine maps the endpoint (lookupaddress) to the receiver (AddAddress):
application = webapp.WSGIApplication([('/', MainPage), #initial landing page
('/handshake', Handshake), #after connection is created, dispatch whatever updates need to be sent to users
('/lookupaddress', AddAddress), #adds a new establishment
('/onmyway', OnMyWay), #notified the establishment that a user is on the way
('/mylocations', MyLocations), #provides the user a view of all their establishments they added
('/closed', ConnectionClosed)],debug=True)
the receiver checks the address and gets the lat / long to display from googles geocoding. if it comes back correctly, the location is stored and an update is sent to all the locations.
class AddAddress(webapp.RequestHandler):
def post(self):
address = self.request.get('address');
name = self.request.get('name');
key = self.request.get('g');
querystring = { 'address' : address, 'sensor' : 'false' }
url = "http://maps.googleapis.com/maps/api/geocode/json?" + urllib.urlencode(querystring)
result = urlfetch.fetch(url)
res = simplejson.loads(result.content)
if(res['results'][0] != None):
lat = res['results'][0]['geometry']['location']['lat']
lon = res['results'][0]['geometry']['location']['lng']
address_formatted = res['results'][0]['formatted_address']
logging.log(logging.INFO, name)
placekey = addLocation(name, address_formatted, lat, lon)
response = {
'action': "display_address_lookup", #action recieved in javascript onMessage
'results' : "congrats! you have successfully added your location.",
'placekey' : placekey
}
channel.send_message(key, simplejson.dumps(response))
#kick off an update to all the users
update = LocationsUpdate()
update.dispatch(placekey)
else:
response = {
'action': "display_address_lookup", #action recieved in javascript onMessage
'results' : "The address could not be found. please check the address and try again."
}
channel.send_message(key, simplejson.dumps(response))
the new location is dispatched to all the connections:
which triggers some javascript to drop the new location marker onto the map, and offer functionality to “check in” at the location and send a message.
onMessage = function(m) {
$m = eval("("+m.data+")");
if(!!$m.action){
switch($m.action){
//...
case "display_locations":
bindLocations($m.locations);
break;
// ...
}
}
}
bindLocations = function(locations){
$(locations).each(function(){
var obj = $.grep( markers, function(n,i){
return n.title == this.title;
});
if(obj.length < 1) {
var placelatlon = new google.maps.LatLng(this.latitude,this.longitude);
var marker = new google.maps.Marker({
position: placelatlon,
title : this.address,
animation: google.maps.Animation.DROP
});
var infowindow = new google.maps.InfoWindow({
content: createLocationForm(this)
});
google.maps.event.addListener(marker, 'click', function() {
infowindow.open(map,marker);
// set up the form for the user to send the location / establishment a message and let them know they're coming.
// this will send a notification to the user who added the location, but noone else.
$('.onMyWayButton').unbind().click(function(){
sendMessage('/onmyway', 'message=' + $('div[placekey='+$(this).attr('placekey')+']').find('textarea').val() + '&placekey=' + $(this).attr('placekey'));
});
});
markers.push(marker);
}
});
$(markers).each(function(idx, marker){
setTimeout(function() {marker.setMap(map);}, idx * 200);
});
}
... and thats it. I'm not going to detail out all of the python / javascript, this should give you the low down about how simple this communication mechanism really is. Ajax but better with this example you were able to send a message from one user to another, and have that message show up instantaneously in the users browser, and you were also able to send a message from the server to all connected users, and have all connected users see that updated location drop into place immediately.
This is SERIOUSLY cool functionality. hope you enjoyed, and I'd love to take this to another level of usability so please comment if theres anything you would like to see. There is so much available in this, I left some other functionality in there as well like querying a folder of documents and displaying the titles.
FYI: all images, buttons, and other graphics were created by yours truly with the gimp (free), on ubuntu (free) with aptana (free) and the google app engine with python (free and free), the desktop recording was done with gtkRecordMyDesktop (free) and encoded with mencoder (free). source control is done via git on github (free and free). are you seeing a pattern yet? there are options people, wonderful, powerful, free, open source, reliable secure and just flat out RAD options. I encourage you to explore
So, you wanna validate forms huh? you wanna do it client side and add a sweet little tooltip the the control with a custom message? well, so did I. I did not wanna write my own though, and couldn’t find a library that gave me exactly what I wanted… so I used 2 of them. You can see the demos and screenshots and further usage / documentation there.
I also don’t like using that pesky form tag that most validation libraries expect you to embrace, I just wanna validate some controls, thats all.
Probably what you want to do is create a common js lib that can be used throughout your app for various common tasks, and add the validity / qtip duo into that common lib: (I also left my date deserialization and formatting functionality in here just because it’s quite handy.)
(function ($) {
$.common = {
//////////////////////////////////////////////////////////
// common validation methods - uses the validity lib
//////////////////////////////////////////////////////////
validateRequired: function (control, message) {
var $this = this;
$.validity.start();
$(control).require(" "); // pass in empty message, we're using the tooltip.
var result = $.validity.end();
if (!result.valid) { // no? open a tooltip.
$(control).select();
$this.openErrorMessage(control, message);
}
return result.valid;
},
validate: function (type, control, message) {
var $this = this;
var valid = false;
$(control).each(function () {
$.validity.start();
$(this).match(type, " "); // pass in empty message, we're using the tooltip.
var result = $.validity.end();
if (!result.valid) { // no? open a tooltip.
$(this).select();
$this.openErrorMessage(this, message);
}
valid = result.valid;
if (!valid) return false;
});
return valid;
},
validateUrl: function (control, message) {
var $this = this;
return $this.validate("url", control, message);
},
validateZip: function (control, message) {
var $this = this;
return $this.validate("zip", control, message);
},
validateUsd: function (control, message) {
var $this = this;
return $this.validate("usd", control, message);
},
validateEmail: function (control, message) {
var $this = this;
return $this.validate("email", control, message);
},
validateSame : function(control, message){
var $this = this;
$.validity.start();
$(control).equal("");
var result = $.validity.end();
if (!result.valid) { // no? open a tooltip.
// -- select the control so the user doesn't have to move the mouse
$(control).select();
// -- use the qtip stuff to display the error message.
$this.openErrorMessage(control[1], message);
}
return result.valid;
},
//////////////////////////////////////////////////////////
// end common validation methods
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
// tooltip functionality - uses the qtip lib
//////////////////////////////////////////////////////////
openErrorMessage: function (control, message) {
$(control).qtip({
content: message,
position: {
corner: {
target: 'rightMiddle',
tooltip: 'leftMiddle'
}
},
style: {
title: { 'font-family:': 'Verdana', 'font-size': '8pt', 'color': '#999' },
width: 125,
border: { width: 1, radius: 3 },
padding: 5,
background: '#eee',
textAlign: 'left',
tip: 'leftMiddle', // Give it a speech bubble tip with automatic corner detection
name: 'red' // because red means bad.
},
show: {
when: false, // Don't specify a show event
ready: true // Show the tooltip when ready
},
hide: {
when: "unfocus"
}
});
},
//////////////////////////////////////////////////////////
// end tooltip functionality
//////////////////////////////////////////////////////////
dateDeserialize: function (dateStr) {
try{
return eval('new' + dateStr.replace(/\//g, ' '));
}catch(err){
return null;
}
},
dateTimeFriendly : function(dateStr){
var dt = this.dateDeserialize(dateStr);
if(dt != null)
return (dt.getMonth() + 1) + '/' +
dt.getDay() + '/' +
dt.getFullYear() + ' ' +
(dt.getHours() == 0 ? "12" :
(dt.getHours() > 12 ? dt.getHours() - 12 : dt.getHours())
) + ":"
+ dt.getMinutes() + " " +
(dt.getHours() > 12 ? " pm" : " am");
else return "";
},
dateFriendly : function(dateStr){
var dt = this.dateDeserialize(dateStr);
if(dt != null)
return (dt.getMonth() + 1) + '/' + dt.getDate() + '/' + dt.getFullYear();
else return "";
}
};
})(jQuery);
usage:
...
_validateUserObject: function (form) {
var valid = $.common.validateRequired($(form).find('.name'), 'name is required please.');
if (!valid) return valid;
valid = $.common.validateRequired($(form).find('.email'), 'email is a required field.');
if (!valid) return valid;
valid = $.common.validateEmail($(form).find('.email'), 'this email address appears to be incorrect.');
return valid;
},
...
// -- this is the pattern i use for process:
// -- bind add user button click
// -- open form modal
// -- validate input
// -- get object from form modal
// -- send object to service via ajax
// -- callback on success
$('.addUser').unbind().bind('click', function () {
// write clearUserObject yourself to empty any data out of the form that may have been previously added.
$this._clearUserObject($('#newUserDlg'));
// -- open the form dialog
// write openFormDialog yourself - mine opens the form in a jquery modal with a ok, i pass it the
// form obj and the save click callback
$.common.openFormDialog($('#newUserDlg'),
// on save button clicked
function (form) {
// is it valid?
if ($this._validateUserObject(form)) {
// -- get the user object from the form
// write getUserObject yourself to extract the form fields into
// whatever object your service is waiting to receive
// return { name : $(form).find('.name').val(), email : $(form).find('.email').val(), ... };
var user = $this._getUserObject(form);
// save the user
// write addUser ajax functionality yourself ..
$.ajaxstuff.addUser(user,
// -- success!
function (result) {
// -- do something awesome, like reload the view or tell the user
// -- that the button they clicked actually did something. close dialogs and the like.
// -- you wanna handle the error that "wont ever happen" anyway tho, in teh common
// -- ajax functionality that you are of course using a different jquery plugin for...
});
}
});
});
...
Workflow. A word which triggers images of process, flowcharts, automation, and visions of yourself watching things happen instead of making things happen. I can almost hear the chanting at the last moss conference… what do we need? workflow! why do we need it? … silence….
The word needs to be dissected. work. flow. work is far too vague a word. flow is something that you wanna go with, not against. the concept is more like process automation, or automated decision engine, or data notification, or a combination of these things. workflow removes some human interaction, but should be able to communicate the outcome of the automated process, hopefully based on some rules that can be defined by the human being replaced by the automation.
workflow and lists go hand in hand in the world of moss. It’s actually quite a problem solver, but it still feels clunky to the user. An item gets added to a list, via manual entry or from a form or what have you. workflow defines what happens when that data is added to the list, theres a listener in there waiting for it to happen, and kick off an action. that action could be a request for human input, a notification, or another automated process, which would trigger another action, etc.
This is the tutorial I’m going to replicate with the google apps scripts today. There weren’t a lot to choose from, so I apologize for the length and sheer amount of steps to read through.
Exercise Duration : 20 minutes
I *seriously* doubt that. But, ok.
Dissected functionality from the tutorial: When an item is added to a list, create a document library (folder) using the data entered by the user as the name of the new document library. This workflow should be reusable: ie – you can attach it to any list.
Personally, I think this is a depressingly useless workflow. Are you excited yet?
How to do this with google scripts
step 1:
if you haven’t already, open up google docs. click “create new” -> “form”. This form feeds into a spreadsheet that is created for you based on the questions you provide in the form, (which in itself is basically the tutorial we’re replicating here, but i digress… )
notice the bottom of the screen, the link to the published form to have a looksee at what its going to look like to people filling it out, you can even try it. there is a link at the top to “see responses”, click that and select -> spreadsheet. it’ll take you over to the spreadsheet that stores all the data for the customer form. open tools -> scripts -> script editor and lets begin.
step 2:
First: rename the default “MyFunction” function in the script editor to “CreateFolderFromCustomerNameEntered” and then *save the script with a name and or description*. This is important because until you do, trying to set the trigger on the script will prompt you with that name and description box, and not open the “create trigger” menu, which may be confusing. Set the trigger like so (more on events and triggers):
notice the notifications options? yes, theres more to be explored here, feel free.
Next: back in the script editor, replace everything currently there with this huge chunk of code:
function CreateFolderFromCustomerNameEntered(e) {
var company = e.values[1];
var folder = DocsList.createFolder(company + " Documents (created from form)");
}
and save.
to test:
1. open 3 tabs – the form, the spreadsheet, and the view of your doc list.
2. fill out the form. look at the other tabs. notice you didn’t have to refresh the spreadsheet? ooh, eerie…
to reuse the workflow:
in the customers spreadsheet, go to file: make a copy. Call it “More Customers” or something. In the resulting “More Customers” spreadsheet, open Tools -> Scripts -> Manage. select the “CreateFolderFromCustomerNameEntered” and repeat the part about setting the trigger to fire on form submit to the spreadsheet.
to make it more awesome:
take the 15 minutes you have left and make it do some other cool things, like email the owner, or make a soap call…
I was getting an error trying to run the google app engine, AttributeError: ‘module’ object has no attribute ‘ssl’ – fixed that then got another that AttributeError: ‘module’ object has no attribute ‘sqlite’
had to get sqllite – configure, make, make install
Download python2.5, extract, edit Modules/Setup.dist, uncomment the lines pertaining to ssl:
204:# Socket module helper for SSL support; you must comment out the other
205:# socket line above, and possibly edit the SSL variable:
206:SSL=/usr/local/ssl
207:_ssl _ssl.c
208: -DUSE_SSL -I$(SSL)/include -I$(SSL)/include/openssl
209: -L$(SSL)/lib -lssl -lcrypto
make sure that Modules/Setup has these changes too.
recently I played around with the google docs and scripting API and the conclusion that I bet a lot people have… awesome but what do i do with it? With the Sharepoint background that I have, I feel it is my personal duty to describe to you how to accomplish the tasks most often attempted with sharepoint, except with the google scripting api.
In my years upon years of experience with creating custom applications and functionality inside sharepoint for a multitude of various clients, all with disparate needs and processes, one of the most powerful and misunderstood pieces of functionality I implemented was the bdc ( business data catalog, now called bcs ( business connectivity services )), to allow a database full of old LOB data to be exposed a nice ui that it can be searched and filtered upon.
Most of the linked tutorial is explanation about the intricacies of the BDC / BCS data model and connection / crawling functionality, and what it needs to survive – finder methods, idenumerator, lastseenid, etc. We can safely ignore most of this and just get on to the meat of the query – the xml file that defines the hook into the database, what it queries, what the parameters are.
I don’t know about you, but I got tired just reading all through that tutorial. from what i saw in the xml and sql in that metadata definition, there are 2 columns being selected from the adventureworks db:
SELECT TOP 1000 SalesOrderId, ModifiedDate FROM [Sales].[SalesOrderHeader] ORDER BY SalesOrderId ASC
The selection is done via the sales order id parameter. the mysql equivalent to that would be:
SELECT salesorderid, modifieddate
FROM salesorderheader
ORDER BY SalesOrderId ASC
LIMIT 1000
Thats not enough data to really be of any interest…
how about this query instead?
select `ModifiedDate`,`SalesOrderID`,`OrderDate`, `Status`, `PurchaseOrderNumber`, `SubTotal`, `TaxAmt`, `Freight`, `TotalDue` from `salesorderheader` order by `salesorderheader`.`OrderDate` desc limit 1000
I log into google docs, and create a new spreadsheet. in that spreadsheet view, I select tools -> scripts -> and open the script editor. Yes, that’s javascript. are you smiling yet? I sure was.
check this out – this is the javascript to create a user input form, read from a remote database, and populate the spreadsheet with the result. Put this in your script editor and run it with your own db settings of course…
function OpenSearchForm() {
var doc = SpreadsheetApp.getActiveSpreadsheet();
var app = UiApp.createApplication().setTitle('search by sales order');
var grid = app.createGrid(1, 2);
grid.setWidget(0, 0, app.createLabel('Sales Order:'));
grid.setWidget(0, 1, app.createTextBox().setName('salesOrderId').setId('salesOrderId'));
var panel = app.createVerticalPanel();
panel.add(grid);
var buttonPanel = app.createHorizontalPanel();
var button = app.createButton('submit');
var submitHandler = app.createServerClickHandler('submit');
submitHandler.addCallbackElement(grid);
button.addClickHandler(submitHandler);
buttonPanel.add(button);
var closeButton = app.createButton('close');
var closeHandler = app.createServerClickHandler('close');
closeButton.addClickHandler(closeHandler);
buttonPanel.add(closeButton);
var statusLabel = app.createLabel().setId('status').setVisible(false);
panel.add(statusLabel);
panel.add(buttonPanel);
app.add(panel);
doc.show(app);
// set up the spreadsheet headers
var cell = doc.getRange('a1').offset(0, 0);
cell.setValue("Modified Date");
cell.offset(0, 1).setValue("Sales Order Id");
cell.offset(0, 2).setValue("Order Date");
cell.offset(0, 3).setValue("Status");
cell.offset(0, 4).setValue("PurchaseOrderNumber");
cell.offset(0, 5).setValue("SubTotal");
cell.offset(0, 6).setValue("Tax Amount");
cell.offset(0, 7).setValue("Freight");
cell.offset(0, 8).setValue("Total Due");
}
// Close everything return when the close button is clicked
function close() {
var app = UiApp.getActiveApplication();
app.close();
// The following line is REQUIRED for the widget to actually close.
return app;
}
// function called when submit button is clicked
function submit(e) {
var doc = SpreadsheetApp.getActiveSpreadsheet();
var lastRow = doc.getLastRow();
var sid = e.parameter.salesOrderId;
var conn = Jdbc.getConnection("jdbc:mysql://12.23.34.45:3306/adventureworks", "dbuser", "password");
var stmt = conn.createStatement();
stmt.setMaxRows(100);
var rs = stmt.executeQuery("select `ModifiedDate`,`SalesOrderID`,`OrderDate`, `Status`, `PurchaseOrderNumber`, `SubTotal`, `TaxAmt`, `Freight`, `TotalDue` from `salesorderheader` where `PurchaseOrderNumber` is not null and `SalesOrderNumber` like '"+sid+"' order by `salesorderheader`.`OrderDate` desc limit 1000");
while(rs.next()) {
var cell = doc.getRange('a2').offset(lastRow, 0);
cell.setValue(rs.getString("ModifiedDate"));
cell.offset(0, 1).setValue(rs.getString("SalesOrderId"));
cell.offset(0, 2).setValue(rs.getString("OrderDate"));
cell.offset(0, 3).setValue(rs.getString("Status"));
cell.offset(0, 4).setValue(rs.getString("PurchaseOrderNumber"));
cell.offset(0, 5).setValue(rs.getString("SubTotal"));
cell.offset(0, 6).setValue(rs.getString("TaxAmt"));
cell.offset(0, 7).setValue(rs.getString("Freight"));
cell.offset(0, 8).setValue(rs.getString("TotalDue"));
lastRow = lastRow+1;
}
rs.close();
stmt.close();
conn.close();
var app = UiApp.getActiveApplication();
app.getElementById('salesOrderId').setValue('');
app.getElementById('status').setVisible(true).setText('last search: ' + e.parameter.salesOrderId + '. search another order or close to exit.');
return app;
}
and…. you’re done. you just connected your LOB data into a spreadsheet inside google docs. it was *that* easy. Search for the order from the bcs tutorial – SO43659:
or, even better, search for a wildcard like so: SO43% and get back a whole result set that you can then filter on! then you can just insert a graph gadget and get a visual representation in a lot of pretty colors for the size (total) of each purchase order. Right?
then you just import that form into your google sites page, and bam, instant lob data and reporting. *glee*
finally, my first actual project / contract / job built on the app engine! Pretty basic, but the one hurdle I had to face was custom authentication and user management. the client is uncomfortable with using google accounts, and wants a traditional account creation mechanism. I spent the last 2 [...]
exciting! the Intention Engine is live and available for download, the device app was created with titanium and google app engine runs everything behind the scenes. so far so good, maybe 15 downloads in the last couple hours and no app engine hiccups yet. The app seems to hang on [...]
a while back I wrote an app i always intended to put into the market, but (heh) kept rewriting the backend to use various storage devices. I’m rewriting it one more time to use the app engine. third times a charm right? One thing I wanted to do that I [...]
Previously in our adventures, we talked about using various google services for tasks common to sharepoint. While this was a fun little undertaking on my part, I realize that I’m doing you all a huge disservice. What I need to show you is what you CANNOT do with sharepoint, things [...]
So, you wanna validate forms huh? you wanna do it client side and add a sweet little tooltip the the control with a custom message? well, so did I. I did not wanna write my own though, and couldn’t find a library that gave me exactly what I wanted… so [...]
Workflow. A word which triggers images of process, flowcharts, automation, and visions of yourself watching things happen instead of making things happen. I can almost hear the chanting at the last moss conference… what do we need? workflow! why do we need it? … silence…. Click here to get to [...]
I was getting an error trying to run the google app engine, AttributeError: ‘module’ object has no attribute ‘ssl’ – fixed that then got another that AttributeError: ‘module’ object has no attribute ‘sqlite’ to fix both – had to get openssl – configure, make, make install (check out this link) had [...]
recently I played around with the google docs and scripting API and the conclusion that I bet a lot people have… awesome but what do i do with it? With the Sharepoint background that I have, I feel it is my personal duty to describe to you how to accomplish [...]