Projet

Général

Profil

0004-studio-recent-errors-changes-on-home-page-more-links.patch

Lauréline Guérin, 03 mars 2022 14:35

Télécharger (23,5 ko)

Voir les différences:

Subject: [PATCH 4/4] studio: recent errors & changes on home page, more links
 (#58796)

 tests/admin_pages/test_studio.py         | 272 ++++++++++++++++++++++-
 wcs/backoffice/studio.py                 |  41 +++-
 wcs/qommon/static/css/dc2/admin.scss     |  16 +-
 wcs/snapshots.py                         |  19 +-
 wcs/sql.py                               |  16 ++
 wcs/templates/wcs/backoffice/studio.html |  62 ++++--
 6 files changed, 394 insertions(+), 32 deletions(-)
tests/admin_pages/test_studio.py
1
import datetime
2
from collections import defaultdict
3

  
1 4
import pytest
2 5

  
6
from wcs.blocks import BlockDef
7
from wcs.carddef import CardDef
8
from wcs.data_sources import NamedDataSource
9
from wcs.formdef import FormDef
10
from wcs.mail_templates import MailTemplate
3 11
from wcs.qommon.http_request import HTTPRequest
12
from wcs.qommon.storage import Equal
13
from wcs.workflows import Workflow
14
from wcs.wscalls import NamedWsCall
4 15

  
5 16
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
6 17
from .test_all import create_superuser
......
11 22
        metafunc.parametrize('pub', ['pickle', 'sql'], indirect=True)
12 23

  
13 24

  
14
@pytest.fixture
15
def pub(request):
16
    pub = create_temporary_pub(sql_mode=bool('sql' in request.param))
25
def pub_fixture(**kwargs):
26
    pub = create_temporary_pub(**kwargs)
17 27

  
18 28
    req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
19 29
    pub.set_app_dir(req)
......
24 34
    return pub
25 35

  
26 36

  
37
@pytest.fixture
38
def pub(request):
39
    return pub_fixture(sql_mode=(request.param == 'sql'))
40

  
41

  
42
@pytest.fixture
43
def sql_pub(request):
44
    return pub_fixture(sql_mode=True)
45

  
46

  
27 47
def teardown_module(module):
28 48
    clean_temporary_pub()
29 49

  
......
37 57
    assert '../forms/' in resp.text
38 58
    assert '../cards/' in resp.text
39 59
    assert '../workflows/' in resp.text
40
    assert 'Logged Errors' in resp.text
60
    assert '../forms/data-sources/' in resp.text
61
    assert '../workflows/data-sources/' not in resp.text
62
    assert '../settings/data-sources/' not in resp.text
63
    assert '../forms/blocks/' in resp.text
64
    assert '../workflows/mail-templates/' in resp.text
65
    assert '../settings/wscalls/' in resp.text
66
    assert 'see all errors' in resp.text
41 67

  
42 68
    pub.cfg['admin-permissions'] = {}
43 69
    for part in ('forms', 'cards', 'workflows'):
......
47 73
        if part != 'workflows':
48 74
            resp = app.get('/backoffice/studio/')
49 75
            assert '../%s/' % part not in resp.text
76
            assert '../forms/data-sources/' not in resp.text
77
            assert '../workflows/data-sources/' in resp.text
78
            assert '../settings/data-sources/' not in resp.text
50 79
        else:
51 80
            resp = app.get('/backoffice/studio/', status=403)  # totally closed
52 81

  
53 82
    resp = app.get('/backoffice/')
54
    assert 'studio' not in resp.text
83
    assert 'backoffice/studio' not in resp.text
84

  
85
    # access to cards only (and settings)
86
    pub.cfg['admin-permissions'] = {}
87
    pub.cfg['admin-permissions'].update({'forms': ['x'], 'workflows': ['x']})
88
    pub.write_cfg()
89
    resp = app.get('/backoffice/studio/')
90
    assert '../forms/' not in resp.text
91
    assert '../cards/' in resp.text
92
    assert '../workflows/' not in resp.text
93
    assert '../settings/data-sources/' in resp.text
94
    assert '../settings/wscalls/' in resp.text
95

  
96
    # no access to settings
97
    pub.cfg['admin-permissions'].update({'settings': ['x']})
98
    pub.write_cfg()
99
    resp = app.get('/backoffice/studio/')
100
    assert '../forms/' not in resp.text
101
    assert '../cards/' in resp.text
102
    assert '../workflows/' not in resp.text
103
    assert '../settings/' not in resp.text
104

  
105

  
106
def test_studio_home_recent_errors(sql_pub):
107
    pub = sql_pub
108
    create_superuser(pub)
109

  
110
    app = login(get_app(pub))
111
    resp = app.get('/backoffice/studio/')
112
    assert resp.text.count('logged-errors/') == 1
113

  
114
    def new_error():
115
        error = pub.loggederror_class()
116
        error.summary = 'Lonely Logged Error'
117
        error.exception_class = 'Exception'
118
        error.exception_message = 'foo bar'
119
        error.first_occurence_timestamp = datetime.datetime.now()
120
        error.occurences_count = 17654032
121
        error.store()
122
        return error
123

  
124
    errors = [new_error()]
125
    resp = app.get('/backoffice/studio/')
126
    assert resp.text.count('logged-errors/') == 2
127
    assert 'logged-errors/%s/' % errors[0].id in resp
128

  
129
    for i in range(5):
130
        errors.append(new_error())
131
    resp = app.get('/backoffice/studio/')
132
    assert resp.text.count('logged-errors/') == 6
133
    # five recent errors displayed
134
    assert 'logged-errors/%s/' % errors[0].id not in resp
135
    assert 'logged-errors/%s/' % errors[1].id in resp
136
    assert 'logged-errors/%s/' % errors[2].id in resp
137
    assert 'logged-errors/%s/' % errors[3].id in resp
138
    assert 'logged-errors/%s/' % errors[4].id in resp
139
    assert 'logged-errors/%s/' % errors[5].id in resp
140

  
141

  
142
def test_studio_home_recent_changes(sql_pub):
143
    pub = sql_pub
144
    create_superuser(pub)
145

  
146
    BlockDef.wipe()
147
    CardDef.wipe()
148
    NamedDataSource.wipe()
149
    FormDef.wipe()
150
    MailTemplate.wipe()
151
    Workflow.wipe()
152
    NamedWsCall.wipe()
153

  
154
    objects = defaultdict(list)
155
    for i in range(6):
156
        for klass in [BlockDef, CardDef, NamedDataSource, FormDef, MailTemplate, Workflow, NamedWsCall]:
157
            obj = klass()
158
            obj.name = 'foo %s' % i
159
            obj.store()
160
            objects[klass.xml_root_node].append(obj)
161
    for klass in [BlockDef, CardDef, NamedDataSource, FormDef, MailTemplate, Workflow, NamedWsCall]:
162
        assert pub.snapshot_class.count(clause=[Equal('object_type', klass.xml_root_node)]) == 6
163
        # 2 snapshots for this one, but will be displayed only once
164
        objects[klass.xml_root_node][-1].name += ' bar'
165
        objects[klass.xml_root_node][-1].store()
166
        assert pub.snapshot_class.count(clause=[Equal('object_type', klass.xml_root_node)]) == 7
167

  
168
    app = login(get_app(pub))
169
    resp = app.get('/backoffice/studio/')
170
    # too old
171
    for i in range(5):
172
        assert 'backoffice/forms/blocks/%s/' % objects[BlockDef.xml_root_node][i].id not in resp
173
        assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][i].id not in resp
174
        assert (
175
            'backoffice/settings/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
176
        )
177
        assert 'backoffice/forms/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
178
        assert (
179
            'backoffice/workflows/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
180
        )
181
        assert 'backoffice/forms/%s/' % objects[FormDef.xml_root_node][i].id not in resp
182
        assert (
183
            'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][i].id not in resp
184
        )
185
        assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][i].id not in resp
186
        assert 'backoffice/settings/wscalls/%s/' % objects[NamedWsCall.xml_root_node][i].id not in resp
187

  
188
    # too old
189
    assert 'backoffice/forms/blocks/%s/' % objects[BlockDef.xml_root_node][5].id not in resp
190
    assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][5].id not in resp
191
    # only 5 elements
192
    assert 'backoffice/settings/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id in resp
193
    assert (
194
        'backoffice/forms/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id not in resp
195
    )  # not this url
196
    assert (
197
        'backoffice/workflows/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id
198
        not in resp  # not this url
199
    )
200
    assert 'backoffice/forms/%s/' % objects[FormDef.xml_root_node][5].id in resp
201
    assert 'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][5].id in resp
202
    assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][5].id in resp
203
    assert 'backoffice/settings/wscalls/%s/' % objects[NamedWsCall.xml_root_node][5].id in resp
204

  
205
    pub.cfg['admin-permissions'] = {}
206
    pub.cfg['admin-permissions'].update({'settings': ['x']})
207
    pub.write_cfg()
208

  
209
    app = login(get_app(pub))
210
    resp = app.get('/backoffice/studio/')
211
    # no access to settings
212
    for i in range(6):
213
        assert (
214
            'backoffice/settings/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
215
        )
216
        assert 'backoffice/settings/wscalls/%s/' % objects[NamedWsCall.xml_root_node][i].id not in resp
217
    # too old
218
    for i in range(5):
219
        assert 'backoffice/forms/blocks/%s/' % objects[BlockDef.xml_root_node][i].id not in resp
220
        assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][i].id not in resp
221
        assert 'backoffice/forms/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
222
        assert (
223
            'backoffice/workflows/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
224
        )
225
        assert 'backoffice/forms/%s/' % objects[FormDef.xml_root_node][i].id not in resp
226
        assert (
227
            'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][i].id not in resp
228
        )
229
        assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][i].id not in resp
230
    # too old
231
    assert 'backoffice/forms/blocks/%s/' % objects[BlockDef.xml_root_node][5].id not in resp
232
    # only 5 elements
233
    assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][5].id in resp
234
    assert 'backoffice/forms/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id in resp
235
    assert (
236
        'backoffice/workflows/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id
237
        not in resp  # not this url
238
    )
239
    assert 'backoffice/forms/%s/' % objects[FormDef.xml_root_node][5].id in resp
240
    assert 'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][5].id in resp
241
    assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][5].id in resp
242

  
243
    pub.cfg['admin-permissions'] = {}
244
    pub.cfg['admin-permissions'].update({'settings': ['x'], 'forms': ['x']})
245
    pub.write_cfg()
246

  
247
    app = login(get_app(pub))
248
    resp = app.get('/backoffice/studio/')
249
    # no access to settings or forms
250
    for i in range(6):
251
        assert 'backoffice/forms/blocks/%s/' % objects[BlockDef.xml_root_node][i].id not in resp
252
        assert (
253
            'backoffice/settings/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
254
        )
255
        assert 'backoffice/forms/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
256
        assert 'backoffice/forms/%s/' % objects[FormDef.xml_root_node][i].id not in resp
257
        assert 'backoffice/settings/wscalls/%s/' % objects[NamedWsCall.xml_root_node][i].id not in resp
258
    # too old
259
    for i in range(4):
260
        assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][i].id not in resp
261
        assert (
262
            'backoffice/workflows/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
263
        )
264
        assert (
265
            'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][i].id not in resp
266
        )
267
        assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][i].id not in resp
268
    # too old
269
    assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][4].id not in resp
270
    assert 'backoffice/workflows/data-sources/%s/' % objects[NamedDataSource.xml_root_node][4].id not in resp
271
    assert 'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][4].id not in resp
272
    # only 5 elements
273
    assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][4].id in resp
274
    assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][5].id in resp
275
    assert 'backoffice/workflows/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id in resp
276
    assert 'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][5].id in resp
277
    assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][5].id in resp
278

  
279
    pub.cfg['admin-permissions'] = {}
280
    pub.cfg['admin-permissions'].update({'settings': ['x'], 'forms': ['x'], 'workflows': ['x']})
281
    pub.write_cfg()
282

  
283
    app = login(get_app(pub))
284
    resp = app.get('/backoffice/studio/')
285
    # no access to settings, forms or workflows
286
    for i in range(6):
287
        assert 'backoffice/forms/blocks/%s/' % objects[BlockDef.xml_root_node][i].id not in resp
288
        assert (
289
            'backoffice/settings/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
290
        )
291
        assert 'backoffice/forms/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
292
        assert 'backoffice/forms/%s/' % objects[FormDef.xml_root_node][i].id not in resp
293
        assert 'backoffice/settings/wscalls/%s/' % objects[NamedWsCall.xml_root_node][i].id not in resp
294
        assert (
295
            'backoffice/workflows/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
296
        )
297
        assert (
298
            'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][i].id not in resp
299
        )
300
        assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][i].id not in resp
301
    # too old
302
    assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][0].id not in resp
303
    # only 5 elements
304
    for i in range(1, 6):
305
        assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][i].id in resp
306

  
307
    objects[CardDef.xml_root_node][5].remove_self()
308
    app = login(get_app(pub))
309
    resp = app.get('/backoffice/studio/')
310
    # too old
311
    assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][0].id not in resp
312
    # only 4 elements, one was deleted
313
    for i in range(1, 5):
314
        assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][i].id in resp
315
        # deleted
316
    assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][5].id not in resp
55 317

  
56 318

  
57 319
def test_studio_workflows(pub):
wcs/backoffice/studio.py
18 18
from quixote.directory import Directory
19 19

  
20 20
from wcs.admin.logged_errors import LoggedErrorsDirectory
21
from wcs.qommon import _, template
21
from wcs.blocks import BlockDef
22
from wcs.carddef import CardDef
23
from wcs.data_sources import NamedDataSource
24
from wcs.formdef import FormDef
25
from wcs.mail_templates import MailTemplate
26
from wcs.qommon import _, pgettext, template
22 27
from wcs.qommon.backoffice.menu import html_top
23 28
from wcs.qommon.form import get_response
29
from wcs.workflows import Workflow
30
from wcs.wscalls import NamedWsCall
24 31

  
25 32

  
26 33
class StudioDirectory(Directory):
......
38 45

  
39 46
    def _q_index(self):
40 47
        self.html_top(_('Studio'))
48
        extra_links = []
49
        backoffice_root = get_publisher().get_backoffice_root()
50
        object_types = []
51
        if backoffice_root.is_accessible('forms'):
52
            extra_links.append(('../forms/data-sources/', pgettext('studio', 'Data sources')))
53
            extra_links.append(('../forms/blocks/', pgettext('studio', 'Field blocks')))
54
            object_types += [NamedDataSource, BlockDef, FormDef]
55
        elif backoffice_root.is_accessible('workflows'):
56
            extra_links.append(('../workflows/data-sources/', pgettext('studio', 'Data sources')))
57
            object_types += [NamedDataSource]
58
        elif backoffice_root.is_accessible('settings'):
59
            extra_links.append(('../settings/data-sources/', pgettext('studio', 'Data sources')))
60
            object_types += [NamedDataSource]
61
        if backoffice_root.is_accessible('workflows'):
62
            extra_links.append(('../workflows/mail-templates/', pgettext('studio', 'Mail templates')))
63
            object_types += [Workflow, MailTemplate]
64
        if backoffice_root.is_accessible('settings'):
65
            extra_links.append(('../settings/wscalls/', pgettext('studio', 'Webservice calls')))
66
            object_types += [NamedWsCall]
67
        if backoffice_root.is_accessible('cards'):
68
            object_types += [CardDef]
69
        context = {
70
            'has_sidebar': False,
71
            'extra_links': extra_links,
72
            'recent_errors': LoggedErrorsDirectory.get_errors(offset=0, limit=5)[0],
73
        }
74
        if get_publisher().snapshot_class:
75
            context['recent_objects'] = get_publisher().snapshot_class.get_recent_changes(
76
                object_types=[ot.xml_root_node for ot in object_types]
77
            )
41 78
        return template.QommonTemplateResponse(
42
            templates=['wcs/backoffice/studio.html'], context={'has_sidebar': True}, is_django_native=True
79
            templates=['wcs/backoffice/studio.html'], context=context, is_django_native=True
43 80
        )
44 81

  
45 82
    def is_accessible(self, user):
wcs/qommon/static/css/dc2/admin.scss
1922 1922
div#studio {
1923 1923
	background: url(studio.svg) bottom right no-repeat;
1924 1924
	background-size: auto 90%;
1925
	width: 100%;
1925
	width: 99%;
1926 1926
	height: 90%;
1927 1927
}
1928 1928

  
......
1931 1931
	box-sizing: border-box;
1932 1932
	display: block;
1933 1933
	max-width: 100%;
1934
	width: 40rem;
1935 1934
	margin-bottom: 1rem;
1936 1935
}
1937 1936

  
......
1950 1949
	color: white;
1951 1950
}
1952 1951

  
1952
div.paragraph {
1953
	background: white;
1954
	box-sizing: border-box;
1955
	border: 1px solid #386ede;
1956
	border-radius: 3px;
1957
	max-width: 100%;
1958
	padding: 5px 15px;
1959
	margin-bottom: 1rem;
1960
	a.logged-errors-all {
1961
		font-style: italic,
1962
	}
1963
}
1964

  
1953 1965
#main-content form#multi-actions {
1954 1966
	padding: 0;
1955 1967
}
wcs/snapshots.py
182 182
            # else: keep serialization and ignore patch
183 183
            obj.store()
184 184

  
185
    @classmethod
186
    def get_recent_changes(cls, object_types):
187
        elements = cls._get_recent_changes(object_types)
188
        instances = []
189
        for object_type, object_id in elements:
190
            klass = cls.get_class(object_type)
191
            instance = klass.get(object_id, ignore_errors=True)
192
            if instance:
193
                instances.append(instance)
194
        return instances
195

  
185 196
    def get_object_class(self):
197
        return Snapshot.get_class(self.object_type)
198

  
199
    @classmethod
200
    def get_class(cls, object_type):
186 201
        from wcs.blocks import BlockDef
187 202
        from wcs.carddef import CardDef
188 203
        from wcs.categories import BlockCategory, CardDefCategory, Category, WorkflowCategory
......
205 220
            WorkflowCategory,
206 221
            BlockCategory,
207 222
        ):
208
            if klass.xml_root_node == self.object_type:
223
            if klass.xml_root_node == object_type:
209 224
                return klass
210
        raise KeyError('no class for object type: %s' % self.object_type)
225
        raise KeyError('no class for object type: %s' % object_type)
211 226

  
212 227
    def get_serialization(self, indented=True):
213 228
        # there is a complete serialization
wcs/sql.py
3264 3264
            return None
3265 3265
        return cls.get(row[0])
3266 3266

  
3267
    @classmethod
3268
    def _get_recent_changes(cls, object_types):
3269
        conn, cur = get_connection_and_cursor()
3270
        sql_statement = '''SELECT object_type, object_id, MAX(timestamp) AS m
3271
                           FROM snapshots
3272
                           WHERE object_type IN %(object_types)s
3273
                           GROUP BY object_type, object_id
3274
                           ORDER BY m DESC
3275
                           LIMIT 5'''
3276
        parameters = {'object_types': tuple(object_types)}
3277
        cur.execute(sql_statement, parameters)
3278
        result = [(x, y) for x, y, z in cur.fetchall()]
3279
        conn.commit()
3280
        cur.close()
3281
        return result
3282

  
3267 3283

  
3268 3284
class LoggedError(SqlMixin, wcs.logged_errors.LoggedError):
3269 3285
    _table_name = 'loggederrors'
wcs/templates/wcs/backoffice/studio.html
10 10
{% endblock %}
11 11

  
12 12
<div id="studio">
13
{% if user.can_go_in_backoffice_forms %}
14
<a class="button button-paragraph" href="../forms/">{% trans "Forms" context "studio" %}
15
  <p>{% trans "Forms are typically used to collect user demands." %}</p>
16
</a>
17
{% endif %}
18
{% if user.can_go_in_backoffice_cards %}
19
<a class="button button-paragraph" href="../cards/">{% trans "Cards" context "studio" %}
20
  <p>{% trans "Cards are used to store list of structured data." %}</p>
21
</a>
22
{% endif %}
23
{% if user.can_go_in_backoffice_workflows %}
24
<a class="button button-paragraph" href="../workflows/">{% trans "Workflows" context "studio" %}
25
  <p>{% trans "Workflows are used to add custom behaviours or actions to forms and cards." %}</p>
26
</a>
27
{% endif %}
28
</div>
29
{% endblock %}
13
  <div class="fx-grid--t3">
14
    <div class="fx-grid--auto">
15
      {% if user.can_go_in_backoffice_forms %}
16
      <a class="button button-paragraph size--1-1" href="../forms/">{% trans "Forms" context "studio" %}
17
        <p>{% trans "Forms are typically used to collect user demands." %}</p>
18
      </a>
19
      {% endif %}
20
      {% if user.can_go_in_backoffice_cards %}
21
      <a class="button button-paragraph size--1-1" href="../cards/">{% trans "Cards" context "studio" %}
22
        <p>{% trans "Cards are used to store list of structured data." %}</p>
23
      </a>
24
      {% endif %}
25
      {% if user.can_go_in_backoffice_workflows %}
26
      <a class="button button-paragraph size--1-1" href="../workflows/">{% trans "Workflows" context "studio" %}
27
        <p>{% trans "Workflows are used to add custom behaviours or actions to forms and cards." %}</p>
28
      </a>
29
      {% endif %}
30
      {% for link, label in extra_links %}
31
      <a class="button button-paragraph" href="{{ link }}">{{ label }}</a>
32
      {% endfor %}
33
    </div>
30 34

  
31
{% block sidebar-content %}
32
<ul id="sidebar-actions">
33
  <li><a href="logged-errors/">{% trans "Logged Errors" %}</a></li>
34
</ul>
35
    <div class="paragraph">
36
      <h3>{% trans "Recent changes" context "studio" %}</h3>
37
      <ul>
38
        {% for obj in recent_objects %}
39
        <li><a href="{{ obj.get_admin_url }}">{{ obj.name }} ({{ obj.verbose_name }})</a></li>
40
        {% endfor %}
41
      </ul>
42
    </div>
43

  
44
    <div class="paragraph">
45
      <h3>{% trans "Recent errors" context "studio" %}</h3>
46
      <ul>
47
        {% for error in recent_errors %}
48
        <li><a href="logged-errors/{{ error.id }}/">{{ error.summary }}</a></li>
49
        {% endfor %}
50
        <li><a class="logged-errors-all" href="logged-errors/">({% trans "see all errors" context "studio" %})</a>
51
      </ul>
52
    </div>
53
  </div>
54
</div>
35 55
{% endblock %}
36
-