Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Place features are in.

  • Loading branch information...
commit baeb65e396ea2d7b9ff0f93bf06c58b99a4097c0 1 parent ae4be58
@livid authored
View
10 app.yaml.example
@@ -104,6 +104,12 @@ handlers:
- url: /images(/.*)?
script: images.py
+
+- url: /place/([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})
+ script: place.py
+
+- url: /remove/place_message/(.*)
+ script: place.py
- url: /_ah/mail/.+
script: mail.py
@@ -113,6 +119,10 @@ handlers:
script: xmpp.py
login: admin
+- url: /remote_api
+ script: $PYTHON_LIB/google/appengine/ext/remote_api/handler.py
+ login: admin
+
- url: .*
script: main.py
View
25 appengine_console.py
@@ -0,0 +1,25 @@
+#!/usr/bin/python
+import code
+import getpass
+import sys
+
+sys.path.append("/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine")
+sys.path.append("/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/lib/yaml/lib")
+
+from google.appengine.ext.remote_api import remote_api_stub
+from google.appengine.ext import db
+
+def auth_func():
+ return raw_input('Username:'), getpass.getpass('Password:')
+
+if len(sys.argv) < 2:
+ print "Usage: %s app_id [host]" % (sys.argv[0],)
+app_id = sys.argv[1]
+if len(sys.argv) > 2:
+ host = sys.argv[2]
+else:
+ host = '%s.appspot.com' % app_id
+
+remote_api_stub.ConfigureRemoteDatastore(app_id, '/remote_api', auth_func, host)
+
+code.interact('App Engine interactive console for %s' % (app_id,), None, locals())
View
6 index.yaml
@@ -59,6 +59,12 @@ indexes:
- name: last_modified
direction: desc
+- kind: PlaceMessage
+ properties:
+ - name: place
+ - name: created
+ direction: desc
+
- kind: Reply
properties:
- name: __key__
View
4 main.py
@@ -48,6 +48,10 @@ def head(self):
pass
def get(self):
+ host = self.request.headers['Host']
+ if host == 'beta.v2ex.com':
+ self.redirect('http://v2ex.appspot.com/')
+ return
browser = detect(self.request)
self.session = Session()
template_values = {}
View
148 place.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python
+# coding=utf-8
+
+import os
+import re
+import time
+import datetime
+import hashlib
+import string
+import random
+
+from google.appengine.ext import webapp
+from google.appengine.api import memcache
+from google.appengine.ext import db
+from google.appengine.ext.webapp import util
+from google.appengine.ext.webapp import template
+
+from v2ex.babel import Member
+from v2ex.babel import Counter
+from v2ex.babel import Section
+from v2ex.babel import Node
+from v2ex.babel import Topic
+from v2ex.babel import Reply
+from v2ex.babel import Note
+from v2ex.babel import Place
+from v2ex.babel import PlaceMessage
+
+from v2ex.babel import SYSTEM_VERSION
+
+from v2ex.babel.security import *
+from v2ex.babel.ua import *
+from v2ex.babel.da import *
+from v2ex.babel.ext.cookies import Cookies
+
+template.register_template_library('v2ex.templatetags.filters')
+
+class PlaceHandler(webapp.RequestHandler):
+ def get(self, ip):
+ template_values = {}
+ template_values['rnd'] = random.randrange(1, 100)
+ member = CheckAuth(self)
+ if member:
+ template_values['member'] = member
+ template_values['ip'] = ip
+ substance = GetPlaceByIP(ip)
+ if substance:
+ template_values['substance'] = substance
+ template_values['messages'] = db.GqlQuery("SELECT * FROM PlaceMessage WHERE place = :1 ORDER BY created DESC LIMIT 30", substance)
+ else:
+ if member:
+ if member.ip == ip:
+ substance = CreatePlaceByIP(ip)
+ template_values['substance'] = substance
+ can_post = False
+ can_see = True
+ if member:
+ if member.ip == ip:
+ can_post = True
+ can_see = True
+ else:
+ can_see = False
+ else:
+ if 'X-Real-IP' in handler.request.headers:
+ ip_guest = handler.request.headers['X-Real-IP']
+ else:
+ ip_guest = handler.request.remote_addr
+ if ip_guest == ip:
+ can_see = True
+ else:
+ can_see = False
+ template_values['can_post'] = can_post
+ template_values['can_see'] = can_see
+ if member:
+ template_values['ip_guest'] = member.ip
+ else:
+ template_values['ip_guest'] = ip_guest
+ template_values['page_title'] = u'V2EX › ' + ip
+ path = os.path.join(os.path.dirname(__file__), 'tpl', 'desktop', 'place.html')
+ output = template.render(path, template_values)
+ self.response.out.write(output)
+
+ def post(self, ip):
+ if 'Referer' in self.request.headers:
+ go = self.request.headers['Referer']
+ else:
+ go = '/place'
+ member = CheckAuth(self)
+ place = GetPlaceByIP(ip)
+ say = self.request.get('say').strip()
+ if len(say) > 0 and len(say) < 280 and member and place:
+ if member.ip == ip:
+ message = PlaceMessage()
+ q = db.GqlQuery('SELECT * FROM Counter WHERE name = :1', 'place_message.max')
+ if (q.count() == 1):
+ counter = q[0]
+ counter.value = counter.value + 1
+ else:
+ counter = Counter()
+ counter.name = 'place_message.max'
+ counter.value = 1
+ q2 = db.GqlQuery('SELECT * FROM Counter WHERE name = :1', 'place_message.total')
+ if (q2.count() == 1):
+ counter2 = q2[0]
+ counter2.value = counter2.value + 1
+ else:
+ counter2 = Counter()
+ counter2.name = 'place_message.total'
+ counter2.value = 1
+ message.num = counter.value
+ message.place = place
+ message.place_num = place.num
+ message.member = member
+ message.content = say
+ message.put()
+ counter.put()
+ counter2.put()
+ self.redirect(go)
+
+class PlaceMessageRemoveHandler(webapp.RequestHandler):
+ def get(self, key):
+ if 'Referer' in self.request.headers:
+ go = self.request.headers['Referer']
+ else:
+ go = '/place'
+ member = CheckAuth(self)
+ if member:
+ message = db.get(db.Key(key))
+ if message:
+ if message.member.num == member.num:
+ message.delete()
+ q = db.GqlQuery('SELECT * FROM Counter WHERE name = :1', 'place_message.total')
+ if (q.count() == 1):
+ counter = q[0]
+ counter.value = counter.value - 1
+ counter.put()
+ self.redirect(go)
+
+def main():
+ application = webapp.WSGIApplication([
+ ('/place/([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})', PlaceHandler),
+ ('/remove/place_message/(.*)', PlaceMessageRemoveHandler)
+ ],
+ debug=True)
+ util.run_wsgi_app(application)
+
+
+if __name__ == '__main__':
+ main()
View
39 static/css/desktop/style.css
@@ -631,4 +631,41 @@ table.grid td.left {
-moz-border-radius: 4px 4px 0px 0px; -webkit-border-radius: 4px;
}
-.cell .gist { max-width: 576px; }
+.cell .gist { max-width: 576px; }
+
+.place_title {
+ font-family: "DINPro";
+ font-size: 32px;
+ line-height: 32px;
+ padding: 0px;
+ margin: 0px;
+}
+
+.place_visitors {
+ font-family: "DINPro";
+ font-size: 14px;
+ line-height: 14px;
+ color: #999;
+}
+
+.place_say {
+ font-family: "DINPro";
+ font-size: 14px;
+ line-height: 14px;
+ color: #999;
+}
+
+a.tiny_label:link, a.tiny_label:visited, a.tiny_label:active {
+ font-size: 10px;
+ line-height: 10px;
+ color: #fff;
+ background-color: #dde;
+ -moz-border-radius: 3px;
+ -webkit-border-radius: 3px;
+ padding: 2px 5px 2px 5px;
+}
+
+a.tiny_label:hover {
+ text-decoration: none;
+ background-color: #99a;
+}
View
BIN  static/img/info_128.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  static/img/say.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
24 tpl/desktop/common/say.html
@@ -0,0 +1,24 @@
+<script type="text/javascript">
+ var updateCount = function() {
+ s = document.getElementById("say");
+ r = 280 - s.value.length;
+ tc = document.getElementById("tc");
+ tc.innerHTML = r;
+ }
+</script>
+<form method="post" action="/place/{{ ip }}">
+<textarea class="mll short" name="say" maxlength="280" id="say" onkeyup="updateCount();"></textarea>
+<div class="sep10"></div>
+<div class="fr"><input type="submit" value="Say" class="super normal button" /></div>
+<div class="sep10"></div>
+<div class="sep10"></div>
+<div class="sep5"></div>
+</form>
+<script type="text/javascript">
+ var focusStatus = function() {
+ s = document.getElementById("say");
+ s.focus();
+ }
+
+ setTimeout("focusStatus()", 200);
+</script>
View
1  tpl/desktop/common/top.html
@@ -12,6 +12,7 @@
{% endifequal %}
<!--<li><a href="/mentions" class="white">提到我的</a></li>-->
<li><a href="/notes" class="white">记事本</a></li>
+ <li><a href="/place/{{ member.ip }}" class="white">附近</a></li>
<li><a href="/settings" class="white">设置</a></li>
{% ifequal member.num 1 %}
<li><a href="/backstage" class="white">后台</a></li>
View
43 tpl/desktop/images_home.html
@@ -0,0 +1,43 @@
+{% include 'common/head.html' %}
+<body>
+ {% include 'common/top.html' %}
+ <div id="Wrapper">
+ <div id="Main">
+ <div id="Sidebar">
+ </div>
+ <div id="Rightbar">
+ {% include 'rightbar/user.html' %}
+ </div>
+ <div id="Content">
+ <div class="box">
+ <div class="cell">
+ <span class="fade"><a href="/">V2EX</a> <span class="chevron">&nbsp;&nbsp;</span> 图片上传</span>
+ </div>
+ <div class="inner">
+ </div>
+ <div class="grid">
+ <table cellpadding="5" cellspacing="0" border="1" width="100%" class="grid">
+ <tr>
+ <td width="25%" class="left"></td>
+ <td width="25%"></td>
+ <td width="25%"></td>
+ <td width="25%"></td>
+ </tr>
+ <tr>
+ <td width="25%" class="left"></td>
+ <td width="25%"></td>
+ <td width="25%"></td>
+ <td width="25%"></td>
+ </tr>
+ </table>
+ </div>
+ <div class="inner">
+ </div>
+ </div>
+ </div>
+ <div class="c"></div>
+ </div>
+ </div>
+ {% include 'common/bottom.html' %}
+</body>
+</html>
View
2  tpl/desktop/index.html
@@ -17,6 +17,8 @@
{% include 'rightbar/recent_nodes.html' %}
<div class="sep20"></div>
{% include 'rightbar/ads.html' %}
+ <div class="sep20"></div>
+ {% include 'rightbar/goodies.html' %}
</div>
<div id="Content">
<div class="box">
View
66 tpl/desktop/place.html
@@ -0,0 +1,66 @@
+{% include 'common/head.html' %}
+<body>
+ {% include 'common/top.html' %}
+ <div id="Wrapper">
+ <div id="Main">
+ <div id="Sidebar">
+ </div>
+ <div id="Rightbar">
+ {% include 'rightbar/user.html' %}
+ <div class="sep20"></div>
+ {% include 'rightbar/ads.html' %}
+ </div>
+ <div id="Content">
+ <div class="box">
+ <div class="cell"><div class="fr"><!-- TODO: Check-in --></div>
+ <span class="bigger fade"><a href="/">V2EX</a> <span class="chevron">&nbsp;&nbsp;</span> {{ ip }}</span>
+ </div>
+ {% if substance %}
+ <div class="cell">
+ <h1 class="place_title">{{ substance.ip }}</h1>
+ <!--<div class="sep10"></div>
+ <span class="place_visitors">{{ substance.visitors }} visitors so far</span>-->
+ </div>
+ {% if can_post %}
+ <div class="cell"><div class="fr"><strong><span class="snow bigger" id="tc">280</span></strong></div>
+ <span class="place_say"><img src="/static/img/say.png" align="absmiddle" /> &nbsp; Say something about this place?</span>
+ <div class="sep10"></div>
+ {% include 'common/say.html' %}
+ </div>
+ {% endif %}
+ {% if can_see %}
+ {% for message in messages %}
+ <div class="cell">
+ <table cellpadding="0" cellspacing="0" border="0" width="100%"><tr>
+ <td valign="top" class="avatar"><a href="/member/{{ message.member.username }}">{{ message.member|avatar:"normal" }}</a></td>
+ <td style="padding-left: 12px" valign="top"><div class="fr"><span class="created">{{ message.created|timesince }} ago{% ifequal message.member.num member.num %} &nbsp; <a href="#;" onclick="if (confirm('确认删除?')) { location.href = '/remove/place_message/{{ message.key }}'; }" class="tiny_label">DELETE</a>{% endifequal %}</span></div><span class="fade" style="font-size: 10px;"><strong><a href="/member/{{ message.member.username|escape }}" class="dark">{{ message.member.username|escape }}</a></strong></span>
+ <div class="sep5"></div>
+ <span class="bigger" style="font-size: 16px">{{ message.content|escape|urlize }}</span>
+ </td>
+ </tr>
+ </table>
+ </div>
+ {% endfor %}
+ {% else %}
+ <div class="cell">
+ <div align="center"><div class="sep20"></div>
+ <img src="/static/img/info_128.png" />
+ <div class="sep5"></div>
+ <span class="bigger snow">只有确实位于此位置才能看到这里的消息</span>
+ <div class="sep5"></div>
+ <small class="fade">Your Current IP › <strong>{{ ip_guest }}</strong></small>
+ <div class="sep20"></div>
+ </div>
+ </div>
+ {% endif %}
+ {% else %}
+ {% endif %}
+
+ </div>
+ </div>
+ <div class="c"></div>
+ </div>
+ </div>
+ {% include 'common/bottom.html' %}
+</body>
+</html>
View
5 tpl/desktop/rightbar/goodies.html
@@ -0,0 +1,5 @@
+<div class="box">
+ <div class="inner">
+ <small class="fade">Worth Trying <span class="chevron">&nbsp;&nbsp;</span> <a href="http://www.btodo.com/" target="_blank">bTodo</a> &nbsp; 简洁易用的在线任务管理工具</small>
+ </div>
+</div>
View
2  tpl/desktop/topic.html
@@ -36,7 +36,7 @@
<tr>
<td width="48" valign="top"><a href="/member/{{ reply.created_by }}">{{ reply.member|avatar:"normal" }}</a></td>
<td width="10"></td>
- <td width="auto" valign="top"><div class="fr" id="reply_{{ reply.num }}_buttons"><strong><small class="snow">#{{ forloop.counter }} - {{ reply.created|timesince }} ago {% ifequal member.num 1 %}&nbsp;<a href="/backstage/tidy/reply/{{ reply.num }}">TIDY</a> &nbsp; <a href="/edit/reply/{{ reply.num }}">EDIT</a>{% endifequal %}</small></strong></div>
+ <td width="auto" valign="top"><div class="fr" id="reply_{{ reply.num }}_buttons"><strong><small class="snow">#{{ forloop.counter }} - {{ reply.created|timesince }} ago {% ifequal member.num 1 %}&nbsp;<a href="/backstage/tidy/reply/{{ reply.num }}">TIDY</a> &nbsp; <a href="/edit/reply/{{ reply.num }}">EDIT</a> &nbsp; <a href="#;" onclick="if (confirm('确认删除此回复?')) { location.href = '/backstage/remove/reply/{{ reply.num }}'; }">DEL</a>{% endifequal %}</small></strong></div>
<div class="sep3"></div>
<strong><a href="/member/{{ reply.created_by }}" class="dark">{{ reply.created_by }}</a></strong>{% if reply.source %}<span class="snow">&nbsp; via {{ reply.source }}</span>{% endif %}
<div class="sep5"></div>
View
29 v2ex/babel/__init__.py
@@ -1,4 +1,4 @@
-SYSTEM_VERSION = '2.3.1'
+SYSTEM_VERSION = '2.3.3'
import datetime
import hashlib
@@ -127,4 +127,29 @@ class PasswordResetToken(db.Model):
email = db.StringProperty(required=False, indexed=True)
member = db.ReferenceProperty(Member)
valid = db.IntegerProperty(required=False, indexed=True, default=1)
- timestamp = db.IntegerProperty(required=False, indexed=True, default=0)
+ timestamp = db.IntegerProperty(required=False, indexed=True, default=0)
+
+class Place(db.Model):
+ num = db.IntegerProperty(required=False, indexed=True)
+ ip = db.StringProperty(required=False, indexed=True)
+ name = db.StringProperty(required=False, indexed=False)
+ visitors = db.IntegerProperty(required=False, default=0, indexed=True)
+ longitude = db.FloatProperty(required=False, default=0.0, indexed=True)
+ latitude = db.FloatProperty(required=False, default=0.0, indexed=True)
+ created = db.DateTimeProperty(auto_now_add=True)
+ last_modified = db.DateTimeProperty(auto_now=True)
+
+class PlaceMessage(db.Model):
+ num = db.IntegerProperty(indexed=True)
+ place = db.ReferenceProperty(Place)
+ place_num = db.IntegerProperty(indexed=True)
+ member = db.ReferenceProperty(Member)
+ content = db.TextProperty(required=False)
+ in_reply_to = db.SelfReferenceProperty()
+ source = db.StringProperty(required=False, indexed=True)
+ created = db.DateTimeProperty(auto_now_add=True)
+
+class Checkin(db.Model):
+ place = db.ReferenceProperty(Place)
+ member = db.ReferenceProperty(Member)
+ last_checked_in = db.DateTimeProperty(auto_now=True)
View
48 v2ex/babel/da/__init__.py
@@ -1,6 +1,7 @@
# coding=utf-8
import hashlib
+import logging
from google.appengine.ext import db
from google.appengine.api import memcache
@@ -11,6 +12,7 @@
from v2ex.babel import Node
from v2ex.babel import Topic
from v2ex.babel import Reply
+from v2ex.babel import Place
def GetKindByNum(kind, num):
K = str(kind.capitalize())
@@ -65,4 +67,48 @@ def GetMemberByEmail(email):
memcache.set(cache, one, 86400)
return one
else:
- return False
+ return False
+
+def ip2long(ip):
+ ip_array = ip.split('.')
+ ip_long = int(ip_array[0]) * 16777216 + int(ip_array[1]) * 65536 + int(ip_array[2]) * 256 + int(ip_array[3])
+ return ip_long
+
+def GetPlaceByIP(ip):
+ cache = 'Place_' + ip
+ place = memcache.get(cache)
+ if place:
+ return place
+ else:
+ q = db.GqlQuery("SELECT * FROM Place WHERE ip = :1", ip)
+ if q.count() == 1:
+ place = q[0]
+ memcache.set(cache, place, 86400)
+ return place
+ else:
+ return False
+
+def CreatePlaceByIP(ip):
+ place = Place()
+ q = db.GqlQuery('SELECT * FROM Counter WHERE name = :1', 'place.max')
+ if (q.count() == 1):
+ counter = q[0]
+ counter.value = counter.value + 1
+ else:
+ counter = Counter()
+ counter.name = 'place.max'
+ counter.value = 1
+ q2 = db.GqlQuery('SELECT * FROM Counter WHERE name = :1', 'place.total')
+ if (q2.count() == 1):
+ counter2 = q2[0]
+ counter2.value = counter2.value + 1
+ else:
+ counter2 = Counter()
+ counter2.name = 'place.total'
+ counter2.value = 1
+ place.num = ip2long(ip)
+ place.ip = ip
+ place.put()
+ counter.put()
+ counter2.put()
+ return place
View
68 v2ex/babel/security/__init__.py
@@ -1,43 +1,55 @@
# coding=utf-8
+import os
import hashlib
+import logging
from google.appengine.ext import db
from google.appengine.api import memcache
from v2ex.babel.ext.cookies import Cookies
-def CheckAuth(request):
- cookies = Cookies(request, max_age = 86400 * 365, path = '/')
- if 'auth' in cookies:
- auth = cookies['auth']
- member_num = memcache.get(auth)
- if (member_num > 0):
- member = memcache.get('member_' + str(member_num))
- if member is None:
- q = db.GqlQuery("SELECT * FROM Member WHERE num = :1", member_num)
- if q.count() == 1:
- member = q[0]
- memcache.set('member_' + str(member_num), member, 86400 * 365)
- else:
- member = False
- return member
- else:
- q = db.GqlQuery("SELECT * FROM Member WHERE auth = :1", auth)
- if (q.count() == 1):
- member_num = q[0].num
- member = q[0]
- memcache.set(auth, member_num, 86400 * 365)
- memcache.set('member_' + str(member_num), member, 86400 * 365)
- return member
- else:
- return False
- else:
- return False
+def CheckAuth(handler):
+ ip = GetIP(handler)
+ cookies = Cookies(handler, max_age = 86400 * 365, path = '/')
+ if 'auth' in cookies:
+ auth = cookies['auth']
+ member_num = memcache.get(auth)
+ if (member_num > 0):
+ member = memcache.get('member_' + str(member_num))
+ if member is None:
+ q = db.GqlQuery("SELECT * FROM Member WHERE num = :1", member_num)
+ if q.count() == 1:
+ member = q[0]
+ memcache.set('member_' + str(member_num), member, 86400 * 365)
+ else:
+ member = False
+ if member:
+ member.ip = ip
+ return member
+ else:
+ q = db.GqlQuery("SELECT * FROM Member WHERE auth = :1", auth)
+ if (q.count() == 1):
+ member_num = q[0].num
+ member = q[0]
+ memcache.set(auth, member_num, 86400 * 365)
+ memcache.set('member_' + str(member_num), member, 86400 * 365)
+ member.ip = ip
+ return member
+ else:
+ return False
+ else:
+ return False
def DoAuth(request, destination, message = None):
if message != None:
request.session['message'] = message
else:
request.session['message'] = u'请首先登入或注册'
- return request.redirect('/signin?destination=' + destination)
+ return request.redirect('/signin?destination=' + destination)
+
+def GetIP(handler):
+ if 'X-Real-IP' in handler.request.headers:
+ return handler.request.headers['X-Real-IP']
+ else:
+ return handler.request.remote_addr
Please sign in to comment.
Something went wrong with that request. Please try again.