Projet

Général

Profil

0001-backoffice-add-advisory-locking-to-formdata-10075.patch

Frédéric Péters, 28 février 2016 11:58

Télécharger (16,2 ko)

Voir les différences:

Subject: [PATCH] backoffice: add advisory locking to formdata (#10075)

 tests/test_backoffice_pages.py      | 44 ++++++++++++++++++++++
 tests/test_sessions.py              | 73 +++++++++++++++++++++++++++++++++++++
 wcs/backoffice/management.py        | 13 +++++--
 wcs/forms/backoffice.py             | 17 +++++++--
 wcs/forms/common.py                 | 18 +++++++++
 wcs/publisher.py                    |  9 +++++
 wcs/qommon/static/css/dc2/admin.css | 13 +++++++
 wcs/sessions.py                     | 46 ++++++++++++++++++++++-
 8 files changed, 225 insertions(+), 8 deletions(-)
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
-