Projet

Général

Profil

0001-general-remove-legacy-theming-code-70128.patch

Frédéric Péters, 12 octobre 2022 09:28

Télécharger (63,5 ko)

Voir les différences:

Subject: [PATCH] general: remove legacy theming code (#70128)

 MANIFEST.in                                |   2 -
 data/themes/alto/desc.xml                  |   6 -
 data/themes/alto/icon.png                  | Bin 728 -> 0 bytes
 data/themes/alto/img/bottom.jpg            | Bin 10161 -> 0 bytes
 data/themes/alto/img/page.jpg              | Bin 508 -> 0 bytes
 data/themes/alto/img/top.jpg               | Bin 10248 -> 0 bytes
 data/themes/alto/template.ezt              |  18 --
 data/themes/alto/wcs.css                   | 270 ---------------------
 data/themes/default/desc.xml               |   6 -
 data/themes/default/icon.png               | Bin 675 -> 0 bytes
 data/themes/default/mobile.css             |   0
 data/themes/django/templates/wcs/base.html |   2 +-
 tests/admin_pages/test_settings.py         | 122 +---------
 tests/form_pages/test_all.py               |  22 --
 tests/test_hobo.py                         |   6 +-
 tests/utilities.py                         |  22 +-
 wcs/admin/settings.py                      | 269 +-------------------
 wcs/compat.py                              |  20 +-
 wcs/qommon/publisher.py                    |   7 +-
 wcs/qommon/template.py                     | 212 +---------------
 wcs/root.py                                |   1 -
 wcs/utils.py                               |   2 +-
 22 files changed, 16 insertions(+), 971 deletions(-)
 delete mode 100644 data/themes/alto/desc.xml
 delete mode 100644 data/themes/alto/icon.png
 delete mode 100644 data/themes/alto/img/bottom.jpg
 delete mode 100644 data/themes/alto/img/page.jpg
 delete mode 100644 data/themes/alto/img/top.jpg
 delete mode 100644 data/themes/alto/template.ezt
 delete mode 100644 data/themes/alto/wcs.css
 delete mode 100644 data/themes/default/desc.xml
 delete mode 100644 data/themes/default/icon.png
 delete mode 100644 data/themes/default/mobile.css
MANIFEST.in
4 4
recursive-include wcs/locale *.po *.mo
5 5
recursive-include extra/ *.py
6 6
recursive-include data/web/ *.html *.css *.png
7
recursive-include data/themes/default/ *.html *.css *.png *.gif *.jpg *.js *.ezt *.xml
8
recursive-include data/themes/alto/ *.html *.css *.png *.gif *.jpg *.js *.ezt *.xml
9 7
recursive-include data/vendor/ *.dat
10 8
recursive-include wcs/qommon/static/ *.css *.scss *.png *.gif *.jpg *.js *.eot *.svg *.ttf *.woff *.map
11 9
recursive-include wcs/templates *.html *.txt
data/themes/alto/desc.xml
1
<?xml version="1.0"?>
2
<theme name="alto" version="1.0">
3
  <label>Alto</label>
4
  <desc>Alto theme</desc>
5
  <author>Frederic Peters (original Dotclear theme (alto studio) by David Jubert)</author>
6
</theme>
data/themes/alto/template.ezt
1
<!DOCTYPE html>
2
<html lang="[site_lang]">
3
  <head>
4
    <title>[page_title]</title>
5
    <link rel="stylesheet" type="text/css" href="[css]"/>
6
    [script]
7
  </head>
8
  <body[if-any onload] onload="[onload]"[end]>
9
    <div id="page">
10
    <div id="top"> <h1>[if-any title][title][else][site_name][end]</h1> </div>
11
    <div id="main-content">
12
    [if-any breadcrumb]<p id="breadcrumb">Vous &ecirc;tes ici : [breadcrumb]</p>[end]
13
    [body]
14
    </div>
15
    <div id="footer"></div>
16
    </div>
17
  </body>
18
</html>
data/themes/alto/wcs.css
1
/* adapted from alto dotclear theme */
2

  
3
@import url(/static/xstatic/themes/smoothness/jquery-ui.min.css);
4
@import url(/static/css/qommon.css);
5

  
6
html, body {
7
	background: #CCCCCC;
8
	font-family: sans-serif;
9
	color: #333333;
10
	margin: 0;
11
	padding: 0;
12
	text-align: center;
13
	height: 100%;
14
	margin-bottom: 1px;
15
}
16

  
17
fieldset {
18
	border: none;
19
}
20

  
21
label {
22
	cursor: pointer;
23
	cursor: hand;
24
}
25

  
26
img {
27
	border: 0;
28
}
29

  
30
input,textarea {
31
	border: 1px solid #999;
32
}
33

  
34
textarea {
35
	width: 99%;
36
}
37

  
38
a {
39
	color: #000;
40
	text-decoration : none;
41
}
42

  
43
a:hover {
44
	color: #0273B9;
45
	text-decoration : underline;
46
}
47

  
48
a:visited {
49
	color: #0273B9;
50
	text-decoration : none;
51
}
52

  
53
#page {
54
	background: #fff url(img/page.jpg) repeat-y center top;
55
	color: inherit;
56
	width: 886px;
57
	margin: 0 auto;
58
	text-align: left;
59
	padding: 0px;
60
}
61
 
62
#top {
63
	margin: 0;
64
	padding: 0;
65
	background: #CCCCCC url(img/top.jpg) no-repeat left top;
66
	margin-bottom: 2em;
67
}
68

  
69
#top h1 {
70
	width: 706px;
71
	margin: 0 auto;
72
	padding-top: 70px;
73
}
74

  
75
#side {
76
	float: right;
77
	width: 204px;
78
	padding: 0;
79
	margin: 0 -20px 0 20px;
80
}
81

  
82
#side #tracking-code {
83
	margin-bottom: 1em;
84
	border: 1px solid #bfbfbf;
85
	color: #333333;
86
	background: #e6e6e6;
87
	padding: 1ex;
88
}
89

  
90
#side #tracking-code h3 {
91
	margin: 0;
92
}
93

  
94
#side #tracking-code button,
95
#side #tracking-code a {
96
	margin: 1ex auto;
97
	display: block;
98
	text-align: center;
99
	font-size: 120%;
100
	background: white;
101
	border: 1px solid black;
102
	padding: 0.5ex 0;
103
	width: 10em;
104
}
105

  
106
#side #tracking-code button {
107
	background: #0273B9;
108
	color: white;
109
}
110

  
111
input[name=savedraft] {
112
	display: none;
113
}
114

  
115
#steps {
116
	background: white;
117
	border: 1px solid #bfbfbf;
118
	color: #333333;
119
	background: #e6e6e6;
120
	-moz-border-radius: 6px;
121
	text-align: left;
122
}
123

  
124

  
125
#footer {
126
	width: 886px;
127
	height: 123px;
128
	background: #CCCCCC url(img/bottom.jpg) no-repeat left top;
129
	margin: 0;
130
	margin-top: 1em;
131
	color: #666;
132
	clear: both;
133
}
134

  
135
#footer p {
136
	width: 706px;
137
	margin: 0 auto;
138
	padding-top: 24px;
139
	text-align: right;
140
	font-size: 80%;
141
}
142

  
143

  
144
#main-content {
145
	width: 735px;
146
	padding-left: 65px;
147
	text-align: justify;
148
}
149

  
150

  
151
div#steps ol {
152
	list-style: none;
153
	margin: 0;
154
	padding: 0.5em;
155
}
156

  
157
div#steps li {
158
	display: block;
159
	border: 1px solid #ddd;
160
	margin: 0.5em 0;
161
	background: #eee;
162
	color: #aaa;
163
}
164

  
165
#steps span.marker {
166
	padding: 0 1ex 0 1ex;
167
	font-weight: bold;
168
	color: white;
169
	text-align: center;
170
	background: #ddd;
171
}
172

  
173
#steps li.current span.marker {
174
	background: #0273b9;
175
}
176

  
177

  
178
#steps li.current {
179
	font-weight: bold;
180
	border: 1px solid #333333;
181
}
182

  
183
#steps li.current span.label {
184
	color: #333333;
185
}
186

  
187
#steps ol ul {
188
	margin-right: 1em;
189
	font-size: 90%;
190
}
191

  
192
#steps ol ul li {
193
	padding: 0 2px;
194
	font-weight: normal;
195
	margin-left: -1ex;
196
}
197

  
198
#steps ol ul li.current {
199
	border-color: inherit;
200
	color: #333333;
201
}
202

  
203

  
204
div.widget {
205
	clear: none;
206
	margin-bottom: 1.5em;
207
}
208

  
209
hr {
210
	visibility: hidden;
211
}
212

  
213
textarea {
214
}
215

  
216
p#breadcrumb {
217
	background: #e6e6e6;
218
	-moz-border-radius: 6px;
219
	width: 750px;
220
	padding: 3px;
221
	font-size: 90%;
222
	border: 1px solid #bfbfbf;
223
}
224

  
225
div#receipt {
226
}
227

  
228
div#receipt span.label {
229
	font-weight: bold;
230
	display: block;
231
}
232

  
233
div#receipt span.value {
234
	display: block;
235
	margin-left: 1em;
236
}
237

  
238
form div.page,
239
div#receipt div.page {
240
	border: 1px solid #bfbfbf;
241
	padding: 1ex;
242
	margin-bottom: 1em;
243
}
244

  
245
form div.page p,
246
div#receipt div.page p {
247
	margin-top: 0;
248
}
249

  
250
form div.page h3,
251
div#receipt div.page h3 {
252
	margin: 0;
253
	margin-bottom: 1ex;
254
}
255

  
256

  
257
p#receiver {
258
	margin: 0;
259
	margin-left: 2em;
260
	margin-top: -0.7em;
261
	margin-bottom: 1em;
262
	padding: 2px 5px;
263
	font-weight: bold;
264
}
265

  
266
table#listing {
267
	background: white;
268
	border: 1px solid #888;
269
}
270

  
data/themes/default/desc.xml
1
<?xml version="1.0"?>
2
<theme name="default" version="1.0">
3
  <label>Default</label>
4
  <desc>Default theme</desc>
5
  <author>Frederic Peters &amp; Dotclear Team</author>
6
</theme>
data/themes/django/templates/wcs/base.html
12 12
      <div id="page">
13 13
        <div id="top">
14 14
          {% block header %}
15
            <h1>WIP/DJANGO - {% if title %}{{ title }}{% else %}{{ site_name }}{% endif %}</h1>
15
            <h1>{% if title %}{{ title }}{% else %}{{ site_name }}{% endif %}</h1>
16 16
          {% endblock %}
17 17
        </div>
18 18
        <div id="main-content">
tests/admin_pages/test_settings.py
28 28
from wcs.formdef import FormDef
29 29
from wcs.qommon.form import UploadedFile
30 30
from wcs.qommon.http_request import HTTPRequest
31
from wcs.qommon.template import get_current_theme
32 31
from wcs.wf.export_to_model import ExportToModel
33 32
from wcs.workflows import Workflow
34 33
from wcs.wscalls import NamedWsCall
......
74 73
    app = login(get_app(pub))
75 74
    resp = app.get('/backoffice/settings/')
76 75
    assert 'Identification' in resp.text
77
    assert 'Theme' in resp.text
78 76

  
79 77
    if not pub.site_options.has_section('options'):
80 78
        pub.site_options.add_section('options')
81
    pub.site_options.set('options', 'settings-disabled-screens', 'identification, theme')
79
    pub.site_options.set('options', 'settings-disabled-screens', 'identification')
82 80
    with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
83 81
        pub.site_options.write(fd)
84 82
    resp = app.get('/backoffice/settings/')
85 83
    assert 'Identification' not in resp.text
86
    assert 'Theme' not in resp.text
87 84

  
88 85

  
89 86
def test_settings_export_import(pub):
......
318 315
    assert 'Unknown referenced objects [Unknown fields blocks: unknown]' in resp
319 316

  
320 317

  
321
def test_settings_themes(pub):
322
    create_superuser(pub)
323
    app = login(get_app(pub))
324

  
325
    # create mock theme
326
    os.mkdir(os.path.join(pub.app_dir, 'themes'))
327
    os.mkdir(os.path.join(pub.app_dir, 'themes', 'test'))
328
    with open(os.path.join(pub.app_dir, 'themes', 'test', 'desc.xml'), 'w') as fd:
329
        fd.write(
330
            '<?xml version="1.0"?>'
331
            '<theme name="test" version="1.0">'
332
            '  <label>Test Theme</label>'
333
            '</theme>'
334
        )
335

  
336
    resp = app.get('/backoffice/settings/themes')
337
    assert 'biglist themes' in resp.text
338
    assert 'Test Theme (1.0)' in resp.text
339

  
340
    # just for the kick, there's no support for uploading file in webtest 1.3
341
    resp = app.get('/backoffice/settings/themes')
342
    resp.click('Install New Theme')
343

  
344
    # select the theme
345
    resp = app.get('/backoffice/settings/themes')
346
    resp.forms[0]['theme'].value = 'test'
347
    resp = resp.forms[0].submit()
348
    assert resp.location == 'http://example.net/backoffice/settings/'
349

  
350
    resp = app.get('/backoffice/settings/themes')
351
    assert 'checked' in resp.text
352
    assert get_current_theme()['name'] == 'test'
353

  
354

  
355
def test_settings_template(pub):
356
    create_superuser(pub)
357
    app = login(get_app(pub))
358
    resp = app.get('/backoffice/settings/template')
359

  
360
    # change template
361
    orig_value = resp.forms[0]['template'].value
362
    assert 'foobar' not in orig_value
363
    resp.forms[0]['template'] = orig_value + '<!-- foobar -->'
364
    resp = resp.forms[0].submit('submit')
365

  
366
    # restore default template
367
    resp = app.get('/backoffice/settings/template')
368
    assert 'foobar' in resp.forms[0]['template'].value
369
    resp = resp.forms[0].submit('restore-default')
370

  
371
    # check
372
    resp = app.get('/backoffice/settings/template')
373
    assert resp.forms[0]['template'].value == orig_value
374

  
375

  
376 318
def test_settings_user(pub):
377 319
    user = create_superuser(pub)
378 320
    app = login(get_app(pub))
......
829 771
    assert pub.cfg['admin-permissions']['workflows'] == []
830 772

  
831 773

  
832
def test_settings_theme_preview(pub):
833
    create_superuser(pub)
834

  
835
    FormDef.wipe()
836
    formdef = FormDef()
837
    formdef.name = 'form title'
838
    formdef.fields = []
839
    formdef.store()
840

  
841
    app = login(get_app(pub))
842
    assert 'alto/wcs.css' not in app.get('/').text
843
    resp = app.get('/backoffice/settings/themes')
844
    assert resp.form['theme'].value in ('default', 'django')
845

  
846
    # visit theme preview
847
    resp = resp.click(href='theme_preview/alto/')
848
    assert 'alto/wcs.css' in resp.text
849

  
850
    # get into a form, making sure we are kept in theme preview
851
    resp = resp.click('form title')
852
    assert 'alto/wcs.css' in resp.text
853

  
854
    # verify submits are not allowed
855
    resp = resp.form.submit('submit')
856
    assert "The theme preview doesn&#39;t support this." in resp.text
857

  
858

  
859
def test_settings_theme_download_upload(pub):
860
    create_superuser(pub)
861

  
862
    # download existing theme
863
    app = login(get_app(pub))
864
    resp = app.get('/backoffice/settings/themes')
865
    resp = resp.click('download', index=0)
866
    assert resp.headers['content-type'] == 'application/zip'
867

  
868
    zip_content = io.BytesIO(resp.body)
869
    with zipfile.ZipFile(zip_content, 'a') as zipf:
870
        filelist = zipf.namelist()
871
        assert 'alto/icon.png' in filelist
872
        assert 'alto/desc.xml' in filelist
873
        assert 'alto/template.ezt' in filelist
874
        assert 'alto/wcs.css' in filelist
875

  
876
        # modify it
877
        zipf.writestr('alto/foobar.txt', 'XXX')
878

  
879
    # upload it
880
    resp = app.get('/backoffice/settings/themes')
881
    resp = resp.click('Install New Theme')
882
    resp.form['file'] = Upload('alto-modified.zip', zip_content.getvalue())
883
    resp = resp.form.submit()
884
    assert os.path.exists(os.path.join(pub.app_dir, 'themes/alto/foobar.txt'))
885

  
886
    assert app.get('/themes/alto/foobar.txt').text == 'XXX'
887
    assert 'Directory listing denied' in app.get('/themes/alto/', status=200).text
888

  
889
    assert app.get('/themes/alto/plop', status=404)
890
    assert app.get('/themes/alto/../', status=404)
891
    assert app.get('/themes/xxx/../', status=404)
892

  
893

  
894 774
def test_postgresql_settings(pub):
895 775
    create_superuser(pub)
896 776

  
tests/form_pages/test_all.py
343 343
    assert resp.location == 'http://www.example.com/en/foobar/'
344 344

  
345 345

  
346
def test_legacy_theme_misc():
347
    pub = create_temporary_pub(legacy_theme_mode=True)
348
    pub.cfg['language'] = {'language': 'en'}
349
    pub.write_cfg()
350

  
351
    formdef = create_formdef()
352
    formdef.fields = [fields.StringField(id='1', label='string')]
353
    formdef.store()
354

  
355
    resp = get_app(pub).get('/')
356
    assert '<title>' in resp.text
357
    assert '/static/js/qommon.forms.js' not in resp.text
358
    assert '<a class="" href="test/">test</a>' in resp.text
359
    resp = resp.click('test')
360
    assert '/static/js/qommon.forms.js' in resp.text
361
    resp.form['f1'] = 'TEST'
362
    resp = resp.form.submit('submit')
363
    assert 'Check values then click submit.' in resp.text
364
    resp = resp.form.submit('submit').follow()
365
    assert 'The form has been recorded on' in resp.text
366

  
367

  
368 346
def test_form_access(pub):
369 347
    formdef = create_formdef()
370 348
    get_app(pub).get('/test/', status=200)
tests/test_hobo.py
314 314

  
315 315
def test_update_themes(setuptest):
316 316
    pub, hobo_cmd = setuptest
317
    pub.cfg['branding'] = {'theme': 'default'}
317
    pub.cfg['branding'] = {'theme': 'django'}
318 318
    service = [x for x in HOBO_JSON.get('services', []) if x.get('service-id') == 'wcs'][0]
319 319
    hobo_cmd.update_configuration(service, pub)
320
    assert pub.cfg['branding']['theme'] == 'default'
320
    assert pub.cfg['branding']['theme'] == 'django'
321 321

  
322 322
    service['variables']['theme'] = 'foobar'
323 323
    hobo_cmd.update_configuration(service, pub)
324
    assert pub.cfg['branding']['theme'] == 'default'
324
    assert pub.cfg['branding']['theme'] == 'django'
325 325

  
326 326
    hobo_cmd.THEMES_DIRECTORY = os.path.join(os.path.dirname(__file__), 'themes')
327 327
    hobo_cmd.update_configuration(service, pub)
tests/utilities.py
33 33
    pickle_app_dir = None
34 34
    sql_app_dir = None
35 35
    sql_db_name = None
36
    legacy_theme_app_dir = None
37 36
    lazy_app_dir = None
38 37

  
39 38

  
40 39
known_elements = KnownElements()
41 40

  
42 41

  
43
def create_temporary_pub(pickle_mode=False, legacy_theme_mode=False, lazy_mode=False):
42
def create_temporary_pub(pickle_mode=False, lazy_mode=False):
44 43
    if get_publisher():
45 44
        get_publisher().cleanup()
46 45
        cleanup()
47
    if legacy_theme_mode and known_elements.legacy_theme_app_dir:
48
        APP_DIR = known_elements.legacy_theme_app_dir
49
    elif lazy_mode and known_elements.lazy_app_dir:
46
    if lazy_mode and known_elements.lazy_app_dir:
50 47
        APP_DIR = known_elements.lazy_app_dir
51 48
    elif pickle_mode and known_elements.pickle_app_dir:
52 49
        APP_DIR = known_elements.pickle_app_dir
53
    elif not (legacy_theme_mode or lazy_mode or pickle_mode) and known_elements.sql_app_dir:
50
    elif not (lazy_mode or pickle_mode) and known_elements.sql_app_dir:
54 51
        APP_DIR = known_elements.sql_app_dir
55 52
    else:
56 53
        APP_DIR = tempfile.mkdtemp()
57
        if legacy_theme_mode:
58
            known_elements.legacy_theme_app_dir = APP_DIR
59
        elif lazy_mode:
54
        if lazy_mode:
60 55
            known_elements.lazy_app_dir = APP_DIR
61 56
        elif pickle_mode:
62 57
            known_elements.pickle_app_dir = APP_DIR
......
124 119
        'frontoffice-url': 'http://example.net',
125 120
    }
126 121
    pub.cfg['language'] = {'language': 'en'}
127

  
128
    if legacy_theme_mode:
129
        pub.cfg['branding'] = {'theme': 'default'}
130
    else:
131
        pub.cfg['branding'] = {'theme': 'django'}
132

  
133 122
    pub.write_cfg()
134 123

  
135 124
    if not created:
......
183 172
def clean_temporary_pub():
184 173
    if get_publisher():
185 174
        get_publisher().cleanup()
186
    if known_elements.legacy_theme_app_dir and os.path.exists(known_elements.legacy_theme_app_dir):
187
        shutil.rmtree(known_elements.legacy_theme_app_dir)
188
        known_elements.legacy_theme_app_dir = None
189 175
    if known_elements.pickle_app_dir and os.path.exists(known_elements.pickle_app_dir):
190 176
        shutil.rmtree(known_elements.pickle_app_dir)
191 177
        known_elements.pickle_app_dir = None
wcs/admin/settings.py
24 24
    import lasso
25 25
except ImportError:
26 26
    lasso = None
27
import shutil
28 27
import xml.etree.ElementTree as ET
29 28
import zipfile
30 29

  
31
from django.utils.encoding import force_bytes, force_text
30
from django.utils.encoding import force_bytes
32 31
from quixote import get_publisher, get_request, get_response, get_session, redirect
33 32
from quixote.directory import Directory
34 33
from quixote.html import TemplateIO, htmltext
......
41 40
from wcs.qommon import _, errors, get_cfg, ident, misc, template
42 41
from wcs.qommon.admin.cfg import cfg_submit
43 42
from wcs.qommon.admin.emails import EmailsDirectory
44
from wcs.qommon.admin.menu import error_page
45 43
from wcs.qommon.admin.settings import SettingsDirectory as QommonSettingsDirectory
46 44
from wcs.qommon.admin.texts import TextsDirectory
47 45
from wcs.qommon.afterjobs import AfterJob
......
59 57
    SingleSelectWidget,
60 58
    StringWidget,
61 59
    TextWidget,
62
    UrlWidget,
63 60
    WidgetList,
64 61
)
65 62
from wcs.workflows import Workflow, WorkflowImportError
......
449 446
        return r.getvalue()
450 447

  
451 448

  
452
class ThemePreviewDirectory(Directory):
453
    def _q_traverse(self, path):
454
        if len(path) < 2:
455
            return error_page('settings', _('Invalid URL'))
456

  
457
        theme_id = path[0]
458
        branding = get_publisher().cfg.get('branding', {})
459
        original_branding = branding.copy()
460
        get_publisher().cfg['branding'] = branding
461
        get_publisher().cfg['branding']['theme'] = theme_id
462
        if 'template' in get_publisher().cfg['branding']:
463
            del get_publisher().cfg['branding']['template']
464

  
465
        root_directory = get_publisher().root_directory_class()
466

  
467
        response = get_response()
468
        response.reset_includes()
469
        response.filter = {}
470
        del response.breadcrumb
471

  
472
        if path[1] in ('backoffice', 'admin') or get_request().get_method() == 'POST':
473
            from wcs.qommon.template import error_page as base_error_page
474

  
475
            output = base_error_page(_("The theme preview doesn't support this."))
476
        else:
477
            output = root_directory._q_traverse(path[1:])
478

  
479
        from wcs.qommon.template import decorate
480

  
481
        if isinstance(output, template.QommonTemplateResponse):
482
            output = template.render(output.templates, output.context)
483
        theme_preview = decorate(output, response)
484

  
485
        # restore original branding in case it has been changed
486
        get_publisher().cfg['branding'] = original_branding
487
        get_publisher().write_cfg()
488
        response.filter['raw'] = True
489

  
490
        return theme_preview
491

  
492

  
493 449
class SettingsDirectory(QommonSettingsDirectory):
494 450
    _q_exports = [
495 451
        '',
496
        'themes',
497 452
        'users',
498 453
        'template',
499 454
        'emails',
......
506 461
        'sms',
507 462
        'certificates',
508 463
        'texts',
509
        'install_theme',
510
        'download_theme',
511 464
        'postgresql',
512 465
        ('admin-permissions', 'admin_permissions'),
513 466
        'geolocation',
514
        'theme_preview',
515 467
        'filetypes',
516 468
        ('user-templates', 'user_templates'),
517 469
        ('data-sources', 'data_sources'),
......
523 475
    identification = IdentificationDirectory()
524 476
    users = UsersDirectory()
525 477
    texts = TextsDirectory()
526
    theme_preview = ThemePreviewDirectory()
527 478
    filetypes = FileTypesDirectory()
528 479
    data_sources = NamedDataSourcesDirectory()
529 480
    wscalls = NamedWsCallsDirectory()
......
636 587
                _('Language'),
637 588
                _('Configure site language'),
638 589
            )
639
        if enabled('theme'):
640
            r += htmltext('<dt><a href="themes">%s</a></dt> <dd>%s</dd>') % (_('Theme'), _('Configure theme'))
641
        if enabled('template'):
642
            r += htmltext('<dt><a href="template">%s</a></dt> <dd>%s</dd>') % (
643
                _('Template'),
644
                _('Configure template'),
645
            )
646 590
        if enabled('geolocation'):
647 591
            r += htmltext('<dt><a href="geolocation">%s</a></dt> <dd>%s</dd>') % (
648 592
                _('Geolocation'),
......
766 710
            get_publisher().write_cfg()
767 711
            return redirect('.')
768 712

  
769
    def themes(self):
770
        request = get_request()
771

  
772
        if 'theme' not in request.form:
773
            current_theme = get_cfg('branding', {}).get('theme', 'default')
774

  
775
            get_response().breadcrumb.append(('themes', _('Themes')))
776
            html_top('settings', title=_('Themes'))
777
            r = TemplateIO(html=True)
778
            r += htmltext("<h2>%s</h2>") % _('Themes')
779

  
780
            r += get_session().display_message()
781

  
782
            r += htmltext('<a rel="popup" href="install_theme">%s</a>') % _('Install New Theme')
783

  
784
            r += htmltext('<form action="themes" enctype="multipart/form-data" method="post">')
785
            themes = template.get_themes_dict()
786
            r += htmltext('<ul class="biglist themes">')
787
            for theme, theme_dict in sorted(themes.items()):
788
                label = theme_dict.get('label')
789
                if 'version' in theme_dict:
790
                    label = '%s (%s)' % (label, theme_dict.get('version'))
791
                if current_theme == theme:
792
                    checked = ' checked="checked"'
793
                else:
794
                    checked = ''
795
                r += htmltext('<li>')
796
                r += htmltext('<strong class="label"><label>')
797
                r += htmltext('  <input name="theme" value="%s" type="radio"%s>%s</input>') % (
798
                    theme,
799
                    checked,
800
                    label,
801
                )
802
                r += htmltext('</label></strong>')
803
                if theme_dict.get('icon'):
804
                    r += htmltext('<img src="/themes/%s/icon.png" alt="" class="theme-icon" />') % theme
805
                r += htmltext('<p class="details">%s') % theme_dict.get('desc', '')
806
                r += htmltext(' [<a href="download_theme?theme=%s">%s</a>]') % (theme, _('download'))
807
                r += htmltext(' [<a class="theme-preview" href="theme_preview/%s/">%s</a>]') % (
808
                    theme,
809
                    _('preview'),
810
                )
811
                if theme_dict.get('author'):
812
                    r += htmltext('<br/>')
813
                    r += htmltext(_('by %s')) % theme_dict.get('author')
814
                r += htmltext('</p>')
815
                r += htmltext('</li>')
816
            r += htmltext('</ul>')
817
            r += htmltext('<div class="buttons">')
818
            r += htmltext('<button>%s</button>') % _('Submit')
819
            r += htmltext('</div>')
820
            r += htmltext('</form>')
821
            return r.getvalue()
822
        else:
823
            themes = template.get_themes()
824
            if str(request.form['theme']) in themes:
825
                branding_cfg = get_cfg('branding', {})
826
                branding_cfg['theme'] = str(request.form['theme'])
827
                get_publisher().cfg['branding'] = branding_cfg
828
                get_publisher().write_cfg()
829
            return redirect('.')
830

  
831
    def download_theme(self):
832
        theme_id = get_request().form.get('theme')
833
        if not theme_id:
834
            return redirect('themes')
835

  
836
        theme_directory = template.get_theme_directory(theme_id)
837
        if not theme_directory:
838
            return redirect('themes')
839

  
840
        parent_theme_directory = os.path.dirname(theme_directory)
841
        c = io.BytesIO()
842
        with zipfile.ZipFile(c, 'w') as z:
843
            for base, dummy, filenames in os.walk(theme_directory):
844
                basetheme = base[len(parent_theme_directory) + 1 :]
845
                for filename in filenames:
846
                    z.write(os.path.join(base, filename), os.path.join(basetheme, filename))
847

  
848
        response = get_response()
849
        response.set_content_type('application/zip')
850
        response.set_header('content-disposition', 'attachment; filename=%s.zip' % theme_id)
851
        return c.getvalue()
852

  
853
    def install_theme(self):
854
        form = Form(enctype='multipart/form-data')
855
        form.add(FileWidget, 'file', title=_('Theme File'), required=False)
856
        form.add(UrlWidget, 'url', title=_('Theme Address'), required=False, size=50)
857
        form.add_submit('submit', _('Install'))
858
        form.add_submit('cancel', _('Cancel'))
859

  
860
        if form.get_submit() == 'cancel':
861
            return redirect('.')
862

  
863
        if form.is_submitted() and not form.has_errors():
864
            try:
865
                return self.install_theme_submit(form)
866
            except ValueError:
867
                form.get_widget('file').set_error(_('Invalid Theme'))
868

  
869
        get_response().breadcrumb.append(('install_theme', _('Install Theme')))
870
        html_top('forms', title=_('Install Theme'))
871
        r = TemplateIO(html=True)
872
        r += htmltext('<h2>%s</h2>') % _('Install Theme')
873
        r += htmltext('<p>%s</p>') % _(
874
            'You can install a new theme by uploading a file or by pointing to the theme URL.'
875
        )
876
        r += form.render()
877
        return r.getvalue()
878

  
879
    def install_theme_submit(self, form):
880
        if form.get_widget('url').parse():
881
            return self.install_theme_from_url(form.get_widget('url').parse())
882
        if form.get_widget('file').parse():
883
            return self.install_theme_from_file(form.get_widget('file').parse().fp)
884
        get_session().message = ('error', _('You have to enter a file or a URL.'))
885
        return redirect('themes')
886

  
887
    def install_theme_from_file(self, fp):
888
        try:
889
            with zipfile.ZipFile(fp, 'r') as z:
890
                theme_dir = os.path.join(get_publisher().app_dir, 'themes')
891
                filename_list = [x for x in z.namelist() if x[0] != '/' and x[-1] != '/']
892
                if len(filename_list) == 0:
893
                    get_session().message = ('error', _('Empty theme file.'))
894
                    return redirect('themes')
895
                theme_name = filename_list[0].split('/')[0]
896
                if ('%s/desc.xml' % theme_name) not in filename_list:
897
                    get_session().message = ('error', _('Theme is missing a desc.xml file.'))
898
                    return redirect('themes')
899
                desc_xml = z.read('%s/desc.xml' % theme_name)
900
                theme_dict = template.get_theme_dict(io.StringIO(force_text(desc_xml)))
901
                if theme_dict.get('name') != theme_name:
902
                    get_session().message = ('error', _('desc.xml is missing a name attribute.'))
903
                    return redirect('themes')
904
                if os.path.exists(os.path.join(theme_dir, theme_name)):
905
                    shutil.rmtree(os.path.join(theme_dir, theme_name))
906
                for f in z.namelist():
907
                    if f[-1] == '/':
908
                        continue
909
                    path = os.path.join(theme_dir, f)
910
                    data = z.read(f)
911
                    if not os.path.exists(os.path.dirname(path)):
912
                        os.makedirs(os.path.dirname(path))
913
                    with open(path, 'wb') as _f:
914
                        _f.write(data)
915
                return redirect('themes')
916
        except Exception as e:
917
            get_session().message = ('error', _('Failed to read theme file.  (%s)') % str(e))
918
            return redirect('themes')
919

  
920
    def install_theme_from_url(self, url):
921
        try:
922
            fp = misc.urlopen(url)
923
        except misc.ConnectionError as e:
924
            get_session().message = ('error', _('Error loading theme (%s).') % str(e))
925
            return redirect('themes')
926

  
927
        return self.install_theme_from_file(io.StringIO(fp.read()))
928

  
929
    def template(self):
930
        from wcs.qommon.template import get_default_ezt_template
931

  
932
        default_template_ezt = get_default_ezt_template()
933
        branding_cfg = get_cfg('branding', {})
934
        template = branding_cfg.get('template', default_template_ezt)
935
        form = Form(enctype="multipart/form-data")
936
        form.add(TextWidget, 'template', title=_('Site Template'), value=template, cols=80, rows=25)
937
        form.add_submit('submit', _('Submit'))
938
        form.add_submit('restore-default', _('Restore default template'))
939
        form.add_submit('cancel', _('Cancel'))
940
        if form.get_widget('cancel').parse():
941
            return redirect('.')
942

  
943
        if form.get_submit() == 'cancel':
944
            return redirect('.')
945

  
946
        if form.get_submit() == 'restore-default':
947
            self.template_submit()
948
            return redirect('.')
949

  
950
        if form.is_submitted() and not form.has_errors():
951
            self.template_submit(form)
952
            return redirect('.')
953

  
954
        get_response().breadcrumb.append(('template', _('Template')))
955
        html_top('settings', title=_('Template'))
956
        r = TemplateIO(html=True)
957
        r += htmltext('<h2>%s</h2>') % _('Template')
958
        r += form.render()
959
        return r.getvalue()
960

  
961
    def template_submit(self, form=None):
962
        from wcs.qommon.template import DEFAULT_TEMPLATE_EZT, get_default_ezt_template
963

  
964
        theme_default_template_ezt = get_default_ezt_template()
965

  
966
        get_publisher().reload_cfg()
967
        branding_cfg = get_cfg('branding', {})
968
        if not form:
969
            template = None
970
        else:
971
            template = form.get_widget('template').parse()
972
        if template in (DEFAULT_TEMPLATE_EZT, theme_default_template_ezt) or not template:
973
            if 'template' in branding_cfg:
974
                del branding_cfg['template']
975
        else:
976
            branding_cfg['template'] = template
977
        get_publisher().cfg['branding'] = branding_cfg
978
        get_publisher().write_cfg()
979

  
980 713
    def export(self):
981 714
        if get_request().form.get('download'):
982 715
            return self.export_download()
wcs/compat.py
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import os
18 17
from contextlib import contextmanager
19 18
from threading import Lock
20 19

  
......
31 30
from .publisher import WcsPublisher
32 31
from .qommon import force_str, template
33 32
from .qommon.http_request import HTTPRequest
34
from .qommon.publisher import get_cfg, set_publisher_class
33
from .qommon.publisher import set_publisher_class
35 34

  
36 35

  
37 36
class TemplateWithFallbackView(TemplateView):
......
65 64
        elif request.headers.get('X-Popup') == 'true':
66 65
            response = HttpResponse('<div class="popup-content">%s</div>' % context['body'])
67 66
        elif 'raw' in (getattr(self.quixote_response, 'filter') or {}):
68
            # used for theme preview (generated in /backoffice/ but cannot
69
            # obviously receive the admin template.
67
            # used for raw HTML snippets (for example in the test tool
68
            # results in inspect page).
70 69
            response = HttpResponse(context['body'])
71 70
        else:
72 71
            response = self.render_to_response(context)
......
145 144
            return output
146 145
        if request.headers.get('X-Popup') == 'true':
147 146
            return '<div class="popup-content">%s</div>' % output
148
        if response.filter and response.filter.get('admin_ezt'):
149
            return self.render_response(output)
150

  
151
        current_theme = get_cfg('branding', {}).get('theme', 'default')
152
        theme_directory = template.get_theme_directory(current_theme)
153
        if not theme_directory:
154
            return self.render_response(output)
155

  
156
        if not os.path.exists(os.path.join(theme_directory, 'templates')):
157
            return self.render_response(output)
158

  
159
        if not os.path.exists(os.path.join(theme_directory, 'templates/wcs/base.html')):
160
            return self.render_response(output)
161 147

  
162 148
        if isinstance(output, template.QommonTemplateResponse):
163 149
            template_response = output
wcs/qommon/publisher.py
116 116
    after_login_url = ''
117 117
    qommon_static_dir = 'static/'
118 118
    qommon_admin_css = 'css/dc2/admin.css'
119
    default_theme = 'default'
119
    default_theme = 'django'
120 120

  
121 121
    site_options = None
122 122
    site_charset = 'utf-8'
......
346 346

  
347 347
        return error_page
348 348

  
349
    def render_response(self, content):
350
        if isinstance(content, template.QommonTemplateResponse):
351
            content = template.render(content.templates, content.context)
352
        return template.decorate(content, self.get_request().response)
353

  
354 349
    def install_lang(self, request=None):
355 350
        if request:
356 351
            lang = request.language
wcs/qommon/template.py
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import glob
18 17
import io
19 18
import os
20 19
import re
21
import xml.etree.ElementTree as ET
22 20

  
23 21
import django.template
24 22
from django.template import TemplateSyntaxError as DjangoTemplateSyntaxError
......
27 25
from django.template.loader import render_to_string
28 26
from django.utils.encoding import force_text, smart_text
29 27
from quixote import get_publisher, get_request, get_response, get_session
30
from quixote.directory import Directory
31 28
from quixote.html import TemplateIO, htmlescape, htmltext
32
from quixote.util import StaticDirectory, StaticFile
33 29

  
34 30
from . import ezt, force_str
35 31

  
......
53 49
    return location
54 50

  
55 51

  
56
class ThemesDirectory(Directory):
57
    def _q_lookup(self, component):
58
        from . import errors
59

  
60
        if component in ('.', '..'):
61
            raise errors.TraversalError()
62

  
63
        location = get_theme_directory(component)
64
        if location is None:
65
            raise errors.TraversalError()
66

  
67
        if os.path.isdir(location):
68
            return StaticDirectory(location)
69
        else:
70
            return StaticFile(location)
71

  
72

  
73
def get_themes_dict():
74
    system_location = os.path.join(get_publisher().data_dir, 'themes')
75
    local_location = os.path.join(get_publisher().app_dir, 'themes')
76

  
77
    themes = {}
78
    for theme_xml in glob.glob(os.path.join(system_location, '*/desc.xml')) + glob.glob(
79
        os.path.join(local_location, '*/desc.xml')
80
    ):
81
        theme_dict = get_theme_dict(theme_xml)
82
        if not theme_dict:
83
            continue
84
        themes[theme_dict.get('name')] = theme_dict
85
    return themes
86

  
87

  
88
def get_theme_dict(theme_xml):
89
    try:
90
        tree = ET.parse(theme_xml).getroot()
91
    except Exception:  # parse error
92
        return None
93
    name = force_str(tree.attrib['name'])
94
    version = force_str(tree.attrib.get('version') or '')
95
    label = force_str(tree.findtext('label') or '')
96
    desc = force_str(tree.findtext('desc') or '')
97
    author = force_str(tree.findtext('author') or '')
98
    icon = None
99
    if isinstance(theme_xml, str):
100
        icon = os.path.join(os.path.dirname(theme_xml), 'icon.png')
101
        if not os.path.exists(icon):
102
            icon = None
103
    theme = {'name': name, 'label': label, 'desc': desc, 'author': author, 'icon': icon, 'version': version}
104
    theme['keywords'] = []
105
    for keyword in tree.findall('keywords/keyword'):
106
        theme['keywords'].append(keyword.text)
107
    return theme
108

  
109

  
110
def get_themes():
111
    # backward compatibility function, it returns a tuple with theme info,
112
    # newer code should use get_themes_dict()
113
    themes = {}
114
    for k, v in get_themes_dict().items():
115
        themes[k] = (v['label'], v['desc'], v['author'], v['icon'])
116
    return themes
117

  
118

  
119
def get_current_theme():
120
    from .publisher import get_cfg
121

  
122
    current_theme = get_cfg('branding', {}).get('theme', 'default')
123
    system_location = os.path.join(get_publisher().data_dir, 'themes', current_theme)
124
    local_location = os.path.join(get_publisher().app_dir, 'themes', current_theme)
125
    for location in (local_location, system_location):
126
        if os.path.exists(location):
127
            return get_theme_dict(os.path.join(location, 'desc.xml'))
128
    default_theme_location = os.path.join(get_publisher().data_dir, 'themes', 'default')
129
    return get_theme_dict(os.path.join(default_theme_location, 'desc.xml'))
130

  
131

  
132
DEFAULT_TEMPLATE_EZT = """<!DOCTYPE html>
133
<html lang="[site_lang]">
134
  <head>
135
    <title>[page_title]</title>
136
    <link rel="stylesheet" type="text/css" href="[css]"/>
137
    [script]
138
  </head>
139
  <body[if-any onload] onload="[onload]"[end]>
140
    <div id="page">
141
    <div id="top"> <h1>[if-any title][title][else][site_name][end]</h1> </div>
142
    <div id="main-content">
143
    [if-any breadcrumb]<p id="breadcrumb">[breadcrumb]</p>[end]
144
    [body]
145
    </div>
146
    <div id="footer">[if-any footer][footer][end]</div>
147
    </div>
148
  </body>
149
</html>"""
150

  
151
DEFAULT_IFRAME_EZT = """<!DOCTYPE html>
152
<html lang="[site_lang]">
153
  <head>
154
    <title>[page_title]</title>
155
    <link rel="stylesheet" type="text/css" href="[css]"/>
156
    [script]
157
  </head>
158
  <body[if-any onload] onload="[onload]"[end]>
159
    <div id="main-content">
160
    [if-any breadcrumb]<p id="breadcrumb">[breadcrumb]</p>[end]
161
    [body]
162
    </div>
163
  </body>
164
</html>"""
165

  
166
default_template = ezt.Template()
167
default_template.parse(DEFAULT_TEMPLATE_EZT)
168

  
169
default_iframe_template = ezt.Template()
170
default_iframe_template.parse(DEFAULT_IFRAME_EZT)
171

  
172

  
173 52
def html_top(title=None, default_org=None):
174 53
    if not hasattr(get_response(), 'filter'):
175 54
        get_response().filter = {}
......
207 86
    return htmltext(r.getvalue())
208 87

  
209 88

  
210
def get_default_ezt_template():
211
    from .publisher import get_cfg
212

  
213
    current_theme = get_cfg('branding', {}).get('theme', 'default')
214

  
215
    filename = os.path.join(
216
        get_publisher().app_dir, 'themes', current_theme, 'template.%s.ezt' % get_publisher().APP_NAME
217
    )
218
    if os.path.exists(filename):
219
        with open(filename) as fd:
220
            return fd.read()
221

  
222
    filename = os.path.join(
223
        get_publisher().data_dir, 'themes', current_theme, 'template.%s.ezt' % get_publisher().APP_NAME
224
    )
225
    if os.path.exists(filename):
226
        with open(filename) as fd:
227
            return fd.read()
228

  
229
    filename = os.path.join(get_publisher().app_dir, 'themes', current_theme, 'template.ezt')
230
    if os.path.exists(filename):
231
        with open(filename) as fd:
232
            return fd.read()
233

  
234
    filename = os.path.join(get_publisher().data_dir, 'themes', current_theme, 'template.ezt')
235
    if os.path.exists(filename):
236
        with open(filename) as fd:
237
            return fd.read()
238

  
239
    return DEFAULT_TEMPLATE_EZT
240

  
241

  
242 89
def get_decorate_vars(body, response, generate_breadcrumb=True, **kwargs):
243 90
    from .publisher import get_cfg
244 91

  
......
303 150
        subtitle = kwargs.get('subtitle')
304 151
        sidebar = kwargs.get('sidebar')
305 152
        css = root_url + get_publisher().qommon_static_dir + get_publisher().qommon_admin_css
306

  
307
        app_dir_filename = os.path.join(get_publisher().app_dir, 'themes', current_theme, 'admin.css')
308
        data_dir_filename = os.path.join(get_publisher().data_dir, 'themes', current_theme, 'admin.css')
309
        for filename in (app_dir_filename, data_dir_filename):
310
            if os.path.exists(filename):
311
                extra_css = root_url + 'themes/%s/admin.css' % current_theme
312
                break
313 153
        extra_head = get_publisher().get_site_option('backoffice_extra_head')
314 154
        app_label = get_publisher().get_site_option('app_label') or 'w.c.s.'
315 155
    else:
316
        if current_theme == 'default':
317
            css = root_url + 'static/css/%s.css' % get_publisher().APP_NAME
318
        else:
319
            css = root_url + 'themes/%s/%s.css' % (current_theme, get_publisher().APP_NAME)
156
        css = root_url + 'themes/%s/%s.css' % (current_theme, get_publisher().APP_NAME)
320 157

  
321 158
        # this variable is kept in locals() as it was once part of the default
322 159
        # template and existing installations may have template changes that
......
354 191
    return vars
355 192

  
356 193

  
357
def decorate(body, response):
358
    if get_request().get_header('x-popup') == 'true':
359
        return '''<div class="popup-content"> %s </div>''' % body
360

  
361
    from .publisher import get_cfg
362

  
363
    generate_breadcrumb = True
364
    template_ezt = get_cfg('branding', {}).get('template')
365
    current_theme = get_cfg('branding', {}).get('theme', 'default')
366
    if not template_ezt:
367
        # the theme can provide a default template
368
        possible_filenames = []
369
        possible_filenames.append('template.%s.ezt' % get_publisher().APP_NAME)
370
        possible_filenames.append('template.ezt')
371

  
372
        possible_dirnames = [
373
            os.path.join(get_publisher().app_dir, 'themes', current_theme),
374
            os.path.join(get_publisher().data_dir, 'themes', current_theme),
375
            os.path.join(get_publisher().data_dir, 'themes', 'default'),
376
        ]
377

  
378
        for fname in possible_filenames:
379
            for dname in possible_dirnames:
380
                filename = os.path.join(dname, fname)
381
                if os.path.exists(filename):
382
                    with open(filename) as fd:
383
                        template_ezt = fd.read()
384
                    break
385
            else:
386
                continue
387
            break
388

  
389
    if template_ezt:
390
        generate_breadcrumb = '[breadcrumb]' in template_ezt
391

  
392
        template = ezt.Template()
393
        template.parse(template_ezt)
394
    else:
395
        template = default_template
396

  
397
    fd = io.StringIO()
398
    vars = get_decorate_vars(body, response, generate_breadcrumb=generate_breadcrumb)
399

  
400
    template.generate(fd, vars)
401
    return fd.getvalue()
402

  
403

  
404 194
def render(template_name, context):
405 195
    request = getattr(get_request(), 'django_request', None)
406 196
    result = render_to_string(template_name, context, request=request)
wcs/root.py
265 265
    ]
266 266

  
267 267
    api = ApiDirectory()
268
    themes = template.ThemesDirectory()
269 268
    myspace = MyspaceDirectory()
270 269
    pages = PagesDirectory()
271 270
    fargo = portfolio.FargoDirectory()
wcs/utils.py
38 38
            template_dirs.append(os.path.join(get_publisher().app_dir, 'templates'))
39 39
            template_dirs.append(os.path.join(get_publisher().app_dir, 'theme', 'templates'))
40 40

  
41
            current_theme = get_cfg('branding', {}).get('theme', 'default')
41
            current_theme = get_cfg('branding', {}).get('theme', get_publisher().default_theme)
42 42
            theme_directory = get_theme_directory(current_theme)
43 43
            if theme_directory:
44 44
                # templates from theme directory
45
-