A plethora of fixes to make this more usable on non-mobile devices; should gracefully degrade a bit better now... if it can't get any GPS coordinates, the user can enter an address and bypass that requirement, and we'll do a reverse lookup and get some GPS coordinates based on said address. Users can also now view the checkin history of other people - someone wanna implement friendships/privacy? We have an open source 4Square here. ;D

This commit is contained in:
Ryan McGrath 2010-10-29 05:10:41 -04:00
parent c17458cc8a
commit d503e3fe08
11 changed files with 223 additions and 116 deletions

View file

@ -23,7 +23,7 @@ class Location(models.Model):
name = models.CharField(max_length=200)
street_address = models.CharField(max_length=200, blank=True)
category = models.ForeignKey(LocationCategory, blank=True, null=True)
geometry = models.PointField(srid=4326)
geometry = models.PointField(srid=4326, blank=True, null=True)
objects = models.GeoManager()
def __str__(self):
@ -33,7 +33,6 @@ class Location(models.Model):
"""
Tally up how many checkins this location had in the past day.
"""
print self.checkin_set.filter(timestamp__gte = datetime.now() + timedelta(days=-1))
return self.checkin_set.filter(timestamp__gte = datetime.now() + timedelta(days=-1)).count()
def address_for_geocode(self):

View file

@ -1,17 +1,26 @@
import urllib2
from urllib2 import HTTPError
try:
# Python 2.6 and up
import json as simplejson
except ImportError:
# This case gets rarer by the day, but if we need to, we can pull it from Django provided it's there.
from django.utils import simplejson
from datetime import datetime, timedelta
from django.contrib.gis.geos import *
from django.contrib.gis.measure import D
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render_to_response
from django.utils import simplejson
from django.contrib.auth.models import User
from django.template import RequestContext, Context
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator, InvalidPage, EmptyPage
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm
from drinkkit.redditors.models import LocationCategory, Location, Tip, Checkin
@ -153,16 +162,45 @@ def add_location(request):
fixed at some point. ;P
"""
if request.POST:
if request.POST['location_name'] and request.POST['lat'] and request.POST['long']:
# Somewhat ugly, but meh.
if request.POST['location_name'] and (request.POST['street_address'] or (request.POST['lat'] and request.POST['long'])):
new_location = Location()
new_location.name = request.POST['location_name']
if request.POST['lat'] and request.POST['long']:
new_location.geometry = 'POINT(%s %s)' %(request.POST['lat'], request.POST['long'])
else:
# If we don't have a lat/long pair, we know they got here by providing a street address, so
# let's do some geocoding via Google APIs and find the lat/long ourselves.
#
# Note: We assume Washington, DC here because of the region this application is suited to. If
# you wanted to expand on this, you'd probably wanna get the region/IP-(lat/long) (which isn't as accurate)
# and use it to sandbox your results. This is a pretty hacky 5AM thing. ;P
fixed_address = request.POST['street_address'].replace(" ", "+")
api_url = "http://maps.googleapis.com/maps/api/geocode/json?&address=%s,Washington,DC&sensor=false" % fixed_address
try:
# Download and parse the JSON response into native Python objects.
geocode_request = urllib2.Request(api_url)
geocoded_data = simplejson.load(urllib2.urlopen(geocode_request))
# Store latitude and longitude, for readability
for result in geocoded_data["results"]:
latitude = result["geometry"]["location"]["lat"]
longitude = result["geometry"]["location"]["lng"]
# Save our determined geometry coordinates
new_location.geometry = 'POINT(%s %s)' %(latitude, longitude)
except HTTPError:
# You're boned, Google's either blocking you or you done goofed. Ordinarily, you'd
# wanna handle errors properly here, but in my case I want a hard failure. YMMV.
pass
# If they've supplied a street address, sweet, use it.
if request.POST['street_address']:
new_location.street_address = request.POST['street_address']
# If they set a location, let's record it... (5AM code)
# If they set a location type/category, let's record it... (5AM code)
if request.POST['location_type']:
try:
category = LocationCategory.objects.get(id = request.POST['location_type'])
@ -174,7 +212,7 @@ def add_location(request):
return HttpResponseRedirect('/locations/%s/' % str(new_location.id))
else:
if not request.POST['lat'] or not request.POST['lat']:
errmsg = "We weren't able to get coordinates for where you are right now. Does your phone or device have GPS?"
errmsg = "We weren't able to get coordinates for where you are right now. Does your phone or device have GPS? If not, specify an address and we'll use that instead!"
if not request.POST['location_name']:
errmsg = "You didn't even bother to enter a name for this location. Wtf?"
return render_to_response('locations/add.html',
@ -235,8 +273,17 @@ def add_tip(request, location_id):
else:
return HttpResponse(status=404)
def find_redditors(request):
def view_redditor(request, redditor_name):
"""
Handles locating all Redditors in a given area who recently checked in.
View a Redditor "profile" - right now, all we do is show their checkin
history (where they've been). Stalking is fun, right?
...right?
"""
pass
try:
redditor = User.objects.get(username = redditor_name)
return render_to_response('redditors/view_profile.html',
context_instance = RequestContext(request, {'redditor': redditor})
)
except User.DoesNotExist:
return HttpResponse(status=404)

View file

@ -13,11 +13,11 @@
body {
width: 100%;
color: #555;
font-family:Helvetica, "Helvetica Neue", Arial, sans-serif;
font-family: Helvetica, "Helvetica Neue", Arial, sans-serif;
height: 100%;
padding: 0;
margin: 0;
background:#c5ccd3;
background: #c5ccd3;
-webkit-font-smoothing: antialiased;
}
@ -27,10 +27,27 @@
margin: 0;
}
.rounded_6px {
-moz-border-radius: 6px;
-webkit-border-radius: 6px;
-khtml-border-radius: 6px;
border-radius: 6px;
}
.rounded_4px {
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
-khtml-border-radius: 4px;
border-radius: 4px;
}
#sandbox { clear: both; }
#title {
width: 100%;
position: relative;
background: #cee3f8;
background:-moz-linear-gradient(-90deg, #CEE3F8, #A8C4E0) repeat scroll 0 0 transparent;
background: -webkit-gradient(linear, left top, left bottom, from(#cee3f8), to(#a8c4e0));
padding: 7px 0 6px 0;
margin: 0;
@ -45,6 +62,7 @@
width: 100%;
position: relative;
background: #cdcdcd;
background:-moz-linear-gradient(-90deg, #fafafa, #cdcdcd) repeat scroll 0 0 transparent;
background: -webkit-gradient(linear, left top, left bottom, from(#fafafa), to(#cdcdcd));
padding: 4px 0 6px;
font-size: 1.2em;
@ -58,20 +76,27 @@
background-color: #fff;
-webkit-border-radius: 4px;
border: 1px solid #8f8f8f;
padding-bottom: 5px;
}
.button {
background-color: #80A2C4;
border: 1px solid #517191;
background:-moz-linear-gradient(-90deg, #bfd0e0, #80a2c4) repeat scroll 0 0 transparent;
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#bfd0e0), to(#80a2c4));
-moz-border-radius: 6px;
-webkit-border-radius: 6px;
text-decoration: none;
color: #fff;
font-size: 11px;
padding: 3px 7px;
}
.orange_button {
border: 1px solid #ff4500;
background-color: #ff4500;
background:-moz-linear-gradient(-90deg, #ff9f7b, #ff4500) repeat scroll 0 0 transparent;
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#ff9f7b), to(#ff4500));
}
.large {
font-size: 14px;
padding: 4px 10px;
@ -86,9 +111,8 @@
#home_link, #nearby_link, #search_link, #find_link, #add_location {
background-color: #80A2C4;
border: 1px solid #517191;
background:-moz-linear-gradient(-90deg, #bfd0e0, #80a2c4) repeat scroll 0 0 transparent;
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#bfd0e0), to(#80a2c4));
-moz-border-radius: 6px;
-webkit-border-radius: 6px;
display: inline-block;
text-decoration: none;
color: #fff;
@ -102,11 +126,11 @@
h2 {
font-size: 18px;
/*color: #ff7041;*/
color: #6c7ca1;
margin: 2px;
padding: 8px;
background-color: #ebf3fc;
background:-moz-linear-gradient(-90deg, #fafcfe, #ebf3fc) repeat scroll 0 0 transparent;
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#fafcfe), to(#ebf3fc));
border: 1px solid #c4dbf1;
}
@ -134,46 +158,48 @@
#button_center { text-align: center; }
/* Oh god I'm so tired... */
.go_location {
width: 70px !important;
position: absolute;
top: 0px;
right: 10px;
color: #fff !important;
font-size: 14px !important;
font-weight: bold;
padding: 5px 12px !important;
text-align: center !important;
border: 1px solid #517191 !important;
-webkit-border-bottom-left-radius: 6px !important;
-webkit-border-bottom-right-radius: 6px !important;
}
.top_level {
margin-bottom: 10px;
position: relative;
}
#results li a, #results li a:visited {
border: 1px solid #c9c9c9;
background-color: #f9f9f9;
.top_level {
border: 1px solid #c4dbf1;
-webkit-border-radius: 2px;
padding: 10px;
display: block;
color: #555;
text-decoration: none;
padding-bottom: 3px;
margin-bottom: 5px;
width: 92%;
}
-moz-border-radius: 2px;
border-radius: 2px;
float: left;
width: 99%;
padding-bottom: 5px;
margin-bottom: 10px;
}
.location_name { font-weight: bold; color: #ff8070; }
.top_level ul { padding: 5px 0 0 0; }
.top_level li { margin-bottom: 5px;}
.top_level h3 {
background-color: #ebf3fc;
background:-moz-linear-gradient(-90deg, #fafcfe, #ebf3fc) repeat scroll 0 0 transparent;
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#fafcfe), to(#ebf3fc));
border-bottom: 1px solid #c4dbf1;
margin: 0;
padding: 5px;
font-size: 13px;
color: #6C7CA1;
}
.top_level ul { padding: 5px; font-size: 12px; }
.top_level ul li { margin-bottom: 3px !important; }
.checkin_list_timestamp { color: #555; font-weight: normal; float: right; }
#results li a, #results li a:visited { text-decoration: none; }
.anchored_action {
float: right;
clear: right;
margin: 5px 5px 0 0;
width: auto !important;
color: #fff !important;
font-size: 12px !important;
font-weight: bold;
padding: 3px 9px !important;
text-align: center !important;
-moz-border-radius: 6px;
-webkit-border-radius: 6px;
border-radius: 6px;
min-width: 100px;
}
</style>
<script type="text/javascript">
@ -220,13 +246,16 @@
if(typeof _GPS !== "undefined") _GPS.getLocation();
bareddit.about = document.getElementById("about");
bareddit.about_button = document.getElementById("about_button");
/* Use the annoying event handler syntax in the hopes of hitting all odd mobile browsers.
touchstart events are faster in the browsers that support them, so fork for it */
if(bareddit.about_button) {
if(isEventSupported('touchstart')) {
document.getElementById("about_button").ontouchstart = bareddit.toggleAbout;
bareddit.about_button.ontouchstart = bareddit.toggleAbout;
} else {
document.getElementById("about_button").onclick = bareddit.toggleAbout;
bareddit.about_button.onclick = bareddit.toggleAbout;
}
}
}
</script>
@ -234,11 +263,11 @@
<body onload="whenReady();">
<div id="title">
{% if not user.is_authenticated %}
<a href="/auth/" id="login_button" class="button">Login</a>
<a href="/register/" id="register_button" class="button">Register</a>
<a href="/auth/" id="login_button" class="button rounded_6px">Login</a>
<a href="/register/" id="register_button" class="button rounded_6px">Register</a>
{% else %}
<a href="#" id="about_button" class="button">About</a>
<a href="/unauth/" id="login_button" class="button">Logout</a>
<a href="#" id="about_button" class="button rounded_6px">About</a>
<a href="/unauth/" id="login_button" class="button rounded_6px">Logout</a>
{% endif %}
drinkkit
</div>
@ -250,11 +279,11 @@
- Ryan, 3:45AM
-->
<a href="/" id="home_link" class="button">Home</a>
<a href="/locations/nearby/" id="nearby_link" class="button">Nearby</a>
<a href="/locations/search/" id="search_link" class="button">Search</a>
<a href="/" id="home_link" class="button rounded_6px">Home</a>
<a href="/locations/nearby/" id="nearby_link" class="button rounded_6px">Nearby</a>
<a href="/locations/search/" id="search_link" class="button rounded_6px">Search</a>
{% if user.is_authenticated %}
<a href="/locations/add/" id="add_location" class="button">Add Location</a>
<a href="/locations/add/" id="add_location" class="button rounded_6px">Add Location</a>
{% endif %}
</div>
@ -287,5 +316,7 @@
<div id="junk_in_trunk">
{% block content %}{% endblock %}
<div>
<div id="sandbox"></div>
</body>
</html>

View file

@ -88,7 +88,7 @@ _GPS.getLocation = function() {
<input type="hidden" name="long" id="long" value="">
<div id="button_center">
<input type="submit" value="Add This Location" id="add_location_submit" class="button large">
<input type="submit" value="Add This Location" id="add_location_submit" class="button large rounded_6px">
</div>
</form>

View file

@ -26,7 +26,7 @@
</p>
<div id="button_center" style="margin-bottom: 14px;">
<input type="submit" value="Check In Here!" class="button large">
<input type="submit" value="Check In Here!" class="button large rounded_6px">
</div>
</form>
{% endblock %}

View file

@ -89,7 +89,7 @@ _GPS.getLocation = function() {
<input type="hidden" name="long" id="long" value="">
<div id="button_center">
<input type="submit" value="Add This Location" id="add_location_submit" class="button large">
<input type="submit" value="Add This Location" id="add_location_submit" class="button large rounded_6px">
</div>
<div id="bert" style="margin-bottom: 10px;">

View file

@ -8,7 +8,7 @@
<h2>Where all the DC Redditors at?</h2>
<p>
Check out where other DC Redditors are drinking! If you'd like to join in, register or log in using the buttons
at the top right of the screen.
at the top of the screen.
</p>
<p>
@ -21,13 +21,14 @@
<ul id="results">
{% for checkin in checkins.object_list %}
<li class="top_level">
<a href="/locations/{{ checkin.location.id }}/" class="go_location button">View</a>
<a href="/locations/{{ checkin.location.id }}/">
<span class="location_name">{{ checkin.location.name }}</span>
<h3>{{ checkin.user.username }} <span class="checkin_list_timestamp">{{ checkin.timestamp|timesince }} ago</span></h3>
<a href="/locations/{{ checkin.location.id }}/" class="anchored_action button orange_button">View Location</a>
<a href="/redditor/{{ checkin.user.username }}/" class="anchored_action button">View User</a>
<ul>
<li><strong>{{ checkin.user.username }}</strong> checked in here {{ checkin.timestamp|timesince }} ago</li>
<li><strong>Checked in at:</strong> {{ checkin.location.name }}</li>
<li><strong>Identify by:</strong> {{ checkin.identify_by }}</li>
<li><strong>Should be there for:</strong> {{ checkin.estimated_time_here }}</li>
</ul>
</a>
</li>
{% endfor %}
</ul>

View file

@ -21,7 +21,7 @@
</p>
<div id="button_center">
<input type="submit" value="Find Locations" id="search_btn" class="button large">
<input type="submit" value="Find Locations" id="search_btn" class="button large rounded_6px">
</div>
</form>
@ -32,16 +32,15 @@
<ul id="results">
{% for location in results.object_list %}
<li class="top_level">
<a href="/locations/{{ checkin.location.id }}/" class="go_location button">View</a>
<a href="/locations/{{ location.id }}/">
<span class="location_name">{{ location.name }}</span>
<h3>{{ location.name }}</h3>
<a href="/locations/{{ location.id }}/" class="anchored_action button orange_button">View Location</a>
<ul>
<li><strong>Recent Checkins:</strong> {{ location.get_recent_checkins_count }}</li>
<li><strong>Address:</strong> {{ location.street_address|default:"No address set" }}</li>
<li><strong>Category:</strong> {{ location.category|default:"No category set" }}</li>
</ul>
</a>
</li>
{% endfor %}
</ul>

View file

@ -43,29 +43,29 @@
#checkin_here_link, #checked_in_already {
position: absolute;
top: 10px;
right: 10px;
top: 5px;
right: 8px;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
text-decoration: none;
color: #fff;
font-weight: bold;
padding: 3px 7px !important;
font-size: 12px;
}
#checked_in_already { padding: 3px !important; font-size: 11px !important; top: 5px; /* I'm tired, hush */ }
#checkin_here_link {
background-color: #ff4500;
border: 1px solid #ff4500;
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#ff9f7b), to(#ff4500));
font-size: 20px;
padding: 7px 12px;
}
#checked_in_already {
background-color: #cdcdcd;
border: 1px solid #8f8f8f;
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#f2f2f2), to(#cdcdcd));
font-size: 14px;
padding: 7px 10px;
color: #888;
}
@ -160,10 +160,13 @@
{% if recent_checkins %}
<ul id="checkins">
{% for checkin in recent_checkins %}
<li>
<span class="comment_header"><strong>{{ checkin.user.username }}</strong> <em>{{ checkin.timestamp|timesince }} ago</em></span>
<span class="pointers">Can be identified by:</span> {{ checkin.identify_by|default:"Doesn't want to be found. :(" }}<br>
<span class="pointers">Estimated time spent here:</span> {{ checkin.estimated_time_here|default:"They're flying by the seat of their vintage pants (no idea)."}}
<li class="top_level">
<h3>{{ checkin.user.username }} <span class="checkin_list_timestamp">{{ checkin.timestamp|timesince }} ago</span></h3>
<a href="/redditor/{{ checkin.user.username }}/" class="anchored_action button">View User</a>
<ul>
<li><strong>Identify by:</strong> {{ checkin.identify_by }}</li>
<li><strong>Should be there for:</strong> {{ checkin.estimated_time_here }}</li>
</ul>
</li>
{% endfor %}
</ul>
@ -171,13 +174,15 @@
<p class="none_yet">No checkins yet. You could be the first!</p>
{% endif %}
<div style="clear: both;"><!-- It's 5AM and I have better things to do with my time... like sleep. --></div>
{% if user.is_authenticated %}
<h2>Leave a Tip or Todo</h2>
<form action="/locations/{{ location.id }}/add_tip/" method="post" id="bert">
{% csrf_token %}
<textarea name="tip_body" id="tip_body"></textarea>
<input type="submit" value="Post Tip" id="submit_tip" class="button large">
<input type="submit" value="Post Tip" id="submit_tip" class="button large rounded_6px">
</form>
{% endif %}

View file

@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block extra_head %}
{% endblock %}
{% block content %}
<h2>Where has {{ redditor.username }} been?</h2>
<ul id="results">
{% for checkin in redditor.checkin_set.all %}
<li class="top_level">
<h3>{{ checkin.location.name }} <span class="checkin_list_timestamp">{{ checkin.timestamp|timesince }} ago</span></h3>
<a href="/locations/{{ checkin.location.id }}/" class="anchored_action button orange_button">View Location</a>
<ul>
<li><strong>Was identifiable by:</strong> {{ checkin.identify_by }}</li>
<li><strong>Was there for:</strong> {{ checkin.estimated_time_here }}</li>
</ul>
</li>
{% endfor %}
</ul>
{% endblock %}

View file

@ -5,7 +5,7 @@ from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
(r'^admin/', include(admin.site.urls)),
(r'^knux/', include(admin.site.urls)),
# Viewing and adding tips/locations
(r'^locations/add/$', 'drinkkit.redditors.views.add_location'),
@ -16,6 +16,9 @@ urlpatterns = patterns('',
(r'^locations/(?P<location_id>[a-zA-Z0-9_.-]+)/checkin/$', 'drinkkit.redditors.views.checkin_location'),
(r'^locations/(?P<location_id>[a-zA-Z0-9_.-]+)/$', 'drinkkit.redditors.views.view_location'),
# Query and see who's getting into what
(r'^redditor/(?P<redditor_name>[a-zA-Z0-9_.-]+)/$', 'drinkkit.redditors.views.view_redditor'),
# Registration
(r'^register/$', 'drinkkit.redditors.views.register'),