diff --git a/celeryconfig.py b/celeryconfig.py --- a/celeryconfig.py +++ b/celeryconfig.py @@ -8,6 +8,7 @@ CELERY_IMPORTS = ("pylons_app.lib.celery CELERY_RESULT_BACKEND = "database" CELERY_RESULT_DBURI = "sqlite:///hg_app.db" +BROKER_CONNECTION_MAX_RETRIES = 30 ## Broker settings. BROKER_HOST = "localhost" diff --git a/development.ini b/development.ini --- a/development.ini +++ b/development.ini @@ -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 diff --git a/pylons_app/config/routing.py b/pylons_app/config/routing.py --- a/pylons_app/config/routing.py +++ b/pylons_app/config/routing.py @@ -110,10 +110,11 @@ def make_map(config): #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', diff --git a/pylons_app/controllers/login.py b/pylons_app/controllers/login.py --- a/pylons_app/controllers/login.py +++ b/pylons_app/controllers/login.py @@ -28,7 +28,9 @@ from pylons import request, response, se 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 @@ class LoginController(BaseController): 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 @@ class LoginController(BaseController): 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 @@ class LoginController(BaseController): 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 @@ class LoginController(BaseController): 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() diff --git a/pylons_app/lib/auth.py b/pylons_app/lib/auth.py --- a/pylons_app/lib/auth.py +++ b/pylons_app/lib/auth.py @@ -34,9 +34,36 @@ from sqlalchemy.orm.exc import NoResultF 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 @@ class LoginRequired(object): 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""" diff --git a/pylons_app/lib/celerylib/__init__.py b/pylons_app/lib/celerylib/__init__.py --- a/pylons_app/lib/celerylib/__init__.py +++ b/pylons_app/lib/celerylib/__init__.py @@ -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 @@ class ResultWrapper(object): 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 diff --git a/pylons_app/lib/celerylib/tasks.py b/pylons_app/lib/celerylib/tasks.py --- a/pylons_app/lib/celerylib/tasks.py +++ b/pylons_app/lib/celerylib/tasks.py @@ -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 @@ def whoosh_index(repo_location,full_inde 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 @@ def get_commits_stats(repo): % (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 diff --git a/pylons_app/lib/smtp_mailer.py b/pylons_app/lib/smtp_mailer.py new file mode 100644 --- /dev/null +++ b/pylons_app/lib/smtp_mailer.py @@ -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() diff --git a/pylons_app/model/forms.py b/pylons_app/model/forms.py --- a/pylons_app/model/forms.py +++ b/pylons_app/model/forms.py @@ -102,7 +102,7 @@ class ValidAuth(formencode.validators.Fa 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 @@ class ValidPath(formencode.validators.Fa 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 @@ def UserForm(edit=False, old_data={}): 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 diff --git a/pylons_app/model/user_model.py b/pylons_app/model/user_model.py --- a/pylons_app/model/user_model.py +++ b/pylons_app/model/user_model.py @@ -2,7 +2,7 @@ # encoding: utf-8 # Model for users # Copyright (C) 2009-2010 Marcin Kuzminski - +# # 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 @@ Created on April 9, 2010 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 @@ class UserModel(object): 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 @@ class UserModel(object): 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 @@ class UserModel(object): 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 @@ class UserModel(object): 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 @@ class UserModel(object): 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 @@ class UserModel(object): " 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']) diff --git a/pylons_app/templates/login.html b/pylons_app/templates/login.html --- a/pylons_app/templates/login.html +++ b/pylons_app/templates/login.html @@ -60,7 +60,7 @@