changeset 474:a3d9d24acbec celery

Implemented password reset(forms/models/ tasks) and mailing tasks. Added smtp mailer, configurations, cleaned user model
author Marcin Kuzminski <marcin@python-works.com>
date Mon, 13 Sep 2010 01:27:41 +0200
parents 6b934c9607e7
children 9dd38344c466
files celeryconfig.py development.ini pylons_app/config/routing.py pylons_app/controllers/login.py pylons_app/lib/auth.py pylons_app/lib/celerylib/__init__.py pylons_app/lib/celerylib/tasks.py pylons_app/lib/smtp_mailer.py pylons_app/model/forms.py pylons_app/model/user_model.py pylons_app/templates/login.html pylons_app/templates/password_reset.html
diffstat 12 files changed, 429 insertions(+), 49 deletions(-) [+]
line wrap: on
line diff
--- a/celeryconfig.py	Mon Sep 13 01:23:58 2010 +0200
+++ b/celeryconfig.py	Mon Sep 13 01:27:41 2010 +0200
@@ -8,6 +8,7 @@
 CELERY_RESULT_BACKEND = "database"
 CELERY_RESULT_DBURI = "sqlite:///hg_app.db"
 
+BROKER_CONNECTION_MAX_RETRIES = 30
 
 ## Broker settings.
 BROKER_HOST = "localhost"
--- a/development.ini	Mon Sep 13 01:23:58 2010 +0200
+++ b/development.ini	Mon Sep 13 01:27:41 2010 +0200
@@ -1,32 +1,37 @@
 ################################################################################
 ################################################################################
-# pylons_app - Pylons environment configuration                                #
+# hg-app - Pylons environment configuration                                    #
 #                                                                              # 
 # The %(here)s variable will be replaced with the parent directory of this file#
 ################################################################################
 
 [DEFAULT]
 debug = true
-############################################
-## Uncomment and replace with the address ##
-## which should receive any error reports ##
-############################################
+################################################################################
+## Uncomment and replace with the address which should receive                ## 
+## any error reports after application crash								  ##
+## Additionally those settings will be used by hg-app mailing system          ##
+################################################################################
 #email_to = admin@localhost
+#error_email_from = paste_error@localhost
+#app_email_from = hg-app-noreply@localhost
+#error_message =
+
 #smtp_server = mail.server.com
-#error_email_from = paste_error@localhost
 #smtp_username = 
-#smtp_password = 
-#error_message = 'mercurial crash !'
+#smtp_password =
+#smtp_port = 
+#smtp_use_tls = 
 
 [server:main]
 ##nr of threads to spawn
 threadpool_workers = 5
 
 ##max request before
-threadpool_max_requests = 2
+threadpool_max_requests = 6
 
 ##option to use threads of process
-use_threadpool = true
+use_threadpool = false
 
 use = egg:Paste#http
 host = 127.0.0.1
--- a/pylons_app/config/routing.py	Mon Sep 13 01:23:58 2010 +0200
+++ b/pylons_app/config/routing.py	Mon Sep 13 01:27:41 2010 +0200
@@ -110,10 +110,11 @@
     #SEARCH
     map.connect('search', '/_admin/search', controller='search')
     
-    #LOGIN/LOGOUT
+    #LOGIN/LOGOUT/REGISTER/SIGN IN
     map.connect('login_home', '/_admin/login', controller='login')
     map.connect('logout_home', '/_admin/logout', controller='login', action='logout')
     map.connect('register', '/_admin/register', controller='login', action='register')
+    map.connect('reset_password', '/_admin/password_reset', controller='login', action='password_reset')
         
     #FEEDS
     map.connect('rss_feed_home', '/{repo_name:.*}/feed/rss',
--- a/pylons_app/controllers/login.py	Mon Sep 13 01:23:58 2010 +0200
+++ b/pylons_app/controllers/login.py	Mon Sep 13 01:27:41 2010 +0200
@@ -28,7 +28,9 @@
 from pylons.controllers.util import abort, redirect
 from pylons_app.lib.auth import AuthUser, HasPermissionAnyDecorator
 from pylons_app.lib.base import BaseController, render
-from pylons_app.model.forms import LoginForm, RegisterForm
+import pylons_app.lib.helpers as h 
+from pylons.i18n.translation import _
+from pylons_app.model.forms import LoginForm, RegisterForm, PasswordResetForm
 from pylons_app.model.user_model import UserModel
 import formencode
 import logging
@@ -42,7 +44,7 @@
 
     def index(self):
         #redirect if already logged in
-        c.came_from = request.GET.get('came_from',None)
+        c.came_from = request.GET.get('came_from', None)
         
         if c.hg_app_user.is_authenticated:
             return redirect(url('hg_home'))
@@ -82,7 +84,7 @@
                         
         return render('/login.html')
     
-    @HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate', 
+    @HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate',
                                'hg.register.manual_activate')
     def register(self):
         user_model = UserModel()
@@ -99,6 +101,8 @@
                 form_result = register_form.to_python(dict(request.POST))
                 form_result['active'] = c.auto_active
                 user_model.create_registration(form_result)
+                h.flash(_('You have successfully registered into hg-app'),
+                            category='success')                
                 return redirect(url('login_home'))
                                
             except formencode.Invalid as errors:
@@ -110,7 +114,29 @@
                     encoding="UTF-8")
         
         return render('/register.html')
-    
+
+    def password_reset(self):
+        user_model = UserModel()
+        if request.POST:
+                
+            password_reset_form = PasswordResetForm()()
+            try:
+                form_result = password_reset_form.to_python(dict(request.POST))
+                user_model.reset_password(form_result)
+                h.flash(_('Your new password was sent'),
+                            category='success')                 
+                return redirect(url('login_home'))
+                               
+            except formencode.Invalid as errors:
+                return htmlfill.render(
+                    render('/password_reset.html'),
+                    defaults=errors.value,
+                    errors=errors.error_dict or {},
+                    prefix_error=False,
+                    encoding="UTF-8")
+        
+        return render('/password_reset.html')
+        
     def logout(self):
         session['hg_app_user'] = AuthUser()
         session.save()
--- a/pylons_app/lib/auth.py	Mon Sep 13 01:23:58 2010 +0200
+++ b/pylons_app/lib/auth.py	Mon Sep 13 01:27:41 2010 +0200
@@ -34,9 +34,36 @@
 import bcrypt
 from decorator import decorator
 import logging
+import random
 
 log = logging.getLogger(__name__) 
 
+class PasswordGenerator(object):
+    """This is a simple class for generating password from
+        different sets of characters
+        usage:
+        passwd_gen = PasswordGenerator()
+        #print 8-letter password containing only big and small letters of alphabet
+        print passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)        
+    """
+    ALPHABETS_NUM = r'''1234567890'''#[0]
+    ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''#[1]
+    ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''#[2]
+    ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''    #[3]
+    ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM + ALPHABETS_SPECIAL#[4]
+    ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM#[5]
+    ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
+    ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM#[6]
+    ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM#[7]
+            
+    def __init__(self, passwd=''):
+        self.passwd = passwd
+
+    def gen_password(self, len, type):
+        self.passwd = ''.join([random.choice(type) for _ in xrange(len)])
+        return self.passwd
+
+    
 def get_crypt_password(password):
     """Cryptographic function used for password hashing based on sha1
     @param password: password to hash
@@ -231,9 +258,9 @@
 
             p = request.environ.get('PATH_INFO')
             if request.environ.get('QUERY_STRING'):
-                p+='?'+request.environ.get('QUERY_STRING')
-            log.debug('redirecting to login page with %s',p)                
-            return redirect(url('login_home',came_from=p))
+                p += '?' + request.environ.get('QUERY_STRING')
+            log.debug('redirecting to login page with %s', p)                
+            return redirect(url('login_home', came_from=p))
 
 class PermsDecorator(object):
     """Base class for decorators"""
--- a/pylons_app/lib/celerylib/__init__.py	Mon Sep 13 01:23:58 2010 +0200
+++ b/pylons_app/lib/celerylib/__init__.py	Mon Sep 13 01:27:41 2010 +0200
@@ -1,5 +1,8 @@
 from vcs.utils.lazy import LazyProperty
 import logging
+import os
+import sys
+import traceback
 
 log = logging.getLogger(__name__)
 
@@ -11,14 +14,13 @@
     def result(self):
         return self.task
 
-def run_task(task,async,*args,**kwargs):
+def run_task(task,*args,**kwargs):
     try:
         t = task.delay(*args,**kwargs)
         log.info('running task %s',t.task_id)
-        if not async:
-            t.wait()
         return t
     except:
+        log.error(traceback.format_exc())
         #pure sync version
         return ResultWrapper(task(*args,**kwargs))
     
\ No newline at end of file
--- a/pylons_app/lib/celerylib/tasks.py	Mon Sep 13 01:23:58 2010 +0200
+++ b/pylons_app/lib/celerylib/tasks.py	Mon Sep 13 01:27:41 2010 +0200
@@ -1,18 +1,82 @@
 from celery.decorators import task
+from celery.task.sets import subtask
 from datetime import datetime, timedelta
+from os.path import dirname as dn
+from pylons.i18n.translation import _
+from pylons_app.lib.celerylib import run_task
 from pylons_app.lib.helpers import person
+from pylons_app.lib.smtp_mailer import SmtpMailer
 from pylons_app.lib.utils import OrderedDict
 from time import mktime
+from vcs.backends.hg import MercurialRepository
+import ConfigParser
 import calendar
-import logging
-from vcs.backends.hg import MercurialRepository
+import os
+import traceback
+
+
+root = dn(dn(dn(dn(os.path.realpath(__file__)))))
+config = ConfigParser.ConfigParser({'here':root})
+config.read('%s/development.ini' % root)
+
+__all__ = ['whoosh_index', 'get_commits_stats',
+           'reset_user_password', 'send_email']
+
+def get_session():
+    from sqlalchemy import engine_from_config
+    from sqlalchemy.orm import sessionmaker, scoped_session
+    engine = engine_from_config(dict(config.items('app:main')), 'sqlalchemy.db1.')
+    sa = scoped_session(sessionmaker(bind=engine))
+    return sa
 
-log = logging.getLogger(__name__)
+def get_hg_settings():
+    from pylons_app.model.db import HgAppSettings
+    try:
+        sa = get_session()
+        ret = sa.query(HgAppSettings).all()
+    finally:
+        sa.remove()
+        
+    if not ret:
+        raise Exception('Could not get application settings !')
+    settings = {}
+    for each in ret:
+        settings['hg_app_' + each.app_settings_name] = each.app_settings_value    
+    
+    return settings
 
-@task()
-def whoosh_index(repo_location,full_index):
+def get_hg_ui_settings():
+    from pylons_app.model.db import HgAppUi
+    try:
+        sa = get_session()
+        ret = sa.query(HgAppUi).all()
+    finally:
+        sa.remove()
+        
+    if not ret:
+        raise Exception('Could not get application ui settings !')
+    settings = {}
+    for each in ret:
+        k = each.ui_key
+        v = each.ui_value
+        if k == '/':
+            k = 'root_path'
+        
+        if k.find('.') != -1:
+            k = k.replace('.', '_')
+        
+        if each.ui_section == 'hooks':
+            v = each.ui_active
+        
+        settings[each.ui_section + '_' + k] = v  
+    
+    return settings   
+
+@task
+def whoosh_index(repo_location, full_index):
+    log = whoosh_index.get_logger()
     from pylons_app.lib.indexers import DaemonLock
-    from pylons_app.lib.indexers.daemon import WhooshIndexingDaemon,LockHeld
+    from pylons_app.lib.indexers.daemon import WhooshIndexingDaemon, LockHeld
     try:
         l = DaemonLock()
         WhooshIndexingDaemon(repo_location=repo_location)\
@@ -23,10 +87,12 @@
         log.info('LockHeld')
         return 'LockHeld'    
 
-@task()
+@task
 def get_commits_stats(repo):
+    log = get_commits_stats.get_logger()
     aggregate = OrderedDict()
-    repo = MercurialRepository('/home/marcink/hg_repos/'+repo)
+    repos_path = get_hg_ui_settings()['paths_root_path'].replace('*','')
+    repo = MercurialRepository(repos_path + repo)
     #graph range
     td = datetime.today() + timedelta(days=1) 
     y, m, d = td.year, td.month, td.day
@@ -90,3 +156,60 @@
             % (author_key_cleaner(repo.contact),
                author_key_cleaner(repo.contact))
     return (ts_min, ts_max, d)    
+
+@task
+def reset_user_password(user_email):
+    log = reset_user_password.get_logger()
+    from pylons_app.lib import auth
+    from pylons_app.model.db import User
+    
+    try:
+        
+        try:
+            sa = get_session()
+            user = sa.query(User).filter(User.email == user_email).scalar()
+            new_passwd = auth.PasswordGenerator().gen_password(8,
+                             auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
+            user.password = auth.get_crypt_password(new_passwd)
+            sa.add(user)
+            sa.commit()
+            log.info('change password for %s', user_email)
+            if new_passwd is None:
+                raise Exception('unable to generate new password')
+            
+        except:
+            log.error(traceback.format_exc())
+            sa.rollback()
+        
+        run_task(send_email, user_email,
+                 "Your new hg-app password",
+                 'Your new hg-app password:%s' % (new_passwd))
+        log.info('send new password mail to %s', user_email)
+        
+        
+    except:
+        log.error('Failed to update user password')
+        log.error(traceback.format_exc())
+    return True
+
+@task    
+def send_email(recipients, subject, body):
+    log = send_email.get_logger()
+    email_config = dict(config.items('DEFAULT')) 
+    mail_from = email_config.get('app_email_from')
+    user = email_config.get('smtp_username')
+    passwd = email_config.get('smtp_password')
+    mail_server = email_config.get('smtp_server')
+    mail_port = email_config.get('smtp_port')
+    tls = email_config.get('smtp_use_tls')
+    ssl = False
+    
+    try:
+        m = SmtpMailer(mail_from, user, passwd, mail_server, 
+                       mail_port, ssl, tls)
+        m.send(recipients, subject, body)  
+    except:
+        log.error('Mail sending failed')
+        log.error(traceback.format_exc())
+        return False
+    return True
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pylons_app/lib/smtp_mailer.py	Mon Sep 13 01:27:41 2010 +0200
@@ -0,0 +1,118 @@
+import logging
+import smtplib
+import mimetypes
+from email.mime.multipart import MIMEMultipart
+from email.mime.image import MIMEImage
+from email.mime.audio import MIMEAudio
+from email.mime.base import MIMEBase
+from email.mime.text import MIMEText
+from email.utils import formatdate
+from email import encoders
+
+class SmtpMailer(object):
+    """simple smtp mailer class
+    
+    mailer = SmtpMailer(mail_from, user, passwd, mail_server, mail_port, ssl, tls)
+    mailer.send(recipients, subject, body, attachment_files)    
+    
+    :param recipients might be a list of string or single string
+    :param attachment_files is a dict of {filename:location} 
+    it tries to guess the mimetype and attach the file
+    """
+
+    def __init__(self, mail_from, user, passwd, mail_server,
+                    mail_port=None, ssl=False, tls=False):
+        
+        self.mail_from = mail_from
+        self.mail_server = mail_server
+        self.mail_port = mail_port
+        self.user = user
+        self.passwd = passwd
+        self.ssl = ssl
+        self.tls = tls
+        self.debug = False
+        
+    def send(self, recipients=[], subject='', body='', attachment_files={}):
+
+        if isinstance(recipients, basestring):
+            recipients = [recipients]
+        if self.ssl:
+            smtp_serv = smtplib.SMTP_SSL(self.mail_server, self.mail_port)
+        else:
+            smtp_serv = smtplib.SMTP(self.mail_server, self.mail_port)
+
+        if self.tls:
+            smtp_serv.starttls()
+         
+        if self.debug:    
+            smtp_serv.set_debuglevel(1)
+
+        smtp_serv.ehlo("mailer")
+
+        #if server requires authorization you must provide login and password
+        smtp_serv.login(self.user, self.passwd)
+
+        date_ = formatdate(localtime=True)
+        msg = MIMEMultipart()
+        msg['From'] = self.mail_from
+        msg['To'] = ','.join(recipients)
+        msg['Date'] = date_
+        msg['Subject'] = subject
+        msg.preamble = 'You will not see this in a MIME-aware mail reader.\n'
+
+        msg.attach(MIMEText(body))
+
+        if attachment_files:
+            self.__atach_files(msg, attachment_files)
+
+        smtp_serv.sendmail(self.mail_from, recipients, msg.as_string())
+        logging.info('MAIL SEND TO: %s' % recipients)
+        smtp_serv.quit()
+
+
+    def __atach_files(self, msg, attachment_files):
+        if isinstance(attachment_files, dict):
+            for f_name, msg_file in attachment_files.items():
+                ctype, encoding = mimetypes.guess_type(f_name)
+                logging.info("guessing file %s type based on %s" , ctype, f_name)
+                if ctype is None or encoding is not None:
+                    # No guess could be made, or the file is encoded (compressed), so
+                    # use a generic bag-of-bits type.
+                    ctype = 'application/octet-stream'
+                maintype, subtype = ctype.split('/', 1)
+                if maintype == 'text':
+                    # Note: we should handle calculating the charset
+                    file_part = MIMEText(self.get_content(msg_file), 
+                                         _subtype=subtype)
+                elif maintype == 'image':
+                    file_part = MIMEImage(self.get_content(msg_file), 
+                                          _subtype=subtype)
+                elif maintype == 'audio':
+                    file_part = MIMEAudio(self.get_content(msg_file), 
+                                          _subtype=subtype)
+                else:
+                    file_part = MIMEBase(maintype, subtype)
+                    file_part.set_payload(self.get_content(msg_file))
+                    # Encode the payload using Base64
+                    encoders.encode_base64(msg)
+                # Set the filename parameter
+                file_part.add_header('Content-Disposition', 'attachment', 
+                                     filename=f_name)
+                file_part.add_header('Content-Type', ctype, name=f_name)
+                msg.attach(file_part)
+        else:
+            raise Exception('Attachment files should be' 
+                            'a dict in format {"filename":"filepath"}')    
+
+    def get_content(self, msg_file):
+        '''
+        Get content based on type, if content is a string do open first
+        else just read because it's a probably open file object
+        @param msg_file:
+        '''
+        if isinstance(msg_file, str):
+            return open(msg_file, "rb").read()
+        else:
+            #just for safe seek to 0
+            msg_file.seek(0)
+            return msg_file.read()
--- a/pylons_app/model/forms.py	Mon Sep 13 01:23:58 2010 +0200
+++ b/pylons_app/model/forms.py	Mon Sep 13 01:27:41 2010 +0200
@@ -102,7 +102,7 @@
                                      error_dict=self.e_dict)            
         if user:
             if user.active:
-                if user.username == username and check_password(password, 
+                if user.username == username and check_password(password,
                                                                 user.password):
                     return value
                 else:
@@ -208,7 +208,20 @@
         
         raise formencode.Invalid(msg, value, state,
                                      error_dict={'paths_root_path':msg})            
-                       
+
+class ValidSystemEmail(formencode.validators.FancyValidator):
+    def to_python(self, value, state):
+        sa = meta.Session
+        try:
+            user = sa.query(User).filter(User.email == value).scalar()
+            if  user is None:
+                raise formencode.Invalid(_("That e-mail address doesn't exist.") ,
+                                         value, state)
+        finally:
+            meta.Session.remove()
+            
+        return value     
+
 #===============================================================================
 # FORMS        
 #===============================================================================
@@ -255,8 +268,14 @@
     return _UserForm
 
 RegisterForm = UserForm
-    
-    
+
+def PasswordResetForm():
+    class _PasswordResetForm(formencode.Schema):
+        allow_extra_fields = True
+        filter_extra_fields = True
+        email = All(ValidSystemEmail(), Email(not_empty=True))             
+    return _PasswordResetForm
+
 def RepoForm(edit=False, old_data={}):
     class _RepoForm(formencode.Schema):
         allow_extra_fields = True
--- a/pylons_app/model/user_model.py	Mon Sep 13 01:23:58 2010 +0200
+++ b/pylons_app/model/user_model.py	Mon Sep 13 01:27:41 2010 +0200
@@ -2,7 +2,7 @@
 # encoding: utf-8
 # Model for users
 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
- 
+# 
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License
 # as published by the Free Software Foundation; version 2
@@ -23,10 +23,12 @@
 Model for users
 @author: marcink
 """
-
+from pylons_app.lib import auth
+from pylons.i18n.translation import _
+from pylons_app.lib.celerylib import tasks, run_task
 from pylons_app.model.db import User
 from pylons_app.model.meta import Session
-from pylons.i18n.translation import _
+import traceback
 import logging
 log = logging.getLogger(__name__)
 
@@ -43,7 +45,7 @@
     def get_user(self, id):
         return self.sa.query(User).get(id)
     
-    def get_user_by_name(self,name):
+    def get_user_by_name(self, name):
         return self.sa.query(User).filter(User.username == name).scalar()
     
     def create(self, form_data):
@@ -54,8 +56,8 @@
                 
             self.sa.add(new_user)
             self.sa.commit()
-        except Exception as e:
-            log.error(e)
+        except:
+            log.error(traceback.format_exc())
             self.sa.rollback()
             raise      
     
@@ -68,8 +70,8 @@
                 
             self.sa.add(new_user)
             self.sa.commit()
-        except Exception as e:
-            log.error(e)
+        except:
+            log.error(traceback.format_exc())
             self.sa.rollback()
             raise      
     
@@ -88,8 +90,8 @@
                 
             self.sa.add(new_user)
             self.sa.commit()
-        except Exception as e:
-            log.error(e)
+        except:
+            log.error(traceback.format_exc())
             self.sa.rollback()
             raise      
         
@@ -109,13 +111,12 @@
                 
             self.sa.add(new_user)
             self.sa.commit()
-        except Exception as e:
-            log.error(e)
+        except:
+            log.error(traceback.format_exc())
             self.sa.rollback()
             raise 
                 
     def delete(self, id):
-        
         try:
             
             user = self.sa.query(User).get(id)
@@ -125,7 +126,10 @@
                                   " crucial for entire application"))
             self.sa.delete(user)
             self.sa.commit()            
-        except Exception as e:
-            log.error(e)
+        except:
+            log.error(traceback.format_exc())
             self.sa.rollback()
             raise        
+
+    def reset_password(self, data):
+        run_task(tasks.reset_user_password, data['email'])
--- a/pylons_app/templates/login.html	Mon Sep 13 01:23:58 2010 +0200
+++ b/pylons_app/templates/login.html	Mon Sep 13 01:27:41 2010 +0200
@@ -60,7 +60,7 @@
                     <!-- end fields -->
                     <!-- links -->
                     <div class="links">
-                        ${h.link_to(_('Forgot your password ?'),h.url('#'))}
+                        ${h.link_to(_('Forgot your password ?'),h.url('reset_password'))}
                         %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
 	                         / 
 	                        ${h.link_to(_("Don't have an account ?"),h.url('register'))}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pylons_app/templates/password_reset.html	Mon Sep 13 01:27:41 2010 +0200
@@ -0,0 +1,54 @@
+## -*- coding: utf-8 -*-
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" id="mainhtml">
+    <head>
+        <title>${_('Reset You password to hg-app')}</title>
+        <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
+        <link rel="icon" href="/images/hgicon.png" type="image/png" />
+        <meta name="robots" content="index, nofollow"/>
+            
+        <!-- stylesheets -->
+        <link rel="stylesheet" type="text/css" href="/css/reset.css" />
+        <link rel="stylesheet" type="text/css" href="/css/style.css" media="screen" />
+        <link id="color" rel="stylesheet" type="text/css" href="/css/colors/blue.css" />
+
+        <!-- scripts -->
+
+    </head>
+    <body>
+		<div id="register">
+			
+			<div class="title">
+				<h5>${_('Reset You password to hg-app')}</h5>
+                <div class="corner tl"></div>
+                <div class="corner tr"></div>				
+			</div>
+			<div class="inner">
+			    ${h.form(url('password_reset'))}
+			    <div class="form">
+			        <!-- fields -->
+			        <div class="fields">
+			            
+			             <div class="field">
+			                <div class="label">
+			                    <label for="email">${_('Email address')}:</label>
+			                </div>
+			                <div class="input">
+			                    ${h.text('email')}
+			                </div>
+			             </div>
+			                        
+			            <div class="buttons">
+				            <div class="nohighlight">
+				              ${h.submit('send','Reset my password',class_="ui-button ui-widget ui-state-default ui-corner-all")}
+							  	<div class="activation_msg">${_('Your new password will be send to matching email address')}</div>
+				            </div>
+			            </div>             
+			    	</div>
+			    </div>
+			    ${h.end_form()}
+			</div>    
+	    </div>
+    </body>
+</html>
+