├── .cfignore ├── .gitignore ├── Procfile ├── README.md ├── manage.py ├── manifest.yml ├── pong_matcher_django ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py ├── pongmatcher ├── __init__.py ├── admin.py ├── finders.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── serializers.py ├── tests.py ├── urls.py └── views.py ├── requirements.txt └── runtime.txt /.cfignore: -------------------------------------------------------------------------------- 1 | **/*/__pycache__ 2 | tags 3 | env 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/*/__pycache__ 2 | tags 3 | env 4 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: python manage.py migrate && gunicorn pong_matcher_django.wsgi --log-file - 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CF example app: ping-pong matching server 2 | 3 | This is an app to match ping-pong players with each other. It's currently an 4 | API only, so you have to use `curl` to interact with it. 5 | 6 | It has an [acceptance test suite][acceptance-test] you might like to look at. 7 | 8 | **Note**: We highly recommend that you use the latest versions of any software required by this sample application. 9 | 10 | ## Running on [Pivotal Web Services][pws] 11 | 12 | Log in. 13 | 14 | ```bash 15 | cf login -a https://api.run.pivotal.io 16 | ``` 17 | 18 | Target your org / space. An empty space is recommended, to avoid naming collisions. 19 | 20 | ```bash 21 | cf target -o myorg -s myspace 22 | ``` 23 | 24 | Sign up for a cleardb instance. 25 | 26 | ```bash 27 | cf create-service cleardb spark mysql 28 | ``` 29 | 30 | Push the app. Its manifest assumes you called your ClearDB instance 'mysql'. 31 | 32 | ```bash 33 | cf push -n mysubdomain 34 | ``` 35 | 36 | Export the test host 37 | 38 | ```bash 39 | export HOST=http://mysubdomain.cfapps.io 40 | ``` 41 | 42 | Now follow the [interaction instructions](#interaction-instructions). 43 | 44 | NB: By default, the app runs with an insecure, shared 45 | [SECRET_KEY][django-deployment]. If you care about security in your app, you 46 | should set this in an environment variable: 47 | 48 | ```bash 49 | cf set-env djangopong SECRET_KEY thesecretkeythatonlyyouknow 50 | cf restage djangopong 51 | ``` 52 | 53 | ## Running locally 54 | 55 | The following assumes you have a working, 3.4.x version of [Python][python] 56 | installed. You'll also need [pip][pip], the Python dependency manager. If you 57 | installed Python using Homebrew on OSX, you're already set. On Ubuntu, the 58 | package you want is 'python3-pip'. 59 | 60 | Install and start MySQL: 61 | 62 | ```bash 63 | brew install mysql 64 | mysql.server start 65 | mysql -u root 66 | ``` 67 | 68 | Create a database user and table in the MySQL REPL you just opened: 69 | 70 | ```sql 71 | CREATE USER 'djangopong'@'localhost' IDENTIFIED BY 'djangopong'; 72 | CREATE DATABASE pong_matcher_django_development; 73 | GRANT ALL ON pong_matcher_django_development.* TO 'djangopong'@'localhost'; 74 | exit 75 | ``` 76 | 77 | Install virtualenv: 78 | 79 | ```bash 80 | pip3 install virtualenv 81 | ``` 82 | 83 | Create and activate a new Python environment: 84 | 85 | ```bash 86 | virtualenv env 87 | source env/bin/activate 88 | ``` 89 | 90 | Install the project's dependencies: 91 | 92 | ```bash 93 | pip install -r requirements.txt --allow-external mysql-connector-python 94 | ``` 95 | 96 | Migrate the database: 97 | 98 | ```bash 99 | ./manage.py migrate 100 | ``` 101 | 102 | Start the application server: 103 | 104 | ```bash 105 | ./manage.py runserver 106 | ``` 107 | 108 | Export the test host in another shell, ready to run the interactions. 109 | 110 | ```bash 111 | export HOST=http://localhost:8000 112 | ``` 113 | 114 | Now follow the [interaction instructions](#interaction-instructions). 115 | 116 | NB: you can also use Foreman to run the migrations and start the app server 117 | with `foreman start`. However, Foreman defaults to a different port (5000), so 118 | be sure to export the test host with port 5000 instead of 8000. 119 | 120 | ## Interaction instructions 121 | 122 | Start by clearing the database from any previous tests. You should get a 200. 123 | 124 | ```bash 125 | curl -v -X DELETE $HOST/all 126 | ``` 127 | 128 | Then request a match, providing both a request ID and player ID. Again, you 129 | should get a 200. 130 | 131 | ```bash 132 | curl -v -H "Content-Type: application/json" -X PUT $HOST/match_requests/firstrequest -d '{"player": "andrew"}' 133 | ``` 134 | 135 | Now pretend to be someone else requesting a match: 136 | 137 | ```bash 138 | curl -v -H "Content-Type: application/json" -X PUT $HOST/match_requests/secondrequest -d '{"player": "navratilova"}' 139 | ``` 140 | 141 | Let's check on the status of our first match request: 142 | 143 | ```bash 144 | curl -v -X GET $HOST/match_requests/firstrequest 145 | ``` 146 | 147 | The bottom of the output should show you the match_id. You'll need this in the 148 | next step. 149 | 150 | Now pretend that you've got back to your desk and need to enter the result: 151 | 152 | ```bash 153 | curl -v -H "Content-Type: application/json" -X POST $HOST/results -d ' 154 | { 155 | "match_id":"thematchidyoureceived", 156 | "winner":"andrew", 157 | "loser":"navratilova" 158 | }' 159 | ``` 160 | 161 | You should get a 201 Created response. 162 | 163 | Future requests with different player IDs should not cause a match with someone 164 | who has already played. The program is not yet useful enough to 165 | allow pairs who've already played to play again. 166 | 167 | [acceptance-test]:https://github.com/cloudfoundry-samples/pong_matcher_acceptance 168 | [pws]:https://run.pivotal.io 169 | [python]:https://www.python.org 170 | [pip]:https://pip.pypa.io/en/latest/ 171 | [django-deployment]:https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ 172 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pong_matcher_django.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: djangopong 4 | services: 5 | - mysql 6 | -------------------------------------------------------------------------------- /pong_matcher_django/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-samples/pong_matcher_django/9d444569f39a984059b9bbb420c836f6bd8e0753/pong_matcher_django/__init__.py -------------------------------------------------------------------------------- /pong_matcher_django/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for pong_matcher_django project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.7/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.7/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 14 | 15 | 16 | # Quick-start development settings - unsuitable for production 17 | # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ 18 | 19 | # SECURITY WARNING: keep the secret key used in production secret! 20 | SECRET_KEY = os.environ.get('SECRET_KEY', 'INSECUREKEY') 21 | 22 | # SECURITY WARNING: don't run with debug turned on in production! 23 | DEBUG = True 24 | 25 | TEMPLATE_DEBUG = True 26 | 27 | ALLOWED_HOSTS = [ '*' ] 28 | 29 | 30 | # Application definition 31 | 32 | INSTALLED_APPS = ( 33 | 'django.contrib.admin', 34 | 'django.contrib.auth', 35 | 'django.contrib.contenttypes', 36 | 'django.contrib.sessions', 37 | 'django.contrib.messages', 38 | 'django.contrib.staticfiles', 39 | 'rest_framework', 40 | 'pongmatcher', 41 | ) 42 | 43 | MIDDLEWARE_CLASSES = ( 44 | 'django.contrib.sessions.middleware.SessionMiddleware', 45 | 'django.middleware.common.CommonMiddleware', 46 | 'django.middleware.csrf.CsrfViewMiddleware', 47 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 48 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ) 52 | 53 | ROOT_URLCONF = 'pong_matcher_django.urls' 54 | 55 | WSGI_APPLICATION = 'pong_matcher_django.wsgi.application' 56 | 57 | 58 | # Database 59 | # https://docs.djangoproject.com/en/1.7/ref/settings/#databases 60 | 61 | from urllib.parse import urlparse 62 | url = urlparse( 63 | os.environ.get( 64 | 'DATABASE_URL', 65 | 'mysql2://djangopong:djangopong@127.0.0.1:3306/pong_matcher_django_development' 66 | ) 67 | ) 68 | DATABASES = { 69 | 'default': { 70 | 'ENGINE': 'django.db.backends.mysql', 71 | 'NAME': url.path[1:], 72 | 'USER': url.username, 73 | 'PASSWORD': url.password, 74 | 'HOST': url.hostname, 75 | 'PORT': url.port, 76 | } 77 | } 78 | 79 | # Internationalization 80 | # https://docs.djangoproject.com/en/1.7/topics/i18n/ 81 | 82 | LANGUAGE_CODE = 'en-us' 83 | 84 | TIME_ZONE = 'UTC' 85 | 86 | USE_I18N = True 87 | 88 | USE_L10N = True 89 | 90 | USE_TZ = True 91 | 92 | 93 | # Static files (CSS, JavaScript, Images) 94 | # https://docs.djangoproject.com/en/1.7/howto/static-files/ 95 | 96 | PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) 97 | STATIC_ROOT = os.path.join(PROJECT_ROOT, 'staticfiles') 98 | STATIC_URL = '/static/' 99 | -------------------------------------------------------------------------------- /pong_matcher_django/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | from django.contrib import admin 3 | 4 | urlpatterns = patterns('', 5 | url(r'^', include('pongmatcher.urls')), 6 | ) 7 | -------------------------------------------------------------------------------- /pong_matcher_django/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for pong_matcher_django project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pong_matcher_django.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | from dj_static import Cling 15 | 16 | application = Cling(get_wsgi_application()) 17 | -------------------------------------------------------------------------------- /pongmatcher/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-samples/pong_matcher_django/9d444569f39a984059b9bbb420c836f6bd8e0753/pongmatcher/__init__.py -------------------------------------------------------------------------------- /pongmatcher/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /pongmatcher/finders.py: -------------------------------------------------------------------------------- 1 | from pongmatcher.models import Participant 2 | 3 | class Match: 4 | @classmethod 5 | def get(cls, uuid): 6 | participants = Participant.objects.filter(match_uuid=uuid) 7 | if len(participants) > 0: 8 | return Match(uuid=participants[0].match_uuid, 9 | match_request_1_uuid=participants[0].match_request_uuid, 10 | match_request_2_uuid=participants[1].match_request_uuid) 11 | else: 12 | return None 13 | 14 | def __init__(self, uuid, match_request_1_uuid, match_request_2_uuid): 15 | self.uuid = uuid 16 | self.match_request_1_uuid = match_request_1_uuid 17 | self.match_request_2_uuid = match_request_2_uuid 18 | -------------------------------------------------------------------------------- /pongmatcher/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='MatchRequest', 15 | fields=[ 16 | ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), 17 | ('uuid', models.CharField(max_length=200)), 18 | ('player_uuid', models.CharField(max_length=200)), 19 | ], 20 | options={ 21 | }, 22 | bases=(models.Model,), 23 | ), 24 | migrations.CreateModel( 25 | name='Participant', 26 | fields=[ 27 | ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), 28 | ('match_uuid', models.CharField(max_length=200)), 29 | ('match_request_uuid', models.CharField(max_length=200)), 30 | ('player_uuid', models.CharField(max_length=200)), 31 | ('opponent_uuid', models.CharField(max_length=200)), 32 | ], 33 | options={ 34 | }, 35 | bases=(models.Model,), 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /pongmatcher/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-samples/pong_matcher_django/9d444569f39a984059b9bbb420c836f6bd8e0753/pongmatcher/migrations/__init__.py -------------------------------------------------------------------------------- /pongmatcher/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | import uuid 3 | 4 | class MatchRequest(models.Model): 5 | uuid = models.CharField(max_length=200) 6 | player_uuid = models.CharField(max_length=200) 7 | 8 | def __str__(self): 9 | return "request: %(request)s\nfor player: %(player)s" % { 10 | "request": self.uuid, 11 | "player": self.player_uuid, 12 | } 13 | 14 | def match_id(self): 15 | participants = Participant.objects.filter(match_request_uuid=self.uuid) 16 | if len(participants) == 0: 17 | return None 18 | else: 19 | return participants[0].match_uuid 20 | 21 | def persist_participants(self): 22 | opponent_request = self.find_opponent_request() 23 | 24 | if opponent_request: 25 | match_uuid = uuid.uuid4() 26 | Participant.objects.create( 27 | match_uuid=match_uuid, 28 | match_request_uuid=opponent_request.uuid, 29 | player_uuid=opponent_request.player_uuid, 30 | opponent_uuid=self.player_uuid 31 | ) 32 | Participant.objects.create( 33 | match_uuid=match_uuid, 34 | match_request_uuid=self.uuid, 35 | player_uuid=self.player_uuid, 36 | opponent_uuid=opponent_request.player_uuid 37 | ) 38 | 39 | def find_opponent_request(self): 40 | requests = MatchRequest.objects.raw('''SELECT * FROM pongmatcher_matchrequest 41 | WHERE player_uuid <> %s 42 | AND uuid NOT IN (SELECT match_request_uuid FROM pongmatcher_participant) 43 | AND player_uuid NOT IN 44 | (SELECT opponent_uuid FROM pongmatcher_participant WHERE player_uuid = %s)''', 45 | [self.player_uuid, self.player_uuid]) 46 | try: 47 | return requests[0] 48 | except IndexError: 49 | return None 50 | 51 | class Participant(models.Model): 52 | match_uuid = models.CharField(max_length=200) 53 | match_request_uuid = models.CharField(max_length=200) 54 | player_uuid = models.CharField(max_length=200) 55 | opponent_uuid = models.CharField(max_length=200) 56 | 57 | def __str__(self): 58 | return "match: %(match)s\nrequest: %(request)s\nplayer: %(player)s\nopponent: %(opponent)s" % { 59 | "match": self.match_uuid, 60 | "request": self.match_request_uuid, 61 | "player": self.player_uuid, 62 | "opponent": self.opponent_uuid 63 | } 64 | 65 | -------------------------------------------------------------------------------- /pongmatcher/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from pongmatcher.models import MatchRequest, Participant 3 | from pongmatcher.finders import Match 4 | import json 5 | 6 | class MatchRequestSerializer(serializers.Serializer): 7 | id = serializers.CharField(source='uuid') 8 | player = serializers.CharField(source='player_uuid') 9 | match_id = serializers.CharField(required=False) 10 | 11 | def create(self, validated_data): 12 | match_request = MatchRequest.objects.create(**validated_data) 13 | match_request.persist_participants() 14 | return match_request 15 | 16 | class MatchSerializer: 17 | def __init__(self, match): 18 | self.match = match 19 | self.data = { 20 | 'id': match.uuid, 21 | 'match_request_1_id': match.match_request_1_uuid, 22 | 'match_request_2_id': match.match_request_2_uuid, 23 | } 24 | -------------------------------------------------------------------------------- /pongmatcher/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /pongmatcher/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from pongmatcher import views 3 | 4 | urlpatterns = [ 5 | url(r'^all', views.wipe), 6 | url(r'^match_requests$', views.match_request_list), 7 | url(r'^match_requests/([\w-]+)$', views.match_request_detail), 8 | url(r'^matches/([\w-]+)$', views.match_detail), 9 | url(r'^results$', views.result_list), 10 | ] 11 | -------------------------------------------------------------------------------- /pongmatcher/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.views.decorators.csrf import csrf_exempt 3 | from django.shortcuts import render 4 | from rest_framework.renderers import JSONRenderer 5 | from rest_framework.parsers import JSONParser 6 | from pongmatcher.models import MatchRequest, Participant 7 | from pongmatcher.serializers import MatchRequestSerializer, MatchSerializer 8 | from pongmatcher.finders import Match 9 | 10 | class JSONResponse(HttpResponse): 11 | def __init__(self, data, **kwargs): 12 | content = JSONRenderer().render(data) 13 | kwargs['content_type'] = 'application/json' 14 | super(JSONResponse, self).__init__(content, **kwargs) 15 | 16 | @csrf_exempt 17 | def wipe(request): 18 | if request.method == 'DELETE': 19 | MatchRequest.objects.all().delete() 20 | Participant.objects.all().delete() 21 | return HttpResponse(status=200) 22 | else: 23 | return HttpResponse(status=404) 24 | 25 | @csrf_exempt 26 | def match_request_list(request): 27 | return HttpResponse(status=404) 28 | 29 | @csrf_exempt 30 | def match_request_detail(request, uuid): 31 | if request.method == 'PUT': 32 | data = JSONParser().parse(request) 33 | writeable_data = dict(data.items() | [('id', uuid)]) 34 | serializer = MatchRequestSerializer(data=writeable_data) 35 | if serializer.is_valid(): 36 | serializer.save() 37 | return JSONResponse(serializer.data, status=200) 38 | else: 39 | return HttpResponse(status=422) 40 | elif request.method == 'GET': 41 | try: 42 | match_request = MatchRequest.objects.get(uuid=uuid) 43 | serializer = MatchRequestSerializer(match_request) 44 | return JSONResponse(serializer.data) 45 | except MatchRequest.DoesNotExist: 46 | return HttpResponse(status=404) 47 | else: 48 | return HttpResponse(status=405) 49 | 50 | @csrf_exempt 51 | def match_detail(request, uuid): 52 | match = Match.get(uuid) 53 | if match: 54 | serializer = MatchSerializer(match) 55 | return JSONResponse(serializer.data) 56 | else: 57 | return HttpResponse(status=404) 58 | 59 | @csrf_exempt 60 | def result_list(request): 61 | if request.method == 'POST': 62 | return HttpResponse(status=201) 63 | else: 64 | return HttpResponse(status=405) 65 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dj-database-url==0.4.1 2 | dj-static==0.0.6 3 | Django==1.9.6 4 | django-toolbelt==0.0.1 5 | djangorestframework==3.3.3 6 | gunicorn==19.4.5 7 | mysqlclient==1.3.13 8 | psycopg2==2.7.5 9 | static3==0.7.0 10 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.5.x 2 | --------------------------------------------------------------------------------