##// END OF EJS Templates
final implementation of #210 journal filtering.
marcink -
r3070:cc7eedb5 beta
parent child Browse files
Show More
@@ -30,19 +30,20 b' from sqlalchemy.orm import joinedload'
30 from webhelpers.paginate import Page
30 from webhelpers.paginate import Page
31 from whoosh.qparser.default import QueryParser
31 from whoosh.qparser.default import QueryParser
32 from whoosh import query
32 from whoosh import query
33 from sqlalchemy.sql.expression import or_
33 from sqlalchemy.sql.expression import or_, and_
34
34
35 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
35 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
36 from rhodecode.lib.base import BaseController, render
36 from rhodecode.lib.base import BaseController, render
37 from rhodecode.model.db import UserLog, User
37 from rhodecode.model.db import UserLog, User
38 from rhodecode.lib.utils2 import safe_int, remove_prefix, remove_suffix
38 from rhodecode.lib.utils2 import safe_int, remove_prefix, remove_suffix
39 from rhodecode.lib.indexers import JOURNAL_SCHEMA
39 from rhodecode.lib.indexers import JOURNAL_SCHEMA
40 from whoosh.qparser.dateparse import DateParserPlugin
40
41
41
42
42 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
43
44
44
45
45 def _filter(user_log, search_term):
46 def _journal_filter(user_log, search_term):
46 """
47 """
47 Filters sqlalchemy user_log based on search_term with whoosh Query language
48 Filters sqlalchemy user_log based on search_term with whoosh Query language
48 http://packages.python.org/Whoosh/querylang.html
49 http://packages.python.org/Whoosh/querylang.html
@@ -54,6 +55,7 b' def _filter(user_log, search_term):'
54 qry = None
55 qry = None
55 if search_term:
56 if search_term:
56 qp = QueryParser('repository', schema=JOURNAL_SCHEMA)
57 qp = QueryParser('repository', schema=JOURNAL_SCHEMA)
58 qp.add_plugin(DateParserPlugin())
57 qry = qp.parse(unicode(search_term))
59 qry = qp.parse(unicode(search_term))
58 log.debug('Filtering using parsed query %r' % qry)
60 log.debug('Filtering using parsed query %r' % qry)
59
61
@@ -87,20 +89,25 b' def _filter(user_log, search_term):'
87 return wildcard_handler(field, val)
89 return wildcard_handler(field, val)
88 elif isinstance(term, query.Prefix):
90 elif isinstance(term, query.Prefix):
89 return field.startswith(val)
91 return field.startswith(val)
92 elif isinstance(term, query.DateRange):
93 return and_(field >= val[0], field <= val[1])
90 return field == val
94 return field == val
91
95
92 if isinstance(qry, (query.And, query.Term, query.Prefix, query.Wildcard)):
96 if isinstance(qry, (query.And, query.Term, query.Prefix, query.Wildcard,
97 query.DateRange)):
93 if not isinstance(qry, query.And):
98 if not isinstance(qry, query.And):
94 qry = [qry]
99 qry = [qry]
95 for term in qry:
100 for term in qry:
96 field = term.fieldname
101 field = term.fieldname
97 val = term.text
102 val = (term.text if not isinstance(term, query.DateRange)
103 else [term.startdate, term.enddate])
98 user_log = user_log.filter(get_filterion(field, val, term))
104 user_log = user_log.filter(get_filterion(field, val, term))
99 elif isinstance(qry, query.Or):
105 elif isinstance(qry, query.Or):
100 filters = []
106 filters = []
101 for term in qry:
107 for term in qry:
102 field = term.fieldname
108 field = term.fieldname
103 val = term.text
109 val = (term.text if not isinstance(term, query.DateRange)
110 else [term.startdate, term.enddate])
104 filters.append(get_filterion(field, val, term))
111 filters.append(get_filterion(field, val, term))
105 user_log = user_log.filter(or_(*filters))
112 user_log = user_log.filter(or_(*filters))
106
113
@@ -122,7 +129,7 b' class AdminController(BaseController):'
122 #FILTERING
129 #FILTERING
123 c.search_term = request.GET.get('filter')
130 c.search_term = request.GET.get('filter')
124 try:
131 try:
125 users_log = _filter(users_log, c.search_term)
132 users_log = _journal_filter(users_log, c.search_term)
126 except:
133 except:
127 # we want this to crash for now
134 # we want this to crash for now
128 raise
135 raise
@@ -42,6 +42,7 b' from rhodecode.model.meta import Session'
42 from sqlalchemy.sql.expression import func
42 from sqlalchemy.sql.expression import func
43 from rhodecode.model.scm import ScmModel
43 from rhodecode.model.scm import ScmModel
44 from rhodecode.lib.utils2 import safe_int
44 from rhodecode.lib.utils2 import safe_int
45 from rhodecode.controllers.admin.admin import _journal_filter
45
46
46 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
47
48
@@ -65,9 +66,14 b' class JournalController(BaseController):'
65 .options(joinedload(UserFollowing.follows_repository))\
66 .options(joinedload(UserFollowing.follows_repository))\
66 .all()
67 .all()
67
68
69 #FILTERING
70 c.search_term = request.GET.get('filter')
68 journal = self._get_journal_data(c.following)
71 journal = self._get_journal_data(c.following)
69
72
70 c.journal_pager = Page(journal, page=p, items_per_page=20)
73 def url_generator(**kw):
74 return url.current(filter=c.search_term, **kw)
75
76 c.journal_pager = Page(journal, page=p, items_per_page=20, url=url_generator)
71 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
77 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
72
78
73 c.journal_data = render('journal/journal_data.html')
79 c.journal_data = render('journal/journal_data.html')
@@ -141,8 +147,14 b' class JournalController(BaseController):'
141 if filtering_criterion is not None:
147 if filtering_criterion is not None:
142 journal = self.sa.query(UserLog)\
148 journal = self.sa.query(UserLog)\
143 .options(joinedload(UserLog.user))\
149 .options(joinedload(UserLog.user))\
144 .options(joinedload(UserLog.repository))\
150 .options(joinedload(UserLog.repository))
145 .filter(filtering_criterion)\
151 #filter
152 try:
153 journal = _journal_filter(journal, c.search_term)
154 except:
155 # we want this to crash for now
156 raise
157 journal = journal.filter(filtering_criterion)\
146 .order_by(UserLog.action_date.desc())
158 .order_by(UserLog.action_date.desc())
147 else:
159 else:
148 journal = []
160 journal = []
@@ -11,6 +11,7 b' import math'
11 import logging
11 import logging
12 import re
12 import re
13 import urlparse
13 import urlparse
14 import textwrap
14
15
15 from datetime import datetime
16 from datetime import datetime
16 from pygments.formatters.html import HtmlFormatter
17 from pygments.formatters.html import HtmlFormatter
@@ -1135,3 +1136,23 b' def changeset_status_lbl(changeset_statu'
1135
1136
1136 def get_permission_name(key):
1137 def get_permission_name(key):
1137 return dict(Permission.PERMS).get(key)
1138 return dict(Permission.PERMS).get(key)
1139
1140
1141 def journal_filter_help():
1142 return _(textwrap.dedent('''
1143 Example filter terms:
1144 repository:vcs
1145 username:marcin
1146 action:*push*
1147 ip:127.0.0.1
1148 date:20120101
1149 date:[20120101100000 TO 20120102]
1150
1151 Generate wildcards using '*' character:
1152 "repositroy:vcs*" - search everything starting with 'vcs'
1153 "repository:*vcs*" - search for repository containing 'vcs'
1154
1155 Optional AND / OR operators in queries
1156 "repository:vcs OR repository:test"
1157 "username:test AND repository:test*"
1158 '''))
@@ -7,25 +7,8 b''
7
7
8 <%def name="breadcrumbs_links()">
8 <%def name="breadcrumbs_links()">
9 <form id="filter_form">
9 <form id="filter_form">
10 <input class="q_filter_box ${'' if c.search_term else 'initial'}" id="q_filter" size="15" type="text" name="filter" value="${c.search_term or _('quick filter...')}"/>
10 <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 _('journal filter...')}"/>
11 <span class="tooltip" title="${h.tooltip(_('''
11 <span class="tooltip" title="${h.tooltip(h.journal_filter_help())}">?</span>
12 Example search query:
13 "repository:vcs"
14 "username:marcin"
15
16 You can use wildcards using '*'
17 "repositroy:vcs*" - search everything starting with 'vcs'
18 "repository:*vcs*" - search for repository containing 'vcs'
19 Use AND / OR operators in queries
20 "repository:vcs OR repository:test"
21 "username:test AND repository:test*"
22 List of valid search filters:
23 repository:
24 username:
25 action:
26 ip:
27 date:
28 '''))}">?</span>
29 <input type='submit' value="${_('filter')}" class="ui-btn" style="padding:0px 2px 0px 2px;margin:0px"/>
12 <input type='submit' value="${_('filter')}" class="ui-btn" style="padding:0px 2px 0px 2px;margin:0px"/>
30 ${_('Admin journal')} - ${ungettext('%s entry', '%s entries', c.users_log.item_count) % (c.users_log.item_count)}
13 ${_('Admin journal')} - ${ungettext('%s entry', '%s entries', c.users_log.item_count) % (c.users_log.item_count)}
31 </form>
14 </form>
@@ -50,24 +33,24 b''
50 </div>
33 </div>
51
34
52 <script>
35 <script>
53 YUE.on('q_filter','click',function(){
36 YUE.on('j_filter','click',function(){
54 var qfilter = YUD.get('q_filter');
37 var jfilter = YUD.get('j_filter');
55 if(YUD.hasClass(qfilter, 'initial')){
38 if(YUD.hasClass(jfilter, 'initial')){
56 qfilter.value = '';
39 jfilter.value = '';
57 }
40 }
58 });
41 });
59 var fix_q_filter_width = function(len){
42 var fix_j_filter_width = function(len){
60 YUD.setStyle(YUD.get('q_filter'),'width',Math.max(80, len*6.50)+'px');
43 YUD.setStyle(YUD.get('j_filter'),'width',Math.max(80, len*6.50)+'px');
61 }
44 }
62 YUE.on('q_filter','keyup',function(){
45 YUE.on('j_filter','keyup',function(){
63 fix_q_filter_width(YUD.get('q_filter').value.length);
46 fix_j_filter_width(YUD.get('j_filter').value.length);
64 });
47 });
65 YUE.on('filter_form','submit',function(e){
48 YUE.on('filter_form','submit',function(e){
66 YUE.preventDefault(e)
49 YUE.preventDefault(e)
67 var val = YUD.get('q_filter').value;
50 var val = YUD.get('j_filter').value;
68 window.location = "${url.current(filter='__FILTER__')}".replace('__FILTER__',val);
51 window.location = "${url.current(filter='__FILTER__')}".replace('__FILTER__',val);
69 });
52 });
70 fix_q_filter_width(YUD.get('q_filter').value.length);
53 fix_j_filter_width(YUD.get('j_filter').value.length);
71 </script>
54 </script>
72 </%def>
55 </%def>
73
56
@@ -4,7 +4,15 b''
4 ${_('Journal')} - ${c.rhodecode_name}
4 ${_('Journal')} - ${c.rhodecode_name}
5 </%def>
5 </%def>
6 <%def name="breadcrumbs()">
6 <%def name="breadcrumbs()">
7 ${c.rhodecode_name}
7 <h5>
8 <form id="filter_form">
9 <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 _('quick filter...')}"/>
10 <span class="tooltip" title="${h.tooltip(h.journal_filter_help())}">?</span>
11 <input type='submit' value="${_('filter')}" class="ui-btn" style="padding:0px 2px 0px 2px;margin:0px"/>
12 ${_('journal')} - ${ungettext('%s entry', '%s entries', c.journal_pager.item_count) % (c.journal_pager.item_count)}
13 </form>
14 ${h.end_form()}
15 </h5>
8 </%def>
16 </%def>
9 <%def name="page_nav()">
17 <%def name="page_nav()">
10 ${self.menu('home')}
18 ${self.menu('home')}
@@ -18,7 +26,7 b''
18 <div class="box box-left">
26 <div class="box box-left">
19 <!-- box / title -->
27 <!-- box / title -->
20 <div class="title">
28 <div class="title">
21 <h5>${_('Journal')}</h5>
29 ${self.breadcrumbs()}
22 <ul class="links">
30 <ul class="links">
23 <li>
31 <li>
24 <span><a id="refresh" href="${h.url('journal')}"><img class="icon" title="${_('Refresh')}" alt="${_('Refresh')}" src="${h.url('/images/icons/arrow_refresh.png')}"/></a></span>
32 <span><a id="refresh" href="${h.url('journal')}"><img class="icon" title="${_('Refresh')}" alt="${_('Refresh')}" src="${h.url('/images/icons/arrow_refresh.png')}"/></a></span>
@@ -106,6 +114,26 b''
106 </div>
114 </div>
107
115
108 <script type="text/javascript">
116 <script type="text/javascript">
117
118 YUE.on('j_filter','click',function(){
119 var jfilter = YUD.get('j_filter');
120 if(YUD.hasClass(jfilter, 'initial')){
121 jfilter.value = '';
122 }
123 });
124 var fix_j_filter_width = function(len){
125 YUD.setStyle(YUD.get('j_filter'),'width',Math.max(80, len*6.50)+'px');
126 }
127 YUE.on('j_filter','keyup',function(){
128 fix_j_filter_width(YUD.get('j_filter').value.length);
129 });
130 YUE.on('filter_form','submit',function(e){
131 YUE.preventDefault(e)
132 var val = YUD.get('j_filter').value;
133 window.location = "${url.current(filter='__FILTER__')}".replace('__FILTER__',val);
134 });
135 fix_j_filter_width(YUD.get('j_filter').value.length);
136
109 var show_my = function(e){
137 var show_my = function(e){
110 YUD.setStyle('watched','display','none');
138 YUD.setStyle('watched','display','none');
111 YUD.setStyle('my','display','');
139 YUD.setStyle('my','display','');
@@ -153,7 +181,7 b''
153 }
181 }
154
182
155 YUE.on('refresh','click',function(e){
183 YUE.on('refresh','click',function(e){
156 ypjax(e.currentTarget.href,"journal",function(){
184 ypjax("${h.url.current(filter=c.search_term)}","journal",function(){
157 show_more_event();
185 show_more_event();
158 tooltip_activate();
186 tooltip_activate();
159 show_changeset_tooltip();
187 show_changeset_tooltip();
@@ -100,4 +100,16 b' class TestAdminController(TestController'
100 self.log_user()
100 self.log_user()
101 response = self.app.get(url(controller='admin/admin', action='index',
101 response = self.app.get(url(controller='admin/admin', action='index',
102 filter='action:*pull_request*'))
102 filter='action:*pull_request*'))
103 response.mustcontain('187 entries') No newline at end of file
103 response.mustcontain('187 entries')
104
105 def test_filter_journal_filter_on_date(self):
106 self.log_user()
107 response = self.app.get(url(controller='admin/admin', action='index',
108 filter='date:20121010'))
109 response.mustcontain('47 entries')
110
111 def test_filter_journal_filter_on_date_2(self):
112 self.log_user()
113 response = self.app.get(url(controller='admin/admin', action='index',
114 filter='date:20121020'))
115 response.mustcontain('17 entries') No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now