├── .gitignore ├── ConferenceCentral_Complete ├── LICENSE ├── README.md ├── app.yaml ├── conference.py ├── cron.yaml ├── index.yaml ├── main.py ├── models.py ├── settings.py ├── static │ ├── bootstrap │ │ └── css │ │ │ ├── bootstrap-cosmo.css │ │ │ ├── bootstrap-responsive.css │ │ │ ├── bootstrap.css │ │ │ ├── main.css │ │ │ └── offcanvas.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff │ ├── img │ │ ├── CloudPlatform_logo.png │ │ ├── ajax-loader.gif │ │ ├── business1.jpg │ │ ├── business2.jpg │ │ ├── business3.jpg │ │ ├── favicon.ico │ │ └── meeting-room.jpg │ ├── js │ │ ├── app.js │ │ └── controllers.js │ └── partials │ │ ├── conference_detail.html │ │ ├── create_conferences.html │ │ ├── home.html │ │ ├── login.modal.html │ │ ├── profile.html │ │ └── show_conferences.html ├── templates │ └── index.html └── utils.py ├── LICENSE ├── Lesson_2 ├── 000_Hello_Endpoints │ ├── app.yaml │ ├── helloworld_api.py │ └── static │ │ ├── hello.js │ │ └── index.html ├── 001_Hello_Endpoints_Solutions │ ├── app.yaml │ ├── helloworld_api.py │ └── static │ │ ├── hello.js │ │ └── index.html └── 00_Conference_Central │ ├── LICENSE │ ├── README.md │ ├── app.yaml │ ├── conference.py │ ├── models.py │ ├── settings.py │ ├── static │ ├── bootstrap │ │ └── css │ │ │ ├── bootstrap-cosmo.css │ │ │ ├── bootstrap-responsive.css │ │ │ ├── bootstrap.css │ │ │ ├── main.css │ │ │ └── offcanvas.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff │ ├── img │ │ ├── CloudPlatform_logo.png │ │ ├── ajax-loader.gif │ │ ├── business1.jpg │ │ ├── business2.jpg │ │ ├── business3.jpg │ │ ├── favicon.ico │ │ └── meeting-room.jpg │ ├── js │ │ ├── app.js │ │ └── controllers.js │ └── partials │ │ ├── conference_detail.html │ │ ├── create_conferences.html │ │ ├── home.html │ │ ├── login.modal.html │ │ ├── profile.html │ │ └── show_conferences.html │ └── templates │ └── index.html ├── Lesson_3 ├── 00_Conference_Central │ ├── LICENSE │ ├── README.md │ ├── app.yaml │ ├── conference.py │ ├── models.py │ ├── settings.py │ ├── static │ │ ├── bootstrap │ │ │ └── css │ │ │ │ ├── bootstrap-cosmo.css │ │ │ │ ├── bootstrap-responsive.css │ │ │ │ ├── bootstrap.css │ │ │ │ ├── main.css │ │ │ │ └── offcanvas.css │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ └── glyphicons-halflings-regular.woff │ │ ├── img │ │ │ ├── CloudPlatform_logo.png │ │ │ ├── ajax-loader.gif │ │ │ ├── business1.jpg │ │ │ ├── business2.jpg │ │ │ ├── business3.jpg │ │ │ ├── favicon.ico │ │ │ └── meeting-room.jpg │ │ ├── js │ │ │ ├── app.js │ │ │ └── controllers.js │ │ └── partials │ │ │ ├── conference_detail.html │ │ │ ├── create_conferences.html │ │ │ ├── home.html │ │ │ ├── login.modal.html │ │ │ ├── profile.html │ │ │ └── show_conferences.html │ └── templates │ │ └── index.html └── additions │ └── utils.py ├── Lesson_4 ├── 00_Conference_Central │ ├── LICENSE │ ├── README.md │ ├── app.yaml │ ├── conference.py │ ├── models.py │ ├── settings.py │ ├── static │ │ ├── bootstrap │ │ │ └── css │ │ │ │ ├── bootstrap-cosmo.css │ │ │ │ ├── bootstrap-responsive.css │ │ │ │ ├── bootstrap.css │ │ │ │ ├── main.css │ │ │ │ └── offcanvas.css │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ └── glyphicons-halflings-regular.woff │ │ ├── img │ │ │ ├── CloudPlatform_logo.png │ │ │ ├── ajax-loader.gif │ │ │ ├── business1.jpg │ │ │ ├── business2.jpg │ │ │ ├── business3.jpg │ │ │ ├── favicon.ico │ │ │ └── meeting-room.jpg │ │ ├── js │ │ │ ├── app.js │ │ │ └── controllers.js │ │ └── partials │ │ │ ├── conference_detail.html │ │ │ ├── create_conferences.html │ │ │ ├── home.html │ │ │ ├── login.modal.html │ │ │ ├── profile.html │ │ │ └── show_conferences.html │ ├── templates │ │ └── index.html │ └── utils.py └── Additions │ ├── TODO_1_conference.py │ ├── TODO_1_models.py │ ├── TODO_2_conference.py │ ├── TODO_2_models.py │ ├── TODO_3_conference.py │ ├── TODO_4_conference.py │ ├── TODO_5_conference.py │ ├── TODO_6_conference.py │ ├── TODO_6_models.py │ ├── TODO_7_conference.py │ ├── conference_api.partial.txt │ └── conference_models.partial.txt ├── Lesson_5 ├── 00_Conference_Central │ ├── LICENSE │ ├── README.md │ ├── app.yaml │ ├── conference.py │ ├── index.yaml │ ├── main.py │ ├── models.py │ ├── settings.py │ ├── static │ │ ├── bootstrap │ │ │ └── css │ │ │ │ ├── bootstrap-cosmo.css │ │ │ │ ├── bootstrap-responsive.css │ │ │ │ ├── bootstrap.css │ │ │ │ ├── main.css │ │ │ │ └── offcanvas.css │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ └── glyphicons-halflings-regular.woff │ │ ├── img │ │ │ ├── CloudPlatform_logo.png │ │ │ ├── ajax-loader.gif │ │ │ ├── business1.jpg │ │ │ ├── business2.jpg │ │ │ ├── business3.jpg │ │ │ ├── favicon.ico │ │ │ └── meeting-room.jpg │ │ ├── js │ │ │ ├── app.js │ │ │ └── controllers.js │ │ └── partials │ │ │ ├── conference_detail.html │ │ │ ├── create_conferences.html │ │ │ ├── home.html │ │ │ ├── login.modal.html │ │ │ ├── profile.html │ │ │ └── show_conferences.html │ ├── templates │ │ └── index.html │ └── utils.py └── Additions │ ├── TODO_1_app.yaml │ ├── TODO_1_conference.py │ ├── TODO_1_main.py │ ├── TODO_1_models.py │ ├── TODO_2_app.yaml │ ├── TODO_2_conference.py │ ├── TODO_2_main.py │ └── TODO_3_cron.yaml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | 38 | # OS X Generated Files # 39 | ######################## 40 | .DS_Store 41 | .DS_Store? 42 | ._* 43 | .Spotlight-V100 44 | .Trashes 45 | Icon? 46 | ehthumbs.db 47 | Thumbs.db 48 | .AppleDouble 49 | 50 | *.elc 51 | *~ 52 | *.aux 53 | *.log 54 | *.mx1 55 | .com.apple.timemachine.supported -------------------------------------------------------------------------------- /ConferenceCentral_Complete/README.md: -------------------------------------------------------------------------------- 1 | App Engine application for the Udacity training course. 2 | 3 | ## Products 4 | - [App Engine][1] 5 | 6 | ## Language 7 | - [Python][2] 8 | 9 | ## APIs 10 | - [Google Cloud Endpoints][3] 11 | 12 | ## Setup Instructions 13 | 1. Update the value of `application` in `app.yaml` to the app ID you 14 | have registered in the App Engine admin console and would like to use to host 15 | your instance of this sample. 16 | 1. Update the values at the top of `settings.py` to 17 | reflect the respective client IDs you have registered in the 18 | [Developer Console][4]. 19 | 1. Update the value of CLIENT_ID in `static/js/app.js` to the Web client ID 20 | 1. (Optional) Mark the configuration files as unchanged as follows: 21 | `$ git update-index --assume-unchanged app.yaml settings.py static/js/app.js` 22 | 1. Run the app with the devserver using `dev_appserver.py DIR`, and ensure it's running by visiting your local server's address (by default [localhost:8080][5].) 23 | 1. (Optional) Generate your client library(ies) with [the endpoints tool][6]. 24 | 1. Deploy your application. 25 | 26 | 27 | [1]: https://developers.google.com/appengine 28 | [2]: http://python.org 29 | [3]: https://developers.google.com/appengine/docs/python/endpoints/ 30 | [4]: https://console.developers.google.com/ 31 | [5]: https://localhost:8080/ 32 | [6]: https://developers.google.com/appengine/docs/python/endpoints/endpoints_tool 33 | -------------------------------------------------------------------------------- /ConferenceCentral_Complete/app.yaml: -------------------------------------------------------------------------------- 1 | application: your-project-id 2 | version: 1 3 | runtime: python27 4 | api_version: 1 5 | threadsafe: yes 6 | 7 | handlers: # static then dynamic 8 | 9 | - url: /favicon\.ico 10 | static_files: favicon.ico 11 | upload: favicon\.ico 12 | 13 | - url: /js 14 | static_dir: static/js 15 | 16 | - url: /img 17 | static_dir: static/img 18 | 19 | - url: /css 20 | static_dir: static/bootstrap/css 21 | 22 | - url: /fonts 23 | static_dir: static/fonts 24 | 25 | - url: /partials 26 | static_dir: static/partials 27 | 28 | - url: / 29 | static_files: templates/index.html 30 | upload: templates/index\.html 31 | secure: always 32 | 33 | - url: /tasks/send_confirmation_email 34 | script: main.app 35 | 36 | - url: /crons/set_announcement 37 | script: main.app 38 | 39 | - url: /_ah/spi/.* 40 | script: conference.api 41 | secure: always 42 | 43 | libraries: 44 | 45 | - name: webapp2 46 | version: latest 47 | 48 | - name: endpoints 49 | version: latest 50 | 51 | # pycrypto library used for OAuth2 (req'd for authenticated APIs) 52 | - name: pycrypto 53 | version: latest 54 | -------------------------------------------------------------------------------- /ConferenceCentral_Complete/cron.yaml: -------------------------------------------------------------------------------- 1 | cron: 2 | - description: Repopulate the announcement every 1 hour 3 | url: /crons/set_announcement 4 | schedule: every 1 hours -------------------------------------------------------------------------------- /ConferenceCentral_Complete/index.yaml: -------------------------------------------------------------------------------- 1 | indexes: 2 | 3 | # AUTOGENERATED 4 | 5 | # This index.yaml is automatically updated whenever the dev_appserver 6 | # detects that a new type of query is run. If you want to manage the 7 | # index.yaml file manually, remove the above marker line (the line 8 | # saying "# AUTOGENERATED"). If you want to manage some indexes 9 | # manually, move them above the marker line. The index.yaml file is 10 | # automatically uploaded to the admin console when you next deploy 11 | # your application using appcfg.py. 12 | 13 | - kind: Conference 14 | properties: 15 | - name: city 16 | - name: maxAttendees 17 | - name: month 18 | - name: name 19 | 20 | - kind: Conference 21 | properties: 22 | - name: city 23 | - name: maxAttendees 24 | - name: month 25 | - name: topics 26 | - name: name 27 | 28 | - kind: Conference 29 | properties: 30 | - name: city 31 | - name: maxAttendees 32 | - name: name 33 | 34 | - kind: Conference 35 | properties: 36 | - name: city 37 | - name: month 38 | - name: name 39 | 40 | - kind: Conference 41 | properties: 42 | - name: city 43 | - name: month 44 | - name: topics 45 | - name: name 46 | 47 | - kind: Conference 48 | properties: 49 | - name: city 50 | - name: name 51 | 52 | - kind: Conference 53 | properties: 54 | - name: city 55 | - name: topics 56 | - name: name 57 | 58 | - kind: Conference 59 | properties: 60 | - name: maxAttendees 61 | - name: month 62 | - name: name 63 | 64 | - kind: Conference 65 | properties: 66 | - name: maxAttendees 67 | - name: month 68 | - name: topics 69 | - name: name 70 | 71 | - kind: Conference 72 | properties: 73 | - name: maxAttendees 74 | - name: name 75 | 76 | - kind: Conference 77 | properties: 78 | - name: maxAttendees 79 | - name: topics 80 | - name: name 81 | 82 | - kind: Conference 83 | properties: 84 | - name: month 85 | - name: name 86 | 87 | - kind: Conference 88 | properties: 89 | - name: month 90 | - name: topics 91 | - name: name 92 | 93 | - kind: Conference 94 | properties: 95 | - name: topics 96 | - name: name 97 | -------------------------------------------------------------------------------- /ConferenceCentral_Complete/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | main.py -- Udacity conference server-side Python App Engine 5 | HTTP controller handlers for memcache & task queue access 6 | 7 | $Id$ 8 | 9 | created by wesc on 2014 may 24 10 | 11 | """ 12 | 13 | __author__ = 'wesc+api@google.com (Wesley Chun)' 14 | 15 | import webapp2 16 | from google.appengine.api import app_identity 17 | from google.appengine.api import mail 18 | from conference import ConferenceApi 19 | 20 | class SetAnnouncementHandler(webapp2.RequestHandler): 21 | def get(self): 22 | """Set Announcement in Memcache.""" 23 | ConferenceApi._cacheAnnouncement() 24 | self.response.set_status(204) 25 | 26 | 27 | class SendConfirmationEmailHandler(webapp2.RequestHandler): 28 | def post(self): 29 | """Send email confirming Conference creation.""" 30 | mail.send_mail( 31 | 'noreply@%s.appspotmail.com' % ( 32 | app_identity.get_application_id()), # from 33 | self.request.get('email'), # to 34 | 'You created a new Conference!', # subj 35 | 'Hi, you have created a following ' # body 36 | 'conference:\r\n\r\n%s' % self.request.get( 37 | 'conferenceInfo') 38 | ) 39 | 40 | 41 | app = webapp2.WSGIApplication([ 42 | ('/crons/set_announcement', SetAnnouncementHandler), 43 | ('/tasks/send_confirmation_email', SendConfirmationEmailHandler), 44 | ], debug=True) 45 | -------------------------------------------------------------------------------- /ConferenceCentral_Complete/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """models.py 4 | 5 | Udacity conference server-side Python App Engine data & ProtoRPC models 6 | 7 | $Id: models.py,v 1.1 2014/05/24 22:01:10 wesc Exp $ 8 | 9 | created/forked from conferences.py by wesc on 2014 may 24 10 | 11 | """ 12 | 13 | __author__ = 'wesc+api@google.com (Wesley Chun)' 14 | 15 | import httplib 16 | import endpoints 17 | from protorpc import messages 18 | from google.appengine.ext import ndb 19 | 20 | class ConflictException(endpoints.ServiceException): 21 | """ConflictException -- exception mapped to HTTP 409 response""" 22 | http_status = httplib.CONFLICT 23 | 24 | class Profile(ndb.Model): 25 | """Profile -- User profile object""" 26 | displayName = ndb.StringProperty() 27 | mainEmail = ndb.StringProperty() 28 | teeShirtSize = ndb.StringProperty(default='NOT_SPECIFIED') 29 | conferenceKeysToAttend = ndb.StringProperty(repeated=True) 30 | 31 | class ProfileMiniForm(messages.Message): 32 | """ProfileMiniForm -- update Profile form message""" 33 | displayName = messages.StringField(1) 34 | teeShirtSize = messages.EnumField('TeeShirtSize', 2) 35 | 36 | class ProfileForm(messages.Message): 37 | """ProfileForm -- Profile outbound form message""" 38 | displayName = messages.StringField(1) 39 | mainEmail = messages.StringField(2) 40 | teeShirtSize = messages.EnumField('TeeShirtSize', 3) 41 | conferenceKeysToAttend = messages.StringField(4, repeated=True) 42 | 43 | class StringMessage(messages.Message): 44 | """StringMessage-- outbound (single) string message""" 45 | data = messages.StringField(1, required=True) 46 | 47 | class BooleanMessage(messages.Message): 48 | """BooleanMessage-- outbound Boolean value message""" 49 | data = messages.BooleanField(1) 50 | 51 | class Conference(ndb.Model): 52 | """Conference -- Conference object""" 53 | name = ndb.StringProperty(required=True) 54 | description = ndb.StringProperty() 55 | organizerUserId = ndb.StringProperty() 56 | topics = ndb.StringProperty(repeated=True) 57 | city = ndb.StringProperty() 58 | startDate = ndb.DateProperty() 59 | month = ndb.IntegerProperty() # TODO: do we need for indexing like Java? 60 | endDate = ndb.DateProperty() 61 | maxAttendees = ndb.IntegerProperty() 62 | seatsAvailable = ndb.IntegerProperty() 63 | 64 | class ConferenceForm(messages.Message): 65 | """ConferenceForm -- Conference outbound form message""" 66 | name = messages.StringField(1) 67 | description = messages.StringField(2) 68 | organizerUserId = messages.StringField(3) 69 | topics = messages.StringField(4, repeated=True) 70 | city = messages.StringField(5) 71 | startDate = messages.StringField(6) #DateTimeField() 72 | month = messages.IntegerField(7, variant=messages.Variant.INT32) 73 | maxAttendees = messages.IntegerField(8, variant=messages.Variant.INT32) 74 | seatsAvailable = messages.IntegerField(9, variant=messages.Variant.INT32) 75 | endDate = messages.StringField(10) #DateTimeField() 76 | websafeKey = messages.StringField(11) 77 | organizerDisplayName = messages.StringField(12) 78 | 79 | class ConferenceForms(messages.Message): 80 | """ConferenceForms -- multiple Conference outbound form message""" 81 | items = messages.MessageField(ConferenceForm, 1, repeated=True) 82 | 83 | class TeeShirtSize(messages.Enum): 84 | """TeeShirtSize -- t-shirt size enumeration value""" 85 | NOT_SPECIFIED = 1 86 | XS_M = 2 87 | XS_W = 3 88 | S_M = 4 89 | S_W = 5 90 | M_M = 6 91 | M_W = 7 92 | L_M = 8 93 | L_W = 9 94 | XL_M = 10 95 | XL_W = 11 96 | XXL_M = 12 97 | XXL_W = 13 98 | XXXL_M = 14 99 | XXXL_W = 15 100 | 101 | class ConferenceQueryForm(messages.Message): 102 | """ConferenceQueryForm -- Conference query inbound form message""" 103 | field = messages.StringField(1) 104 | operator = messages.StringField(2) 105 | value = messages.StringField(3) 106 | 107 | class ConferenceQueryForms(messages.Message): 108 | """ConferenceQueryForms -- multiple ConferenceQueryForm inbound form message""" 109 | filters = messages.MessageField(ConferenceQueryForm, 1, repeated=True) 110 | 111 | -------------------------------------------------------------------------------- /ConferenceCentral_Complete/settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """settings.py 4 | 5 | Udacity conference server-side Python App Engine app user settings 6 | 7 | $Id$ 8 | 9 | created/forked from conference.py by wesc on 2014 may 24 10 | 11 | """ 12 | 13 | # Replace the following lines with client IDs obtained from the APIs 14 | # Console or Cloud Console. 15 | WEB_CLIENT_ID = 'replace with Web client ID' 16 | ANDROID_CLIENT_ID = 'replace with Android client ID' 17 | IOS_CLIENT_ID = 'replace with iOS client ID' 18 | ANDROID_AUDIENCE = WEB_CLIENT_ID 19 | -------------------------------------------------------------------------------- /ConferenceCentral_Complete/static/bootstrap/css/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | padding-top: 70px; 7 | } 8 | 9 | #signInLink, #signOutLink { 10 | cursor: pointer; 11 | } 12 | 13 | /* To disable the Google+ Sign In Button, but gapi.signin.render should be invoked to store the credential 14 | if that is stored in cookie. */ 15 | #signInButton iframe { 16 | display: none; 17 | } 18 | 19 | .required { 20 | color: red; 21 | } 22 | 23 | .dismiss-messages { 24 | cursor: pointer; 25 | } 26 | 27 | /* In a small screen, make the alert message sticky to the top */ 28 | @media (max-width: 768px) { 29 | #messages.alert, #rootMessages.alert { 30 | position: fixed; 31 | left: 0; 32 | right: 0; 33 | top: 65px; 34 | z-index: 1000; 35 | } 36 | } 37 | 38 | .form-group-condensed { 39 | margin-top: 0; 40 | margin-bottom: 5px; 41 | } 42 | 43 | .label-separated { 44 | margin-right: 8px; 45 | } 46 | 47 | .spinner { 48 | position: fixed; 49 | top: 70px; 50 | z-index: 9999; 51 | } 52 | 53 | #signInButton { 54 | cursor: pointer; 55 | vertical-align: middle; 56 | } 57 | 58 | #profile-container { 59 | float: right; 60 | font-size: 85%; 61 | } 62 | 63 | #profile img { 64 | max-height: 35px; 65 | width: auto; 66 | vertical-align: middle; 67 | } 68 | 69 | #show-conferences-tab { 70 | margin-bottom: 20px; 71 | } 72 | 73 | ul#filters { 74 | list-style: none; 75 | padding-left: 0px; 76 | font-size: 85%; 77 | } 78 | 79 | ul#filters span.glyphicon-remove { 80 | font-size: 80%; 81 | } 82 | 83 | ul#conferences-list { 84 | list-style: none; 85 | } 86 | 87 | .intro-header { 88 | padding-top: 50px; 89 | padding-bottom: 50px; 90 | color: #f8f8f8; 91 | text-shadow: black 0.1em 0.1em 0.2em; 92 | background: url(/img/meeting-room.jpg) no-repeat center center; 93 | background-size: cover; 94 | text-align: center; 95 | } 96 | 97 | .intro-message { 98 | position: relative; 99 | padding-top: 5%; 100 | padding-bottom: 5%; 101 | vertical-align: middle; 102 | } 103 | 104 | .section-a { 105 | padding: 50px 0; 106 | } 107 | -------------------------------------------------------------------------------- /ConferenceCentral_Complete/static/bootstrap/css/offcanvas.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Style tweaks 3 | * -------------------------------------------------- 4 | */ 5 | html, 6 | body { 7 | overflow-x: hidden; /* Prevent scroll on narrow devices */ 8 | } 9 | 10 | footer { 11 | padding: 30px 0; 12 | } 13 | 14 | /* 15 | * Off Canvas 16 | * -------------------------------------------------- 17 | */ 18 | @media screen and (max-width: 767px) { 19 | .row-offcanvas { 20 | position: relative; 21 | -webkit-transition: all .25s ease-out; 22 | -moz-transition: all .25s ease-out; 23 | transition: all .25s ease-out; 24 | } 25 | 26 | .row-offcanvas-right { 27 | right: 0; 28 | } 29 | 30 | .row-offcanvas-left { 31 | left: 0; 32 | } 33 | 34 | .row-offcanvas-right 35 | .sidebar-offcanvas { 36 | right: -50%; /* 6 columns */ 37 | } 38 | 39 | .row-offcanvas-left 40 | .sidebar-offcanvas { 41 | left: -50%; /* 6 columns */ 42 | } 43 | 44 | .row-offcanvas-right.active { 45 | right: 50%; /* 6 columns */ 46 | } 47 | 48 | .row-offcanvas-left.active { 49 | left: 50%; /* 6 columns */ 50 | } 51 | 52 | .sidebar-offcanvas { 53 | position: absolute; 54 | top: 0; 55 | width: 50%; /* 6 columns */ 56 | } 57 | } -------------------------------------------------------------------------------- /ConferenceCentral_Complete/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/ConferenceCentral_Complete/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /ConferenceCentral_Complete/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/ConferenceCentral_Complete/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /ConferenceCentral_Complete/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/ConferenceCentral_Complete/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /ConferenceCentral_Complete/static/img/CloudPlatform_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/ConferenceCentral_Complete/static/img/CloudPlatform_logo.png -------------------------------------------------------------------------------- /ConferenceCentral_Complete/static/img/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/ConferenceCentral_Complete/static/img/ajax-loader.gif -------------------------------------------------------------------------------- /ConferenceCentral_Complete/static/img/business1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/ConferenceCentral_Complete/static/img/business1.jpg -------------------------------------------------------------------------------- /ConferenceCentral_Complete/static/img/business2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/ConferenceCentral_Complete/static/img/business2.jpg -------------------------------------------------------------------------------- /ConferenceCentral_Complete/static/img/business3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/ConferenceCentral_Complete/static/img/business3.jpg -------------------------------------------------------------------------------- /ConferenceCentral_Complete/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/ConferenceCentral_Complete/static/img/favicon.ico -------------------------------------------------------------------------------- /ConferenceCentral_Complete/static/img/meeting-room.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/ConferenceCentral_Complete/static/img/meeting-room.jpg -------------------------------------------------------------------------------- /ConferenceCentral_Complete/static/js/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc object 5 | * @name conferenceApp 6 | * @requires $routeProvider 7 | * @requires conferenceControllers 8 | * @requires ui.bootstrap 9 | * 10 | * @description 11 | * Root app, which routes and specifies the partial html and controller depending on the url requested. 12 | * 13 | */ 14 | var app = angular.module('conferenceApp', 15 | ['conferenceControllers', 'ngRoute', 'ui.bootstrap']). 16 | config(['$routeProvider', 17 | function ($routeProvider) { 18 | $routeProvider. 19 | when('/conference', { 20 | templateUrl: '/partials/show_conferences.html', 21 | controller: 'ShowConferenceCtrl' 22 | }). 23 | when('/conference/create', { 24 | templateUrl: '/partials/create_conferences.html', 25 | controller: 'CreateConferenceCtrl' 26 | }). 27 | when('/conference/detail/:websafeConferenceKey', { 28 | templateUrl: '/partials/conference_detail.html', 29 | controller: 'ConferenceDetailCtrl' 30 | }). 31 | when('/profile', { 32 | templateUrl: '/partials/profile.html', 33 | controller: 'MyProfileCtrl' 34 | }). 35 | when('/', { 36 | templateUrl: '/partials/home.html' 37 | }). 38 | otherwise({ 39 | redirectTo: '/' 40 | }); 41 | }]); 42 | 43 | /** 44 | * @ngdoc filter 45 | * @name startFrom 46 | * 47 | * @description 48 | * A filter that extracts an array from the specific index. 49 | * 50 | */ 51 | app.filter('startFrom', function () { 52 | /** 53 | * Extracts an array from the specific index. 54 | * 55 | * @param {Array} data 56 | * @param {Integer} start 57 | * @returns {Array|*} 58 | */ 59 | var filter = function (data, start) { 60 | return data.slice(start); 61 | } 62 | return filter; 63 | }); 64 | 65 | 66 | /** 67 | * @ngdoc constant 68 | * @name HTTP_ERRORS 69 | * 70 | * @description 71 | * Holds the constants that represent HTTP error codes. 72 | * 73 | */ 74 | app.constant('HTTP_ERRORS', { 75 | 'UNAUTHORIZED': 401 76 | }); 77 | 78 | 79 | /** 80 | * @ngdoc service 81 | * @name oauth2Provider 82 | * 83 | * @description 84 | * Service that holds the OAuth2 information shared across all the pages. 85 | * 86 | */ 87 | app.factory('oauth2Provider', function ($modal) { 88 | var oauth2Provider = { 89 | CLIENT_ID: 'web-client-id', 90 | SCOPES: 'email profile', 91 | signedIn: false 92 | } 93 | 94 | /** 95 | * Calls the OAuth2 authentication method. 96 | */ 97 | oauth2Provider.signIn = function (callback) { 98 | gapi.auth.signIn({ 99 | 'clientid': oauth2Provider.CLIENT_ID, 100 | 'cookiepolicy': 'single_host_origin', 101 | 'accesstype': 'online', 102 | 'approveprompt': 'auto', 103 | 'scope': oauth2Provider.SCOPES, 104 | 'callback': callback 105 | }); 106 | }; 107 | 108 | /** 109 | * Logs out the user. 110 | */ 111 | oauth2Provider.signOut = function () { 112 | gapi.auth.signOut(); 113 | // Explicitly set the invalid access token in order to make the API calls fail. 114 | gapi.auth.setToken({access_token: ''}) 115 | oauth2Provider.signedIn = false; 116 | }; 117 | 118 | /** 119 | * Shows the modal with Google+ sign in button. 120 | * 121 | * @returns {*|Window} 122 | */ 123 | oauth2Provider.showLoginModal = function() { 124 | var modalInstance = $modal.open({ 125 | templateUrl: '/partials/login.modal.html', 126 | controller: 'OAuth2LoginModalCtrl' 127 | }); 128 | return modalInstance; 129 | }; 130 | 131 | return oauth2Provider; 132 | }); 133 | -------------------------------------------------------------------------------- /ConferenceCentral_Complete/static/partials/conference_detail.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 8 |
9 | 10 |
11 |
12 | 13 |
14 |
15 |
16 |

{{conference.name}}

17 |
{{conference.description}}
18 |
19 | 20 | {{conference.maxAttendees - conference.seatsAvailable}} / {{conference.maxAttendees}} 21 |
22 |
23 | 24 | {{conference.organizerDisplayName}} 25 |
26 |

Register

28 |

Unregister

30 |
31 | 32 |
33 |
34 |
35 | 36 | {{conference.city}} 37 |
38 |
39 | 40 | 41 | {{topic}} 42 | 43 |
44 |
45 | 46 | {{conference.startDate | date:'dd-MMMM-yyyy'}} 47 |
48 |
49 | 50 | {{conference.endDate | date:'dd-MMMM-yyyy'}} 51 |
52 |
53 |
54 |
55 |
56 |
57 | -------------------------------------------------------------------------------- /ConferenceCentral_Complete/static/partials/create_conferences.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 8 |
9 | 10 |
11 |
12 |
13 |
14 |

Create a conference

15 | 16 |
17 |
18 | 19 | Required! 21 | 23 |
24 | 25 |
26 | 27 | 30 |
31 | 32 |
33 | 34 | 36 |
37 | 38 |
39 | 40 | 44 |
45 | 46 |
47 | 48 |

49 | 53 | 54 | 57 | 58 |

59 |
60 | 61 |
62 | 63 | End Date must be later or equal to Start Date! 65 |

66 | 70 | 71 | 74 | 75 |

76 |
77 | 78 |
79 | 80 | Must be an integer! 82 | 84 | 86 |
87 | 88 | 91 |
92 |
93 |
94 |
-------------------------------------------------------------------------------- /ConferenceCentral_Complete/static/partials/home.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

Welcome to Conference Central

6 | 7 |

Lets you manage conferences

8 |
9 | 17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |

View conferences

27 | 28 |

View by city, topics, date, max attendees.

29 | View conferences 30 |
31 |
32 | 33 |
34 |
35 |
36 | 37 |
38 |
39 |
40 |
41 |
42 |

Create new conferences

43 | 44 |

In 10 seconds or less.

45 | Create a conference 46 |
47 |
48 | 49 |
50 |
51 |
52 | 53 |
54 |
55 |
56 |
57 |
58 |

Update your profile

59 | View my profile 60 |
61 |
62 | 63 |
64 |
65 |
66 | -------------------------------------------------------------------------------- /ConferenceCentral_Complete/static/partials/login.modal.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Please sign in to complete this action.

4 |
5 | 8 |
-------------------------------------------------------------------------------- /ConferenceCentral_Complete/static/partials/profile.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 8 |
9 | 10 |
11 |
12 |
13 |
14 |

My Profile

15 |
16 |
17 | 18 | Changed 20 | 22 |
23 | 24 |
25 | 26 | Changed 28 | 32 |
33 | 34 | 37 |
38 |
39 |
40 |
-------------------------------------------------------------------------------- /ConferenceCentral_Complete/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Conference Central 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 67 | 68 |
69 |
70 |
71 |
72 | 73 | 75 |
76 |
77 |
78 | 79 |
80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /ConferenceCentral_Complete/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import time 4 | import uuid 5 | 6 | from google.appengine.api import urlfetch 7 | from models import Profile 8 | 9 | def getUserId(user, id_type="email"): 10 | if id_type == "email": 11 | return user.email() 12 | 13 | if id_type == "oauth": 14 | """A workaround implementation for getting userid.""" 15 | auth = os.getenv('HTTP_AUTHORIZATION') 16 | bearer, token = auth.split() 17 | token_type = 'id_token' 18 | if 'OAUTH_USER_ID' in os.environ: 19 | token_type = 'access_token' 20 | url = ('https://www.googleapis.com/oauth2/v1/tokeninfo?%s=%s' 21 | % (token_type, token)) 22 | user = {} 23 | wait = 1 24 | for i in range(3): 25 | resp = urlfetch.fetch(url) 26 | if resp.status_code == 200: 27 | user = json.loads(resp.content) 28 | break 29 | elif resp.status_code == 400 and 'invalid_token' in resp.content: 30 | url = ('https://www.googleapis.com/oauth2/v1/tokeninfo?%s=%s' 31 | % ('access_token', token)) 32 | else: 33 | time.sleep(wait) 34 | wait = wait + i 35 | return user.get('user_id', '') 36 | 37 | if id_type == "custom": 38 | # implement your own user_id creation and getting algorythm 39 | # this is just a sample that queries datastore for an existing profile 40 | # and generates an id if profile does not exist for an email 41 | profile = Conference.query(Conference.mainEmail == user.email()) 42 | if profile: 43 | return profile.id() 44 | else: 45 | return str(uuid.uuid1().get_hex()) 46 | -------------------------------------------------------------------------------- /Lesson_2/000_Hello_Endpoints/app.yaml: -------------------------------------------------------------------------------- 1 | # This file specifies your Python application's runtime configuration 2 | # including URL routing, versions, static file uploads, etc. See 3 | # https://developers.google.com/appengine/docs/python/config/appconfig 4 | # for details. 5 | 6 | # TODO: Enter your application id below. If you have signed up 7 | # using cloud.google.com/console use the "project id" for your application 8 | # id. 9 | application: your-project-id 10 | version: 1 11 | runtime: python27 12 | threadsafe: true 13 | api_version: 1 14 | 15 | # Handlers tell App Engine how to route requests to your application. 16 | handlers: 17 | 18 | # The following three handlers route requests to static resources: JS, CSS, 19 | # and HTML. 20 | - url: /static 21 | static_dir: static 22 | - url: / 23 | static_files: static/index.html 24 | upload: static/index\.html 25 | secure: always 26 | 27 | # This handler routes requests to your APIs to your Endpoints code. 28 | # See https://developers.google.com/appengine/docs/python/endpoints/ 29 | - url: /_ah/spi/.* 30 | script: helloworld_api.APPLICATION 31 | secure: always 32 | 33 | # Third party libraries that are included in the App Engine SDK must be listed 34 | # here if you want to use them. See 35 | # https://developers.google.com/appengine/docs/python/tools/libraries27 for 36 | # a list of libraries included in the SDK. Third party libs that are *not* part 37 | # of the App Engine SDK don't need to be listed here, instead add them to your 38 | # project directory, either as a git submodule or as a plain subdirectory. 39 | # Note that dependencies must be located in your project directory - packages 40 | # installed in the Python environment are not loaded by the App Engine 41 | # development server or deployment tools. 42 | # TODO: List any other App Engine SDK libs you may need here. 43 | libraries: 44 | 45 | # You must include the Endpoints library if you wish to serve an API. 46 | - name: endpoints 47 | version: 1.0 48 | 49 | -------------------------------------------------------------------------------- /Lesson_2/000_Hello_Endpoints/helloworld_api.py: -------------------------------------------------------------------------------- 1 | """Hello World API implemented using Google Cloud Endpoints. 2 | 3 | Contains declarations of endpoint, endpoint methods, 4 | as well as the ProtoRPC message class and container required 5 | for endpoint method definition. 6 | """ 7 | import endpoints 8 | from protorpc import messages 9 | from protorpc import message_types 10 | from protorpc import remote 11 | 12 | 13 | # If the request contains path or querystring arguments, 14 | # you cannot use a simple Message class. 15 | # Instead, you must use a ResourceContainer class 16 | REQUEST_CONTAINER = endpoints.ResourceContainer( 17 | message_types.VoidMessage, 18 | name=messages.StringField(1), 19 | ) 20 | 21 | 22 | package = 'Hello' 23 | 24 | 25 | class Hello(messages.Message): 26 | """String that stores a message.""" 27 | greeting = messages.StringField(1) 28 | 29 | 30 | @endpoints.api(name='helloworldendpoints', version='v1') 31 | class HelloWorldApi(remote.Service): 32 | """Helloworld API v1.""" 33 | 34 | @endpoints.method(message_types.VoidMessage, Hello, 35 | path = "sayHello", http_method='GET', name = "sayHello") 36 | def say_hello(self, request): 37 | return Hello(greeting="Hello World") 38 | 39 | @endpoints.method(REQUEST_CONTAINER, Hello, 40 | path = "sayHelloByName", http_method='GET', name = "sayHelloByName") 41 | def say_hello_by_name(self, request): 42 | greet = "Hello {}".format(request.name) 43 | return Hello(greeting=greet) 44 | 45 | 46 | APPLICATION = endpoints.api_server([HelloWorldApi]) 47 | -------------------------------------------------------------------------------- /Lesson_2/000_Hello_Endpoints/static/hello.js: -------------------------------------------------------------------------------- 1 | /* 2 | * http://stackoverflow.com/questions/18260815/use-gapi-client-javascript-to-execute-my-custom-google-api 3 | * https://developers.google.com/appengine/docs/java/endpoints/consume_js 4 | * https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiclientload 5 | * 6 | */ 7 | 8 | /** 9 | * After the client library has loaded, this init() function is called. 10 | * The init() function loads the helloworldendpoints API. 11 | */ 12 | 13 | function init() { 14 | 15 | // You need to pass the root path when you load your API 16 | // otherwise calls to execute the API run into a problem 17 | 18 | // rootpath will evaulate to either of these, depending on where the app is running: 19 | // //localhost:8080/_ah/api 20 | // //your-app-id/_ah/api 21 | 22 | var rootpath = "//" + window.location.host + "/_ah/api"; 23 | 24 | // Load the helloworldendpoints API 25 | // If loading completes successfully, call loadCallback function 26 | gapi.client.load('helloworldendpoints', 'v1', loadCallback, rootpath); 27 | } 28 | 29 | /* 30 | * When helloworldendpoints API has loaded, this callback is called. 31 | * 32 | * We need to wait until the helloworldendpoints API has loaded to 33 | * enable the actions for the buttons in index.html, 34 | * because the buttons call functions in the helloworldendpoints API 35 | */ 36 | function loadCallback () { 37 | // Enable the button actions 38 | enableButtons (); 39 | } 40 | 41 | function enableButtons () { 42 | // Set the onclick action for the first button 43 | btn = document.getElementById("input_greet_generically"); 44 | btn.onclick= function(){greetGenerically();}; 45 | 46 | // Update the button label now that the button is active 47 | btn.value="Click me for a generic greeting"; 48 | 49 | // Set the onclick action for the second button 50 | btn = document.getElementById("input_greet_by_name"); 51 | btn.onclick=function(){greetByName();}; 52 | 53 | // Update the button label now that the button is active 54 | btn.value="Click me for a personal greeting"; 55 | } 56 | 57 | /* 58 | * Execute a request to the sayHello() endpoints function 59 | */ 60 | function greetGenerically () { 61 | // Construct the request for the sayHello() function 62 | var request = gapi.client.helloworldendpoints.sayHello(); 63 | 64 | // Execute the request. 65 | // On success, pass the response to sayHelloCallback() 66 | request.execute(sayHelloCallback); 67 | } 68 | 69 | /* 70 | * Execute a request to the sayHelloByName() endpoints function. 71 | * Illustrates calling an endpoints function that takes an argument. 72 | */ 73 | function greetByName () { 74 | // Get the name from the name_field element 75 | var name = document.getElementById("name_field").value; 76 | 77 | // Call the sayHelloByName() function. 78 | // It takes one argument "name" 79 | // On success, pass the response to sayHelloCallback() 80 | var request = gapi.client.helloworldendpoints.sayHelloByName({'name': name}); 81 | request.execute(sayHelloCallback); 82 | } 83 | 84 | // Process the JSON response 85 | // In this case, just show an alert dialog box 86 | // displaying the value of the message field in the response 87 | function sayHelloCallback (response) { 88 | alert(response.greeting); 89 | } 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /Lesson_2/000_Hello_Endpoints/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello World Endpoints 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

My First Endpoints App

17 | 18 |

20 | 21 |

What is your name?

22 | 23 |

25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Lesson_2/001_Hello_Endpoints_Solutions/app.yaml: -------------------------------------------------------------------------------- 1 | # This file specifies your Python application's runtime configuration 2 | # including URL routing, versions, static file uploads, etc. See 3 | # https://developers.google.com/appengine/docs/python/config/appconfig 4 | # for details. 5 | 6 | # TODO: Enter your application id below. If you have signed up 7 | # using cloud.google.com/console use the "project id" for your application 8 | # id. 9 | application: your-project-id 10 | version: 1 11 | runtime: python27 12 | threadsafe: true 13 | api_version: 1 14 | 15 | # Handlers tell App Engine how to route requests to your application. 16 | handlers: 17 | 18 | # The following three handlers route requests to static resources: JS, CSS, 19 | # and HTML. 20 | - url: /static 21 | static_dir: static 22 | - url: / 23 | static_files: static/index.html 24 | upload: static/index\.html 25 | secure: always 26 | 27 | # This handler routes requests to your APIs to your Endpoints code. 28 | # See https://developers.google.com/appengine/docs/python/endpoints/ 29 | - url: /_ah/spi/.* 30 | script: helloworld_api.APPLICATION 31 | secure: always 32 | 33 | # Third party libraries that are included in the App Engine SDK must be listed 34 | # here if you want to use them. See 35 | # https://developers.google.com/appengine/docs/python/tools/libraries27 for 36 | # a list of libraries included in the SDK. Third party libs that are *not* part 37 | # of the App Engine SDK don't need to be listed here, instead add them to your 38 | # project directory, either as a git submodule or as a plain subdirectory. 39 | # Note that dependencies must be located in your project directory - packages 40 | # installed in the Python environment are not loaded by the App Engine 41 | # development server or deployment tools. 42 | # TODO: List any other App Engine SDK libs you may need here. 43 | libraries: 44 | 45 | # You must include the Endpoints library if you wish to serve an API. 46 | - name: endpoints 47 | version: 1.0 48 | 49 | -------------------------------------------------------------------------------- /Lesson_2/001_Hello_Endpoints_Solutions/helloworld_api.py: -------------------------------------------------------------------------------- 1 | """Hello World API implemented using Google Cloud Endpoints. 2 | 3 | Contains declarations of endpoint, endpoint methods, 4 | as well as the ProtoRPC message class and container required 5 | for endpoint method definition. 6 | """ 7 | import endpoints 8 | from protorpc import messages 9 | from protorpc import message_types 10 | from protorpc import remote 11 | 12 | 13 | # If the request contains path or querystring arguments, 14 | # you cannot use a simple Message class. 15 | # Instead, you must use a ResourceContainer class 16 | REQUEST_CONTAINER = endpoints.ResourceContainer( 17 | message_types.VoidMessage, 18 | name=messages.StringField(1), 19 | ) 20 | 21 | REQUEST_GREETING_CONTAINER = endpoints.ResourceContainer( 22 | period=messages.StringField(1), 23 | name=messages.StringField(2), 24 | ) 25 | 26 | package = 'Hello' 27 | 28 | 29 | class Hello(messages.Message): 30 | """String that stores a message.""" 31 | greeting = messages.StringField(1) 32 | 33 | 34 | @endpoints.api(name='helloworldendpoints', version='v1') 35 | class HelloWorldApi(remote.Service): 36 | """Helloworld API v1.""" 37 | 38 | @endpoints.method(message_types.VoidMessage, Hello, 39 | path = "sayHello", http_method='GET', name = "sayHello") 40 | def say_hello(self, request): 41 | return Hello(greeting="Hello World") 42 | 43 | @endpoints.method(REQUEST_CONTAINER, Hello, 44 | path = "sayHelloByName", http_method='GET', name = "sayHelloByName") 45 | def say_hello_by_name(self, request): 46 | greet = "Hello {}".format(request.name) 47 | return Hello(greeting=greet) 48 | 49 | @endpoints.method(REQUEST_GREETING_CONTAINER, Hello, 50 | path = "greetByPeriod", http_method='GET', name = "greetByPeriod") 51 | def greet_by_period(self, request): 52 | greet = "Good {} {}".format(request.period, request.name) 53 | return Hello(greeting=greet) 54 | 55 | 56 | APPLICATION = endpoints.api_server([HelloWorldApi]) 57 | -------------------------------------------------------------------------------- /Lesson_2/001_Hello_Endpoints_Solutions/static/hello.js: -------------------------------------------------------------------------------- 1 | /* 2 | * http://stackoverflow.com/questions/18260815/use-gapi-client-javascript-to-execute-my-custom-google-api 3 | * https://developers.google.com/appengine/docs/java/endpoints/consume_js 4 | * https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiclientload 5 | * 6 | */ 7 | 8 | /** 9 | * After the client library has loaded, this init() function is called. 10 | * The init() function loads the helloworldendpoints API. 11 | */ 12 | 13 | function init() { 14 | 15 | // You need to pass the root path when you load your API 16 | // otherwise calls to execute the API run into a problem 17 | 18 | // rootpath will evaulate to either of these, depending on where the app is running: 19 | // //localhost:8080/_ah/api 20 | // //your-app-id/_ah/api 21 | 22 | var rootpath = "//" + window.location.host + "/_ah/api"; 23 | 24 | // Load the helloworldendpoints API 25 | // If loading completes successfully, call loadCallback function 26 | gapi.client.load('helloworldendpoints', 'v1', loadCallback, rootpath); 27 | } 28 | 29 | /* 30 | * When helloworldendpoints API has loaded, this callback is called. 31 | * 32 | * We need to wait until the helloworldendpoints API has loaded to 33 | * enable the actions for the buttons in index.html, 34 | * because the buttons call functions in the helloworldendpoints API 35 | */ 36 | function loadCallback () { 37 | // Enable the button actions 38 | enableButtons (); 39 | } 40 | 41 | function enableButtons () { 42 | // Set the onclick action for the first button 43 | btn = document.getElementById("input_greet_generically"); 44 | btn.onclick= function(){greetGenerically();}; 45 | 46 | // Update the button label now that the button is active 47 | btn.value="Click me for a generic greeting"; 48 | 49 | // Set the onclick action for the second button 50 | btn = document.getElementById("input_greet_by_name"); 51 | btn.onclick=function(){greetByName();}; 52 | 53 | // Update the button label now that the button is active 54 | btn.value="Click me for a personal greeting"; 55 | } 56 | 57 | /* 58 | * Execute a request to the sayHello() endpoints function 59 | */ 60 | function greetGenerically () { 61 | // Construct the request for the sayHello() function 62 | var request = gapi.client.helloworldendpoints.sayHello(); 63 | 64 | // Execute the request. 65 | // On success, pass the response to sayHelloCallback() 66 | request.execute(sayHelloCallback); 67 | } 68 | 69 | /* 70 | * Execute a request to the sayHelloByName() endpoints function. 71 | * Illustrates calling an endpoints function that takes an argument. 72 | */ 73 | function greetByName () { 74 | // Get the name from the name_field element 75 | var name = document.getElementById("name_field").value; 76 | 77 | // Call the sayHelloByName() function. 78 | // It takes one argument "name" 79 | // On success, pass the response to sayHelloCallback() 80 | var request = gapi.client.helloworldendpoints.sayHelloByName({'name': name}); 81 | request.execute(sayHelloCallback); 82 | } 83 | 84 | // Process the JSON response 85 | // In this case, just show an alert dialog box 86 | // displaying the value of the message field in the response 87 | function sayHelloCallback (response) { 88 | alert(response.greeting); 89 | } 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /Lesson_2/001_Hello_Endpoints_Solutions/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello World Endpoints 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

My First Endpoints App

17 | 18 |

20 | 21 |

What is your name?

22 | 23 |

25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Lesson_2/00_Conference_Central/README.md: -------------------------------------------------------------------------------- 1 | App Engine application for the Udacity training course. 2 | 3 | ## Products 4 | - [App Engine][1] 5 | 6 | ## Language 7 | - [Python][2] 8 | 9 | ## APIs 10 | - [Google Cloud Endpoints][3] 11 | 12 | ## Setup Instructions 13 | 1. Update the value of `application` in `app.yaml` to the app ID you 14 | have registered in the App Engine admin console and would like to use to host 15 | your instance of this sample. 16 | 1. Update the values at the top of `settings.py` to 17 | reflect the respective client IDs you have registered in the 18 | [Developer Console][4]. 19 | 1. Update the value of CLIENT_ID in `static/js/app.js` to the Web client ID 20 | 1. (Optional) Mark the configuration files as unchanged as follows: 21 | `$ git update-index --assume-unchanged app.yaml settings.py static/js/app.js` 22 | 1. Run the app with the devserver using `dev_appserver.py DIR`, and ensure it's running by visiting 23 | your local server's address (by default [localhost:8080][5].) 24 | 1. Generate your client library(ies) with [the endpoints tool][6]. 25 | 1. Deploy your application. 26 | 27 | 28 | [1]: https://developers.google.com/appengine 29 | [2]: http://python.org 30 | [3]: https://developers.google.com/appengine/docs/python/endpoints/ 31 | [4]: https://console.developers.google.com/ 32 | [5]: https://localhost:8080/ 33 | [6]: https://developers.google.com/appengine/docs/python/endpoints/endpoints_tool 34 | -------------------------------------------------------------------------------- /Lesson_2/00_Conference_Central/app.yaml: -------------------------------------------------------------------------------- 1 | application: ud858-dev-playground 2 | version: 1 3 | runtime: python27 4 | api_version: 1 5 | threadsafe: yes 6 | 7 | handlers: # static then dynamic 8 | 9 | - url: /favicon\.ico 10 | static_files: favicon.ico 11 | upload: favicon\.ico 12 | 13 | - url: /js 14 | static_dir: static/js 15 | 16 | - url: /img 17 | static_dir: static/img 18 | 19 | - url: /css 20 | static_dir: static/bootstrap/css 21 | 22 | - url: /fonts 23 | static_dir: static/fonts 24 | 25 | - url: /partials 26 | static_dir: static/partials 27 | 28 | - url: / 29 | static_files: templates/index.html 30 | upload: templates/index\.html 31 | secure: always 32 | 33 | - url: /_ah/spi/.* 34 | script: conference.api 35 | secure: always 36 | 37 | libraries: 38 | 39 | - name: endpoints 40 | version: latest 41 | 42 | # pycrypto library used for OAuth2 (req'd for authenticated APIs) 43 | - name: pycrypto 44 | version: latest 45 | -------------------------------------------------------------------------------- /Lesson_2/00_Conference_Central/conference.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | conference.py -- Udacity conference server-side Python App Engine API; 5 | uses Google Cloud Endpoints 6 | 7 | $Id: conference.py,v 1.25 2014/05/24 23:42:19 wesc Exp wesc $ 8 | 9 | created by wesc on 2014 apr 21 10 | 11 | """ 12 | 13 | __author__ = 'wesc+api@google.com (Wesley Chun)' 14 | 15 | 16 | from datetime import datetime 17 | import json 18 | import os 19 | import time 20 | 21 | import endpoints 22 | from protorpc import messages 23 | from protorpc import message_types 24 | from protorpc import remote 25 | 26 | from google.appengine.api import urlfetch 27 | from google.appengine.ext import ndb 28 | 29 | from models import Profile 30 | from models import ProfileMiniForm 31 | from models import ProfileForm 32 | from models import TeeShirtSize 33 | 34 | from settings import WEB_CLIENT_ID 35 | 36 | EMAIL_SCOPE = endpoints.EMAIL_SCOPE 37 | API_EXPLORER_CLIENT_ID = endpoints.API_EXPLORER_CLIENT_ID 38 | 39 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 40 | 41 | @endpoints.api( name='conference', 42 | version='v1', 43 | allowed_client_ids=[WEB_CLIENT_ID, API_EXPLORER_CLIENT_ID], 44 | scopes=[EMAIL_SCOPE]) 45 | class ConferenceApi(remote.Service): 46 | """Conference API v0.1""" 47 | 48 | # - - - Profile objects - - - - - - - - - - - - - - - - - - - 49 | 50 | def _copyProfileToForm(self, prof): 51 | """Copy relevant fields from Profile to ProfileForm.""" 52 | # copy relevant fields from Profile to ProfileForm 53 | pf = ProfileForm() 54 | for field in pf.all_fields(): 55 | if hasattr(prof, field.name): 56 | # convert t-shirt string to Enum; just copy others 57 | if field.name == 'teeShirtSize': 58 | setattr(pf, field.name, getattr(TeeShirtSize, getattr(prof, field.name))) 59 | else: 60 | setattr(pf, field.name, getattr(prof, field.name)) 61 | pf.check_initialized() 62 | return pf 63 | 64 | 65 | def _getProfileFromUser(self): 66 | """Return user Profile from datastore, creating new one if non-existent.""" 67 | ## TODO 2 68 | ## step 1: make sure user is authed 69 | ## uncomment the following lines: 70 | # user = endpoints.get_current_user() 71 | # if not user: 72 | # raise endpoints.UnauthorizedException('Authorization required') 73 | profile = None 74 | ## step 2: create a new Profile from logged in user data 75 | ## you can use user.nickname() to get displayName 76 | ## and user.email() to get mainEmail 77 | if not profile: 78 | profile = Profile( 79 | userId = None, 80 | key = None, 81 | displayName = "Test", 82 | mainEmail= None, 83 | teeShirtSize = str(TeeShirtSize.NOT_SPECIFIED), 84 | ) 85 | 86 | return profile # return Profile 87 | 88 | 89 | def _doProfile(self, save_request=None): 90 | """Get user Profile and return to user, possibly updating it first.""" 91 | # get user Profile 92 | prof = self._getProfileFromUser() 93 | 94 | # if saveProfile(), process user-modifyable fields 95 | if save_request: 96 | for field in ('displayName', 'teeShirtSize'): 97 | if hasattr(save_request, field): 98 | val = getattr(save_request, field) 99 | if val: 100 | setattr(prof, field, str(val)) 101 | 102 | # return ProfileForm 103 | return self._copyProfileToForm(prof) 104 | 105 | 106 | @endpoints.method(message_types.VoidMessage, ProfileForm, 107 | path='profile', http_method='GET', name='getProfile') 108 | def getProfile(self, request): 109 | """Return user profile.""" 110 | return self._doProfile() 111 | 112 | # TODO 1 113 | # 1. change request class 114 | # 2. pass request to _doProfile function 115 | @endpoints.method(message_types.VoidMessage, ProfileForm, 116 | path='profile', http_method='POST', name='saveProfile') 117 | def saveProfile(self, request): 118 | """Update & return user profile.""" 119 | return self._doProfile() 120 | 121 | 122 | # registers API 123 | api = endpoints.api_server([ConferenceApi]) 124 | -------------------------------------------------------------------------------- /Lesson_2/00_Conference_Central/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """models.py 4 | 5 | Udacity conference server-side Python App Engine data & ProtoRPC models 6 | 7 | $Id: models.py,v 1.1 2014/05/24 22:01:10 wesc Exp $ 8 | 9 | created/forked from conferences.py by wesc on 2014 may 24 10 | 11 | """ 12 | 13 | __author__ = 'wesc+api@google.com (Wesley Chun)' 14 | 15 | import httplib 16 | import endpoints 17 | from protorpc import messages 18 | from google.appengine.ext import ndb 19 | 20 | 21 | class Profile(ndb.Model): 22 | """Profile -- User profile object""" 23 | userId = ndb.StringProperty() 24 | displayName = ndb.StringProperty() 25 | mainEmail = ndb.StringProperty() 26 | teeShirtSize = ndb.StringProperty(default='NOT_SPECIFIED') 27 | 28 | 29 | class ProfileMiniForm(messages.Message): 30 | """ProfileMiniForm -- update Profile form message""" 31 | displayName = messages.StringField(1) 32 | teeShirtSize = messages.EnumField('TeeShirtSize', 2) 33 | 34 | 35 | class ProfileForm(messages.Message): 36 | """ProfileForm -- Profile outbound form message""" 37 | userId = messages.StringField(1) 38 | displayName = messages.StringField(2) 39 | mainEmail = messages.StringField(3) 40 | teeShirtSize = messages.EnumField('TeeShirtSize', 4) 41 | 42 | 43 | class TeeShirtSize(messages.Enum): 44 | """TeeShirtSize -- t-shirt size enumeration value""" 45 | NOT_SPECIFIED = 1 46 | XS_M = 2 47 | XS_W = 3 48 | S_M = 4 49 | S_W = 5 50 | M_M = 6 51 | M_W = 7 52 | L_M = 8 53 | L_W = 9 54 | XL_M = 10 55 | XL_W = 11 56 | XXL_M = 12 57 | XXL_W = 13 58 | XXXL_M = 14 59 | XXXL_W = 15 60 | -------------------------------------------------------------------------------- /Lesson_2/00_Conference_Central/settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """settings.py 4 | 5 | Udacity conference server-side Python App Engine app user settings 6 | 7 | $Id$ 8 | 9 | created/forked from conference.py by wesc on 2014 may 24 10 | 11 | """ 12 | 13 | # Replace the following lines with client IDs obtained from the APIs 14 | # Console or Cloud Console. 15 | WEB_CLIENT_ID = 'replace with Web client ID' 16 | 17 | -------------------------------------------------------------------------------- /Lesson_2/00_Conference_Central/static/bootstrap/css/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | padding-top: 70px; 7 | } 8 | 9 | #signInLink, #signOutLink { 10 | cursor: pointer; 11 | } 12 | 13 | /* To disable the Google+ Sign In Button, but gapi.signin.render should be invoked to store the credential 14 | if that is stored in cookie. */ 15 | #signInButton iframe { 16 | display: none; 17 | } 18 | 19 | .required { 20 | color: red; 21 | } 22 | 23 | .dismiss-messages { 24 | cursor: pointer; 25 | } 26 | 27 | /* In a small screen, make the alert message sticky to the top */ 28 | @media (max-width: 768px) { 29 | #messages.alert, #rootMessages.alert { 30 | position: fixed; 31 | left: 0; 32 | right: 0; 33 | top: 65px; 34 | z-index: 1000; 35 | } 36 | } 37 | 38 | .form-group-condensed { 39 | margin-top: 0; 40 | margin-bottom: 5px; 41 | } 42 | 43 | .label-separated { 44 | margin-right: 8px; 45 | } 46 | 47 | .spinner { 48 | position: fixed; 49 | top: 70px; 50 | z-index: 9999; 51 | } 52 | 53 | #signInButton { 54 | cursor: pointer; 55 | vertical-align: middle; 56 | } 57 | 58 | #profile-container { 59 | float: right; 60 | font-size: 85%; 61 | } 62 | 63 | #profile img { 64 | max-height: 35px; 65 | width: auto; 66 | vertical-align: middle; 67 | } 68 | 69 | #show-conferences-tab { 70 | margin-bottom: 20px; 71 | } 72 | 73 | ul#filters { 74 | list-style: none; 75 | padding-left: 0px; 76 | font-size: 85%; 77 | } 78 | 79 | ul#filters span.glyphicon-remove { 80 | font-size: 80%; 81 | } 82 | 83 | ul#conferences-list { 84 | list-style: none; 85 | } 86 | 87 | .intro-header { 88 | padding-top: 50px; 89 | padding-bottom: 50px; 90 | color: #f8f8f8; 91 | text-shadow: black 0.1em 0.1em 0.2em; 92 | background: url(/img/meeting-room.jpg) no-repeat center center; 93 | background-size: cover; 94 | text-align: center; 95 | } 96 | 97 | .intro-message { 98 | position: relative; 99 | padding-top: 5%; 100 | padding-bottom: 5%; 101 | vertical-align: middle; 102 | } 103 | 104 | .section-a { 105 | padding: 50px 0; 106 | } 107 | -------------------------------------------------------------------------------- /Lesson_2/00_Conference_Central/static/bootstrap/css/offcanvas.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Style tweaks 3 | * -------------------------------------------------- 4 | */ 5 | html, 6 | body { 7 | overflow-x: hidden; /* Prevent scroll on narrow devices */ 8 | } 9 | 10 | footer { 11 | padding: 30px 0; 12 | } 13 | 14 | /* 15 | * Off Canvas 16 | * -------------------------------------------------- 17 | */ 18 | @media screen and (max-width: 767px) { 19 | .row-offcanvas { 20 | position: relative; 21 | -webkit-transition: all .25s ease-out; 22 | -moz-transition: all .25s ease-out; 23 | transition: all .25s ease-out; 24 | } 25 | 26 | .row-offcanvas-right { 27 | right: 0; 28 | } 29 | 30 | .row-offcanvas-left { 31 | left: 0; 32 | } 33 | 34 | .row-offcanvas-right 35 | .sidebar-offcanvas { 36 | right: -50%; /* 6 columns */ 37 | } 38 | 39 | .row-offcanvas-left 40 | .sidebar-offcanvas { 41 | left: -50%; /* 6 columns */ 42 | } 43 | 44 | .row-offcanvas-right.active { 45 | right: 50%; /* 6 columns */ 46 | } 47 | 48 | .row-offcanvas-left.active { 49 | left: 50%; /* 6 columns */ 50 | } 51 | 52 | .sidebar-offcanvas { 53 | position: absolute; 54 | top: 0; 55 | width: 50%; /* 6 columns */ 56 | } 57 | } -------------------------------------------------------------------------------- /Lesson_2/00_Conference_Central/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_2/00_Conference_Central/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /Lesson_2/00_Conference_Central/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_2/00_Conference_Central/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /Lesson_2/00_Conference_Central/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_2/00_Conference_Central/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /Lesson_2/00_Conference_Central/static/img/CloudPlatform_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_2/00_Conference_Central/static/img/CloudPlatform_logo.png -------------------------------------------------------------------------------- /Lesson_2/00_Conference_Central/static/img/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_2/00_Conference_Central/static/img/ajax-loader.gif -------------------------------------------------------------------------------- /Lesson_2/00_Conference_Central/static/img/business1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_2/00_Conference_Central/static/img/business1.jpg -------------------------------------------------------------------------------- /Lesson_2/00_Conference_Central/static/img/business2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_2/00_Conference_Central/static/img/business2.jpg -------------------------------------------------------------------------------- /Lesson_2/00_Conference_Central/static/img/business3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_2/00_Conference_Central/static/img/business3.jpg -------------------------------------------------------------------------------- /Lesson_2/00_Conference_Central/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_2/00_Conference_Central/static/img/favicon.ico -------------------------------------------------------------------------------- /Lesson_2/00_Conference_Central/static/img/meeting-room.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_2/00_Conference_Central/static/img/meeting-room.jpg -------------------------------------------------------------------------------- /Lesson_2/00_Conference_Central/static/js/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc object 5 | * @name conferenceApp 6 | * @requires $routeProvider 7 | * @requires conferenceControllers 8 | * @requires ui.bootstrap 9 | * 10 | * @description 11 | * Root app, which routes and specifies the partial html and controller depending on the url requested. 12 | * 13 | */ 14 | var app = angular.module('conferenceApp', 15 | ['conferenceControllers', 'ngRoute', 'ui.bootstrap']). 16 | config(['$routeProvider', 17 | function ($routeProvider) { 18 | $routeProvider. 19 | when('/conference', { 20 | templateUrl: '/partials/show_conferences.html', 21 | controller: 'ShowConferenceCtrl' 22 | }). 23 | when('/conference/create', { 24 | templateUrl: '/partials/create_conferences.html', 25 | controller: 'CreateConferenceCtrl' 26 | }). 27 | when('/conference/detail/:websafeConferenceKey', { 28 | templateUrl: '/partials/conference_detail.html', 29 | controller: 'ConferenceDetailCtrl' 30 | }). 31 | when('/profile', { 32 | templateUrl: '/partials/profile.html', 33 | controller: 'MyProfileCtrl' 34 | }). 35 | when('/', { 36 | templateUrl: '/partials/home.html' 37 | }). 38 | otherwise({ 39 | redirectTo: '/' 40 | }); 41 | }]); 42 | 43 | /** 44 | * @ngdoc filter 45 | * @name startFrom 46 | * 47 | * @description 48 | * A filter that extracts an array from the specific index. 49 | * 50 | */ 51 | app.filter('startFrom', function () { 52 | /** 53 | * Extracts an array from the specific index. 54 | * 55 | * @param {Array} data 56 | * @param {Integer} start 57 | * @returns {Array|*} 58 | */ 59 | var filter = function (data, start) { 60 | return data.slice(start); 61 | } 62 | return filter; 63 | }); 64 | 65 | 66 | /** 67 | * @ngdoc constant 68 | * @name HTTP_ERRORS 69 | * 70 | * @description 71 | * Holds the constants that represent HTTP error codes. 72 | * 73 | */ 74 | app.constant('HTTP_ERRORS', { 75 | 'UNAUTHORIZED': 401 76 | }); 77 | 78 | 79 | /** 80 | * @ngdoc service 81 | * @name oauth2Provider 82 | * 83 | * @description 84 | * Service that holds the OAuth2 information shared across all the pages. 85 | * 86 | */ 87 | app.factory('oauth2Provider', function ($modal) { 88 | var oauth2Provider = { 89 | CLIENT_ID: 'web-client-id', 90 | SCOPES: 'email profile', 91 | signedIn: false 92 | } 93 | 94 | /** 95 | * Calls the OAuth2 authentication method. 96 | */ 97 | oauth2Provider.signIn = function (callback) { 98 | gapi.auth.signIn({ 99 | 'clientid': oauth2Provider.CLIENT_ID, 100 | 'cookiepolicy': 'single_host_origin', 101 | 'accesstype': 'online', 102 | 'approveprompt': 'auto', 103 | 'scope': oauth2Provider.SCOPES, 104 | 'callback': callback 105 | }); 106 | }; 107 | 108 | /** 109 | * Logs out the user. 110 | */ 111 | oauth2Provider.signOut = function () { 112 | gapi.auth.signOut(); 113 | // Explicitly set the invalid access token in order to make the API calls fail. 114 | gapi.auth.setToken({access_token: ''}) 115 | oauth2Provider.signedIn = false; 116 | }; 117 | 118 | /** 119 | * Shows the modal with Google+ sign in button. 120 | * 121 | * @returns {*|Window} 122 | */ 123 | oauth2Provider.showLoginModal = function() { 124 | var modalInstance = $modal.open({ 125 | templateUrl: '/partials/login.modal.html', 126 | controller: 'OAuth2LoginModalCtrl' 127 | }); 128 | return modalInstance; 129 | }; 130 | 131 | return oauth2Provider; 132 | }); 133 | -------------------------------------------------------------------------------- /Lesson_2/00_Conference_Central/static/partials/conference_detail.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 8 |
9 | 10 |
11 |
12 | 13 |
14 |
15 |
16 |

{{conference.name}}

17 |
{{conference.description}}
18 |
19 | 20 | {{conference.maxAttendees - conference.seatsAvailable}} / {{conference.maxAttendees}} 21 |
22 |
23 | 24 | {{conference.organizerDisplayName}} 25 |
26 |

Register

28 |

Unregister

30 |
31 | 32 |
33 |
34 |
35 | 36 | {{conference.city}} 37 |
38 |
39 | 40 | 41 | {{topic}} 42 | 43 |
44 |
45 | 46 | {{conference.startDate | date:'dd-MMMM-yyyy'}} 47 |
48 |
49 | 50 | {{conference.endDate | date:'dd-MMMM-yyyy'}} 51 |
52 |
53 |
54 |
55 |
56 |
57 | -------------------------------------------------------------------------------- /Lesson_2/00_Conference_Central/static/partials/home.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

Welcome to Conference Central

6 | 7 |

Lets you manage conferences

8 |
9 | 17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |

View conferences

27 | 28 |

View by city, topics, date, max attendees.

29 | View conferences 30 |
31 |
32 | 33 |
34 |
35 |
36 | 37 |
38 |
39 |
40 |
41 |
42 |

Create new conferences

43 | 44 |

In 10 seconds or less.

45 | Create a conference 46 |
47 |
48 | 49 |
50 |
51 |
52 | 53 |
54 |
55 |
56 |
57 |
58 |

Update your profile

59 | View my profile 60 |
61 |
62 | 63 |
64 |
65 |
66 | -------------------------------------------------------------------------------- /Lesson_2/00_Conference_Central/static/partials/login.modal.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Please sign in to complete this action.

4 |
5 | 8 |
-------------------------------------------------------------------------------- /Lesson_2/00_Conference_Central/static/partials/profile.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 8 |
9 | 10 |
11 |
12 |
13 |
14 |

My Profile

15 |
16 |
17 | 18 | Changed 20 | 22 |
23 | 24 |
25 | 26 | Changed 28 | 32 |
33 | 34 | 37 |
38 |
39 |
40 |
-------------------------------------------------------------------------------- /Lesson_2/00_Conference_Central/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Conference Central 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 67 | 68 |
69 |
70 |
71 |
72 | 73 | 75 |
76 |
77 |
78 | 79 |
80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Lesson_3/00_Conference_Central/README.md: -------------------------------------------------------------------------------- 1 | App Engine application for the Udacity training course. 2 | 3 | ## Products 4 | - [App Engine][1] 5 | 6 | ## Language 7 | - [Python][2] 8 | 9 | ## APIs 10 | - [Google Cloud Endpoints][3] 11 | 12 | ## Setup Instructions 13 | 1. Update the value of `application` in `app.yaml` to the app ID you 14 | have registered in the App Engine admin console and would like to use to host 15 | your instance of this sample. 16 | 1. Update the values at the top of `settings.py` to 17 | reflect the respective client IDs you have registered in the 18 | [Developer Console][4]. 19 | 1. Update the value of CLIENT_ID in `static/js/app.js` to the Web client ID 20 | 1. (Optional) Mark the configuration files as unchanged as follows: 21 | `$ git update-index --assume-unchanged app.yaml settings.py static/js/app.js` 22 | 1. Run the app with the devserver using `dev_appserver.py DIR`, and ensure it's running by visiting 23 | your local server's address (by default [localhost:8080][5].) 24 | 1. Generate your client library(ies) with [the endpoints tool][6]. 25 | 1. Deploy your application. 26 | 27 | 28 | [1]: https://developers.google.com/appengine 29 | [2]: http://python.org 30 | [3]: https://developers.google.com/appengine/docs/python/endpoints/ 31 | [4]: https://console.developers.google.com/ 32 | [5]: https://localhost:8080/ 33 | [6]: https://developers.google.com/appengine/docs/python/endpoints/endpoints_tool 34 | -------------------------------------------------------------------------------- /Lesson_3/00_Conference_Central/app.yaml: -------------------------------------------------------------------------------- 1 | application: ud858-dev-playground 2 | version: 1 3 | runtime: python27 4 | api_version: 1 5 | threadsafe: yes 6 | 7 | handlers: # static then dynamic 8 | 9 | - url: /favicon\.ico 10 | static_files: favicon.ico 11 | upload: favicon\.ico 12 | 13 | - url: /js 14 | static_dir: static/js 15 | 16 | - url: /img 17 | static_dir: static/img 18 | 19 | - url: /css 20 | static_dir: static/bootstrap/css 21 | 22 | - url: /fonts 23 | static_dir: static/fonts 24 | 25 | - url: /partials 26 | static_dir: static/partials 27 | 28 | - url: / 29 | static_files: templates/index.html 30 | upload: templates/index\.html 31 | secure: always 32 | 33 | - url: /_ah/spi/.* 34 | script: conference.api 35 | secure: always 36 | 37 | libraries: 38 | 39 | - name: endpoints 40 | version: latest 41 | 42 | # pycrypto library used for OAuth2 (req'd for authenticated APIs) 43 | - name: pycrypto 44 | version: latest 45 | -------------------------------------------------------------------------------- /Lesson_3/00_Conference_Central/conference.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | conference.py -- Udacity conference server-side Python App Engine API; 5 | uses Google Cloud Endpoints 6 | 7 | $Id: conference.py,v 1.25 2014/05/24 23:42:19 wesc Exp wesc $ 8 | 9 | created by wesc on 2014 apr 21 10 | 11 | """ 12 | 13 | __author__ = 'wesc+api@google.com (Wesley Chun)' 14 | 15 | 16 | from datetime import datetime 17 | 18 | import endpoints 19 | from protorpc import messages 20 | from protorpc import message_types 21 | from protorpc import remote 22 | 23 | from google.appengine.ext import ndb 24 | 25 | from models import Profile 26 | from models import ProfileMiniForm 27 | from models import ProfileForm 28 | from models import TeeShirtSize 29 | 30 | from settings import WEB_CLIENT_ID 31 | 32 | EMAIL_SCOPE = endpoints.EMAIL_SCOPE 33 | API_EXPLORER_CLIENT_ID = endpoints.API_EXPLORER_CLIENT_ID 34 | 35 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 36 | 37 | 38 | @endpoints.api( name='conference', 39 | version='v1', 40 | allowed_client_ids=[WEB_CLIENT_ID, API_EXPLORER_CLIENT_ID], 41 | scopes=[EMAIL_SCOPE]) 42 | class ConferenceApi(remote.Service): 43 | """Conference API v0.1""" 44 | 45 | # - - - Profile objects - - - - - - - - - - - - - - - - - - - 46 | 47 | def _copyProfileToForm(self, prof): 48 | """Copy relevant fields from Profile to ProfileForm.""" 49 | # copy relevant fields from Profile to ProfileForm 50 | pf = ProfileForm() 51 | for field in pf.all_fields(): 52 | if hasattr(prof, field.name): 53 | # convert t-shirt string to Enum; just copy others 54 | if field.name == 'teeShirtSize': 55 | setattr(pf, field.name, getattr(TeeShirtSize, getattr(prof, field.name))) 56 | else: 57 | setattr(pf, field.name, getattr(prof, field.name)) 58 | pf.check_initialized() 59 | return pf 60 | 61 | 62 | def _getProfileFromUser(self): 63 | """Return user Profile from datastore, creating new one if non-existent.""" 64 | user = endpoints.get_current_user() 65 | if not user: 66 | raise endpoints.UnauthorizedException('Authorization required') 67 | 68 | # TODO 1 69 | # step 1. copy utils.py from additions folder to this folder 70 | # and import getUserId from it 71 | # step 2. get user id by calling getUserId(user) 72 | # step 3. create a new key of kind Profile from the id 73 | 74 | # TODO 3 75 | # get the entity from datastore by using get() on the key 76 | profile = None 77 | if not profile: 78 | profile = Profile( 79 | key = None, # TODO 1 step 4. replace with the key from step 3 80 | displayName = user.nickname(), 81 | mainEmail= user.email(), 82 | teeShirtSize = str(TeeShirtSize.NOT_SPECIFIED), 83 | ) 84 | # TODO 2 85 | # save the profile to datastore 86 | 87 | return profile # return Profile 88 | 89 | 90 | def _doProfile(self, save_request=None): 91 | """Get user Profile and return to user, possibly updating it first.""" 92 | # get user Profile 93 | prof = self._getProfileFromUser() 94 | 95 | # if saveProfile(), process user-modifyable fields 96 | if save_request: 97 | for field in ('displayName', 'teeShirtSize'): 98 | if hasattr(save_request, field): 99 | val = getattr(save_request, field) 100 | if val: 101 | setattr(prof, field, str(val)) 102 | # TODO 4 103 | # put the modified profile to datastore 104 | 105 | # return ProfileForm 106 | return self._copyProfileToForm(prof) 107 | 108 | 109 | @endpoints.method(message_types.VoidMessage, ProfileForm, 110 | path='profile', http_method='GET', name='getProfile') 111 | def getProfile(self, request): 112 | """Return user profile.""" 113 | return self._doProfile() 114 | 115 | 116 | @endpoints.method(ProfileMiniForm, ProfileForm, 117 | path='profile', http_method='POST', name='saveProfile') 118 | def saveProfile(self, request): 119 | """Update & return user profile.""" 120 | return self._doProfile(request) 121 | 122 | 123 | # registers API 124 | api = endpoints.api_server([ConferenceApi]) 125 | -------------------------------------------------------------------------------- /Lesson_3/00_Conference_Central/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """models.py 4 | 5 | Udacity conference server-side Python App Engine data & ProtoRPC models 6 | 7 | $Id: models.py,v 1.1 2014/05/24 22:01:10 wesc Exp $ 8 | 9 | created/forked from conferences.py by wesc on 2014 may 24 10 | 11 | """ 12 | 13 | __author__ = 'wesc+api@google.com (Wesley Chun)' 14 | 15 | import httplib 16 | import endpoints 17 | from protorpc import messages 18 | from google.appengine.ext import ndb 19 | 20 | 21 | class Profile(ndb.Model): 22 | """Profile -- User profile object""" 23 | displayName = ndb.StringProperty() 24 | mainEmail = ndb.StringProperty() 25 | teeShirtSize = ndb.StringProperty(default='NOT_SPECIFIED') 26 | 27 | 28 | class ProfileMiniForm(messages.Message): 29 | """ProfileMiniForm -- update Profile form message""" 30 | displayName = messages.StringField(1) 31 | teeShirtSize = messages.EnumField('TeeShirtSize', 2) 32 | 33 | 34 | class ProfileForm(messages.Message): 35 | """ProfileForm -- Profile outbound form message""" 36 | displayName = messages.StringField(1) 37 | mainEmail = messages.StringField(2) 38 | teeShirtSize = messages.EnumField('TeeShirtSize', 3) 39 | 40 | 41 | class TeeShirtSize(messages.Enum): 42 | """TeeShirtSize -- t-shirt size enumeration value""" 43 | NOT_SPECIFIED = 1 44 | XS_M = 2 45 | XS_W = 3 46 | S_M = 4 47 | S_W = 5 48 | M_M = 6 49 | M_W = 7 50 | L_M = 8 51 | L_W = 9 52 | XL_M = 10 53 | XL_W = 11 54 | XXL_M = 12 55 | XXL_W = 13 56 | XXXL_M = 14 57 | XXXL_W = 15 58 | -------------------------------------------------------------------------------- /Lesson_3/00_Conference_Central/settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """settings.py 4 | 5 | Udacity conference server-side Python App Engine app user settings 6 | 7 | $Id$ 8 | 9 | created/forked from conference.py by wesc on 2014 may 24 10 | 11 | """ 12 | 13 | # Replace the following lines with client IDs obtained from the APIs 14 | # Console or Cloud Console. 15 | WEB_CLIENT_ID = 'replace with Web client ID' 16 | 17 | -------------------------------------------------------------------------------- /Lesson_3/00_Conference_Central/static/bootstrap/css/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | padding-top: 70px; 7 | } 8 | 9 | #signInLink, #signOutLink { 10 | cursor: pointer; 11 | } 12 | 13 | /* To disable the Google+ Sign In Button, but gapi.signin.render should be invoked to store the credential 14 | if that is stored in cookie. */ 15 | #signInButton iframe { 16 | display: none; 17 | } 18 | 19 | .required { 20 | color: red; 21 | } 22 | 23 | .dismiss-messages { 24 | cursor: pointer; 25 | } 26 | 27 | /* In a small screen, make the alert message sticky to the top */ 28 | @media (max-width: 768px) { 29 | #messages.alert, #rootMessages.alert { 30 | position: fixed; 31 | left: 0; 32 | right: 0; 33 | top: 65px; 34 | z-index: 1000; 35 | } 36 | } 37 | 38 | .form-group-condensed { 39 | margin-top: 0; 40 | margin-bottom: 5px; 41 | } 42 | 43 | .label-separated { 44 | margin-right: 8px; 45 | } 46 | 47 | .spinner { 48 | position: fixed; 49 | top: 70px; 50 | z-index: 9999; 51 | } 52 | 53 | #signInButton { 54 | cursor: pointer; 55 | vertical-align: middle; 56 | } 57 | 58 | #profile-container { 59 | float: right; 60 | font-size: 85%; 61 | } 62 | 63 | #profile img { 64 | max-height: 35px; 65 | width: auto; 66 | vertical-align: middle; 67 | } 68 | 69 | #show-conferences-tab { 70 | margin-bottom: 20px; 71 | } 72 | 73 | ul#filters { 74 | list-style: none; 75 | padding-left: 0px; 76 | font-size: 85%; 77 | } 78 | 79 | ul#filters span.glyphicon-remove { 80 | font-size: 80%; 81 | } 82 | 83 | ul#conferences-list { 84 | list-style: none; 85 | } 86 | 87 | .intro-header { 88 | padding-top: 50px; 89 | padding-bottom: 50px; 90 | color: #f8f8f8; 91 | text-shadow: black 0.1em 0.1em 0.2em; 92 | background: url(/img/meeting-room.jpg) no-repeat center center; 93 | background-size: cover; 94 | text-align: center; 95 | } 96 | 97 | .intro-message { 98 | position: relative; 99 | padding-top: 5%; 100 | padding-bottom: 5%; 101 | vertical-align: middle; 102 | } 103 | 104 | .section-a { 105 | padding: 50px 0; 106 | } 107 | -------------------------------------------------------------------------------- /Lesson_3/00_Conference_Central/static/bootstrap/css/offcanvas.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Style tweaks 3 | * -------------------------------------------------- 4 | */ 5 | html, 6 | body { 7 | overflow-x: hidden; /* Prevent scroll on narrow devices */ 8 | } 9 | 10 | footer { 11 | padding: 30px 0; 12 | } 13 | 14 | /* 15 | * Off Canvas 16 | * -------------------------------------------------- 17 | */ 18 | @media screen and (max-width: 767px) { 19 | .row-offcanvas { 20 | position: relative; 21 | -webkit-transition: all .25s ease-out; 22 | -moz-transition: all .25s ease-out; 23 | transition: all .25s ease-out; 24 | } 25 | 26 | .row-offcanvas-right { 27 | right: 0; 28 | } 29 | 30 | .row-offcanvas-left { 31 | left: 0; 32 | } 33 | 34 | .row-offcanvas-right 35 | .sidebar-offcanvas { 36 | right: -50%; /* 6 columns */ 37 | } 38 | 39 | .row-offcanvas-left 40 | .sidebar-offcanvas { 41 | left: -50%; /* 6 columns */ 42 | } 43 | 44 | .row-offcanvas-right.active { 45 | right: 50%; /* 6 columns */ 46 | } 47 | 48 | .row-offcanvas-left.active { 49 | left: 50%; /* 6 columns */ 50 | } 51 | 52 | .sidebar-offcanvas { 53 | position: absolute; 54 | top: 0; 55 | width: 50%; /* 6 columns */ 56 | } 57 | } -------------------------------------------------------------------------------- /Lesson_3/00_Conference_Central/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_3/00_Conference_Central/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /Lesson_3/00_Conference_Central/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_3/00_Conference_Central/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /Lesson_3/00_Conference_Central/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_3/00_Conference_Central/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /Lesson_3/00_Conference_Central/static/img/CloudPlatform_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_3/00_Conference_Central/static/img/CloudPlatform_logo.png -------------------------------------------------------------------------------- /Lesson_3/00_Conference_Central/static/img/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_3/00_Conference_Central/static/img/ajax-loader.gif -------------------------------------------------------------------------------- /Lesson_3/00_Conference_Central/static/img/business1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_3/00_Conference_Central/static/img/business1.jpg -------------------------------------------------------------------------------- /Lesson_3/00_Conference_Central/static/img/business2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_3/00_Conference_Central/static/img/business2.jpg -------------------------------------------------------------------------------- /Lesson_3/00_Conference_Central/static/img/business3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_3/00_Conference_Central/static/img/business3.jpg -------------------------------------------------------------------------------- /Lesson_3/00_Conference_Central/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_3/00_Conference_Central/static/img/favicon.ico -------------------------------------------------------------------------------- /Lesson_3/00_Conference_Central/static/img/meeting-room.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_3/00_Conference_Central/static/img/meeting-room.jpg -------------------------------------------------------------------------------- /Lesson_3/00_Conference_Central/static/js/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc object 5 | * @name conferenceApp 6 | * @requires $routeProvider 7 | * @requires conferenceControllers 8 | * @requires ui.bootstrap 9 | * 10 | * @description 11 | * Root app, which routes and specifies the partial html and controller depending on the url requested. 12 | * 13 | */ 14 | var app = angular.module('conferenceApp', 15 | ['conferenceControllers', 'ngRoute', 'ui.bootstrap']). 16 | config(['$routeProvider', 17 | function ($routeProvider) { 18 | $routeProvider. 19 | when('/conference', { 20 | templateUrl: '/partials/show_conferences.html', 21 | controller: 'ShowConferenceCtrl' 22 | }). 23 | when('/conference/create', { 24 | templateUrl: '/partials/create_conferences.html', 25 | controller: 'CreateConferenceCtrl' 26 | }). 27 | when('/conference/detail/:websafeConferenceKey', { 28 | templateUrl: '/partials/conference_detail.html', 29 | controller: 'ConferenceDetailCtrl' 30 | }). 31 | when('/profile', { 32 | templateUrl: '/partials/profile.html', 33 | controller: 'MyProfileCtrl' 34 | }). 35 | when('/', { 36 | templateUrl: '/partials/home.html' 37 | }). 38 | otherwise({ 39 | redirectTo: '/' 40 | }); 41 | }]); 42 | 43 | /** 44 | * @ngdoc filter 45 | * @name startFrom 46 | * 47 | * @description 48 | * A filter that extracts an array from the specific index. 49 | * 50 | */ 51 | app.filter('startFrom', function () { 52 | /** 53 | * Extracts an array from the specific index. 54 | * 55 | * @param {Array} data 56 | * @param {Integer} start 57 | * @returns {Array|*} 58 | */ 59 | var filter = function (data, start) { 60 | return data.slice(start); 61 | } 62 | return filter; 63 | }); 64 | 65 | 66 | /** 67 | * @ngdoc constant 68 | * @name HTTP_ERRORS 69 | * 70 | * @description 71 | * Holds the constants that represent HTTP error codes. 72 | * 73 | */ 74 | app.constant('HTTP_ERRORS', { 75 | 'UNAUTHORIZED': 401 76 | }); 77 | 78 | 79 | /** 80 | * @ngdoc service 81 | * @name oauth2Provider 82 | * 83 | * @description 84 | * Service that holds the OAuth2 information shared across all the pages. 85 | * 86 | */ 87 | app.factory('oauth2Provider', function ($modal) { 88 | var oauth2Provider = { 89 | CLIENT_ID: 'web-client-id', 90 | SCOPES: 'email profile', 91 | signedIn: false 92 | } 93 | 94 | /** 95 | * Calls the OAuth2 authentication method. 96 | */ 97 | oauth2Provider.signIn = function (callback) { 98 | gapi.auth.signIn({ 99 | 'clientid': oauth2Provider.CLIENT_ID, 100 | 'cookiepolicy': 'single_host_origin', 101 | 'accesstype': 'online', 102 | 'approveprompt': 'auto', 103 | 'scope': oauth2Provider.SCOPES, 104 | 'callback': callback 105 | }); 106 | }; 107 | 108 | /** 109 | * Logs out the user. 110 | */ 111 | oauth2Provider.signOut = function () { 112 | gapi.auth.signOut(); 113 | // Explicitly set the invalid access token in order to make the API calls fail. 114 | gapi.auth.setToken({access_token: ''}) 115 | oauth2Provider.signedIn = false; 116 | }; 117 | 118 | /** 119 | * Shows the modal with Google+ sign in button. 120 | * 121 | * @returns {*|Window} 122 | */ 123 | oauth2Provider.showLoginModal = function() { 124 | var modalInstance = $modal.open({ 125 | templateUrl: '/partials/login.modal.html', 126 | controller: 'OAuth2LoginModalCtrl' 127 | }); 128 | return modalInstance; 129 | }; 130 | 131 | return oauth2Provider; 132 | }); 133 | -------------------------------------------------------------------------------- /Lesson_3/00_Conference_Central/static/partials/conference_detail.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 8 |
9 | 10 |
11 |
12 | 13 |
14 |
15 |
16 |

{{conference.name}}

17 |
{{conference.description}}
18 |
19 | 20 | {{conference.maxAttendees - conference.seatsAvailable}} / {{conference.maxAttendees}} 21 |
22 |
23 | 24 | {{conference.organizerDisplayName}} 25 |
26 |

Register

28 |

Unregister

30 |
31 | 32 |
33 |
34 |
35 | 36 | {{conference.city}} 37 |
38 |
39 | 40 | 41 | {{topic}} 42 | 43 |
44 |
45 | 46 | {{conference.startDate | date:'dd-MMMM-yyyy'}} 47 |
48 |
49 | 50 | {{conference.endDate | date:'dd-MMMM-yyyy'}} 51 |
52 |
53 |
54 |
55 |
56 |
57 | -------------------------------------------------------------------------------- /Lesson_3/00_Conference_Central/static/partials/home.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

Welcome to Conference Central

6 | 7 |

Lets you manage conferences

8 |
9 | 17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |

View conferences

27 | 28 |

View by city, topics, date, max attendees.

29 | View conferences 30 |
31 |
32 | 33 |
34 |
35 |
36 | 37 |
38 |
39 |
40 |
41 |
42 |

Create new conferences

43 | 44 |

In 10 seconds or less.

45 | Create a conference 46 |
47 |
48 | 49 |
50 |
51 |
52 | 53 |
54 |
55 |
56 |
57 |
58 |

Update your profile

59 | View my profile 60 |
61 |
62 | 63 |
64 |
65 |
66 | -------------------------------------------------------------------------------- /Lesson_3/00_Conference_Central/static/partials/login.modal.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Please sign in to complete this action.

4 |
5 | 8 |
-------------------------------------------------------------------------------- /Lesson_3/00_Conference_Central/static/partials/profile.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 8 |
9 | 10 |
11 |
12 |
13 |
14 |

My Profile

15 |
16 |
17 | 18 | Changed 20 | 22 |
23 | 24 |
25 | 26 | Changed 28 | 32 |
33 | 34 | 37 |
38 |
39 |
40 |
-------------------------------------------------------------------------------- /Lesson_3/00_Conference_Central/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Conference Central 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 67 | 68 |
69 |
70 |
71 |
72 | 73 | 75 |
76 |
77 |
78 | 79 |
80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Lesson_3/additions/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import time 4 | import uuid 5 | 6 | from google.appengine.api import urlfetch 7 | from models import Profile 8 | 9 | def getUserId(user, id_type="email"): 10 | if id_type == "email": 11 | return user.email() 12 | 13 | if id_type == "oauth": 14 | """A workaround implementation for getting userid.""" 15 | auth = os.getenv('HTTP_AUTHORIZATION') 16 | bearer, token = auth.split() 17 | token_type = 'id_token' 18 | if 'OAUTH_USER_ID' in os.environ: 19 | token_type = 'access_token' 20 | url = ('https://www.googleapis.com/oauth2/v1/tokeninfo?%s=%s' 21 | % (token_type, token)) 22 | user = {} 23 | wait = 1 24 | for i in range(3): 25 | resp = urlfetch.fetch(url) 26 | if resp.status_code == 200: 27 | user = json.loads(resp.content) 28 | break 29 | elif resp.status_code == 400 and 'invalid_token' in resp.content: 30 | url = ('https://www.googleapis.com/oauth2/v1/tokeninfo?%s=%s' 31 | % ('access_token', token)) 32 | else: 33 | time.sleep(wait) 34 | wait = wait + i 35 | return user.get('user_id', '') 36 | 37 | if id_type == "custom": 38 | # implement your own user_id creation and getting algorythm 39 | # this is just a sample that queries datastore for an existing profile 40 | # and generates an id if profile does not exist for an email 41 | profile = Conference.query(Conference.mainEmail == user.email()) 42 | if profile: 43 | return profile.id() 44 | else: 45 | return str(uuid.uuid1().get_hex()) 46 | -------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/README.md: -------------------------------------------------------------------------------- 1 | App Engine application for the Udacity training course. 2 | 3 | ## Products 4 | - [App Engine][1] 5 | 6 | ## Language 7 | - [Python][2] 8 | 9 | ## APIs 10 | - [Google Cloud Endpoints][3] 11 | 12 | ## Setup Instructions 13 | 1. Update the value of `application` in `app.yaml` to the app ID you 14 | have registered in the App Engine admin console and would like to use to host 15 | your instance of this sample. 16 | 1. Update the values at the top of `settings.py` to 17 | reflect the respective client IDs you have registered in the 18 | [Developer Console][4]. 19 | 1. Update the value of CLIENT_ID in `static/js/app.js` to the Web client ID 20 | 1. (Optional) Mark the configuration files as unchanged as follows: 21 | `$ git update-index --assume-unchanged app.yaml settings.py static/js/app.js` 22 | 1. Run the app with the devserver using `dev_appserver.py DIR`, and ensure it's running by visiting 23 | your local server's address (by default [localhost:8080][5].) 24 | 1. Generate your client library(ies) with [the endpoints tool][6]. 25 | 1. Deploy your application. 26 | 27 | 28 | [1]: https://developers.google.com/appengine 29 | [2]: http://python.org 30 | [3]: https://developers.google.com/appengine/docs/python/endpoints/ 31 | [4]: https://console.developers.google.com/ 32 | [5]: https://localhost:8080/ 33 | [6]: https://developers.google.com/appengine/docs/python/endpoints/endpoints_tool 34 | -------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/app.yaml: -------------------------------------------------------------------------------- 1 | application: ud858-dev-playground 2 | version: 1 3 | runtime: python27 4 | api_version: 1 5 | threadsafe: yes 6 | 7 | handlers: # static then dynamic 8 | 9 | - url: /favicon\.ico 10 | static_files: favicon.ico 11 | upload: favicon\.ico 12 | 13 | - url: /js 14 | static_dir: static/js 15 | 16 | - url: /img 17 | static_dir: static/img 18 | 19 | - url: /css 20 | static_dir: static/bootstrap/css 21 | 22 | - url: /fonts 23 | static_dir: static/fonts 24 | 25 | - url: /partials 26 | static_dir: static/partials 27 | 28 | - url: / 29 | static_files: templates/index.html 30 | upload: templates/index\.html 31 | secure: always 32 | 33 | - url: /_ah/spi/.* 34 | script: conference.api 35 | secure: always 36 | 37 | libraries: 38 | 39 | - name: endpoints 40 | version: latest 41 | 42 | # pycrypto library used for OAuth2 (req'd for authenticated APIs) 43 | - name: pycrypto 44 | version: latest 45 | -------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/conference.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | conference.py -- Udacity conference server-side Python App Engine API; 5 | uses Google Cloud Endpoints 6 | 7 | $Id: conference.py,v 1.25 2014/05/24 23:42:19 wesc Exp wesc $ 8 | 9 | created by wesc on 2014 apr 21 10 | 11 | """ 12 | 13 | __author__ = 'wesc+api@google.com (Wesley Chun)' 14 | 15 | 16 | from datetime import datetime 17 | 18 | import endpoints 19 | from protorpc import messages 20 | from protorpc import message_types 21 | from protorpc import remote 22 | 23 | from google.appengine.ext import ndb 24 | 25 | from models import Profile 26 | from models import ProfileMiniForm 27 | from models import ProfileForm 28 | from models import TeeShirtSize 29 | 30 | from utils import getUserId 31 | 32 | from settings import WEB_CLIENT_ID 33 | 34 | EMAIL_SCOPE = endpoints.EMAIL_SCOPE 35 | API_EXPLORER_CLIENT_ID = endpoints.API_EXPLORER_CLIENT_ID 36 | 37 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 38 | 39 | 40 | @endpoints.api( name='conference', 41 | version='v1', 42 | allowed_client_ids=[WEB_CLIENT_ID, API_EXPLORER_CLIENT_ID], 43 | scopes=[EMAIL_SCOPE]) 44 | class ConferenceApi(remote.Service): 45 | """Conference API v0.1""" 46 | 47 | # - - - Profile objects - - - - - - - - - - - - - - - - - - - 48 | 49 | def _copyProfileToForm(self, prof): 50 | """Copy relevant fields from Profile to ProfileForm.""" 51 | # copy relevant fields from Profile to ProfileForm 52 | pf = ProfileForm() 53 | for field in pf.all_fields(): 54 | if hasattr(prof, field.name): 55 | # convert t-shirt string to Enum; just copy others 56 | if field.name == 'teeShirtSize': 57 | setattr(pf, field.name, getattr(TeeShirtSize, getattr(prof, field.name))) 58 | else: 59 | setattr(pf, field.name, getattr(prof, field.name)) 60 | pf.check_initialized() 61 | return pf 62 | 63 | 64 | def _getProfileFromUser(self): 65 | """Return user Profile from datastore, creating new one if non-existent.""" 66 | user = endpoints.get_current_user() 67 | if not user: 68 | raise endpoints.UnauthorizedException('Authorization required') 69 | 70 | # get Profile from datastore 71 | user_id = getUserId(user) 72 | p_key = ndb.Key(Profile, user_id) 73 | profile = p_key.get() 74 | # create new Profile if not there 75 | if not profile: 76 | profile = Profile( 77 | key = p_key, 78 | displayName = user.nickname(), 79 | mainEmail= user.email(), 80 | teeShirtSize = str(TeeShirtSize.NOT_SPECIFIED), 81 | ) 82 | profile.put() 83 | 84 | return profile # return Profile 85 | 86 | 87 | def _doProfile(self, save_request=None): 88 | """Get user Profile and return to user, possibly updating it first.""" 89 | # get user Profile 90 | prof = self._getProfileFromUser() 91 | 92 | # if saveProfile(), process user-modifyable fields 93 | if save_request: 94 | for field in ('displayName', 'teeShirtSize'): 95 | if hasattr(save_request, field): 96 | val = getattr(save_request, field) 97 | if val: 98 | setattr(prof, field, str(val)) 99 | prof.put() 100 | 101 | # return ProfileForm 102 | return self._copyProfileToForm(prof) 103 | 104 | 105 | @endpoints.method(message_types.VoidMessage, ProfileForm, 106 | path='profile', http_method='GET', name='getProfile') 107 | def getProfile(self, request): 108 | """Return user profile.""" 109 | return self._doProfile() 110 | 111 | 112 | @endpoints.method(ProfileMiniForm, ProfileForm, 113 | path='profile', http_method='POST', name='saveProfile') 114 | def saveProfile(self, request): 115 | """Update & return user profile.""" 116 | return self._doProfile(request) 117 | 118 | 119 | # - - - Conference objects - - - - - - - - - - - - - - - - - - - 120 | 121 | # TODO 122 | 123 | # registers API 124 | api = endpoints.api_server([ConferenceApi]) 125 | -------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """models.py 4 | 5 | Udacity conference server-side Python App Engine data & ProtoRPC models 6 | 7 | $Id: models.py,v 1.1 2014/05/24 22:01:10 wesc Exp $ 8 | 9 | created/forked from conferences.py by wesc on 2014 may 24 10 | 11 | """ 12 | 13 | __author__ = 'wesc+api@google.com (Wesley Chun)' 14 | 15 | import httplib 16 | import endpoints 17 | from protorpc import messages 18 | from google.appengine.ext import ndb 19 | 20 | 21 | class Profile(ndb.Model): 22 | """Profile -- User profile object""" 23 | displayName = ndb.StringProperty() 24 | mainEmail = ndb.StringProperty() 25 | teeShirtSize = ndb.StringProperty(default='NOT_SPECIFIED') 26 | 27 | 28 | class ProfileMiniForm(messages.Message): 29 | """ProfileMiniForm -- update Profile form message""" 30 | displayName = messages.StringField(1) 31 | teeShirtSize = messages.EnumField('TeeShirtSize', 2) 32 | 33 | 34 | class ProfileForm(messages.Message): 35 | """ProfileForm -- Profile outbound form message""" 36 | displayName = messages.StringField(1) 37 | mainEmail = messages.StringField(2) 38 | teeShirtSize = messages.EnumField('TeeShirtSize', 3) 39 | 40 | 41 | class TeeShirtSize(messages.Enum): 42 | """TeeShirtSize -- t-shirt size enumeration value""" 43 | NOT_SPECIFIED = 1 44 | XS_M = 2 45 | XS_W = 3 46 | S_M = 4 47 | S_W = 5 48 | M_M = 6 49 | M_W = 7 50 | L_M = 8 51 | L_W = 9 52 | XL_M = 10 53 | XL_W = 11 54 | XXL_M = 12 55 | XXL_W = 13 56 | XXXL_M = 14 57 | XXXL_W = 15 58 | -------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """settings.py 4 | 5 | Udacity conference server-side Python App Engine app user settings 6 | 7 | $Id$ 8 | 9 | created/forked from conference.py by wesc on 2014 may 24 10 | 11 | """ 12 | 13 | # Replace the following lines with client IDs obtained from the APIs 14 | # Console or Cloud Console. 15 | WEB_CLIENT_ID = 'replace with Web client ID' 16 | 17 | -------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/static/bootstrap/css/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | padding-top: 70px; 7 | } 8 | 9 | #signInLink, #signOutLink { 10 | cursor: pointer; 11 | } 12 | 13 | /* To disable the Google+ Sign In Button, but gapi.signin.render should be invoked to store the credential 14 | if that is stored in cookie. */ 15 | #signInButton iframe { 16 | display: none; 17 | } 18 | 19 | .required { 20 | color: red; 21 | } 22 | 23 | .dismiss-messages { 24 | cursor: pointer; 25 | } 26 | 27 | /* In a small screen, make the alert message sticky to the top */ 28 | @media (max-width: 768px) { 29 | #messages.alert, #rootMessages.alert { 30 | position: fixed; 31 | left: 0; 32 | right: 0; 33 | top: 65px; 34 | z-index: 1000; 35 | } 36 | } 37 | 38 | .form-group-condensed { 39 | margin-top: 0; 40 | margin-bottom: 5px; 41 | } 42 | 43 | .label-separated { 44 | margin-right: 8px; 45 | } 46 | 47 | .spinner { 48 | position: fixed; 49 | top: 70px; 50 | z-index: 9999; 51 | } 52 | 53 | #signInButton { 54 | cursor: pointer; 55 | vertical-align: middle; 56 | } 57 | 58 | #profile-container { 59 | float: right; 60 | font-size: 85%; 61 | } 62 | 63 | #profile img { 64 | max-height: 35px; 65 | width: auto; 66 | vertical-align: middle; 67 | } 68 | 69 | #show-conferences-tab { 70 | margin-bottom: 20px; 71 | } 72 | 73 | ul#filters { 74 | list-style: none; 75 | padding-left: 0px; 76 | font-size: 85%; 77 | } 78 | 79 | ul#filters span.glyphicon-remove { 80 | font-size: 80%; 81 | } 82 | 83 | ul#conferences-list { 84 | list-style: none; 85 | } 86 | 87 | .intro-header { 88 | padding-top: 50px; 89 | padding-bottom: 50px; 90 | color: #f8f8f8; 91 | text-shadow: black 0.1em 0.1em 0.2em; 92 | background: url(/img/meeting-room.jpg) no-repeat center center; 93 | background-size: cover; 94 | text-align: center; 95 | } 96 | 97 | .intro-message { 98 | position: relative; 99 | padding-top: 5%; 100 | padding-bottom: 5%; 101 | vertical-align: middle; 102 | } 103 | 104 | .section-a { 105 | padding: 50px 0; 106 | } 107 | -------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/static/bootstrap/css/offcanvas.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Style tweaks 3 | * -------------------------------------------------- 4 | */ 5 | html, 6 | body { 7 | overflow-x: hidden; /* Prevent scroll on narrow devices */ 8 | } 9 | 10 | footer { 11 | padding: 30px 0; 12 | } 13 | 14 | /* 15 | * Off Canvas 16 | * -------------------------------------------------- 17 | */ 18 | @media screen and (max-width: 767px) { 19 | .row-offcanvas { 20 | position: relative; 21 | -webkit-transition: all .25s ease-out; 22 | -moz-transition: all .25s ease-out; 23 | transition: all .25s ease-out; 24 | } 25 | 26 | .row-offcanvas-right { 27 | right: 0; 28 | } 29 | 30 | .row-offcanvas-left { 31 | left: 0; 32 | } 33 | 34 | .row-offcanvas-right 35 | .sidebar-offcanvas { 36 | right: -50%; /* 6 columns */ 37 | } 38 | 39 | .row-offcanvas-left 40 | .sidebar-offcanvas { 41 | left: -50%; /* 6 columns */ 42 | } 43 | 44 | .row-offcanvas-right.active { 45 | right: 50%; /* 6 columns */ 46 | } 47 | 48 | .row-offcanvas-left.active { 49 | left: 50%; /* 6 columns */ 50 | } 51 | 52 | .sidebar-offcanvas { 53 | position: absolute; 54 | top: 0; 55 | width: 50%; /* 6 columns */ 56 | } 57 | } -------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_4/00_Conference_Central/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_4/00_Conference_Central/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_4/00_Conference_Central/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/static/img/CloudPlatform_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_4/00_Conference_Central/static/img/CloudPlatform_logo.png -------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/static/img/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_4/00_Conference_Central/static/img/ajax-loader.gif -------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/static/img/business1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_4/00_Conference_Central/static/img/business1.jpg -------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/static/img/business2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_4/00_Conference_Central/static/img/business2.jpg -------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/static/img/business3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_4/00_Conference_Central/static/img/business3.jpg -------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_4/00_Conference_Central/static/img/favicon.ico -------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/static/img/meeting-room.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_4/00_Conference_Central/static/img/meeting-room.jpg -------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/static/js/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc object 5 | * @name conferenceApp 6 | * @requires $routeProvider 7 | * @requires conferenceControllers 8 | * @requires ui.bootstrap 9 | * 10 | * @description 11 | * Root app, which routes and specifies the partial html and controller depending on the url requested. 12 | * 13 | */ 14 | var app = angular.module('conferenceApp', 15 | ['conferenceControllers', 'ngRoute', 'ui.bootstrap']). 16 | config(['$routeProvider', 17 | function ($routeProvider) { 18 | $routeProvider. 19 | when('/conference', { 20 | templateUrl: '/partials/show_conferences.html', 21 | controller: 'ShowConferenceCtrl' 22 | }). 23 | when('/conference/create', { 24 | templateUrl: '/partials/create_conferences.html', 25 | controller: 'CreateConferenceCtrl' 26 | }). 27 | when('/conference/detail/:websafeConferenceKey', { 28 | templateUrl: '/partials/conference_detail.html', 29 | controller: 'ConferenceDetailCtrl' 30 | }). 31 | when('/profile', { 32 | templateUrl: '/partials/profile.html', 33 | controller: 'MyProfileCtrl' 34 | }). 35 | when('/', { 36 | templateUrl: '/partials/home.html' 37 | }). 38 | otherwise({ 39 | redirectTo: '/' 40 | }); 41 | }]); 42 | 43 | /** 44 | * @ngdoc filter 45 | * @name startFrom 46 | * 47 | * @description 48 | * A filter that extracts an array from the specific index. 49 | * 50 | */ 51 | app.filter('startFrom', function () { 52 | /** 53 | * Extracts an array from the specific index. 54 | * 55 | * @param {Array} data 56 | * @param {Integer} start 57 | * @returns {Array|*} 58 | */ 59 | var filter = function (data, start) { 60 | return data.slice(start); 61 | } 62 | return filter; 63 | }); 64 | 65 | 66 | /** 67 | * @ngdoc constant 68 | * @name HTTP_ERRORS 69 | * 70 | * @description 71 | * Holds the constants that represent HTTP error codes. 72 | * 73 | */ 74 | app.constant('HTTP_ERRORS', { 75 | 'UNAUTHORIZED': 401 76 | }); 77 | 78 | 79 | /** 80 | * @ngdoc service 81 | * @name oauth2Provider 82 | * 83 | * @description 84 | * Service that holds the OAuth2 information shared across all the pages. 85 | * 86 | */ 87 | app.factory('oauth2Provider', function ($modal) { 88 | var oauth2Provider = { 89 | CLIENT_ID: 'web-client-id', 90 | SCOPES: 'email profile', 91 | signedIn: false 92 | } 93 | 94 | /** 95 | * Calls the OAuth2 authentication method. 96 | */ 97 | oauth2Provider.signIn = function (callback) { 98 | gapi.auth.signIn({ 99 | 'clientid': oauth2Provider.CLIENT_ID, 100 | 'cookiepolicy': 'single_host_origin', 101 | 'accesstype': 'online', 102 | 'approveprompt': 'auto', 103 | 'scope': oauth2Provider.SCOPES, 104 | 'callback': callback 105 | }); 106 | }; 107 | 108 | /** 109 | * Logs out the user. 110 | */ 111 | oauth2Provider.signOut = function () { 112 | gapi.auth.signOut(); 113 | // Explicitly set the invalid access token in order to make the API calls fail. 114 | gapi.auth.setToken({access_token: ''}) 115 | oauth2Provider.signedIn = false; 116 | }; 117 | 118 | /** 119 | * Shows the modal with Google+ sign in button. 120 | * 121 | * @returns {*|Window} 122 | */ 123 | oauth2Provider.showLoginModal = function() { 124 | var modalInstance = $modal.open({ 125 | templateUrl: '/partials/login.modal.html', 126 | controller: 'OAuth2LoginModalCtrl' 127 | }); 128 | return modalInstance; 129 | }; 130 | 131 | return oauth2Provider; 132 | }); 133 | -------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/static/partials/conference_detail.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 8 |
9 | 10 |
11 |
12 | 13 |
14 |
15 |
16 |

{{conference.name}}

17 |
{{conference.description}}
18 |
19 | 20 | {{conference.maxAttendees - conference.seatsAvailable}} / {{conference.maxAttendees}} 21 |
22 |
23 | 24 | {{conference.organizerDisplayName}} 25 |
26 |

Register

28 |

Unregister

30 |
31 | 32 |
33 |
34 |
35 | 36 | {{conference.city}} 37 |
38 |
39 | 40 | 41 | {{topic}} 42 | 43 |
44 |
45 | 46 | {{conference.startDate | date:'dd-MMMM-yyyy'}} 47 |
48 |
49 | 50 | {{conference.endDate | date:'dd-MMMM-yyyy'}} 51 |
52 |
53 |
54 |
55 |
56 |
57 | -------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/static/partials/home.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

Welcome to Conference Central

6 | 7 |

Lets you manage conferences

8 |
9 | 17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |

View conferences

27 | 28 |

View by city, topics, date, max attendees.

29 | View conferences 30 |
31 |
32 | 33 |
34 |
35 |
36 | 37 |
38 |
39 |
40 |
41 |
42 |

Create new conferences

43 | 44 |

In 10 seconds or less.

45 | Create a conference 46 |
47 |
48 | 49 |
50 |
51 |
52 | 53 |
54 |
55 |
56 |
57 |
58 |

Update your profile

59 | View my profile 60 |
61 |
62 | 63 |
64 |
65 |
66 | -------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/static/partials/login.modal.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Please sign in to complete this action.

4 |
5 | 8 |
-------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/static/partials/profile.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 8 |
9 | 10 |
11 |
12 |
13 |
14 |

My Profile

15 |
16 |
17 | 18 | Changed 20 | 22 |
23 | 24 |
25 | 26 | Changed 28 | 32 |
33 | 34 | 37 |
38 |
39 |
40 |
-------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Conference Central 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 67 | 68 |
69 |
70 |
71 |
72 | 73 | 75 |
76 |
77 |
78 | 79 |
80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Lesson_4/00_Conference_Central/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import time 4 | import uuid 5 | 6 | from google.appengine.api import urlfetch 7 | from models import Profile 8 | 9 | def getUserId(user, id_type="email"): 10 | if id_type == "email": 11 | return user.email() 12 | 13 | if id_type == "oauth": 14 | """A workaround implementation for getting userid.""" 15 | auth = os.getenv('HTTP_AUTHORIZATION') 16 | bearer, token = auth.split() 17 | token_type = 'id_token' 18 | if 'OAUTH_USER_ID' in os.environ: 19 | token_type = 'access_token' 20 | url = ('https://www.googleapis.com/oauth2/v1/tokeninfo?%s=%s' 21 | % (token_type, token)) 22 | user = {} 23 | wait = 1 24 | for i in range(3): 25 | resp = urlfetch.fetch(url) 26 | if resp.status_code == 200: 27 | user = json.loads(resp.content) 28 | break 29 | elif resp.status_code == 400 and 'invalid_token' in resp.content: 30 | url = ('https://www.googleapis.com/oauth2/v1/tokeninfo?%s=%s' 31 | % ('access_token', token)) 32 | else: 33 | time.sleep(wait) 34 | wait = wait + i 35 | return user.get('user_id', '') 36 | 37 | if id_type == "custom": 38 | # implement your own user_id creation and getting algorythm 39 | # this is just a sample that queries datastore for an existing profile 40 | # and generates an id if profile does not exist for an email 41 | profile = Conference.query(Conference.mainEmail == user.email()) 42 | if profile: 43 | return profile.id() 44 | else: 45 | return str(uuid.uuid1().get_hex()) 46 | -------------------------------------------------------------------------------- /Lesson_4/Additions/TODO_1_conference.py: -------------------------------------------------------------------------------- 1 | from models import Conference 2 | from models import ConferenceForm 3 | 4 | 5 | DEFAULTS = { 6 | "city": "Default City", 7 | "maxAttendees": 0, 8 | "seatsAvailable": 0, 9 | "topics": [ "Default", "Topic" ], 10 | } 11 | 12 | 13 | # - - - Conference objects - - - - - - - - - - - - - - - - - 14 | 15 | def _copyConferenceToForm(self, conf, displayName): 16 | """Copy relevant fields from Conference to ConferenceForm.""" 17 | cf = ConferenceForm() 18 | for field in cf.all_fields(): 19 | if hasattr(conf, field.name): 20 | # convert Date to date string; just copy others 21 | if field.name.endswith('Date'): 22 | setattr(cf, field.name, str(getattr(conf, field.name))) 23 | else: 24 | setattr(cf, field.name, getattr(conf, field.name)) 25 | elif field.name == "websafeKey": 26 | setattr(cf, field.name, conf.key.urlsafe()) 27 | if displayName: 28 | setattr(cf, 'organizerDisplayName', displayName) 29 | cf.check_initialized() 30 | return cf 31 | 32 | 33 | def _createConferenceObject(self, request): 34 | """Create or update Conference object, returning ConferenceForm/request.""" 35 | # preload necessary data items 36 | user = endpoints.get_current_user() 37 | if not user: 38 | raise endpoints.UnauthorizedException('Authorization required') 39 | user_id = getUserId(user) 40 | 41 | if not request.name: 42 | raise endpoints.BadRequestException("Conference 'name' field required") 43 | 44 | # copy ConferenceForm/ProtoRPC Message into dict 45 | data = {field.name: getattr(request, field.name) for field in request.all_fields()} 46 | del data['websafeKey'] 47 | del data['organizerDisplayName'] 48 | 49 | # add default values for those missing (both data model & outbound Message) 50 | for df in DEFAULTS: 51 | if data[df] in (None, []): 52 | data[df] = DEFAULTS[df] 53 | setattr(request, df, DEFAULTS[df]) 54 | 55 | # convert dates from strings to Date objects; set month based on start_date 56 | if data['startDate']: 57 | data['startDate'] = datetime.strptime(data['startDate'][:10], "%Y-%m-%d").date() 58 | data['month'] = data['startDate'].month 59 | else: 60 | data['month'] = 0 61 | if data['endDate']: 62 | data['endDate'] = datetime.strptime(data['endDate'][:10], "%Y-%m-%d").date() 63 | 64 | # set seatsAvailable to be same as maxAttendees on creation 65 | # both for data model & outbound Message 66 | if data["maxAttendees"] > 0: 67 | data["seatsAvailable"] = data["maxAttendees"] 68 | setattr(request, "seatsAvailable", data["maxAttendees"]) 69 | 70 | # make Profile Key from user ID 71 | p_key = ndb.Key(Profile, user_id) 72 | # allocate new Conference ID with Profile key as parent 73 | c_id = Conference.allocate_ids(size=1, parent=p_key)[0] 74 | # make Conference key from ID 75 | c_key = ndb.Key(Conference, c_id, parent=p_key) 76 | data['key'] = c_key 77 | data['organizerUserId'] = request.organizerUserId = user_id 78 | 79 | # create Conference & return (modified) ConferenceForm 80 | Conference(**data).put() 81 | 82 | return request 83 | 84 | 85 | @endpoints.method(ConferenceForm, ConferenceForm, path='conference', 86 | http_method='POST', name='createConference') 87 | def createConference(self, request): 88 | """Create new conference.""" 89 | return self._createConferenceObject(request) 90 | -------------------------------------------------------------------------------- /Lesson_4/Additions/TODO_1_models.py: -------------------------------------------------------------------------------- 1 | class Conference(ndb.Model): 2 | """Conference -- Conference object""" 3 | name = ndb.StringProperty(required=True) 4 | description = ndb.StringProperty() 5 | organizerUserId = ndb.StringProperty() 6 | topics = ndb.StringProperty(repeated=True) 7 | city = ndb.StringProperty() 8 | startDate = ndb.DateProperty() 9 | month = ndb.IntegerProperty() 10 | endDate = ndb.DateProperty() 11 | maxAttendees = ndb.IntegerProperty() 12 | seatsAvailable = ndb.IntegerProperty() 13 | 14 | class ConferenceForm(messages.Message): 15 | """ConferenceForm -- Conference outbound form message""" 16 | name = messages.StringField(1) 17 | description = messages.StringField(2) 18 | organizerUserId = messages.StringField(3) 19 | topics = messages.StringField(4, repeated=True) 20 | city = messages.StringField(5) 21 | startDate = messages.StringField(6) 22 | month = messages.IntegerField(7, variant=messages.Variant.INT32) 23 | maxAttendees = messages.IntegerField(8, variant=messages.Variant.INT32) 24 | seatsAvailable = messages.IntegerField(9, variant=messages.Variant.INT32) 25 | endDate = messages.StringField(10) 26 | websafeKey = messages.StringField(11) 27 | organizerDisplayName = messages.StringField(12) -------------------------------------------------------------------------------- /Lesson_4/Additions/TODO_2_conference.py: -------------------------------------------------------------------------------- 1 | from models import ConferenceForms 2 | from models import ConferenceQueryForm 3 | from models import ConferenceQueryForms 4 | 5 | 6 | @endpoints.method(ConferenceQueryForms, ConferenceForms, 7 | path='queryConferences', 8 | http_method='POST', 9 | name='queryConferences') 10 | def queryConferences(self, request): 11 | """Query for conferences.""" 12 | conferences = Conference.query() 13 | 14 | # return individual ConferenceForm object per Conference 15 | return ConferenceForms( 16 | items=[self._copyConferenceToForm(conf, "") \ 17 | for conf in conferences] 18 | ) 19 | -------------------------------------------------------------------------------- /Lesson_4/Additions/TODO_2_models.py: -------------------------------------------------------------------------------- 1 | class ConferenceForms(messages.Message): 2 | """ConferenceForms -- multiple Conference outbound form message""" 3 | items = messages.MessageField(ConferenceForm, 1, repeated=True) 4 | 5 | class ConferenceQueryForm(messages.Message): 6 | """ConferenceQueryForm -- Conference query inbound form message""" 7 | field = messages.StringField(1) 8 | operator = messages.StringField(2) 9 | value = messages.StringField(3) 10 | 11 | class ConferenceQueryForms(messages.Message): 12 | """ConferenceQueryForms -- multiple ConferenceQueryForm inbound form message""" 13 | filters = messages.MessageField(ConferenceQueryForm, 1, repeated=True) 14 | -------------------------------------------------------------------------------- /Lesson_4/Additions/TODO_3_conference.py: -------------------------------------------------------------------------------- 1 | @endpoints.method(message_types.VoidMessage, ConferenceForms, 2 | path='getConferencesCreated', 3 | http_method='POST', name='getConferencesCreated') 4 | def getConferencesCreated(self, request): 5 | """Return conferences created by user.""" 6 | # make sure user is authed 7 | user = endpoints.get_current_user() 8 | if not user: 9 | raise endpoints.UnauthorizedException('Authorization required') 10 | 11 | # make profile key 12 | p_key = ndb.Key(Profile, getUserId(user)) 13 | # create ancestor query for this user 14 | conferences = Conference.query(ancestor=p_key) 15 | # get the user profile and display name 16 | prof = p_key.get() 17 | displayName = getattr(prof, 'displayName') 18 | # return set of ConferenceForm objects per Conference 19 | return ConferenceForms( 20 | items=[self._copyConferenceToForm(conf, displayName) for conf in conferences] 21 | ) 22 | 23 | 24 | @endpoints.method(CONF_GET_REQUEST, ConferenceForm, 25 | path='conference/{websafeConferenceKey}', 26 | http_method='GET', name='getConference') 27 | def getConference(self, request): 28 | """Return requested conference (by websafeConferenceKey).""" 29 | # get Conference object from request; bail if not found 30 | conf = ndb.Key(urlsafe=request.websafeConferenceKey).get() 31 | if not conf: 32 | raise endpoints.NotFoundException( 33 | 'No conference found with key: %s' % request.websafeConferenceKey) 34 | prof = conf.key.parent().get() 35 | # return ConferenceForm 36 | return self._copyConferenceToForm(conf, getattr(prof, 'displayName')) 37 | 38 | -------------------------------------------------------------------------------- /Lesson_4/Additions/TODO_4_conference.py: -------------------------------------------------------------------------------- 1 | @endpoints.method(message_types.VoidMessage, ConferenceForms, 2 | path='filterPlayground', 3 | http_method='GET', name='filterPlayground') 4 | def filterPlayground(self, request): 5 | q = Conference.query() 6 | # simple filter usage: 7 | # q = q.filter(Conference.city == "Paris") 8 | 9 | # advanced filter building and usage 10 | # field = "city" 11 | # operator = "=" 12 | # value = "London" 13 | # f = ndb.query.FilterNode(field, operator, value) 14 | # q = q.filter(f) 15 | 16 | # TODO 17 | # add 2 filters: 18 | # 1: city equals to London 19 | # 2: topic equals "Medical Innovations" 20 | 21 | return ConferenceForms( 22 | items=[self._copyConferenceToForm(conf, "") for conf in q] 23 | ) -------------------------------------------------------------------------------- /Lesson_4/Additions/TODO_5_conference.py: -------------------------------------------------------------------------------- 1 | OPERATORS = { 2 | 'EQ': '=', 3 | 'GT': '>', 4 | 'GTEQ': '>=', 5 | 'LT': '<', 6 | 'LTEQ': '<=', 7 | 'NE': '!=' 8 | } 9 | 10 | FIELDS = { 11 | 'CITY': 'city', 12 | 'TOPIC': 'topics', 13 | 'MONTH': 'month', 14 | 'MAX_ATTENDEES': 'maxAttendees', 15 | } 16 | 17 | 18 | def _getQuery(self, request): 19 | """Return formatted query from the submitted filters.""" 20 | q = Conference.query() 21 | inequality_filter, filters = self._formatFilters(request.filters) 22 | 23 | # If exists, sort on inequality filter first 24 | if not inequality_filter: 25 | q = q.order(Conference.name) 26 | else: 27 | q = q.order(ndb.GenericProperty(inequality_filter)) 28 | q = q.order(Conference.name) 29 | 30 | for filtr in filters: 31 | if filtr["field"] in ["month", "maxAttendees"]: 32 | filtr["value"] = int(filtr["value"]) 33 | formatted_query = ndb.query.FilterNode(filtr["field"], filtr["operator"], filtr["value"]) 34 | q = q.filter(formatted_query) 35 | return q 36 | 37 | 38 | def _formatFilters(self, filters): 39 | """Parse, check validity and format user supplied filters.""" 40 | formatted_filters = [] 41 | inequality_field = None 42 | 43 | for f in filters: 44 | filtr = {field.name: getattr(f, field.name) for field in f.all_fields()} 45 | 46 | try: 47 | filtr["field"] = FIELDS[filtr["field"]] 48 | filtr["operator"] = OPERATORS[filtr["operator"]] 49 | except KeyError: 50 | raise endpoints.BadRequestException("Filter contains invalid field or operator.") 51 | 52 | # Every operation except "=" is an inequality 53 | if filtr["operator"] != "=": 54 | # check if inequality operation has been used in previous filters 55 | # disallow the filter if inequality was performed on a different field before 56 | # track the field on which the inequality operation is performed 57 | if inequality_field and inequality_field != filtr["field"]: 58 | raise endpoints.BadRequestException("Inequality filter is allowed on only one field.") 59 | else: 60 | inequality_field = filtr["field"] 61 | 62 | formatted_filters.append(filtr) 63 | return (inequality_field, formatted_filters) 64 | -------------------------------------------------------------------------------- /Lesson_4/Additions/TODO_6_conference.py: -------------------------------------------------------------------------------- 1 | from models import BooleanMessage 2 | from models import ConflictException 3 | 4 | CONF_GET_REQUEST = endpoints.ResourceContainer( 5 | message_types.VoidMessage, 6 | websafeConferenceKey=messages.StringField(1), 7 | ) 8 | 9 | # - - - Registration - - - - - - - - - - - - - - - - - - - - 10 | 11 | def _conferenceRegistration(self, request, reg=True): 12 | """Register or unregister user for selected conference.""" 13 | retval = None 14 | prof = self._getProfileFromUser() # get user Profile 15 | 16 | # check if conf exists given websafeConfKey 17 | # get conference; check that it exists 18 | wsck = request.websafeConferenceKey 19 | conf = ndb.Key(urlsafe=wsck).get() 20 | if not conf: 21 | raise endpoints.NotFoundException( 22 | 'No conference found with key: %s' % wsck) 23 | 24 | # register 25 | if reg: 26 | # check if user already registered otherwise add 27 | if wsck in prof.conferenceKeysToAttend: 28 | raise ConflictException( 29 | "You have already registered for this conference") 30 | 31 | # check if seats avail 32 | if conf.seatsAvailable <= 0: 33 | raise ConflictException( 34 | "There are no seats available.") 35 | 36 | # register user, take away one seat 37 | prof.conferenceKeysToAttend.append(wsck) 38 | conf.seatsAvailable -= 1 39 | retval = True 40 | 41 | # unregister 42 | else: 43 | # check if user already registered 44 | if wsck in prof.conferenceKeysToAttend: 45 | 46 | # unregister user, add back one seat 47 | prof.conferenceKeysToAttend.remove(wsck) 48 | conf.seatsAvailable += 1 49 | retval = True 50 | else: 51 | retval = False 52 | 53 | # write things back to the datastore & return 54 | prof.put() 55 | conf.put() 56 | return BooleanMessage(data=retval) 57 | 58 | 59 | @endpoints.method(CONF_GET_REQUEST, BooleanMessage, 60 | path='conference/{websafeConferenceKey}', 61 | http_method='POST', name='registerForConference') 62 | def registerForConference(self, request): 63 | """Register user for selected conference.""" 64 | return self._conferenceRegistration(request) 65 | -------------------------------------------------------------------------------- /Lesson_4/Additions/TODO_6_models.py: -------------------------------------------------------------------------------- 1 | # replace your existing Profile class with this 2 | class Profile(ndb.Model): 3 | """Profile -- User profile object""" 4 | displayName = ndb.StringProperty() 5 | mainEmail = ndb.StringProperty() 6 | teeShirtSize = ndb.StringProperty(default='NOT_SPECIFIED') 7 | conferenceKeysToAttend = ndb.StringProperty(repeated=True) 8 | 9 | # needed for conference registration 10 | class BooleanMessage(messages.Message): 11 | """BooleanMessage-- outbound Boolean value message""" 12 | data = messages.BooleanField(1) 13 | 14 | class ConflictException(endpoints.ServiceException): 15 | """ConflictException -- exception mapped to HTTP 409 response""" 16 | http_status = httplib.CONFLICT -------------------------------------------------------------------------------- /Lesson_4/Additions/TODO_7_conference.py: -------------------------------------------------------------------------------- 1 | @endpoints.method(message_types.VoidMessage, ConferenceForms, 2 | path='conferences/attending', 3 | http_method='GET', name='getConferencesToAttend') 4 | def getConferencesToAttend(self, request): 5 | """Get list of conferences that user has registered for.""" 6 | # TODO: 7 | # step 1: get user profile 8 | # step 2: get conferenceKeysToAttend from profile. 9 | # to make a ndb key from websafe key you can use: 10 | # ndb.Key(urlsafe=my_websafe_key_string) 11 | # step 3: fetch conferences from datastore. 12 | # Use get_multi(array_of_keys) to fetch all keys at once. 13 | # Do not fetch them one by one! 14 | 15 | # return set of ConferenceForm objects per Conference 16 | return ConferenceForms(items=[self._copyConferenceToForm(conf, "")\ 17 | for conf in conferences] 18 | ) -------------------------------------------------------------------------------- /Lesson_4/Additions/conference_models.partial.txt: -------------------------------------------------------------------------------- 1 | class Conference(ndb.Model): 2 | """Conference -- Conference object""" 3 | name = ndb.StringProperty(required=True) 4 | description = ndb.StringProperty() 5 | organizerUserId = ndb.StringProperty() 6 | topics = ndb.StringProperty(repeated=True) 7 | city = ndb.StringProperty() 8 | startDate = ndb.DateProperty() 9 | month = ndb.IntegerProperty() 10 | endDate = ndb.DateProperty() 11 | maxAttendees = ndb.IntegerProperty() 12 | seatsAvailable = ndb.IntegerProperty() 13 | 14 | class ConferenceForm(messages.Message): 15 | """ConferenceForm -- Conference outbound form message""" 16 | name = messages.StringField(1) 17 | description = messages.StringField(2) 18 | organizerUserId = messages.StringField(3) 19 | topics = messages.StringField(4, repeated=True) 20 | city = messages.StringField(5) 21 | startDate = messages.StringField(6) #DateTimeField() 22 | month = messages.IntegerField(7) 23 | maxAttendees = messages.IntegerField(8) 24 | seatsAvailable = messages.IntegerField(9) 25 | endDate = messages.StringField(10) #DateTimeField() 26 | websafeKey = messages.StringField(11) 27 | organizerDisplayName = messages.StringField(12) 28 | 29 | class ConferenceForms(messages.Message): 30 | """ConferenceForms -- multiple Conference outbound form message""" 31 | items = messages.MessageField(ConferenceForm, 1, repeated=True) 32 | 33 | class ConferenceQueryForm(messages.Message): 34 | """ConferenceQueryForm -- Conference query inbound form message""" 35 | field = messages.StringField(1) 36 | operator = messages.StringField(2) 37 | value = messages.StringField(3) 38 | 39 | class ConferenceQueryForms(messages.Message): 40 | """ConferenceQueryForms -- multiple ConferenceQueryForm inbound form message""" 41 | filters = messages.MessageField(ConferenceQueryForm, 1, repeated=True) 42 | -------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/README.md: -------------------------------------------------------------------------------- 1 | App Engine application for the Udacity training course. 2 | 3 | ## Products 4 | - [App Engine][1] 5 | 6 | ## Language 7 | - [Python][2] 8 | 9 | ## APIs 10 | - [Google Cloud Endpoints][3] 11 | 12 | ## Setup Instructions 13 | 1. Update the value of `application` in `app.yaml` to the app ID you 14 | have registered in the App Engine admin console and would like to use to host 15 | your instance of this sample. 16 | 1. Update the values at the top of `settings.py` to 17 | reflect the respective client IDs you have registered in the 18 | [Developer Console][4]. 19 | 1. Update the value of CLIENT_ID in `static/js/app.js` to the Web client ID 20 | 1. (Optional) Mark the configuration files as unchanged as follows: 21 | `$ git update-index --assume-unchanged app.yaml settings.py static/js/app.js` 22 | 1. Run the app with the devserver using `dev_appserver.py DIR`, and ensure it's running by visiting your local server's address (by default [localhost:8080][5].) 23 | 1. (Optional) Generate your client library(ies) with [the endpoints tool][6]. 24 | 1. Deploy your application. 25 | 26 | 27 | [1]: https://developers.google.com/appengine 28 | [2]: http://python.org 29 | [3]: https://developers.google.com/appengine/docs/python/endpoints/ 30 | [4]: https://console.developers.google.com/ 31 | [5]: https://localhost:8080/ 32 | [6]: https://developers.google.com/appengine/docs/python/endpoints/endpoints_tool 33 | -------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/app.yaml: -------------------------------------------------------------------------------- 1 | application: ud858-final-python 2 | version: 1 3 | runtime: python27 4 | api_version: 1 5 | threadsafe: yes 6 | 7 | handlers: # static then dynamic 8 | 9 | - url: /favicon\.ico 10 | static_files: favicon.ico 11 | upload: favicon\.ico 12 | 13 | - url: /js 14 | static_dir: static/js 15 | 16 | - url: /img 17 | static_dir: static/img 18 | 19 | - url: /css 20 | static_dir: static/bootstrap/css 21 | 22 | - url: /fonts 23 | static_dir: static/fonts 24 | 25 | - url: /partials 26 | static_dir: static/partials 27 | 28 | - url: / 29 | static_files: templates/index.html 30 | upload: templates/index\.html 31 | secure: always 32 | 33 | - url: /_ah/spi/.* 34 | script: conference.api 35 | secure: always 36 | 37 | libraries: 38 | 39 | - name: webapp2 40 | version: latest 41 | 42 | - name: endpoints 43 | version: latest 44 | 45 | # pycrypto library used for OAuth2 (req'd for authenticated APIs) 46 | - name: pycrypto 47 | version: latest 48 | -------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/index.yaml: -------------------------------------------------------------------------------- 1 | indexes: 2 | 3 | # AUTOGENERATED 4 | 5 | # This index.yaml is automatically updated whenever the dev_appserver 6 | # detects that a new type of query is run. If you want to manage the 7 | # index.yaml file manually, remove the above marker line (the line 8 | # saying "# AUTOGENERATED"). If you want to manage some indexes 9 | # manually, move them above the marker line. The index.yaml file is 10 | # automatically uploaded to the admin console when you next deploy 11 | # your application using appcfg.py. 12 | 13 | - kind: Conference 14 | properties: 15 | - name: city 16 | - name: maxAttendees 17 | - name: month 18 | - name: name 19 | 20 | - kind: Conference 21 | properties: 22 | - name: city 23 | - name: maxAttendees 24 | - name: month 25 | - name: topics 26 | - name: name 27 | 28 | - kind: Conference 29 | properties: 30 | - name: city 31 | - name: maxAttendees 32 | - name: name 33 | 34 | - kind: Conference 35 | properties: 36 | - name: city 37 | - name: month 38 | - name: name 39 | 40 | - kind: Conference 41 | properties: 42 | - name: city 43 | - name: month 44 | - name: topics 45 | - name: name 46 | 47 | - kind: Conference 48 | properties: 49 | - name: city 50 | - name: name 51 | 52 | - kind: Conference 53 | properties: 54 | - name: city 55 | - name: topics 56 | - name: name 57 | 58 | - kind: Conference 59 | properties: 60 | - name: maxAttendees 61 | - name: month 62 | - name: name 63 | 64 | - kind: Conference 65 | properties: 66 | - name: maxAttendees 67 | - name: month 68 | - name: topics 69 | - name: name 70 | 71 | - kind: Conference 72 | properties: 73 | - name: maxAttendees 74 | - name: name 75 | 76 | - kind: Conference 77 | properties: 78 | - name: maxAttendees 79 | - name: topics 80 | - name: name 81 | 82 | - kind: Conference 83 | properties: 84 | - name: month 85 | - name: name 86 | 87 | - kind: Conference 88 | properties: 89 | - name: month 90 | - name: topics 91 | - name: name 92 | 93 | - kind: Conference 94 | properties: 95 | - name: topics 96 | - name: name 97 | -------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | main.py -- Udacity conference server-side Python App Engine 5 | HTTP controller handlers for memcache & task queue access 6 | 7 | $Id$ 8 | 9 | created by wesc on 2014 may 24 10 | 11 | """ 12 | 13 | __author__ = 'wesc+api@google.com (Wesley Chun)' 14 | 15 | import webapp2 16 | from google.appengine.api import app_identity 17 | from google.appengine.api import mail 18 | from conference import ConferenceApi 19 | 20 | class SetAnnouncementHandler(webapp2.RequestHandler): 21 | def get(self): 22 | """Set Announcement in Memcache.""" 23 | # TODO 1 24 | 25 | 26 | class SendConfirmationEmailHandler(webapp2.RequestHandler): 27 | def post(self): 28 | """Send email confirming Conference creation.""" 29 | mail.send_mail( 30 | 'noreply@%s.appspotmail.com' % ( 31 | app_identity.get_application_id()), # from 32 | self.request.get('email'), # to 33 | 'You created a new Conference!', # subj 34 | 'Hi, you have created a following ' # body 35 | 'conference:\r\n\r\n%s' % self.request.get( 36 | 'conferenceInfo') 37 | ) 38 | 39 | 40 | app = webapp2.WSGIApplication([ 41 | ('/crons/set_announcement', SetAnnouncementHandler), 42 | ('/tasks/send_confirmation_email', SendConfirmationEmailHandler), 43 | ], debug=True) 44 | -------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """models.py 4 | 5 | Udacity conference server-side Python App Engine data & ProtoRPC models 6 | 7 | $Id: models.py,v 1.1 2014/05/24 22:01:10 wesc Exp $ 8 | 9 | created/forked from conferences.py by wesc on 2014 may 24 10 | 11 | """ 12 | 13 | __author__ = 'wesc+api@google.com (Wesley Chun)' 14 | 15 | import httplib 16 | import endpoints 17 | from protorpc import messages 18 | from google.appengine.ext import ndb 19 | 20 | class ConflictException(endpoints.ServiceException): 21 | """ConflictException -- exception mapped to HTTP 409 response""" 22 | http_status = httplib.CONFLICT 23 | 24 | class Profile(ndb.Model): 25 | """Profile -- User profile object""" 26 | displayName = ndb.StringProperty() 27 | mainEmail = ndb.StringProperty() 28 | teeShirtSize = ndb.StringProperty(default='NOT_SPECIFIED') 29 | conferenceKeysToAttend = ndb.StringProperty(repeated=True) 30 | 31 | class ProfileMiniForm(messages.Message): 32 | """ProfileMiniForm -- update Profile form message""" 33 | displayName = messages.StringField(1) 34 | teeShirtSize = messages.EnumField('TeeShirtSize', 2) 35 | 36 | class ProfileForm(messages.Message): 37 | """ProfileForm -- Profile outbound form message""" 38 | displayName = messages.StringField(1) 39 | mainEmail = messages.StringField(2) 40 | teeShirtSize = messages.EnumField('TeeShirtSize', 3) 41 | conferenceKeysToAttend = messages.StringField(4, repeated=True) 42 | 43 | class BooleanMessage(messages.Message): 44 | """BooleanMessage-- outbound Boolean value message""" 45 | data = messages.BooleanField(1) 46 | 47 | class Conference(ndb.Model): 48 | """Conference -- Conference object""" 49 | name = ndb.StringProperty(required=True) 50 | description = ndb.StringProperty() 51 | organizerUserId = ndb.StringProperty() 52 | topics = ndb.StringProperty(repeated=True) 53 | city = ndb.StringProperty() 54 | startDate = ndb.DateProperty() 55 | month = ndb.IntegerProperty() # TODO: do we need for indexing like Java? 56 | endDate = ndb.DateProperty() 57 | maxAttendees = ndb.IntegerProperty() 58 | seatsAvailable = ndb.IntegerProperty() 59 | 60 | class ConferenceForm(messages.Message): 61 | """ConferenceForm -- Conference outbound form message""" 62 | name = messages.StringField(1) 63 | description = messages.StringField(2) 64 | organizerUserId = messages.StringField(3) 65 | topics = messages.StringField(4, repeated=True) 66 | city = messages.StringField(5) 67 | startDate = messages.StringField(6) #DateTimeField() 68 | month = messages.IntegerField(7, variant=messages.Variant.INT32) 69 | maxAttendees = messages.IntegerField(8, variant=messages.Variant.INT32) 70 | seatsAvailable = messages.IntegerField(9, variant=messages.Variant.INT32) 71 | endDate = messages.StringField(10) #DateTimeField() 72 | websafeKey = messages.StringField(11) 73 | organizerDisplayName = messages.StringField(12) 74 | 75 | class ConferenceForms(messages.Message): 76 | """ConferenceForms -- multiple Conference outbound form message""" 77 | items = messages.MessageField(ConferenceForm, 1, repeated=True) 78 | 79 | class TeeShirtSize(messages.Enum): 80 | """TeeShirtSize -- t-shirt size enumeration value""" 81 | NOT_SPECIFIED = 1 82 | XS_M = 2 83 | XS_W = 3 84 | S_M = 4 85 | S_W = 5 86 | M_M = 6 87 | M_W = 7 88 | L_M = 8 89 | L_W = 9 90 | XL_M = 10 91 | XL_W = 11 92 | XXL_M = 12 93 | XXL_W = 13 94 | XXXL_M = 14 95 | XXXL_W = 15 96 | 97 | class ConferenceQueryForm(messages.Message): 98 | """ConferenceQueryForm -- Conference query inbound form message""" 99 | field = messages.StringField(1) 100 | operator = messages.StringField(2) 101 | value = messages.StringField(3) 102 | 103 | class ConferenceQueryForms(messages.Message): 104 | """ConferenceQueryForms -- multiple ConferenceQueryForm inbound form message""" 105 | filters = messages.MessageField(ConferenceQueryForm, 1, repeated=True) 106 | 107 | -------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """settings.py 4 | 5 | Udacity conference server-side Python App Engine app user settings 6 | 7 | $Id$ 8 | 9 | created/forked from conference.py by wesc on 2014 may 24 10 | 11 | """ 12 | 13 | # Replace the following lines with client IDs obtained from the APIs 14 | # Console or Cloud Console. 15 | WEB_CLIENT_ID = 'replace with Web client ID' 16 | ANDROID_CLIENT_ID = 'replace with Android client ID' 17 | IOS_CLIENT_ID = 'replace with iOS client ID' 18 | ANDROID_AUDIENCE = WEB_CLIENT_ID 19 | -------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/static/bootstrap/css/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | padding-top: 70px; 7 | } 8 | 9 | #signInLink, #signOutLink { 10 | cursor: pointer; 11 | } 12 | 13 | /* To disable the Google+ Sign In Button, but gapi.signin.render should be invoked to store the credential 14 | if that is stored in cookie. */ 15 | #signInButton iframe { 16 | display: none; 17 | } 18 | 19 | .required { 20 | color: red; 21 | } 22 | 23 | .dismiss-messages { 24 | cursor: pointer; 25 | } 26 | 27 | /* In a small screen, make the alert message sticky to the top */ 28 | @media (max-width: 768px) { 29 | #messages.alert, #rootMessages.alert { 30 | position: fixed; 31 | left: 0; 32 | right: 0; 33 | top: 65px; 34 | z-index: 1000; 35 | } 36 | } 37 | 38 | .form-group-condensed { 39 | margin-top: 0; 40 | margin-bottom: 5px; 41 | } 42 | 43 | .label-separated { 44 | margin-right: 8px; 45 | } 46 | 47 | .spinner { 48 | position: fixed; 49 | top: 70px; 50 | z-index: 9999; 51 | } 52 | 53 | #signInButton { 54 | cursor: pointer; 55 | vertical-align: middle; 56 | } 57 | 58 | #profile-container { 59 | float: right; 60 | font-size: 85%; 61 | } 62 | 63 | #profile img { 64 | max-height: 35px; 65 | width: auto; 66 | vertical-align: middle; 67 | } 68 | 69 | #show-conferences-tab { 70 | margin-bottom: 20px; 71 | } 72 | 73 | ul#filters { 74 | list-style: none; 75 | padding-left: 0px; 76 | font-size: 85%; 77 | } 78 | 79 | ul#filters span.glyphicon-remove { 80 | font-size: 80%; 81 | } 82 | 83 | ul#conferences-list { 84 | list-style: none; 85 | } 86 | 87 | .intro-header { 88 | padding-top: 50px; 89 | padding-bottom: 50px; 90 | color: #f8f8f8; 91 | text-shadow: black 0.1em 0.1em 0.2em; 92 | background: url(/img/meeting-room.jpg) no-repeat center center; 93 | background-size: cover; 94 | text-align: center; 95 | } 96 | 97 | .intro-message { 98 | position: relative; 99 | padding-top: 5%; 100 | padding-bottom: 5%; 101 | vertical-align: middle; 102 | } 103 | 104 | .section-a { 105 | padding: 50px 0; 106 | } 107 | -------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/static/bootstrap/css/offcanvas.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Style tweaks 3 | * -------------------------------------------------- 4 | */ 5 | html, 6 | body { 7 | overflow-x: hidden; /* Prevent scroll on narrow devices */ 8 | } 9 | 10 | footer { 11 | padding: 30px 0; 12 | } 13 | 14 | /* 15 | * Off Canvas 16 | * -------------------------------------------------- 17 | */ 18 | @media screen and (max-width: 767px) { 19 | .row-offcanvas { 20 | position: relative; 21 | -webkit-transition: all .25s ease-out; 22 | -moz-transition: all .25s ease-out; 23 | transition: all .25s ease-out; 24 | } 25 | 26 | .row-offcanvas-right { 27 | right: 0; 28 | } 29 | 30 | .row-offcanvas-left { 31 | left: 0; 32 | } 33 | 34 | .row-offcanvas-right 35 | .sidebar-offcanvas { 36 | right: -50%; /* 6 columns */ 37 | } 38 | 39 | .row-offcanvas-left 40 | .sidebar-offcanvas { 41 | left: -50%; /* 6 columns */ 42 | } 43 | 44 | .row-offcanvas-right.active { 45 | right: 50%; /* 6 columns */ 46 | } 47 | 48 | .row-offcanvas-left.active { 49 | left: 50%; /* 6 columns */ 50 | } 51 | 52 | .sidebar-offcanvas { 53 | position: absolute; 54 | top: 0; 55 | width: 50%; /* 6 columns */ 56 | } 57 | } -------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_5/00_Conference_Central/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_5/00_Conference_Central/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_5/00_Conference_Central/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/static/img/CloudPlatform_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_5/00_Conference_Central/static/img/CloudPlatform_logo.png -------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/static/img/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_5/00_Conference_Central/static/img/ajax-loader.gif -------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/static/img/business1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_5/00_Conference_Central/static/img/business1.jpg -------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/static/img/business2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_5/00_Conference_Central/static/img/business2.jpg -------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/static/img/business3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_5/00_Conference_Central/static/img/business3.jpg -------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_5/00_Conference_Central/static/img/favicon.ico -------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/static/img/meeting-room.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/ud858/bd400cf7caf39fed433d95565441298e6400f9e9/Lesson_5/00_Conference_Central/static/img/meeting-room.jpg -------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/static/js/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc object 5 | * @name conferenceApp 6 | * @requires $routeProvider 7 | * @requires conferenceControllers 8 | * @requires ui.bootstrap 9 | * 10 | * @description 11 | * Root app, which routes and specifies the partial html and controller depending on the url requested. 12 | * 13 | */ 14 | var app = angular.module('conferenceApp', 15 | ['conferenceControllers', 'ngRoute', 'ui.bootstrap']). 16 | config(['$routeProvider', 17 | function ($routeProvider) { 18 | $routeProvider. 19 | when('/conference', { 20 | templateUrl: '/partials/show_conferences.html', 21 | controller: 'ShowConferenceCtrl' 22 | }). 23 | when('/conference/create', { 24 | templateUrl: '/partials/create_conferences.html', 25 | controller: 'CreateConferenceCtrl' 26 | }). 27 | when('/conference/detail/:websafeConferenceKey', { 28 | templateUrl: '/partials/conference_detail.html', 29 | controller: 'ConferenceDetailCtrl' 30 | }). 31 | when('/profile', { 32 | templateUrl: '/partials/profile.html', 33 | controller: 'MyProfileCtrl' 34 | }). 35 | when('/', { 36 | templateUrl: '/partials/home.html' 37 | }). 38 | otherwise({ 39 | redirectTo: '/' 40 | }); 41 | }]); 42 | 43 | /** 44 | * @ngdoc filter 45 | * @name startFrom 46 | * 47 | * @description 48 | * A filter that extracts an array from the specific index. 49 | * 50 | */ 51 | app.filter('startFrom', function () { 52 | /** 53 | * Extracts an array from the specific index. 54 | * 55 | * @param {Array} data 56 | * @param {Integer} start 57 | * @returns {Array|*} 58 | */ 59 | var filter = function (data, start) { 60 | return data.slice(start); 61 | } 62 | return filter; 63 | }); 64 | 65 | 66 | /** 67 | * @ngdoc constant 68 | * @name HTTP_ERRORS 69 | * 70 | * @description 71 | * Holds the constants that represent HTTP error codes. 72 | * 73 | */ 74 | app.constant('HTTP_ERRORS', { 75 | 'UNAUTHORIZED': 401 76 | }); 77 | 78 | 79 | /** 80 | * @ngdoc service 81 | * @name oauth2Provider 82 | * 83 | * @description 84 | * Service that holds the OAuth2 information shared across all the pages. 85 | * 86 | */ 87 | app.factory('oauth2Provider', function ($modal) { 88 | var oauth2Provider = { 89 | CLIENT_ID: 'web-client-id', 90 | SCOPES: 'email profile', 91 | signedIn: false 92 | } 93 | 94 | /** 95 | * Calls the OAuth2 authentication method. 96 | */ 97 | oauth2Provider.signIn = function (callback) { 98 | gapi.auth.signIn({ 99 | 'clientid': oauth2Provider.CLIENT_ID, 100 | 'cookiepolicy': 'single_host_origin', 101 | 'accesstype': 'online', 102 | 'approveprompt': 'auto', 103 | 'scope': oauth2Provider.SCOPES, 104 | 'callback': callback 105 | }); 106 | }; 107 | 108 | /** 109 | * Logs out the user. 110 | */ 111 | oauth2Provider.signOut = function () { 112 | gapi.auth.signOut(); 113 | // Explicitly set the invalid access token in order to make the API calls fail. 114 | gapi.auth.setToken({access_token: ''}) 115 | oauth2Provider.signedIn = false; 116 | }; 117 | 118 | /** 119 | * Shows the modal with Google+ sign in button. 120 | * 121 | * @returns {*|Window} 122 | */ 123 | oauth2Provider.showLoginModal = function() { 124 | var modalInstance = $modal.open({ 125 | templateUrl: '/partials/login.modal.html', 126 | controller: 'OAuth2LoginModalCtrl' 127 | }); 128 | return modalInstance; 129 | }; 130 | 131 | return oauth2Provider; 132 | }); 133 | -------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/static/partials/conference_detail.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 8 |
9 | 10 |
11 |
12 | 13 |
14 |
15 |
16 |

{{conference.name}}

17 |
{{conference.description}}
18 |
19 | 20 | {{conference.maxAttendees - conference.seatsAvailable}} / {{conference.maxAttendees}} 21 |
22 |
23 | 24 | {{conference.organizerDisplayName}} 25 |
26 |

Register

28 |

Unregister

30 |
31 | 32 |
33 |
34 |
35 | 36 | {{conference.city}} 37 |
38 |
39 | 40 | 41 | {{topic}} 42 | 43 |
44 |
45 | 46 | {{conference.startDate | date:'dd-MMMM-yyyy'}} 47 |
48 |
49 | 50 | {{conference.endDate | date:'dd-MMMM-yyyy'}} 51 |
52 |
53 |
54 |
55 |
56 |
57 | -------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/static/partials/home.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

Welcome to Conference Central

6 | 7 |

Lets you manage conferences

8 |
9 | 17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |

View conferences

27 | 28 |

View by city, topics, date, max attendees.

29 | View conferences 30 |
31 |
32 | 33 |
34 |
35 |
36 | 37 |
38 |
39 |
40 |
41 |
42 |

Create new conferences

43 | 44 |

In 10 seconds or less.

45 | Create a conference 46 |
47 |
48 | 49 |
50 |
51 |
52 | 53 |
54 |
55 |
56 |
57 |
58 |

Update your profile

59 | View my profile 60 |
61 |
62 | 63 |
64 |
65 |
66 | -------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/static/partials/login.modal.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Please sign in to complete this action.

4 |
5 | 8 |
-------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/static/partials/profile.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 8 |
9 | 10 |
11 |
12 |
13 |
14 |

My Profile

15 |
16 |
17 | 18 | Changed 20 | 22 |
23 | 24 |
25 | 26 | Changed 28 | 32 |
33 | 34 | 37 |
38 |
39 |
40 |
-------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Conference Central 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 67 | 68 |
69 |
70 |
71 |
72 | 73 | 75 |
76 |
77 |
78 | 79 |
80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Lesson_5/00_Conference_Central/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import time 4 | import uuid 5 | 6 | from google.appengine.api import urlfetch 7 | from models import Profile 8 | 9 | def getUserId(user, id_type="email"): 10 | if id_type == "email": 11 | return user.email() 12 | 13 | if id_type == "oauth": 14 | """A workaround implementation for getting userid.""" 15 | auth = os.getenv('HTTP_AUTHORIZATION') 16 | bearer, token = auth.split() 17 | token_type = 'id_token' 18 | if 'OAUTH_USER_ID' in os.environ: 19 | token_type = 'access_token' 20 | url = ('https://www.googleapis.com/oauth2/v1/tokeninfo?%s=%s' 21 | % (token_type, token)) 22 | user = {} 23 | wait = 1 24 | for i in range(3): 25 | resp = urlfetch.fetch(url) 26 | if resp.status_code == 200: 27 | user = json.loads(resp.content) 28 | break 29 | elif resp.status_code == 400 and 'invalid_token' in resp.content: 30 | url = ('https://www.googleapis.com/oauth2/v1/tokeninfo?%s=%s' 31 | % ('access_token', token)) 32 | else: 33 | time.sleep(wait) 34 | wait = wait + i 35 | return user.get('user_id', '') 36 | 37 | if id_type == "custom": 38 | # implement your own user_id creation and getting algorythm 39 | # this is just a sample that queries datastore for an existing profile 40 | # and generates an id if profile does not exist for an email 41 | profile = Conference.query(Conference.mainEmail == user.email()) 42 | if profile: 43 | return profile.id() 44 | else: 45 | return str(uuid.uuid1().get_hex()) 46 | -------------------------------------------------------------------------------- /Lesson_5/Additions/TODO_1_app.yaml: -------------------------------------------------------------------------------- 1 | - url: /crons/set_announcement 2 | script: main.app 3 | login: admin -------------------------------------------------------------------------------- /Lesson_5/Additions/TODO_1_conference.py: -------------------------------------------------------------------------------- 1 | from google.appengine.api import memcache 2 | 3 | from models import StringMessage 4 | 5 | # - - - Announcements - - - - - - - - - - - - - - - - - - - - 6 | 7 | @staticmethod 8 | def _cacheAnnouncement(): 9 | """Create Announcement & assign to memcache; used by 10 | memcache cron job & putAnnouncement(). 11 | """ 12 | confs = Conference.query(ndb.AND( 13 | Conference.seatsAvailable <= 5, 14 | Conference.seatsAvailable > 0) 15 | ).fetch(projection=[Conference.name]) 16 | 17 | if confs: 18 | # If there are almost sold out conferences, 19 | # format announcement and set it in memcache 20 | announcement = '%s %s' % ( 21 | 'Last chance to attend! The following conferences ' 22 | 'are nearly sold out:', 23 | ', '.join(conf.name for conf in confs)) 24 | memcache.set(MEMCACHE_ANNOUNCEMENTS_KEY, announcement) 25 | else: 26 | # If there are no sold out conferences, 27 | # delete the memcache announcements entry 28 | announcement = "" 29 | memcache.delete(MEMCACHE_ANNOUNCEMENTS_KEY) 30 | 31 | return announcement 32 | 33 | 34 | @endpoints.method(message_types.VoidMessage, StringMessage, 35 | path='conference/announcement/get', 36 | http_method='GET', name='getAnnouncement') 37 | def getAnnouncement(self, request): 38 | """Return Announcement from memcache.""" 39 | # TODO 1 40 | # return an existing announcement from Memcache or an empty string. 41 | announcement = "" 42 | return StringMessage(data=announcement) 43 | 44 | 45 | -------------------------------------------------------------------------------- /Lesson_5/Additions/TODO_1_main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import webapp2 3 | from google.appengine.api import app_identity 4 | from google.appengine.api import mail 5 | from conference import ConferenceApi 6 | 7 | class SetAnnouncementHandler(webapp2.RequestHandler): 8 | def get(self): 9 | """Set Announcement in Memcache.""" 10 | # TODO 1 11 | # use _cacheAnnouncement() to set announcement in Memcache 12 | 13 | app = webapp2.WSGIApplication([ 14 | ('/crons/set_announcement', SetAnnouncementHandler), 15 | ], debug=True) 16 | -------------------------------------------------------------------------------- /Lesson_5/Additions/TODO_1_models.py: -------------------------------------------------------------------------------- 1 | class StringMessage(messages.Message): 2 | """StringMessage-- outbound (single) string message""" 3 | data = messages.StringField(1, required=True) -------------------------------------------------------------------------------- /Lesson_5/Additions/TODO_2_app.yaml: -------------------------------------------------------------------------------- 1 | - url: /tasks/send_confirmation_email 2 | script: main.app 3 | login: admin -------------------------------------------------------------------------------- /Lesson_5/Additions/TODO_2_conference.py: -------------------------------------------------------------------------------- 1 | from google.appengine.api import taskqueue 2 | 3 | # Look for TODO 2 4 | # create Conference, send email to organizer confirming 5 | # creation of Conference & return (modified) ConferenceForm 6 | Conference(**data).put() 7 | taskqueue.add(params={'email': user.email(), 8 | 'conferenceInfo': repr(request)}, 9 | url='/tasks/send_confirmation_email' 10 | ) -------------------------------------------------------------------------------- /Lesson_5/Additions/TODO_2_main.py: -------------------------------------------------------------------------------- 1 | from google.appengine.api import app_identity 2 | from google.appengine.api import mail 3 | 4 | 5 | class SendConfirmationEmailHandler(webapp2.RequestHandler): 6 | def post(self): 7 | """Send email confirming Conference creation.""" 8 | mail.send_mail( 9 | 'noreply@%s.appspotmail.com' % ( 10 | app_identity.get_application_id()), # from 11 | self.request.get('email'), # to 12 | 'You created a new Conference!', # subj 13 | 'Hi, you have created a following ' # body 14 | 'conference:\r\n\r\n%s' % self.request.get( 15 | 'conferenceInfo') 16 | ) 17 | 18 | 19 | app = webapp2.WSGIApplication([ 20 | ('/crons/set_announcement', SetAnnouncementHandler), 21 | ('/tasks/send_confirmation_email', SendConfirmationEmailHandler), 22 | ], debug=True) -------------------------------------------------------------------------------- /Lesson_5/Additions/TODO_3_cron.yaml: -------------------------------------------------------------------------------- 1 | cron: 2 | - description: Repopulate the announcement every 1 hour 3 | url: /crons/set_announcement 4 | schedule: every 1 hours -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ud858 2 | ===== 3 | 4 | Course code for Building Scalable Apps with Google App Engine in Python class 5 | 6 | # Archival Note 7 | This repository is deprecated; therefore, we are going to archive it. However, learners will be able to fork it to their personal Github account but cannot submit PRs to this repository. If you have any issues or suggestions to make, feel free to: 8 | - Utilize the https://knowledge.udacity.com/ forum to seek help on content-specific issues. 9 | - Submit a support ticket along with the link to your forked repository if (learners are) blocked for other reasons. Here are the links for the [retail consumers](https://udacity.zendesk.com/hc/en-us/requests/new) and [enterprise learners](https://udacityenterprise.zendesk.com/hc/en-us/requests/new?ticket_form_id=360000279131). --------------------------------------------------------------------------------