tag:blogger.com,1999:blog-87559261508139519462009-06-10T12:48:39.057+02:00App Engine GuyA blog about one guys quest to learn Python, Django and Google App Engine.Mattias Johanssonnoreply@blogger.comBlogger22125tag:blogger.com,1999:blog-8755926150813951946.post-65860212744104782832008-12-28T10:10:00.004+01:002008-12-28T10:33:11.362+01:00HTTP response was too largeIn case you run into this error:<div><span class="Apple-style-span" style="font-weight: bold;">HTTP response was too large: 3410251. The limit is: 1048576.</span></div><div>(The number 3410251 above might vary completely arbitrarily)</div><div> </div><div><a href="http://aralbalkan.com/1434">Solutions like this won't help you</a>, because you know for a fact that you have no file that large or anything that could reasonably cause it, then it might be because you have a maximum recursion depth reached error. This causes an enourmous stack trace which will exceed Google App Engines maxiumum response length (1MB). Try sweeping your code for any place where infinite recursion might occur.</div><div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8755926150813951946-6586021274410478283?l=appengineguy.com'/></div>Mattias Johanssonnoreply@blogger.com4tag:blogger.com,1999:blog-8755926150813951946.post-31716111142374944222008-08-18T07:15:00.003+02:002008-08-18T07:19:23.932+02:00Serving static files with Django and AWS - going fast on a budget<p>Found this little gem linked from TechCrunch today. </p> <p><a href="http://eventseer.net/p/thomas_brox_roest/whiteboardentry/13/">Serving static files with Django and AWS - going fast on a budget</a></p> <blockquote>Speed matters. When Google tried adding 20 additional results to their search pages, traffic dropped by 20%. The reason? Page generation took an extra .5 seconds. This article will show how Eventseer utilizes an often overlooked way of improving the responsiveness of a web application: Pre-generating and serving static files instead of dynamic pages.</blockquote><div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8755926150813951946-3171611114237494422?l=appengineguy.com'/></div>Mattias Johanssonnoreply@blogger.com4tag:blogger.com,1999:blog-8755926150813951946.post-49709727095434922042008-08-11T02:28:00.003+02:002008-08-11T02:35:00.301+02:00New Django Helper<p class="inledning">I've been way too hard at work on SlurpBOX to notice, but the fine App Engine Helper for Django guys have made a new release! See the top item in the release notes? I found that bug! :P <a href="http://code.google.com/p/google-app-engine-django/">Go download.</a></p> <p> <p>Release notes<br /> Wed 6 August 2008<br /> =================<br /> </p> <p>This is the last version of the Google App Engine Helper for Django that will support Django 0.96. Future development of the helper will be targetted for the upcoming 1.0 release of Django.</p> <ul> <li>Improved SDK detection on Windows by looking at both the PATH variable that may be set by the installer and using the win32api module (if available) to look for the SDK in the default Program Files location.</li> <li>Replaced the startapp command with a version that installs an App Engine Compatible application skeleton. Patch contributed by Andi Albrecht.</li> <li>Changed the default runserver port to 8000 to match standard Django behaviour. Path contributed by Waldemar Kornewald</li> <li>Email server settings from the Django settings file are provided to the App Engine Mail API. Patch contributed by Waldemar Kornewald</li> <li>Added support for the Django memcache cache backend. Patch contributed by Jonca Rafal.</li> <li>Added support for the Django session middle with db and cache backends for Django 1.0alpha only. Patches contributed by Jonca Rafal and Waldemar Kornewald</li> <li>Moved the Django compatible login_required decorator to the standard Django location. Patch contributed by Andi Albrecht</li> <li>Replaced the Django ModelForm class with the App Engine ModelForm class</li> <li>Added a repr implementation for the BaseModel class</li> <li>Many minor improvements to increase robustness and avoid errors if portions of Django are not present.</li> </ul> </p><div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8755926150813951946-4970972709543492204?l=appengineguy.com'/></div>Mattias Johanssonnoreply@blogger.com6tag:blogger.com,1999:blog-8755926150813951946.post-54121172168481789152008-08-08T15:12:00.002+02:002008-08-08T15:16:21.877+02:00My first Real App Engine App<p><br /><br /><br /><br />Hey folks! Check out my first real App Engine application, <a href="http://appgallery.appspot.com/about_app?app_id=agphcHBnYWxsZXJ5chMLEgxBcHBsaWNhdGlvbnMYvyEM">SlurpBOX</a>! Please check it out, and offer suggestions on how to improve it!</p><div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8755926150813951946-5412117216848178915?l=appengineguy.com'/></div>Mattias Johanssonnoreply@blogger.com5tag:blogger.com,1999:blog-8755926150813951946.post-48786837618789839762008-08-01T16:47:00.004+02:002008-08-04T17:26:21.469+02:00Encoding/unicode reminders to myself and others<div class="inledning"><div class="inledningInner">Encoding is, along with Regular Expressions, one of those subjects I never seem to properly learn. Here are a few pointers to my future self whenever I run into a pesky UnicodeEncodeError.</div></div> <ol> <li>Read <a href="http://www.joelonsoftware.com/articles/Unicode.html">this article</a> for the umtillionth time. This might be the time it "clicks" for you.</li> <li>Don't get fancy with ISO-8859-1. Just use UTF-8. App Engine hates everything that is UTF-8.</li> <li>Make sure your text editor saves the files using UTF-8 encoding.</li> <li>Make sure that the <?xml tag at the top of your XML documents specifies that the file uses UTF-8.<li> <li>Put <b># coding=UTF-8</b> at the top of every .py file</li> <li>Put a <b>u</b> in front of every string that contains non-ascii chars. Like this u"Jag är en liten hatt och är bög"</li> <li>When handling incoming requests, make sure you set the encoding correctly using request.encoding = "UTF-8" in your view</li> <li>Use ugettext as your alias for _</li> <li>If a method (such as quote() or hashlib.sha224() requires bytestrings as input, encode your unicode strings first - like this: theunicodestring.encode("utf-8")</li> </ol><div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8755926150813951946-4878683761878983976?l=appengineguy.com'/></div>Mattias Johanssonnoreply@blogger.com7tag:blogger.com,1999:blog-8755926150813951946.post-17236294766580566862008-07-29T01:30:00.006+02:002008-07-29T01:59:46.449+02:00Internationalization problems<p>Just ran into a few issues when trying to get internationalization working - these are tips I really could have used.</p> <p> First, you need to create a directory called <b>locale</b> in your django application dir (i.e. the directory where you keep the views.py file). </p> <p>To create the language files, you need to run <b>django-admin.py makemessages</b> from your django app dir. When I ran this, I got this:</p> <p><b>Error message: makemessages is an unknown command on django-admin.py</b><br /> Took me a while to figure this one out - this can occur if django is not installed - i.e if you just have it in your app engine dir, it won't cut it. You need to run <b>setup.py install</b> to sort it out. Bastard. </p> <p>You need to have <b>gettext</b> on your system for the localization to work. Unix systems have this per default, but Windows does not, and getting it to work involves a frickin' moon process. This is what you need to do:</p> <br /><br /> <ol> <li>Download and unzip gettext into some directory.</li> <li>Add the /bin subdirectory of that directory to your path.</li> <li>Download iconv.dll and slap it into the bin dir.</li> <li>Download intl.dll (I have no idea what this is) and slap it into the bin dir.</li> </ol> <p>Sorry for this jumbled up post - it's written at 2AM after fighting with this for way too long.</p><div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8755926150813951946-1723629476658056686?l=appengineguy.com'/></div>Mattias Johanssonnoreply@blogger.com2tag:blogger.com,1999:blog-8755926150813951946.post-53689020678675835932008-07-22T13:47:00.002+02:002008-07-22T13:51:48.073+02:00Gist: Hosted version control of code snippets<p>Okay, this is just seriously cool. GitHub (hosting provider for the open source version control system <a href="http://git.or.cz/">git</a>) has just launched <a href="http://gist.github.com"><b>Gist</b></a> - a version control system for little snippets of code. I'll be trying it out on my code examples in the blog, henceforth.</p><div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8755926150813951946-5368902067867583593?l=appengineguy.com'/></div>Mattias Johanssonnoreply@blogger.com6tag:blogger.com,1999:blog-8755926150813951946.post-38887073054515230972008-07-16T15:09:00.024+02:002008-07-21T16:25:42.186+02:00How to do AVG and SUM in Google App Engine Data Store<div class="inledning"><div class="inledningInner">People who are used to relational databases, which is pretty much every gosh-darned web developer out there, will run into pretty much the same obstacles with the app engine datastore - one of them is <b>How the heck do I do SUM or AVG?</b>. Yeah, due to how the Data Store works - you cannot do any kind of aggregate query. Instead, you have to re-calculate the totals at write time and keep them in a Counter instead. Like this:</div></div> <p> <b>models.py</b><br /> <textarea name="code" class="py"> from appengine_django.models import BaseModel from google.appengine.ext import db from google.appengine.ext.db import NotSavedError from decimal import Decimal, getcontext class Callable: # this is a class that wraps a method so that we can call it like a static method, directly on the Class, instead of on an instance. def __init__(self, anycallable): self.__call__ = anycallable class GlobalCounter(BaseModel): name = db.StringProperty(required=True) value = db.IntegerProperty(required=True) class Rating(BaseModel): user = db.UserProperty(required=True) rating = db.IntegerProperty(required=True) # hide this oldRating = 0 def set_rating(self, val): self.oldRating = self.rating self.rating = val def put(self): self.save() def save(self): # get the global counters from the datastore. votes = GlobalCounter.all().filter("name = ", "numberOfVotes").get() total = GlobalCounter.all().filter("name = ", "totalAmountOfVotes").get() if (votes == None): votes = GlobalCounter(name = "numberOfVotes", value = 0) votes.save() if (total == None): total = GlobalCounter(name = "totalAmountOfVotes", value = 0) votes.save() # check wheter the rating is a new rating, or if the user is changing his vote. isSaved = False try: self.key() isSaved = True except NotSavedError: isSaved = False if (isSaved): # the user is re-saving (and possibly changing) his vote - subtract his old vote from the total, and add his new vote total.value = total.value - self.oldRating + self.rating else: votes.value += 1 total.value += self.rating votes.save() total.save() BaseModel.save(self) def ___getAverage___(): votesCounter = GlobalCounter.all().filter("name = ", "numberOfVotes").get() totalCounter = GlobalCounter.all().filter("name = ", "totalAmountOfVotes").get() getcontext().prec = 3 return Decimal(str(totalCounter.value)) / Decimal(str(votesCounter.value)) average = Callable(___getAverage___) # make average() behave like a static variable def ___getTotal___(): totalCounter = GlobalCounter.all().filter("name = ", "totalAmountOfVotes").get() return totalCounter.value sum = Callable(___getTotal___) # make average() behave like a static variable </textarea> </p> <p>And you use it like this...</p> <p> <b>tests.py</b><br /> <textarea name="code" class="py"> import os from decimal import Decimal, getcontext #import the stubs, i.e. the fake datastore, user and mail service and urlfetch from google.appengine.api import apiproxy_stub_map from google.appengine.api import datastore_file_stub from google.appengine.api import mail_stub from google.appengine.api import urlfetch_stub from google.appengine.api import user_service_stub from google.appengine.api import users # import the Django Forms replacement provided by the App Engine SDK, so that Django Forms get Data Store model support. from google.appengine.ext.db.djangoforms import ModelForm # I use Python Mocker to create mock objects: http://labix.org/mocker import mocker from mocker import MockerTestCase # importing our Model from avgtest.models import Rating from avgtest.models import GlobalCounter class TestStart(MockerTestCase): def setUp(self): # Start with a fresh api proxy. apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap() # Use a fresh stub datastore. # From this point on in the tests, all calls to the Data Store, such as get and put,, # will be to the temporary, in-memory datastore stub. stub = datastore_file_stub.DatastoreFileStub(u'myTemporaryDataStorage', '/dev/null', '/dev/null') apiproxy_stub_map.apiproxy.RegisterStub('datastore_v3', stub) # Use a fresh stub UserService. apiproxy_stub_map.apiproxy.RegisterStub('user', user_service_stub.UserServiceStub()) os.environ['AUTH_DOMAIN'] = 'gmail.com' os.environ['USER_EMAIL'] = 'myself@appengineguy.com' # set to '' for no logged in user os.environ['SERVER_NAME'] = 'fakeserver.com' os.environ['SERVER_PORT'] = '9999' # Use a fresh urlfetch stub. apiproxy_stub_map.apiproxy.RegisterStub('urlfetch', urlfetch_stub.URLFetchServiceStub()) # Use a fresh mail stub. apiproxy_stub_map.apiproxy.RegisterStub('mail', mail_stub.MailServiceStub()) # Mock all calls to HttpResponseRedirect self.HttpResponseRedirect = self.mocker.replace("django.http.HttpResponseRedirect") # Mock all calls to the render_to_response shortcut self.render_to_response = self.mocker.replace("django.shortcuts.render_to_response") def testAverage(self): rA = Rating(user = users.get_current_user(), rating = 1) rA.save() self.assertEquals(1, Rating.average()) rB = Rating(user = users.get_current_user(), rating = 4) rB.put() self.assertEquals(Decimal("2.5"), Rating.average()) rC = Rating(user = users.get_current_user(), rating = 2) rC.save() self.assertEquals(Decimal("2.33"), Rating.average()) def testSum(self): rA = Rating(user = users.get_current_user(), rating = 2) rA.save() rB = Rating(user = users.get_current_user(), rating = 4) rB.put() rC = Rating(user = users.get_current_user(), rating = 3) rC.save() self.assertEquals(9, Rating.sum()) def testAverageChangeVote(self): rA = Rating(user = users.get_current_user(), rating = 1) rA.save() self.assertEquals(1, Rating.average()) rB = Rating(user = users.get_current_user(), rating = 4) rB.put() self.assertEquals(Decimal("2.5"), Rating.average()) rC = Rating(user = users.get_current_user(), rating = 2) rC.save() self.assertEquals(Decimal("2.33"), Rating.average()) rC.set_rating(3) # the C user changes his mind to 3 instead. rC.save() self.assertEquals(Decimal("2.67"), Rating.average()) </textarea> </p> <p><b>Performance note:</b> It is a slightly bad idea to do counters like I have done above, but in my defence, I have done it for simplicity. In practice, it's important that you <a href="http://blog.appenginefan.com/2008/06/efficient-global-counters.html">shard your counters</a>.</p> <p>As always, comments and questions are always welcome!</p><div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8755926150813951946-3888707305451523097?l=appengineguy.com'/></div>Mattias Johanssonnoreply@blogger.com15tag:blogger.com,1999:blog-8755926150813951946.post-31117581622185206762008-06-30T12:14:00.015+02:002008-07-20T12:24:06.324+02:00Proper Unit Testing of App Engine/Django<div class="inledning">While there are a couple of things written on unit testing with App Engine, I unfortunately found them all lacking in one way or another. With this post, I'd like to describe <strong>how to unit test Django views in App Engine</strong>, using as little mocking as possible.</div> <p>More specifically, I did not want to mock the Data Store Model objects, since that very quickly turns into mocking hell - you just end up spending three times as much time thinking about mocking than you are about code. Fortunately, after quite a bit of digging, I discovered that Google provides a <b>temporary, in-memory datastore stub, specifically made for unit testing</b>! They also provide stubs for mail, urlfetch and the user service. MAN that is nice!</p> <p>For this example, I am using:<br /> <ul> <li>App Engine SDK 1.1</li> <li><a href="http://code.google.com/appengine/articles/appengine_helper_for_django.html">Django Helper r30</a></li> <li><a href="http://appengineguy.com/2008/06/how-to-check-out-latest-version-of.html">Django 0.97</a></li> </ul> </p> <p>We will be using the Test Runner provided by Django. Django looks for tests in a number of places, one is <b>tests.py</b> - which it expects to be in same folder as models.py and views.py. To have django run your tests you go:</p> <code>manage.py test nameofYourApp</code> <h3>The code</h3> <p> <b>models.py</b><br /> <textarea name="code" class="py"> from appengine_django.models import BaseModel from google.appengine.ext import db class Address(BaseModel): user = db.UserProperty(required=True) name = db.StringProperty(verbose_name="Full name",required=True) addressLine1 = db.StringProperty(verbose_name="Address Line 1",required=True) addressLine2 = db.StringProperty(verbose_name="Address Line 2",required=True) zip = db.StringProperty(verbose_name="Zip code",required=True) city = db.StringProperty(required=True) country = db.StringProperty(required=True) </textarea> </p> <p> <b>tests.py</b><br /> <textarea name="code" class="py"> import os #import the stubs, i.e. the fake datastore, user and mail service and urlfetch from google.appengine.api import apiproxy_stub_map from google.appengine.api import datastore_file_stub from google.appengine.api import mail_stub from google.appengine.api import urlfetch_stub from google.appengine.api import user_service_stub from google.appengine.api import users # import the Django Forms replacement provided by the App Engine SDK, # so that Django Forms get Data Store model support. from google.appengine.ext.db.djangoforms import ModelForm # I use Python Mocker to create mock objects: http://labix.org/mocker import mocker from mocker import MockerTestCase from views import address # importing our view from santastormsite.models import Address # importing our Model class TestStart(MockerTestCase): def setUp(self): # Start with a fresh api proxy. apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap() # Use a fresh stub datastore. # From this point on in the tests, all calls to the Data Store, such as get and put, # will be to the temporary, in-memory datastore stub. stub = datastore_file_stub.DatastoreFileStub(u'myTemporaryDataStorage', '/dev/null', '/dev/null') apiproxy_stub_map.apiproxy.RegisterStub('datastore_v3', stub) # Use a fresh stub UserService. apiproxy_stub_map.apiproxy.RegisterStub('user', user_service_stub.UserServiceStub()) os.environ['AUTH_DOMAIN'] = 'gmail.com' os.environ['USER_EMAIL'] = 'myself@appengineguy.com' # set to '' for no logged in user os.environ['SERVER_NAME'] = 'fakeserver.com' os.environ['SERVER_PORT'] = '9999' # Use a fresh urlfetch stub. apiproxy_stub_map.apiproxy.RegisterStub('urlfetch', urlfetch_stub.URLFetchServiceStub()) # Use a fresh mail stub. apiproxy_stub_map.apiproxy.RegisterStub('mail', mail_stub.MailServiceStub()) # Mock all calls to HttpResponseRedirect self.HttpResponseRedirect = self.mocker.replace("django.http.HttpResponseRedirect") # Mock all calls to the render_to_response shortcut self.render_to_response = self.mocker.replace("django.shortcuts.render_to_response") def testAddress(self): # setting the mocker to a shorthand variable. m = self.mocker # Making sure the current user has no address. # This is not necessary for a good unit test, but it makes the test more descriptive. addressOfCurrentUser = Address.all().filter("user = ", users.get_current_user()).get() self.assertTrue(addressOfCurrentUser == None) # create a fake httprequest. mockRequest = self.mocker.mock() # create a fake collection that we will use to fake the POST variables. mockPostCollection = self.mocker.mock() mockRequest.POST # expect the tested code to call the POST collection m.count(1, None) # expect it to call it one or more times m.result(mockPostCollection) #make it return the fake collection # expect the tested code to get some variables from the faked collection mockPostCollection.get("name", None) m.result("Mattias Johansson") mockPostCollection.get("addressLine1", None) m.result("Phat Cool Street 5") mockPostCollection.get("addressLine2", None) m.result("Park Lane") mockPostCollection.get("zip", None) m.result("361.12-1") mockPostCollection.get("city", None) m.result("New York") mockPostCollection.get("country", None) m.result("Hatland") #expect the tested code to do a redirect. self.HttpResponseRedirect("/profile-was-saved") # okay, we are done setting up our expectations! self.mocker.replay() # call the view! address(mockRequest) #make sure that an Address entity was created and properly saved. retrAddress = Address.all().filter("user = ", users.get_current_user()).get() self.assertEquals("Mattias Johansson", retrAddress.name) self.assertEquals("Phat Cool Street 5", retrAddress.addressLine1) self.assertEquals("Park Lane", retrAddress.addressLine2) self.assertEquals("361.12-1", retrAddress.zip) self.assertEquals("New York", retrAddress.city) self.assertEquals("Hatland", retrAddress.country) def testAddressAddressExists(self): # If a logged in user navigates directly to /address, # his existing address should be displayed in the form. m = self.mocker # Create an address for the user. sampleAddress = Address(user = users.get_current_user(), name = "John Wayne", addressLine1 = "Banana Street 13", addressLine2 = "Wanana",zip = "55555", city = "Woogaboo City", country = "Oompaloompaland" ) sampleAddress.put() # fake the request and POST, as in the test above, but this time, return None. mockRequest = self.mocker.mock() mockRequest.POST m.result(None) # This is the function that gets called instead of the real, actual render_to_response. def checkArgumentsOfRenderToResponse(path, params): containsCorrectName = (params['form']['name'].as_text().count("John Wayne") == 1) self.assert_(containsCorrectName, "name field had incorrect data.") containsCorrectAddressLine1 = (params['form']['addressLine1'].as_text().count("Banana Street 13") == 1) self.assert_(containsCorrectAddressLine1, "address line 1 field had incorrect data.") containsCorrectAddressLine2 = (params['form']['addressLine2'].as_text().count("Wanana") == 1) self.assert_(containsCorrectAddressLine2, "address line 2 field had incorrect data.") containsCorrectZip = (params['form']['zip'].as_text().count("55555") == 1) self.assert_(containsCorrectZip, "zip field had incorrect data.") containsCorrectCity = (params['form']['city'].as_text().count("Woogaboo City") == 1) self.assert_(containsCorrectCity, "city field had incorrect data.") containsCorrectCountry = (params['form']['country'].as_text().count("Oompaloompaland") == 1) self.assert_(containsCorrectCountry, "country field had incorrect data.") return '<html/>' # expect a call to render_to_response. We expect ANY parameters here, # since checkArgumentsOfRenderToResponse will be testing the input. self.render_to_response(mocker.ANY, mocker.ANY) # forward the render_to_response call to checkArgumentsOfRenderToResponse m.call(checkArgumentsOfRenderToResponse) #okay, we are done recording our expectations. m.replay() # call the view! address(mockRequest) retrAddress = Address.all().filter("user = ", users.get_current_user()).get() self.assertTrue("Mattias Johansson", retrAddress.name) def testAddressNotLoggedInRedirect(self): # fake the request mockRequest = self.mocker.mock() os.environ['USER_EMAIL'] = '' self.assertTrue(users.get_current_user() == None) # The user is now None # Expect a redirect to login. self.HttpResponseRedirect(users.create_login_url("/address")) #okay, we are done recording our expectations. self.mocker.replay() # call the view! address(mockRequest) </textarea> </p> <p> <b>views.py</b><br /> <textarea name="code" class="py"> from django.http import HttpResponse from django.http import HttpResponseRedirect from django.shortcuts import render_to_response from google.appengine.api import users from models import * from google.appengine.ext.db.djangoforms import ModelForm class AddressForm(ModelForm): class Meta: model = Address exclude = ("user") def address(request): if not users.get_current_user(): return HttpResponseRedirect(users.create_login_url("/address")); currentUser = users.get_current_user() addressOfCurrentUser = Address.all().filter("user = ", currentUser).get() if request.POST: if not addressOfCurrentUser: # assign dummy values to required fields addressOfCurrentUser = Address(user = users.get_current_user(), name = "#", addressLine1 = "#", addressLine2 = "#",zip = "#", city = "#", country ="#" ) form = AddressForm(request.POST, instance=addressOfCurrentUser) else: if addressOfCurrentUser: form = AddressForm(instance=addressOfCurrentUser) else: form = AddressForm(user = users.get_current_user()) if form.is_valid(): address = form.save() return HttpResponseRedirect('/profile-was-saved') return render_to_response('address.html', locals()) </textarea> </p> <p> <b>address.html</b> is basically just "{{ form }}" and some HTML. </p> <p>There ya go. Post any questions as comments, and I will try to answer them! </p> <div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8755926150813951946-3111758162218520676?l=appengineguy.com'/></div>Mattias Johanssonnoreply@blogger.com28tag:blogger.com,1999:blog-8755926150813951946.post-9086611887666030082008-06-28T11:09:00.002+02:002008-06-28T11:22:16.863+02:00Google App Engine: The Book<p>For some people, this is old news, but I didn't catch it until now. It turns out that <a href="http://www.oreillynet.com/pub/au/3039">Noah Gift</a> (co-author of <a href="http://oreilly.com/catalog/9780596515829/">Python for Unix and Linux</a>) and <a href="http://linuxgazette.net/authors/orr.html">Mike Orr</a> (an editor of Linux Gazette), are already writing a bloody book on Google App Engine!</p> <p>It is going to get published by <a href="http://www.manning.com/">Manning</a>. One really nice thing about Manning is that they gradually release their books in electronic form as they are getting written through their <a href="http://www.manning.com/about/meap">early access program</a>. Anyway, they have a <a href="http://blog.gaebook.com/">blog</a> where you can follow their progress (and get chapters) - it doesn't look much for the world yet, but it's most likely worth keeping an eye on! </p><div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8755926150813951946-908661188766603008?l=appengineguy.com'/></div>Mattias Johanssonnoreply@blogger.com5tag:blogger.com,1999:blog-8755926150813951946.post-86196050080918471402008-06-26T09:18:00.004+02:002008-06-28T10:34:12.459+02:00Unit Tests for Google App Engine<p>I am a firm believer that if you don't use unit tests, you are being a bad developer.</p> <p>Read this!<br /> <a href="http://blog.appenginefan.com/2008/06/unit-tests-for-google-app-engine-apps.html">http://blog.appenginefan.com/2008/06/unit-tests-for-google-app-engine-apps.html</a></p><div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8755926150813951946-8619605008091847140?l=appengineguy.com'/></div>Mattias Johanssonnoreply@blogger.com7tag:blogger.com,1999:blog-8755926150813951946.post-70437166853490811272008-06-25T19:32:00.013+02:002008-07-21T16:56:44.879+02:00How to check out the latest version of Django using TortoiseSVN<div class="inledning"> <p><b>This is a few pointers on how to get Django 0.97 and the App Engine Django Helper running.</b></p> <p>I notice that a lot of people are using the included 0.96 version of Django included with the App Engine. Don't do this, it's stupid, and not recommended by anyone. The 0.96 just doesn't work all that well, it doesn't properly work with the <a href="http://code.google.com/appengine/articles/appengine_helper_for_django.html">App Engine Django Helper</a>, and lacks a LOT of really nice stuff.</p></div> <p>I think that the main reason people don't use the 0.97 version is that it's an unreleased version, which means it cannot be downloaded in a nice zip - instead, you need to check it out from the Django Subversion repository. New developers don't realize how incredibly easy it is to do this - it's almost easier than downloading a zip file. </p> <p>Anyway, here is how you check out the current development version of Django (as of writing, v0.97), on windows:</p> <ol> <li><a href="http://tortoisesvn.net/downloads">Download TortoiseSVN</a></li> <li>Install it and restart your computer.<br /></li> <li>Create a folder called django or something, right click it, and click CheckOut.<br /><br /></li> <a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://4.bp.blogspot.com/_TtIxFOydoMQ/SGKEDw8ct_I/AAAAAAAAABY/Xxe7k7QgiIg/s1600-h/1checkout.png"><img style="display:block; margin:0 10px 10px 0;cursor:pointer; cursor:hand;" src="http://4.bp.blogspot.com/_TtIxFOydoMQ/SGKEDw8ct_I/AAAAAAAAABY/Xxe7k7QgiIg/s400/1checkout.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5215876518661371890" /></a> <li>Enter <i>http://code.djangoproject.com/svn/django/trunk/</i> in the URL field and click OK.<br /><br /></li> <a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://3.bp.blogspot.com/_TtIxFOydoMQ/SGKEgTwZLaI/AAAAAAAAABg/RdwLMtiR1L4/s1600-h/2acheckout.png"><img style="display:block; margin:0 10px 10px 0;cursor:pointer; cursor:hand;" src="http://3.bp.blogspot.com/_TtIxFOydoMQ/SGKEgTwZLaI/AAAAAAAAABg/RdwLMtiR1L4/s400/2acheckout.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5215877009042386338" /></a> <li>Grab a cup of coffee REALLY FUCKING QUICKLY.<br /><br /></li> <a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://2.bp.blogspot.com/_TtIxFOydoMQ/SGKFUCwlsmI/AAAAAAAAABo/GQek7EggfGk/s1600-h/2bcheckout.png"><img style="display:block; margin:0 10px 10px 0;cursor:pointer; cursor:hand;" src="http://2.bp.blogspot.com/_TtIxFOydoMQ/SGKFUCwlsmI/AAAAAAAAABo/GQek7EggfGk/s400/2bcheckout.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5215877897833001570" /></a> <li>All done! You now have Django 0.97!<br /><br /></li> <a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/_TtIxFOydoMQ/SGKGKJOIdRI/AAAAAAAAABw/XnwmqZG6Fq4/s1600-h/3checkedout.png"><img style="display:block; margin:0 10px 10px 0;cursor:pointer; cursor:hand;" src="http://1.bp.blogspot.com/_TtIxFOydoMQ/SGKGKJOIdRI/AAAAAAAAABw/XnwmqZG6Fq4/s400/3checkedout.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5215878827280463122" /></a> </ol> <h3>Installning the Django Helper</h3> As for installing the Django Helper, the <a href="http://code.google.com/appengine/articles/appengine_helper_for_django.html">introduction</a> is pretty straightforward. There are three issues that you need to be aware of though: <p>1. You must remove a few unnecessary files from django to get below the 1000 file limit of App Engine. Waste these directories:</p> <ul> <li>django/bin</li> <li>django/contrib/admin</li> <li>django/contrib/databrowse</li> </ul> <p>(<b>DON'T</b> delete <i>django/contrib/auth</i> like the Django Helper inroduction says. It's actually needed.)</p> <p> <b style="color:red:font-size:14px">2. VERY IMPORTANT</b> if you have a non-english operating system:<br /> There is a very annoying bug in the Django helper r30 that causes the error "The Google App Engine SDK could not be found" when you try to run the Django Server if you run a non-english operating system. <a href="http://code.google.com/p/google-app-engine-django/issues/detail?id=51&q=non-english">Workaround and patch is availiable</a>.</p> <p>3. You need to have the <a href="http://sourceforge.net/project/platformdownload.php?group_id=78018">Python win32 extensions</a> installed if you are on windows.</p> <p>Stay curious,<br /> <b>Mattias</b></p><div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8755926150813951946-7043716685349081127?l=appengineguy.com'/></div>Mattias Johanssonnoreply@blogger.com6tag:blogger.com,1999:blog-8755926150813951946.post-51743222485874817292008-06-24T22:47:00.007+02:002008-07-21T16:59:03.859+02:00Uploading files to App Engine via Django ModelForms<div class="inledning"> <p>The FileField of the Django forms can be tied up just fine to the BlobProperty of a Data Store entity. This is how you do it.</p></div> <p> <b>The model</b><br /><br /> <textarea name="code" class="py"> from appengine_django.models import BaseModel from google.appengine.ext import db class AlbumItem(BaseModel): creationDate = db.DateTimeProperty(auto_now_add=True) image = db.BlobProperty(required=True) note = db.StringProperty() </textarea> <br /> <b>Displaying the form</b><br /><br /> <textarea name="code" class="py"> class AlbumItemForm(ModelForm): image = FileField() note = CharField(widget=Textarea) class Meta: model = AlbumItem exclude = ("creationDate") def albumentry(request): if request.POST: form = AlbumItemForm(request.POST, request.FILES) if form.is_valid(): albumEntry = form.save() return HttpResponseRedirect('/showalbumentry/%s' % albumEntry.key()) else: return render_to_response('albumentry.html', locals()) else: form = AlbumItemForm() return render_to_response('albumentry.html', locals()) </textarea> </p> <p>The <i>albumentry.html</i> file is rather uninteresting, just stick {{ form }} in there and remember to put the enctype="multipart/form-data" attribute on the &lt;form&gt; tag. The above example will work just fine, and save the image as blob data to your BlobProperty.</p> <p>The code used in this blog entry requires: App Engine SDK 1.1 Django Helper r30 Django 0.97 </p><div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8755926150813951946-5174322248587481729?l=appengineguy.com'/></div>Mattias Johanssonnoreply@blogger.com15tag:blogger.com,1999:blog-8755926150813951946.post-27036239481998921682008-06-24T12:51:00.002+02:002008-07-21T16:59:52.007+02:00GMemsess<p>Found this today: http://code.google.com/p/gmemsess/ <blockquote>gmemsess is a secure lightweight memcache-backed session class for Google appengine. Currently it is only suitable for short-term sessions, for providing your own authentication system or shopping cart, for instance. The session cookie expires when the browser is closed, and the reliability of memcache for longer-term storage has yet to be demonstrated. </blockquote> Pretty nice. I guess it should have a datastore backup, though, since we don't know how the MemCache API evicts data. Still cool that the little open source projects plop up so fast!</p><div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8755926150813951946-2703623948199892168?l=appengineguy.com'/></div>Mattias Johanssonnoreply@blogger.com3tag:blogger.com,1999:blog-8755926150813951946.post-56395477567758056162008-06-23T00:18:00.006+02:002008-07-21T17:01:50.676+02:00Getting random entities from the DataStore<div class="inledning"> <p>Sometimes it amazes me how easy it is to do some things using App Engine/Django, and how some simple things are a goddamn science. This one is somewhere in the middle. Getting a random entity from the Datastore. <b>This is how you do it!</b></p></div> <textarea name="code" class="py"> # Setting up the model class myModel(BaseModel): random = db.FloatProperty(required=True) # Creating an entity myModelInstance = myModel(random = random.random()) myModelInstance.save() # Querying the datastore gql = "SELECT * FROM myModel WHERE random >= :random ORDER BY random ASC LIMIT 1" randomSantaRelation = GqlQuery(gql, random=random.random()).get() if randomSantaRelation is None: gql = "SELECT * FROM myModel WHERE random <= :random ORDER BY random DESC LIMIT 1" randomSantaRelation = GqlQuery(gql, random=random.random()).get() </textarea> <p>See what happens here? We assign a random float to the entity on it's creation - they will look something like this in the datastore: </p> 0.216565955485 <p>Then, when doing a query, we simply create a new random value and query the Datastore for a single entity that is larger than that random value. In the off chance that this query returns no results, we'll have a backup query that runs the whole thing in reverse. This can happen if you have very few entities, and none of them have a random value in the upper ranges:</p> 0.628912291991<br /> 0.416565323566<br /> 0.216565955485<br /> 0.118278328322<br /> 0.013212121212 <p>If the random value you send into the GqlQuery above is 0.898912291991, it will return no rows. Therefore, the backup query is needed. The system will, of course, rely less and less on the backup query as more entities are added.</p><div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8755926150813951946-5639547756775805616?l=appengineguy.com'/></div>Mattias Johanssonnoreply@blogger.com8tag:blogger.com,1999:blog-8755926150813951946.post-60138465556675675122008-06-22T00:12:00.003+02:002008-07-21T17:02:55.062+02:00FileField not working?<p>I was getting very frustrated with my FileField not working in Django, but then I ran across this solution after some intense googling:</p> http://hurley.wordpress.com/2007/10/06/django-binding-uploaded-files-to-a-form/ <p>Turns out that you have to go:<br /> <textarea name="code" class="py"> form = NoteForm(request.POST, request.FILES) </textarea></p> <p>instead of just</p> <textarea name="code" class="py"> form = NoteForm(request.POST) </textarea> <p>Makes sense, but had me totally stumped!</p><div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8755926150813951946-6013846555667567512?l=appengineguy.com'/></div>Mattias Johanssonnoreply@blogger.com5tag:blogger.com,1999:blog-8755926150813951946.post-11880586628572739892008-06-19T21:07:00.004+02:002008-06-19T21:14:53.792+02:00Mutual Model relationsGot this baby today. <b>DuplicatePropertyError</b> ModelName already has property ModelName_set. Caused by this code: <textarea name="code" class="py"> class Address(BaseModel): name = db.StringProperty() addressLine1 = db.StringProperty() addressLine2 = db.StringProperty() zip = db.StringProperty() city = db.StringProperty() country = db.StringProperty() class SantaRelation(BaseModel): code = db.StringProperty() giver = db.ReferenceProperty(Address) receiver = db.ReferenceProperty(Address) isReceived = db.BooleanProperty() creationDate = db.DateTimeProperty(auto_now_add=True) image = db.Blob() note = db.StringProperty() </textarea> The solution is to define the underlying collection name via the <i>collection_name</i> argument to the <i>ReferenceProperty</i> constructor, as it will default to "modelname_set" otherwise, causing the DuplicatePropertyError. <pre><code> giver = db.ReferenceProperty(Address, collection_name='address_giver_set') receiver = db.ReferenceProperty(Address, collection_name='address_receiver_set') </code></pre> Presto!<div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8755926150813951946-1188058662857273989?l=appengineguy.com'/></div>Mattias Johanssonnoreply@blogger.com3tag:blogger.com,1999:blog-8755926150813951946.post-6072091301117775222008-06-16T13:13:00.005+02:002008-06-16T13:16:36.595+02:00Even faster websitesI ran across this just <a href="http://sites.google.com/site/io/even-faster-web-sites">awesome presentation by Steve Sounders</a> whom Google stole from Yahoo. :P <p> <object width="425" height="344"><param name="movie" value="http://www.youtube.com/v/QRUqVyP27Hw&hl=en"></param><embed src="http://www.youtube.com/v/QRUqVyP27Hw&hl=en" type="application/x-shockwave-flash" width="425" height="344"></embed></object> </p> <p>You can get the <a href="http://sites.google.com/site/io/even-faster-web-sites">slides here.</a></p><div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8755926150813951946-607209130111777522?l=appengineguy.com'/></div>Mattias Johanssonnoreply@blogger.com2tag:blogger.com,1999:blog-8755926150813951946.post-61981452434989869152008-06-13T11:44:00.004+02:002008-06-13T12:49:03.890+02:00Google I/O videos about app engineIn case you did not know, the <a href="http://code.google.com/events/io/">Google IO</a> videos are now online! The App Engine specific talks are: <ul> <li><a href="http://sites.google.com/site/io/rapid-development-with-python-django-and-google-app-engine"> Rapid Development with Python, Django, and Google App Engine</a></li> <li><a href="http://sites.google.com/site/io/building-scalable-web-applications-with-google-app-engine"> Building Scalable Web Applications with Google App Engine</a></li> <li><a href="http://sites.google.com/site/io/engaging-user-experiences-with-google-app-engine">Engaging User Experiences with Google App Engine</a></li> <li><a href="http://sites.google.com/site/io/under-the-covers-of-the-google-app-engine-datastore"> Under the Covers of the Google App Engine Datastore</a></li> <li><a href="http://sites.google.com/site/io/best-practices---building-a-production-quality-application-on-google-app-engine"> Best Practices - Building a Production Quality Application on Google App Engine</a></li> <li><a href="http://sites.google.com/site/io/working-with-google-app-engine-models">Working with Google App Engine Models</a></li> </ul><div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8755926150813951946-6198145243498986915?l=appengineguy.com'/></div>Mattias Johanssonnoreply@blogger.com4tag:blogger.com,1999:blog-8755926150813951946.post-76604497840252892222008-06-12T11:08:00.052+02:002008-06-12T20:20:02.575+02:00How-to: Full-text search in Google App Engine<p>It turns out that Google App Engine DOES have support for full-text search, it's just not documented, because the feature is still in development. </p> <p>When App Engine first arrived, a lot of people, including myself, was baffled at the lack of full-text search in the DataStore API. What the fudge - Google is THE full-text search company, and their database solution does not have support for Full-text indexing!</p> <p>The DataStore is built on top of <a href="http://en.wikipedia.org/wiki/BigTable"/>Googles BigTable</a>, which is a huge-arse database that powers a lot of projects at Google, <i>including Search Indexing</i>. <b>Yes</b>, the insanely limited, strange Data Storage is what Google is using to power their blazingly fast search engine.</p> <p>The Google App Engine API has a very primitive implementation of a full-text index for the datastore, hidden away in google.appengine.ext.search. (There is basically no documentation of it, so you have to <a href="http://code.google.com/p/googleappengine/source/browse">read the source</a>, lazy boy) You use it by creating your models from <b>search.SearchableModel</b>, instead of the usual <b>db.Model</b>.</p> Like this: <textarea name="code" class="py"> from google.appengine.ext import db from google.appengine.ext import search class Article(search.SearchableModel): title = db.TextProperty() publishDate = db.DateTimeProperty(auto_now_add=True) text = db.TextProperty() # should NOT be returned article = Article(text ='''This is the totally secret article text which talks about sausages and cheese in the middle of itself.''') article.title = "Fine cuisine" article.save() # should BE returned article = Article() article.title = "What I feed my dogs" article.text = '''This is the totally secret article text which talks about sausages and cheese in the middle of itself.''' article.save() print "Results" query = Article.all().search("sausages cheese dogs").order("-publishDate") for a in query: print "%s | %s" % (a.title, a.publishDate) print "Done printing" </textarea> <p><h3>Limitations</h3> This is basically just "find entries that contains these words" - it has no exact phrase match, substring match, boolean operators, <a href="http://en.wikipedia.org/wiki/Stemming">stemming</a>, or other common full-text features.</p> <p> <h3>The nitty gritty</h3> <b>Save latency</b> The philophy behind the Data Store is to make use of the fact that disk space is cheap, and perform and store calculations when a piece of data is stored. This applies to SearchableModels as well - they create the index for the entity when Save() is called. This means that instances created from SearchableModels take slightly longer to save than standard models. Keep this in mind.</p> <p><b>Index of the index</b> As you might now, The Google App Engine SDK generates indexes in index.yaml for all queries that you run while you are developing the app. However, since you might not be running all the imaginable cases of queries while you are developing, the index.yaml might be inadequate, and need to be manually appended with indexes. In these cases, you need to know that the full-text index is placed in a propertly called __searchable_text_index. To add indexes for it, the full-text index property: </p> <pre><code> - kind: Article properties: - name: __searchable_text_index - name: publishDate direction: desc </code></pre> There you go! Full-text indexing on App Engine. Not perfect at all, but it works for a lot of scenarios!<div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8755926150813951946-7660449784025289222?l=appengineguy.com'/></div>Mattias Johanssonnoreply@blogger.com39tag:blogger.com,1999:blog-8755926150813951946.post-57486364088567030742008-06-12T09:55:00.002+02:002008-06-12T10:04:59.764+02:00web2py: A first look from meWeb2Py (formerly called Gluon) is a very cool concept. It's a framework very, very similiar to Django, except it has almost zero configuration, and allows you to create and develop your applications entirely online. It does a LOT for you, and kind of runs on Google App Engine. <object width="400" height="251"> <param name="allowfullscreen" value="true" /> <param name="allowscriptaccess" value="always" /> <param name="movie" value="http://www.vimeo.com/moogaloop.swf?clip_id=932708&amp;server=www.vimeo.com&amp;show_title=1&amp;show_byline=1&amp;show_portrait=0&amp;color=&amp;fullscreen=1" /> <embed src="http://www.vimeo.com/moogaloop.swf?clip_id=932708&amp;server=www.vimeo.com&amp;show_title=1&amp;show_byline=1&amp;show_portrait=0&amp;color=&amp;fullscreen=1" type="application/x-shockwave-flash" allowfullscreen="true" allowscriptaccess="always" width="400" height="251"></embed></object><br /><a href="http://www.vimeo.com/932708?pg=embed&sec=932708">web2py on the Google appengine</a> from <a href="http://www.vimeo.com/user315328?pg=embed&sec=932708">mdipierro</a> on <a href="http://vimeo.com?pg=embed&sec=932708">Vimeo</a>. I'm always sceptical of frameworks that does lots of stuff for you in enourmously elegant ways, because they normally stop being elegant the minute you want to do something a little more custom. That said, the idea of getting this entire shabang running on Google App Engine, allowing you to do your entire development for App Engine in a browser, is a very funky idea. Web2py is worth keeping an eye on.<div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8755926150813951946-5748636408856703074?l=appengineguy.com'/></div>Mattias Johanssonnoreply@blogger.com6tag:blogger.com,1999:blog-8755926150813951946.post-4349732635165528572008-06-11T19:23:00.003+02:002008-06-11T19:44:48.994+02:00RightThis is my first entry for this blog. It's gonna be about me learning <a href="http://code.google.com/appengine/">App Engine</a>, since I learn the best by teaching others. But first, a little insight about it, to get be going with the writing. I work at a consulting company, and is currently rented, full time, to a local company that does internal development of it's CRM system. The last few days, I've been trying to deploy a very, very simple web application. It's basically a simple one-page form, tied to a database and a web service. It also does some mailing stuff. Problem is, deploying this app is insanely complicated, even in this organization that has a pretty small IT department. I have to order a bloody server, along with a functional specification. I have to specifically order the SQL server, and the mail server. I also have to request IP access to the web service. And I have to go through two guys which are pretty hard to get a hold of. The whole thing has taken like 3 days. To upload a web app. This is something that would have taken me approximately one hour TOPS on my own server. On app engine? 10 minutes. I wonder what would have happened if I had developed the application in Python and uploaded it onto app engine. I would probably had gotten into trouble, but I imagine this happening in a lot of companies. Google very much considers App Engine to be a part of it's Google Apps suite - you can tie an app engine to your <a href="http://www.google.com/a/help/intl/en/org/index.html">Google Apps for your Domain account</a>, and it's listed right there, next to your Docs, Mail, Calendar and whatnot: <a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://4.bp.blogspot.com/_TtIxFOydoMQ/SFANVGtdAKI/AAAAAAAAAAQ/4YhsCfEEQhs/s1600-h/appenginetest.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="http://4.bp.blogspot.com/_TtIxFOydoMQ/SFANVGtdAKI/AAAAAAAAAAQ/4YhsCfEEQhs/s400/appenginetest.jpg" alt="" id="BLOGGER_PHOTO_ID_5210679425097334946" border="0" /></a>This made me realize - Google is REALLY going after the internal IT departments of companies, in a very sneaky way. Lets just say that I was a little insane, and got just one wiki (Sites) and one app engine going in the organization, and got a subdomain of the company tied up to them (theapp.thecompanyiworkfor.com) through some minor but clever politics and bribery by candy. Lets then say that the organization started to rely a bit on these two things. Boom. Suddendly, google is in your organization, and you are not getting rid of it. You might as well start to use docs. Or mail. EVERYONE uses skype in the organization, by the way, even though absolutely no company policy dicates it's use. It's just there, and has grown into the organization. I imagine the same happening with Google Apps. Really, really frickin' sneaky, google.<div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8755926150813951946-434973263516552857?l=appengineguy.com'/></div>Mattias Johanssonnoreply@blogger.com5