name: inverse layout: true class: center, middle, inverse --- name: normal layout: false --- template: inverse # Django as a mobile backend ## Vik Paruchuri ## vik@dataquest.io ## Find this at github.com/VikParuchuri/boston-django --- template: normal # Mobile apps ## Need a high level of UI polish ## Have to be ultra-responsive ## Can't have any bugs ## Need a backend? --- template: normal # Questions to answer ## Which backend do I choose? ## When do I switch backends? ## How do I work with django and rest framework? --- template: inverse # Let's make a game where you can capture territory by running --- template: normal # Major features ## Get user location as they move ## Determine which territory they're in ## Give them points in the territory ## Compute the territory leader and send pushes --- template: normal # Screenshots ![image](img/turfly.png) --- template: normal # Parse ## Javascript/mongo based backend as a service ## Very quick to get started with --- template: normal # Making and storing an object ```objective-c PFObject *language = [PFObject objectWithClassName:@"Language"]; language[@"name"] = @"python"; language[@"rating"] = @100; [language saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { }]; ``` --- template: normal # Querying ```objective-c PFQuery *query = [PFQuery queryWithClassName:@"Language"]; [query whereKey:@"name" equalTo:@"python"]; [query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) { }]; ``` --- template: normal # And there's more! ## Location-based querying ## Push notifications ## "Cloud code" that runs on server --- template: normal # For most MVP use cases, Parse is the best option. --- template: inverse # But there are huge catches as you scale! --- template: normal class: small-code # Nested callbacks are the worst ```javascript var userQuery = new Parse.Query(Parse.User); userQuery.each(function(user){ return Leaderboard.createUserScore(user); }).then(function(){ var UserTileRecord = Parse.Object.extend("UserTileRecord"); var query = new Parse.Query(UserTileRecord); query.include("user.userScore"); var userPoints = {}; query.each(function(tileRecord) { promise.resolve(); return promise; }).then(function(){ var keys = Object.keys(userPoints); var promises = []; for(var i = 0; i < keys.length; i++){ var key = keys[i]; promises.push(Leaderboard.addPoints(userPoints[key].user, userPoints[key].points, userPoints[key].turfs)); } return Parse.Promise.when(promises); }, function(error){ var message = "Error creating user score" + error.code + " " + error.message; console.log(message); status.error(message); }).then(function(){ status.success(); }, function(error){ }); }, function(error){ }); ``` --- template: normal # The main downsides ## Impossible to debug or test ## Limited query system ## Cloud code has strict time limits --- template: inverse # Finding a new backend --- template: normal # Main goals ## Easy to debug/test ## Quicker to build new features ## Known, mature stack ## Easy to use location features --- template: normal # Why a framework? ## Encourage best practices ## Quicker to develop with ## Easier to test --- template: normal # Why DRF? ## Actively developed ## Good documentation ## Well-structured --- template: normal # Final stack ## Django + geodjango ## Celery ## Django rest framework ## Postgres + postgis --- template: normal # Authentication ## Token auth ```python REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.TokenAuthentication', 'rest_framework.authentication.SessionAuthentication' ), ... } ``` --- template: normal # Social auth ## Get tokens from device and verify + store ```python params = {'fields': 'id', 'access_token': access_token} text = requests.get("https://graph.facebook.com/me", params=params).text tokens_valid = json.loads(text).get("id", None) == uid ``` ```python twitter = Twython(consumer_key, consumer_secret, auth_token, auth_token_secret) credentials = twitter.verify_credentials() tokens_valid = uid in [credentials["id_str"], credentials["id"]] ``` --- template: normal # Social auth ## When user logs in next, match record based on facebook/twitter id ```python id_ok = verify_facebook_id(facebook_id, access_token) fb_info_exists = FacebookInfo.objects.filter(facebook_id=facebook_id).exists() user = FacebookInfo.objects.get(facebook_id=facebook_id).user ``` --- template: normal # Push notifications ## Step one, register device ```python install = Installation.objects.get(id=installation_id) sns = boto.connect_sns(settings.AWS_ACCESS_KEY_ID, settings.AWS_SECRET_ACCESS_KEY) endpoint_data = sns.create_platform_endpoint(...) endpoint_arn = endpoint_data...["EndpointArn"] install.amazon_arn = endpoint_arn install.save() ``` --- template: normal # Push notifications ## Step two, send push ```python user = User.objects.get(id=user_id) arn = user.installations[0].amazon_arn sns = boto.connect_sns(settings.AWS_ACCESS_KEY_ID, settings.AWS_SECRET_ACCESS_KEY) sns.publish( message=message_json, target_arn=arn, message_structure='json' ) ``` --- template: normal # Sending email ## Use mandrill + python client ```python mandrill_client = mandrill.Mandrill(settings.MANDRILL_API_KEY) mandrill_client.messages.send_template( template_name='welcome-email-template', message={ "subject": "Welcome!", "from_email": from_address, ... } ) ``` --- template: normal # Delayed tasks ## Use celery and the shared_task decorator ```python @shared_task def handle_all_tracks(): tracks = Track.objects.all() for t in tracks: handle_track(t, should_send_push=False) ``` --- template: normal # Geographic queries ## Geodjango + PostGIS ```python class TerritoryRecord(models.Model): point = PointField(blank=True, null=True) objects = GeoManager() ``` ```python bbox = (lon_min, lat_min, lon_max, lat_max) geom = Polygon.from_bbox(bbox) query = TerritoryRecord.objects.filter(point__contained=geom).... ``` --- template: normal # Permissions ## Object level with django guardian ```python def assign_installation_perms(sender, instance=None, created=False, **kwargs): if created: user = instance.user if user is None: return assign_perm("view_installation", user, instance) assign_perm("change_installation", user, instance) post_save.connect(assign_installation_perms, sender=Installation) ``` --- template: normal # Making API URLs ```python class InstallationViewSet(mixins.CreateModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet): permission_classes = (permissions.DjangoObjectPermissions,) queryset = Installation.objects.all() serializer_class = InstallationSerializer paginate_by = 25 ``` --- template: normal # Making custom views ```python class SetSuggestedFriendsSeen(APIView): def post(self, request, format=None): profile = request.user.profile ... ``` --- template: inverse # Let's take a quick look at the device side --- template: normal # Use AFNetworking -- it's the best ```objective-c AGHTTPSessionManager *manager = [AGUtil getUnauthenticatedManager]; NSDictionary *parameters = @{@"email": email, @"password": password}; NSString* url = [NSString stringWithFormat:@"%@/accounts/get_auth_token/", HTTP_BASE]; [manager POST:url parameters:parameters success:successBlock failure:failureBlock]; ``` --- template: normal # General tips ## Singleton user class with useful data ## One API "wrapper" file per django app ## Make a custom table view that can handle pagination ## Grab social auth tokens on the device --- template: normal # User experience tips ## Cache results from the server (especially views) ## Profile server requests and speed them up ## Have unobtrusive loading indicators --- template: inverse # Questions? ## vik@dataquest.io