Commit 029fbfce by Owo Sugiana

Add forgot password

1 parent 7bbfdd64
0.1.1
-----
- Forgot password.
0.1 0.1
--- ---
......
Web Starter Web Starter
=========== ===========
Getting Started
---------------
Change directory into your newly created project:: Change directory into your newly created project::
cd web-starter cd web-starter
Create a Python virtual environment:: Create a Python virtual environment::
python3 -m venv ../env python3 -m venv ../env
Upgrade packaging tools:: Upgrade packaging tools::
../env/bin/pip install --upgrade pip setuptools ../env/bin/pip install --upgrade pip setuptools
Install required package:: Install required package::
../env/bin/python setup.py develop-use-pip ../env/bin/python setup.py develop-use-pip
Set sqlalchemy.url on development.ini and create tables:: Set sqlalchemy.url on development.ini and create tables::
../env/bin/initialize_web_starter_db development.ini ../env/bin/initialize_web_starter_db development.ini
Run your project:: Run your project::
......
...@@ -18,6 +18,12 @@ sqlalchemy.url = postgresql://user:pass@localhost/dbname ...@@ -18,6 +18,12 @@ sqlalchemy.url = postgresql://user:pass@localhost/dbname
timezone = Asia/Jakarta timezone = Asia/Jakarta
localization = id_ID.UTF-8 localization = id_ID.UTF-8
mail.host = localhost
mail.port = 25
mail.username = user@example.com
mail.password = FIXME
mail.sender_name = Example Name
# By default, the toolbar only appears for clients from IP addresses # By default, the toolbar only appears for clients from IP addresses
# '127.0.0.1' and '::1'. # '127.0.0.1' and '::1'.
# debugtoolbar.hosts = 127.0.0.1 ::1 # debugtoolbar.hosts = 127.0.0.1 ::1
......
NAME="$1"
msgfmt -o web_starter/locale/id/LC_MESSAGES/$NAME.mo web_starter/locale/id/LC_MESSAGES/$NAME.po
...@@ -16,6 +16,12 @@ sqlalchemy.url = postgresql://user:pass@localhost/dbname ...@@ -16,6 +16,12 @@ sqlalchemy.url = postgresql://user:pass@localhost/dbname
timezone = Asia/Jakarta timezone = Asia/Jakarta
localization = id_ID.UTF-8 localization = id_ID.UTF-8
mail.host = localhost
mail.port = 25
mail.username = user@example.com
mail.password = FIXME
mail.sender_name = Example Name
### ###
# wsgi server configuration # wsgi server configuration
### ###
......
name,path
home,/
login,/login
logout,/logout
password,/password
...@@ -14,6 +14,7 @@ requires = [ ...@@ -14,6 +14,7 @@ requires = [
'pyramid', 'pyramid',
'pyramid_chameleon', 'pyramid_chameleon',
'pyramid_debugtoolbar', 'pyramid_debugtoolbar',
'pyramid_tm',
'waitress', 'waitress',
'zope.sqlalchemy', 'zope.sqlalchemy',
'psycopg2-binary', 'psycopg2-binary',
...@@ -23,6 +24,7 @@ requires = [ ...@@ -23,6 +24,7 @@ requires = [
'colander', 'colander',
'deform', 'deform',
'pyramid_beaker', 'pyramid_beaker',
'pyramid_mailer',
] ]
tests_require = [ tests_require = [
......
import os
import csv import csv
import deform import deform
from pkg_resources import resource_filename from pkg_resources import resource_filename
...@@ -7,6 +8,7 @@ from pyramid.config import Configurator ...@@ -7,6 +8,7 @@ from pyramid.config import Configurator
from pyramid_beaker import session_factory_from_settings from pyramid_beaker import session_factory_from_settings
from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy from pyramid.authorization import ACLAuthorizationPolicy
from pyramid_mailer import mailer_factory_from_settings
from sqlalchemy import engine_from_config from sqlalchemy import engine_from_config
from .models import ( from .models import (
DBSession, DBSession,
...@@ -20,6 +22,29 @@ from .tools.this_framework import get_locale_name ...@@ -20,6 +22,29 @@ from .tools.this_framework import get_locale_name
from .views import RemoveSlashNotFoundViewFactory from .views import RemoveSlashNotFoundViewFactory
here = os.path.abspath(os.path.dirname(__file__))
routes_file = os.path.join(here, 'routes.csv')
def set_paths(config):
with open(routes_file) as f:
c = csv.DictReader(f)
for row in c:
path = row['path'] or '/' + row['name']
config.add_route(row['name'], path)
config.scan()
def translator(term):
return get_localizer(get_current_request()).translate(term)
deform_template_dir = resource_filename('deform', 'templates/')
zpt_renderer = deform.ZPTRendererFactory(
[deform_template_dir], translator=translator)
deform.Form.set_default_renderer(zpt_renderer)
def main(global_config, **settings): def main(global_config, **settings):
""" This function returns a Pyramid WSGI application. """ This function returns a Pyramid WSGI application.
""" """
...@@ -32,6 +57,7 @@ def main(global_config, **settings): ...@@ -32,6 +57,7 @@ def main(global_config, **settings):
root_factory='web_starter.models.ziggurat.RootFactory', root_factory='web_starter.models.ziggurat.RootFactory',
session_factory=session_factory, session_factory=session_factory,
locale_negotiator=get_locale_name) locale_negotiator=get_locale_name)
config.include('pyramid_tm')
config.include('pyramid_beaker') config.include('pyramid_beaker')
config.include('pyramid_chameleon') config.include('pyramid_chameleon')
...@@ -48,18 +74,6 @@ def main(global_config, **settings): ...@@ -48,18 +74,6 @@ def main(global_config, **settings):
config.add_static_view('deform_static', 'deform:static') config.add_static_view('deform_static', 'deform:static')
config.add_translation_dirs('locale') config.add_translation_dirs('locale')
def translator(term): config.registry['mailer'] = mailer_factory_from_settings(settings)
return get_localizer(get_current_request()).translate(term) set_paths(config)
deform_template_dir = resource_filename('deform', 'templates/')
zpt_renderer = deform.ZPTRendererFactory(
[deform_template_dir],
translator=translator,)
deform.Form.set_default_renderer(zpt_renderer)
with open('routes.csv') as f:
c = csv.DictReader(f)
for row in c:
config.add_route(row['name'], row['path'])
config.scan()
return config.make_wsgi_app() return config.make_wsgi_app()
...@@ -19,3 +19,15 @@ msgstr "" ...@@ -19,3 +19,15 @@ msgstr ""
#. Default: Copyright © Pylons Project #. Default: Copyright © Pylons Project
msgid "copyright" msgid "copyright"
msgstr "" msgstr ""
msgid "welcome"
msgstr ""
msgid "you-are-logged-in"
msgstr ""
msgid "login-link"
msgstr ""
msgid "Home"
msgstr ""
...@@ -6,7 +6,7 @@ msgid "" ...@@ -6,7 +6,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE 1.0\n" "Project-Id-Version: PACKAGE 1.0\n"
"POT-Creation-Date: 2018-10-15 20:15+0700\n" "POT-Creation-Date: 2018-10-15 20:15+0700\n"
"PO-Revision-Date: 2018-10-15 20:17+0700\n" "PO-Revision-Date: 2018-10-19 17:11+0700\n"
"Last-Translator: Owo Sugiana <sugiana@gmail.com>\n" "Last-Translator: Owo Sugiana <sugiana@gmail.com>\n"
"Language-Team: Indonesian\n" "Language-Team: Indonesian\n"
"Language: id\n" "Language: id\n"
...@@ -24,3 +24,9 @@ msgstr "Selamat datang di <span class=\"font-normal\">Web Starter</span>, sebuah ...@@ -24,3 +24,9 @@ msgstr "Selamat datang di <span class=\"font-normal\">Web Starter</span>, sebuah
msgid "you-are-logged-in" msgid "you-are-logged-in"
msgstr "Anda sedang masuk sebagai ${username}. Klik <a href=\"/logout\">di sini</a> untuk keluar." msgstr "Anda sedang masuk sebagai ${username}. Klik <a href=\"/logout\">di sini</a> untuk keluar."
msgid "login-link"
msgstr "Silakan masuk"
msgid "Home"
msgstr "Beranda"
...@@ -16,11 +16,11 @@ msgstr "" ...@@ -16,11 +16,11 @@ msgstr ""
"Generated-By: Lingua 4.13\n" "Generated-By: Lingua 4.13\n"
#. Default: Enter new password for ${name}: #. Default: Enter new password for ${name}:
msgid "Enter new password for ${name}: " msgid "ask-password-1"
msgstr "Masukkan password baru untuk ${name}: " msgstr "Masukkan password baru untuk ${name}: "
#. Default: Retype new password for ${name}: #. Default: Retype new password for ${name}:
msgid "Retype new password for ${name}: " msgid "ask-password-2"
msgstr "Ulangi password baru untuk ${name}: " msgstr "Ulangi password baru untuk ${name}: "
msgid "Sorry, passwords do not match" msgid "Sorry, passwords do not match"
......
...@@ -33,3 +33,54 @@ msgstr "Nama" ...@@ -33,3 +33,54 @@ msgstr "Nama"
msgid "Password" msgid "Password"
msgstr "Kata kunci" msgstr "Kata kunci"
msgid "Reset password"
msgstr "Pemulihan kata kunci"
msgid "email-reset-password"
msgstr "Tulis email Anda dan kami akan mengirimkan tautan untuk penetapan ulang kata kunci"
msgid "Send password reset email"
msgstr "Kirim email"
msgid "reset-password-body"
msgstr "Kami menerima permintaan pemulihan kata sandi. Silakan klik tautan berikut:\n\n${url}\n\nTautan ini akan kedaluwarsa dalam ${minutes} menit. Mohon abaikan jika Anda tidak memintanya."
msgid "reset-password-link-sent"
msgstr "Periksa email Anda untuk tautan pemulihan kata kunci. Jika tidak muncul dalam beberapa menit, periksa di bagian spam."
msgid "Invalid email"
msgstr "Email tidak terdaftar"
msgid "Forgot password"
msgstr "Lupa kata kunci"
msgid "change-password-done"
msgstr "Kata kunci Anda telah diubah"
msgid "Save"
msgstr "Simpan"
msgid "Cancel"
msgstr "Batalkan"
msgid "Invalid old password"
msgstr "Kata kunci yang lama tidak benar"
msgid "Retype mismatch"
msgstr "Pengulangan kata kunci yang baru tidak sama"
msgid "Old password"
msgstr "Kata kunci yang lama"
msgid "New password"
msgstr "Kata kunci yang baru"
msgid "Retype new password"
msgstr "Ulangi kata kunci yang baru"
msgid "Change password"
msgstr "Ganti kata kunci"
msgid "Invalid security code"
msgstr "Kode keamananan tidak benar"
...@@ -25,3 +25,49 @@ msgstr "" ...@@ -25,3 +25,49 @@ msgstr ""
#: ./views/login.py:58 #: ./views/login.py:58
msgid "login" msgid "login"
msgstr "" msgstr ""
msgid "Reset password"
msgstr ""
#. Default: Enter your email address and we will send you a link to reset your password.
msgid "email-reset-password"
msgstr ""
msgid "Send password reset email"
msgstr ""
msgid "reset-password-body"
msgstr ""
msgid "reset-password-link-sent"
msgstr ""
msgid "change-password-done"
msgstr ""
msgid "Save"
msgstr ""
msgid "Cancel"
msgstr ""
msgid "Invalid old password"
msgstr ""
msgid "Retype mismatch"
msgstr ""
msgid "Old password"
msgstr ""
msgid "New password"
msgstr ""
msgid "Retype new password"
msgstr ""
msgid "Change password"
msgstr ""
msgid "Invalid security code"
msgstr ""
name,path
home,/
login
logout
password
change-password
change-password-done
reset-password
reset-password-sent
login-by-code-failed
id,email,status,user_name email,status,user_name
0,anonymous@local,0 admin@local,1,admin
,admin@local,1,admin
...@@ -4,18 +4,17 @@ import csv ...@@ -4,18 +4,17 @@ import csv
import subprocess import subprocess
import transaction import transaction
from getpass import getpass from getpass import getpass
from translationstring import (
TranslationString,
Translator,
ugettext_policy,
)
from translationstring.tests.translations import Translations
from sqlalchemy import engine_from_config from sqlalchemy import engine_from_config
from ziggurat_foundations.models.services.user import UserService from ziggurat_foundations.models.services.user import UserService
from pyramid.paster import ( from pyramid.paster import (
get_appsettings, get_appsettings,
setup_logging, setup_logging,
) )
from pyramid.i18n import (
Localizer,
TranslationStringFactory,
Translations,
)
from ..models import ( from ..models import (
DBSession, DBSession,
Base, Base,
...@@ -28,34 +27,23 @@ from ..models.ziggurat import ( ...@@ -28,34 +27,23 @@ from ..models.ziggurat import (
) )
class Penerjemah: domain = 'initialize_db'
def __init__(self, domain='initialize_db'): _ = TranslationStringFactory(domain)
self.domain = domain
def configure(self, settings):
locale_ids = [settings['pyramid.default_locale_name']]
here = os.path.abspath(os.path.dirname(__file__))
localedir = os.path.join(here, '..', 'locale')
try:
self.translations = Translations.load(
localedir, locale_ids, self.domain)
except TypeError:
self.translations = None
self.translator = self.translations and Translator(
self.translations, ugettext_policy)
def terjemahkan(self, msg, mapping=None):
ts = TranslationString(msg, mapping=mapping)
if self.translator:
return self.translator(ts)
return ts.interpolate()
my_registry = dict()
obj_penerjemah = Penerjemah()
class MyLocalizer:
def __init__(self):
settings = my_registry['settings']
locale_name = settings['pyramid.default_locale_name']
here = os.path.abspath(os.path.dirname(__file__))
locale_dir = os.path.join(here, '..', 'locale')
translations = Translations.load(locale_dir, [locale_name], domain)
self.localizer = Localizer(locale_name, translations)
def _(msg, mapping=None): def translate(self, ts):
return obj_penerjemah.terjemahkan(msg, mapping) return self.localizer.translate(ts)
def usage(argv): def usage(argv):
...@@ -92,9 +80,16 @@ def get_file(filename): ...@@ -92,9 +80,16 @@ def get_file(filename):
def ask_password(name): def ask_password(name):
localizer = MyLocalizer()
data = dict(name=name) data = dict(name=name)
msg1 = _('Enter new password for ${name}: ', mapping=data) t_msg1 = _(
msg2 = _('Retype new password for ${name}: ', mapping=data) 'ask-password-1', default='Enter new password for ${name}: ',
mapping=data)
t_msg2 = _(
'ask-password-2', default='Retype new password for ${name}: ',
mapping=data)
msg1 = localizer.translate(t_msg1)
msg2 = localizer.translate(t_msg2)
while True: while True:
pass1 = getpass(msg1) pass1 = getpass(msg1)
if not pass1: if not pass1:
...@@ -102,7 +97,8 @@ def ask_password(name): ...@@ -102,7 +97,8 @@ def ask_password(name):
pass2 = getpass(msg2) pass2 = getpass(msg2)
if pass1 == pass2: if pass1 == pass2:
return pass1 return pass1
print(_('Sorry, passwords do not match')) ts = _('Sorry, passwords do not match')
print(localizer.translate(ts))
def restore_csv(table, filename): def restore_csv(table, filename):
...@@ -128,7 +124,7 @@ def main(argv=sys.argv): ...@@ -128,7 +124,7 @@ def main(argv=sys.argv):
config_uri = argv[1] config_uri = argv[1]
setup_logging(config_uri) setup_logging(config_uri)
settings = get_appsettings(config_uri) settings = get_appsettings(config_uri)
obj_penerjemah.configure(settings) my_registry['settings'] = settings
engine = engine_from_config(settings, 'sqlalchemy.') engine = engine_from_config(settings, 'sqlalchemy.')
Base.metadata.bind = engine Base.metadata.bind = engine
Base.metadata.create_all() Base.metadata.create_all()
...@@ -140,6 +136,5 @@ def main(argv=sys.argv): ...@@ -140,6 +136,5 @@ def main(argv=sys.argv):
user = q.first() user = q.first()
password = ask_password(user.user_name) password = ask_password(user.user_name)
UserService.set_password(user, password) UserService.set_password(user, password)
UserService.regenerate_security_code(user)
restore_csv(Group, 'groups.csv') restore_csv(Group, 'groups.csv')
restore_csv(UserGroup, 'users_groups.csv') restore_csv(UserGroup, 'users_groups.csv')
...@@ -37,4 +37,4 @@ body { ...@@ -37,4 +37,4 @@ body {
margin-bottom: 10px; margin-bottom: 10px;
border-top-left-radius: 0; border-top-left-radius: 0;
border-top-right-radius: 0; border-top-right-radius: 0;
}
\ No newline at end of file \ No newline at end of file
}
...@@ -5,6 +5,10 @@ from pyramid.httpexceptions import ( ...@@ -5,6 +5,10 @@ from pyramid.httpexceptions import (
) )
from pyramid.interfaces import IRoutesMapper from pyramid.interfaces import IRoutesMapper
from pyramid.response import Response from pyramid.response import Response
from pyramid.i18n import TranslationStringFactory
_ = TranslationStringFactory('home')
# http://stackoverflow.com/questions/9845669/pyramid-inverse-to-add-notfound-viewappend-slash-true # http://stackoverflow.com/questions/9845669/pyramid-inverse-to-add-notfound-viewappend-slash-true
...@@ -33,12 +37,11 @@ class RemoveSlashNotFoundViewFactory: ...@@ -33,12 +37,11 @@ class RemoveSlashNotFoundViewFactory:
return self.notfound_view(context, request) return self.notfound_view(context, request)
@view_config(route_name='home', renderer='templates/mytemplate.pt', @view_config(route_name='home', renderer='templates/home.pt')
permission='view')
def my_view(request): def my_view(request):
if '_LOCALE_' in request.GET: if '_LOCALE_' in request.GET:
resp = Response() resp = Response()
resp.set_cookie('_LOCALE_', request.GET['_LOCALE_'], 31536000) resp.set_cookie('_LOCALE_', request.GET['_LOCALE_'], 31536000)
return HTTPFound( return HTTPFound(
location=request.route_url('home'), headers=resp.headers) location=request.route_url('home'), headers=resp.headers)
return {'project': 'Web Starter'} return dict(project='Web Starter', title=_('Home'))
We accepted password recovery requests. Please click the following link:
${url}
This link will expire in ${minutes} minutes.
If you did not request this, please ignore it.
<div metal:use-macro="load: layout.pt">
<div metal:fill-slot="content" i18n:domain="login">
<p i18n:translate="change-password-done">
Your password has been changed
</p>
</div>
</div>
<div metal:use-macro="load: layout-form.pt">
<div metal:fill-slot="content">
<div tal:content="structure form" />
</div>
</div>
<div metal:use-macro="load: layout.pt"> <div metal:use-macro="load: layout.pt">
<div metal:fill-slot="content" i18n:domain="ChameleonI18n"> <div metal:fill-slot="content" i18n:domain="home">
<div class="content"> <div class="content">
<h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Starter project</span></h1> <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Starter project</span></h1>
<p i18n:translate="welcome" class="lead">Welcome to <span <p i18n:translate="welcome" class="lead">Welcome to <span
class="font-normal">Web Starter</span>, a&nbsp;Pyramid class="font-normal">Web Starter</span>, a&nbsp;Pyramid
application generated&nbsp;by<br><span class="font-normal">Cookiecutter</span>.</p> application generated&nbsp;by<br><span class="font-normal">Cookiecutter</span>.</p>
<p tal:condition="not request.user">
<a href="/login" i18n:translate="login-link">Login please</a>
</p>
<p i18n:translate="you-are-logged-in" tal:condition="request.user">You are logged in as <p i18n:translate="you-are-logged-in" tal:condition="request.user">You are logged in as
<span tal:replace="request.user.user_name" i18n:name="username" />. Click <a href="/logout">here</a> to <span tal:replace="request.user.user_name" i18n:name="username" />. Click <a href="/logout">here</a> to
logout.</p> logout.</p>
</div> </div>
</div> </div>
......
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<link rel="shortcut icon" href="/static/pyramid-16x16.png">
<title tal:content="title"/>
<!-- Bootstrap core CSS -->
<link href="/deform_static/css/bootstrap.min.css" rel="stylesheet"/>
<link href="/deform_static/css/form.css" rel="stylesheet"/>
<!-- Custom styles for this template -->
<link href="/static/signin.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div metal:define-slot="content"/>
</div> <!-- /container -->
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script type="text/javascript" src="/deform_static/scripts/jquery-2.0.3.min.js"></script>
<script type="text/javascript" src="/deform_static/scripts/bootstrap.min.js"></script>
<script type="text/javascript" src="/deform_static/scripts/deform.js"></script>
<div metal:define-slot="content-script"/>
</body>
</html>
<!DOCTYPE html metal:define-macro="layout"> <!DOCTYPE html metal:define-macro="layout">
<html lang="${request.locale_name}" <html lang="${request.locale_name}"
xmlns:i18n="http://xml.zope.org/namespaces/i18n" xmlns:i18n="http://xml.zope.org/namespaces/i18n"
i18n:domain="ChameleonI18n"> i18n:domain="home">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
<meta name="author" content="Pylons Project"> <meta name="author" content="Pylons Project">
<link rel="shortcut icon" href="${request.static_url('web_starter:static/pyramid-16x16.png')}"> <link rel="shortcut icon" href="${request.static_url('web_starter:static/pyramid-16x16.png')}">
<title>Cookiecutter Starter project for the Pyramid Web Framework</title> <title>Web Starter - <span tal:replace="title"/></title>
<!-- Bootstrap core CSS --> <!-- Bootstrap core CSS -->
<link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
......
<div metal:use-macro="load: layout.pt">
<div metal:fill-slot="content" i18n:domain="login">
<p i18n:translate="Invalid security code">
Invalid security code.
</p>
</div>
</div>
<!DOCTYPE html> <div metal:use-macro="load: layout-form.pt">
<html lang="en"> <div metal:fill-slot="content">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<link rel="shortcut icon" href="/static/pyramid-16x16.png">
<title tal:content="title"/> <div tal:condition="form" tal:content="structure form"/>
<!-- Bootstrap core CSS --> <p tal:condition="request.locale_name != 'en'">
<link href="/deform_static/css/bootstrap.min.css" rel="stylesheet"/> <a href="?_LOCALE_=en">English</a>
<link href="/deform_static/css/form.css" rel="stylesheet"/> </p>
<p tal:condition="request.locale_name != 'id'">
<!-- Custom styles for this template --> <a href="?_LOCALE_=id">Indonesia</a>
<link href="/static/signin.css" rel="stylesheet"> </p>
</head> </div>
<body> <div metal:fill-slot="content-script">
<div class="container"> <script type="text/javascript">
$(window).on('load', function() {
<div tal:content="structure form"/> $("p.help-block").html('<a href="/reset-password">${label_forgot_password}</a>');
});
</script>
<p tal:condition="request.locale_name != 'en'"> </div>
<a href="?_LOCALE_=en">English</a>
</p>
<p tal:condition="request.locale_name != 'id'">
<a href="?_LOCALE_=id">Indonesia</a>
</p>
</div> <!-- /container --> </div>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script type="text/javascript" src="/deform_static/scripts/jquery-2.0.3.min.js"></script>
<script type="text/javascript" src="/deform_static/scripts/bootstrap.min.js"></script>
<script type="text/javascript" src="/deform_static/scripts/deform.js"></script>
</body>
</html>
<div metal:use-macro="load: layout.pt">
<div metal:fill-slot="content" i18n:domain="login">
<p i18n:translate="reset-password-link-sent">
Check your email for a link to reset your password. If it
doesn’t appear within a few minutes, check your spam folder. The
reset password link has been sent. Please check your email.
</p>
</div>
</div>
<div metal:use-macro="load: layout-form.pt">
<div metal:fill-slot="content">
<div tal:content="structure form" />
</div>
</div>
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!