Changeset - 3397e3457f9c
[Not reviewed]
default
0 4 7
Andrew Shadura - 11 years ago 2014-10-26 09:53:22
andrew@shadura.me
email templates: send text/plain part as well

This change adds text parts to the email templates, as
HTML and text templates may be way to different to be
handled automatically. Also, use proper dash-dash-space
signature separator, so the email clients recognise it
for sure.
11 files changed with 65 insertions and 56 deletions:
0 comments (0 inline, 0 general)
kallithea/controllers/admin/settings.py
Show inline comments
 
@@ -305,110 +305,113 @@ class SettingsController(BaseController)
 
            try:
 
                settings = [
 
                    ('show_public_icon', 'show_public_icon', 'bool'),
 
                    ('show_private_icon', 'show_private_icon', 'bool'),
 
                    ('stylify_metatags', 'stylify_metatags', 'bool'),
 
                    ('repository_fields', 'repository_fields', 'bool'),
 
                    ('dashboard_items', 'dashboard_items', 'int'),
 
                    ('admin_grid_items', 'admin_grid_items', 'int'),
 
                    ('show_version', 'show_version', 'bool'),
 
                    ('use_gravatar', 'use_gravatar', 'bool'),
 
                    ('gravatar_url', 'gravatar_url', 'unicode'),
 
                    ('clone_uri_tmpl', 'clone_uri_tmpl', 'unicode'),
 
                ]
 
                for setting, form_key, type_ in settings:
 
                    sett = Setting.create_or_update(setting,
 
                                        form_result[form_key], type_)
 
                    Session().add(sett)
 

	
 
                Session().commit()
 
                set_app_settings(config)
 
                h.flash(_('Updated visualisation settings'),
 
                        category='success')
 

	
 
            except Exception:
 
                log.error(traceback.format_exc())
 
                h.flash(_('Error occurred during updating '
 
                          'visualisation settings'),
 
                        category='error')
 

	
 
            return redirect(url('admin_settings_visual'))
 

	
 
        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)
 

	
 
    @HasPermissionAllDecorator('hg.admin')
 
    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_email_body = ('Kallithea Email test, '
 
            test_body = ('Kallithea Email test, '
 
                               'Kallithea version: %s' % c.kallithea_version)
 
            if not test_email:
 
                h.flash(_('Please enter email address'), category='error')
 
                return redirect(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,
 
                                body=test_email_body)
 
                                'html', body=test_body)
 

	
 
            recipients = [test_email] if test_email else None
 

	
 
            run_task(tasks.send_email, recipients, test_email_subj,
 
                     test_email_body, test_email_html_body)
 
                     test_email_txt_body, test_email_html_body)
 

	
 
            h.flash(_('Send email task created'), category='success')
 
            return redirect(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)
 

	
 
    @HasPermissionAllDecorator('hg.admin')
 
    def settings_hooks(self):
 
        """GET /admin/settings/hooks: All items in the collection"""
 
        # url('admin_settings_hooks')
 
        c.active = 'hooks'
 
        if request.POST:
 
            if c.visual.allow_custom_hooks_settings:
 
                ui_key = request.POST.get('new_hook_ui_key')
 
                ui_value = request.POST.get('new_hook_ui_value')
 

	
 
                hook_id = request.POST.get('hook_id')
 

	
 
                try:
 
                    ui_key = ui_key and ui_key.strip()
 
                    if ui_value and ui_key:
 
                        Ui.create_or_update_hook(ui_key, ui_value)
 
                        h.flash(_('Added new hook'), category='success')
 
                    elif hook_id:
 
                        Ui.delete(hook_id)
 
                        Session().commit()
 

	
 
                    # check for edits
 
                    update = False
 
                    _d = request.POST.dict_of_lists()
 
                    for k, v in zip(_d.get('hook_ui_key', []),
 
                                    _d.get('hook_ui_value_new', [])):
 
                        Ui.create_or_update_hook(k, v)
 
                        update = True
 

	
 
                    if update:
 
                        h.flash(_('Updated hooks'), category='success')
 
                    Session().commit()
kallithea/model/notification.py
Show inline comments
 
@@ -73,112 +73,113 @@ class NotificationModel(BaseModel):
 
        :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:
 
            email_body = None  # we set body to none, we just send HTML emails
 
            ## this is passed into template
 
            kwargs = {'subject': subject,
 
                      'body': h.rst_w_mentions(body),
 
                      'when': h.fmt_date(notif.created_on),
 
                      'user': notif.created_by_user.username,
 
                      }
 

	
 
            kwargs.update(email_kwargs)
 
            email_subject = EmailNotificationModel()\
 
                                .get_email_description(type_, **kwargs)
 
            email_body_html = EmailNotificationModel()\
 
                                .get_email_tmpl(type_, **kwargs)
 
            email_txt_body = EmailNotificationModel()\
 
                                .get_email_tmpl(type_, 'txt', **kwargs)
 
            email_html_body = EmailNotificationModel()\
 
                                .get_email_tmpl(type_, 'html', **kwargs)
 

	
 
            run_task(tasks.send_email, [rec.email], email_subject, email_body,
 
                     email_body_html, headers)
 
            run_task(tasks.send_email, [rec.email], email_subject, email_txt_body,
 
                     email_html_body, headers)
 

	
 
        return notif
 

	
 
    def delete(self, user, notification):
 
        # we don't want to remove actual notification just the assignment
 
        try:
 
            notification = self.__get_notification(notification)
 
            user = self._get_user(user)
 
            if notification and user:
 
                obj = UserNotification.query()\
 
                        .filter(UserNotification.user == user)\
 
                        .filter(UserNotification.notification
 
                                == notification)\
 
                        .one()
 
                Session().delete(obj)
 
                return True
 
        except Exception:
 
            log.error(traceback.format_exc())
 
            raise
 

	
 
    def get_for_user(self, user, filter_=None):
 
        """
 
        Get notifications for given user, filter them if filter dict is given
 

	
 
        :param user:
 
        :param filter:
 
        """
 
        user = self._get_user(user)
 

	
 
        q = UserNotification.query()\
 
            .filter(UserNotification.user == user)\
 
            .join((Notification, UserNotification.notification_id ==
 
                                 Notification.notification_id))
 

	
 
        if filter_:
 
            q = q.filter(Notification.type_.in_(filter_))
 

	
 
        return q.all()
 

	
 
    def mark_read(self, user, notification):
 
        try:
 
            notification = self.__get_notification(notification)
 
            user = self._get_user(user)
 
            if notification and user:
 
                obj = UserNotification.query()\
 
                        .filter(UserNotification.user == user)\
 
                        .filter(UserNotification.notification
 
                                == notification)\
 
@@ -225,94 +226,94 @@ class NotificationModel(BaseModel):
 
        return UserNotification.query()\
 
            .filter(UserNotification.notification == notification)\
 
            .filter(UserNotification.user == user).scalar()
 

	
 
    def make_description(self, notification, show_age=True):
 
        """
 
        Creates a human readable description based on properties
 
        of notification object
 
        """
 
        #alias
 
        _n = notification
 
        _map = {
 
            _n.TYPE_CHANGESET_COMMENT: _('%(user)s commented on changeset at %(when)s'),
 
            _n.TYPE_MESSAGE: _('%(user)s sent message at %(when)s'),
 
            _n.TYPE_MENTION: _('%(user)s mentioned you at %(when)s'),
 
            _n.TYPE_REGISTRATION: _('%(user)s registered in Kallithea at %(when)s'),
 
            _n.TYPE_PULL_REQUEST: _('%(user)s opened new pull request at %(when)s'),
 
            _n.TYPE_PULL_REQUEST_COMMENT: _('%(user)s commented on pull request at %(when)s')
 
        }
 
        tmpl = _map[notification.type_]
 

	
 
        if show_age:
 
            when = h.age(notification.created_on)
 
        else:
 
            when = h.fmt_date(notification.created_on)
 

	
 
        return tmpl % dict(
 
            user=notification.created_by_user.username,
 
            when=when,
 
            )
 

	
 

	
 
class EmailNotificationModel(BaseModel):
 

	
 
    TYPE_CHANGESET_COMMENT = Notification.TYPE_CHANGESET_COMMENT
 
    TYPE_MESSAGE = Notification.TYPE_MESSAGE # only used for testing
 
    # Notification.TYPE_MENTION is not used
 
    TYPE_PASSWORD_RESET = 'password_link'
 
    TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
 
    TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
 
    TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
 
    TYPE_DEFAULT = 'default'
 

	
 
    def __init__(self):
 
        super(EmailNotificationModel, self).__init__()
 
        self._template_root = kallithea.CONFIG['pylons.paths']['templates'][0]
 
        self._tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
 
        self.email_types = {
 
            self.TYPE_CHANGESET_COMMENT: 'email_templates/changeset_comment.html',
 
            self.TYPE_PASSWORD_RESET: 'email_templates/password_reset.html',
 
            self.TYPE_REGISTRATION: 'email_templates/registration.html',
 
            self.TYPE_DEFAULT: 'email_templates/default.html',
 
            self.TYPE_PULL_REQUEST: 'email_templates/pull_request.html',
 
            self.TYPE_PULL_REQUEST_COMMENT: 'email_templates/pull_request_comment.html',
 
            self.TYPE_CHANGESET_COMMENT: 'changeset_comment',
 
            self.TYPE_PASSWORD_RESET: 'password_reset',
 
            self.TYPE_REGISTRATION: 'registration',
 
            self.TYPE_DEFAULT: 'default',
 
            self.TYPE_PULL_REQUEST: 'pull_request',
 
            self.TYPE_PULL_REQUEST_COMMENT: 'pull_request_comment',
 
        }
 
        self._subj_map = {
 
            self.TYPE_CHANGESET_COMMENT: _('Comment on %(repo_name)s changeset %(short_id)s on %(branch)s by %(comment_username)s'),
 
            self.TYPE_MESSAGE: 'Test Message',
 
            # self.TYPE_PASSWORD_RESET
 
            self.TYPE_REGISTRATION: _('New user %(new_username)s registered'),
 
            # self.TYPE_DEFAULT
 
            self.TYPE_PULL_REQUEST: _('Review request on %(repo_name)s pull request #%(pr_id)s from %(ref)s by %(pr_username)s'),
 
            self.TYPE_PULL_REQUEST_COMMENT: _('Comment on %(repo_name)s pull request #%(pr_id)s from %(ref)s by %(comment_username)s'),
 
        }
 

	
 
    def get_email_description(self, type_, **kwargs):
 
        """
 
        return subject for email based on given type
 
        """
 
        tmpl = self._subj_map[type_]
 
        try:
 
            subj = tmpl % kwargs
 
        except KeyError, e:
 
            log.error('error generating email subject for %r from %s: %s', type_, ','.join(self._subj_map.keys()), e)
 
            raise
 
        l = [str(x) for x in [kwargs.get('status_change'), kwargs.get('closing_pr') and _('Closing')] if x]
 
        if l:
 
            subj += ' (%s)' % (', '.join(l))
 
        return subj
 

	
 
    def get_email_tmpl(self, type_, **kwargs):
 
    def get_email_tmpl(self, type_, content_type, **kwargs):
 
        """
 
        return generated template for email based on given type
 
        """
 

	
 
        base = self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT])
 
        base = 'email_templates/' + self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT]) + '.' + content_type
 
        email_template = self._tmpl_lookup.get_template(base)
 
        # translator and helpers inject
 
        _kwargs = {'_': _,
 
                   'h': h,
 
                   'c': c}
 
        _kwargs.update(kwargs)
 
        log.debug('rendering tmpl %s with kwargs %s' % (base, _kwargs))
 
        return email_template.render(**_kwargs)
kallithea/model/user.py
Show inline comments
 
@@ -252,101 +252,106 @@ class UserModel(BaseModel):
 
                    v = get_crypt_password(v)
 

	
 
                setattr(user, k, v)
 
            self.sa.add(user)
 
            return user
 
        except Exception:
 
            log.error(traceback.format_exc())
 
            raise
 

	
 
    def delete(self, user, cur_user=None):
 
        if not cur_user:
 
            cur_user = getattr(get_current_authuser(), 'username', None)
 
        user = self._get_user(user)
 

	
 
        try:
 
            if user.username == User.DEFAULT_USER:
 
                raise DefaultUserException(
 
                    _(u"You can't remove this user since it's"
 
                      " crucial for entire application")
 
                )
 
            if user.repositories:
 
                repos = [x.repo_name for x in user.repositories]
 
                raise UserOwnsReposException(
 
                    _(u'user "%s" still owns %s repositories and cannot be '
 
                      'removed. Switch owners or remove those repositories. %s')
 
                    % (user.username, len(repos), ', '.join(repos))
 
                )
 
            self.sa.delete(user)
 

	
 
            from kallithea.lib.hooks import log_delete_user
 
            log_delete_user(user.get_dict(), cur_user)
 
        except Exception:
 
            log.error(traceback.format_exc())
 
            raise
 

	
 
    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']
 
        try:
 
            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, body)
 
                         _("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)
 
        except Exception:
 
            log.error(traceback.format_exc())
 
            return False
 

	
 
        return True
 

	
 
    def reset_password(self, data):
 
        from kallithea.lib.celerylib import tasks, run_task
 
        from kallithea.lib import auth
 
        user_email = data['email']
 
        pre_db = True
 
        try:
 
            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')
 

	
 
            pre_db = False
 
            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)
 

	
 
        except Exception:
 
            log.error('Failed to update user password')
 
            log.error(traceback.format_exc())
 
            if pre_db:
 
                # we rollback only if local db stuff fails. If it goes into
 
                # run_task, we're pass rollback state this wouldn't work then
 
                Session().rollback()
 

	
 
        return True
 

	
 
    def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
 
        """
 
        Fetches auth_user by user_id,or api_key if present.
 
        Fills auth_user attributes with those taken from database.
 
        Additionally sets is_authenitated if lookup fails
 
        present in database
kallithea/templates/email_templates/changeset_comment.txt
Show inline comments
 
file copied from kallithea/templates/email_templates/changeset_comment.html to kallithea/templates/email_templates/changeset_comment.txt
 
## -*- coding: utf-8 -*-
 
<%inherit file="main.html"/>
 
<%inherit file="main.txt"/>
 

	
 
%if is_mention:
 
<p>${_('Comment from %s on %s changeset %s mentioned you') % (cs_comment_user, cs_target_repo, h.short_id(raw_id))}:</p>
 
${_('Comment from %s on %s changeset %s mentioned you') % (cs_comment_user, cs_target_repo, h.short_id(raw_id))}:
 
%else:
 
<p>${_('Comment from %s on %s changeset %s') % (cs_comment_user, cs_target_repo, h.short_id(raw_id))}:</p>
 
${_('Comment from %s on %s changeset %s') % (cs_comment_user, cs_target_repo, h.short_id(raw_id))}:
 
%endif
 
<p>${body}</p>
 
${body}
 

	
 
%if status_change:
 
    <p>${_('The changeset status was changed to')}: <b>${status_change}</b></p>
 
${_('The changeset status was changed to')}: ${status_change}
 
%endif
 

	
 
<p>${_('URL')}: <a href="${cs_comment_url}">${cs_comment_url}</a></p>
 
${_('URL')}: ${cs_comment_url}
 

	
 
<p>${_('Changeset')}: ${h.short_id(raw_id)}</p>
 
<p>${_('Description')}:<br/>
 
${_('Changeset')}: ${h.short_id(raw_id)}
 
${_('Description')}:
 
${h.shorter(message, 256)}
 
</p>
kallithea/templates/email_templates/default.txt
Show inline comments
 
file copied from kallithea/templates/email_templates/default.html to kallithea/templates/email_templates/default.txt
 
## -*- coding: utf-8 -*-
 
<%inherit file="main.html"/>
 
<%inherit file="main.txt"/>
 

	
 
${body}
kallithea/templates/email_templates/main.html
Show inline comments
 
${self.body()}
 

	
 
<br/>
 
<br/>
 
-- <br/>
 
${_("This is an automatic notification - don't reply to this mail.")}
 
${_("This is an automatic notification. Don't reply to this mail.")}
kallithea/templates/email_templates/main.txt
Show inline comments
 
file copied from kallithea/templates/email_templates/main.html to kallithea/templates/email_templates/main.txt
 
${self.body()}
 
<br/>
 
-- <br/>
 
${_("This is an automatic notification - don't reply to this mail.")}
 

	
 
-- 
 
${_("This is an automatic notification. Don't reply to this mail.")}
kallithea/templates/email_templates/password_reset.txt
Show inline comments
 
file copied from kallithea/templates/email_templates/password_reset.html to kallithea/templates/email_templates/password_reset.txt
 
## -*- coding: utf-8 -*-
 
<%inherit file="main.html"/>
 
<%inherit file="main.txt"/>
 

	
 
<h4>${_('Hello %s') % user}</h4>
 
${_('Hello %s') % user}
 

	
 
<p>${_('We received a request to create a new password for your account.')}</p>
 
<p>${_('You can generate it by clicking following URL')}:</p>
 
<p><a href="${reset_url}">${reset_url}</a></p>
 
${_('We received a request to create a new password for your account.')}
 
${_('You can generate it by clicking following URL')}:
 

	
 
<p>${_("Please ignore this email if you did not request a new password .")}</p>
 
${reset_url}
 

	
 
${_("Please ignore this email if you did not request a new password .")}
kallithea/templates/email_templates/pull_request.txt
Show inline comments
 
file copied from kallithea/templates/email_templates/pull_request.html to kallithea/templates/email_templates/pull_request.txt
 
## -*- coding: utf-8 -*-
 
<%inherit file="main.html"/>
 
<%inherit file="main.txt"/>
 

	
 
%if is_mention:
 
<p>${_('%s mentioned you on %s pull request "%s"') % (pr_user_created, repo_name, pr_title)}</p>
 
${_('%s mentioned you on %s pull request "%s"') % (pr_user_created, repo_name, pr_title)}
 
%else:
 
<p>${_('%s requested your review of %s pull request "%s"') % (pr_user_created, repo_name, pr_title)}</p>
 
${_('%s requested your review of %s pull request "%s"') % (pr_user_created, repo_name, pr_title)}
 
%endif
 

	
 
<p>${_('URL')}: <a href="${pr_url}">${pr_url}</a></p>
 

	
 
<p>${_('Description')}:</p>
 
<p style="white-space: pre-wrap;">${body}</p>
 
${_('URL')}: ${pr_url}
 

	
 
<p>${_('Changesets')}:</p>
 
<p style="white-space: pre-wrap;">
 
${_('Description')}:
 
${body}
 

	
 
${_('Changesets')}:
 
%for r,r_msg in pr_revisions:
 
<i>${h.short_id(r)}</i>:
 
${h.short_id(r)}:
 
${h.shorter(r_msg, 256)}
 

	
 
%endfor
 
</p>
kallithea/templates/email_templates/pull_request_comment.txt
Show inline comments
 
file copied from kallithea/templates/email_templates/pull_request_comment.html to kallithea/templates/email_templates/pull_request_comment.txt
 
## -*- coding: utf-8 -*-
 
<%inherit file="main.html"/>
 
<%inherit file="main.txt"/>
 

	
 
<p>${_('Comment from %s on %s pull request "%s"') % (pr_comment_user, repo_name, pr_title)}:</p>
 
<p>${body}</p>
 
${_('Comment from %s on %s pull request "%s"') % (pr_comment_user, repo_name, pr_title)}:
 
${body}
 

	
 
%if status_change:
 
    %if closing_pr:
 
       <p>${_('The comment closed the pull request with status')}: <b>${status_change}</b></p>
 
${_('The comment closed the pull request with status')}: ${status_change}
 
    %else:
 
       <p>${_('The comment was made with status')}: <b>${status_change}</b></p>
 
${_('The comment was made with status')}: ${status_change}
 
    %endif
 
%endif
 

	
 
<p>${_('URL')}: <a href="${pr_comment_url}">${pr_comment_url}</a></p>
 
${_('URL')}: ${pr_comment_url}
kallithea/templates/email_templates/registration.txt
Show inline comments
 
file copied from kallithea/templates/email_templates/registration.html to kallithea/templates/email_templates/registration.txt
 
## -*- coding: utf-8 -*-
 
<%inherit file="main.html"/>
 
<%inherit file="main.txt"/>
 

	
 
${body}
 

	
 
${_('View this user here')}: <a href="${registered_user_url}">${registered_user_url}</a>
 
${_('View this user here')}: ${registered_user_url}
0 comments (0 inline, 0 general)