diff --git a/rhodecode/apps/admin/__init__.py b/rhodecode/apps/admin/__init__.py
--- a/rhodecode/apps/admin/__init__.py
+++ b/rhodecode/apps/admin/__init__.py
@@ -79,6 +79,11 @@ def admin_routes(config):
name='edit_user_groups_management_updates',
pattern='/users/{user_id:\d+}/edit/edit_user_groups_management/updates')
+ # user audit logs
+ config.add_route(
+ name='edit_user_audit_logs',
+ pattern='/users/{user_id:\d+}/edit/audit')
+
def includeme(config):
settings = config.get_settings()
diff --git a/rhodecode/apps/admin/views/users.py b/rhodecode/apps/admin/views/users.py
--- a/rhodecode/apps/admin/views/users.py
+++ b/rhodecode/apps/admin/views/users.py
@@ -22,6 +22,8 @@ import logging
from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
+
+from rhodecode.lib.helpers import Page
from rhodecode_tools.lib.ext_json import json
from rhodecode.apps._base import BaseAppView
@@ -31,6 +33,7 @@ from rhodecode.lib import helpers as h
from rhodecode.lib.utils import PartialRenderer
from rhodecode.lib.utils2 import safe_int, safe_unicode
from rhodecode.model.auth_token import AuthTokenModel
+from rhodecode.model.user import UserModel
from rhodecode.model.user_group import UserGroupModel
from rhodecode.model.db import User, or_
from rhodecode.model.meta import Session
@@ -238,7 +241,6 @@ class AdminUsersView(BaseAppView):
return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
-
@LoginRequired()
@HasPermissionAllDecorator('hg.admin')
@view_config(
@@ -257,7 +259,6 @@ class AdminUsersView(BaseAppView):
return self._get_template_context(c)
-
@LoginRequired()
@HasPermissionAllDecorator('hg.admin')
@view_config(
@@ -282,4 +283,35 @@ class AdminUsersView(BaseAppView):
c.active = 'user_groups_management'
h.flash(_("Groups successfully changed"), category='success')
- return HTTPFound(h.route_path('edit_user_groups_management', user_id=user_id))
+ return HTTPFound(h.route_path(
+ 'edit_user_groups_management', user_id=user_id))
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @view_config(
+ route_name='edit_user_audit_logs', request_method='GET',
+ renderer='rhodecode:templates/admin/users/user_edit.mako')
+ def user_audit_logs(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+
+ user_id = self.request.matchdict.get('user_id')
+ c.user = User.get_or_404(user_id, pyramid_exc=True)
+ self._redirect_for_default_user(c.user.username)
+ c.active = 'audit'
+
+ p = safe_int(self.request.GET.get('page', 1), 1)
+
+ filter_term = self.request.GET.get('filter')
+ c.user_log = UserModel().get_user_log(c.user, filter_term)
+
+ def url_generator(**kw):
+ if filter_term:
+ kw['filter'] = filter_term
+ return self.request.current_route_path(_query=kw)
+
+ c.user_log = Page(c.user_log, page=p, items_per_page=10,
+ url=url_generator)
+ c.filter_term = filter_term
+ return self._get_template_context(c)
+
diff --git a/rhodecode/controllers/admin/admin.py b/rhodecode/controllers/admin/admin.py
--- a/rhodecode/controllers/admin/admin.py
+++ b/rhodecode/controllers/admin/admin.py
@@ -28,101 +28,17 @@ import logging
from pylons import request, tmpl_context as c, url
from pylons.controllers.util import redirect
from sqlalchemy.orm import joinedload
-from whoosh.qparser.default import QueryParser, query
-from whoosh.qparser.dateparse import DateParserPlugin
-from whoosh.fields import (TEXT, Schema, DATETIME)
-from sqlalchemy.sql.expression import or_, and_, func
from rhodecode.model.db import UserLog, PullRequest
+from rhodecode.lib.user_log_filter import user_log_filter
from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
from rhodecode.lib.base import BaseController, render
-from rhodecode.lib.utils2 import safe_int, remove_prefix, remove_suffix
+from rhodecode.lib.utils2 import safe_int
from rhodecode.lib.helpers import Page
log = logging.getLogger(__name__)
-# JOURNAL SCHEMA used only to generate queries in journal. We use whoosh
-# querylang to build sql queries and filter journals
-JOURNAL_SCHEMA = Schema(
- username=TEXT(),
- date=DATETIME(),
- action=TEXT(),
- repository=TEXT(),
- ip=TEXT(),
-)
-
-
-def _journal_filter(user_log, search_term):
- """
- Filters sqlalchemy user_log based on search_term with whoosh Query language
- http://packages.python.org/Whoosh/querylang.html
-
- :param user_log:
- :param search_term:
- """
- log.debug('Initial search term: %r' % search_term)
- qry = None
- if search_term:
- qp = QueryParser('repository', schema=JOURNAL_SCHEMA)
- qp.add_plugin(DateParserPlugin())
- qry = qp.parse(unicode(search_term))
- log.debug('Filtering using parsed query %r' % qry)
-
- def wildcard_handler(col, wc_term):
- if wc_term.startswith('*') and not wc_term.endswith('*'):
- # postfix == endswith
- wc_term = remove_prefix(wc_term, prefix='*')
- return func.lower(col).endswith(wc_term)
- elif wc_term.startswith('*') and wc_term.endswith('*'):
- # wildcard == ilike
- wc_term = remove_prefix(wc_term, prefix='*')
- wc_term = remove_suffix(wc_term, suffix='*')
- return func.lower(col).contains(wc_term)
-
- def get_filterion(field, val, term):
-
- if field == 'repository':
- field = getattr(UserLog, 'repository_name')
- elif field == 'ip':
- field = getattr(UserLog, 'user_ip')
- elif field == 'date':
- field = getattr(UserLog, 'action_date')
- elif field == 'username':
- field = getattr(UserLog, 'username')
- else:
- field = getattr(UserLog, field)
- log.debug('filter field: %s val=>%s' % (field, val))
-
- # sql filtering
- if isinstance(term, query.Wildcard):
- return wildcard_handler(field, val)
- elif isinstance(term, query.Prefix):
- return func.lower(field).startswith(func.lower(val))
- elif isinstance(term, query.DateRange):
- return and_(field >= val[0], field <= val[1])
- return func.lower(field) == func.lower(val)
-
- if isinstance(qry, (query.And, query.Term, query.Prefix, query.Wildcard,
- query.DateRange)):
- if not isinstance(qry, query.And):
- qry = [qry]
- for term in qry:
- field = term.fieldname
- val = (term.text if not isinstance(term, query.DateRange)
- else [term.startdate, term.enddate])
- user_log = user_log.filter(get_filterion(field, val, term))
- elif isinstance(qry, query.Or):
- filters = []
- for term in qry:
- field = term.fieldname
- val = (term.text if not isinstance(term, query.DateRange)
- else [term.startdate, term.enddate])
- filters.append(get_filterion(field, val, term))
- user_log = user_log.filter(or_(*filters))
-
- return user_log
-
class AdminController(BaseController):
@@ -139,7 +55,7 @@ class AdminController(BaseController):
# FILTERING
c.search_term = request.GET.get('filter')
try:
- users_log = _journal_filter(users_log, c.search_term)
+ users_log = user_log_filter(users_log, c.search_term)
except Exception:
# we want this to crash for now
raise
diff --git a/rhodecode/controllers/journal.py b/rhodecode/controllers/journal.py
--- a/rhodecode/controllers/journal.py
+++ b/rhodecode/controllers/journal.py
@@ -34,7 +34,7 @@ from webob.exc import HTTPBadRequest
from pylons import request, tmpl_context as c, response, url
from pylons.i18n.translation import _
-from rhodecode.controllers.admin.admin import _journal_filter
+from rhodecode.controllers.admin.admin import user_log_filter
from rhodecode.model.db import UserLog, UserFollowing, User, UserApiKeys
from rhodecode.model.meta import Session
import rhodecode.lib.helpers as h
@@ -89,7 +89,7 @@ class JournalController(BaseController):
.options(joinedload(UserLog.repository))
#filter
try:
- journal = _journal_filter(journal, c.search_term)
+ journal = user_log_filter(journal, c.search_term)
except Exception:
# we want this to crash for now
raise
diff --git a/rhodecode/lib/user_log_filter.py b/rhodecode/lib/user_log_filter.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/lib/user_log_filter.py
@@ -0,0 +1,112 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2010-2017 RhodeCode GmbH
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License, version 3
+# (only), as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see
${_('Example Queries')}
+ + % if c.user_log: +${_('Username')} | +${_('Action')} | +${_('Repository')} | +${_('Date')} | +${_('From IP')} | +
---|---|---|---|---|
+ %if l.user is not None: + ${base.gravatar_with_user(l.user.email)} + %else: + ${l.username} + %endif + | +${h.action_parser(l)[0]()}
+
+ ${h.literal(h.action_parser(l)[1]())}
+
+ |
+ + %if l.repository is not None: + ${h.link_to(l.repository.repo_name,h.url('summary_home',repo_name=l.repository.repo_name))} + %else: + ${l.repository_name} + %endif + | + +${h.format_date(l.action_date)} | +${l.user_ip} | +