def reset_password_link(self, data): from kallithea.lib.celerylib import tasks, run_task from kallithea.model.notification import EmailNotificationModel import kallithea.lib.helpers as h user_email = data['email'] user = User.get_by_email(user_email) if user: log.debug('password reset user found %s' % user) link = h.canonical_url('reset_password_confirmation', key=user.api_key) reg_type = EmailNotificationModel.TYPE_PASSWORD_RESET body = EmailNotificationModel().get_email_tmpl(reg_type, 'txt', user=user.short_contact, reset_url=link) html_body = EmailNotificationModel().get_email_tmpl(reg_type, 'html', user=user.short_contact, reset_url=link) log.debug('sending email') run_task(tasks.send_email, [user_email], _("Password reset link"), body, html_body) log.info('send new password mail to %s' % user_email) else: log.debug("password reset email %s not found" % user_email) return True
def statistics(self, repo_name): if c.db_repo.enable_statistics: c.show_stats = True c.no_data_msg = _('No data ready yet') else: c.show_stats = False c.no_data_msg = _('Statistics are disabled for this repository') td = date.today() + timedelta(days=1) td_1m = td - timedelta(days=calendar.mdays[td.month]) td_1y = td - timedelta(days=365) ts_min_m = mktime(td_1m.timetuple()) ts_min_y = mktime(td_1y.timetuple()) ts_max_y = mktime(td.timetuple()) c.ts_min = ts_min_m c.ts_max = ts_max_y stats = self.sa.query(Statistics) \ .filter(Statistics.repository == c.db_repo) \ .scalar() c.stats_percentage = 0 if stats and stats.languages: c.no_data = False is c.db_repo.enable_statistics lang_stats_d = json.loads(stats.languages) c.commit_data = stats.commit_activity c.overview_data = stats.commit_activity_combined lang_stats = ((x, {"count": y, "desc": LANGUAGES_EXTENSIONS_MAP.get(x)}) for x, y in lang_stats_d.items()) c.trending_languages = json.dumps( sorted(lang_stats, reverse=True, key=lambda k: k[1])[:10] ) last_rev = stats.stat_on_revision + 1 c.repo_last_rev = c.db_repo_scm_instance.count() \ if c.db_repo_scm_instance.revisions else 0 if last_rev == 0 or c.repo_last_rev == 0: pass else: c.stats_percentage = '%.2f' % ((float((last_rev)) / c.repo_last_rev) * 100) else: c.commit_data = json.dumps({}) c.overview_data = json.dumps([[ts_min_y, 0], [ts_max_y, 10]]) c.trending_languages = json.dumps({}) c.no_data = True recurse_limit = 500 # don't recurse more than 500 times when parsing run_task(get_commits_stats, c.db_repo.repo_name, ts_min_y, ts_max_y, recurse_limit) return render('summary/statistics.html')
def send_reset_password_email(self, data): """ Sends email with a password reset token and link to the password reset confirmation page with all information (including the token) pre-filled. Also returns URL of that page, only without the token, allowing users to copy-paste or manually enter the token from the email. """ from kallithea.lib.celerylib import tasks, run_task from kallithea.model.notification import EmailNotificationModel import kallithea.lib.helpers as h user_email = data['email'] user = User.get_by_email(user_email) timestamp = int(time.time()) if user is not None: if self.can_change_password(user): log.debug('password reset user %s found', user) token = self.get_reset_password_token(user, timestamp, h.authentication_token()) # URL must be fully qualified; but since the token is locked to # the current browser session, we must provide a URL with the # current scheme and hostname, rather than the canonical_url. link = h.url('reset_password_confirmation', qualified=True, email=user_email, timestamp=timestamp, token=token) else: log.debug('password reset user %s found but was managed', user) token = link = None reg_type = EmailNotificationModel.TYPE_PASSWORD_RESET body = EmailNotificationModel().get_email_tmpl( reg_type, 'txt', user=user.short_contact, reset_token=token, reset_url=link) html_body = EmailNotificationModel().get_email_tmpl( reg_type, 'html', user=user.short_contact, reset_token=token, reset_url=link) log.debug('sending email') run_task(tasks.send_email, [user_email], _("Password reset link"), body, html_body) log.info('send new password mail to %s', user_email) else: log.debug("password reset email %s not found", user_email) return h.url('reset_password_confirmation', email=user_email, timestamp=timestamp)
def statistics(self, repo_name): if c.db_repo.enable_statistics: c.show_stats = True c.no_data_msg = _('No data ready yet') else: c.show_stats = False c.no_data_msg = _('Statistics are disabled for this repository') td = date.today() + timedelta(days=1) td_1m = td - timedelta(days=calendar.mdays[td.month]) td_1y = td - timedelta(days=365) ts_min_m = mktime(td_1m.timetuple()) ts_min_y = mktime(td_1y.timetuple()) ts_max_y = mktime(td.timetuple()) c.ts_min = ts_min_m c.ts_max = ts_max_y stats = self.sa.query(Statistics)\ .filter(Statistics.repository == c.db_repo)\ .scalar() if stats and stats.languages: c.no_data = False is c.db_repo.enable_statistics lang_stats_d = json.loads(stats.languages) c.commit_data = stats.commit_activity c.overview_data = stats.commit_activity_combined lang_stats = ((x, { "count": y, "desc": LANGUAGES_EXTENSIONS_MAP.get(x) }) for x, y in lang_stats_d.items()) c.trending_languages = json.dumps( sorted(lang_stats, reverse=True, key=lambda k: k[1])[:10]) last_rev = stats.stat_on_revision + 1 c.repo_last_rev = c.db_repo_scm_instance.count()\ if c.db_repo_scm_instance.revisions else 0 if last_rev == 0 or c.repo_last_rev == 0: pass else: c.stats_percentage = '%.2f' % ((float( (last_rev)) / c.repo_last_rev) * 100) else: c.commit_data = json.dumps({}) c.overview_data = json.dumps([[ts_min_y, 0], [ts_max_y, 10]]) c.trending_languages = json.dumps({}) c.no_data = True recurse_limit = 500 # don't recurse more than 500 times when parsing run_task(get_commits_stats, c.db_repo.repo_name, ts_min_y, ts_max_y, recurse_limit) return render('summary/statistics.html')
def create_fork(self, form_data, cur_user): """ Simple wrapper into executing celery task for fork creation :param form_data: :param cur_user: """ from kallithea.lib.celerylib import tasks, run_task return run_task(tasks.create_repo_fork, form_data, cur_user)
def create(self, form_data, cur_user): """ Create repository using celery tasks :param form_data: :param cur_user: """ from kallithea.lib.celerylib import tasks, run_task return run_task(tasks.create_repo, form_data, cur_user)
def settings_search(self): """GET /admin/settings/search: All items in the collection""" # url('admin_settings_search') c.active = 'search' if request.POST: repo_location = self._get_hg_ui_settings()['paths_root_path'] full_index = request.POST.get('full_index', False) run_task(tasks.whoosh_index, repo_location, full_index) h.flash(_('Whoosh reindex task scheduled'), category='success') raise HTTPFound(location=url('admin_settings_search')) defaults = Setting.get_app_settings() defaults.update(self._get_hg_ui_settings()) return htmlfill.render( render('admin/settings/settings.html'), defaults=defaults, encoding="UTF-8", force_defaults=False)
def reset_password(self, user_email, new_passwd): from kallithea.lib.celerylib import tasks, run_task from kallithea.lib import auth user = User.get_by_email(user_email) if user is not None: if not self.can_change_password(user): raise Exception('trying to change password for external user') user.password = auth.get_crypt_password(new_passwd) Session().add(user) Session().commit() log.info('change password for %s', user_email) if new_passwd is None: raise Exception('unable to set new password') run_task(tasks.send_email, [user_email], _('Password reset notification'), _('The password to your account %s has been changed using password reset form.') % (user.username,)) log.info('send password reset mail to %s', user_email) return True
def reset_password(self, data): from kallithea.lib.celerylib import tasks, run_task from kallithea.lib import auth user_email = data['email'] user = User.get_by_email(user_email) new_passwd = auth.PasswordGenerator().gen_password(8, auth.PasswordGenerator.ALPHABETS_BIG_SMALL) if user: user.password = auth.get_crypt_password(new_passwd) Session().add(user) Session().commit() log.info('change password for %s' % user_email) if new_passwd is None: raise Exception('unable to generate new password') run_task(tasks.send_email, [user_email], _('Your new password'), _('Your new Kallithea password:%s') % (new_passwd,)) log.info('send new password mail to %s' % user_email) return True
def settings_email(self): """GET /admin/settings/email: All items in the collection""" # url('admin_settings_email') c.active = 'email' if request.POST: test_email = request.POST.get('test_email') test_email_subj = 'Kallithea test email' test_body = ('Kallithea Email test, ' 'Kallithea version: %s' % c.kallithea_version) if not test_email: h.flash(_('Please enter email address'), category='error') raise HTTPFound(location=url('admin_settings_email')) test_email_txt_body = EmailNotificationModel() \ .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT, 'txt', body=test_body) test_email_html_body = EmailNotificationModel() \ .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT, 'html', body=test_body) recipients = [test_email] if test_email else None run_task(tasks.send_email, recipients, test_email_subj, test_email_txt_body, test_email_html_body) h.flash(_('Send email task created'), category='success') raise HTTPFound(location=url('admin_settings_email')) defaults = Setting.get_app_settings() defaults.update(self._get_hg_ui_settings()) import kallithea c.ini = kallithea.CONFIG return htmlfill.render( render('admin/settings/settings.html'), defaults=defaults, encoding="UTF-8", force_defaults=False)
def get_commits_stats(repo_name, ts_min_y, ts_max_y, recurse_limit=100): log = get_logger(get_commits_stats) DBS = get_session() lockkey = __get_lockkey('get_commits_stats', repo_name, ts_min_y, ts_max_y) lockkey_path = config['app_conf']['cache_dir'] log.info('running task with lockkey %s' % lockkey) try: lock = l = DaemonLock(file_=jn(lockkey_path, lockkey)) # for js data compatibility cleans the key for person from ' akc = lambda k: person(k).replace('"', "") co_day_auth_aggr = {} commits_by_day_aggregate = {} repo = Repository.get_by_repo_name(repo_name) if repo is None: return True repo = repo.scm_instance repo_size = repo.count() # return if repo have no revisions if repo_size < 1: lock.release() return True skip_date_limit = True parse_limit = int(config['app_conf'].get('commit_parse_limit')) last_rev = None last_cs = None timegetter = itemgetter('time') dbrepo = DBS.query(Repository)\ .filter(Repository.repo_name == repo_name).scalar() cur_stats = DBS.query(Statistics)\ .filter(Statistics.repository == dbrepo).scalar() if cur_stats is not None: last_rev = cur_stats.stat_on_revision if last_rev == repo.get_changeset().revision and repo_size > 1: # pass silently without any work if we're not on first revision or # current state of parsing revision(from db marker) is the # last revision lock.release() return True if cur_stats: commits_by_day_aggregate = OrderedDict( json.loads(cur_stats.commit_activity_combined)) co_day_auth_aggr = json.loads(cur_stats.commit_activity) log.debug('starting parsing %s' % parse_limit) lmktime = mktime last_rev = last_rev + 1 if last_rev >= 0 else 0 log.debug('Getting revisions from %s to %s' % (last_rev, last_rev + parse_limit)) for cs in repo[last_rev:last_rev + parse_limit]: log.debug('parsing %s' % cs) last_cs = cs # remember last parsed changeset k = lmktime([ cs.date.timetuple()[0], cs.date.timetuple()[1], cs.date.timetuple()[2], 0, 0, 0, 0, 0, 0 ]) if akc(cs.author) in co_day_auth_aggr: try: l = [ timegetter(x) for x in co_day_auth_aggr[akc(cs.author)]['data'] ] time_pos = l.index(k) except ValueError: time_pos = None if time_pos >= 0 and time_pos is not None: datadict = \ co_day_auth_aggr[akc(cs.author)]['data'][time_pos] datadict["commits"] += 1 datadict["added"] += len(cs.added) datadict["changed"] += len(cs.changed) datadict["removed"] += len(cs.removed) else: if k >= ts_min_y and k <= ts_max_y or skip_date_limit: datadict = { "time": k, "commits": 1, "added": len(cs.added), "changed": len(cs.changed), "removed": len(cs.removed), } co_day_auth_aggr[akc(cs.author)]['data']\ .append(datadict) else: if k >= ts_min_y and k <= ts_max_y or skip_date_limit: co_day_auth_aggr[akc(cs.author)] = { "label": akc(cs.author), "data": [{ "time": k, "commits": 1, "added": len(cs.added), "changed": len(cs.changed), "removed": len(cs.removed), }], "schema": ["commits"], } #gather all data by day if k in commits_by_day_aggregate: commits_by_day_aggregate[k] += 1 else: commits_by_day_aggregate[k] = 1 overview_data = sorted(commits_by_day_aggregate.items(), key=itemgetter(0)) if not co_day_auth_aggr: co_day_auth_aggr[akc(repo.contact)] = { "label": akc(repo.contact), "data": [0, 1], "schema": ["commits"], } stats = cur_stats if cur_stats else Statistics() stats.commit_activity = json.dumps(co_day_auth_aggr) stats.commit_activity_combined = json.dumps(overview_data) log.debug('last revision %s' % last_rev) leftovers = len(repo.revisions[last_rev:]) log.debug('revisions to parse %s' % leftovers) if last_rev == 0 or leftovers < parse_limit: log.debug('getting code trending stats') stats.languages = json.dumps(__get_codes_stats(repo_name)) try: stats.repository = dbrepo stats.stat_on_revision = last_cs.revision if last_cs else 0 DBS.add(stats) DBS.commit() except: log.error(traceback.format_exc()) DBS.rollback() lock.release() return False # final release lock.release() # execute another task if celery is enabled if len(repo.revisions) > 1 and CELERY_ON and recurse_limit > 0: recurse_limit -= 1 run_task(get_commits_stats, repo_name, ts_min_y, ts_max_y, recurse_limit) if recurse_limit <= 0: log.debug('Breaking recursive mode due to reach of recurse limit') return True except LockHeld: log.info('LockHeld') return 'Task with key %s already running' % lockkey
def create(self, created_by, subject, body, recipients=None, type_=Notification.TYPE_MESSAGE, with_email=True, email_kwargs={}): """ Creates notification of given type :param created_by: int, str or User instance. User who created this notification :param subject: :param body: :param recipients: list of int, str or User objects, when None is given send to all admins :param type_: type of notification :param with_email: send email with this notification :param email_kwargs: additional dict to pass as args to email template """ from kallithea.lib.celerylib import tasks, run_task if recipients and not getattr(recipients, '__iter__', False): raise Exception('recipients must be a list or iterable') created_by_obj = self._get_user(created_by) recipients_objs = [] if recipients: for u in recipients: obj = self._get_user(u) if obj: recipients_objs.append(obj) else: # TODO: inform user that requested operation couldn't be completed log.error('cannot email unknown user %r', u) recipients_objs = set(recipients_objs) log.debug('sending notifications %s to %s' % (type_, recipients_objs)) elif recipients is None: # empty recipients means to all admins recipients_objs = User.query().filter(User.admin == True).all() log.debug('sending notifications %s to admins: %s' % (type_, recipients_objs)) #else: silently skip notification mails? # TODO: inform user who are notified notif = Notification.create(created_by=created_by_obj, subject=subject, body=body, recipients=recipients_objs, type_=type_) if not with_email: return notif #don't send email to person who created this comment rec_objs = set(recipients_objs).difference(set([created_by_obj])) headers = None if 'threading' in email_kwargs: headers = { 'References': ' '.join('<%s>' % x for x in email_kwargs['threading']) } # send email with notification to all other participants for rec in rec_objs: ## this is passed into template html_kwargs = { 'subject': subject, 'body': h.rst_w_mentions(body), 'when': h.fmt_date(notif.created_on), 'user': notif.created_by_user.username, } txt_kwargs = { 'subject': subject, 'body': body, 'when': h.fmt_date(notif.created_on), 'user': notif.created_by_user.username, } html_kwargs.update(email_kwargs) txt_kwargs.update(email_kwargs) email_subject = EmailNotificationModel()\ .get_email_description(type_, **txt_kwargs) email_txt_body = EmailNotificationModel()\ .get_email_tmpl(type_, 'txt', **txt_kwargs) email_html_body = EmailNotificationModel()\ .get_email_tmpl(type_, 'html', **html_kwargs) run_task(tasks.send_email, [rec.email], email_subject, email_txt_body, email_html_body, headers) return notif
def create(self, created_by, subject, body, recipients=None, type_=Notification.TYPE_MESSAGE, with_email=True, email_kwargs=None, repo_name=None): """ Creates notification of given type :param created_by: int, str or User instance. User who created this notification :param subject: :param body: :param recipients: list of int, str or User objects, when None is given send to all admins :param type_: type of notification :param with_email: send email with this notification :param email_kwargs: additional dict to pass as args to email template """ from kallithea.lib.celerylib import tasks, run_task email_kwargs = email_kwargs or {} if recipients and not getattr(recipients, '__iter__', False): raise Exception('recipients must be a list or iterable') created_by_obj = self._get_user(created_by) recipients_objs = [] if recipients: for u in recipients: obj = self._get_user(u) if obj is not None: recipients_objs.append(obj) else: # TODO: inform user that requested operation couldn't be completed log.error('cannot email unknown user %r', u) recipients_objs = set(recipients_objs) log.debug('sending notifications %s to %s', type_, recipients_objs ) elif recipients is None: # empty recipients means to all admins recipients_objs = User.query().filter(User.admin == True).all() log.debug('sending notifications %s to admins: %s', type_, recipients_objs ) #else: silently skip notification mails? # TODO: inform user who are notified notif = Notification.create( created_by=created_by_obj, subject=subject, body=body, recipients=recipients_objs, type_=type_ ) if not with_email: return notif #don't send email to person who created this comment rec_objs = set(recipients_objs).difference(set([created_by_obj])) headers = None if 'threading' in email_kwargs: headers = {'References': ' '.join('<%s>' % x for x in email_kwargs['threading'])} # send email with notification to all other participants for rec in rec_objs: ## this is passed into template html_kwargs = { 'subject': subject, 'body': h.render_w_mentions(body, repo_name), 'when': h.fmt_date(notif.created_on), 'user': notif.created_by_user.username, } txt_kwargs = { 'subject': subject, 'body': body, 'when': h.fmt_date(notif.created_on), 'user': notif.created_by_user.username, } html_kwargs.update(email_kwargs) txt_kwargs.update(email_kwargs) email_subject = EmailNotificationModel() \ .get_email_description(type_, **txt_kwargs) email_txt_body = EmailNotificationModel() \ .get_email_tmpl(type_, 'txt', **txt_kwargs) email_html_body = EmailNotificationModel() \ .get_email_tmpl(type_, 'html', **html_kwargs) run_task(tasks.send_email, [rec.email], email_subject, email_txt_body, email_html_body, headers, author=created_by_obj) return notif
def get_commits_stats(repo_name, ts_min_y, ts_max_y, recurse_limit=100): log = get_logger(get_commits_stats) DBS = get_session() lockkey = __get_lockkey('get_commits_stats', repo_name, ts_min_y, ts_max_y) lockkey_path = config['app_conf']['cache_dir'] log.info('running task with lockkey %s', lockkey) try: lock = l = DaemonLock(file_=jn(lockkey_path, lockkey)) # for js data compatibility cleans the key for person from ' akc = lambda k: person(k).replace('"', "") co_day_auth_aggr = {} commits_by_day_aggregate = {} repo = Repository.get_by_repo_name(repo_name) if repo is None: return True repo = repo.scm_instance repo_size = repo.count() # return if repo have no revisions if repo_size < 1: lock.release() return True skip_date_limit = True parse_limit = int(config['app_conf'].get('commit_parse_limit')) last_rev = None last_cs = None timegetter = itemgetter('time') dbrepo = DBS.query(Repository) \ .filter(Repository.repo_name == repo_name).scalar() cur_stats = DBS.query(Statistics) \ .filter(Statistics.repository == dbrepo).scalar() if cur_stats is not None: last_rev = cur_stats.stat_on_revision if last_rev == repo.get_changeset().revision and repo_size > 1: # pass silently without any work if we're not on first revision or # current state of parsing revision(from db marker) is the # last revision lock.release() return True if cur_stats: commits_by_day_aggregate = OrderedDict(json.loads( cur_stats.commit_activity_combined)) co_day_auth_aggr = json.loads(cur_stats.commit_activity) log.debug('starting parsing %s', parse_limit) lmktime = mktime last_rev = last_rev + 1 if last_rev >= 0 else 0 log.debug('Getting revisions from %s to %s', last_rev, last_rev + parse_limit ) for cs in repo[last_rev:last_rev + parse_limit]: log.debug('parsing %s', cs) last_cs = cs # remember last parsed changeset k = lmktime([cs.date.timetuple()[0], cs.date.timetuple()[1], cs.date.timetuple()[2], 0, 0, 0, 0, 0, 0]) if akc(cs.author) in co_day_auth_aggr: try: l = [timegetter(x) for x in co_day_auth_aggr[akc(cs.author)]['data']] time_pos = l.index(k) except ValueError: time_pos = None if time_pos >= 0 and time_pos is not None: datadict = \ co_day_auth_aggr[akc(cs.author)]['data'][time_pos] datadict["commits"] += 1 datadict["added"] += len(cs.added) datadict["changed"] += len(cs.changed) datadict["removed"] += len(cs.removed) else: if k >= ts_min_y and k <= ts_max_y or skip_date_limit: datadict = {"time": k, "commits": 1, "added": len(cs.added), "changed": len(cs.changed), "removed": len(cs.removed), } co_day_auth_aggr[akc(cs.author)]['data'] \ .append(datadict) else: if k >= ts_min_y and k <= ts_max_y or skip_date_limit: co_day_auth_aggr[akc(cs.author)] = { "label": akc(cs.author), "data": [{"time":k, "commits":1, "added":len(cs.added), "changed":len(cs.changed), "removed":len(cs.removed), }], "schema": ["commits"], } #gather all data by day if k in commits_by_day_aggregate: commits_by_day_aggregate[k] += 1 else: commits_by_day_aggregate[k] = 1 overview_data = sorted(commits_by_day_aggregate.items(), key=itemgetter(0)) if not co_day_auth_aggr: co_day_auth_aggr[akc(repo.contact)] = { "label": akc(repo.contact), "data": [0, 1], "schema": ["commits"], } stats = cur_stats if cur_stats else Statistics() stats.commit_activity = json.dumps(co_day_auth_aggr) stats.commit_activity_combined = json.dumps(overview_data) log.debug('last revision %s', last_rev) leftovers = len(repo.revisions[last_rev:]) log.debug('revisions to parse %s', leftovers) if last_rev == 0 or leftovers < parse_limit: log.debug('getting code trending stats') stats.languages = json.dumps(__get_codes_stats(repo_name)) try: stats.repository = dbrepo stats.stat_on_revision = last_cs.revision if last_cs else 0 DBS.add(stats) DBS.commit() except: log.error(traceback.format_exc()) DBS.rollback() lock.release() return False # final release lock.release() # execute another task if celery is enabled if len(repo.revisions) > 1 and CELERY_ON and recurse_limit > 0: recurse_limit -= 1 run_task(get_commits_stats, repo_name, ts_min_y, ts_max_y, recurse_limit) if recurse_limit <= 0: log.debug('Breaking recursive mode due to reach of recurse limit') return True except LockHeld: log.info('LockHeld') return 'Task with key %s already running' % lockkey