瀏覽代碼

add search engine based on a ilike sql query and a filterable result page

Damien ACCORSI 10 年之前
父節點
當前提交
d82731c2b0

+ 3 - 30
tracim/tracim/controllers/__init__.py 查看文件

126
 
126
 
127
 class TIMRestControllerWithBreadcrumb(TIMRestController):
127
 class TIMRestControllerWithBreadcrumb(TIMRestController):
128
 
128
 
129
-    def get_breadcrumb(self, folder_id=None) -> [BreadcrumbItem]:
129
+    def get_breadcrumb(self, item_id=None) -> [BreadcrumbItem]:
130
         """
130
         """
131
         TODO - Remove this and factorize it with other get_breadcrumb_xxx methods
131
         TODO - Remove this and factorize it with other get_breadcrumb_xxx methods
132
-        :param folder_id:
132
+        :param item_id: an item id (item may be normal content or folder
133
         :return:
133
         :return:
134
         """
134
         """
135
-        workspace = tmpl_context.workspace
136
-        workspace_id = tmpl_context.workspace_id
137
-        breadcrumb = []
138
-
139
-        breadcrumb.append(BreadcrumbItem(ContentType.icon(ContentType.FAKE_Dashboard), _('Workspaces'), tg.url('/workspaces')))
140
-        breadcrumb.append(BreadcrumbItem(ContentType.icon(ContentType.FAKE_Workspace), workspace.label, tg.url('/workspaces/{}'.format(workspace.workspace_id))))
141
-
142
-        content_api = ContentApi(tmpl_context.current_user)
143
-        if folder_id:
144
-            breadcrumb_folder_items = []
145
-            current_item = content_api.get_one(folder_id, ContentType.Any, workspace)
146
-            is_active = True
147
-
148
-            while current_item:
149
-                breadcrumb_item = BreadcrumbItem(ContentType.icon(current_item.type),
150
-                                                 current_item.label,
151
-                                                 tg.url('/workspaces/{}/folders/{}'.format(workspace_id, current_item.content_id)),
152
-                                                 is_active)
153
-                is_active = False # the first item is True, then all other are False => in the breadcrumb, only the last item is "active"
154
-                breadcrumb_folder_items.append(breadcrumb_item)
155
-                current_item = current_item.parent
156
-
157
-            for item in reversed(breadcrumb_folder_items):
158
-                breadcrumb.append(item)
159
-
160
-
161
-        return breadcrumb
162
-
135
+        return ContentApi(tmpl_context.current_user).build_breadcrumb(tmpl_context.workspace, item_id)
163
 
136
 
164
 
137
 
165
 class TIMWorkspaceContentRestController(TIMRestControllerWithBreadcrumb):
138
 class TIMWorkspaceContentRestController(TIMRestControllerWithBreadcrumb):

+ 0 - 1
tracim/tracim/controllers/content.py 查看文件

762
     @tg.require(current_user_is_content_manager())
762
     @tg.require(current_user_is_content_manager())
763
     @tg.expose()
763
     @tg.expose()
764
     def put_archive_undo(self, item_id):
764
     def put_archive_undo(self, item_id):
765
-        print('AGAGA')
766
         # TODO - CHECK RIGHTS
765
         # TODO - CHECK RIGHTS
767
         item_id = int(item_id)
766
         item_id = int(item_id)
768
         content_api = ContentApi(tmpl_context.current_user, True, True) # Here we do not filter deleted items
767
         content_api = ContentApi(tmpl_context.current_user, True, True) # Here we do not filter deleted items

+ 25 - 0
tracim/tracim/controllers/root.py 查看文件

123
         fake_api = Context(CTX.CURRENT_USER).toDict({'current_user': current_user_content})
123
         fake_api = Context(CTX.CURRENT_USER).toDict({'current_user': current_user_content})
124
 
124
 
125
         return DictLikeClass(fake_api=fake_api)
125
         return DictLikeClass(fake_api=fake_api)
126
+
127
+    @require(predicates.not_anonymous())
128
+    @expose('tracim.templates.search')
129
+    def search(self, keywords = ''):
130
+        from tracim.lib.content import ContentApi
131
+
132
+        user = tmpl_context.current_user
133
+        api = ContentApi(user)
134
+
135
+        items = []
136
+        keyword_list = api.get_keywords(keywords)
137
+
138
+        result = api.search(keyword_list)
139
+        if result:
140
+            items = result.limit(ContentApi.SEARCH_DEFAULT_RESULT_NB).all()
141
+
142
+        current_user_content = Context(CTX.CURRENT_USER).toDict(user)
143
+        fake_api = Context(CTX.CURRENT_USER).toDict({'current_user': current_user_content})
144
+
145
+        search_results = Context(CTX.SEARCH).toDict(items, 'results', 'result_nb')
146
+        search_results.keywords = keyword_list
147
+
148
+        return DictLikeClass(fake_api=fake_api, search=search_results)
149
+
150
+

+ 0 - 1
tracim/tracim/controllers/workspace.py 查看文件

80
             dictified_workspaces = Context(CTX.MENU_API).toDict(workspaces, 'd')
80
             dictified_workspaces = Context(CTX.MENU_API).toDict(workspaces, 'd')
81
             return dictified_workspaces
81
             return dictified_workspaces
82
 
82
 
83
-
84
         allowed_content_types = ContentType.allowed_types_from_str(folder_allowed_content_types)
83
         allowed_content_types = ContentType.allowed_types_from_str(folder_allowed_content_types)
85
         ignored_item_ids = [int(ignore_id)] if ignore_id else []
84
         ignored_item_ids = [int(ignore_id)] if ignore_id else []
86
 
85
 

+ 1 - 1
tracim/tracim/lib/base.py 查看文件

113
         self._logger = logging.getLogger(self._name)
113
         self._logger = logging.getLogger(self._name)
114
 
114
 
115
     def _txt(self, instance_or_class):
115
     def _txt(self, instance_or_class):
116
-        if instance_or_class.__class__.__name__=='type':
116
+        if instance_or_class.__class__.__name__ in ('function', 'type'):
117
             return instance_or_class.__name__
117
             return instance_or_class.__name__
118
         else:
118
         else:
119
             return instance_or_class.__class__.__name__
119
             return instance_or_class.__class__.__name__

+ 96 - 4
tracim/tracim/lib/content.py 查看文件

2
 
2
 
3
 __author__ = 'damien'
3
 __author__ = 'damien'
4
 
4
 
5
+import re
6
+
5
 import tg
7
 import tg
8
+from tg.i18n import ugettext as _
6
 
9
 
10
+import sqlalchemy
11
+from sqlalchemy.orm import aliased
12
+from sqlalchemy.orm import joinedload
7
 from sqlalchemy.orm.attributes import get_history
13
 from sqlalchemy.orm.attributes import get_history
8
 from sqlalchemy import not_
14
 from sqlalchemy import not_
15
+from sqlalchemy import or_
9
 from tracim.lib import cmp_to_key
16
 from tracim.lib import cmp_to_key
10
 from tracim.lib.notifications import NotifierFactory
17
 from tracim.lib.notifications import NotifierFactory
11
 from tracim.model import DBSession
18
 from tracim.model import DBSession
12
 from tracim.model.auth import User
19
 from tracim.model.auth import User
13
-from tracim.model.data import ContentStatus, ContentRevisionRO, ActionDescription
20
+from tracim.model.data import ActionDescription
21
+from tracim.model.data import BreadcrumbItem
22
+from tracim.model.data import ContentStatus
23
+from tracim.model.data import ContentRevisionRO
14
 from tracim.model.data import Content
24
 from tracim.model.data import Content
15
 from tracim.model.data import ContentType
25
 from tracim.model.data import ContentType
16
 from tracim.model.data import NodeTreeItem
26
 from tracim.model.data import NodeTreeItem
48
 
58
 
49
 class ContentApi(object):
59
 class ContentApi(object):
50
 
60
 
61
+    SEARCH_SEPARATORS = ',| '
62
+    SEARCH_DEFAULT_RESULT_NB = 50
63
+
64
+
51
     def __init__(self, current_user: User, show_archived=False, show_deleted=False, all_content_in_treeview=True):
65
     def __init__(self, current_user: User, show_archived=False, show_deleted=False, all_content_in_treeview=True):
52
         self._user = current_user
66
         self._user = current_user
53
         self._show_archived = show_archived
67
         self._show_archived = show_archived
71
         content_list.sort(key=cmp_to_key(compare_content_for_sorting_by_type_and_name))
85
         content_list.sort(key=cmp_to_key(compare_content_for_sorting_by_type_and_name))
72
         return content_list
86
         return content_list
73
 
87
 
88
+    def build_breadcrumb(self, workspace, item_id=None, skip_root=False) -> [BreadcrumbItem]:
89
+        """
90
+        TODO - Remove this and factorize it with other get_breadcrumb_xxx methods
91
+        :param item_id: an item id (item may be normal content or folder
92
+        :return:
93
+        """
94
+        workspace_id = workspace.workspace_id
95
+        breadcrumb = []
96
+
97
+        if not skip_root:
98
+            breadcrumb.append(BreadcrumbItem(ContentType.icon(ContentType.FAKE_Dashboard), _('Workspaces'), tg.url('/workspaces')))
99
+        breadcrumb.append(BreadcrumbItem(ContentType.icon(ContentType.FAKE_Workspace), workspace.label, tg.url('/workspaces/{}'.format(workspace.workspace_id))))
100
+
101
+        if item_id:
102
+            breadcrumb_folder_items = []
103
+            current_item = self.get_one(item_id, ContentType.Any, workspace)
104
+            is_active = True
105
+            if current_item.type==ContentType.Folder:
106
+                next_url = tg.url('/workspaces/{}/folders/{}'.format(workspace_id, current_item.content_id))
107
+            else:
108
+                next_url = tg.url('/workspaces/{}/folders/{}/{}s/{}'.format(workspace_id, current_item.parent_id, current_item.type, current_item.content_id))
109
+
110
+            while current_item:
111
+                breadcrumb_item = BreadcrumbItem(ContentType.icon(current_item.type),
112
+                                                 current_item.label,
113
+                                                 next_url,
114
+                                                 is_active)
115
+                is_active = False # the first item is True, then all other are False => in the breadcrumb, only the last item is "active"
116
+                breadcrumb_folder_items.append(breadcrumb_item)
117
+                current_item = current_item.parent
118
+                if current_item:
119
+                    # In last iteration, the parent is None, and there is no more breadcrumb item to build
120
+                    next_url = tg.url('/workspaces/{}/folders/{}'.format(workspace_id, current_item.content_id))
121
+
122
+            for item in reversed(breadcrumb_folder_items):
123
+                breadcrumb.append(item)
124
+
125
+
126
+        return breadcrumb
74
 
127
 
75
     def _base_query(self, workspace: Workspace=None):
128
     def _base_query(self, workspace: Workspace=None):
76
         result = DBSession.query(Content)
129
         result = DBSession.query(Content)
79
             result = result.filter(Content.workspace_id==workspace.workspace_id)
132
             result = result.filter(Content.workspace_id==workspace.workspace_id)
80
 
133
 
81
         if not self._show_deleted:
134
         if not self._show_deleted:
82
-            result = result.filter(Content.is_deleted==False)
135
+            parent = aliased(Content)
136
+            result.join(parent, Content.parent).\
137
+                filter(Content.is_deleted==False).\
138
+                filter(parent.is_deleted==False)
83
 
139
 
84
         if not self._show_archived:
140
         if not self._show_archived:
85
-            result = result.filter(Content.is_archived==False)
141
+            parent = aliased(Content)
142
+            result.join(parent, Content.parent).\
143
+                filter(Content.is_archived==False).\
144
+                filter(parent.is_archived==False)
86
 
145
 
87
         return result
146
         return result
88
 
147
 
110
             all()
169
             all()
111
 
170
 
112
         if not filter_by_allowed_content_types or len(filter_by_allowed_content_types)<=0:
171
         if not filter_by_allowed_content_types or len(filter_by_allowed_content_types)<=0:
113
-            return folders
172
+            filter_by_allowed_content_types = ContentType.allowed_types_for_folding()
114
 
173
 
115
         # Now, the case is to filter folders by the content that they are allowed to contain
174
         # Now, the case is to filter folders by the content that they are allowed to contain
116
         result = []
175
         result = []
118
             for allowed_content_type in filter_by_allowed_content_types:
177
             for allowed_content_type in filter_by_allowed_content_types:
119
                 if folder.type==ContentType.Folder and folder.properties['allowed_content'][allowed_content_type]==True:
178
                 if folder.type==ContentType.Folder and folder.properties['allowed_content'][allowed_content_type]==True:
120
                     result.append(folder)
179
                     result.append(folder)
180
+                    break
121
 
181
 
122
         return result
182
         return result
123
 
183
 
295
         if do_notify:
355
         if do_notify:
296
             NotifierFactory.create(self._user).notify_content_update(content)
356
             NotifierFactory.create(self._user).notify_content_update(content)
297
 
357
 
358
+
359
+    def get_keywords(self, search_string, search_string_separators=None) -> [str]:
360
+        """
361
+        :param search_string: a list of coma-separated keywords
362
+        :return: a list of str (each keyword = 1 entry
363
+        """
364
+
365
+        search_string_separators = search_string_separators or ContentApi.SEARCH_SEPARATORS
366
+
367
+        keywords = []
368
+        if search_string:
369
+            keywords = [keyword.strip() for keyword in re.split(search_string_separators, search_string)]
370
+
371
+        return keywords
372
+
373
+    def search(self, keywords: [str]) -> sqlalchemy.orm.query.Query:
374
+        """
375
+        :return: a sorted list of Content items
376
+        """
377
+
378
+        if len(keywords)<=0:
379
+            return None
380
+
381
+        filter_group_label = list(Content.label.ilike('%{}%'.format(keyword)) for keyword in keywords)
382
+        filter_group_desc = list(Content.description.ilike('%{}%'.format(keyword)) for keyword in keywords)
383
+        title_keyworded_items = self._base_query().\
384
+            filter(or_(*(filter_group_label+filter_group_desc))).\
385
+            options(joinedload('children')).\
386
+            options(joinedload('parent'))
387
+
388
+        return title_keyworded_items
389
+

+ 11 - 0
tracim/tracim/lib/helpers.py 查看文件

180
             return True
180
             return True
181
     return False
181
     return False
182
 
182
 
183
+def shorten(text: str, maxlength: int, add_three_points=True) -> str:
184
+
185
+    result = text
186
+    if len(text)>maxlength:
187
+        result = text[:maxlength]
188
+
189
+        if add_three_points:
190
+            result += '…'
191
+
192
+    return result
193
+
183
 from tracim.config.app_cfg import CFG as CFG_ORI
194
 from tracim.config.app_cfg import CFG as CFG_ORI
184
 CFG = CFG_ORI.get_instance() # local CFG var is an instance of CFG class found in app_cfg
195
 CFG = CFG_ORI.get_instance() # local CFG var is an instance of CFG class found in app_cfg

+ 17 - 0
tracim/tracim/lib/utils.py 查看文件

1
+# -*- coding: utf-8 -*-
2
+
3
+import time
4
+
5
+from tracim.lib.base import logger
6
+
7
+def exec_time_monitor():
8
+    def decorator_func(func):
9
+        def wrapper_func(*args, **kwargs):
10
+            start = time.time()
11
+            retval = func(*args, **kwargs)
12
+            end = time.time()
13
+            logger.debug(func, 'exec time: {} seconds'.format(end-start))
14
+            return retval
15
+        return wrapper_func
16
+    return decorator_func
17
+

+ 6 - 1
tracim/tracim/model/data.py 查看文件

311
         return [cls.Folder, cls.File, cls.Comment, cls.Thread, cls.Page]
311
         return [cls.Folder, cls.File, cls.Comment, cls.Thread, cls.Page]
312
 
312
 
313
     @classmethod
313
     @classmethod
314
+    def allowed_types_for_folding(cls):
315
+        # This method is used for showing only "main" types in the left-side treeview
316
+        return [cls.Folder, cls.File, cls.Thread, cls.Page]
317
+
318
+    @classmethod
314
     def allowed_types_from_str(cls, allowed_types_as_string: str):
319
     def allowed_types_from_str(cls, allowed_types_as_string: str):
315
         allowed_types = []
320
         allowed_types = []
316
         # HACK - THIS
321
         # HACK - THIS
317
         for item in allowed_types_as_string.split(ContentType._STRING_LIST_SEPARATOR):
322
         for item in allowed_types_as_string.split(ContentType._STRING_LIST_SEPARATOR):
318
-            if item and item in ContentType.allowed_types():
323
+            if item and item in ContentType.allowed_types_for_folding():
319
                 allowed_types.append(item)
324
                 allowed_types.append(item)
320
         return allowed_types
325
         return allowed_types
321
 
326
 

+ 63 - 12
tracim/tracim/model/serializers.py 查看文件

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 import types
2
 import types
3
 
3
 
4
+from bs4 import BeautifulSoup
4
 import tg
5
 import tg
5
 from tg.util import LazyString
6
 from tg.util import LazyString
6
 from tracim.lib.base import logger
7
 from tracim.lib.base import logger
7
-from tracim.model.auth import Group, Profile
8
+from tracim.lib.utils import exec_time_monitor
9
+from tracim.model.auth import Profile
8
 from tracim.model.auth import User
10
 from tracim.model.auth import User
9
 from tracim.model.data import BreadcrumbItem, ActionDescription
11
 from tracim.model.data import BreadcrumbItem, ActionDescription
10
 from tracim.model.data import ContentStatus
12
 from tracim.model.data import ContentStatus
47
         Context.register_converter(self.context, self.model_class, func)
49
         Context.register_converter(self.context, self.model_class, func)
48
         return func
50
         return func
49
 
51
 
50
-
51
 class ContextConverterNotFoundException(Exception):
52
 class ContextConverterNotFoundException(Exception):
52
     def __init__(self, context_string, model_class):
53
     def __init__(self, context_string, model_class):
53
         message = 'converter not found (context: {0} - model: {1})'.format(context_string, model_class.__name__)
54
         message = 'converter not found (context: {0} - model: {1})'.format(context_string, model_class.__name__)
68
     MENU_API_BUILD_FROM_TREE_ITEM = 'MENU_API_BUILD_FROM_TREE_ITEM'
69
     MENU_API_BUILD_FROM_TREE_ITEM = 'MENU_API_BUILD_FROM_TREE_ITEM'
69
     PAGE = 'PAGE'
70
     PAGE = 'PAGE'
70
     PAGES = 'PAGES'
71
     PAGES = 'PAGES'
72
+    SEARCH = 'SEARCH'
71
     THREAD = 'THREAD'
73
     THREAD = 'THREAD'
72
     THREADS = 'THREADS'
74
     THREADS = 'THREADS'
73
     USER = 'USER'
75
     USER = 'USER'
412
 
414
 
413
     if item.type==ContentType.Folder:
415
     if item.type==ContentType.Folder:
414
         return Context(CTX.DEFAULT).toDict(item)
416
         return Context(CTX.DEFAULT).toDict(item)
415
-    ### CODE BELOW IS REPLACED BY THE TWO LINES UP ^^
416
-    # 2014-10-08 - IF YOU FIND THIS COMMENT, YOU CAn REMOVE THE CODE
417
-    #
418
-    #if item.type==ContentType.Folder:
419
-    #    value = DictLikeClass(
420
-    #        id = item.content_id,
421
-    #        label = item.label,
422
-    #    )
423
-    #    return value
424
 
417
 
425
-    raise NotImplementedError
426
 
418
 
427
 
419
 
428
 @pod_serializer(Content, CTX.THREADS)
420
 @pod_serializer(Content, CTX.THREADS)
556
     return result
548
     return result
557
 
549
 
558
 
550
 
551
+from tg import cache
552
+
553
+@pod_serializer(Content, CTX.SEARCH)
554
+def serialize_content_for_search_result(content: Content, context: Context):
555
+
556
+    def serialize_it():
557
+        nonlocal content
558
+
559
+        if content.type == ContentType.Comment:
560
+            logger.info('serialize_content_for_search_result', 'Serializing parent class {} instead of {} [content #{}]'.format(content.parent.type, content.type, content.content_id))
561
+            content = content.parent
562
+
563
+        data_container = content
564
+
565
+        if content.revision_to_serialize>0:
566
+            for revision in content.revisions:
567
+                if revision.revision_id==content.revision_to_serialize:
568
+                    data_container = revision
569
+                    break
570
+
571
+        # FIXME - D.A. - 2015-02-23 - This import should not be there...
572
+        from tracim.lib.content import ContentApi
573
+        breadcrumbs = ContentApi(None).build_breadcrumb(data_container.workspace, data_container.content_id, skip_root=True)
574
+
575
+        last_comment_datetime = data_container.updated
576
+        comments = data_container.get_comments()
577
+        if comments:
578
+            last_comment_datetime = max(last_comment_datetime, max(comment.updated for comment in comments))
579
+
580
+        result = DictLikeClass(
581
+            id = content.content_id,
582
+            parent = context.toDict(content.parent),
583
+            workspace = context.toDict(content.workspace),
584
+            type = content.type,
585
+
586
+            content = data_container.description,
587
+            content_raw = BeautifulSoup(data_container.description).text,
588
+
589
+            created = data_container.created,
590
+            label = data_container.label,
591
+            icon = ContentType.icon(content.type),
592
+            owner = context.toDict(data_container.owner),
593
+            status = context.toDict(data_container.get_status()),
594
+            breadcrumb = context.toDict(breadcrumbs),
595
+            last_activity = last_comment_datetime
596
+        )
597
+
598
+        if content.type==ContentType.File:
599
+            result.label = content.label.__str__() if content.label else content.file_name.__str__()
600
+
601
+        if not result.label or ''==result.label:
602
+            result.label = 'No title'
603
+
604
+        return result
605
+
606
+    return serialize_it()
607
+
608
+
609
+
559
 ########################################################################################################################
610
 ########################################################################################################################
560
 # ContentStatus
611
 # ContentStatus
561
 
612
 

+ 17 - 3
tracim/tracim/public/assets/css/dashboard.css 查看文件

178
 
178
 
179
 table.first_row_headers tr:first-child td {
179
 table.first_row_headers tr:first-child td {
180
     font-weight: bold;
180
     font-weight: bold;
181
-    background-color: #EEE;
181
+    background-color: #EEE;
182
     align: center;
182
     align: center;
183
 }
183
 }
184
 
184
 
185
 table.first_column_headers tr td:first-child {
185
 table.first_column_headers tr td:first-child {
186
     font-weight: bold;
186
     font-weight: bold;
187
     background-color: #EEE;
187
     background-color: #EEE;
188
-    align: center;
189
-}
188
+    align: center;
189
+}
190
+
191
+.search-result-item-breadcrumb {
192
+    margin-top: -1em;
193
+}
194
+
195
+.search-result-item-breadcrumb a {
196
+    color: #090;
197
+}
198
+
199
+#search-result-dynamic-resume {
200
+    color: #090;
201
+    margin-top: -1em;
202
+    margin-bottom: 0;
203
+}

二進制
tracim/tracim/public/assets/icons/32x32/emblems/emblem-checked.png 查看文件


二進制
tracim/tracim/public/assets/icons/32x32/places/jstree-folder.png 查看文件


二進制
tracim/tracim/public/assets/icons/32x32/status/status-open.png 查看文件


二進制
tracim/tracim/public/assets/icons/32x32/status/status-outdated.png 查看文件


+ 7 - 0
tracim/tracim/templates/master_authenticated.mak 查看文件

113
                                 </ul>
113
                                 </ul>
114
                             </li>
114
                             </li>
115
                         % endif
115
                         % endif
116
+                        
117
+                        <form class="navbar-form navbar-left" role="search" action="${tg.url('/search?')}">
118
+                            <div class="form-group">
119
+                                <input type="text" class="form-control" placeholder="${_('Search for...')}" name="keywords" value="${','.join(search.keywords) if search else ''}">
120
+                            </div>
121
+                            <button type="submit" class="btn btn-default">${_('Search')}</button>
122
+                        </form>
116
                     </ul>
123
                     </ul>
117
                 % endif
124
                 % endif
118
 
125
 

+ 77 - 0
tracim/tracim/templates/search.mak 查看文件

1
+<%inherit file="local:templates.master_authenticated_left_treeview_right_toolbar"/>
2
+
3
+<%namespace name="TIM" file="tracim.templates.pod"/>
4
+<%namespace name="TOOLBAR" file="tracim.templates.search_toolbars"/>
5
+<%namespace name="FORMS" file="tracim.templates.user_workspace_forms"/>
6
+<%namespace name="WIDGETS" file="tracim.templates.user_workspace_widgets"/>
7
+
8
+<%def name="title()">${_('My workspaces')}</%def>
9
+
10
+<%def name="SIDEBAR_LEFT_CONTENT()">
11
+    <h4>${_('Workspaces')}</h4>
12
+    ${WIDGETS.TREEVIEW('sidebar-left-menu', '__')}
13
+    <hr/>
14
+</%def>
15
+
16
+<%def name="SIDEBAR_RIGHT_CONTENT()">
17
+    ${TOOLBAR.SECURED_SEARCH(fake_api.current_user)}
18
+</%def>
19
+
20
+<%def name="REQUIRED_DIALOGS()">
21
+</%def>
22
+
23
+############################################################################
24
+##
25
+## PAGE CONTENT BELOW
26
+##
27
+############################################################################
28
+
29
+<div class="row">
30
+    <h1 class="page-header">
31
+        ${TIM.ICO(32, 'actions/system-search')} ${_('Search results')}
32
+        <small>
33
+            ${_('<span class="badge">{}</span> results for keywords: '.format(search.result_nb))|n}
34
+            % for keyword in search.keywords:
35
+                <span class="label label-default">${keyword}</span>
36
+            % endfor
37
+        </small>
38
+    </h1>
39
+</div>
40
+<div class="row">
41
+    <p id="search-result-dynamic-resume">${_('loading...')}</p>
42
+</div>
43
+<div class="row">
44
+    <div id='application-document-panel'>
45
+        <ol class="search-results">
46
+            % for item in search.results:
47
+                <li class="search-result-type-${item.type} search-result-status-${item.status.id}">
48
+                    <h4>
49
+                        <a href="${item.breadcrumb[-1].url}">${TIM.ICO(16, item.icon)} ${item.label}</a>
50
+                         &nbsp;&nbsp;<span style="color: #AAA;">—</span>
51
+                         <button type="button" class="btn btn-default btn-disabled btn-link ">
52
+                             ${TIM.ICO(16, item['status']['icon'])}&nbsp;
53
+                             <span class="${item.status.css}">${item.status.label}</span>
54
+                         </button>
55
+                    </h4>
56
+                    <p style="margin-bottom: 2em;" class="search-result-item-breadcrumb">
57
+                        <i style="color: #AAA;" class="fa fa-fw fa-map-marker"></i>
58
+                        % for bread in item.breadcrumb:
59
+                            / <a href="${bread.url}">${bread.label}</a>
60
+                        % endfor
61
+                        <br/>
62
+                        <span
63
+                            style="color: #AAA;"
64
+##                            rel="tooltip"
65
+##                            data-toggle="tooltip"
66
+##                            data-placement="top"
67
+                            title="${_('Last known activty')}" ><i class="fa fa-fw fa-calendar"></i> ${h.date_time_in_long_format(item.last_activity, '%d %B %Y at %I:%M')}</span> &mdash;
68
+                            ${h.shorten(item.content_raw, 300)}
69
+                    </p>
70
+
71
+##                    <hr style="width: 33%; margin-left: 0;"/>
72
+                </li>
73
+            % endfor
74
+        </ol>
75
+    </div>
76
+</div>
77
+

+ 83 - 0
tracim/tracim/templates/search_toolbars.mak 查看文件

1
+<%namespace name="TIM" file="tracim.templates.pod"/>
2
+
3
+<%def name="SECURED_SEARCH(user)">
4
+    <%
5
+        content_types = [
6
+            ('page', 'mimetypes/text-html'),
7
+            ('file', 'status/mail-attachment'),
8
+            ('thread', 'apps/internet-group-chat'),
9
+            ('folder', 'places/jstree-folder')
10
+        ]
11
+
12
+        statuses = [
13
+            ('open', 'status/status-open'),
14
+            ('closed-validated', 'emblems/emblem-checked'),
15
+            ('closed-cancelled', 'emblems/emblem-unreadable'),
16
+            ('closed-deprecated', 'status/status-outdated')
17
+        ]
18
+    %>
19
+    <div class="btn-group btn-group-vertical" data-toggle="buttons">
20
+        % for content_type, icon in content_types:
21
+            <label class="btn btn-default active search-result-filter-button" id='show-hide-search-result-of-type-${content_type}'>
22
+                <input type="checkbox" autocomplete="off" checked> ${TIM.ICO(32, icon)}
23
+            </label>
24
+        % endfor
25
+    </div>
26
+    <p></p>
27
+    <div class="btn-group btn-group-vertical" data-toggle="buttons">
28
+        % for status, icon in statuses:
29
+            <label class="btn btn-default active search-result-filter-button" id='show-hide-search-result-with-status-${status}'>
30
+                <input type="checkbox" autocomplete="off" checked> ${TIM.ICO(32, icon)}
31
+            </label>
32
+        % endfor
33
+    </div>
34
+    <p></p>
35
+    
36
+    <script>
37
+        $(document).ready(function() {
38
+            % for content_type, icon in content_types: # python code
39
+                $('#show-hide-search-result-of-type-${content_type}').click(function() {
40
+                    if ($('#show-hide-search-result-of-type-${content_type}').hasClass('active')) {
41
+                        $('.search-result-type-${content_type}').hide();
42
+                    } else {
43
+                        $('.search-result-type-${content_type}').show();
44
+                    }
45
+                });
46
+            % endfor # python code
47
+
48
+            % for status, icon in statuses: # python code
49
+                $('#show-hide-search-result-with-status-${status}').click(function() {
50
+                    console.log('clieck')
51
+                    if ($('#show-hide-search-result-with-status-${status}').hasClass('active')) {
52
+                        $('.search-result-status-${status}').hide();
53
+                    } else {
54
+                        $('.search-result-status-${status}').show();
55
+                    }
56
+                });
57
+            % endfor # python code
58
+            
59
+            function refresh_search_result_count() {
60
+                var itemNb = $('ol.search-results > li').length;
61
+                var visibleItemNb = $('ol.search-results > li:visible').length;
62
+                var message = "${_('Showing {0} filtered items of {1}')}"
63
+                if(visibleItemNb<=1) {
64
+                    message = "${_('Showing {0} filtered item of a total of {1}')}"
65
+                } else if (visibleItemNb==itemNb) {
66
+                    message = "${_('Showing all items. You can filter by clicking right toolbar buttons.')}"
67
+                }
68
+
69
+                message = message.replace('{0}', visibleItemNb)
70
+                message = message.replace('{1}', itemNb)
71
+                $('#search-result-dynamic-resume').html(message);
72
+            }
73
+
74
+            $('.search-result-filter-button').click(function() {
75
+                refresh_search_result_count();
76
+            });
77
+            
78
+            refresh_search_result_count();
79
+        });
80
+    </script>
81
+
82
+</%def>
83
+