Skip to content

Commit

Permalink
user-localized timezone support for datetime fields, including depend…
Browse files Browse the repository at this point in the history
…ent libraries and a user-profile
  • Loading branch information
jsled committed Sep 30, 2009
1 parent d40e98f commit e754b40
Show file tree
Hide file tree
Showing 13 changed files with 253 additions and 71 deletions.
14 changes: 9 additions & 5 deletions TODO
Expand Up @@ -132,10 +132,6 @@ What is the essence of "future" steps? They haven't happened, yet.

* Todo
** bugs, misc
*** TOOD lack of timezone handling
http://code.google.com/p/django-timezones/
http://www.djangosnippets.org/snippets/183/

*** TODO cascading /recipe/{id}/hops/hops/hops/… in the case of ingredient add failure
*** TODO no way to add a fermentable (e.g., honey) added at flame-out.
*** TODO unable to add "5 g" of apple must for cider recipes (no volume adds for fermentables)
Expand All @@ -155,6 +151,14 @@ http://www.djangosnippets.org/snippets/183/
*** TODO user profile should require existing password for password change; use django form validation/cleaning routines
*** TODO to add: Fruit/"Other"
(08:42:34 PM) djensen: I think you need 'Other' for fruit
*** DONE lack of timezone handling
CLOSED: [2009-09-29 Tue 20:23]
http://code.google.com/p/django-timezones/
http://www.djangosnippets.org/snippets/183/
http://pypi.python.org/pypi/pytz/

for egg in lib/*.egg; do export PYTHONPATH=$(pwd)/${egg}:$PYTHONPATH; done;

*** DONE to add: Citra hops
CLOSED: [2009-09-21 Mon 22:28]
(08:39:22 PM) djensen: need to add Citra hops
Expand Down Expand Up @@ -411,6 +415,7 @@ see http://www.djangosnippets.org/snippets/97/
*** TODO add time counter for some steps (primary/secondary fermentation)
*** TODO some sort of markdown/wikiformatting/CRLF-><br/> for notes
*** TODO be able to (re-)associate a recipe with a Brew
*** TODO use django `get_absolute_url`, `permalink` methods
*** focus improvements
**** TODO profile form after new user creation
**** /user/{user}
Expand All @@ -420,7 +425,6 @@ see http://www.djangosnippets.org/snippets/97/
***** DONE edit details -> :input:first
***** DONE add -> first form elt
***** TODO GET after POST of {grain, hop, …} -> [add] button on same form.
*** TODO use django `get_absolute_url`, `permalink` methods
*** /recipe/{id}
**** TODO show all brew instances of recipe
***** by: authoring user, viewing user, all users
Expand Down
18 changes: 15 additions & 3 deletions app/fixtures/auth.json
@@ -1,5 +1,5 @@
[
{"model": "auth.User", "pk": null,
{"model": "auth.User", "pk": 1,
"fields": {"username": "jsled",
"email": "jsled@asynchronous.org",
"first_name": "Josh",
Expand All @@ -8,13 +8,25 @@
"is_staff": 1,
"is_active": 1,
"is_superuser": 1}},
{"model": "auth.User", "pk": null,
{"model": "auth.User", "pk": 2,
"fields": {"username": "bob",
"email": "jsled@asynchronous.org",
"first_name": "Bob",
"last_name": "Dobbs",
"password": "sha1$6f483$0b61843d40656d75656c140ca14ca213ab21eaa0",
"is_staff": 0,
"is_active": 1,
"is_superuser": 0}}
"is_superuser": 0}},
{
"pk": 1,
"model": "app.userprofile",
"fields": {
"pref_make_starter": true,
"pref_dispensing_style": "k",
"pref_secondary_ferm": true,
"user": 1,
"pref_brew_type": "a",
"timezone": "UTC"
}
}
]
9 changes: 7 additions & 2 deletions app/models.py
Expand Up @@ -33,12 +33,15 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import datetime
from django.db import models
from django.contrib import auth
import itertools
import urllib
from decimal import Decimal, Context, ROUND_HALF_UP, InvalidOperation

from django.db import models
from django.contrib import auth

from timezones.fields import TimeZoneField

class StepFilter (object):
def __init__(self, conditions=None):
self._conditions = conditions or []
Expand Down Expand Up @@ -87,6 +90,8 @@ class UserProfile (models.Model):
pref_secondary_ferm = models.BooleanField(default=False)
pref_dispensing_style = models.CharField(max_length=1, choices=DispenseTypes, default='b')

timezone = TimeZoneField(default='UTC')

def __getitem__(self, key):
return self.__dict__.get(key, None)

Expand Down
6 changes: 3 additions & 3 deletions app/templates/index.html
Expand Up @@ -74,7 +74,7 @@ <h1>recent brews</h1>
</py:choose></a>
by <a href="/user/${brew.brewer.username}/">${brew.brewer.username}</a>
<py:if test="brew.brew_date">
on ${std.fmt.date.ymd(brew.brew_date)}
on ${std.fmt.date.ymd(brew.brew_date, request.user)}
</py:if>
</li>
</ul>
Expand All @@ -83,15 +83,15 @@ <h1>recent brews</h1>
<h1 class="feed-link"><a href="/feeds/new-recipes">recent recipes</a></h1>
<ul>
<li py:for="recipe in recent_recipes">
"${recipe_a(recipe)}", added ${std.fmt.date.ymd(recipe.insert_date)} by <a href="/user/${recipe.author.username}/">${recipe.author}</a>.
"${recipe_a(recipe)}", added ${std.fmt.date.ymd(recipe.insert_date, request.user)} by <a href="/user/${recipe.author.username}/">${recipe.author}</a>.
</li>
</ul>
</div>
</div>
<div class="bj-recent">
<h1>recent updates</h1>
<ul>
<li py:for="step in recent_updates"><a href="/user/${step.brew.brewer.username}/brew/${step.brew.id}">Step ${step.type}</a> journaled for <a href="/user/${step.brew.brewer.username}/">${step.brew.brewer.username}</a>'s <py:if test="step.brew.recipe">${recipe_a(step.brew.recipe)}</py:if> on ${std.fmt.date.best(step.date)}</li>
<li py:for="step in recent_updates"><a href="/user/${step.brew.brewer.username}/brew/${step.brew.id}">Step ${step.type}</a> journaled for <a href="/user/${step.brew.brewer.username}/">${step.brew.brewer.username}</a>'s <py:if test="step.brew.recipe">${recipe_a(step.brew.recipe)}</py:if> on ${std.fmt.date.best(step.date, request.user)}</li>
</ul>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions app/templates/user/brew/index.html
Expand Up @@ -26,7 +26,7 @@ <h1 py:if="not brew.recipe">un-named brew</h1>

<div id="details">
<div>
<span py:if="brew.brew_date">brewed on ${std.fmt.date.ymd(brew.brew_date)} </span>
<span py:if="brew.brew_date">brewed on ${std.fmt.date.ymdhm(brew.brew_date, user)} </span>
by <a href="/user/${brew.brewer.username}/">${brew.brewer.username}</a>
<span py:if="brew.recipe">from recipe ${recipe_a(brew.recipe)}</span>
</div>
Expand Down Expand Up @@ -75,7 +75,7 @@ <h2>log</h2>
future_steps = [step for step in steps if step.in_future()]
?>
<py:def function="step_row(std,user,brew,step)">
<td><a href="/user/${user.username}/brew/${brew.id}/step/${step.id}">${std.fmt.date.best(step.date)}</a></td>
<td><a href="/user/${user.username}/brew/${brew.id}/step/${step.id}">${std.fmt.date.best(step.date, user)}</a></td>
<td>${step.get_type_display()}</td>
<td><py:if test="step.volume">${step.volume} ${step.volume_units}</py:if></td>
<td><py:if test="step.temp">${step.temp} ${step.temp_units}</py:if></td>
Expand Down
16 changes: 8 additions & 8 deletions app/templates/user/index.html
Expand Up @@ -24,7 +24,7 @@ <h1>upcoming steps</h1>
<li py:for="step in future_steps">
<a href="/user/${user.username}/brew/${step.brew.id}/step/${step.id}/">${step.get_type_display()}</a>
for brew <a href="/user/${user.username}/brew/${step.brew.id}/">${step.brew.title()}</a>
on ${std.fmt.date.best(step.date)}</li>
on ${std.fmt.date.best(step.date, user)}</li>
</ul>
</div>
<div style="display:none">
Expand All @@ -33,7 +33,7 @@ <h1>upcoming steps</h1>
<li py:for="brew in future_brews">brew <a href="/user/${user.username}/brew/${brew.id}/">${brew.title()}</a>
<ul>
<li py:for="step in brew.future_steps()">
${step.get_type_display()} on ${std.fmt.date.best(step.date)}
${step.get_type_display()} on ${std.fmt.date.best(step.date, user)}
</li>
</ul>
</li>
Expand Down Expand Up @@ -75,8 +75,8 @@ <h1>${user.username} current brews</h1>
<py:when test="brew.recipe">${brew.recipe.name or "(unknown)"}</py:when>
<py:otherwise>(no name)</py:otherwise>
</py:choose></a></td>
<td>${std.fmt.date.ymd(brew.brew_date)}</td>
<td>${std.fmt.date.best(brew.last_update_date)}</td>
<td>${std.fmt.date.ymd(brew.brew_date, user)}</td>
<td>${std.fmt.date.best(brew.last_update_date, user)}</td>
<td>${brew.get_last_state_display()}</td>
<span py:def="step(brew, next)" py:strip="True">
<py:choose test="">
Expand Down Expand Up @@ -136,7 +136,7 @@ <h1>starred recipes</h1>
<py:otherwise>(style undefined)</py:otherwise>
</py:choose>
</td>
<td>${std.fmt.date.best(starred.when)}</td>
<td>${std.fmt.date.best(starred.when, user)}</td>
<td py:if="request.user.is_authenticated()"><a href="/recipe/new/?clone_from_recipe_id=${starred.recipe.id}">${action('clone')}</a>
&nbsp;<a href="/user/${request.user.username}/brew/new/?recipe_id=${starred.recipe.id}">${action('brew')}</a>
</td>
Expand Down Expand Up @@ -168,7 +168,7 @@ <h1>authored recipes</h1>
<py:when test="recipe.style">${recipe.style.name}</py:when>
<py:otherwise><em>unknown style</em></py:otherwise>
</py:choose> (${recipe.get_type_display()})</td>
<td>${std.fmt.date.ymd(recipe.insert_date)}</td>
<td>${std.fmt.date.ymd(recipe.insert_date, user)}</td>
<td py:if="request.user.is_authenticated()"><a href="/recipe/new/?clone_from_recipe_id=${recipe.id}">${action('clone')}</a>
<a href="/user/${request.user.username}/star/?recipe_id=${recipe.id}">${action('star')}</a>
<a href="/user/${request.user.username}/brew/new/?recipe_id=${recipe.id}">${action('brew')}</a>
Expand Down Expand Up @@ -197,8 +197,8 @@ <h1>historical brews</h1>
<py:when test="brew.recipe">${brew.recipe.name or "(unknown)"}</py:when>
<py:otherwise>(unnamed recipe)</py:otherwise>
</py:choose></a></td>
<td>${std.fmt.date.ymd(brew.brew_date)}</td>
<td>${std.fmt.date.best(brew.last_update_date)}</td>
<td>${std.fmt.date.ymd(brew.brew_date, user)}</td>
<td>${std.fmt.date.best(brew.last_update_date, user)}</td>
</tr>
</tbody>
</table>
Expand Down
59 changes: 58 additions & 1 deletion app/tests.py
Expand Up @@ -35,7 +35,9 @@
import datetime
import decimal
import itertools
import re
import unittest

from django.contrib import auth
from django.test.client import Client
from django.test import TestCase
Expand All @@ -47,6 +49,7 @@ class AppTestCase (TestCase):

def create_recipe(self, name, date, style, hops, grains):
res = self.client.post('/recipe/new/', {'name': name, 'insert_date': date, 'batch_size': 5, 'batch_size_units': 'gl', 'style': style, 'type': 'a'})
self.assertEquals(302, res.status_code, res)
recipe_url = res['Location']
recipe_base_url = '/'.join(recipe_url.split('/')[3:-1])
hop_post_url = '/%s/hop/' % (recipe_base_url)
Expand Down Expand Up @@ -470,6 +473,60 @@ def testFuture(self):
datetime.datetime = saved_datetime


class TestTimezoneAdjustments (AppTestCase):
def testRecipe(self):
user,passwd = 'jsled','s3kr1t'
self.client.login(username=user,password=passwd)
#
profile = models.UserProfile.objects.get(user__username='jsled')
profile.timezone = 'US/Eastern'
profile.save()
#
now = datetime.datetime.now()
pattern = '%Y-%m-%d %H:%M:%S'
now_str = now.strftime(pattern)
unused_style = 1
recipe_url = self.create_recipe('test', now_str, unused_style, [], [])
recipe_url = '/' + recipe_url + '/' # @fixme: quick fix; do this globally
#
res = self.client.get(recipe_url)
date_pattern = re.compile(r'''name="insert_date" value="([^"]+)"''')
date_eastern_str = date_pattern.search(res.content).group(1)
date_eastern = datetime.datetime.strptime(date_eastern_str, pattern)
profile.timezone = 'US/Pacific'
profile.save()
res2 = self.client.get(recipe_url)
date_pacific_str = date_pattern.search(res2.content).group(1)
date_pacific = datetime.datetime.strptime(date_pacific_str, pattern)
#
delta = date_eastern - date_pacific
MINUTE_SECONDS = 60
HOUR_SECONDS = 60 * MINUTE_SECONDS
self.assertEqual(3 * HOUR_SECONDS, delta.seconds)
#
# @fixme: assert recipe model insert_date unchanged
#
# plus 30 minutes
new_time = date_pacific + datetime.timedelta(seconds = 30 * MINUTE_SECONDS)
new_time_str = new_time.strftime(pattern)
res3 = self.client.post(recipe_url, {'name':'test', 'batch_size': '5', 'batch_size_units': 'gl', 'style': 1, 'type': 'a', 'insert_date': new_time_str})
self.assertEquals(302, res3.status_code)
res3 = self.client.get(recipe_url)
updated_pacific_str = date_pattern.search(res3.content).group(1)
updated_pacific = datetime.datetime.strptime(updated_pacific_str, pattern)
self.assertEquals(new_time, updated_pacific)
#
profile.timezone = 'US/Eastern'
profile.save()
res4 = self.client.get(recipe_url)
updated_eastern_str = date_pattern.search(res4.content).group(1)
updated_eastern = datetime.datetime.strptime(updated_eastern_str, pattern)
updated_delta = updated_eastern - updated_pacific
self.assertEqual(3 * HOUR_SECONDS, updated_delta.seconds)

# def testBrew():


class RecipeSortedIngredients (AppTestCase):
def testInsertedOutOfOrderButStillSorted(self):
user,passwd = 'jsled', 's3kr1t'
Expand Down Expand Up @@ -577,7 +634,7 @@ def testVolumeConversionRoundTripping(self):
'from %s %s to %s %s back to %s %s'
% (random_val, from_units, converted,
to_units, round_tripped, from_units))


class StepTest (TestCase):
def testGravityCorrection(self):
Expand Down

0 comments on commit e754b40

Please sign in to comment.