└── hello-udacity
├── main.pyc
├── favicon.ico
├── app.yaml
├── index.yaml
├── templates
├── front.html
├── newpost.html
├── blog.html
├── login.html
└── signup.html
├── stylesheets
└── stylesheet.css
└── main.py
/hello-udacity/main.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sup/udacity-253/master/hello-udacity/main.pyc
--------------------------------------------------------------------------------
/hello-udacity/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sup/udacity-253/master/hello-udacity/favicon.ico
--------------------------------------------------------------------------------
/hello-udacity/app.yaml:
--------------------------------------------------------------------------------
1 | application: hello-udacity77024
2 | version: 1
3 | runtime: python27
4 | api_version: 1
5 | threadsafe: yes
6 |
7 | handlers:
8 | - url: /favicon\.ico
9 | static_files: favicon.ico
10 | upload: favicon\.ico
11 |
12 | - url: /stylesheets
13 | static_dir: stylesheets
14 |
15 | - url: .*
16 | script: main.app
17 |
18 | libraries:
19 | - name: webapp2
20 | version: "2.5.2"
21 |
22 | - name: jinja2
23 | version: latest
24 |
--------------------------------------------------------------------------------
/hello-udacity/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 |
--------------------------------------------------------------------------------
/hello-udacity/templates/front.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | /ascii/
6 |
7 |
8 |
61 |
62 | /ascii/
63 |
75 |
76 | {% if img_url %}
77 |
78 | {% endif %}
79 |
80 |
81 |
82 | {% for art in arts %}
83 |
84 |
{{art.title}}
85 |
{{art.art}}
86 |
87 | {% endfor %}
88 |
89 |
--------------------------------------------------------------------------------
/hello-udacity/templates/newpost.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
21 | New Post
22 |
34 |
35 |
36 |
37 |
38 |
46 |
47 |
--------------------------------------------------------------------------------
/hello-udacity/stylesheets/stylesheet.css:
--------------------------------------------------------------------------------
1 | /* CSS Stylesheet for Udacity Course Blog */
2 |
3 | body {
4 | position: relative;
5 | font-family: Helvetica, Arial, sans-serif;
6 | font-size: 14px;
7 |
8 | width: 800px;
9 | margin: 0 auto;
10 | padding: 10px;
11 | color: #333;
12 | }
13 |
14 | .login-area {
15 | font-size: 13px;
16 | color: gray;
17 | position: absolute;
18 | top: 10px;
19 | right: 10px;
20 | }
21 |
22 | .login-link {
23 | color: gray;
24 | text-decoration: none;
25 | }
26 |
27 | .login-link:visited {
28 | color: gray;
29 | }
30 |
31 | .login-link:hover {
32 | text-decoration: underline;
33 | }
34 |
35 | h2 {
36 | font-size: 24px;
37 | font-weight: bold;
38 | margin-bottom: 20px;
39 | }
40 |
41 | .error {
42 | color: red;
43 | }
44 |
45 | label {
46 | display: block;
47 | font-size: 20px;
48 | }
49 |
50 | label + label {
51 | margin-top: 20px;
52 | }
53 |
54 | input[type=text] {
55 | width: 500px;
56 | font-size: 20px;
57 | padding: 2px;
58 | font-family: monospace;
59 | }
60 |
61 | textarea {
62 | width: 500px;
63 | height: 400px;
64 | font-size: 17px;
65 | font-family: monospace;
66 | }
67 |
68 | input[type=submit] {
69 | font-size: 24px;
70 | }
71 |
72 | .main-title {
73 | display: block;
74 | color: #222;
75 | font-size: 40px;
76 | font-weight: bold;
77 | text-align: center;
78 | margin-bottom: 30px;
79 | text-decoration: none;
80 | }
81 |
82 | .content + .content {
83 | margin-top: 15px;
84 | }
85 |
86 | .post-heading {
87 | position: relative;
88 | border-bottom: 3px solid #666;
89 | }
90 |
91 | .post-title {
92 | font-size: 24px;
93 | font-weight: bold;
94 |
95 | text-decoration: none;
96 | color: #333;
97 | }
98 |
99 | .post-title:visisted {
100 | color: #333;
101 | }
102 |
103 | .post-date {
104 | position: absolute;
105 | right: 0px;
106 | bottom: 0px;
107 | color: #999;
108 |
109 | }
110 |
111 | .post-content {
112 | margin-top: 5px;
113 | }
114 |
115 | .age {
116 | position: absolute;
117 | bottom: 0px;
118 | right: 0px;
119 | color: gray;
120 | font-size: 12px;
121 | }
122 |
--------------------------------------------------------------------------------
/hello-udacity/templates/blog.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Blog
10 |
11 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
21 | Charles' Blog
22 | {% for post in posts %}
23 |
24 |
25 |
{{post.subject}}
26 |
{{post.created}}
27 |
28 |
{{post.content | safe}}
29 |
30 |
31 |
32 | {% endfor %}
33 |
34 |
35 |
36 |
37 |
45 |
46 |
--------------------------------------------------------------------------------
/hello-udacity/templates/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
21 | Login
22 |
44 |
45 |
46 |
47 |
48 |
56 |
57 |
--------------------------------------------------------------------------------
/hello-udacity/templates/signup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
21 | Signup
22 |
57 |
58 |
59 |
60 |
61 |
69 |
70 |
--------------------------------------------------------------------------------
/hello-udacity/main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright 2007 Google Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 | import hashlib
18 | import os
19 | import jinja2
20 | import webapp2
21 | import urlparse
22 | import re
23 | import random
24 | import string
25 | import urllib2
26 | from xml.dom import minidom
27 | import json
28 | import logging
29 | import time
30 | from google.appengine.api import memcache
31 | from google.appengine.ext import db
32 |
33 | template_dir = os.path.join(os.path.dirname(__file__), 'templates')
34 | jinja_env = jinja2.Environment(loader = jinja2.FileSystemLoader(template_dir),
35 | autoescape = True)
36 |
37 | #== GLOBAL VARIABLES ====================================================
38 | USER_REGEX = re.compile(r"^[a-zA-Z0-9_-]{3,20}$")
39 | PASS_REGEX = re.compile(r"^.{3,20}$")
40 | EMAIL_REGEX = re.compile(r"^[\S]+@[\S]+\.[\S]+$")
41 | GMAPS_URL = "http://maps.googleapis.com/maps/api/staticmap?size=380x263&sensor=false&"
42 |
43 | #== HTML DOCUMENTS ======================================================
44 | date_form = """
45 |
65 | """
66 |
67 | rot13_form = """
68 | Enter some text to ROT13:
69 |
74 | """
75 |
76 | signup_form = """
77 | Signup
78 |
113 | """
114 |
115 | #== Global Handler (Jinja2 Templates) ====================================
116 | class Handler(webapp2.RequestHandler):
117 | def write(self, *a, **kw):
118 | self.response.write(*a, **kw)
119 |
120 | def render_str(self, template, **params):
121 | t = jinja_env.get_template(template)
122 | return t.render(params)
123 |
124 | def render(self, template, **kw):
125 | self.write(self.render_str(template, **kw))
126 |
127 |
128 | #== Unit 2 ===============================================================
129 | class DateHandler(webapp2.RequestHandler):
130 | def write_date_form(self, error="", month="", day="", year=""):
131 | self.response.write(date_form % {"error": error,
132 | "month": escape_html(month),
133 | "day": escape_html(day),
134 | "year": escape_html(year)})
135 |
136 | def get(self):
137 | #self.response.headers['Content-Type'] = 'text/plain'
138 | self.write_date_form()
139 |
140 | def post(self):
141 | user_month = self.request.get('month')
142 | user_day = self.request.get('day')
143 | user_year = self.request.get('year')
144 |
145 | month = valid_month(user_month)
146 | day = valid_day(user_day)
147 | year = valid_year(user_year)
148 |
149 | if not (month and day and year):
150 | self.write_date_form("That doesn't look like a valid date.", user_month, user_day, user_year)
151 | else:
152 | self.redirect("/thanks")
153 |
154 |
155 | class ThanksHandler(webapp2.RequestHandler):
156 | def get(self):
157 | self.response.write("Thanks! That's a valid date!")
158 |
159 |
160 | #== PROBLEM SET 2 ========================================================
161 | class Rot13Handler(webapp2.RequestHandler):
162 | def write_rot13_form(self, message=""):
163 | self.response.write(rot13_form % {'message': message})
164 |
165 | def get(self):
166 | self.write_rot13_form()
167 |
168 | def post(self):
169 | message = self.request.get('text')
170 | message = caeser(message, 13)
171 | #Escape HTML Characters
172 | message = escape_html(message)
173 | self.write_rot13_form(message)
174 |
175 | #== PROBLEM SET 2/4 =======================================================
176 | class SignUpHandler(Handler):
177 | #Method for rendering form using Python string substitution
178 | def write_signup_form(self, username="", email="", user_error="", pass_error="", verify_error="", email_error=""):
179 | self.response.write(signup_form % {"username":username,
180 | "password":"",
181 | "verify":"",
182 | "email":email,
183 | "user_error":user_error,
184 | "pass_error":pass_error,
185 | "verify_error":verify_error,
186 | "email_error":email_error})
187 |
188 | #Method for rendering form using a template
189 | def render_form(self, username="", email="", user_error="", pass_error="", verify_error="", email_error=""):
190 | self.render("signup.html", username=username, password="", verify="", email=email,
191 | user_error=user_error, pass_error=pass_error, verify_error=verify_error,
192 | email_error=email_error)
193 |
194 | def get(self):
195 | self.render_form()
196 |
197 | def post(self):
198 | #Raw input data
199 | user_username = self.request.get('username')
200 | user_password = self.request.get('password')
201 | user_verify = self.request.get('verify')
202 | user_email = self.request.get('email')
203 |
204 | #Test if the input data are valid
205 | username = valid_username(user_username)
206 | password = valid_password(user_password)
207 | verify = valid_verify(user_password, user_verify)
208 | email = valid_email(user_email)
209 |
210 | #If the email field is blank, then it's valid
211 | if user_email == "":
212 | email = True
213 |
214 | #Initialize error statements
215 | USER_ERROR = ""
216 | PASS_ERROR = ""
217 | VERIFY_ERROR = ""
218 | EMAIL_ERROR = ""
219 |
220 | #Discrete system for each error
221 | if not username:
222 | USER_ERROR = "That's not a valid username."
223 | if not verify:
224 | VERIFY_ERROR = "The passwords don't match."
225 | if not password:
226 | VERIFY_ERROR = ""
227 | PASS_ERROR = "That's not a valid password."
228 | if not email:
229 | EMAIL_ERROR = "That's not a valid email."
230 |
231 | #If an error exists, rewrite the form for the user
232 | if not (username and verify and password and email):
233 | self.render_form(user_username, user_email, USER_ERROR, PASS_ERROR, VERIFY_ERROR, EMAIL_ERROR)
234 |
235 | #Else, redirect to the welcome and set a browser cookie
236 | elif username and verify and password and email:
237 | new_user = User.all().filter("username", user_username).get()
238 | print new_user
239 | if new_user == None:
240 | hashed_pw = make_pw_hash(str(user_username), str(user_password))
241 | user = User(username = user_username, password = hashed_pw, email = user_email)
242 | user.put()
243 | self.response.headers['Content-Type'] = 'text/plain'
244 | self.response.headers.add_header('Set-Cookie', 'username=%s;Path=/' % str(user_username))
245 | self.redirect("/blog/welcome")
246 | else:
247 | USER_ERROR = "That user already exists."
248 | self.render_form(user_username, user_email, USER_ERROR, PASS_ERROR, VERIFY_ERROR, EMAIL_ERROR)
249 |
250 |
251 | class WelcomeHandler(Handler):
252 | def get(self):
253 | self.response.headers['Content-Type'] = 'text/plain'
254 | username = self.request.cookies.get('username')
255 | #If the username parameter is changed to be invalid, redirect
256 | if not valid_username(username):
257 | self.redirect("/blog/signup")
258 | self.write("Welcome, " + username + "!")
259 |
260 |
261 | class LoginHandler(Handler):
262 | def render_form(self, username="", login_error=""):
263 | self.render("login.html", username=username, password="", login_error=login_error)
264 |
265 | def get(self):
266 | self.render_form()
267 |
268 | def post(self):
269 | login_error = "Either the username or password is incorrect."
270 | username = self.request.get('username')
271 | password = self.request.get('password')
272 | current_user = User.all().filter("username", username).get()
273 | if current_user == None:
274 | self.render_form(username, login_error)
275 | else:
276 | password_hash = current_user.password
277 | print password_hash
278 | if valid_pw(username, password, password_hash):
279 | self.response.headers['Content-Type'] = 'text/plain'
280 | self.response.headers.add_header('Set-Cookie', 'username=%s;Path=/' % str(username))
281 | self.redirect("/blog/welcome")
282 | print "redirected to welcome"
283 | else:
284 | self.render_form(username, login_error)
285 |
286 |
287 | class LogoutHandler(Handler):
288 | def get(self):
289 | self.response.headers.add_header('Set-Cookie', 'username=;Path=/')
290 | self.redirect("/blog/signup")
291 |
292 |
293 | class User(db.Model):
294 | username = db.StringProperty(required = True)
295 | password = db.StringProperty(required = True)
296 | email = db.StringProperty(required = False)
297 |
298 |
299 | #== ASCII CHAN ===========================================================
300 | def top_arts(update = False):
301 | key = "top"
302 | arts = memcache.get(key)
303 | if arts is None or update:
304 | print "DB QUERY"
305 | arts = db.GqlQuery("SELECT * FROM Art ORDER BY created DESC LIMIT 10")
306 | #Cache the query so we don't keep running them
307 | arts = list(arts)
308 | memcache.set(key, arts)
309 | return arts
310 |
311 | class AsciiHandler(Handler):
312 | def render_front(self, title="", art="", error=""):
313 | arts = top_arts()
314 | #Find which arts have coordinates. If any, make an image url
315 | points = filter(None, (a.coords for a in arts))
316 | img_url = None
317 | if points:
318 | img_url = gmaps_img(points)
319 | #Display the image url
320 | self.render("front.html", title=title, art=art, error=error, arts=arts, img_url=img_url)
321 |
322 | def get(self):
323 | self.render_front()
324 |
325 | def post(self):
326 | title = self.request.get("title")
327 | art = self.request.get("art")
328 | if title and art:
329 | a = Art(title = title, art = art)
330 | #Lookup user coordinates from IP and add them to the art
331 | coords = get_coords(self.request.remote_addr)
332 | if coords:
333 | a.coords = coords
334 | a.put()
335 | time.sleep(.5)
336 | top_arts(True)
337 | self.redirect("/ascii")
338 | else:
339 | error = "you need to post both a title and art!"
340 | self.render_front(title, art, error)
341 |
342 |
343 | class Art(db.Model):
344 | title = db.StringProperty(required = True)
345 | art = db.TextProperty(required = True)
346 | created = db.DateTimeProperty(auto_now_add = True)
347 | coords = db.GeoPtProperty(required = False)
348 |
349 |
350 | #== PROBLEM SET 3 BLOG ===================================================
351 | class BlogHandler(Handler):
352 | """
353 | Handler for displaying the front page of the blog
354 | """
355 | def render_blog(self):
356 | posts = db.GqlQuery("SELECT * FROM BlogPost ORDER BY created DESC")
357 | self.render("blog.html", posts=posts)
358 |
359 | def get(self):
360 | self.render_blog()
361 |
362 |
363 | class PageHandler(Handler):
364 | """
365 | Handler for displaying unique pages for each blog post
366 | """
367 | def render_post(self, post=""):
368 | posts = [post]
369 | self.render("blog.html", posts=posts)
370 |
371 | def get(self, post_id):
372 | post_key = db.Key.from_path('BlogPost', int(post_id))
373 | post = db.get(post_key)
374 | self.render_post(post)
375 |
376 |
377 | class NewPostHandler(Handler):
378 | """
379 | Handler for working with new post form to create new blog posts
380 | """
381 | def render_form(self, subject="", content="", error=""):
382 | self.render("newpost.html", subject=subject, content=content, error=error)
383 |
384 | def get(self):
385 | self.render_form()
386 |
387 | def post(self):
388 | subject = self.request.get("subject")
389 | content = self.request.get("content")
390 | if subject and content:
391 | bp = BlogPost(subject = subject, content = content)
392 | bp.put()
393 | self.redirect('/blog/%s' % str(bp.key().id()))
394 | else:
395 | error = "You need both a subject and some content."
396 | self.render_form(subject, content, error)
397 |
398 | class BlogPost(db.Model):
399 | subject = db.StringProperty(required = True)
400 | content = db.TextProperty(required = True)
401 | created = db.DateTimeProperty(auto_now_add = True)
402 |
403 | #== PROBLEM SET 5 ========================================================
404 | class JSONHandler(Handler):
405 | def get(self, post_id):
406 | self.response.headers["Content-Type"] = "application/json; charset=UTF-8"
407 | if post_id:
408 | output = {}
409 | p = BlogPost.get_by_id(int(post_id))
410 | output.update({"subject":p.subject,"content":p.content,"created":p.created.strftime("%b %d, %Y")})
411 | else:
412 | output = []
413 | posts = db.GqlQuery("Select * From BlogPost Order By created DESC")
414 | for p in posts:
415 | output.append({"subject":p.subject,"content":p.content,"created":p.created.strftime("%b %d, %Y")})
416 | self.write(json.dumps(output))
417 |
418 |
419 | #== MAIN APPLICATION =====================================================
420 | app = webapp2.WSGIApplication([
421 | ('/', DateHandler),
422 | ('/thanks', ThanksHandler),
423 | ('/rot13', Rot13Handler),
424 | ('/blog/signup', SignUpHandler),
425 | ('/blog/welcome', WelcomeHandler),
426 | ('/ascii', AsciiHandler),
427 | ('/blog', BlogHandler),
428 | ('/blog/newpost', NewPostHandler),
429 | ('/blog/([0-9]+)', PageHandler),
430 | ('/blog/login', LoginHandler),
431 | ('/blog/logout', LogoutHandler),
432 | ('/blog/([0-9]*).json+',JSONHandler)
433 | ], debug=True)
434 |
435 | #== Helper Functions/Data ================================================
436 |
437 | #FOR DATES
438 | months = ['January',
439 | 'February',
440 | 'March',
441 | 'April',
442 | 'May',
443 | 'June',
444 | 'July',
445 | 'August',
446 | 'September',
447 | 'October',
448 | 'November',
449 | 'December']
450 |
451 | def valid_day(day):
452 | if day:
453 | try:
454 | i = int(day)
455 | if i >= 1 and i <= 31:
456 | return i
457 | else:
458 | return None
459 | except Exception:
460 | return None
461 |
462 | def valid_month(month):
463 | try:
464 | i = months.index(month.lower().capitalize())
465 | return months[i]
466 | except Exception:
467 | return None
468 |
469 | def valid_year(year):
470 | if year:
471 | try:
472 | y = int(year)
473 | if y >= 1900 and y <= 2020:
474 | return y
475 | except Exception:
476 | return None
477 |
478 | #FOR ESCAPING HTML
479 | def escape_html(s):
480 | """
481 | Returns: Original string with escaped html characters.
482 | """
483 | for (i,o) in (("&", "&"),
484 | (">", ">"),
485 | ("<", "<"),
486 | ('"', """)):
487 | s = s.replace(i,o)
488 | return s
489 |
490 | #FOR ROT13
491 | def caeser(message, key):
492 | """
493 | Returns: Ciphertext of message encrypted with a Caeser cipher. This
494 | encryption function preserves case, whitespace, and punctuation.
495 | """
496 | #Raw Alphabets
497 | low_alpha = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
498 | "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z" ]
499 |
500 | cap_alpha = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
501 | "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ]
502 |
503 | #Dictionaries for each alphabet type
504 | low_dict = {}
505 | cap_dict = {}
506 | for i in range(26):
507 | low_dict[low_alpha[i]] = low_alpha[(i+key) % 26]
508 | cap_dict[cap_alpha[i]] = cap_alpha[(i+key) % 26]
509 |
510 | #Conversion of message to ciphertext
511 | ciphertext = ""
512 | for l in message:
513 | if l in low_dict:
514 | l = low_dict[l]
515 | if l in cap_dict:
516 | l = cap_dict[l]
517 | ciphertext += l
518 |
519 | return ciphertext
520 |
521 | #FOR SIGNUP
522 | def valid_username(username):
523 | """
524 | Returns: True if the username is valid, else False.
525 | """
526 | return USER_REGEX.match(username)
527 |
528 | def valid_password(password):
529 | """
530 | Returns: True if the password is valid, else False.
531 | """
532 | return PASS_REGEX.match(password)
533 |
534 | def valid_email(email):
535 | """
536 | Returns: True if the email is valid, else False.
537 | """
538 | return EMAIL_REGEX.match(email)
539 |
540 | def valid_verify(password,verify):
541 | """
542 | Returns: True if the passwords match, else False.
543 | """
544 | return password == verify
545 |
546 | #For hashing/security (naive)
547 | def hash_str(s):
548 | return hashlib.md5(s).hexdigest()
549 |
550 | def make_secure_val(s):
551 | return "%s|%s" % (s, hash_str(s))
552 |
553 | def check_secure_val(h):
554 | val = h.split("|")[0]
555 | if h == make_secure_val(val):
556 | return val
557 |
558 | def make_salt(size = 5, char = string.letters):
559 | return ''.join(random.choice(char) for x in xrange(size))
560 |
561 | def make_pw_hash(name, pw, salt = None):
562 | if not salt:
563 | salt = make_salt()
564 | return hashlib.sha256(name + pw + salt).hexdigest() + "," + salt
565 |
566 | def valid_pw(name, pw, h):
567 | salt = h.split(",")[1]
568 | return h == hashlib.sha256(name + pw + salt).hexdigest() + "," + salt
569 |
570 | #For geolocation API
571 | def get_coords(ip):
572 | BASE_URL = "http://api.hostip.info/?ip="
573 | url = BASE_URL + ip
574 | content = None
575 | try:
576 | content = urllib2.urlopen(url).read()
577 | except URLError:
578 | return
579 |
580 | if content:
581 | #Parse the XML and find the coordinates
582 | xml = minidom.parseString(content)
583 | coords = xml.getElementsByTagName("gml:coordinates")
584 | if coords and coords[0].childNodes[0].nodeValue:
585 | lon, lat = coords[0].childNodes[0].nodeValue.split(",")
586 | return db.GeoPt(lat, lon)
587 |
588 | def gmaps_img(points):
589 | markers = "&".join("markers=%s,%s" % (p.lat, p.lon) for p in points)
590 | return GMAPS_URL + markers
591 |
592 |
593 |
594 |
--------------------------------------------------------------------------------