Permalink
Browse files

Created a badge/vote server in App Engine that authenticates to GitHu…

…b to generate badge images and handle voting.

Squashed commit of the following:

commit 83895472b2aa7b2e3e1abf8e483bc957c681feae
Author: Alex Komoroske <komoroske@google.com>
Date:   Fri May 4 14:34:34 2012 -0700

    Correct text positioning (for some reason it's different in prod than in dev)

commit 86fc37baa3249047e9d124905d85f98957d35c6a
Author: Alex Komoroske <komoroske@google.com>
Date:   Fri May 4 14:13:25 2012 -0700

    Fixed a bug where the first time you signed in it wouldn't say who you were authenticated as.

commit c1b05aa3371bde2c68a36e07b3b24e2c044e0379
Author: Alex Komoroske <komoroske@google.com>
Date:   Fri May 4 14:10:46 2012 -0700

    Stopped requiring a google account sign in for voting (you need a GH signin anyway)

commit c4ed86b936fe41f9050cf882d25c263265fcb79c
Author: Alex Komoroske <komoroske@google.com>
Date:   Fri May 4 14:08:41 2012 -0700

    Very simple styling for the vote pages.

commit 84e445c6f532cf5ca0222140c2cd8471db1ea1c6
Author: Alex Komoroske <komoroske@google.com>
Date:   Fri May 4 14:05:26 2012 -0700

    A real badge if there isn't such an issue.

commit 9fd5c39073715df4128d2817d2b566b5ba4f7d9e
Author: Alex Komoroske <komoroske@google.com>
Date:   Fri May 4 13:59:52 2012 -0700

    Basic error handling if the vote transaction fails.

commit 0887414beaa1bc5f6408cd7fc128c9b08b009f32
Author: Alex Komoroske <komoroske@google.com>
Date:   Fri May 4 10:45:09 2012 -0700

    Plumbed through voting/unvoting to the UI.

commit 9ba25f21933eb3efc625d1db31bcc2a85b7cc2ac
Author: Alex Komoroske <komoroske@google.com>
Date:   Fri May 4 10:28:37 2012 -0700

    Defined a toggle_vote method on the Issue (not exposed to the UI yet)

commit e129467c1b70d8ef152d72b4ea5126b0771f9892
Author: Alex Komoroske <komoroske@google.com>
Date:   Fri May 4 10:25:34 2012 -0700

    Implemented unvoting functionality (although it's not exposed via the UI yet).

commit 35d2be300619fb9d26dc059a57de81988a5510c2
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 19:36:48 2012 -0700

    Completed drawing code given the current inputs.

commit 8344bbe637837ae6005fc521ec5f581b120f1e2a
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 19:28:19 2012 -0700

    Draw all of the inner orange pill. Drawing works, it just needs some tweaking on padding.

commit a32081e7e23efba8f246bf81518aadd438c1ce28
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 19:21:57 2012 -0700

    Draw vote count (wrong color on purpose)

commit 8ba242aec0ed2f372156b3972725b6930ef6b5d6
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 19:19:27 2012 -0700

    Draw the issue number

commit e94beaec8a83614640836e26fbbde6733b50c26d
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 19:16:36 2012 -0700

    Draw the badge image as well

commit 1a9ef2235ddcb68a2193aad88866b84c1ef77950
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 19:08:50 2012 -0700

    Draw the background of the badge

commit 693ff77926798039049fb81189af0dd67c399276
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 19:06:07 2012 -0700

    Size the badge based on amount of text to print.

commit 33e9c8b2eee0257a812259866327811b4ae1b46e
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 18:54:16 2012 -0700

    Defined color constants that we'll use for the badge.

commit a0dfcfc89e2cb712e084f31b03d5bcb7212c26df
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 18:50:04 2012 -0700

    Made it so badges don't rely on there being a fixed size for badges.

commit 90d5b5d069fc76b0a137d2d5cc18efb81afaa707
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 18:42:16 2012 -0700

    Factored out drawing code in preparation for making it much more legit.

commit 65071ebe0f372c75a51c60a23695cdb07215b9bf
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 17:58:31 2012 -0700

    Include the image badge on the vote page, for reference.

commit 4230862de24cba33254d23c5cd5dcba203ed8393
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 17:57:19 2012 -0700

    Changed the URL patterns so that without the badge parameter you get the vote page.

commit 84c871b544a34eac590a7a950af91bf4b1725b57
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 17:56:31 2012 -0700

    We cache the image in memcache for quickness.

commit b265b488acfb385d32d4eced1cfaa848bf0e3f6e
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 15:58:06 2012 -0700

    Cache the username in memcache so we don't have to keep hitting github.

commit 7f11bcadf435bfc30014b3b77a49273bcfd2dd37
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 15:51:14 2012 -0700

    Actual voting happens in a transaction to avoid cases where the cached number gets out of sync with the actual number of votes.

commit 6c688f49cb0db0654392b04ee0cb2be5f7390de2
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 11:54:06 2012 -0700

    Made each vote have its issue as its parent (so we can do it in a transaction).

commit f4d2e579b7923a15d3ecaebde27a63d4a86baa0a
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 11:46:24 2012 -0700

    Created render_template convenience function for voting pages. Added an error message slot in the vote.html template, and use it for all types of error messages.

commit 343cda97adb187a8746a722eb8b2a0b75b2da9bb
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 11:38:32 2012 -0700

    Switched to only have one voted template,for simplicity.

commit cb1a2f14f37c6e06fe4c6fea59c575bbb4fe2d1f
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 11:37:10 2012 -0700

    Fixed a bug where when voting it said no issue because we passed a string, not an int.

commit d3d7ca9c6d73329d05c7154c9de5441827abf1a5
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 11:32:44 2012 -0700

    Make voting work even if no one has requested the badge image for that issue yet.

commit 0148c3898c3630e4de81729076ac9df38dc252c9
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 11:29:23 2012 -0700

    Show a stub of a no issue image if there isn't an issue that comes back from GitHub.

commit c1e5bee84f30178def22b9300d74b82aebdac2fe
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 11:27:13 2012 -0700

    Check to make sure the issue in GitHub has the appropriate label.

commit cd2783fd42d4ff680be9b269a2730b3874a3ec8a
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 11:22:22 2012 -0700

    Basic checking to make sure the issue actually exists in GitHub.

commit 26b435a3a6ccd76ed4812eec83ea7a42d0a29e7f
Author: Alex Komoroske <komoroske@google.com>
Date:   Thu May 3 11:16:46 2012 -0700

    Added another TODO

commit 4f234055862d83231d4ede4487e02bee3eaeb8a0
Author: Alex Komoroske <komoroske@google.com>
Date:   Wed May 2 08:51:45 2012 -0700

    Fixed the redirect bug (missing the "s" in https).

commit ad65836d12e2b4752555769f5686b3be43d4e340
Author: Alex Komoroske <komoroske@google.com>
Date:   Tue May 1 20:17:02 2012 -0700

    Started work to check if github thinks an issue should exist. Doesn't work because GitHub forwards us to developer.github.com for some reason.

commit 30126b36e076902a0e1263c63b75b5cda46824e9
Author: Alex Komoroske <komoroske@google.com>
Date:   Tue May 1 20:04:53 2012 -0700

    Only allow a given user to vote once.

commit 2092d55326acd7386f8741cf252bc0be4b846f0c
Author: Alex Komoroske <komoroske@google.com>
Date:   Tue May 1 19:59:38 2012 -0700

    Add a gitignore file that will ignore the client_config.json files if you have one in your repo.

commit 74220570cf3f57cc5211f1f7ffc86c9ab3695cdd
Author: Alex Komoroske <komoroske@google.com>
Date:   Tue May 1 19:55:15 2012 -0700

    Added an explicit client_config sample file

commit b34a052d540f664102d1508757e8e72ceccaee72
Author: Alex Komoroske <komoroske@google.com>
Date:   Mon Apr 30 21:02:51 2012 -0700

    Removed an old TODO from the code that no longer applies.

commit 516fbe3b038f09a1e0f5123c735dc061a509d745
Merge: f43b510 f614813
Author: Alex Komoroske <komoroske@google.com>
Date:   Mon Apr 30 21:01:56 2012 -0700

    Merge branch 'github-auth' into badges

commit f614813b5cd47931fbf731e92d80378ee1d4345a
Author: Alex Komoroske <komoroske@google.com>
Date:   Mon Apr 30 21:00:13 2012 -0700

    Actually record the username from github when voting.

commit ce8defb57fb74a6031d6b0525fa8de9c3c07a96d
Author: Alex Komoroske <komoroske@google.com>
Date:   Mon Apr 30 20:52:12 2012 -0700

    Don't allow a vote if the user isn't authenticated with GitHub.

commit 9035a6f836e07e528954b19bd18f0d555fd865a0
Author: Alex Komoroske <komoroske@google.com>
Date:   Mon Apr 30 20:47:00 2012 -0700

    We actually set the access_code cookie clientside.

commit 75458ac6652df3e380cc6b20044f0eca0ac5160e
Author: Alex Komoroske <komoroske@google.com>
Date:   Mon Apr 30 20:33:10 2012 -0700

    Parse out the access token.

commit 0da4d21a38a716fba0045d237e848d81c1a6a36f
Author: Alex Komoroske <komoroske@google.com>
Date:   Mon Apr 30 20:28:58 2012 -0700

    We send the code back to github to get an access token (but we don't currently do anything with it).

commit 37ec74d4f4386de8f8c1fe73ee38b17fafe4164b
Author: Alex Komoroske <komoroske@google.com>
Date:   Mon Apr 30 20:25:13 2012 -0700

    Fixed a bug where after loading config from JSON we couldn't do string substitution.

commit f9fb681b2c23c13641688fe7b5d0fe88a6bd26fe
Author: Alex Komoroske <komoroske@google.com>
Date:   Mon Apr 30 20:16:34 2012 -0700

    Load client_id and client_secret from a file that's deliberately not in the repo.

commit e9e4b86a9010d920faab4bb577cb4ed670e8b3c9
Author: Alex Komoroske <komoroske@google.com>
Date:   Mon Apr 30 20:01:22 2012 -0700

    We redirect you to git hub if you aren't logged in. But when github redirects you back we don't catch you.

commit e7201e2dbd9c00a1c20e346f6454281b5c0ea99e
Author: Alex Komoroske <komoroske@google.com>
Date:   Mon Apr 30 19:53:10 2012 -0700

    Oops, use the already-parsed cookies dictionary on the request object.

commit 3b73ba176260b2784c268196faad102d28622c83
Author: Alex Komoroske <komoroske@google.com>
Date:   Mon Apr 30 19:42:36 2012 -0700

    Pull out the access cookie if it's set.

commit f43b510a89ff1c9b2748a83d4f5c84d91144d79a
Author: Alex Komoroske <komoroske@google.com>
Date:   Mon Apr 30 19:12:43 2012 -0700

    The badge images actually have the vote count.

commit 220789ae755a753bb4aac7f923de64c591c0ae51
Author: Alex Komoroske <komoroske@google.com>
Date:   Mon Apr 30 19:07:37 2012 -0700

    Keep track of vote_count on the issue seperately, for optimization reasons.

commit 2fb1bc5bc3a6c5cce5653d15009daa6569b4d313
Author: Alex Komoroske <komoroske@google.com>
Date:   Mon Apr 30 19:06:37 2012 -0700

    Refactor voting logic to be part of issue.

commit c398268b135c97ee6ff2c379396d4d9324cdb22c
Author: Alex Komoroske <komoroske@google.com>
Date:   Mon Apr 30 19:04:05 2012 -0700

    Very simple voting for an issue when posted to the vote URL.

commit 125308eabae56824742bb8e6677655921c1edfd8
Author: Alex Komoroske <komoroske@google.com>
Date:   Mon Apr 30 19:01:20 2012 -0700

    Fixed up definition of Vote data model.

commit 89003f11dcb1d437142a6c60f3657eddde78d4da
Author: Alex Komoroske <komoroske@google.com>
Date:   Mon Apr 30 18:55:44 2012 -0700

    Present a simple vote form at issue/xxx/vote URLs

commit c4cacde8d6e53761b96cc0a72fc652e6f2a57cab
Author: Alex Komoroske <komoroske@google.com>
Date:   Mon Apr 30 18:30:42 2012 -0700

    Defined a Vote object in the datastore.

commit 66ed5a907a8aba261aea8b2d15ac60dcd121739a
Author: Alex Komoroske <komoroske@google.com>
Date:   Mon Apr 30 18:26:27 2012 -0700

    factored image creation into get_image method on Issue.

commit 41b9470b574c564650fe4feb2972427c3b7309ed
Author: Alex Komoroske <komoroske@google.com>
Date:   Mon Apr 30 18:25:07 2012 -0700

    Very simple get_or_create_issue.

commit 38d2a6f29cbb340c49ec2c294383615e968018dc
Author: Alex Komoroske <komoroske@google.com>
Date:   Mon Apr 30 18:20:18 2012 -0700

    Super stubby stub of Issue datastore object

commit d99826ac4e8eeb643dd3e68ca2a392ccfe1c19ed
Author: Alex Komoroske <komoroske@google.com>
Date:   Sun Apr 29 20:54:19 2012 -0700

    Have very simple printing of (constant) numbers using a font.

commit 7c0c8d095eaa324204cf8ba1bcc6840022b765c9
Author: Alex Komoroske <komoroske@google.com>
Date:   Sun Apr 29 20:51:25 2012 -0700

    Fixed a typo where we still used webapp instead of webapp2 for some uses.

commit e86d92d9cb38c8b6c4dababbc0a9d76dcdfb66e2
Author: Alex Komoroske <komoroske@google.com>
Date:   Sun Apr 29 20:23:37 2012 -0700

    Simple stub that dynamnically generates a content-less image

commit 3cecf5fed97e34d290da6e836f64d8636ec7e8a5
Author: Alex Komoroske <komoroske@google.com>
Date:   Sun Apr 29 20:14:09 2012 -0700

    include PIL

commit 25528e2d6de3c8adc8ee72729ba7ec14cb352623
Author: Alex Komoroske <komoroske@google.com>
Date:   Sun Apr 29 20:11:56 2012 -0700

    Stub of badge page.

commit 068d52c82666b769d0c1c030667106ef89c5804f
Author: Alex Komoroske <komoroske@google.com>
Date:   Sun Apr 29 20:09:02 2012 -0700

    Added stub of hosting for /issue URLs

commit 5c55cf9a34600bf88598a0c016dbbb3d17185c77
Author: Alex Komoroske <komoroske@google.com>
Date:   Sun Apr 29 20:04:20 2012 -0700

    Switch to using concurrent requests and webapp2 in the simple-acl-server. This makes all functionality work again.

commit 3963962dfd4566c84c03056fc583d6f960743888
Author: Alex Komoroske <komoroske@google.com>
Date:   Sun Apr 29 20:00:57 2012 -0700

    Switch to using python27 (which will allow us to use the PIL). The simple-acl-server does not work in this commit.

git-svn-id: https://robohornet.googlecode.com/svn/trunk@149 48a18672-c925-a5f9-4b90-b3a8688770c7
  • Loading branch information...
1 parent 2f7360a commit 811c4de8e287cd948e3b55236663f1794ba4aac0 komoroske@google.com committed May 4, 2012
View
@@ -0,0 +1 @@
+client_config.json
View
@@ -1,13 +1,20 @@
application: robohornet-benchmark
version: 1
-runtime: python
+runtime: python27
api_version: 1
+threadsafe: true
+
+libraries:
+- name: PIL
+ version: latest
handlers:
-# TEMPORARILY SHUNT ALL REQUESTS THROUGH THE ACL HANDLER
+- url: /issue.*
+ script: voting.app
+# TEMPORARILY SHUNT ALL OTHER REQUESTS THROUGH THE ACL HANDLER
# Once this is public, flip back to fully static handlers
- url: .*
- script: simple-acl-server.py
+ script: simple-acl-server.app
login: required
#- url: /
# static_files: static/robohornet.html
@@ -0,0 +1,4 @@
+{
+ "CLIENT_ID" : "YOUR CLIENT ID HERE",
+ "CLIENT_SECRET" : "YOUR CLIENT SECRET HERE"
+}
View
@@ -1,8 +1,7 @@
#This file is only required before launch to check ACLs
#After launch we can do it all with static file hosting in app.yaml
-from google.appengine.ext import webapp
-from google.appengine.ext.webapp.util import run_wsgi_app
+import webapp2
from google.appengine.api import users
import re
@@ -37,14 +36,14 @@
STATIC_BASE_PATH = "static/"
-class ACLPage(webapp.RequestHandler):
+class ACLPage(webapp2.RequestHandler):
def get(self, path):
user = users.get_current_user()
#Check if the user is allowed.
if not user or not any(pattern.match(user.email()) for pattern in ALLOWED_USERS):
self.response.set_status(404)
logging.warning("|%s| tried to log in but was blacklisted" % user.email())
- self.response.out.write(webapp.Response.http_status_message(404))
+ self.response.out.write(webapp2.Response.http_status_message(404))
return
#Remove the leading /
path = path[1:]
@@ -59,15 +58,9 @@ def get(self, path):
f = open(STATIC_BASE_PATH + path)
except IOError:
self.response.set_status(404)
- self.response.out.write(webapp.Response.http_status_message(404))
+ self.response.out.write(webapp2.Response.http_status_message(404))
return
self.response.headers['Content-Type'] = FILE_TYPES[file_type]
self.response.out.write(f.read())
-application = webapp.WSGIApplication([('(.*)', ACLPage)], debug = True)
-
-def main():
- run_wsgi_app(application)
-
-if __name__ == "__main__":
- main()
+app = webapp2.WSGIApplication([('(.*)', ACLPage)], debug = True)
Binary file not shown.
Binary file not shown.
Binary file not shown.
View
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <link rel='stylesheet' href='/main.css'>
+ <style>
+ body {
+ background-color:white;
+ background-image: none;
+ margin: 20px;
+ }
+ </style>
+ </head>
+ <body>
+ <img src='badge'>
+ {% if error %}
+ <div class='error'>
+ <strong>Error:</strong> {{error}}
+ </div>
+ {% else %}
+ <p>You're authenticated as <strong>{{username}}</strong></p>
+ {% if has_vote %}
+ <h2>You've already voted for issue {{issue_number}}.</h2>
+ <form method='post'>
+ <input type='submit' value='Unvote for issue {{issue_number}}'>
+ </form>
+ {% else %}
+ <h2>You haven't voted for issue {{issue_number}}.</h2>
+ <form method='post'>
+ <input type='submit' value='Vote for issue {{issue_number}}'>
+ </form>
+ {% endif %}
+ {% endif %}
+ </body>
+</html>
View
@@ -0,0 +1,267 @@
+import webapp2, logging, json, urllib
+
+from PIL import Image, ImageFont, ImageDraw
+
+from google.appengine.ext import db
+from google.appengine.ext.webapp import template
+from google.appengine.api import urlfetch
+from google.appengine.api import memcache
+
+
+GH_COOKIE_NAME = "gh_a"
+
+#Temporarily use a project name that's not locked down.
+ORGANIZATION_NAME = "jkomoros"
+PROJECT_NAME = "Playground"
+PERFORMANCE_ISSUE_LABEL = "Performance"
+
+#We load up the client_ID and the client_secret from a configuration file that is not open source.
+try:
+ config = json.load(open("client_config.json"))
+except IOError:
+ config = {}
+
+CLIENT_ID = str(config.get("CLIENT_ID", "NO_ID"))
+CLIENT_SECRET = str(config.get("CLIENT_SECRET", "NO_SECRET"))
+
+BADGE_HEIGHT = 70
+HALF_BADGE_HEIGHT = BADGE_HEIGHT / 2
+PADDING = 20
+
+VERY_LIGHT_YELLOW = (254, 241, 202, 255)
+YELLOW = (252, 207, 77, 255)
+LIGHT_ORANGE = (246, 148, 61, 255)
+
+LATO = ImageFont.truetype("static/fonts/Lato-Regular.ttf", 46)
+LATO_BLACK = ImageFont.truetype("static/fonts/Lato-Black.ttf", 46)
+
+#TODO: express the resizing in terms of constants
+LOGO = Image.open("static/robohornet.png").resize((46,46))
+
+def draw_invalid_issue_image():
+ message = "No such issue"
+ text_width = LATO.getsize(message)[0]
+ # End caps + width + padding plus logo overage
+ width = BADGE_HEIGHT + text_width + 20 + PADDING
+
+ im = Image.new("RGBA", (width, BADGE_HEIGHT))
+ draw = ImageDraw.Draw(im)
+
+ #First cap
+ draw.ellipse([0,0, BADGE_HEIGHT, BADGE_HEIGHT], fill = LIGHT_ORANGE)
+
+ draw.rectangle([HALF_BADGE_HEIGHT, 0, width - HALF_BADGE_HEIGHT, BADGE_HEIGHT], fill = LIGHT_ORANGE)
+
+ draw.ellipse([width - BADGE_HEIGHT, 0, width, BADGE_HEIGHT], fill = LIGHT_ORANGE)
+
+ im.paste(LOGO, (10, 10, 56, 56), LOGO)
+
+ draw.text((HALF_BADGE_HEIGHT + PADDING + 20, 7), message, font = LATO, fill=VERY_LIGHT_YELLOW)
+
+ return im
+
+NO_ISSUE_IMAGE = draw_invalid_issue_image()
+
+def draw_image(number, vote_count):
+ #TODO: vote_count string should include commas.
+ #calculate how wide this image will be.
+ number_width = LATO.getsize("#" + str(number))[0]
+ vote_count_width = LATO_BLACK.getsize(str(vote_count))[0]
+ # End caps + cap in the middle + text + padding betweeen number text and surroundings + overage from the logo
+ width = BADGE_HEIGHT + (BADGE_HEIGHT / 2) + number_width + vote_count_width + (PADDING * 2) + 20
+
+ im = Image.new("RGBA", (width, BADGE_HEIGHT))
+ draw = ImageDraw.Draw(im)
+
+ #First cap
+ draw.ellipse([0,0, BADGE_HEIGHT, BADGE_HEIGHT], fill = VERY_LIGHT_YELLOW)
+
+ draw.rectangle([HALF_BADGE_HEIGHT, 0, width - HALF_BADGE_HEIGHT, BADGE_HEIGHT], fill = VERY_LIGHT_YELLOW)
+
+ vote_count_right_edge = width - HALF_BADGE_HEIGHT
+ vote_count_left_edge = vote_count_right_edge - vote_count_width
+
+ #End cap
+ draw.ellipse([width - BADGE_HEIGHT, 0, width, BADGE_HEIGHT], fill = LIGHT_ORANGE)
+
+ #inner cap
+ draw.ellipse([vote_count_left_edge - HALF_BADGE_HEIGHT, 0, vote_count_left_edge + HALF_BADGE_HEIGHT, BADGE_HEIGHT], fill = LIGHT_ORANGE)
+
+ draw.rectangle([vote_count_left_edge, 0, width - HALF_BADGE_HEIGHT, BADGE_HEIGHT], fill = LIGHT_ORANGE)
+
+ im.paste(LOGO, (10, 10, 56, 56), LOGO)
+
+ #Draw the issue number
+ draw.text((vote_count_left_edge - HALF_BADGE_HEIGHT - PADDING - number_width, 7), str("#" + str(number)), font=LATO, fill = YELLOW)
+
+ draw.text((vote_count_left_edge, 7), str(vote_count), font = LATO_BLACK, fill = VERY_LIGHT_YELLOW)
+ return im, width
+
+def get_or_create_issue(number):
+ number = int(number)
+ issue = Issue.all().filter('number =', number).get()
+ if not issue:
+ #Check if GitHub thinks this should exist.
+ response = urlfetch.fetch("https://api.github.com/repos/%s/%s/issues/%s" % (ORGANIZATION_NAME, PROJECT_NAME, str(number)))
+ #TODO: error handle
+ data = json.loads(response.content)
+ if data.get("number") != number:
+ return None
+ #Make sure it has the "Performance" label
+ if not any(item.get("name") == "Performance" for item in data.get("labels", [])):
+ return None
+ #TODO: in the future we should check every so often if this issue is still valid.
+ issue = Issue(number = number)
+ issue.put()
+ return issue
+
+
+class Issue(db.Model):
+ number = db.IntegerProperty(required=True)
+ vote_count = db.IntegerProperty(default = 0)
+ def get_image(self):
+ #TODO: support mulptiple badge sizes
+ memcache_key = "issue_%d_image" % self.number
+ cached_values = memcache.get_multi([memcache_key, memcache_key + "_width"])
+ image_data = cached_values.get(memcache_key)
+ width = cached_values.get(memcache_key + "_width")
+ if image_data:
+ im = Image.fromstring("RGBA", (width, BADGE_HEIGHT), image_data)
+ else:
+ im, width = draw_image(self.number, self.vote_count)
+ memcache.set_multi({memcache_key: im.tostring(), memcache_key + "_width" : width})
+ return im
+ def has_vote(self, username):
+ return bool(self.get_vote(username))
+ def get_vote(self, username):
+ return Vote.all().filter('user =', username).filter('issue =', self).get()
+ def _reset_image_cache(self):
+ memcache_key = "issue_%d_image" % self.number
+ memcache.delete_multi([memcache_key, memcache_key + "_width"])
+ def vote(self, username):
+ if self.get_vote(username):
+ return False
+ try:
+ db.run_in_transaction(do_vote, self.key(), username)
+ except:
+ return False
+ #ensure that later reads will get the updated version.
+ self._reset_image_cache()
+ return True
+ def unvote(self, username):
+ vote = self.get_vote(username)
+ if not vote:
+ return False
+ try:
+ db.run_in_transaction(do_unvote, self.key(), vote.key())
+ except:
+ return False
+ #ensure that later reads will get the updated version.
+ self._reset_image_cache()
+ return True
+ def toggle_vote(self, username):
+ return self.unvote(username) if self.has_vote(username) else self.vote(username)
+
+def do_vote(issue_key, username):
+ issue = db.get(issue_key)
+ vote = Vote(parent = issue, user = username, issue = issue)
+ issue.vote_count = issue.vote_count + 1
+ vote.put()
+ issue.put()
+
+def do_unvote(issue_key, vote_key):
+ issue = db.get(issue_key)
+ vote = db.get(vote_key)
+ issue.vote_count = issue.vote_count - 1
+ vote.delete()
+ issue.put()
+
+def get_username(access_token):
+ memcache_key = access_token + "_username"
+ username = memcache.get(memcache_key)
+ if not username:
+ response = urlfetch.fetch("https://api.github.com/user?access_token=%s" % access_token)
+ #TODO: error handle
+ data = json.loads(response.content)
+ username = data.get("login", "")
+ if username:
+ memcache.set(memcache_key, username)
+ return username
+
+class Vote(db.Model):
+ user = db.StringProperty(required = True)
+ issue = db.ReferenceProperty(Issue, required = True)
+ timestamp = db.DateTimeProperty(auto_now_add = True)
+
+class VotePage(webapp2.RequestHandler):
+ def initialize(self, request, response):
+ super(VotePage, self).initialize(request, response)
+ self.access = self.request.cookies.get(GH_COOKIE_NAME, "")
+ def get(self, issue_number):
+ issue = get_or_create_issue(issue_number)
+ if not issue:
+ self.render_template(issue, {"error" : "That issue either doesn't exist or isn't a performance issue."})
+ return
+ if not self.access:
+ if self.request.get("code", ""):
+ #The user said we're okay to get access. Now we need to get the access token.
+ payload = {
+ "client_id" : CLIENT_ID,
+ "client_secret" : CLIENT_SECRET,
+ "code" : self.request.get("code", ""),
+ "redirect_uri" : self.request.url
+ }
+ #TODO: error handle
+ response = urlfetch.fetch("https://github.com/login/oauth/access_token", urllib.urlencode(payload), "POST")
+ #TODO: parse in a more resilient way
+ access_token = response.content.split("&")[0].split("=")[1]
+ self.access = access_token
+ self.response.headers['Set-Cookie'] = "%s=%s; expires=Thu, 31-Dec-2020 23:59:59 GMT; path=/" % (GH_COOKIE_NAME, access_token)
+ #TODO: remove the ?code= from the URL
+ else:
+ #Okay, this is the first request
+ self.redirect("https://github.com/login/oauth/authorize?client_id=%s&redirect_uri=%s" % (CLIENT_ID, self.request.url), False)
+ return
+ self.render_template(issue)
+ def post(self, issue_number):
+ issue = get_or_create_issue(issue_number)
+ if not issue:
+ self.render_template(issue, {"error" : "That issue either doesn't exist or isn't a performance issue."})
+ return
+ if not self.access:
+ self.render_template(issue, {'error': "You didn't authenticate with GitHub."})
+ return
+ username = get_username(self.access)
+ if not username:
+ self.render_template(issue, {"error" : "There was no username associated with your login."})
+ return
+ if not issue.toggle_vote(username):
+ self.render_template(issue, {"error" : "Your action could not be registered. Please try again."})
+ return
+ self.render_template(issue)
+ def render_template(self, issue, args = None):
+ if not args:
+ args = {}
+ args['issue_number'] = issue.number if issue else ""
+ username = get_username(self.access)
+ args['username'] = username
+ args['has_vote'] = issue.has_vote(username) if issue else False
+ self.response.out.write(template.render("vote.html", args))
+
+
+class BadgePage(webapp2.RequestHandler):
+ def get(self, issue_number):
+ issue_number = int(issue_number)
+ issue = get_or_create_issue(issue_number)
+ im = issue.get_image() if issue else NO_ISSUE_IMAGE
+ self.response.headers['Content-Type'] = "image/png"
+ im.save(self.response.out, "PNG")
+
+class NoSuchPage(webapp2.RequestHandler):
+ def get(self, path):
+ self.response.set_status(404)
+ self.response.out.write("Invalid URL")
+
+app = webapp2.WSGIApplication([('/issue/(\d+)/badge/?', BadgePage),
+ ('/issue/(\d+)/?', VotePage),
+ ('(.*)', NoSuchPage)], debug = True)

0 comments on commit 811c4de

Please sign in to comment.