This commit is contained in:
sork 2011-02-18 00:35:39 +01:00
Родитель 8aaf4e6e02
Коммит 6a3eb1e9de
174 изменённых файлов: 7204 добавлений и 0 удалений

17
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,17 @@
settings_local.py
*.py[co]
*.sw[po]
.coverage
pip-log.txt
docs/_gh-pages
build.py
.DS_Store
*-min.css
*-all.css
*-min.js
*-all.js
vendor
.noseids
tmp/*
*~
locale/*

3
.gitmodules поставляемый Normal file
Просмотреть файл

@ -0,0 +1,3 @@
[submodule "vendor"]
path = vendor
url = git://github.com/mozilla/playdoh-lib.git

25
LICENSE Normal file
Просмотреть файл

@ -0,0 +1,25 @@
Copyright (c) 2011, Mozilla
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright owner nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

4
README.md Normal file
Просмотреть файл

@ -0,0 +1,4 @@
Spark
=====
Spark desktop and mobile campaign websites

0
__init__.py Normal file
Просмотреть файл

0
apps/.gitignore поставляемый Normal file
Просмотреть файл

0
apps/__init__.py Normal file
Просмотреть файл

Просмотреть файл

0
apps/commons/__init__.py Normal file
Просмотреть файл

Просмотреть файл

@ -0,0 +1,10 @@
from django.conf import settings
from django.utils import translation
def i18n(request):
return {'LANGUAGES': settings.LANGUAGES,
'LANG': settings.LANGUAGE_URL_MAP.get(translation.get_language())
or translation.get_language(),
'DIR': 'rtl' if translation.get_language_bidi() else 'ltr',
}

68
apps/commons/helpers.py Normal file
Просмотреть файл

@ -0,0 +1,68 @@
import cgi
import datetime
import urllib
import urlparse
from django.conf import settings
from django.template import defaultfilters
from django.utils.html import strip_tags
from jingo import register
import jinja2
from .urlresolvers import reverse
# Yanking filters from Django.
register.filter(strip_tags)
register.filter(defaultfilters.timesince)
register.filter(defaultfilters.truncatewords)
@register.function
def thisyear():
"""The current year."""
return jinja2.Markup(datetime.date.today().year)
@register.function
def url(viewname, *args, **kwargs):
"""Helper for Django's ``reverse`` in templates."""
return reverse(viewname, args=args, kwargs=kwargs)
@register.filter
def urlparams(url_, hash=None, **query):
"""
Add a fragment and/or query paramaters to a URL.
New query params will be appended to exising parameters, except duplicate
names, which will be replaced.
"""
url = urlparse.urlparse(url_)
fragment = hash if hash is not None else url.fragment
# Use dict(parse_qsl) so we don't get lists of values.
q = url.query
query_dict = dict(urlparse.parse_qsl(smart_str(q))) if q else {}
query_dict.update((k, v) for k, v in query.items())
query_string = _urlencode([(k, v) for k, v in query_dict.items()
if v is not None])
new = urlparse.ParseResult(url.scheme, url.netloc, url.path, url.params,
query_string, fragment)
return new.geturl()
def _urlencode(items):
"""A Unicode-safe URLencoder."""
try:
return urllib.urlencode(items)
except UnicodeEncodeError:
return urllib.urlencode([(k, smart_str(v)) for k, v in items])
@register.filter
def urlencode(txt):
"""Url encode a path."""
return urllib.quote_plus(txt)

Просмотреть файл

@ -0,0 +1,58 @@
"""
Taken from zamboni.amo.middleware.
This is django-localeurl, but with mozilla style capital letters in
the locale codes.
"""
import urllib
from django.http import HttpResponsePermanentRedirect
from django.utils.encoding import smart_str
import tower
from . import urlresolvers
from .helpers import urlparams
class LocaleURLMiddleware(object):
"""
1. Search for the locale.
2. Save it in the request.
3. Strip them from the URL.
"""
def process_request(self, request):
prefixer = urlresolvers.Prefixer(request)
urlresolvers.set_url_prefix(prefixer)
full_path = prefixer.fix(prefixer.shortened_path)
if 'lang' in request.GET:
# Blank out the locale so that we can set a new one. Remove lang
# from the query params so we don't have an infinite loop.
prefixer.locale = ''
new_path = prefixer.fix(prefixer.shortened_path)
query = dict((smart_str(k), request.GET[k]) for k in request.GET)
query.pop('lang')
return HttpResponsePermanentRedirect(urlparams(new_path, **query))
if full_path != request.path:
query_string = request.META.get('QUERY_STRING', '')
full_path = urllib.quote(full_path.encode('utf-8'))
if query_string:
full_path = '%s?%s' % (full_path, query_string)
response = HttpResponsePermanentRedirect(full_path)
# Vary on Accept-Language if we changed the locale
old_locale = prefixer.locale
new_locale, _ = prefixer.split_path(full_path)
if old_locale != new_locale:
response['Vary'] = 'Accept-Language'
return response
request.path_info = '/' + prefixer.shortened_path
request.locale = prefixer.locale
tower.activate(prefixer.locale)

0
apps/commons/models.py Normal file
Просмотреть файл

Просмотреть файл

Просмотреть файл

@ -0,0 +1,50 @@
import re
from os import listdir
from os.path import join, dirname
import test_utils
import manage
class MigrationTests(test_utils.TestCase):
"""Sanity checks for the SQL migration scripts."""
@staticmethod
def _migrations_path():
"""Return the absolute path to the migration script folder."""
return manage.path('migrations')
def test_unique(self):
"""Assert that the numeric prefixes of the DB migrations are unique."""
leading_digits = re.compile(r'^\d+')
seen_numbers = set()
path = self._migrations_path()
for filename in listdir(path):
match = leading_digits.match(filename)
if match:
number = match.group()
if number in seen_numbers:
self.fail('There is more than one migration #%s in %s.' %
(number, path))
seen_numbers.add(number)
def test_innodb_and_utf8(self):
"""Make sure each created table uses the InnoDB engine and UTF-8."""
# Heuristic: make sure there are at least as many "ENGINE=InnoDB"s as
# "CREATE TABLE"s. (There might be additional "InnoDB"s in ALTER TABLE
# statements, which are fine.)
path = self._migrations_path()
for filename in sorted(listdir(path)):
with open(join(path, filename)) as f:
contents = f.read()
creates = contents.count('CREATE TABLE')
engines = contents.count('ENGINE=InnoDB')
encodings = (contents.count('CHARSET=utf8') +
contents.count('CHARACTER SET utf8'))
assert engines >= creates, ("There weren't as many "
'occurrences of "ENGINE=InnoDB" as of "CREATE TABLE" in '
'migration %s.' % filename)
assert encodings >= creates, ("There weren't as many "
'UTF-8 declarations as "CREATE TABLE" occurrences in '
'migration %s.' % filename)

Просмотреть файл

@ -0,0 +1,109 @@
from django.conf import settings
from django.core.urlresolvers import reverse as django_reverse
from django.utils.thread_support import currentThread
from django.utils.translation.trans_real import parse_accept_lang_header
# Thread-local storage for URL prefixes. Access with (get|set)_url_prefix.
_prefixes = {}
def set_url_prefix(prefix):
"""Set the ``prefix`` for the current thread."""
_prefixes[currentThread()] = prefix
def get_url_prefix():
"""Get the prefix for the current thread, or None."""
return _prefixes.get(currentThread())
def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None):
"""Wraps Django's reverse to prepend the correct locale."""
prefixer = get_url_prefix()
if prefixer:
prefix = prefix or '/'
url = django_reverse(viewname, urlconf, args, kwargs, prefix)
if prefixer:
return prefixer.fix(url)
else:
return url
def find_supported(test):
return [settings.LANGUAGE_URL_MAP[x] for
x in settings.LANGUAGE_URL_MAP if
x.split('-', 1)[0] == test.lower().split('-', 1)[0]]
class Prefixer(object):
def __init__(self, request):
self.request = request
split = self.split_path(request.path_info)
self.locale, self.shortened_path = split
def split_path(self, path_):
"""
Split the requested path into (locale, path).
locale will be empty if it isn't found.
"""
path = path_.lstrip('/')
# Use partitition instead of split since it always returns 3 parts
first, _, rest = path.partition('/')
lang = first.lower()
if lang in settings.LANGUAGE_URL_MAP:
return settings.LANGUAGE_URL_MAP[lang], rest
else:
supported = find_supported(first)
if len(supported):
return supported[0], rest
else:
return '', path
def get_language(self):
"""
Return a locale code we support on the site using the
user's Accept-Language header to determine which is best. This
mostly follows the RFCs but read bug 439568 for details.
"""
if 'lang' in self.request.GET:
lang = self.request.GET['lang'].lower()
if lang in settings.LANGUAGE_URL_MAP:
return settings.LANGUAGE_URL_MAP[lang]
if self.request.META.get('HTTP_ACCEPT_LANGUAGE'):
ranked_languages = parse_accept_lang_header(
self.request.META['HTTP_ACCEPT_LANGUAGE'])
# Do we support or remap their locale?
supported = [lang[0] for lang in ranked_languages if lang[0]
in settings.LANGUAGE_URL_MAP]
# Do we support a less specific locale? (xx-YY -> xx)
if not len(supported):
for lang in ranked_languages:
supported = find_supported(lang[0])
if supported:
break
if len(supported):
return settings.LANGUAGE_URL_MAP[supported[0].lower()]
return settings.LANGUAGE_CODE
def fix(self, path):
path = path.lstrip('/')
url_parts = [self.request.META['SCRIPT_NAME']]
if path.partition('/')[0] not in settings.SUPPORTED_NONLOCALES:
locale = self.locale if self.locale else self.get_language()
url_parts.append(locale)
url_parts.append(path)
return '/'.join(url_parts)

0
apps/desktop/__init__.py Normal file
Просмотреть файл

Просмотреть файл

@ -0,0 +1,4 @@
{% extends "base.html" %}
{% block content %}
Dashboard
{% endblock %}

Просмотреть файл

@ -0,0 +1,249 @@
{% extends "desktop/base.html" %}
{% set title = _('Spark Firefox Mobile') %}
{% set scripts = ('jquery-ui', 'desktop') %}
{% block content %}
<div id="masthead" class="">
<a href="#" id="visit-mozilla"></a>
<span id="have-account">{{ _('Already have a Spark?') }} <a href="#" class="popup-trigger">{{ _('Sign in') }} ></a></span>
<h1>
<img src="{{ MEDIA_URL }}img/firefox-logo.png" alt="Mozilla Firefox">
{{ _('Mozilla') }}
<span>{{ _('Firefox Mobile') }}</span>
</h1>
<ul id="main-menu">
<li>{{ _('global spark') }}
<span class="subtitle">{{ _('set the world on fire') }}</span>
</li>
<li>{{ _('learn') }}
<span class="subtitle">{{ _('mobile browser features') }}</span>
</li>
<li>{{ _('download') }}
<span class="subtitle">{{ _('the browser') }}</span>
</li>
</ul>
</div>
<div id="headline">
<h2>
{{ _('Spark Firefox Mobile') }}
</h2>
<h3>{{ _('Ready to light up the world?') }}</h3>
<p>{% trans %}Quisque vitae neque ac felis porta pretium sit amet et risus. Maecenas eu sagittis nulla.
Mauris fermentum odio imperdiet urna interdum eu placerat lorem gravida. Aliquam purus turpis, varius quis sollicitudin et,
consectetur vel mauris. Donec nec cursus purus. Vestibulum vel gravida quam. <a href="#download">Get Firefox and start a Spark ></a>{% endtrans %}</p>
</div>
<div id="main-content" class="">
<div id="global-spark" class="section">
<div class="column left-column">
<h3>
{{ _('Global <br>Spark')|safe }}
<p>{{ _('Get the lowdown on the latest stats.') }}</p>
</h3>
</div>
<div class="column wide-column">
<p><span class="title">Catchy subhead title</span> Share your Firefox spark and add yourself to the visualization. Download the Firefox mobile browser to your Android phone to get started.</p>
<a href="#download">{{ _('Download Firefox Mobile to get started') }} ></a>
</div>
<a href="#" class="button">{{ _('Launch real-time visualization') }}</a>
<div id="globalmap">
<!-- Temporary image here -->
</div>
<div id="list-container">
<ul id="random-stats" class="items">
<li class="first">
<span class="count">138</span>
<span class="title">Fun Global Statistic</span>
<p>Lorem ipsum dolor sit amet sic non ummy sed lacus.</p>
</li>
<li>
<span class="count">138</span>
<span class="title">Fun Global Statistic</span>
<p>Lorem ipsum dolor sit amet sic non ummy sed lacus.</p>
</li>
<li>
<span class="count">138</span>
<span class="title">Fun Global Statistic</span>
<p>Lorem ipsum dolor sit amet sic non ummy sed lacus.</p>
</li>
<li>
<span class="count">138</span>
<span class="title">Fun Global Statistic</span>
<p>Lorem ipsum dolor sit amet sic non ummy sed lacus.</p>
</li>
<li>
<span class="count">138</span>
<span class="title">Fun Global Statistic</span>
<p>Lorem ipsum dolor sit amet sic non ummy sed lacus.</p>
</li>
</ul>
</div>
<div id="slider">
</div>
</div><!-- end global-spark -->
<div id="learn" class="section">
<div class="column left-column">
<h3>
{{ _('Learn') }}
<p>{{ _("Get the lowdown on Firefox mobile's features.") }}</p>
</h3>
</div>
<div class="column middle-column">
<ul class="features-list">
<li class="first">
<h4>{{ _('Type less, Browse More') }}</h4>
<p>{{ _('Easily browse the web with more swiping and less typing.') }}</p>
</li>
<li>
<h4>{{ _('Customize your Browser') }}</h4>
<p>{{ _('Add features to help make your browser your own. Install add-ons directly from your mobile.') }}</p>
</li>
<li>
<h4>{{ _('Stay in Sinc') }}</h4>
<p>{{ _('Access your history, passwords, bookmarks and even open tabs across all your devices.') }}</p>
</li>
</ul>
</div>
<div class="column right-column">
<ul class="features-list">
<li class="first">
<h4>{{ _('Type less, Browse More') }}</h4>
<p>{{ _('Easily browse the web with more swiping and less typing.') }}</p>
</li>
<li>
<h4>{{ _('Customize your Browser') }}</h4>
<p>{{ _('Add features to help make your browser your own. Install add-ons directly from your mobile.') }}</p>
</li>
<li>
<h4>{{ _('Stay in Sinc') }}</h4>
<p>{{ _('Access your history, passwords, bookmarks and even open tabs across all your devices.') }}</p>
</li>
</ul>
<a href="#" class="button">{{ _('Learn more') }}</a>
</div>
</div><!-- end learn -->
<div id="download" class="section">
<div class="column left-column">
<h3>
{{ _('Get Firefox') }}
<p>{{ _('Get Firefox on your phone, get the Spark.') }}</p>
</h3>
</div>
<div class="column wide-column">
<div class="sub-section first">
<span class="title">{{ _('System requirements:') }} </span>{{ _('Firefox is available for Android phones (2.0 and above) and the Nokia N900.') }}
</div>
<div class="sub-section">
<img src="{{ MEDIA_URL }}img/qrcode.png" alt="" id="qrcode">
<h4>{{ _('Option 01 / Scan the QR code') }}</h4>
<p>
{{ _("Scan the QR code to the right with your phone's camera. This will install Firefox on your phone automatically.") }}
<small><span class="title">{{ _("What's this?") }}</span>
{{ _('Install a code reader (we suggest searching for "QR reader" in your app store) and scan this code with your QR reader app.') }}</small>
</p>
</div>
<div class="sub-section">
<h4>{{ _("Option 02 / Use your phone's browser") }}</h4>
<p>
{% trans download_url='http://firefox.com/m' %}
Get Firefox by pointing your phone's default browser to <a href="{{ download_url }}">http://firefox.com/m</a>
{% endtrans %}
</p>
</div>
<div class="sub-section">
<h4>{{ _('Not an Android or MeeGo user?') }}</h4>
<p>
{{ _('No problem, try Firefox Home for iPhone (get it FREE from iTunes) <br/>OR just help spread the word about Firefox Mobile to others')|safe }}
</p>
</div>
</div>
</div><!-- end download -->
<div id="about" class="section">
<div class="column left-column">
<h3>
{{ _('About us') }}
</h3>
</div>
<div class="column wide-column">
<p>
{% trans %}
<span class="title">The Firefox Mobile browser is created by Mozilla</span>,
a non-profit organization whose mission is to promote openness,
innovation and opportunity on the Web.
{% endtrans %}
</p>
<a href="#" class="button">{{ _('More About Mozilla') }}</a>
</div>
</div><!-- end about -->
</div><!-- end main-content -->
<div id="footer" class="">
<a href="#" class="first">{{ _('Privacy Policy') }}</a>|
<a href="#">{{ _('Legal Notices') }}</a>|
<a href="#">{{ _('Report Trademark Abuse') }}</a>
<p id="license">
{% trans %}
Except where otherwise noted, content of this site is licensed under the<br/>
Creative Commons Attribution Share-Alike License v3.0 or any later version.
{% endtrans %}
</p>
</div>
<div id="popup">
<div id="sign-in">
<h3>{{ _('Sign-in') }}</h3>
<p class="legend">{{ _('Welcome back! Ready to keep your Spark going strong? Just fill your deets in to log in.') }}</p>
<hr>
<form action="login" method="post" accept-charset="utf-8">
<fieldset id="account" class="section">
<div class="input-wrapper">
<input tabindex="1" type="text" name="username" value="" placeholder="Username" required>
</div>
<div class="input-wrapper">
<input tabindex="2" type="text" name="password" value="" placeholder="Password" required>
</div>
</fieldset>
<div class="buttons-wrapper">
<button ontouchstart="" tabindex="4" class="left-button" type="reset">{{ _('Cancel') }}</button>
<button ontouchstart="" tabindex="3" class="right-button" type="submit">{{ _('Sign in') }}</button>
</div>
</form>
<a id="forgot-password" href="#">{{ _('Forgot your password?') }} ></a>
</div>
<div id="password-recovery">
<h3>{{ _('Forgot password?') }}</h3>
<p class="legend">
{{ _('No problem. Just enter the email address you used to sign up and well send it to you.') }}
</p>
<hr>
<form id="recover" action="login" method="post" accept-charset="utf-8">
<fieldset id="account" class="section">
<span class="error">{{ _('Oops! There was a problem.') }}</span>
<div class="input-wrapper error">
<input tabindex="1" type="email" name="email" value="" placeholder="Email address">
</div>
</fieldset>
<hr>
<div class="buttons-wrapper">
<button ontouchstart="" tabindex="3" class="left-button" type="reset">{{ _('Cancel') }}</button>
<button ontouchstart="" tabindex="2" class="right-button" type="submit">{{ _('Send') }}</button>
</div>
</form>
</div>
<div id="password-sent">
<h3>{{ _('Password sent') }}</h3>
<p class="legend">
{% trans %}
Youre almost there. We just sent your password to your email address.
Please wait a few moments before it arrives, then follow the instructions to log back in.
{% endtrans %}
</p>
</div>
</div>
<div id="mask"></div>
{% endblock %}

Просмотреть файл

@ -0,0 +1,4 @@
{% extends "base.html" %}
{% block content %}
{{ user.username }}'s Spark page
{% endblock %}

14
apps/desktop/urls.py Normal file
Просмотреть файл

@ -0,0 +1,14 @@
from django.conf.urls.defaults import patterns, url
from spark.views import redirect_to
from . import views
urlpatterns = patterns('',
url(r'^$', redirect_to, {'url': 'desktop.home'}),
url(r'^home$', views.home, name='desktop.home'),
url(r'^dashboard$', views.dashboard, name='desktop.dashboard'),
url(r'^user/(?P<username>[\w\d]+)$', views.user, name='desktop.user'),
url(r'^pwchange', views.ajax_pwchange, name='desktop.pwchange'),
url(r'^delaccount', views.ajax_delaccount, name='desktop.delaccount'),
)

31
apps/desktop/views.py Normal file
Просмотреть файл

@ -0,0 +1,31 @@
from django.shortcuts import get_object_or_404
from spark.decorators import login_required
from users.models import Profile
import jingo
def home(request):
return jingo.render(request, 'desktop/home.html', {})
@login_required
def dashboard(request):
return jingo.render(request, 'desktop/dashboard.html', {})
def user(request, username):
user_profile = get_object_or_404(Profile, user__username=username)
return jingo.render(request, 'desktop/user.html', { 'profile': user_profile })
@login_required
def ajax_pwchange(request):
return jingo.render(request, 'desktop/home.html', {})
@login_required
def ajax_delaccount(request):
return jingo.render(request, 'desktop/home.html', {})

0
apps/mobile/__init__.py Normal file
Просмотреть файл

Просмотреть файл

@ -0,0 +1,7 @@
from spark import decorators
def login_required(func):
return decorators.login_required(func, mobile=True)
def logout_required(func):
return decorators.logout_required(func, mobile=True)

Просмотреть файл

@ -0,0 +1,25 @@
{% extends "mobile/page.html" %}
{% set title = _('Spark! About Mozilla') %}
{% set pagetitle = _('About Mozilla') %}
{% set body_id = 'about' %}
{% set scripts = ('menu',) %}
{% set hide_menu = True %}
{% block pagecontent %}
<div class="section">
<p class="sans">
{% trans %}
Mozilla is a global, non-profit organization dedicated to making the Web better.
We believe in principle over profit, and that the Internet is a shared public resource to be cared for,
not a commodity to be sold. We work with a worldwide community to create free, open source software
like Mozilla Firefox, and to innovate for the benefit of the individual and the betterment of the Web.
The result is great products built by passionate people and better choices for everyone.
For more information, visit <a href="http://www.mozilla.org">www.mozilla.org</a>.
{% endtrans %}
</p>
</div>
<hr>
<h2 class="cta"><a ontouchstart="" href="{{ url('mobile.legal') }}">{{ _('Legal Stuff') }}</a></h2>
<hr>
{% endblock %}

Просмотреть файл

@ -0,0 +1,30 @@
{% extends "mobile/page.html" %}
{% set title = _('Spark! Badges') %}
{% set pagetitle = _('Badges') %}
{% set body_id = 'badges' %}
{% set scripts = ('menu', 'badges') %}
{% block pagecontent %}
<p class="section legend">
{{ _('Keep track of all your completed challenges and share your badges with friends.') }}
</p>
<hr>
<ul ontouchstart="" id="badge-list" class="section" data-count="{{ badges|count }}">
{% for badge in badges %}
<li class="badge" data-index="{{ loop.index }}">
<a href="#"></a>
<div class="badge-wrapper">
</div>
</li>
{% endfor %}
</ul>
{% endblock %}
{% block script %}
<script>
$(document).ready(function() {
initMenu();
initBadges();
});
</script>
{% endblock %}

Просмотреть файл

@ -0,0 +1,30 @@
{% extends "mobile/page.html" %}
{% set title = _('Spark! Boost your Spark') %}
{% set pagetitle = _('Boost your Spark') %}
{% set body_id = 'boost' %}
{% set scripts = ('menu',) %}
{% block pagecontent %}
<div class="section">
<h2>{{ _('Wanna share a few more details?') }}</h2>
<p class="sans">
{{ _('If you do, youll get to unlock even more cool challenges and get to see your Spark transform. Go on, you know you want to!') }}
</p>
</div>
<div class="section">
<h2>{{ _('P.S.') }}</h2>
<p class="sans">
{{ _('Dont worry, we dont share any details about your information with anyone. Everything you tell us is kept private so youre in good hands.') }}
</p>
</div>
<hr>
<h2 id="disclaimer" class="cta"><a ontouchstart="" href="#">{{ _('Privacy Disclaimer') }}</a></h2>
<hr>
<div class="buttons-wrapper">
<div class="button left-button"><a href="{{ url('mobile.home') }}">{{ _('Maybe later') }}</a></div>
<div class="button right-button"><a href="{{ url('mobile.boost1') }}">{{ _("Let's boost") }}</a></div>
</div>
{% endblock %}

Просмотреть файл

@ -0,0 +1,21 @@
{% extends "mobile/page.html" %}
{% set title = _('Spark! Boost your Spark') %}
{% set pagetitle = _('Boost your Spark') %}
{% set body_id = 'boost' %}
{% set scripts = ('menu',) %}
{% block pagecontent %}
<div class="section">
<h2>{{ _('Step 1 of 2') }}</h2>
<p class="sans">
{{ _('Tap the button below so we can find out where you are.') }}
</p>
</div>
<hr>
<div class="buttons-wrapper">
<div class="button left-button"><a href="{{ url('mobile.home') }}">{{ _('Maybe later') }}</a></div>
<div class="button right-button"><a href="#">{{ _('Locate Me') }}</a></div>
</div>
{% endblock %}

Просмотреть файл

@ -0,0 +1,36 @@
{% extends "mobile/page.html" %}
{% set title = _('Spark! Boost your Spark') %}
{% set pagetitle = _('Boost your Spark') %}
{% set body_id = 'boost' %}
{% set scripts = ('menu',) %}
{% block pagecontent %}
<div class="section">
<h2>{{ _('Step 2 of 2') }}</h2>
<p class="sans">
{% trans %}
Did someone send you a Spark? Enter their email address below so you can be connected,
help visualize where your Spark has travelled and make cool stuff happen.
{% endtrans %}
</p>
</div>
<form action="" method="post" accept-charset="utf-8">
<fieldset id="account" class="section">
<div class="input-wrapper">
<input tabindex="1" type="text" name="username" value="" placeholder="Username" required>
</div>
</fieldset>
<hr>
<fieldset id="newsletter">
<p id="custom-cb" class="sans">
<input tabindex="2" id="newsletter-cb" type="checkbox" name="newsletter" value="">
<label ontouchstart="" for="newsletter-cb">{{ _('I started a new Spark from Firefox.com') }}</label>
</p>
</fieldset>
<hr>
<div class="buttons-wrapper">
<div class="button left-button"><a href="{{ url('mobile.home') }}">{{ _('Maybe later') }}</a></div>
<div class="button right-button"><a href="#">{{ _('Find') }}</a></div>
</div>
</form>
{% endblock %}

Просмотреть файл

@ -0,0 +1,9 @@
{% extends "mobile/page.html" %}
{% set title = _('Spark! Challenges') %}
{% set pagetitle = _('Challenges') %}
{% set body_id = 'challenges' %}
{% set scripts = ('menu',) %}
{% block pagecontent %}
Challenges
{% endblock %}

Просмотреть файл

@ -0,0 +1,24 @@
{% extends "mobile/page.html" %}
{% set title = _('Spark! Home') %}
{% set pagetitle = _('Firefox Spark') %}
{% set body_id = 'home' %}
{% set scripts = ('menu',) %}
{% set hide_menu = True %}
{% block flame %}{% endblock %}
{% block pagecontent %}
<div id="static-spark">
</div>
<p class="section sans">
{{ _('Wanna help spread Firefox for Mobile around the world? Take challenges, unlock badges and earn yourself some bragging rights.') }}
</p>
<hr>
<h2 class="cta"><a ontouchstart="" href="{{ url('mobile.instructions') }}">{{ _('What is Firefox Spark?') }}</a></h2>
<hr>
<div class="buttons-wrapper">
<a href="{{ url('users.mobile_register') }}"><button ontouchstart="" class="left-button">{{ _('Join') }}</button></a>
<a href="{{ url('users.mobile_login') }}"><button ontouchstart="" class="right-button">{{ _('Log in') }}</button></a>
</div>
{% endblock %}

Просмотреть файл

@ -0,0 +1,39 @@
{% extends "mobile/page.html" %}
{% set title = _('Spark! What is Spark ?') %}
{% set pagetitle = _('What is Spark ?') %}
{% set body_id = 'whatisspark' %}
{% set scripts = ('menu',) %}
{% set hide_menu = True %}
{% block pagecontent %}
<p class="section legend">
{% trans %}
Good question. Spark is a game based on sharing challenges. The more you share your Spark with others,
the more challenges youll unlock and the more badges youll earn.
{% endtrans %}
</p>
<hr>
<div class="section">
<h2>{{ _('How it works') }}</h2>
<p class="sans">
{{ _('Each player starts with their own Spark.') }}
</p>
</div>
<hr>
<div class="section">
<div class="arrow"></div>
<p class="sans">
{{ _('The more you share it, the more it grows and evolves.') }}
</p>
</div>
<hr> <!-- Fennec inexplicably refuses to display this line in portrait mode -->
<div class="section">
<div class="arrow"></div>
<p class="sans">
{{ _('There are X levels of sharing challenges to get through, and for each challenge you complete, youll earn yourself a sweet little badge.') }}
</p>
</div>
{% endblock %}

Просмотреть файл

@ -0,0 +1,28 @@
{% extends "mobile/page.html" %}
{% set title = _('Spark! Legal') %}
{% set pagetitle = _('Legal') %}
{% set body_id = 'legal' %}
{% set scripts = ('menu',) %}
{% set hide_menu = True %}
{% block pagecontent %}
<p class="section sans">
Quisque commodo hendrerit lorem quis egestas. Maecenas quis tortor arcu. Vivamus rutrum nunc non neque consectetur quis placerat neque lobortis. Nam vestibulum, arcu sodales feugiat consectetur, nisl orci bibendum elit, eu euismod magna sapien..
</p>
<p class="section sans">
Quisque commodo hendrerit lorem quis
egestas. Maecenas quis tortor arcu. Vivamus
rutrum nunc non neque consectetur quis
placerat neque lobortis. Nam vestibulum, arcu
sodales feugiat consectetur, nisl orci bibendum
elit, eu euismod magna sapien..
</p>
<p class="section sans">
Quisque commodo hendrerit lorem quis
egestas. Maecenas quis tortor arcu. Vivamus
rutrum nunc non neque consectetur quis
placerat neque lobortis. Nam vestibulum, arcu
sodales feugiat consectetur, nisl orci bibendum
elit, eu euismod magna sapien..
</p>
{% endblock %}

Просмотреть файл

@ -0,0 +1,19 @@
{% extends "mobile/page.html" %}
{% set title = _('Spark! My Spark') %}
{% set pagetitle = _('My Firefox Spark') %}
{% set body_id = 'myspark' %}
{% set scripts = ('menu',) %}
{% block pagecontent %}
<div id="spark-wrapper" class="fpo">
<div id="clipping">
<div id="spark">
</div>
</div>
</div>
<hr class="clear">
<h2 class="cta"><a ontouchstart="" href="{{ url('mobile.shareqr') }}">{{ _('Share your Spark') }}</a></h2>
<hr>
{% endblock %}

Просмотреть файл

@ -0,0 +1,17 @@
{% extends "mobile/page.html" %}
{% set title = _('Spark! Share with a link') %}
{% set pagetitle = _('Share with a link') %}
{% set body_id = 'share-link' %}
{% set scripts = ('menu',) %}
{% block pagecontent %}
<p class="section legend">
{{ _('Select a link below to start sharing your Spark with friends.') }}
</p>
<ul id="sharing">
<li class="cta"><a href="">{{ _('Share on Twitter') }}</a></li>
<li class="cta"><a href="">{{ _('Share on Facebook') }}</a></li>
<li class="cta"><a href="">{{ _('Share via SMS') }}</a></li>
<li class="cta"><a href="">{{ _('Share via Email') }}</a></li>
</ul>
{% endblock %}

Просмотреть файл

@ -0,0 +1,18 @@
{% extends "mobile/page.html" %}
{% set title = _('Spark! Share your Spark') %}
{% set pagetitle = _('Share your Spark') %}
{% set body_id = 'share-QR' %}
{% set scripts = ('menu',) %}
{% block pagecontent %}
<p class="section legend">
{{ _('Ready to get your Spark out into the world? Just get your buddy to scan the QR code below or share it with a link.') }}
</p>
<hr>
<img id="qr" src="{{ MEDIA_URL }}img/mobile/qrcode.png" alt="">
<hr>
<h2 class="cta"><a ontouchstart="" href="{{ url('mobile.sharelink')}}">{{ _('Share with a link') }}</a></h2>
<hr>
{% endblock %}

Просмотреть файл

@ -0,0 +1,9 @@
{% extends "mobile/page.html" %}
{% set title = _('Spark! Stats') %}
{% set pagetitle = _('Stats') %}
{% set body_id = 'stats' %}
{% set scripts = ('menu',) %}
{% block pagecontent %}
Stats
{% endblock %}

20
apps/mobile/urls.py Normal file
Просмотреть файл

@ -0,0 +1,20 @@
from django.conf.urls.defaults import patterns, url
from spark.views import redirect_to
from . import views
urlpatterns = patterns('',
url(r'^$', redirect_to, {'url': 'mobile.home'}),
url(r'^home$', views.home, name='mobile.home'),
url(r'^badges$', views.badges, name='mobile.badges'),
url(r'^challenges$', views.challenges, name='mobile.challenges'),
url(r'^instructions$', views.instructions, name='mobile.instructions'),
url(r'^stats$', views.stats, name='mobile.stats'),
url(r'^share/qr$', views.shareqr, name='mobile.shareqr'),
url(r'^share/link$', views.sharelink, name='mobile.sharelink'),
url(r'^about$', views.about, name='mobile.about'),
url(r'^legal$', views.legal, name='mobile.legal'),
url(r'^boost$', views.boost, name='mobile.boost'),
url(r'^boost1$', views.boost1, name='mobile.boost1'),
url(r'^boost2$', views.boost2, name='mobile.boost2'),
)

64
apps/mobile/views.py Normal file
Просмотреть файл

@ -0,0 +1,64 @@
from spark.urlresolvers import reverse
import jingo
from .decorators import login_required, logout_required
def home(request):
if request.user.is_authenticated():
return jingo.render(request, 'mobile/myspark.html', {})
return jingo.render(request, 'mobile/home.html', {})
@login_required
def boost(request):
return jingo.render(request, 'mobile/boost.html', {})
@login_required
def boost1(request):
return jingo.render(request, 'mobile/boost_step1.html', {})
@login_required
def boost2(request):
return jingo.render(request, 'mobile/boost_step2.html', {})
@login_required
def badges(request):
badgelist = range(8)
return jingo.render(request, 'mobile/badges.html', { 'badges': badgelist })
@login_required
def challenges(request):
return jingo.render(request, 'mobile/challenges.html', {})
def instructions(request):
return jingo.render(request, 'mobile/instructions.html', {})
@login_required
def stats(request):
return jingo.render(request, 'mobile/stats.html', {})
@login_required
def shareqr(request):
return jingo.render(request, 'mobile/shareqr.html', {})
@login_required
def sharelink(request):
return jingo.render(request, 'mobile/sharelink.html', {})
def about(request):
return jingo.render(request, 'mobile/about.html', {})
def legal(request):
return jingo.render(request, 'mobile/legal.html', {})

0
apps/poster/__init__.py Normal file
Просмотреть файл

0
apps/sharing/__init__.py Normal file
Просмотреть файл

0
apps/spark/__init__.py Normal file
Просмотреть файл

Просмотреть файл

@ -0,0 +1,10 @@
from django.conf import settings
from django.utils import translation
def i18n(request):
return {'LANGUAGES': settings.LANGUAGES,
'LANG': settings.LANGUAGE_URL_MAP.get(translation.get_language())
or translation.get_language(),
'DIR': 'rtl' if translation.get_language_bidi() else 'ltr',
}

141
apps/spark/decorators.py Normal file
Просмотреть файл

@ -0,0 +1,141 @@
import json
from functools import wraps
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect, HttpResponseBadRequest
from django.utils.decorators import available_attrs
from django.utils.http import urlquote
from .urlresolvers import reverse
## Taken from kitsune & zamboni
def ssl_required(view_func):
"""A view decorator that enforces HTTPS.
If settings.DEBUG is True, it doesn't enforce anything."""
def _checkssl(request, *args, **kwargs):
if not settings.DEBUG and not request.is_secure():
url_str = request.build_absolute_uri()
url_str = url_str.replace('http://', 'https://')
return HttpResponseRedirect(url_str)
return view_func(request, *args, **kwargs)
return _checkssl
def user_access_decorator(redirect_func, redirect_url_func, deny_func=None,
redirect_field=REDIRECT_FIELD_NAME, mobile=False):
"""
Helper function that returns a decorator.
* redirect func ----- If truthy, a redirect will occur
* deny_func --------- If truthy, HttpResponseForbidden is returned.
* redirect_url_func - Evaluated at view time, returns the redirect URL
i.e. where to go if redirect_func is truthy.
* redirect_field ---- What field to set in the url, defaults to Django's.
Set this to None to exclude it from the URL.
"""
def decorator(view_fn):
def _wrapped_view(request, *args, **kwargs):
if redirect_func(request.user):
# We must call reverse at the view level, else the threadlocal
# locale prefixing doesn't take effect.
if mobile:
default_login_url = reverse('users.mobile_login')
else:
default_login_url = reverse('users.login')
redirect_url = redirect_url_func() or default_login_url
# Redirect back here afterwards?
if redirect_field:
path = urlquote(request.get_full_path())
redirect_url = '%s?%s=%s' % (
redirect_url, redirect_field, path)
return HttpResponseRedirect(redirect_url)
if deny_func and deny_func(request.user):
return HttpResponseForbidden()
return view_fn(request, *args, **kwargs)
return wraps(view_fn, assigned=available_attrs(view_fn))(_wrapped_view)
return decorator
def logout_required(redirect, mobile=False):
"""Requires that the user *not* be logged in."""
redirect_func = lambda u: u.is_authenticated()
if hasattr(redirect, '__call__'):
home_view = 'mobile.home' if mobile else 'desktop.home'
return user_access_decorator(
redirect_func, redirect_field=None,
redirect_url_func=lambda: reverse(home_view))(redirect)
else:
return user_access_decorator(redirect_func, redirect_field=None,
redirect_url_func=lambda: redirect)
def login_required(func, login_url=None, redirect=REDIRECT_FIELD_NAME,
only_active=True, mobile=False):
"""Requires that the user is logged in."""
if only_active:
redirect_func = lambda u: not (u.is_authenticated() and u.is_active)
else:
redirect_func = lambda u: not u.is_authenticated()
redirect_url_func = lambda: login_url
return user_access_decorator(redirect_func, redirect_field=redirect,
redirect_url_func=redirect_url_func,
mobile=mobile)(func)
def post_required(f):
@wraps(f)
def wrapper(request, *args, **kw):
if request.method != 'POST':
return http.HttpResponseNotAllowed(['POST'])
else:
return f(request, *args, **kw)
return wrapper
def ajax_required(f):
"""
AJAX request required decorator
use it in your views:
@ajax_required
def my_view(request):
....
"""
def wrap(request, *args, **kwargs):
if not request.is_ajax():
return HttpResponseBadRequest()
return f(request, *args, **kwargs)
wrap.__doc__=f.__doc__
wrap.__name__=f.__name__
return wrap
def json_view(f):
@wraps(f)
def wrapper(*args, **kw):
response = f(*args, **kw)
if isinstance(response, HttpResponse):
return response
else:
return HttpResponse(json.dumps(response),
content_type='application/json')
return wrapper
json_view.error = lambda s: http.HttpResponseBadRequest(
json.dumps(s), content_type='application/json')

52
apps/spark/helpers.py Normal file
Просмотреть файл

@ -0,0 +1,52 @@
import urlparse
from django.http import QueryDict
from django.utils.encoding import smart_unicode, smart_str
from django.utils.http import urlencode
from jingo import register
import jinja2
from .urlresolvers import reverse
@register.function
def url(viewname, *args, **kwargs):
"""Helper for Django's ``reverse`` in templates."""
locale = kwargs.pop('locale', None)
return reverse(viewname, locale=locale, args=args, kwargs=kwargs)
@register.filter
def label_with_help(f):
"""Print the label tag for a form field, including the help_text
value as a title attribute."""
label = u'<label for="%s" title="%s">%s</label>'
return jinja2.Markup(label % (f.auto_id, f.help_text, f.label))
@register.filter
def urlparams(url_, hash=None, query_dict=None, **query):
"""
Add a fragment and/or query paramaters to a URL.
New query params will be appended to exising parameters, except duplicate
names, which will be replaced.
"""
url_ = urlparse.urlparse(url_)
fragment = hash if hash is not None else url_.fragment
q = url_.query
new_query_dict = (QueryDict(smart_str(q), mutable=True) if
q else QueryDict('', mutable=True))
if query_dict:
for k, l in query_dict.lists():
new_query_dict[k] = None # Replace, don't append.
for v in l:
new_query_dict.appendlist(k, v)
for k, v in query.items():
new_query_dict[k] = v # Replace, don't append.
query_string = urlencode([(k, v) for k, l in new_query_dict.lists() for
v in l if v is not None])
new = urlparse.ParseResult(url_.scheme, url_.netloc, url_.path,
url_.params, query_string, fragment)
return new.geturl()

107
apps/spark/middleware.py Normal file
Просмотреть файл

@ -0,0 +1,107 @@
import contextlib
import re
import urllib
from django.http import HttpResponsePermanentRedirect, HttpResponseForbidden
from django.middleware import common
from django.utils.encoding import iri_to_uri, smart_str, smart_unicode
import jingo
import MySQLdb as mysql
import tower
from .helpers import urlparams
from .urlresolvers import Prefixer, set_url_prefixer, split_path
from .views import handle403
## Taken from kitsune
class LocaleURLMiddleware(object):
"""
Based on zamboni.amo.middleware.
Tried to use localeurl but it choked on 'en-US' with capital letters.
1. Search for the locale.
2. Save it in the request.
3. Strip them from the URL.
"""
def process_request(self, request):
prefixer = Prefixer(request)
set_url_prefixer(prefixer)
full_path = prefixer.fix(prefixer.shortened_path)
if 'lang' in request.GET:
# Blank out the locale so that we can set a new one. Remove lang
# from the query params so we don't have an infinite loop.
prefixer.locale = ''
new_path = prefixer.fix(prefixer.shortened_path)
query = dict((smart_str(k), v) for
k, v in request.GET.iteritems() if k != 'lang')
return HttpResponsePermanentRedirect(urlparams(new_path, **query))
if full_path != request.path:
query_string = request.META.get('QUERY_STRING', '')
full_path = urllib.quote(full_path.encode('utf-8'))
if query_string:
full_path = '%s?%s' % (full_path, query_string)
response = HttpResponsePermanentRedirect(full_path)
# Vary on Accept-Language if we changed the locale
old_locale = prefixer.locale
new_locale, _ = split_path(full_path)
if old_locale != new_locale:
response['Vary'] = 'Accept-Language'
return response
request.path_info = '/' + prefixer.shortened_path
request.locale = prefixer.locale
tower.activate(prefixer.locale)
def process_response(self, request, response):
"""Unset the thread-local var we set during `process_request`."""
# This makes mistaken tests (that should use LocalizingClient but
# use Client instead) fail loudly and reliably. Otherwise, the set
# prefixer bleeds from one test to the next, making tests
# order-dependent and causing hard-to-track failures.
set_url_prefixer(None)
return response
def process_exception(self, request, exception):
set_url_prefixer(None)
class Forbidden403Middleware(object):
"""
Renders a 403.html page if response.status_code == 403.
"""
def process_response(self, request, response):
if isinstance(response, HttpResponseForbidden):
return handle403(request)
# If not 403, return response unmodified
return response
class RemoveSlashMiddleware(object):
"""
Middleware that tries to remove a trailing slash if there was a 404.
If the response is a 404 because url resolution failed, we'll look for a
better url without a trailing slash.
"""
def process_response(self, request, response):
if (response.status_code == 404
and request.path_info.endswith('/')
and not common._is_valid_path(request.path_info)
and common._is_valid_path(request.path_info[:-1])):
# Use request.path because we munged app/locale in path_info.
newurl = request.path[:-1]
if request.GET:
with safe_query_string(request):
newurl += '?' + request.META['QUERY_STRING']
return HttpResponsePermanentRedirect(newurl)
return response

55
apps/spark/models.py Normal file
Просмотреть файл

@ -0,0 +1,55 @@
from django.db import models
class ModelBase(models.Model):
"""
Base class for models to abstract some common features.
* Adds automatic created and modified fields to the model.
"""
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
get_latest_by = 'created'
class Continent(models.Model):
code = models.CharField(max_length=2, primary_key=True)
name = models.CharField(max_length=128)
class Meta:
ordering = ('name',)
def __unicode__(self):
return self.name
class Country(models.Model):
iso = models.CharField(max_length=2, primary_key=True)
name = models.CharField(max_length=128)
full_name = models.CharField(max_length=255)
continent = models.ForeignKey(Continent, null=True)
iso3 = models.CharField(max_length=3)
number = models.CharField(max_length=3)
class Meta:
ordering = ('name',)
def __unicode__(self):
return self.name
class City(ModelBase):
"""
Represents cities used by the global visualization
"""
country = models.ForeignKey(Country)
city = models.CharField(max_length=255)
class Meta:
unique_together = ('country', 'city')
def __unicode__(self):
return '%s, %s' % (self.country, self.city)

Просмотреть файл

@ -0,0 +1,10 @@
{% extends "desktop/base.html" %}
{% set title = _('Access denied') %}
{% block content %}
<article id="error-page" class="main">
<h1>{{ title }}</h1>
<p>{{ _('You do not have permission to access this page.') }}</p>
</article>
{% endblock %}

Просмотреть файл

@ -0,0 +1,11 @@
{% extends "desktop/base.html" %}
{% set title = _('Page Not Found') %}
{% block content %}
<article id="error-page" class="main">
<h1>{{ title }}</h1>
<p>{% trans %}Sorry, we couldn't find the page you were looking for. Please,
try searching our site using the form below.{% endtrans %}</p>
</article>
{% endblock %}

Просмотреть файл

@ -0,0 +1,12 @@
{% extends "desktop/base.html" %}
{% set title = _('An Error Occurred') %}
{% block content %}
<article id="error-page" class="main">
<h1>{{ title }}</h1>
<p>{% trans %}Oh, no! It looks like an unexpected error occurred. We've
already notified the site administrators. Please try again now, or in a few
minutes.{% endtrans %}</p>
</article>
{% endblock %}

Просмотреть файл

@ -0,0 +1,10 @@
{% extends "mobile/base.html" %}
{% set title = _('Access denied') %}
{% block content %}
<article id="error-page" class="main">
<h1>{{ title }}</h1>
<p>{{ _('You do not have permission to access this page.') }}</p>
</article>
{% endblock %}

Просмотреть файл

@ -0,0 +1,11 @@
{% extends "mobile/base.html" %}
{% set title = _('Page Not Found') %}
{% block content %}
<article id="error-page" class="main">
<h1>{{ title }}</h1>
<p>{% trans %}Sorry, we couldn't find the page you were looking for. Please,
try searching our site using the form below.{% endtrans %}</p>
</article>
{% endblock %}

Просмотреть файл

@ -0,0 +1,12 @@
{% extends "mobile/base.html" %}
{% set title = _('An Error Occurred') %}
{% block content %}
<article id="error-page" class="main">
<h1>{{ title }}</h1>
<p>{% trans %}Oh, no! It looks like an unexpected error occurred. We've
already notified the site administrators. Please try again now, or in a few
minutes.{% endtrans %}</p>
</article>
{% endblock %}

Просмотреть файл

@ -0,0 +1,4 @@
# robots.txt
User-Agent: *
Disallow: /admin/

Просмотреть файл

@ -0,0 +1,29 @@
from django.test.client import Client
from django.conf import settings
from test_utils import TestCase
from spark.urlresolvers import reverse, split_path
get = lambda c, v, **kw: c.get(reverse(v, **kw), follow=True)
post = lambda c, v, data={}, **kw: c.post(reverse(v, **kw), data, follow=True)
class LocalizingClient(Client):
"""Client which prepends a locale so test requests can get through
LocaleURLMiddleware without resulting in a locale-prefix-adding 301.
Otherwise, we'd have to hard-code locales into our tests everywhere or
{mock out reverse() and make LocaleURLMiddleware not fire}.
"""
def request(self, **request):
"""Make a request, but prepend a locale if there isn't one already."""
# Fall back to defaults as in the superclass's implementation:
path = request.get('PATH_INFO', self.defaults.get('PATH_INFO', '/'))
locale, shortened = split_path(path)
if not locale:
request['PATH_INFO'] = '/%s/%s' % (settings.LANGUAGE_CODE,
shortened)
return super(LocalizingClient, self).request(**request)

135
apps/spark/urlresolvers.py Normal file
Просмотреть файл

@ -0,0 +1,135 @@
import threading
from django.conf import settings
from django.core.handlers.wsgi import WSGIRequest
from django.core.urlresolvers import reverse as django_reverse
from django.utils.translation.trans_real import parse_accept_lang_header
# Thread-local storage for URL prefixes. Access with (get|set)_url_prefix.
_locals = threading.local()
def set_url_prefixer(prefixer):
"""Set the Prefixer for the current thread."""
_locals.prefixer = prefixer
def get_url_prefixer():
"""Get the Prefixer for the current thread, or None."""
return getattr(_locals, 'prefixer', None)
def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None,
force_locale=False, locale=None):
"""Wraps Django's reverse to prepend the correct locale.
force_locale -- Ordinarily, if get_url_prefixer() returns None, we return
an unlocalized URL, which will be localized via redirect when visited.
Set force_locale to True to force the insertion of a default locale
when there is no set prefixer. If you are writing a test and simply
wish to avoid LocaleURLMiddleware's initial 301 when passing in an
unprefixed URL, it is probably easier to substitute LocalizingClient
for any uses of django.test.client.Client and forgo this kwarg.
locale -- By default, reverse prepends the current locale (if set) or
the default locale if force_locale == True. To override this behavior
and have it prepend a different locale, pass in the locale parameter
with the desired locale. When passing a locale, the force_locale is
not used and is implicitly True.
"""
if locale:
prefixer = Prefixer(locale=locale)
else:
prefixer = get_url_prefixer()
if not prefixer and force_locale:
prefixer = Prefixer()
if prefixer:
prefix = prefix or '/'
url = django_reverse(viewname, urlconf, args, kwargs, prefix)
if prefixer:
return prefixer.fix(url)
else:
return url
def find_supported(test):
return [settings.LANGUAGE_URL_MAP[x] for
x in settings.LANGUAGE_URL_MAP if
x.split('-', 1)[0] == test.lower().split('-', 1)[0]]
def split_path(path):
"""
Split the requested path into (locale, path).
locale will be empty if it isn't found.
"""
path = path.lstrip('/')
# Use partition instead of split since it always returns 3 parts
first, _, rest = path.partition('/')
lang = first.lower()
if lang in settings.LANGUAGE_URL_MAP:
return settings.LANGUAGE_URL_MAP[lang], rest
else:
supported = find_supported(first)
if supported:
return supported[0], rest
else:
return '', path
class Prefixer(object):
def __init__(self, request=None, locale=None):
"""If request is omitted, fall back to a default locale."""
self.request = request or WSGIRequest({'REQUEST_METHOD': 'bogus'})
self.locale, self.shortened_path = split_path(self.request.path_info)
if locale:
self.locale = locale
def get_language(self):
"""
Return a locale code we support on the site using the
user's Accept-Language header to determine which is best. This
mostly follows the RFCs but read bug 439568 for details.
"""
if 'lang' in self.request.GET:
lang = self.request.GET['lang'].lower()
if lang in settings.LANGUAGE_URL_MAP:
return settings.LANGUAGE_URL_MAP[lang]
if self.request.META.get('HTTP_ACCEPT_LANGUAGE'):
ranked_languages = parse_accept_lang_header(
self.request.META['HTTP_ACCEPT_LANGUAGE'])
# Do we support or remap their locale?
supported = [lang[0] for lang in ranked_languages if lang[0]
in settings.LANGUAGE_URL_MAP]
# Do we support a less specific locale? (xx-YY -> xx)
if not len(supported):
for lang in ranked_languages:
supported = find_supported(lang[0])
if supported:
break
if len(supported):
return settings.LANGUAGE_URL_MAP[supported[0].lower()]
return settings.LANGUAGE_CODE
def fix(self, path):
path = path.lstrip('/')
url_parts = [self.request.META['SCRIPT_NAME']]
if path.partition('/')[0] not in settings.SUPPORTED_NONLOCALES:
locale = self.locale if self.locale else self.get_language()
url_parts.append(locale)
url_parts.append(path)
return '/'.join(url_parts)

6
apps/spark/urls.py Normal file
Просмотреть файл

@ -0,0 +1,6 @@
from django.conf.urls.defaults import patterns, url
from django.views.generic.simple import direct_to_template
urlpatterns = patterns('',
url(r'^robots.txt$', direct_to_template, {'template': 'spark/robots.html', 'mimetype': 'text/plain'}),
)

63
apps/spark/views.py Normal file
Просмотреть файл

@ -0,0 +1,63 @@
import logging
import os
import socket
import StringIO
import time
import re
from django import http
from django.conf import settings
from django.core.cache import cache, parse_backend_uri
from django.http import (HttpResponsePermanentRedirect, HttpResponseRedirect,
HttpResponse)
from django.views.decorators.cache import never_cache
import celery.task
import jingo
from spark.urlresolvers import reverse
def _mobile_request(request):
mobile_url = re.compile(r'.+/m/.+')
return mobile_url.match(request.path) != None
def handle403(request):
"""A 403 message that looks nicer than the normal Apache forbidden page."""
if(_mobile_request(request)):
template = 'spark/handlers/mobile/403.html'
else:
template = 'spark/handlers/desktop/403.html'
return jingo.render(request, template, status=403)
def handle404(request):
"""A handler for 404s."""
if(_mobile_request(request)):
template = 'spark/handlers/mobile/404.html'
else:
template = 'spark/handlers/desktop/404.html'
return jingo.render(request, template, status=404)
def handle500(request):
"""A 500 message that looks nicer than the normal Apache error page."""
if(_mobile_request(request)):
template = 'spark/handlers/mobile/500.html'
else:
template = 'spark/handlers/desktop/500.html'
return jingo.render(request, template, status=500)
def redirect_to(request, url, permanent=True, **kwargs):
"""Like Django's redirect_to except that 'url' is passed to reverse."""
dest = reverse(url, kwargs=kwargs)
if permanent:
return HttpResponsePermanentRedirect(dest)
return HttpResponseRedirect(dest)
def robots(request):
"""Generate a robots.txt."""
template = jingo.render(request, 'robots.html')
return HttpResponse(template, mimetype='text/plain')

0
apps/stats/__init__.py Normal file
Просмотреть файл

29
apps/stats/models.py Normal file
Просмотреть файл

@ -0,0 +1,29 @@
from django.db import models
from django.contrib.auth.models import User
from spark.models import ModelBase
class PersonalStats(ModelBase):
user = models.OneToOneField(User, primary_key=True, related_name='stats',
verbose_name=_lazy(u'User'))
longest_chain = models.PositiveIntegerField(default=1,
verbose_name=_lazy(u'Longest chain'))
# TODO: other user-specific stats
def __unicode__(self):
return unicode(self.user)
class GlobalStats(ModelBase):
name = models.CharField(verbose_name=_lazy(u('Name')))
title = models.CharField(blank=True, null=True,
verbose_name=_lazy(u'Title'))
description = models.CharField(blank=True, null=True,
verbose_name=_lazy(u'Description'))
value = models.FloatField(blank=True, null=True,
verbose_name=_lazy(u'Value'))
def __unicode__(self):
return unicode(self.name)

1
apps/users/__init__.py Normal file
Просмотреть файл

@ -0,0 +1 @@
## User registration based on kitsune/apps/users/

41
apps/users/backends.py Normal file
Просмотреть файл

@ -0,0 +1,41 @@
import hashlib
import os
from django.contrib.auth import models as auth_models
from django.contrib.auth.backends import ModelBackend
# http://fredericiana.com/2010/10/12/adding-support-for-stronger-password-hashes-to-django/
"""
from future import django_sha256_support
Monkey-patch SHA-256 support into Django's auth system. If Django ticket #5600
ever gets fixed, this can be removed.
"""
def get_hexdigest(algorithm, salt, raw_password):
"""Generate SHA-256 hash."""
if algorithm == 'sha256':
return hashlib.sha256((salt + raw_password).encode('utf8')).hexdigest()
else:
return get_hexdigest_old(algorithm, salt, raw_password)
get_hexdigest_old = auth_models.get_hexdigest
auth_models.get_hexdigest = get_hexdigest
def set_password(self, raw_password):
"""Set SHA-256 password."""
algo = 'sha256'
salt = os.urandom(5).encode('hex') # Random, 10-digit (hex) salt.
hsh = get_hexdigest(algo, salt, raw_password)
self.password = '$'.join((algo, salt, hsh))
auth_models.User.set_password = set_password
class Sha256Backend(ModelBackend):
"""
Overriding the Django model backend without changes ensures our
monkeypatching happens by the time we import auth.
"""
pass

Просмотреть файл

@ -0,0 +1,161 @@
[
{
"pk": 1,
"model": "auth.group",
"fields": {
"name": "ForumsModerator",
"permissions": []
}
},
{
"pk": 1,
"model": "auth.user",
"fields": {
"username": "admin",
"first_name": "",
"last_name": "",
"is_active": 1,
"is_superuser": 1,
"is_staff": 1,
"last_login": "2010-01-01 00:00:00",
"groups": [],
"user_permissions": [],
"password": "sha1$d0fcb$661bd5197214051ed4de6da4ecdabe17f5549c7c",
"email": "user1@nowhere",
"date_joined": "2010-01-01 00:00:00"
}
},
{
"pk": 2,
"model": "auth.user",
"fields": {
"username": "tagger",
"first_name": "",
"last_name": "",
"is_active": 1,
"is_superuser": 0,
"is_staff": 0,
"last_login": "2010-01-01 00:00:00",
"groups": [],
"user_permissions": [],
"password": "sha1$d0fcb$661bd5197214051ed4de6da4ecdabe17f5549c7c",
"email": "user2@nowhere",
"date_joined": "2010-01-01 00:00:00"
}
},
{
"pk": 47963,
"model": "auth.user",
"fields": {
"username": "pcraciunoiu",
"first_name": "",
"last_name": "",
"is_active": 1,
"is_superuser": 0,
"is_staff": 0,
"last_login": "2010-04-13 10:20:21",
"groups": [1],
"user_permissions": [],
"password": "sha1$d0fcb$661bd5197214051ed4de6da4ecdabe17f5549c7c",
"email": "user47963@nowhere",
"date_joined": "2008-10-06 10:34:21"
}
},
{
"pk": 118533,
"model": "auth.user",
"fields": {
"username": "jsocol",
"first_name": "",
"last_name": "",
"is_active": 1,
"is_superuser": 0,
"is_staff": 0,
"last_login": "2010-04-26 19:01:45",
"groups": [],
"user_permissions": [],
"password": "sha1$d0fcb$661bd5197214051ed4de6da4ecdabe17f5549c7c",
"email": "user118533@nowhere",
"date_joined": "2009-08-10 16:09:45"
}
},
{
"pk": 118577,
"model": "auth.user",
"fields": {
"username": "rrosario",
"first_name": "",
"last_name": "",
"is_active": 1,
"is_superuser": 0,
"is_staff": 0,
"last_login": "2010-06-29 10:20:21",
"groups": [1],
"user_permissions": [],
"password": "sha1$d0fcb$661bd5197214051ed4de6da4ecdabe17f5549c7c",
"email": "user118577@nowhere",
"date_joined": "2010-06-29 10:20:21"
}
},
{
"pk": 180054,
"model": "auth.user",
"fields": {
"username": "AnonymousUser",
"first_name": "",
"last_name": "",
"is_active": 1,
"is_superuser": 0,
"is_staff": 0,
"last_login": "2010-08-13 15:04:35",
"groups": [],
"user_permissions": [],
"password": "sha1$d0fcb$661bd5197214051ed4de6da4ecdabe17f5549c7c",
"email": "user180054@nowhere",
"date_joined": "2010-06-10 14:08:53"
}
},
{
"pk": 1,
"model": "users.profile",
"fields": {
"user": 1
}
},
{
"pk": 2,
"model": "users.profile",
"fields": {
"user": 2
}
},
{
"pk": 47963,
"model": "users.profile",
"fields": {
"user": 47963
}
},
{
"pk": 118533,
"model": "users.profile",
"fields": {
"user": 118533
}
},
{
"pk": 118577,
"model": "users.profile",
"fields": {
"user": 118577
}
},
{
"pk": 180054,
"model": "users.profile",
"fields": {
"user": 180054
}
}
]

132
apps/users/forms.py Normal file
Просмотреть файл

@ -0,0 +1,132 @@
import re
from django import forms
from django.conf import settings
from django.contrib.auth import authenticate, forms as auth_forms
from django.contrib.auth.models import User
from tower import ugettext as _, ugettext_lazy as _lazy
from users.models import Profile
USERNAME_INVALID = _lazy(u'Username may contain only letters, '
'numbers and @/./+/-/_ characters.')
USERNAME_REQUIRED = _lazy(u'Please enter a username.')
USERNAME_SHORT = _lazy(u'Username is too short (%(show_value)s characters). '
'It must be at least %(limit_value)s characters.')
USERNAME_LONG = _lazy(u'Username is too long (%(show_value)s characters). '
'It must be %(limit_value)s characters or less.')
EMAIL_INVALID = _lazy(u'Please enter a valid email address.')
PASSWD_REQUIRED = _lazy(u'Please enter a valid password.')
#PASSWD2_REQUIRED = _lazy(u'Please enter your password twice.')
class RegisterForm(forms.ModelForm):
"""A user registration form that detects duplicate email addresses.
The default Django user creation form does not require email addresses
to be unique. This form does, and sets a minimum length
for usernames.
"""
username = forms.RegexField(max_length=30, min_length=4,
regex=r'^[\w.@+-]+$',
error_messages={'invalid': USERNAME_INVALID,
'required': USERNAME_REQUIRED,
'min_length': USERNAME_SHORT,
'max_length': USERNAME_LONG})
password = forms.CharField(error_messages={'required': PASSWD_REQUIRED})
email = forms.EmailField(error_messages={'invalid': EMAIL_INVALID},
required=False)
# password2 = forms.CharField(error_messages={'required': PASSWD2_REQUIRED})
newsletter = forms.BooleanField(required=False)
class Meta(object):
model = User
#fields = ('username', 'password', 'password2', 'email')
fields = ('username', 'password', 'email')
def clean(self):
super(RegisterForm, self).clean()
password = self.cleaned_data.get('password')
#password2 = self.cleaned_data.get('password2')
#if not password == password2:
# raise forms.ValidationError(_('Passwords must match.'))
return self.cleaned_data
def clean_email(self):
email = self.cleaned_data['email']
if email and User.objects.filter(email=email).exists():
raise forms.ValidationError(_('A user with that email address '
'already exists.'))
return email
def __init__(self, request=None, *args, **kwargs):
super(RegisterForm, self).__init__(request, auto_id='id_for_%s',
*args, **kwargs)
class AuthenticationForm(auth_forms.AuthenticationForm):
"""Overrides the default django form.
* Doesn't prefill password on validation error.
"""
password = forms.CharField(label=_lazy(u"Password"),
widget=forms.PasswordInput(render_value=False))
def clean(self):
username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password')
if username and password:
self.user_cache = authenticate(username=username,
password=password)
if self.user_cache is None:
raise forms.ValidationError(
_('Please enter a correct username and password. Note '
'that both fields are case-sensitive.'))
if self.request:
if not self.request.session.test_cookie_worked():
raise forms.ValidationError(
_("Your Web browser doesn't appear to have cookies "
"enabled. Cookies are required for logging in."))
return self.cleaned_data
class EmailConfirmationForm(forms.Form):
"""A simple form that requires an email address."""
email = forms.EmailField(label=_lazy(u'Email address:'))
class EmailChangeForm(forms.Form):
"""A simple form that requires a password and an email address.
It validates that it's the correct password for the current user and that it is not
the current user's email.
"""
password = forms.CharField(label=_lazy(u"Password:"),
widget=forms.PasswordInput(render_value=False,
attrs={'placeholder':_lazy(u'Password')}))
new_email = forms.EmailField(label=_lazy(u'New email address:'),
widget=forms.TextInput(attrs={'placeholder':_lazy(u'Email address')}))
def __init__(self, user, *args, **kwargs):
super(EmailChangeForm, self).__init__(*args, **kwargs)
self.user = user
def clean_new_email(self):
password = self.cleaned_data['password']
new_email = self.cleaned_data['new_email']
if not self.user.check_password(password):
raise forms.ValidationError(_('Please enter a correct password.'))
if self.user.email == new_email:
raise forms.ValidationError(_('This is your current email.'))
if User.objects.filter(email=new_email).exists():
raise forms.ValidationError(_('A user with that email address '
'already exists.'))
return self.cleaned_data['new_email']
## TODO : PasswordChangeForm, PasswordConfirmation

42
apps/users/models.py Normal file
Просмотреть файл

@ -0,0 +1,42 @@
from django.db import models
from django.contrib.auth.models import User
from tower import ugettext as _
from tower import ugettext_lazy as _lazy
from mptt.models import MPTTModel
from spark.models import City
class Profile(models.Model):
user = models.OneToOneField(User, primary_key=True,
verbose_name=_lazy(u'User'))
level = models.PositiveIntegerField(default=1,
verbose_name=_lazy(u'Level'))
latitude = models.FloatField(blank=True, null=True)
longitude = models.FloatField(blank=True, null=True)
city = models.ForeignKey(City, null=True)
def __unicode__(self):
return unicode(self.user)
class UserNode(MPTTModel):
"""
Represents a user in the Spark sharing hierarchy.
This model is mainly used for calculating chains of shares.
"""
user = models.OneToOneField(User, related_name='node',
verbose_name=_lazy(u'User'))
parent = models.ForeignKey('self', default=None, blank=True, null=True,
related_name='children')
class Meta:
db_table='users_tree'
class MPTTMeta:
pass
def __unicode__(self):
return unicode(self.user)

Просмотреть файл

@ -0,0 +1,29 @@
{% extends "desktop/base.html" %}
{% set styles = ('users',) %}
{% set scripts = ('users',) %}
{% block side_top %}
{% if not profile and user.is_authenticated() %}
{% set profile = user.get_profile() %}
{% endif %}
{% if profile %}
<nav id="doc-tabs">
<ul>{# If form is set, we're editing #}
<li{% if not form %} class="active"{% endif %}>
<a href="{{ profile_url(profile.user) }}">
{% if user == profile.user %}
{{ _('My profile') }}
{% else %}
{{ display_name(profile.user) }}
{% endif %}
</a>
</li>
{% if request.user == profile.user %}
<li{% if form %} class="active"{% endif %}>
<a href="{{ url('users.edit_profile') }}">{{ _('Edit my profile') }}</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% endblock %}

Просмотреть файл

@ -0,0 +1,24 @@
{% extends "users/desktop/base.html" %}
{% from "macros/errorlist.html" import errorlist %}
{% set title = _('Change email address') %}
{% set classes = 'password' %}
{% block content %}
<article id="pw-reset" class="main">
<h1>{{ title }}</h1>
<form method="post" action="">
{{ csrf() }}
<fieldset>
<p class="instruct">
{{ _('Your current email address is: ') }} <span id="email">{{ request.user.email }}</span>
</p>
<ul>
{{ form.as_ul()|safe }}
</ul>
</fieldset>
<div class="submit">
<input type="submit" value="{{ _('Change my email') }}" />
</div>
</form>
</article>
{% endblock content %}

Просмотреть файл

@ -0,0 +1,22 @@
{% extends "users/desktop/base.html" %}
{% if duplicate %}
{% set title = _('Unable to change email for user {username}')|f(username=username) %}
{% else %}
{% set title = _('Email changed for user {username}')|f(username=username) %}
{% endif %}
{% set classes = 'register' %}
{% block content %}
<article id="register" class="main">
<h1>{{ title }}</h1>
<p>
{% if duplicate %}
{{ _('The email you entered ({new_email}) already exists in the system. The current email for your account is still {old_email}.')|f(
old_email=old_email, new_email=new_email) }}
{% else %}
{{ _('Your email has been changed from {old_email} to {new_email}.')|f(
old_email=old_email, new_email=new_email) }}
{% endif %}
</p>
</article>
{% endblock %}

Просмотреть файл

@ -0,0 +1,10 @@
{% extends "users/desktop/base.html" %}
{% set title = _('Log In') %}
{% set classes = 'login' %}
{% block content %}
<article id="login" class="main">
<h1>{{ _('Log In') }}</h1>
{% include "users/desktop/login_form.html" %}
</article>
{% endblock %}

Просмотреть файл

@ -0,0 +1,27 @@
{% from "macros/errorlist.html" import errorlist %}
{{ errorlist(form) }}
<form method="post" action="{{ url('users.login') }}">
{{ csrf() }}
<input type="hidden" name="next" value="{{ next_url }}" />
<fieldset>
<ul>
<li>
<label for="id_username">{{ _('Username:') }}</label>
{{ form.username|safe }}
</li>
<li>
<label for="id_password">{{ _('Password:') }}</label>
{{ form.password|safe }}
</li>
</ul>
</fieldset>
<div class="submit">
<input type="submit" value="{{ _('Log in') }}" />
</div>
</form>
<div id="login-help">
<h3>{{ _('Login Problems?') }}</h3>
<ul>
<li><a href="{{ url('users.pw_reset') }}">{{ _('I forgot my password.') }}</a></li>
</ul>
</div>

Просмотреть файл

@ -0,0 +1,21 @@
{% extends "users/desktop/base.html" %}
{% from "macros/errorlist.html" import errorlist %}
{% set title = _('Change Password') %}
{% set classes = 'password' %}
{% block content %}
<article id="pw-change" class="main">
<h1>{{ title }}</h1>
<form method="post" action="">
{{ csrf() }}
<fieldset>
<ul>
{{ form.as_ul()|safe }}
</ul>
</fieldset>
<div class="submit">
<input type="submit" value="{{ _('Change my password') }}" />
</div>
</form>
</article>
{% endblock content %}

Просмотреть файл

@ -0,0 +1,12 @@
{% extends "users/desktop/base.html" %}
{% set title = _('Password changed successful!') %}
{% set classes = 'password' %}
{% block content %}
<article id="pw-reset" class="main">
<h1>{{ title }}</h1>
<p>{% trans profile_url=url('users.edit_profile') %}You have successfully changed your password.
<a href="{{ profile_url }}">Go back to edit profile page.</a>{% endtrans %}
</p>
</article>
{% endblock content %}

Просмотреть файл

@ -0,0 +1,15 @@
{% load i18n %}{# L10n: This is an email. Whitespace matters! #}{% blocktrans %}
{{ domain }} Password Reset
A request was received to reset the password for this account on
{{ site_name }}. To change this password please click on the following link,
or paste it into your browser's location bar:
{{ protocol }}://{{ domain }}/users/pwreset/{{ uid }}/{{ token }}
If you did not request this email there is no need for further action.
Thanks,
The {{ site_name }} team
{% endblocktrans %}

Просмотреть файл

@ -0,0 +1,38 @@
{% extends "mobile/page.html" %}
{% from "mobile/macros/errorlist.html" import errorlist %}
{% set title = _('Spark! Log in') %}
{% set pagetitle = _('Log in') %}
{% set body_id = 'login' %}
{% set scripts = ('menu',) %}
{% set hide_menu = True %}
{% block pagecontent %}
<div id="content">
<p class="section legend">
{{ _('Welcome back! Ready to keep your Spark going strong? Just fill your deets in to log in.') }}
</p>
<hr>
{{ errorlist(form) }}
<form action="" method="post" accept-charset="utf-8">
{{ csrf() }}
<fieldset id="account" class="section">
<div class="input-wrapper">
<input tabindex="1" type="text" name="username" value="{{ form.username.data|safe|replace('None','') }}" placeholder="Username" required>
</div>
<div class="input-wrapper">
<input tabindex="2" type="text" name="password" value="" placeholder="Password" required>
</div>
</fieldset>
<hr>
<div class="buttons-wrapper">
<div class="button left-button"><a href="{{ url('mobile.home') }}">{{ _('Cancel') }}</a></div>
<button ontouchstart="" tabindex="3" class="right-button" type="submit">{{ _('Log in') }}</button>
</div>
</form>
<hr class="clear">
<h2 class="cta"><a ontouchstart="" href="#">{{ _('Forgot your password?') }}</a></h2>
<hr>
</div> <!-- end content -->
{% endblock %}

Просмотреть файл

@ -0,0 +1,10 @@
{% extends "mobile/page.html" %}
{% set title = _('Password successfully reset.') %}
{% set classes = 'password' %}
{% block content %}
<article id="pw-reset" class="main">
<h1>{{ title }}</h1>
<p>{{ _('You can now log in.') }}</p>
</article>
{% endblock content %}

Просмотреть файл

@ -0,0 +1,34 @@
{% extends "mobile/page.html" %}
{% from "mobile/macros/errorlist.html" import errorlist %}
{% set title = _('Password Reset') %}
{% set classes = 'password' %}
{% block content %}
<article id="pw-reset" class="main">
{% if validlink %}
<h1>{{ title }}</h1>
<form method="post" action="">
{{ csrf() }}
<fieldset>
{{ errorlist(form) }}
<ul>
<li>
<label for="id_new_password1">{{ _('New password') }}</label>
{{ form.new_password1|safe }}
</li>
<li>
<label for="id_new_password2">{{ _('Confirm password') }}</label>
{{ form.new_password2|safe }}
</li>
</ul>
</fieldset>
<div class="submit">
<input type="submit" value="{{ _('Update') }}" />
</div>
</form>
{% else %}
<h1>{{ _('Password reset unsuccessful') }}</h1>
<p>{{ _('The password reset link was invalid, possibly because it has already been used. Please request a new password reset.') }}</p>
{% endif %}
</article>
{% endblock content %}

Просмотреть файл

@ -0,0 +1,30 @@
{% extends "users/mobile/base.html" %}
{% from "mobile/macros/errorlist.html" import errorlist %}
{% set title = _('Password Reset') %}
{% set classes = 'password' %}
{% block content %}
<article id="pw-reset" class="main">
<h1>{{ title }}</h1>
<form method="post" action="">
{{ csrf() }}
<fieldset>
<p class="instruct">
{% trans %} Forgotten your password? Enter your email address below,
and we'll send you instructions for setting a new one.
{% endtrans %}
</p>
{{ errorlist(form) }}
<ul>
<li>
<label for="id_email">{{ _('Email Address:') }}</label>
{{ form.email|safe }}
</li>
</ul>
</fieldset>
<div class="submit">
<input type="submit" value="{{ _('Send password reset link') }}" />
</div>
</form>
</article>
{% endblock content %}

Просмотреть файл

@ -0,0 +1,16 @@
{% extends "mobile/page.html" %}
{% from "mobile/macros/errorlist.html" import errorlist %}
{% set title = _('Password Reset') %}
{% set classes = 'password' %}
{% block content %}
<article id="pw-reset" class="main">
<h1>{{ title }}</h1>
<p>
{% trans %}
We've sent an email to any account using this address.
Please follow the link in the email to reset your password.
{% endtrans %}
</p>
</article>
{% endblock content %}

Просмотреть файл

@ -0,0 +1,43 @@
{% extends "mobile/page.html" %}
{% from "mobile/macros/errorlist.html" import errorlist %}
{% set title = _('Spark! Get Started') %}
{% set pagetitle = _('Get Started') %}
{% set body_id = 'getstarted' %}
{% set scripts = ('menu',) %}
{% set hide_menu = True %}
{% block pagecontent %}
<div id="content">
<hr>
{{ errorlist(form) }}
<form id="signup" action="" method="post" accept-charset="utf-8">
{{ csrf() }}
<fieldset id="account" class="section">
<div class="input-wrapper">
<input tabindex="1" type="text" name="username" value="{{ form.username.data|safe|replace('None','') }}" placeholder="{{ _('Username') }}" required>
</div>
<div class="input-wrapper">
<input tabindex="2" type="password" name="password" value="" placeholder="{{ _('Password') }}" required>
</div>
{{ _('Optional') }}
<div class="input-wrapper">
<input tabindex="3" type="email" name="email" value="{{ form.email.data|safe|replace('None','') }}" placeholder="{{ _('Email address') }}" required>
</div>
{{ _('Email is required to retrieve a lost password') }}
</fieldset>
<hr>
<fieldset id="newsletter">
<p id="custom-cb" class="sans">
<input tabindex="4" id="newsletter-cb" type="checkbox" name="newsletter" value="{{ form.newsletter.data }}">
<label ontouchstart="" for="newsletter-cb">{{ _('Wanna sign up to our newsletter ?') }}</label>
</p>
</fieldset>
<hr>
<div class="buttons-wrapper">
<div class="button left-button"><a href="{{ url('mobile.home') }}">{{ _('Cancel') }}</a></div>
<button ontouchstart="" tabindex="5" class="right-button" type="submit">{{ _('Join') }}</button>
</div>
</form>
</div> <!-- end content -->
{% endblock %}

Просмотреть файл

@ -0,0 +1,14 @@
{% extends "mobile/page.html" %}
{% set title = _('Spark! Get Started') %}
{% set pagetitle = _('Get Started') %}
{% set body_id = 'getstarted' %}
{% set scripts = ('menu',) %}
{% set hide_menu = True %}
{% block pagecontent %}
<article id="register" class="main">
<h1>{{ _('Thank you for registering!') }}</h1>
{# L10n: This string appears on the 'thank you for registering' page. #}
<p>{% trans %}Thank you for being awesome!{% endtrans %}</p>
</article>
{% endblock %}

Просмотреть файл

@ -0,0 +1,21 @@
from spark.tests import LocalizingClient, TestCase
from users.models import Profile
class TestCaseBase(TestCase):
"""Base TestCase for the users app test cases."""
def setUp(self):
super(TestCaseBase, self).setUp()
self.client = LocalizingClient()
def profile(user, **kwargs):
"""Return a saved profile for a given user."""
defaults = { 'user': user }
defaults.update(kwargs)
p = Profile(**defaults)
p.save()
return p

Просмотреть файл

@ -0,0 +1,25 @@
import re
from django.contrib.auth.models import User
from django.forms import ValidationError
from nose.tools import eq_
from pyquery import PyQuery as pq
from users.forms import RegisterForm, EmailChangeForm
from users.tests import TestCaseBase
class RegisterFormTestCase(TestCaseBase):
pass #TODO
class EmailChangeFormTestCase(TestCaseBase):
fixtures = ['users.json']
def test_correct_password(self):
user = User.objects.get(username='rrosario')
assert user.is_active
form = EmailChangeForm(user, data={'password': 'wrongpass',
'new_email': 'new_email@example.com'})
assert not form.is_valid()

Просмотреть файл

@ -0,0 +1,17 @@
from django.contrib.auth.models import User
from nose.tools import eq_
from spark.tests import TestCase
from users.tests import profile
class ProfileTestCase(TestCase):
fixtures = ['users.json']
def test_user_get_profile(self):
"""user.get_profile() returns what you'd expect."""
user = User.objects.all()[0]
p = profile(user)
eq_(p, user.get_profile())

Просмотреть файл

@ -0,0 +1,285 @@
from copy import deepcopy
import hashlib
import os
import json
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.models import Site
from django.core import mail
from django.core.files import File
from django.utils.http import int_to_base36
import mock
from nose.tools import eq_
from pyquery import PyQuery as pq
from test_utils import RequestFactory
from spark.urlresolvers import reverse
from spark.helpers import urlparams
from spark.tests import post
from users.models import Profile
from users.tests import TestCaseBase
from users.views import _clean_next_url
class LoginTests(TestCaseBase):
"""Login tests."""
fixtures = ['users.json']
def setUp(self):
super(LoginTests, self).setUp()
self.orig_debug = settings.DEBUG
settings.DEBUG = True
def tearDown(self):
super(LoginTests, self).tearDown()
settings.DEBUG = self.orig_debug
def test_login_bad_password(self):
'''Test login with a good username and bad password.'''
response = post(self.client, 'users.login',
{'username': 'rrosario', 'password': 'foobar'})
eq_(200, response.status_code)
doc = pq(response.content)
eq_('Please enter a correct username and password. Note that both '
'fields are case-sensitive.', doc('ul.errorlist li').text())
def test_login_bad_username(self):
'''Test login with a bad username.'''
response = post(self.client, 'users.login',
{'username': 'foobarbizbin', 'password': 'testpass'})
eq_(200, response.status_code)
doc = pq(response.content)
eq_('Please enter a correct username and password. Note that both '
'fields are case-sensitive.', doc('ul.errorlist li').text())
def test_login(self):
'''Test a valid login.'''
response = self.client.post(reverse('users.login'),
{'username': 'rrosario',
'password': 'testpass'})
eq_(302, response.status_code)
eq_('http://testserver' +
reverse('desktop.home', locale=settings.LANGUAGE_CODE),
response['location'])
def test_login_next_parameter(self):
'''Test with a valid ?next=url parameter.'''
next = '/kb/new'
# Verify that next parameter is set in form hidden field.
response = self.client.get(urlparams(reverse('users.login'),
next=next))
eq_(200, response.status_code)
doc = pq(response.content)
eq_(next, doc('input[name="next"]')[0].attrib['value'])
# Verify that it gets used on form POST.
response = self.client.post(reverse('users.login'),
{'username': 'rrosario',
'password': 'testpass',
'next': next})
eq_(302, response.status_code)
eq_('http://testserver' + next, response['location'])
@mock.patch_object(Site.objects, 'get_current')
def test_clean_url(self, get_current):
'''Verify that protocol and domain get removed.'''
get_current.return_value.domain = 'su.mo.com'
r = RequestFactory().post('/users/login',
{'next': 'https://su.mo.com/kb/new?f=b'})
eq_('/kb/new?f=b', _clean_next_url(r))
r = RequestFactory().post('/users/login',
{'next': 'http://su.mo.com/kb/new'})
eq_('/kb/new', _clean_next_url(r))
@mock.patch_object(Site.objects, 'get_current')
def test_login_invalid_next_parameter(self, get_current):
'''Test with an invalid ?next=http://example.com parameter.'''
get_current.return_value.domain = 'testserver.com'
invalid_next = 'http://foobar.com/evil/'
valid_next = reverse('desktop.home', locale=settings.LANGUAGE_CODE)
# Verify that _valid_ next parameter is set in form hidden field.
response = self.client.get(urlparams(reverse('users.login'),
next=invalid_next))
eq_(200, response.status_code)
doc = pq(response.content)
eq_(valid_next, doc('input[name="next"]')[0].attrib['value'])
# Verify that it gets used on form POST.
response = self.client.post(reverse('users.login'),
{'username': 'rrosario',
'password': 'testpass',
'next': invalid_next})
eq_(302, response.status_code)
eq_('http://testserver' + valid_next, response['location'])
def test_login_legacy_password(self):
'''Test logging in with a legacy md5 password.'''
legacypw = 'legacypass'
# Set the user's password to an md5
user = User.objects.get(username='rrosario')
user.password = hashlib.md5(legacypw).hexdigest()
user.save()
# Log in and verify that it's updated to a SHA-256
response = self.client.post(reverse('users.login'),
{'username': 'rrosario',
'password': legacypw})
eq_(302, response.status_code)
user = User.objects.get(username='rrosario')
assert user.password.startswith('sha256$')
# Try to log in again.
response = self.client.post(reverse('users.login'),
{'username': 'rrosario',
'password': legacypw})
eq_(302, response.status_code)
class PasswordReset(TestCaseBase):
fixtures = ['users.json']
def setUp(self):
super(PasswordReset, self).setUp()
self.user = User.objects.get(username='rrosario')
self.user.email = 'valid@email.com'
self.user.save()
self.uidb36 = int_to_base36(self.user.id)
self.token = default_token_generator.make_token(self.user)
self.orig_debug = settings.DEBUG
settings.DEBUG = True
def tearDown(self):
super(PasswordReset, self).tearDown()
settings.DEBUG = self.orig_debug
def test_bad_email_ajax(self):
r = self.client.post(reverse('users.pw_reset'),
{'email': 'foo@bar.com'})
eq_(200, r.status_code)
eq_(True, json.loads(r.content)['pw_reset_sent'])
eq_(0, len(mail.outbox))
def test_bad_email_mobile(self):
r = self.client.post(reverse('users.mobile_pw_reset'),
{'email': 'foo@bar.com'})
eq_(302, r.status_code)
eq_('http://testserver/en-US/m/pwresetsent', r['location'])
eq_(0, len(mail.outbox))
@mock.patch_object(Site.objects, 'get_current')
def test_success(self, get_current):
get_current.return_value.domain = 'testserver.com'
r = self.client.post(reverse('users.mobile_pw_reset'),
{'email': self.user.email})
eq_(302, r.status_code)
eq_('http://testserver/en-US/m/pwresetsent', r['location'])
eq_(1, len(mail.outbox))
assert mail.outbox[0].subject.find('Password reset') == 0
assert mail.outbox[0].body.find('pwreset/%s' % self.uidb36) > 0
def _get_mobile_reset_url(self):
return reverse('users.mobile_pw_reset_confirm',
args=[self.uidb36, self.token])
def test_bad_reset_url(self):
r = self.client.get('/m/pwreset/junk/', follow=True)
eq_(r.status_code, 404)
r = self.client.get(reverse('users.mobile_pw_reset_confirm',
args=[self.uidb36, '12-345']))
eq_(200, r.status_code)
doc = pq(r.content)
eq_('Password reset unsuccessful', doc('article h1').text())
def test_reset_fail(self):
url = self._get_mobile_reset_url()
r = self.client.post(url, {'new_password1': '', 'new_password2': ''})
eq_(200, r.status_code)
doc = pq(r.content)
eq_(1, len(doc('ul.errorlist')))
r = self.client.post(url, {'new_password1': 'one',
'new_password2': 'two'})
eq_(200, r.status_code)
doc = pq(r.content)
eq_("The two password fields didn't match.",
doc('ul.errorlist li').text())
def test_reset_success(self):
url = self._get_mobile_reset_url()
new_pw = 'fjdka387fvstrongpassword!'
assert self.user.check_password(new_pw) is False
r = self.client.post(url, {'new_password1': new_pw,
'new_password2': new_pw})
eq_(302, r.status_code)
eq_('http://testserver/en-US/m/pwresetcomplete', r['location'])
self.user = User.objects.get(username='rrosario')
assert self.user.check_password(new_pw)
# TODO add missing ajax tests
class PasswordChangeTests(TestCaseBase):
fixtures = ['users.json']
def setUp(self):
super(PasswordChangeTests, self).setUp()
self.user = User.objects.get(username='rrosario')
self.url = reverse('users.pw_change')
self.new_pw = 'fjdka387fvstrongpassword!'
self.client.login(username='rrosario', password='testpass')
def test_change_password(self):
assert self.user.check_password(self.new_pw) is False
r = self.client.post(self.url, {'old_password': 'testpass',
'new_password1': self.new_pw,
'new_password2': self.new_pw})
eq_(200, r.status_code)
eq_(True, json.loads(r.content)['pw_change_complete'])
self.user = User.objects.get(username='rrosario')
assert self.user.check_password(self.new_pw)
def test_bad_old_password(self):
r = self.client.post(self.url, {'old_password': 'testpqss',
'new_password1': self.new_pw,
'new_password2': self.new_pw})
eq_(200, r.status_code)
doc = pq(r.content)
eq_('Your old password was entered incorrectly. Please enter it '
'again.', doc('ul.errorlist').text())
def test_new_pw_doesnt_match(self):
r = self.client.post(self.url, {'old_password': 'testpqss',
'new_password1': self.new_pw,
'new_password2': self.new_pw + '1'})
eq_(200, r.status_code)
doc = pq(r.content)
eq_("Your old password was entered incorrectly. Please enter it "
"again. The two password fields didn't match.",
doc('ul.errorlist').text())
class EmailChangeTests(TestCaseBase):
fixtures = ['users.json']
def setUp(self):
super(EmailChangeTests, self).setUp()
self.url = reverse('users.change_email')
self.user = User.objects.get(username='rrosario')
self.client.login(username='rrosario', password='testpass')
def test_display_current_email(self):
"""Page should display the user's current email."""
r = self.client.get(self.url)
eq_(200, r.status_code)
doc = pq(r.content)
eq_("user118577@nowhere", doc('#email').text())

Просмотреть файл

@ -0,0 +1,139 @@
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.core import mail
import mock
from nose.tools import eq_
from pyquery import PyQuery as pq
from spark.tests import TestCase, LocalizingClient
from spark.urlresolvers import reverse
class RegisterTestCase(TestCase):
fixtures = ['users.json']
def setUp(self):
self.old_debug = settings.DEBUG
settings.DEBUG = True
self.client.logout()
def tearDown(self):
settings.DEBUG = self.old_debug
@mock.patch_object(Site.objects, 'get_current')
def test_new_user(self, get_current):
get_current.return_value.domain = 'su.mo.com'
response = self.client.post(reverse('users.mobile_register', locale='en-US'),
{'username': 'newbie',
'email': 'newbie@example.com',
'password': 'foo',
'password2': 'foo'}, follow=True)
eq_(200, response.status_code)
u = User.objects.get(username='newbie')
assert u.password.startswith('sha256')
# Now try to log in
u.save()
response = self.client.post(reverse('users.mobile_login', locale='en-US'),
{'username': 'newbie',
'password': 'foo'}, follow=True)
eq_(200, response.status_code)
eq_('http://testserver/en-US/m/home', response.redirect_chain[0][0])
@mock.patch_object(Site.objects, 'get_current')
def test_unicode_password(self, get_current):
u_str = u'\xe5\xe5\xee\xe9\xf8\xe7\u6709\u52b9'
get_current.return_value.domain = 'su.mo.com'
response = self.client.post(reverse('users.mobile_register', locale='en-US'),#locale='ja'),
{'username': 'cjkuser',
'email': 'cjkuser@example.com',
'password': u_str,
'password2': u_str}, follow=True)
eq_(200, response.status_code)
u = User.objects.get(username='cjkuser')
u.save()
assert u.password.startswith('sha256')
# make sure you can login now
response = self.client.post(reverse('users.mobile_login', locale='en-US'),#locale='ja'),
{'username': 'cjkuser',
'password': u_str}, follow=True)
eq_(200, response.status_code)
#eq_('http://testserver/ja/home', response.redirect_chain[0][0])
eq_('http://testserver/en-US/m/home', response.redirect_chain[0][0])
def test_duplicate_username(self):
response = self.client.post(reverse('users.mobile_register', locale='en-US'),
{'username': 'jsocol',
'email': 'newbie@example.com',
'password': 'foo',
'password2': 'foo'}, follow=True)
self.assertContains(response, 'already exists')
def test_duplicate_email(self):
User.objects.create(username='noob', email='noob@example.com').save()
response = self.client.post(reverse('users.mobile_register', locale='en-US'),
{'username': 'newbie',
'email': 'noob@example.com',
'password': 'foo',
'password2': 'foo'}, follow=True)
self.assertContains(response, 'already exists')
## Not sure yet if we need a password2 field
# def test_no_match_passwords(self):
# response = self.client.post(reverse('users.register', locale='en-US'),
# {'username': 'newbie',
# 'email': 'newbie@example.com',
# 'password': 'foo',
# 'password2': 'bar'}, follow=True)
# self.assertContains(response, 'must match')
class ChangeEmailTestCase(TestCase):
fixtures = ['users.json']
def setUp(self):
self.client = LocalizingClient()
self.url = reverse('users.change_email')
def test_user_change_email_same(self):
"""Changing to same email shows validation error."""
self.client.login(username='rrosario', password='testpass')
user = User.objects.get(username='rrosario')
user.email = 'valid@email.com'
user.save()
response = self.client.post(self.url,
{'password': 'testpass',
'new_email': user.email})
eq_(200, response.status_code)
doc = pq(response.content)
eq_('This is your current email.', doc('ul.errorlist').text())
def test_user_change_email_duplicate(self):
"""Changing to same email shows validation error."""
self.client.login(username='rrosario', password='testpass')
email = 'newvalid@email.com'
User.objects.filter(username='pcraciunoiu').update(email=email)
response = self.client.post(self.url,
{'password': 'testpass',
'new_email': email})
eq_(200, response.status_code)
doc = pq(response.content)
eq_('A user with that email address already exists.',
doc('ul.errorlist').text())
def test_user_enters_wrong_password(self):
"""Entering wrong password shows validation error."""
self.client.login(username='rrosario', password='testpass')
user = User.objects.get(username='rrosario')
user.email = 'valid@email.com'
user.save()
response = self.client.post(self.url,
{'password': 'wrongpass',
'new_email': 'new_email@example.com'})
eq_(200, response.status_code)
doc = pq(response.content)
eq_('Please enter a correct password.', doc('ul.errorlist').text())

51
apps/users/urls.py Normal file
Просмотреть файл

@ -0,0 +1,51 @@
from django.conf.urls.defaults import patterns, url, include
from spark.views import redirect_to
from users import views
desktop_patterns = patterns('',
# Login/logout (ajax)
url(r'^login$', views.login, name='users.login'),
url(r'^logout$', views.logout, name='users.logout'),
# Password reset (ajax)
url(r'^pwreset$', views.password_reset, name='users.pw_reset'),
url(r'^pwreset/(?P<uidb36>[-\w]+)/(?P<token>[-\w]+)$',
views.password_reset_confirm, name="users.pw_reset_confirm"),
# Change password (ajax)
url(r'^pwchange$', views.password_change, name='users.pw_change'),
# Change email (ajax)
url(r'^change_email$', views.change_email, name='users.change_email'),
# Delete account (ajax)
url(r'^delaccount$', views.delete_account, name='users.delete_account'),
)
opts = {'mobile': True}
mobile_patterns = patterns('',
# Login/logout
url(r'^login$', views.login, opts, name='users.mobile_login'),
url(r'^logout$', views.logout, opts, name='users.mobile_logout'),
# Sign up
url(r'^register$', views.register, name='users.mobile_register'),
# Forgot password
url(r'^pwreset$', views.password_reset, opts,
name='users.mobile_pw_reset'),
url(r'^pwreset/(?P<uidb36>[-\w]+)/(?P<token>[-\w]+)$',
views.password_reset_confirm, opts,
name='users.mobile_pw_reset_confirm'),
url(r'^pwresetsent$', views.password_reset_sent,
name='users.mobile_pw_reset_sent'),
url(r'^pwresetcomplete$', views.password_reset_complete,
name="users.mobile_pw_reset_complete"),
)
urlpatterns = patterns('',
url(r'^users/', include(desktop_patterns)),
url(r'^m/', include(mobile_patterns)),
)

37
apps/users/utils.py Normal file
Просмотреть файл

@ -0,0 +1,37 @@
from django.contrib import auth
from django.contrib.auth.models import User
from users.forms import RegisterForm, AuthenticationForm
from users.models import Profile
def handle_login(request):
auth.logout(request)
if request.method == 'POST':
form = AuthenticationForm(data=request.POST)
if form.is_valid():
auth.login(request, form.get_user())
if request.session.test_cookie_worked():
request.session.delete_test_cookie()
return form
request.session.set_test_cookie()
return AuthenticationForm()
def handle_register(request):
"""Handle to help registration."""
if request.method == 'POST':
form = RegisterForm(request.POST)
if form.is_valid():
username = form.cleaned_data['username']
email = form.cleaned_data['email']
password = form.cleaned_data['password']
new_user = User.objects.create_user(username, email, password)
new_user.save()
Profile.objects.create(user=new_user)
return form
return RegisterForm()

230
apps/users/views.py Normal file
Просмотреть файл

@ -0,0 +1,230 @@
import os
import urlparse
import json
from django.conf import settings
from django.contrib import auth
from django.contrib.auth.forms import (PasswordResetForm, SetPasswordForm,
PasswordChangeForm)
from django.contrib.auth.models import User
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.models import Site
from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.views.decorators.http import require_http_methods, require_GET
from django.shortcuts import get_object_or_404
from django.utils.http import base36_to_int
import jingo
from spark.urlresolvers import reverse
from spark.helpers import url
from spark.decorators import ssl_required, logout_required, login_required, post_required, json_view
from users.backends import Sha256Backend
from users.forms import (EmailConfirmationForm, AuthenticationForm, EmailChangeForm)
from users.models import Profile
from users.utils import handle_login, handle_register
@ssl_required
def login(request, mobile=False):
"""Try to log the user in."""
if mobile:
home_view_name = 'mobile.home'
login_template = 'users/mobile/login.html'
else:
home_view_name = 'desktop.home'
login_template = 'users/desktop/login.html'
next_url = _clean_next_url(request) or reverse(home_view_name)
form = handle_login(request)
if request.user.is_authenticated():
return HttpResponseRedirect(next_url)
return jingo.render(request, login_template,
{'form': form, 'next_url': next_url})
@ssl_required
def logout(request, mobile=False):
"""Log the user out."""
auth.logout(request)
next_url = _clean_next_url(request) if 'next' in request.GET else ''
home_view = 'mobile.home' if mobile else 'desktop.home'
return HttpResponseRedirect(next_url or reverse(home_view))
@ssl_required
@logout_required
@require_http_methods(['GET', 'POST'])
def register(request):
"""Register a new user."""
form = handle_register(request)
if form.is_valid():
return jingo.render(request, 'users/mobile/register_done.html')
return jingo.render(request, 'users/mobile/register.html',
{'form': form})
@login_required
@require_http_methods(['GET', 'POST'])
def change_email(request):
"""Change user's email."""
if request.method == 'POST':
form = EmailChangeForm(request.user, request.POST)
u = request.user
if form.is_valid() and u.email != form.cleaned_data['new_email']:
return jingo.render(request,
'users/desktop/change_email_done.html',
{'new_email': form.cleaned_data['new_email']})
else:
form = EmailChangeForm(request.user,
initial={'email': request.user.email})
return jingo.render(request, 'users/desktop/change_email.html',
{'form': form})
@post_required
@json_view
def password_reset(request, mobile=False):
"""Password reset form.
Based on django.contrib.auth.views. This view sends the email.
"""
if request.method == "POST":
form = PasswordResetForm(request.POST)
if form.is_valid():
form.save(use_https=request.is_secure(),
token_generator=default_token_generator,
email_template_name='users/email/pw_reset.ltxt')
# Don't leak existence of email addresses
# (No error if wrong email address)
if mobile:
return HttpResponseRedirect(reverse('users.mobile_pw_reset_sent'))
else:
return {'pw_reset_sent': True}
else:
form = PasswordResetForm()
if mobile:
return jingo.render(request, 'users/mobile/pw_reset_form.html', {'form': form})
# else:
# return http.HttpResponse(json.dumps(response),
# content_type='application/json')
def password_reset_sent(request):
"""Password reset email sent.
Based on django.contrib.auth.views. This view shows a success message after
email is sent.
"""
return jingo.render(request, 'users/mobile/pw_reset_sent.html')
@ssl_required
@json_view
def password_reset_confirm(request, uidb36=None, token=None, mobile=False):
"""View that checks the hash in a password reset link and presents a
form for entering a new password.
Based on django.contrib.auth.views.
"""
try:
uid_int = base36_to_int(uidb36)
except ValueError:
raise Http404
user = get_object_or_404(User, id=uid_int)
context = {}
if default_token_generator.check_token(user, token):
context['validlink'] = True
if request.method == 'POST':
form = SetPasswordForm(user, request.POST)
if form.is_valid():
form.save()
if mobile:
return HttpResponseRedirect(reverse('users.mobile_pw_reset_complete'))
else:
return {'pw_reset_complete': True}
else:
form = SetPasswordForm(None)
else:
context['validlink'] = False
form = None
context['form'] = form
return jingo.render(request, 'users/mobile/pw_reset_confirm.html', context)
def password_reset_complete(request):
"""Password reset complete.
Based on django.contrib.auth.views. Show a success message.
"""
form = AuthenticationForm()
return jingo.render(request, 'users/mobile/pw_reset_complete.html',
{'form': form})
@login_required
@json_view
def password_change(request):
"""Change password form page."""
if request.method == 'POST':
form = PasswordChangeForm(user=request.user, data=request.POST)
if form.is_valid():
form.save()
return {'pw_change_complete': True}
else:
form = PasswordChangeForm(user=request.user)
return jingo.render(request, 'users/desktop/pw_change.html', {'form': form})
@login_required
def password_change_complete(request):
"""Change password complete page."""
return jingo.render(request, 'users/desktop/pw_change_complete.html')
@login_required
def delete_account(request):
pass # TODO implement
def _clean_next_url(request):
if 'next' in request.POST:
url = request.POST.get('next')
elif 'next' in request.GET:
url = request.GET.get('next')
else:
url = request.META.get('HTTP_REFERER')
if url:
parsed_url = urlparse.urlparse(url)
# Don't redirect outside of Spark.
# Don't include protocol+domain, so if we are https we stay that way.
if parsed_url.scheme:
site_domain = Site.objects.get_current().domain
url_domain = parsed_url.netloc
if site_domain != url_domain:
url = None
else:
url = u'?'.join([getattr(parsed_url, x) for x in
('path', 'query') if getattr(parsed_url, x)])
# Don't redirect right back to login or logout page
if parsed_url.path in [settings.LOGIN_URL, settings.LOGOUT_URL]:
url = None
return url

37
bin/autol10n.sh Executable file
Просмотреть файл

@ -0,0 +1,37 @@
#!/bin/bash
# Automatically pull L10n dirs from SVN, compile, then push to git.
# Runs on all project dirs named *-autol10n.
# Settings
GIT=`/usr/bin/which git`
FIND=`/usr/bin/which find`
DEVDIR=$HOME/dev
# Update everything
for dir in `$FIND "$DEVDIR" -maxdepth 1 -name '*-autol10n'`; do
cd $dir
$GIT pull -q origin master
cd locale
$GIT svn rebase
# Compile .mo, commit if changed
./compile-mo.sh .
$FIND . -name '*.mo' -exec $GIT add {} \;
$GIT status
if [ $? -eq 0 ]; then
$GIT commit -m 'compiled .mo files (automatic commit)'
fi
# Push to SVN and git
$GIT svn dcommit && $GIT push -q origin master
cd ..
$GIT add locale
$GIT status locale
if [ $? -eq 0 ]; then
$GIT commit -m 'L10n update (automatic commit)'
$GIT push -q origin master
fi
done

19
bin/compile-mo.sh Executable file
Просмотреть файл

@ -0,0 +1,19 @@
#!/bin/bash
# syntax:
# compile-mo.sh locale-dir/
function usage() {
echo "syntax:"
echo "compile.sh locale-dir/"
exit 1
}
# check if file and dir are there
if [[ ($# -ne 1) || (! -d "$1") ]]; then usage; fi
for lang in `find $1 -type f -name "*.po"`; do
dir=`dirname $lang`
stem=`basename $lang .po`
msgfmt -o ${dir}/${stem}.mo $lang
done

123
bin/update_site.py Executable file
Просмотреть файл

@ -0,0 +1,123 @@
#!/usr/bin/env python
"""
Usage: update_site.py [options]
Updates a server's sources, vendor libraries, packages CSS/JS
assets, migrates the database, and other nifty deployment tasks.
Options:
-h, --help show this help message and exit
-e ENVIRONMENT, --environment=ENVIRONMENT
Type of environment. One of (prod|dev|stage) Example:
update_site.py -e stage
-v, --verbose Echo actions before taking them.
"""
import os
import sys
from textwrap import dedent
from optparse import OptionParser
# Constants
PROJECT = 0
VENDOR = 1
ENV_BRANCH = {
# 'environment': [PROJECT_BRANCH, VENDOR_BRANCH],
'dev': ['base', 'master'],
'stage': ['master', 'master'],
'prod': ['prod', 'master'],
}
GIT_PULL = "git pull -q origin %(branch)s"
GIT_SUBMODULE = "git submodule update --init"
SVN_UP = "svn update"
EXEC = 'exec'
CHDIR = 'chdir'
def update_site(env, debug):
"""Run through commands to update this site."""
error_updating = False
here = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
project_branch = {'branch': ENV_BRANCH[env][PROJECT]}
vendor_branch = {'branch': ENV_BRANCH[env][VENDOR]}
commands = [
(CHDIR, here),
(EXEC, GIT_PULL % project_branch),
(EXEC, GIT_SUBMODULE),
]
# Update locale dir if applicable
if os.path.exists(os.path.join(here, 'locale', '.svn')):
commands += [
(CHDIR, os.path.join(here, 'locale')),
(EXEC, SVN_UP),
(CHDIR, here),
]
elif os.path.exists(os.path.join(here, 'locale', '.git')):
commands += [
(CHDIR, os.path.join(here, 'locale')),
(EXEC, GIT_PULL % 'master'),
(CHDIR, here),
]
commands += [
(CHDIR, os.path.join(here, 'vendor')),
(EXEC, GIT_PULL % vendor_branch),
(EXEC, GIT_SUBMODULE),
(CHDIR, os.path.join(here)),
(EXEC, 'python vendor/src/schematic/schematic migrations/'),
(EXEC, 'python manage.py compress_assets'),
]
for cmd, cmd_args in commands:
if CHDIR == cmd:
if debug:
sys.stdout.write("cd %s\n" % cmd_args)
os.chdir(cmd_args)
elif EXEC == cmd:
if debug:
sys.stdout.write("%s\n" % cmd_args)
if not 0 == os.system(cmd_args):
error_updating = True
break
else:
raise Exception("Unknown type of command %s" % cmd)
if error_updating:
sys.stderr.write("There was an error while updating. Please try again "
"later. Aborting.\n")
def main():
""" Handels command line args. """
debug = False
usage = dedent("""\
%prog [options]
Updates a server's sources, vendor libraries, packages CSS/JS
assets, migrates the database, and other nifty deployment tasks.
""".rstrip())
options = OptionParser(usage=usage)
e_help = "Type of environment. One of (%s) Example: update_site.py \
-e stage" % '|'.join(ENV_BRANCH.keys())
options.add_option("-e", "--environment", help=e_help)
options.add_option("-v", "--verbose",
help="Echo actions before taking them.",
action="store_true", dest="verbose")
(opts, _) = options.parse_args()
if opts.verbose:
debug = True
if opts.environment in ENV_BRANCH.keys():
update_site(opts.environment, debug)
else:
sys.stderr.write("Invalid environment!\n")
options.print_help(sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()

130
docs/Makefile Normal file
Просмотреть файл

@ -0,0 +1,130 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/playdoh.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/playdoh.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/playdoh"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/playdoh"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
make -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

0
docs/_static/.gitignore поставляемый Normal file
Просмотреть файл

0
docs/_templates/.gitignore поставляемый Normal file
Просмотреть файл

75
docs/bestpractices.rst Normal file
Просмотреть файл

@ -0,0 +1,75 @@
.. _bestpractices:
==============
Best Practices
==============
This page lists several best practices for writing secure web applications
with playdoh.
``|safe`` considered harmful
----------------------------
Using something like ``mystring|safe`` in a template will prevent Jinja2 from
auto-escaping it. Sadly, this requires us to be really sure that ``mystring``
is not raw, user-entered data. Otherwise we introduce an XSS vulnerability.
``|safe`` is safe to use in cases where, for example, you have a localized
string that contains some HTML::
{{ _('Welcome to <strong>playdoh</strong>!')|safe }}
String interpolation
~~~~~~~~~~~~~~~~~~~~
When you *interpolate* data into such a string, do not use ``|f(...)|safe``.
The data could be unsafe. Instead, use the helper ``|fe(...)``. It will
escape all its arguments before doing string interpolation, then return
HTML that's safe to use::
{{ _('Welcome back, <strong>{username}</strong>!')|fe(username=user.display_name) }}
``|f(...)|safe`` is to be considered unsafe and should not pass code review.
If you interpolate into a base string that does *not contain HTML*, you may
keep on using ``|f(...)`` without ``|safe``, of course, as it will be
auto-escaped on output::
{{ _('Author name: {author}')|f(author=user.display_name) }}
Form fields
~~~~~~~~~~~
Jinja2, unlike Django templates, by default does not consider Django forms
"safe" to display. Thus, you'd use something like ``{{ form.myfield|safe }}``.
In order to minimize the use of ``|safe`` (and thus possible unsafe uses of
it), playdoh monkey-patches the Django forms framework so that form fields'
HTML representations are considered safe by Jinja2 as well. Therefore, the
following works as expected::
{{ form.myfield }}
Mmmmh, Cookies
--------------
Django's default way of setting a cookie is set_cookie_ on the HTTP response.
Unfortunately, both **secure** cookies (i.e., HTTPS-only) and **httponly**
(i.e., cookies not readable by JavaScript, if the browser supports it) are
disabled by default.
To be secure by default, we use commonware's ``cookies`` app. It makes secure
and httponly cookies the default, unless specifically requested otherwise.
To disable either of these patches, set ``COOKIES_SECURE = False`` or
``COOKIES_HTTPONLY = False`` in ``settings.py``.
You can exempt any cookie by passing ``secure=False`` or ``httponly=False`` to
the ``set_cookie`` call, respectively::
response.set_cookie('hello', value='world', secure=False, httponly=False)
.. _set_cookie: http://docs.djangoproject.com/en/dev/ref/request-response/#django.http.HttpResponse.set_cookie

30
docs/build-github.zsh Executable file
Просмотреть файл

@ -0,0 +1,30 @@
#!/bin/zsh
# Should be run from the docs directory: (cd docs && ./build-github.zsh)
REPO=$(git config remote.origin.url)
GH=_gh-pages
# Checkout the gh-pages branch, if necessary.
if [[ ! -d $GH ]]; then
git clone $REPO $GH
pushd $GH
git checkout -b gh-pages origin/gh-pages
popd
fi
# Update and clean out the _gh-pages target dir.
pushd $GH && git pull && rm -rf * && popd
# Make a clean build.
make clean dirhtml
# Move the fresh build over.
cp -r _build/dirhtml/* $GH
cd $GH
# Commit.
git add .
git commit -am "gh-pages build on $(date)"
git push origin gh-pages

220
docs/conf.py Normal file
Просмотреть файл

@ -0,0 +1,220 @@
# -*- coding: utf-8 -*-
#
# playdoh documentation build configuration file, created by
# sphinx-quickstart on Tue Jan 4 15:11:09 2011.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'playdoh'
copyright = u'2011, Mozilla'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '1.0'
# The full version, including alpha/beta/rc tags.
release = '1.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'playdohdoc'
# -- Options for LaTeX output --------------------------------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'playdoh.tex', u'playdoh Documentation',
u'Mozilla', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Additional stuff for the LaTeX preamble.
#latex_preamble = ''
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'playdoh', u'playdoh Documentation',
[u'Mozilla'], 1)
]
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'http://docs.python.org/': None}

81
docs/docs.rst Normal file
Просмотреть файл

@ -0,0 +1,81 @@
.. _docs:
=========================
Documentation with Sphinx
=========================
*(Borrowed from Zamboni)*
For a reStructuredText Primer, see http://sphinx.pocoo.org/rest.html.
Sections
--------
Sphinx doesn't care what punctuation you use for marking headings, but each new
kind that it sees means one level lower in the outline. So, ::
=========
Heading 1
=========
Heading 2
---------
will correspond to ::
<h1>Heading 1</h1>
<h2>Heading 2</h2>
For consistency, this is what I'm using for headings in Zamboni::
=========
Heading 1
=========
Heading 2
---------
Heading 3
~~~~~~~~~
Heading 4
*********
Heading 5
+++++++++
Heading 6
^^^^^^^^^
Use two newlines prior to a new heading. I'm only using one here for space
efficiency.
Sphinx Extras
-------------
Use ``:src:`path/to/file.py``` to link to source files online. Example:
:src:`settings.py`.
Vim
---
Here's a nice macro for creating headings::
let @h = "yypVr"
Compiling Documentation
-----------------------
Playdoh hosts its documentation on `github pages
<http://mozilla.github.com/playdoh/>`_. When you change the docs, make sure
they still build properly and look all right locally::
cd docs && make html && open _build/html/index.html
If they do, run a build and push it to gh-pages::
./build-github.zsh

39
docs/gettingstarted.rst Normal file
Просмотреть файл

@ -0,0 +1,39 @@
Getting started
===============
Requirements
------------
You need Python 2.6.
To check out playdoh, run::
git clone --recursive git://github.com/mozilla/playdoh.git
This project is set up to use a vendor library, i.e. a subdirectory ``vendor``
that contains all pure Python libraries required by this project. The recursive
checkout will also clone these requirements.
In addition, there are compiled libraries (such as Jinja2) that you will need
to build yourself, either by installing them from ``pypi`` or by using your
favorite package manager for your OS.
For development, you can run this in a `virtualenv environment`_::
easy_install pip
pip install -r requirements/compiled.txt
For more information on vendor libraries, read :ref:`packages`.
.. _virtualenv environment: http://pypi.python.org/pypi/virtualenv
Starting a project based on playdoh
-----------------------------------
The default branch of playdoh is ``base``. To start a new project, you fork
playdoh and start working on your app in ``master`` (branched from base). If
you start adding pieces that should go back into playdoh, you can apply the
patch to base and move it upstream.
Eventually you'll probably diverge enough that you'll want to delete the base
branch.

55
docs/index.rst Normal file
Просмотреть файл

@ -0,0 +1,55 @@
===================================
Welcome to playdoh's documentation!
===================================
**Mozilla's Playdoh** is a web application template based on Django_.
Patches are welcome! Feel free to fork and contribute to this project on
Github_.
.. _Django: http://www.djangoproject.com/
.. _Github: https://github.com/mozilla/playdoh
Features
--------
Quick and dirty (and probably incomplete) feature list:
* Rich, but "cherry-pickable" features out of the box:
* Django
* jinja2 template engine
* Celery support
* Simple database migrations
* Full localization support
* Secure by default:
* SHA-512 default password hashing
* X-Frame-Options: DENY by default
* secure and httponly flags on cookies enabled by default
Contents
--------
.. toctree::
:maxdepth: 1
gettingstarted
libs
operations
migrations
l10n_setup
l10n_update
packages
docs
bestpractices
Indices and tables
------------------
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше