##// END OF EJS Templates
Implemented password reset(forms/models/ tasks) and mailing tasks....
marcink -
r474:a3d9d24a celery
parent child Browse files
Show More
@@ -0,0 +1,118 b''
1 import logging
2 import smtplib
3 import mimetypes
4 from email.mime.multipart import MIMEMultipart
5 from email.mime.image import MIMEImage
6 from email.mime.audio import MIMEAudio
7 from email.mime.base import MIMEBase
8 from email.mime.text import MIMEText
9 from email.utils import formatdate
10 from email import encoders
11
12 class SmtpMailer(object):
13 """simple smtp mailer class
14
15 mailer = SmtpMailer(mail_from, user, passwd, mail_server, mail_port, ssl, tls)
16 mailer.send(recipients, subject, body, attachment_files)
17
18 :param recipients might be a list of string or single string
19 :param attachment_files is a dict of {filename:location}
20 it tries to guess the mimetype and attach the file
21 """
22
23 def __init__(self, mail_from, user, passwd, mail_server,
24 mail_port=None, ssl=False, tls=False):
25
26 self.mail_from = mail_from
27 self.mail_server = mail_server
28 self.mail_port = mail_port
29 self.user = user
30 self.passwd = passwd
31 self.ssl = ssl
32 self.tls = tls
33 self.debug = False
34
35 def send(self, recipients=[], subject='', body='', attachment_files={}):
36
37 if isinstance(recipients, basestring):
38 recipients = [recipients]
39 if self.ssl:
40 smtp_serv = smtplib.SMTP_SSL(self.mail_server, self.mail_port)
41 else:
42 smtp_serv = smtplib.SMTP(self.mail_server, self.mail_port)
43
44 if self.tls:
45 smtp_serv.starttls()
46
47 if self.debug:
48 smtp_serv.set_debuglevel(1)
49
50 smtp_serv.ehlo("mailer")
51
52 #if server requires authorization you must provide login and password
53 smtp_serv.login(self.user, self.passwd)
54
55 date_ = formatdate(localtime=True)
56 msg = MIMEMultipart()
57 msg['From'] = self.mail_from
58 msg['To'] = ','.join(recipients)
59 msg['Date'] = date_
60 msg['Subject'] = subject
61 msg.preamble = 'You will not see this in a MIME-aware mail reader.\n'
62
63 msg.attach(MIMEText(body))
64
65 if attachment_files:
66 self.__atach_files(msg, attachment_files)
67
68 smtp_serv.sendmail(self.mail_from, recipients, msg.as_string())
69 logging.info('MAIL SEND TO: %s' % recipients)
70 smtp_serv.quit()
71
72
73 def __atach_files(self, msg, attachment_files):
74 if isinstance(attachment_files, dict):
75 for f_name, msg_file in attachment_files.items():
76 ctype, encoding = mimetypes.guess_type(f_name)
77 logging.info("guessing file %s type based on %s" , ctype, f_name)
78 if ctype is None or encoding is not None:
79 # No guess could be made, or the file is encoded (compressed), so
80 # use a generic bag-of-bits type.
81 ctype = 'application/octet-stream'
82 maintype, subtype = ctype.split('/', 1)
83 if maintype == 'text':
84 # Note: we should handle calculating the charset
85 file_part = MIMEText(self.get_content(msg_file),
86 _subtype=subtype)
87 elif maintype == 'image':
88 file_part = MIMEImage(self.get_content(msg_file),
89 _subtype=subtype)
90 elif maintype == 'audio':
91 file_part = MIMEAudio(self.get_content(msg_file),
92 _subtype=subtype)
93 else:
94 file_part = MIMEBase(maintype, subtype)
95 file_part.set_payload(self.get_content(msg_file))
96 # Encode the payload using Base64
97 encoders.encode_base64(msg)
98 # Set the filename parameter
99 file_part.add_header('Content-Disposition', 'attachment',
100 filename=f_name)
101 file_part.add_header('Content-Type', ctype, name=f_name)
102 msg.attach(file_part)
103 else:
104 raise Exception('Attachment files should be'
105 'a dict in format {"filename":"filepath"}')
106
107 def get_content(self, msg_file):
108 '''
109 Get content based on type, if content is a string do open first
110 else just read because it's a probably open file object
111 @param msg_file:
112 '''
113 if isinstance(msg_file, str):
114 return open(msg_file, "rb").read()
115 else:
116 #just for safe seek to 0
117 msg_file.seek(0)
118 return msg_file.read()
@@ -0,0 +1,54 b''
1 ## -*- coding: utf-8 -*-
2 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3 <html xmlns="http://www.w3.org/1999/xhtml" id="mainhtml">
4 <head>
5 <title>${_('Reset You password to hg-app')}</title>
6 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
7 <link rel="icon" href="/images/hgicon.png" type="image/png" />
8 <meta name="robots" content="index, nofollow"/>
9
10 <!-- stylesheets -->
11 <link rel="stylesheet" type="text/css" href="/css/reset.css" />
12 <link rel="stylesheet" type="text/css" href="/css/style.css" media="screen" />
13 <link id="color" rel="stylesheet" type="text/css" href="/css/colors/blue.css" />
14
15 <!-- scripts -->
16
17 </head>
18 <body>
19 <div id="register">
20
21 <div class="title">
22 <h5>${_('Reset You password to hg-app')}</h5>
23 <div class="corner tl"></div>
24 <div class="corner tr"></div>
25 </div>
26 <div class="inner">
27 ${h.form(url('password_reset'))}
28 <div class="form">
29 <!-- fields -->
30 <div class="fields">
31
32 <div class="field">
33 <div class="label">
34 <label for="email">${_('Email address')}:</label>
35 </div>
36 <div class="input">
37 ${h.text('email')}
38 </div>
39 </div>
40
41 <div class="buttons">
42 <div class="nohighlight">
43 ${h.submit('send','Reset my password',class_="ui-button ui-widget ui-state-default ui-corner-all")}
44 <div class="activation_msg">${_('Your new password will be send to matching email address')}</div>
45 </div>
46 </div>
47 </div>
48 </div>
49 ${h.end_form()}
50 </div>
51 </div>
52 </body>
53 </html>
54
@@ -8,6 +8,7 b' CELERY_IMPORTS = ("pylons_app.lib.celery'
8 8 CELERY_RESULT_BACKEND = "database"
9 9 CELERY_RESULT_DBURI = "sqlite:///hg_app.db"
10 10
11 BROKER_CONNECTION_MAX_RETRIES = 30
11 12
12 13 ## Broker settings.
13 14 BROKER_HOST = "localhost"
@@ -1,32 +1,37 b''
1 1 ################################################################################
2 2 ################################################################################
3 # pylons_app - Pylons environment configuration #
3 # hg-app - Pylons environment configuration #
4 4 # #
5 5 # The %(here)s variable will be replaced with the parent directory of this file#
6 6 ################################################################################
7 7
8 8 [DEFAULT]
9 9 debug = true
10 ############################################
11 ## Uncomment and replace with the address ##
12 ## which should receive any error reports ##
13 ############################################
10 ################################################################################
11 ## Uncomment and replace with the address which should receive ##
12 ## any error reports after application crash ##
13 ## Additionally those settings will be used by hg-app mailing system ##
14 ################################################################################
14 15 #email_to = admin@localhost
16 #error_email_from = paste_error@localhost
17 #app_email_from = hg-app-noreply@localhost
18 #error_message =
19
15 20 #smtp_server = mail.server.com
16 #error_email_from = paste_error@localhost
17 21 #smtp_username =
18 #smtp_password =
19 #error_message = 'mercurial crash !'
22 #smtp_password =
23 #smtp_port =
24 #smtp_use_tls =
20 25
21 26 [server:main]
22 27 ##nr of threads to spawn
23 28 threadpool_workers = 5
24 29
25 30 ##max request before
26 threadpool_max_requests = 2
31 threadpool_max_requests = 6
27 32
28 33 ##option to use threads of process
29 use_threadpool = true
34 use_threadpool = false
30 35
31 36 use = egg:Paste#http
32 37 host = 127.0.0.1
@@ -110,10 +110,11 b' def make_map(config):'
110 110 #SEARCH
111 111 map.connect('search', '/_admin/search', controller='search')
112 112
113 #LOGIN/LOGOUT
113 #LOGIN/LOGOUT/REGISTER/SIGN IN
114 114 map.connect('login_home', '/_admin/login', controller='login')
115 115 map.connect('logout_home', '/_admin/logout', controller='login', action='logout')
116 116 map.connect('register', '/_admin/register', controller='login', action='register')
117 map.connect('reset_password', '/_admin/password_reset', controller='login', action='password_reset')
117 118
118 119 #FEEDS
119 120 map.connect('rss_feed_home', '/{repo_name:.*}/feed/rss',
@@ -28,7 +28,9 b' from pylons import request, response, se'
28 28 from pylons.controllers.util import abort, redirect
29 29 from pylons_app.lib.auth import AuthUser, HasPermissionAnyDecorator
30 30 from pylons_app.lib.base import BaseController, render
31 from pylons_app.model.forms import LoginForm, RegisterForm
31 import pylons_app.lib.helpers as h
32 from pylons.i18n.translation import _
33 from pylons_app.model.forms import LoginForm, RegisterForm, PasswordResetForm
32 34 from pylons_app.model.user_model import UserModel
33 35 import formencode
34 36 import logging
@@ -42,7 +44,7 b' class LoginController(BaseController):'
42 44
43 45 def index(self):
44 46 #redirect if already logged in
45 c.came_from = request.GET.get('came_from',None)
47 c.came_from = request.GET.get('came_from', None)
46 48
47 49 if c.hg_app_user.is_authenticated:
48 50 return redirect(url('hg_home'))
@@ -82,7 +84,7 b' class LoginController(BaseController):'
82 84
83 85 return render('/login.html')
84 86
85 @HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate',
87 @HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate',
86 88 'hg.register.manual_activate')
87 89 def register(self):
88 90 user_model = UserModel()
@@ -99,6 +101,8 b' class LoginController(BaseController):'
99 101 form_result = register_form.to_python(dict(request.POST))
100 102 form_result['active'] = c.auto_active
101 103 user_model.create_registration(form_result)
104 h.flash(_('You have successfully registered into hg-app'),
105 category='success')
102 106 return redirect(url('login_home'))
103 107
104 108 except formencode.Invalid as errors:
@@ -110,7 +114,29 b' class LoginController(BaseController):'
110 114 encoding="UTF-8")
111 115
112 116 return render('/register.html')
113
117
118 def password_reset(self):
119 user_model = UserModel()
120 if request.POST:
121
122 password_reset_form = PasswordResetForm()()
123 try:
124 form_result = password_reset_form.to_python(dict(request.POST))
125 user_model.reset_password(form_result)
126 h.flash(_('Your new password was sent'),
127 category='success')
128 return redirect(url('login_home'))
129
130 except formencode.Invalid as errors:
131 return htmlfill.render(
132 render('/password_reset.html'),
133 defaults=errors.value,
134 errors=errors.error_dict or {},
135 prefix_error=False,
136 encoding="UTF-8")
137
138 return render('/password_reset.html')
139
114 140 def logout(self):
115 141 session['hg_app_user'] = AuthUser()
116 142 session.save()
@@ -34,9 +34,36 b' from sqlalchemy.orm.exc import NoResultF'
34 34 import bcrypt
35 35 from decorator import decorator
36 36 import logging
37 import random
37 38
38 39 log = logging.getLogger(__name__)
39 40
41 class PasswordGenerator(object):
42 """This is a simple class for generating password from
43 different sets of characters
44 usage:
45 passwd_gen = PasswordGenerator()
46 #print 8-letter password containing only big and small letters of alphabet
47 print passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
48 """
49 ALPHABETS_NUM = r'''1234567890'''#[0]
50 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''#[1]
51 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''#[2]
52 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?''' #[3]
53 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM + ALPHABETS_SPECIAL#[4]
54 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM#[5]
55 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
56 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM#[6]
57 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM#[7]
58
59 def __init__(self, passwd=''):
60 self.passwd = passwd
61
62 def gen_password(self, len, type):
63 self.passwd = ''.join([random.choice(type) for _ in xrange(len)])
64 return self.passwd
65
66
40 67 def get_crypt_password(password):
41 68 """Cryptographic function used for password hashing based on sha1
42 69 @param password: password to hash
@@ -231,9 +258,9 b' class LoginRequired(object):'
231 258
232 259 p = request.environ.get('PATH_INFO')
233 260 if request.environ.get('QUERY_STRING'):
234 p+='?'+request.environ.get('QUERY_STRING')
235 log.debug('redirecting to login page with %s',p)
236 return redirect(url('login_home',came_from=p))
261 p += '?' + request.environ.get('QUERY_STRING')
262 log.debug('redirecting to login page with %s', p)
263 return redirect(url('login_home', came_from=p))
237 264
238 265 class PermsDecorator(object):
239 266 """Base class for decorators"""
@@ -1,5 +1,8 b''
1 1 from vcs.utils.lazy import LazyProperty
2 2 import logging
3 import os
4 import sys
5 import traceback
3 6
4 7 log = logging.getLogger(__name__)
5 8
@@ -11,14 +14,13 b' class ResultWrapper(object):'
11 14 def result(self):
12 15 return self.task
13 16
14 def run_task(task,async,*args,**kwargs):
17 def run_task(task,*args,**kwargs):
15 18 try:
16 19 t = task.delay(*args,**kwargs)
17 20 log.info('running task %s',t.task_id)
18 if not async:
19 t.wait()
20 21 return t
21 22 except:
23 log.error(traceback.format_exc())
22 24 #pure sync version
23 25 return ResultWrapper(task(*args,**kwargs))
24 26 No newline at end of file
@@ -1,18 +1,82 b''
1 1 from celery.decorators import task
2 from celery.task.sets import subtask
2 3 from datetime import datetime, timedelta
4 from os.path import dirname as dn
5 from pylons.i18n.translation import _
6 from pylons_app.lib.celerylib import run_task
3 7 from pylons_app.lib.helpers import person
8 from pylons_app.lib.smtp_mailer import SmtpMailer
4 9 from pylons_app.lib.utils import OrderedDict
5 10 from time import mktime
11 from vcs.backends.hg import MercurialRepository
12 import ConfigParser
6 13 import calendar
7 import logging
8 from vcs.backends.hg import MercurialRepository
14 import os
15 import traceback
16
17
18 root = dn(dn(dn(dn(os.path.realpath(__file__)))))
19 config = ConfigParser.ConfigParser({'here':root})
20 config.read('%s/development.ini' % root)
21
22 __all__ = ['whoosh_index', 'get_commits_stats',
23 'reset_user_password', 'send_email']
24
25 def get_session():
26 from sqlalchemy import engine_from_config
27 from sqlalchemy.orm import sessionmaker, scoped_session
28 engine = engine_from_config(dict(config.items('app:main')), 'sqlalchemy.db1.')
29 sa = scoped_session(sessionmaker(bind=engine))
30 return sa
9 31
10 log = logging.getLogger(__name__)
32 def get_hg_settings():
33 from pylons_app.model.db import HgAppSettings
34 try:
35 sa = get_session()
36 ret = sa.query(HgAppSettings).all()
37 finally:
38 sa.remove()
39
40 if not ret:
41 raise Exception('Could not get application settings !')
42 settings = {}
43 for each in ret:
44 settings['hg_app_' + each.app_settings_name] = each.app_settings_value
45
46 return settings
11 47
12 @task()
13 def whoosh_index(repo_location,full_index):
48 def get_hg_ui_settings():
49 from pylons_app.model.db import HgAppUi
50 try:
51 sa = get_session()
52 ret = sa.query(HgAppUi).all()
53 finally:
54 sa.remove()
55
56 if not ret:
57 raise Exception('Could not get application ui settings !')
58 settings = {}
59 for each in ret:
60 k = each.ui_key
61 v = each.ui_value
62 if k == '/':
63 k = 'root_path'
64
65 if k.find('.') != -1:
66 k = k.replace('.', '_')
67
68 if each.ui_section == 'hooks':
69 v = each.ui_active
70
71 settings[each.ui_section + '_' + k] = v
72
73 return settings
74
75 @task
76 def whoosh_index(repo_location, full_index):
77 log = whoosh_index.get_logger()
14 78 from pylons_app.lib.indexers import DaemonLock
15 from pylons_app.lib.indexers.daemon import WhooshIndexingDaemon,LockHeld
79 from pylons_app.lib.indexers.daemon import WhooshIndexingDaemon, LockHeld
16 80 try:
17 81 l = DaemonLock()
18 82 WhooshIndexingDaemon(repo_location=repo_location)\
@@ -23,10 +87,12 b' def whoosh_index(repo_location,full_inde'
23 87 log.info('LockHeld')
24 88 return 'LockHeld'
25 89
26 @task()
90 @task
27 91 def get_commits_stats(repo):
92 log = get_commits_stats.get_logger()
28 93 aggregate = OrderedDict()
29 repo = MercurialRepository('/home/marcink/hg_repos/'+repo)
94 repos_path = get_hg_ui_settings()['paths_root_path'].replace('*','')
95 repo = MercurialRepository(repos_path + repo)
30 96 #graph range
31 97 td = datetime.today() + timedelta(days=1)
32 98 y, m, d = td.year, td.month, td.day
@@ -90,3 +156,60 b' def get_commits_stats(repo):'
90 156 % (author_key_cleaner(repo.contact),
91 157 author_key_cleaner(repo.contact))
92 158 return (ts_min, ts_max, d)
159
160 @task
161 def reset_user_password(user_email):
162 log = reset_user_password.get_logger()
163 from pylons_app.lib import auth
164 from pylons_app.model.db import User
165
166 try:
167
168 try:
169 sa = get_session()
170 user = sa.query(User).filter(User.email == user_email).scalar()
171 new_passwd = auth.PasswordGenerator().gen_password(8,
172 auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
173 user.password = auth.get_crypt_password(new_passwd)
174 sa.add(user)
175 sa.commit()
176 log.info('change password for %s', user_email)
177 if new_passwd is None:
178 raise Exception('unable to generate new password')
179
180 except:
181 log.error(traceback.format_exc())
182 sa.rollback()
183
184 run_task(send_email, user_email,
185 "Your new hg-app password",
186 'Your new hg-app password:%s' % (new_passwd))
187 log.info('send new password mail to %s', user_email)
188
189
190 except:
191 log.error('Failed to update user password')
192 log.error(traceback.format_exc())
193 return True
194
195 @task
196 def send_email(recipients, subject, body):
197 log = send_email.get_logger()
198 email_config = dict(config.items('DEFAULT'))
199 mail_from = email_config.get('app_email_from')
200 user = email_config.get('smtp_username')
201 passwd = email_config.get('smtp_password')
202 mail_server = email_config.get('smtp_server')
203 mail_port = email_config.get('smtp_port')
204 tls = email_config.get('smtp_use_tls')
205 ssl = False
206
207 try:
208 m = SmtpMailer(mail_from, user, passwd, mail_server,
209 mail_port, ssl, tls)
210 m.send(recipients, subject, body)
211 except:
212 log.error('Mail sending failed')
213 log.error(traceback.format_exc())
214 return False
215 return True
@@ -102,7 +102,7 b' class ValidAuth(formencode.validators.Fa'
102 102 error_dict=self.e_dict)
103 103 if user:
104 104 if user.active:
105 if user.username == username and check_password(password,
105 if user.username == username and check_password(password,
106 106 user.password):
107 107 return value
108 108 else:
@@ -208,7 +208,20 b' class ValidPath(formencode.validators.Fa'
208 208
209 209 raise formencode.Invalid(msg, value, state,
210 210 error_dict={'paths_root_path':msg})
211
211
212 class ValidSystemEmail(formencode.validators.FancyValidator):
213 def to_python(self, value, state):
214 sa = meta.Session
215 try:
216 user = sa.query(User).filter(User.email == value).scalar()
217 if user is None:
218 raise formencode.Invalid(_("That e-mail address doesn't exist.") ,
219 value, state)
220 finally:
221 meta.Session.remove()
222
223 return value
224
212 225 #===============================================================================
213 226 # FORMS
214 227 #===============================================================================
@@ -255,8 +268,14 b' def UserForm(edit=False, old_data={}):'
255 268 return _UserForm
256 269
257 270 RegisterForm = UserForm
258
259
271
272 def PasswordResetForm():
273 class _PasswordResetForm(formencode.Schema):
274 allow_extra_fields = True
275 filter_extra_fields = True
276 email = All(ValidSystemEmail(), Email(not_empty=True))
277 return _PasswordResetForm
278
260 279 def RepoForm(edit=False, old_data={}):
261 280 class _RepoForm(formencode.Schema):
262 281 allow_extra_fields = True
@@ -2,7 +2,7 b''
2 2 # encoding: utf-8
3 3 # Model for users
4 4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
5
5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; version 2
@@ -23,10 +23,12 b' Created on April 9, 2010'
23 23 Model for users
24 24 @author: marcink
25 25 """
26
26 from pylons_app.lib import auth
27 from pylons.i18n.translation import _
28 from pylons_app.lib.celerylib import tasks, run_task
27 29 from pylons_app.model.db import User
28 30 from pylons_app.model.meta import Session
29 from pylons.i18n.translation import _
31 import traceback
30 32 import logging
31 33 log = logging.getLogger(__name__)
32 34
@@ -43,7 +45,7 b' class UserModel(object):'
43 45 def get_user(self, id):
44 46 return self.sa.query(User).get(id)
45 47
46 def get_user_by_name(self,name):
48 def get_user_by_name(self, name):
47 49 return self.sa.query(User).filter(User.username == name).scalar()
48 50
49 51 def create(self, form_data):
@@ -54,8 +56,8 b' class UserModel(object):'
54 56
55 57 self.sa.add(new_user)
56 58 self.sa.commit()
57 except Exception as e:
58 log.error(e)
59 except:
60 log.error(traceback.format_exc())
59 61 self.sa.rollback()
60 62 raise
61 63
@@ -68,8 +70,8 b' class UserModel(object):'
68 70
69 71 self.sa.add(new_user)
70 72 self.sa.commit()
71 except Exception as e:
72 log.error(e)
73 except:
74 log.error(traceback.format_exc())
73 75 self.sa.rollback()
74 76 raise
75 77
@@ -88,8 +90,8 b' class UserModel(object):'
88 90
89 91 self.sa.add(new_user)
90 92 self.sa.commit()
91 except Exception as e:
92 log.error(e)
93 except:
94 log.error(traceback.format_exc())
93 95 self.sa.rollback()
94 96 raise
95 97
@@ -109,13 +111,12 b' class UserModel(object):'
109 111
110 112 self.sa.add(new_user)
111 113 self.sa.commit()
112 except Exception as e:
113 log.error(e)
114 except:
115 log.error(traceback.format_exc())
114 116 self.sa.rollback()
115 117 raise
116 118
117 119 def delete(self, id):
118
119 120 try:
120 121
121 122 user = self.sa.query(User).get(id)
@@ -125,7 +126,10 b' class UserModel(object):'
125 126 " crucial for entire application"))
126 127 self.sa.delete(user)
127 128 self.sa.commit()
128 except Exception as e:
129 log.error(e)
129 except:
130 log.error(traceback.format_exc())
130 131 self.sa.rollback()
131 132 raise
133
134 def reset_password(self, data):
135 run_task(tasks.reset_user_password, data['email'])
@@ -60,7 +60,7 b''
60 60 <!-- end fields -->
61 61 <!-- links -->
62 62 <div class="links">
63 ${h.link_to(_('Forgot your password ?'),h.url('#'))}
63 ${h.link_to(_('Forgot your password ?'),h.url('reset_password'))}
64 64 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
65 65 /
66 66 ${h.link_to(_("Don't have an account ?"),h.url('register'))}
General Comments 0
You need to be logged in to leave comments. Login now