Initial Spark commit
This commit is contained in:
Родитель
8aaf4e6e02
Коммит
6a3eb1e9de
|
@ -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/*
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "vendor"]
|
||||
path = vendor
|
||||
url = git://github.com/mozilla/playdoh-lib.git
|
|
@ -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.
|
|
@ -0,0 +1,4 @@
|
|||
Spark
|
||||
=====
|
||||
|
||||
Spark desktop and mobile campaign websites
|
|
@ -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',
|
||||
}
|
|
@ -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,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,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 we’ll 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 %}
|
||||
You’re 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 %}
|
|
@ -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'),
|
||||
)
|
|
@ -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,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, you’ll 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">
|
||||
{{ _('Don’t worry, we don’t share any details about your information with anyone. Everything you tell us is kept private so you’re 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 you’ll unlock and the more badges you’ll 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, you’ll 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 %}
|
|
@ -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'),
|
||||
)
|
|
@ -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,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',
|
||||
}
|
|
@ -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')
|
||||
|
||||
|
|
@ -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()
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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'}),
|
||||
)
|
|
@ -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,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)
|
|
@ -0,0 +1 @@
|
|||
## User registration based on kitsune/apps/users/
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -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
|
|
@ -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())
|
||||
|
|
@ -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)),
|
||||
)
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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()
|
|
@ -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,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
|
|
@ -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
|
|
@ -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}
|
|
@ -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
|
|
@ -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.
|
|
@ -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`
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче