##// END OF EJS Templates
user-audit: share same template for rendering audit logs between user and admin views.
marcink -
r1696:089e4a01 default
parent child Browse files
Show More
@@ -0,0 +1,60 b''
1 <%namespace name="base" file="/base/base.mako"/>
2
3 %if c.audit_logs:
4 <table class="rctable admin_log">
5 <tr>
6 <th>${_('Username')}</th>
7 <th>${_('Action')}</th>
8 <th>${_('Action Data')}</th>
9 <th>${_('Repository')}</th>
10 <th>${_('Date')}</th>
11 <th>${_('IP')}</th>
12 </tr>
13
14 %for cnt,l in enumerate(c.audit_logs):
15 <tr class="parity${cnt%2}">
16 <td class="td-user">
17 %if l.user is not None:
18 ${base.gravatar_with_user(l.user.email)}
19 %else:
20 ${l.username}
21 %endif
22 </td>
23 <td class="td-journalaction">
24 % if l.version == l.VERSION_1:
25 ${h.action_parser(l)[0]()}
26 % else:
27 ${h.literal(l.action)}
28 % endif
29
30 <div class="journal_action_params">
31 % if l.version == l.VERSION_1:
32 ${h.literal(h.action_parser(l)[1]())}
33 % endif
34 </div>
35 </td>
36 <td>
37 % if l.version == l.VERSION_2:
38 ${l.action_data}
39 % endif
40 </td>
41 <td class="td-componentname">
42 %if l.repository is not None:
43 ${h.link_to(l.repository.repo_name,h.url('summary_home',repo_name=l.repository.repo_name))}
44 %else:
45 ${l.repository_name}
46 %endif
47 </td>
48
49 <td class="td-time">${h.format_date(l.action_date)}</td>
50 <td class="td-ip">${l.user_ip}</td>
51 </tr>
52 %endfor
53 </table>
54
55 <div class="pagination-wh pagination-left">
56 ${c.audit_logs.pager('$link_previous ~2~ $link_next')}
57 </div>
58 %else:
59 ${_('No actions yet')}
60 %endif No newline at end of file
@@ -1,307 +1,307 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23
24 24 from pyramid.httpexceptions import HTTPFound
25 25 from pyramid.view import view_config
26 26 from sqlalchemy.sql.functions import coalesce
27 27
28 28 from rhodecode.lib.helpers import Page
29 29 from rhodecode_tools.lib.ext_json import json
30 30
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 32 from rhodecode.lib.auth import (
33 33 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
34 34 from rhodecode.lib import helpers as h
35 35 from rhodecode.lib.utils import PartialRenderer
36 36 from rhodecode.lib.utils2 import safe_int, safe_unicode
37 37 from rhodecode.model.auth_token import AuthTokenModel
38 38 from rhodecode.model.user import UserModel
39 39 from rhodecode.model.user_group import UserGroupModel
40 40 from rhodecode.model.db import User, or_
41 41 from rhodecode.model.meta import Session
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 class AdminUsersView(BaseAppView, DataGridAppView):
47 47 ALLOW_SCOPED_TOKENS = False
48 48 """
49 49 This view has alternative version inside EE, if modified please take a look
50 50 in there as well.
51 51 """
52 52
53 53 def load_default_context(self):
54 54 c = self._get_local_tmpl_context()
55 55 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
56 56 self._register_global_c(c)
57 57 return c
58 58
59 59 def _redirect_for_default_user(self, username):
60 60 _ = self.request.translate
61 61 if username == User.DEFAULT_USER:
62 62 h.flash(_("You can't edit this user"), category='warning')
63 63 # TODO(marcink): redirect to 'users' admin panel once this
64 64 # is a pyramid view
65 65 raise HTTPFound('/')
66 66
67 67 @HasPermissionAllDecorator('hg.admin')
68 68 @view_config(
69 69 route_name='users', request_method='GET',
70 70 renderer='rhodecode:templates/admin/users/users.mako')
71 71 def users_list(self):
72 72 c = self.load_default_context()
73 73 return self._get_template_context(c)
74 74
75 75 @HasPermissionAllDecorator('hg.admin')
76 76 @view_config(
77 77 # renderer defined below
78 78 route_name='users_data', request_method='GET',
79 79 renderer='json_ext', xhr=True)
80 80 def users_list_data(self):
81 81 draw, start, limit = self._extract_chunk(self.request)
82 82 search_q, order_by, order_dir = self._extract_ordering(self.request)
83 83
84 84 _render = PartialRenderer('data_table/_dt_elements.mako')
85 85
86 86 def user_actions(user_id, username):
87 87 return _render("user_actions", user_id, username)
88 88
89 89 users_data_total_count = User.query()\
90 90 .filter(User.username != User.DEFAULT_USER) \
91 91 .count()
92 92
93 93 # json generate
94 94 base_q = User.query().filter(User.username != User.DEFAULT_USER)
95 95
96 96 if search_q:
97 97 like_expression = u'%{}%'.format(safe_unicode(search_q))
98 98 base_q = base_q.filter(or_(
99 99 User.username.ilike(like_expression),
100 100 User._email.ilike(like_expression),
101 101 User.name.ilike(like_expression),
102 102 User.lastname.ilike(like_expression),
103 103 ))
104 104
105 105 users_data_total_filtered_count = base_q.count()
106 106
107 107 sort_col = getattr(User, order_by, None)
108 108 if sort_col:
109 109 if order_dir == 'asc':
110 110 # handle null values properly to order by NULL last
111 111 if order_by in ['last_activity']:
112 112 sort_col = coalesce(sort_col, datetime.date.max)
113 113 sort_col = sort_col.asc()
114 114 else:
115 115 # handle null values properly to order by NULL last
116 116 if order_by in ['last_activity']:
117 117 sort_col = coalesce(sort_col, datetime.date.min)
118 118 sort_col = sort_col.desc()
119 119
120 120 base_q = base_q.order_by(sort_col)
121 121 base_q = base_q.offset(start).limit(limit)
122 122
123 123 users_list = base_q.all()
124 124
125 125 users_data = []
126 126 for user in users_list:
127 127 users_data.append({
128 128 "username": h.gravatar_with_user(user.username),
129 129 "email": user.email,
130 130 "first_name": h.escape(user.name),
131 131 "last_name": h.escape(user.lastname),
132 132 "last_login": h.format_date(user.last_login),
133 133 "last_activity": h.format_date(user.last_activity),
134 134 "active": h.bool2icon(user.active),
135 135 "active_raw": user.active,
136 136 "admin": h.bool2icon(user.admin),
137 137 "extern_type": user.extern_type,
138 138 "extern_name": user.extern_name,
139 139 "action": user_actions(user.user_id, user.username),
140 140 })
141 141
142 142 data = ({
143 143 'draw': draw,
144 144 'data': users_data,
145 145 'recordsTotal': users_data_total_count,
146 146 'recordsFiltered': users_data_total_filtered_count,
147 147 })
148 148
149 149 return data
150 150
151 151 @LoginRequired()
152 152 @HasPermissionAllDecorator('hg.admin')
153 153 @view_config(
154 154 route_name='edit_user_auth_tokens', request_method='GET',
155 155 renderer='rhodecode:templates/admin/users/user_edit.mako')
156 156 def auth_tokens(self):
157 157 _ = self.request.translate
158 158 c = self.load_default_context()
159 159
160 160 user_id = self.request.matchdict.get('user_id')
161 161 c.user = User.get_or_404(user_id, pyramid_exc=True)
162 162 self._redirect_for_default_user(c.user.username)
163 163
164 164 c.active = 'auth_tokens'
165 165
166 166 c.lifetime_values = [
167 167 (str(-1), _('forever')),
168 168 (str(5), _('5 minutes')),
169 169 (str(60), _('1 hour')),
170 170 (str(60 * 24), _('1 day')),
171 171 (str(60 * 24 * 30), _('1 month')),
172 172 ]
173 173 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
174 174 c.role_values = [
175 175 (x, AuthTokenModel.cls._get_role_name(x))
176 176 for x in AuthTokenModel.cls.ROLES]
177 177 c.role_options = [(c.role_values, _("Role"))]
178 178 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
179 179 c.user.user_id, show_expired=True)
180 180 return self._get_template_context(c)
181 181
182 182 def maybe_attach_token_scope(self, token):
183 183 # implemented in EE edition
184 184 pass
185 185
186 186 @LoginRequired()
187 187 @HasPermissionAllDecorator('hg.admin')
188 188 @CSRFRequired()
189 189 @view_config(
190 190 route_name='edit_user_auth_tokens_add', request_method='POST')
191 191 def auth_tokens_add(self):
192 192 _ = self.request.translate
193 193 c = self.load_default_context()
194 194
195 195 user_id = self.request.matchdict.get('user_id')
196 196 c.user = User.get_or_404(user_id, pyramid_exc=True)
197 197 self._redirect_for_default_user(c.user.username)
198 198
199 199 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
200 200 description = self.request.POST.get('description')
201 201 role = self.request.POST.get('role')
202 202
203 203 token = AuthTokenModel().create(
204 204 c.user.user_id, description, lifetime, role)
205 205 self.maybe_attach_token_scope(token)
206 206 Session().commit()
207 207
208 208 h.flash(_("Auth token successfully created"), category='success')
209 209 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
210 210
211 211 @LoginRequired()
212 212 @HasPermissionAllDecorator('hg.admin')
213 213 @CSRFRequired()
214 214 @view_config(
215 215 route_name='edit_user_auth_tokens_delete', request_method='POST')
216 216 def auth_tokens_delete(self):
217 217 _ = self.request.translate
218 218 c = self.load_default_context()
219 219
220 220 user_id = self.request.matchdict.get('user_id')
221 221 c.user = User.get_or_404(user_id, pyramid_exc=True)
222 222 self._redirect_for_default_user(c.user.username)
223 223
224 224 del_auth_token = self.request.POST.get('del_auth_token')
225 225
226 226 if del_auth_token:
227 227 AuthTokenModel().delete(del_auth_token, c.user.user_id)
228 228 Session().commit()
229 229 h.flash(_("Auth token successfully deleted"), category='success')
230 230
231 231 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
232 232
233 233 @LoginRequired()
234 234 @HasPermissionAllDecorator('hg.admin')
235 235 @view_config(
236 236 route_name='edit_user_groups_management', request_method='GET',
237 237 renderer='rhodecode:templates/admin/users/user_edit.mako')
238 238 def groups_management(self):
239 239 c = self.load_default_context()
240 240
241 241 user_id = self.request.matchdict.get('user_id')
242 242 c.user = User.get_or_404(user_id, pyramid_exc=True)
243 243 c.data = c.user.group_member
244 244 self._redirect_for_default_user(c.user.username)
245 245 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
246 246 for group in c.user.group_member]
247 247 c.groups = json.dumps(groups)
248 248 c.active = 'groups'
249 249
250 250 return self._get_template_context(c)
251 251
252 252 @LoginRequired()
253 253 @HasPermissionAllDecorator('hg.admin')
254 254 @view_config(
255 255 route_name='edit_user_groups_management_updates', request_method='POST')
256 256 def groups_management_updates(self):
257 257 _ = self.request.translate
258 258 c = self.load_default_context()
259 259
260 260 user_id = self.request.matchdict.get('user_id')
261 261 c.user = User.get_or_404(user_id, pyramid_exc=True)
262 262 self._redirect_for_default_user(c.user.username)
263 263
264 264 users_groups = set(self.request.POST.getall('users_group_id'))
265 265 users_groups_model = []
266 266
267 267 for ugid in users_groups:
268 268 users_groups_model.append(UserGroupModel().get_group(safe_int(ugid)))
269 269 user_group_model = UserGroupModel()
270 270 user_group_model.change_groups(c.user, users_groups_model)
271 271
272 272 Session().commit()
273 273 c.active = 'user_groups_management'
274 274 h.flash(_("Groups successfully changed"), category='success')
275 275
276 276 return HTTPFound(h.route_path(
277 277 'edit_user_groups_management', user_id=user_id))
278 278
279 279 @LoginRequired()
280 280 @HasPermissionAllDecorator('hg.admin')
281 281 @view_config(
282 282 route_name='edit_user_audit_logs', request_method='GET',
283 283 renderer='rhodecode:templates/admin/users/user_edit.mako')
284 284 def user_audit_logs(self):
285 285 _ = self.request.translate
286 286 c = self.load_default_context()
287 287
288 288 user_id = self.request.matchdict.get('user_id')
289 289 c.user = User.get_or_404(user_id, pyramid_exc=True)
290 290 self._redirect_for_default_user(c.user.username)
291 291 c.active = 'audit'
292 292
293 293 p = safe_int(self.request.GET.get('page', 1), 1)
294 294
295 295 filter_term = self.request.GET.get('filter')
296 c.user_log = UserModel().get_user_log(c.user, filter_term)
296 user_log = UserModel().get_user_log(c.user, filter_term)
297 297
298 298 def url_generator(**kw):
299 299 if filter_term:
300 300 kw['filter'] = filter_term
301 301 return self.request.current_route_path(_query=kw)
302 302
303 c.user_log = Page(c.user_log, page=p, items_per_page=10,
303 c.audit_logs = Page(user_log, page=p, items_per_page=10,
304 304 url=url_generator)
305 305 c.filter_term = filter_term
306 306 return self._get_template_context(c)
307 307
@@ -1,89 +1,89 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Controller for Admin panel of RhodeCode Enterprise
23 23 """
24 24
25 25
26 26 import logging
27 27
28 28 from pylons import request, tmpl_context as c, url
29 29 from pylons.controllers.util import redirect
30 30 from sqlalchemy.orm import joinedload
31 31
32 32 from rhodecode.model.db import UserLog, PullRequest
33 33 from rhodecode.lib.user_log_filter import user_log_filter
34 34 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
35 35 from rhodecode.lib.base import BaseController, render
36 36 from rhodecode.lib.utils2 import safe_int
37 37 from rhodecode.lib.helpers import Page
38 38
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 class AdminController(BaseController):
44 44
45 45 @LoginRequired()
46 46 def __before__(self):
47 47 super(AdminController, self).__before__()
48 48
49 49 @HasPermissionAllDecorator('hg.admin')
50 50 def index(self):
51 51 users_log = UserLog.query()\
52 52 .options(joinedload(UserLog.user))\
53 53 .options(joinedload(UserLog.repository))
54 54
55 55 # FILTERING
56 56 c.search_term = request.GET.get('filter')
57 57 try:
58 58 users_log = user_log_filter(users_log, c.search_term)
59 59 except Exception:
60 60 # we want this to crash for now
61 61 raise
62 62
63 63 users_log = users_log.order_by(UserLog.action_date.desc())
64 64
65 65 p = safe_int(request.GET.get('page', 1), 1)
66 66
67 67 def url_generator(**kw):
68 68 return url.current(filter=c.search_term, **kw)
69 69
70 c.users_log = Page(users_log, page=p, items_per_page=10,
70 c.audit_logs = Page(users_log, page=p, items_per_page=10,
71 71 url=url_generator)
72 72 c.log_data = render('admin/admin_log.mako')
73 73
74 74 if request.is_xhr:
75 75 return c.log_data
76 76 return render('admin/admin.mako')
77 77
78 78 # global redirect doesn't need permissions
79 79 def pull_requests(self, pull_request_id):
80 80 """
81 81 Global redirect for Pull Requests
82 82
83 83 :param pull_request_id: id of pull requests in the system
84 84 """
85 85 pull_request = PullRequest.get_or_404(pull_request_id)
86 86 repo_name = pull_request.target_repo.repo_name
87 87 return redirect(url(
88 88 'pullrequest_show', repo_name=repo_name,
89 89 pull_request_id=pull_request_id))
@@ -1,40 +1,40 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('Admin journal')}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10
11 11 <%def name="breadcrumbs_links()">
12 12 ${h.form(None, id_="filter_form", method="get")}
13 13 <input class="q_filter_box ${'' if c.search_term else 'initial'}" id="j_filter" size="15" type="text" name="filter" value="${c.search_term or ''}" placeholder="${_('journal filter...')}"/>
14 14 <input type='submit' value="${_('filter')}" class="btn" />
15 ${_('Admin journal')} - ${ungettext('%s entry', '%s entries', c.users_log.item_count) % (c.users_log.item_count)}
15 ${_('Admin journal')} - ${ungettext('%s entry', '%s entries', c.audit_logs.item_count) % (c.audit_logs.item_count)}
16 16 ${h.end_form()}
17 17 <p class="tooltip filterexample" title="${h.tooltip(h.journal_filter_help())}">${_('Example Queries')}</p>
18 18 </%def>
19 19
20 20 <%def name="menu_bar_nav()">
21 21 ${self.menu_items(active='admin')}
22 22 </%def>
23 23 <%def name="main()">
24 24 <div class="box">
25 25 <!-- box / title -->
26 26 <div class="title">
27 27 ${self.breadcrumbs()}
28 28 </div>
29 29 <!-- end box / title -->
30 30 <div class="table">
31 31 <div id="user_log">
32 32 ${c.log_data}
33 33 </div>
34 34 </div>
35 35 </div>
36 36
37 37 <script>
38 38 $('#j_filter').autoGrowInput();
39 39 </script>
40 40 </%def>
@@ -1,74 +1,15 b''
1 1 ## -*- coding: utf-8 -*-
2 <%namespace name="base" file="/base/base.mako"/>
3
4 %if c.users_log:
5 <table class="rctable admin_log">
6 <tr>
7 <th>${_('Username')}</th>
8 <th>${_('Action')}</th>
9 <th>${_('Action Data')}</th>
10 <th>${_('Repository')}</th>
11 <th>${_('Date')}</th>
12 <th>${_('IP')}</th>
13 </tr>
14
15 %for cnt,l in enumerate(c.users_log):
16 <tr class="parity${cnt%2}">
17 <td class="td-user">
18 %if l.user is not None:
19 ${base.gravatar_with_user(l.user.email)}
20 %else:
21 ${l.username}
22 %endif
23 </td>
24 <td class="td-journalaction">
25 % if l.version == l.VERSION_1:
26 ${h.action_parser(l)[0]()}
27 % else:
28 ${h.literal(l.action)}
29 % endif
30
31 <div class="journal_action_params">
32 % if l.version == l.VERSION_1:
33 ${h.literal(h.action_parser(l)[1]())}
34 % endif
35 </div>
36 </td>
37 <td>
38 % if l.version == l.VERSION_2:
39 ${l.action_data}
40 % endif
41 </td>
42 <td class="td-componentname">
43 %if l.repository is not None:
44 ${h.link_to(l.repository.repo_name,h.url('summary_home',repo_name=l.repository.repo_name))}
45 %else:
46 ${l.repository_name}
47 %endif
48 </td>
49
50 <td class="td-time">${h.format_date(l.action_date)}</td>
51 <td class="td-ip">${l.user_ip}</td>
52 </tr>
53 %endfor
54 </table>
55
56 <div class="pagination-wh pagination-left">
57 ${c.users_log.pager('$link_previous ~2~ $link_next')}
58 </div>
59 %else:
60 ${_('No actions yet')}
61 %endif
2 <%include file="/admin/admin_log_base.mako" />
62 3
63 4 <script type="text/javascript">
64 5 $(function(){
65 6 //because this is loaded on every pjax request, it must run only once
66 7 //therefore the .one method
67 8 $(document).on('pjax:complete',function(){
68 9 show_more_event();
69 10 });
70 11
71 12 $(document).pjax('#user_log .pager_link', '#user_log');
72 13 });
73 14 </script>
74 15
@@ -1,65 +1,22 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3
4 4
5 5 <div class="panel panel-default">
6 6 <div class="panel-heading">
7 7 <h3 class="panel-title">${_('User Audit Logs')} -
8 ${_ungettext('%s entry', '%s entries', c.user_log.item_count) % (c.user_log.item_count)}
8 ${_ungettext('%s entry', '%s entries', c.audit_logs.item_count) % (c.audit_logs.item_count)}
9 9 </h3>
10 10 </div>
11 11 <div class="panel-body">
12 12
13 13 ${h.form(None, id_="filter_form", method="get")}
14 14 <input class="q_filter_box ${'' if c.filter_term else 'initial'}" id="j_filter" size="15" type="text" name="filter" value="${c.filter_term or ''}" placeholder="${_('audit filter...')}"/>
15 15 <input type='submit' value="${_('filter')}" class="btn" />
16 16 ${h.end_form()}
17 17 <p class="tooltip filterexample" style="position: inherit" title="${h.tooltip(h.journal_filter_help())}">${_('Example Queries')}</p>
18 18
19 % if c.user_log:
20 <table class="rctable admin_log">
21 <tr>
22 <th>${_('Username')}</th>
23 <th>${_('Action')}</th>
24 <th>${_('Repository')}</th>
25 <th>${_('Date')}</th>
26 <th>${_('From IP')}</th>
27 </tr>
28
29 %for cnt,l in enumerate(c.user_log):
30 <tr class="parity${cnt%2}">
31 <td class="td-user">
32 %if l.user is not None:
33 ${base.gravatar_with_user(l.user.email)}
34 %else:
35 ${l.username}
36 %endif
37 </td>
38 <td class="td-journalaction">${h.action_parser(l)[0]()}
39 <div class="journal_action_params">
40 ${h.literal(h.action_parser(l)[1]())}
41 </div>
42 </td>
43 <td class="td-componentname">
44 %if l.repository is not None:
45 ${h.link_to(l.repository.repo_name,h.url('summary_home',repo_name=l.repository.repo_name))}
46 %else:
47 ${l.repository_name}
48 %endif
49 </td>
50
51 <td class="td-time">${h.format_date(l.action_date)}</td>
52 <td class="td-ip">${l.user_ip}</td>
53 </tr>
54 %endfor
55 </table>
56
57 <div class="pagination-wh pagination-left">
58 ${c.user_log.pager('$link_previous ~2~ $link_next')}
59 </div>
60 % else:
61 ${_('No actions yet')}
62 % endif
19 <%include file="/admin/admin_log_base.mako" />
63 20
64 21 </div>
65 22 </div>
General Comments 0
You need to be logged in to leave comments. Login now