0001-backoffice-add-advisory-locking-to-formdata-10075.patch
tests/test_backoffice_pages.py | ||
---|---|---|
1715 | 1715 |
resp = resp.form.submit('button_commentable') |
1716 | 1716 |
resp = resp.follow() |
1717 | 1717 |
assert '<h2 class="foldable folded">Summary</h2>' in resp.body |
1718 | ||
1719 |
def test_backoffice_advistory_lock(pub): |
|
1720 |
pub.session_manager.session_class.wipe() |
|
1721 |
create_superuser(pub) |
|
1722 |
create_environment(pub) |
|
1723 | ||
1724 |
second_user = pub.user_class(name='foobar') |
|
1725 |
second_user.roles = Role.keys() |
|
1726 |
second_user.store() |
|
1727 |
account = PasswordAccount(id='foobar') |
|
1728 |
account.set_password('foobar') |
|
1729 |
account.user_id = second_user.id |
|
1730 |
account.store() |
|
1731 | ||
1732 |
app = login(get_app(pub)) |
|
1733 |
resp = app.get('/backoffice/management/form-title/') |
|
1734 |
first_link = re.findall('data-link="(\d+/)?"', resp.body)[0] |
|
1735 |
assert not 'actively-visited' in resp.body |
|
1736 | ||
1737 |
app2 = login(get_app(pub), username='foobar', password='foobar') |
|
1738 |
resp = app2.get('/backoffice/management/form-title/') |
|
1739 |
assert not 'actively-visited' in resp.body |
|
1740 | ||
1741 |
resp = app.get('/backoffice/management/form-title/' + first_link) |
|
1742 |
resp = app2.get('/backoffice/management/form-title/') |
|
1743 |
assert 'actively-visited' in resp.body |
|
1744 |
resp = app.get('/backoffice/management/form-title/') |
|
1745 |
assert not 'actively-visited' in resp.body |
|
1746 | ||
1747 |
if not pub.is_using_postgresql(): |
|
1748 |
return |
|
1749 | ||
1750 |
resp = app2.get('/backoffice/management/listing?limit=100') |
|
1751 |
assert 'actively-visited' in resp.body |
|
1752 |
resp = app.get('/backoffice/management/listing?limit=100') |
|
1753 |
assert not 'actively-visited' in resp.body |
|
1754 | ||
1755 |
resp = app.get('/backoffice/management/form-title/' + first_link) |
|
1756 |
assert not 'Be warned this form is also being' in resp.body |
|
1757 |
resp = app2.get('/backoffice/management/form-title/' + first_link) |
|
1758 |
assert 'Be warned this form is also being' in resp.body |
|
1759 |
# revisit with first user |
|
1760 |
resp = app.get('/backoffice/management/form-title/' + first_link) |
|
1761 |
assert 'Be warned this form is also being' in resp.body |
tests/test_sessions.py | ||
---|---|---|
6 | 6 |
from quixote import cleanup |
7 | 7 | |
8 | 8 |
from wcs.qommon.ident.password_accounts import PasswordAccount |
9 |
from wcs.qommon.http_request import HTTPRequest |
|
9 | 10 | |
10 | 11 |
from utilities import create_temporary_pub, clean_temporary_pub, get_app, login |
11 | 12 | |
... | ... | |
29 | 30 |
pub.write_cfg() |
30 | 31 |
return pub |
31 | 32 | |
33 |
@pytest.fixture |
|
34 |
def http_request(pub): |
|
35 |
req = HTTPRequest(None, {}) |
|
36 |
req.language = None |
|
37 |
pub._set_request(req) |
|
38 | ||
32 | 39 | |
33 | 40 |
@pytest.fixture |
34 | 41 |
def user(pub): |
... | ... | |
71 | 78 |
session.set_expire(time.time() - 1) |
72 | 79 |
session.store() |
73 | 80 |
assert 'Logout' not in app.get('/') |
81 | ||
82 |
def test_sessions_visiting_objects(pub, http_request): |
|
83 |
manager = pub.session_manager_class() |
|
84 |
# check it starts with nothing |
|
85 |
assert len(pub.get_visited_objects()) == 0 |
|
86 | ||
87 |
# mark two visits |
|
88 |
session1 = manager.session_class(id='session1') |
|
89 |
session1.user = 'FOO' |
|
90 |
session1.mark_visited_object('formdata-foobar-1') |
|
91 |
session1.mark_visited_object('formdata-foobar-2') |
|
92 |
session1.store() |
|
93 |
assert len(pub.get_visited_objects()) == 2 |
|
94 |
assert pub.get_object_visitors('formdata-foobar-2') == ['FOO'] |
|
95 | ||
96 |
# mark a visit as being in the past |
|
97 |
session1.visiting_objects['formdata-foobar-1'] = time.time() - 35*60 |
|
98 |
session1.store() |
|
99 |
assert len(pub.get_visited_objects()) == 1 |
|
100 | ||
101 |
# check older visits are automatically removed |
|
102 |
session1 = manager.session_class.get('session1') |
|
103 |
assert len(session1.visiting_objects.keys()) == 2 |
|
104 |
session1.mark_visited_object('formdata-foobar-2') |
|
105 |
assert len(session1.visiting_objects.keys()) == 1 |
|
106 |
session1.store() |
|
107 |
assert len(pub.get_visited_objects()) == 1 |
|
108 |
assert pub.get_visited_objects() == ['formdata-foobar-2'] |
|
109 | ||
110 |
# check with a second session |
|
111 |
session1.mark_visited_object('formdata-foobar-1') |
|
112 |
session1.mark_visited_object('formdata-foobar-2') |
|
113 |
session1.store() |
|
114 |
assert len(pub.get_visited_objects()) == 2 |
|
115 | ||
116 |
# mark a visit as being in the past |
|
117 |
session1.visiting_objects['formdata-foobar-1'] = time.time() - 35*60 |
|
118 |
session1.store() |
|
119 |
assert len(pub.get_visited_objects()) == 1 |
|
120 | ||
121 |
# check older visits are automatically removed |
|
122 |
session1 = manager.session_class.get('session1') |
|
123 |
assert len(session1.visiting_objects.keys()) == 2 |
|
124 |
session1.mark_visited_object('formdata-foobar-2') |
|
125 |
assert len(session1.visiting_objects.keys()) == 1 |
|
126 |
session1.store() |
|
127 |
assert len(pub.get_visited_objects()) == 1 |
|
128 |
assert pub.get_visited_objects() == ['formdata-foobar-2'] |
|
129 | ||
130 |
# check with a second session |
|
131 |
session2 = manager.session_class(id='session2') |
|
132 |
session2.user = 'BAR' |
|
133 |
session2.store() |
|
134 |
assert len(pub.get_visited_objects()) == 1 |
|
135 |
session2.mark_visited_object('formdata-foobar-2') |
|
136 |
session2.store() |
|
137 |
assert len(pub.get_visited_objects()) == 1 |
|
138 |
session2.mark_visited_object('formdata-foobar-3') |
|
139 |
session2.store() |
|
140 |
assert len(pub.get_visited_objects()) == 2 |
|
141 | ||
142 |
assert pub.get_visited_objects(exclude_user='BAR') == ['formdata-foobar-2'] |
|
143 | ||
144 |
# check visitors |
|
145 |
assert set(pub.get_object_visitors('formdata-foobar-2')) == set(['FOO', 'BAR']) |
|
146 |
assert set(pub.get_object_visitors('formdata-foobar-1')) == set([]) |
wcs/backoffice/management.py | ||
---|---|---|
687 | 687 |
r = TemplateIO(html=True) |
688 | 688 |
r += htmltext('<table id="listing" class="main">') |
689 | 689 |
r += htmltext('<thead>') |
690 |
r += htmltext('<th></th>') # lock |
|
690 | 691 |
if include_submission_channel: |
691 | 692 |
r += htmltext('<th data-field-sort-key="submission_channel"><span>%s</span></th>') % _('Channel') |
692 | 693 |
r += htmltext('<th data-field-sort-key="formdef_name"><span>%s</span></th>') % _('Form') |
... | ... | |
698 | 699 |
r += htmltext('</thead>') |
699 | 700 |
r += htmltext('<tbody>') |
700 | 701 |
workflows = {} |
702 |
visited_objects = get_publisher().get_visited_objects(exclude_user=get_session().user) |
|
701 | 703 |
for formdata in formdatas: |
702 | 704 |
if not formdata.formdef.workflow_id in workflows: |
703 | 705 |
workflows[formdata.formdef.workflow_id] = formdata.formdef.workflow |
704 |
r += htmltext('<tr class="status-%s-%s" data-link="%s">' % ( |
|
705 |
formdata.formdef.workflow.id, |
|
706 |
formdata.status, |
|
706 | ||
707 |
classes = ['status-%s-%s' % (formdata.formdef.workflow.id, formdata.status)] |
|
708 |
object_key = 'formdata-%s-%s' % (formdata.formdef.url_name, formdata.id) |
|
709 |
if object_key in visited_objects: |
|
710 |
classes.append('actively-visited') |
|
711 |
r += htmltext('<tr class="%s" data-link="%s">' % ( |
|
712 |
' '.join(classes), |
|
707 | 713 |
formdata.get_url(backoffice=True))) |
714 |
r += htmltext('<td></td>') # lock |
|
708 | 715 |
if include_submission_channel: |
709 | 716 |
r += htmltext('<td>%s</td>') % formdata.get_submission_channel_label() |
710 | 717 |
r += htmltext('<td>%s</td>') % formdata.formdef.name |
wcs/forms/backoffice.py | ||
---|---|---|
68 | 68 |
r += htmltext('<table id="listing" class="sortable tablesorter">') |
69 | 69 | |
70 | 70 |
r += htmltext('<colgroup>') |
71 |
r += htmltext('<col/>') # lock |
|
71 | 72 |
r += htmltext('<col/>') |
72 | 73 |
r += htmltext('<col/>') |
73 | 74 |
for f in fields: |
... | ... | |
75 | 76 |
r += htmltext('</colgroup>') |
76 | 77 | |
77 | 78 |
r += htmltext('<thead><tr>') |
79 |
r += htmltext('<td></td>') # lock |
|
78 | 80 |
for f in fields: |
79 | 81 |
field_sort_key = None |
80 | 82 |
if getattr(f, 'fake', False): |
... | ... | |
206 | 208 |
else: |
207 | 209 |
url_action = '' |
208 | 210 |
root_url = get_publisher().get_root_url() |
211 |
visited_objects = get_publisher().get_visited_objects(exclude_user=get_session().user) |
|
209 | 212 |
for i, filled in enumerate(items): |
213 |
classes = ['status-%s-%s' % (filled.formdef.workflow.id, filled.status)] |
|
210 | 214 |
if i%2: |
211 |
style = 'even'
|
|
215 |
classes.append('even')
|
|
212 | 216 |
else: |
213 |
style = 'odd' |
|
217 |
classes.append('odd') |
|
218 | ||
219 |
object_key = 'formdata-%s-%s' % (filled.formdef.url_name, filled.id) |
|
220 |
if object_key in visited_objects: |
|
221 |
classes.append('actively-visited') |
|
222 | ||
214 | 223 |
link = str(filled.id) + '/' |
215 | 224 |
data = ' data-link="%s"' % link |
216 | 225 |
if filled.anonymised: |
217 | 226 |
data += ' data-anonymised="true"' |
218 |
r += htmltext('<tr class="status-%s-%s %s"%s>' % (filled.formdef.workflow.id,
|
|
219 |
filled.status, style, data))
|
|
227 |
r += htmltext('<tr class="%s"%s>' % (' '.join(classes), data))
|
|
228 |
r += htmltext('<td></td>') # lock
|
|
220 | 229 |
for i, f in enumerate(fields): |
221 | 230 |
if f.type == 'id': |
222 | 231 |
r += htmltext('<td class="cell-id"><a href="%s%s">%s</a></td>') % (link, url_action, |
wcs/forms/common.py | ||
---|---|---|
522 | 522 |
r += self.history() |
523 | 523 | |
524 | 524 |
if form: |
525 |
object_key = 'formdata-%s-%s' % (self.formdef.url_name, self.filled.id) |
|
526 |
visitors = [x for x in get_publisher().get_object_visitors(object_key) |
|
527 |
if x != get_session().user] |
|
528 |
if visitors: |
|
529 |
visitor_users = [] |
|
530 |
for visitor_id in visitors: |
|
531 |
try: |
|
532 |
visitor_users.append(get_publisher().user_class.get(visitor_id).display_name) |
|
533 |
except KeyError: |
|
534 |
pass |
|
535 |
if visitor_users: |
|
536 |
r += htmltext('<div class="infonotice"><p>') |
|
537 |
r += _('Be warned this form is also being looked at by: ' |
|
538 |
'%s.') % ', '.join(visitor_users) |
|
539 |
r += htmltext('</p></div>') |
|
525 | 540 |
r += form.render() |
541 |
get_session().mark_visited_object(object_key) |
|
526 | 542 | |
527 | 543 |
if (self.filled.get_status() and self.filled.get_status().backoffice_info_text) or ( |
528 | 544 |
form and any((getattr(button, 'backoffice_info_text', None) |
... | ... | |
654 | 670 |
f.edited_data = self.filled |
655 | 671 |
f.edit_action_id = action_id |
656 | 672 |
f.action_url = 'wfedit-%s' % action_id |
673 |
get_session().mark_visited_object('formdata-%s-%s' % ( |
|
674 |
self.formdef.url_name, self.filled.id)) |
|
657 | 675 |
get_response().breadcrumb = get_response().breadcrumb[:-1] |
658 | 676 |
get_response().breadcrumb.append((f.action_url, _('Edit'))) |
659 | 677 |
return f._q_index() |
wcs/publisher.py | ||
---|---|---|
214 | 214 |
request.response.iframe_mode = True |
215 | 215 |
return QommonPublisher.try_publish(self, request) |
216 | 216 | |
217 |
def get_object_visitors(self, object_key): |
|
218 |
session_manager = self.session_manager_class() |
|
219 |
return session_manager.session_class.get_object_visitors(object_key) |
|
220 | ||
221 |
def get_visited_objects(self, exclude_user=None): |
|
222 |
session_manager = self.session_manager_class() |
|
223 |
return session_manager.session_class.get_visited_objects( |
|
224 |
exclude_user=exclude_user) |
|
225 | ||
217 | 226 |
def initialize_sql(self): |
218 | 227 |
import sql |
219 | 228 |
sql.get_connection(new=True) |
wcs/qommon/static/css/dc2/admin.css | ||
---|---|---|
339 | 339 |
margin-bottom: 1em; |
340 | 340 |
} |
341 | 341 | |
342 |
#listing thead td { |
|
343 |
background: white; |
|
344 |
} |
|
345 | ||
342 | 346 |
ul.biglist, table#listing { |
343 | 347 |
-webkit-transition: opacity 500ms ease-out; |
344 | 348 |
-moz-transition: opacity 500ms ease-out; |
... | ... | |
412 | 416 |
margin: 0 1ex; |
413 | 417 |
} |
414 | 418 | |
419 |
table.main tr.actively-visited { |
|
420 |
opacity: 0.5; |
|
421 |
} |
|
422 | ||
423 |
tr.actively-visited td:first-child::before { |
|
424 |
font-family: FontAwesome; |
|
425 |
content: "\f023"; /* lock */ |
|
426 |
} |
|
427 | ||
415 | 428 |
div.bo-block ul.biglist li.user-is-admin strong a { |
416 | 429 |
border-left: 5px solid #0099ff; |
417 | 430 |
} |
wcs/sessions.py | ||
---|---|---|
15 | 15 |
# along with this program; if not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 | 17 |
import random |
18 |
import time |
|
18 | 19 | |
19 | 20 |
import qommon.sessions |
20 | 21 |
from qommon.sessions import Session |
... | ... | |
24 | 25 |
anonymous_key = None |
25 | 26 |
magictokens = None |
26 | 27 |
anonymous_formdata_keys = None |
28 |
visiting_objects = None |
|
27 | 29 | |
28 | 30 |
def has_info(self): |
29 |
return self.anonymous_formdata_keys or self.anonymous_key or self.magictokens or Session.has_info(self) |
|
31 |
return (self.anonymous_formdata_keys or self.anonymous_key or |
|
32 |
self.magictokens or self.visiting_objects or Session.has_info(self)) |
|
30 | 33 |
is_dirty = has_info |
31 | 34 | |
32 | 35 |
def get_anonymous_key(self, generate = False): |
... | ... | |
63 | 66 |
formdata_key = '%s-%s' % (formdata.formdef.id, formdata.id) |
64 | 67 |
return formdata_key in self.anonymous_formdata_keys |
65 | 68 | |
69 |
def mark_visited_object(self, key): |
|
70 |
if not self.visiting_objects: |
|
71 |
self.visiting_objects = {} |
|
72 |
# first clean older objects |
|
73 |
current_timestamp = time.time() |
|
74 |
for object_key, object_timestamp in self.visiting_objects.items(): |
|
75 |
if object_timestamp < (current_timestamp - 30*60): |
|
76 |
del self.visiting_objects[object_key] |
|
77 |
self.visiting_objects[key] = current_timestamp |
|
78 | ||
79 |
@classmethod |
|
80 |
def get_visited_objects(cls, exclude_user=None): |
|
81 |
# return the list of visited objects |
|
82 |
current_timestamp = time.time() |
|
83 |
visited_objects = {} |
|
84 |
for session in cls.select(): |
|
85 |
if session.user and session.user == exclude_user: |
|
86 |
continue |
|
87 |
visiting_objects = getattr(session, 'visiting_objects', None) |
|
88 |
if not visiting_objects: |
|
89 |
continue |
|
90 |
for object_key, object_timestamp in visiting_objects.items(): |
|
91 |
if object_timestamp > (current_timestamp - 30*60): |
|
92 |
visited_objects[object_key] = True |
|
93 |
return visited_objects.keys() |
|
94 | ||
95 |
@classmethod |
|
96 |
def get_object_visitors(cls, object_key): |
|
97 |
current_timestamp = time.time() |
|
98 |
visitors = {} |
|
99 |
for session in cls.select(): |
|
100 |
visiting_objects = getattr(session, 'visiting_objects', None) |
|
101 |
if not visiting_objects: |
|
102 |
continue |
|
103 |
object_timestamp = visiting_objects.get(object_key) |
|
104 |
if not object_timestamp: |
|
105 |
continue |
|
106 |
if object_timestamp > (current_timestamp - 30*60): |
|
107 |
visitors[session.user] = True |
|
108 |
return visitors.keys() |
|
109 | ||
66 | 110 |
qommon.sessions.BasicSession = BasicSession |
67 | 111 |
StorageSessionManager = qommon.sessions.StorageSessionManager |
68 |
- |