|
|
@@ -2,15 +2,25 @@
|
|
2
|
2
|
|
|
3
|
3
|
__author__ = 'damien'
|
|
4
|
4
|
|
|
|
5
|
+import re
|
|
|
6
|
+
|
|
5
|
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
|
13
|
from sqlalchemy.orm.attributes import get_history
|
|
8
|
14
|
from sqlalchemy import not_
|
|
|
15
|
+from sqlalchemy import or_
|
|
9
|
16
|
from tracim.lib import cmp_to_key
|
|
10
|
17
|
from tracim.lib.notifications import NotifierFactory
|
|
11
|
18
|
from tracim.model import DBSession
|
|
12
|
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
|
24
|
from tracim.model.data import Content
|
|
15
|
25
|
from tracim.model.data import ContentType
|
|
16
|
26
|
from tracim.model.data import NodeTreeItem
|
|
|
@@ -48,6 +58,10 @@ def compare_tree_items_for_sorting_by_type_and_name(item1: NodeTreeItem, item2:
|
|
48
|
58
|
|
|
49
|
59
|
class ContentApi(object):
|
|
50
|
60
|
|
|
|
61
|
+ SEARCH_SEPARATORS = ',| '
|
|
|
62
|
+ SEARCH_DEFAULT_RESULT_NB = 50
|
|
|
63
|
+
|
|
|
64
|
+
|
|
51
|
65
|
def __init__(self, current_user: User, show_archived=False, show_deleted=False, all_content_in_treeview=True):
|
|
52
|
66
|
self._user = current_user
|
|
53
|
67
|
self._show_archived = show_archived
|
|
|
@@ -71,6 +85,45 @@ class ContentApi(object):
|
|
71
|
85
|
content_list.sort(key=cmp_to_key(compare_content_for_sorting_by_type_and_name))
|
|
72
|
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
|
128
|
def _base_query(self, workspace: Workspace=None):
|
|
76
|
129
|
result = DBSession.query(Content)
|
|
|
@@ -79,10 +132,16 @@ class ContentApi(object):
|
|
79
|
132
|
result = result.filter(Content.workspace_id==workspace.workspace_id)
|
|
80
|
133
|
|
|
81
|
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
|
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
|
146
|
return result
|
|
88
|
147
|
|
|
|
@@ -110,7 +169,7 @@ class ContentApi(object):
|
|
110
|
169
|
all()
|
|
111
|
170
|
|
|
112
|
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
|
174
|
# Now, the case is to filter folders by the content that they are allowed to contain
|
|
116
|
175
|
result = []
|
|
|
@@ -118,6 +177,7 @@ class ContentApi(object):
|
|
118
|
177
|
for allowed_content_type in filter_by_allowed_content_types:
|
|
119
|
178
|
if folder.type==ContentType.Folder and folder.properties['allowed_content'][allowed_content_type]==True:
|
|
120
|
179
|
result.append(folder)
|
|
|
180
|
+ break
|
|
121
|
181
|
|
|
122
|
182
|
return result
|
|
123
|
183
|
|
|
|
@@ -295,3 +355,35 @@ class ContentApi(object):
|
|
295
|
355
|
if do_notify:
|
|
296
|
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
|
+
|