PaferaPy Async 0.1
ASGI framework focused on simplicity and efficiency
All Classes Namespaces Files Functions Variables
page.py
Go to the documentation of this file.
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4import os
5import json
6import traceback
7import importlib
8
9from pafera.db import *
11
12from pafera.utils import *
13from pafera.validators import *
14
15# Page flag constants
16PAGE_DONT_CACHE = 0x01
17PAGE_LOAD_HOOKS = 0x02
18
19# *********************************************************************
21 """Fragments are parts of a page which can be reused across different
22 pages. Think headers and footers, navigation bars, widgets, and such.
23 """
24
25 _dbfields = {
26 'rid': ('INT', 'NOT NULL PRIMARY KEY',),
27 'path': ('TEXT', 'NOT NULL', BlankValidator()),
28 'content': ('TRANSLATION', 'NOT NULL'),
29 'markdown': ('TRANSLATION', "NOT NULL DEFAULT ''"),
30 'contentfunc': ('TEXT', "NOT NULL DEFAULT ''",),
31 'translations': ('NEWLINELIST', "NOT NULL DEFAULT ''",),
32 'flags': ('INT', 'NOT NULL DEFAULT 0',),
33 }
34 _dbindexes = (
35 ('unique', ('path',)),
36 )
37 _dbdisplay = ['path', 'content']
38 _dblinks = []
39 _dbflags = 0
40
41 # -------------------------------------------------------------------
42 def __init__(self):
43 super().__init__()
44
45 # -------------------------------------------------------------------
46 def Render(self, g):
47 """Returns the text representation of this fragment in the current
48 language.
49 """
50 if self.translations:
51 for t in self.translations:
52 try:
53 appname, translationname = t.split('/')
54 langloaded = LoadTranslation(g, appname, translationname, g.lang)
55 g.page.jsfiles.append(f'/{appname}/translations/{langloaded}/{translationname}.js')
56 except Exception as e:
57 print('!!!\tProblem loading translation ', t, e)
58
59 if not self.flags & PAGE_DONT_CACHE:
60 result = g.cache.Load('pagefragment_' + self.path + '_' + g.session.lang)
61
62 if result:
63 return result.decode('utf-8')
64
65 content = []
66
67 content.append(BestTranslation(self.content, g.session.lang))
68
69 if self.contentfunc:
70 try:
71 exec(self.contentfunc)
72 except Exception as e:
73 content.append('<pre>' + traceback.format_exc() + '</pre>')
74
75 result = '\n'.join(content).encode('utf-16', 'surrogatepass').decode('utf-16')
76
77 if not self.flags & PAGE_DONT_CACHE:
78 g.cache.Store('pagefragment_' + self.path + '_' + g.session.lang, result.encode('utf-8'))
79
80 return result
81
82# *********************************************************************
84 """Represents a webpage in the system. Note that database pages can
85 contain a variety of page fragments in addition to different languages,
86 so they are quite versatile in what they can do.
87
88 requiredgroups are a list of group names that are needed to view
89 the page.
90
91 headerid, contentid, and the rest are used to load page fragments
92 into various sections.
93
94 path is the URL for the page such as /index.html
95
96 pyfile is the module and name of a python function which will be
97 called with the global object g and add content to the page
98 by returning a string.
99
100 template is the template file used to create the page. It's
101 unused as of now since we only use one standard template.
102
103 jsfiles is a list of URLs to load JavaScript files. They can be
104 local or remote. Note that the system will automatically load a
105 JavaScript file of the same path and name. Thus, if you create
106 a page at /newapp/testing.html, the system will try to find and load
107 /newapp/testing.js without you having to do anything.
108
109 cssfiles is the same but for CSS files.
110
111 translations are system translation files to automatically load
112 for use in the page. They will also be available to any JavaScript
113 files included in the system.
114
115 title, content, and markdown are all translation fields, meaning
116 that you can have completely separate content for each language
117 at the same URL. markdown text will be automatically converted
118 by the Python markdown library and stored in content.
119
120 contentfunc is pure python code run in the context of the
121 page.Render() function and passed to the exec() function. Yes,
122 I'm quite aware of the dangers of dynamic code evaluation, but
123 honestly, if you have someone running around randomly inserting
124 code into your database, you already have much bigger problems
125 than a page function. Look at the entry for /logout.html to see
126 a simple and convenient use of this field.
127
128 wallpaper is the URL to a file which will automatically fill
129 the background of the page. It's a quick way to give different
130 pages distinctive feels.
131
132 flags can contain PAGE_DONT_CACHE, which is self-explanatory, and
133 PAGE_LOAD_HOOKS. Hooks are JavaScript files with the same name
134 as this page in other apps which can add functionality to this
135 app's pages. See /system/manageusers.html for an example of this.
136 """
137
138 _dbfields = {
139 'rid': ('INT', 'NOT NULL PRIMARY KEY',),
140 'flags': ('INT', 'NOT NULL DEFAULT 0',),
141 'requiredgroups': ('NEWLINELIST', "NOT NULL DEFAULT ''",),
142
143 'headerid': ('INT', 'NOT NULL DEFAULT 0',),
144 'contentid': ('INT', 'NOT NULL DEFAULT 0',),
145 'footerid': ('INT', 'NOT NULL DEFAULT 0',),
146 'leftbarid': ('INT', 'NOT NULL DEFAULT 0',),
147 'topbarid': ('INT', 'NOT NULL DEFAULT 0',),
148 'rightbarid': ('INT', 'NOT NULL DEFAULT 0',),
149 'bottombarid': ('INT', 'NOT NULL DEFAULT 0',),
150
151 'path': ('TEXT', 'NOT NULL', BlankValidator()),
152 'pyfile': ('TEXT', "NOT NULL DEFAULT ''",),
153 'template': ('TEXT', "NOT NULL DEFAULT ''",),
154 'jsfiles': ('NEWLINELIST', "NOT NULL DEFAULT ''",),
155 'cssfiles': ('NEWLINELIST', "NOT NULL DEFAULT ''",),
156 'translations': ('NEWLINELIST', "NOT NULL DEFAULT ''",),
157
158 'title': ('TRANSLATION', 'NOT NULL', BlankValidator()),
159 'content': ('TRANSLATION', 'NOT NULL', BlankValidator()),
160 'contentfunc': ('TEXT', "NOT NULL DEFAULT ''",),
161 'markdown': ('TRANSLATION', "NOT NULL DEFAULT ''"),
162 'wallpaper': ('TEXT', "NOT NULL DEFAULT ''",),
163 }
164 _dbindexes = (
165 ('unique', ('path',)),
166 )
167 _dbdisplay = ['path', 'title', 'content', 'cssfiles', 'jsfiles', 'translations', 'headerid', 'footerid']
168 _dblinks = []
169 _dbflags = 0
170
171 _managejs = """
172G.fragmentstart = 0;
173
174G.EditPageFragment = function(fragmentid)
175{
176 if (fragmentid && fragmentid != '0')
177 {
178 G.fragmentid = fragmentid;
179
180 P.DialogAPI(
181 '/system/dbapi',
182 {
183 command: 'load',
184 model: 'system_pagefragment',
185 modelid: fragmentid
186 },
187 function(d)
188 {
189 G.EditPageFragmentForm(d.data);
190 }
191 );
192 } else
193 {
194 G.EditPageFragmentForm({});
195 }
196}
197
198G.EditPageFragmentForm = function(fragmentobj)
199{
200 let flags = fragmentobj.flags ? fragmentobj.flags : 0;
201
202 P.EditPopup(
203 [
204 ['rid', 'hidden', '', '', '', ''],
205 ['path', 'text', '', 'Path', '', 'required'],
206 ['content', 'translation', '', 'Content', '', ''],
207 ['contentfunc', 'multitext', '', 'Content Function', '', ''],
208 ['flags', 'bitflags', flags, 'Flags',
209 [
210 ["Don't Cache", 'dontcache', 0x01, flags & 0x01 ? 'on' : 'unset']
211 ]
212 ]
213 ],
214 function(formdiv, data, resultsdiv, e)
215 {
216 P.DialogAPI(
217 '/system/dbapi',
218 {
219 command: 'save',
220 model: 'system_pagefragment',
221 data: data
222 },
223 function(d)
224 {
225 P.HTML(resultsdiv, '<div class="Pad50 greeng">' + T.allgood + '</div>');
226
227 G.fragmentinput.value = d.data.rid;
228
229 if (e.target.classList.contains('SaveAndContinueButton'))
230 {
231 P.HTML('.EditFragmentAreaResults', '<div class="Center Pad25 greenb">' + T.allgood + '</div>');
232 return;
233 }
234
235 setTimeout(
236 function()
237 {
238 P.RemoveFullScreen();
239 },
240 500
241 );
242 }
243 );
244 },
245 {
246 cancelfunc: function() { P.RemoveFullScreen(); },
247 enabletranslation: 1,
248 formdiv: '.EditFragmentArea',
249 extrabuttons: `
250 <a class="Color5" onclick="G.FragmentNew()">New</a>
251 <a class="Color4" onclick="G.FragmentNone()">None</a>`,
252 obj: fragmentobj,
253 saveandcontinue: 1
254 }
255 );
256}
257
258G.FragmentNew = function(start)
259{
260 G.EditPageFragmentForm({});
261}
262
263G.FragmentNone = function(start)
264{
265 G.fragmentinput.value = '';
266 P.RemoveFullScreen();
267}
268
269G.ListFragments = function(start)
270{
271 if (start == undefined)
272 {
273 start = G.fragmentstart;
274 }
275
276 G.fragmentstart = start;
277
278 let filter = E('.FragmentFilter').value;
279
280 P.LoadingAPI(
281 '.EditFragmentTiles',
282 '/system/dbapi',
283 {
284 command: 'search',
285 model: 'system_pagefragment',
286 condition: filter ? `WHERE path LIKE '%${EscapeSQL(filter)}%'` : '',
287 start: start,
288 limit: 20,
289 orderby: 'path'
290 },
291 function(d, resultsdiv)
292 {
293 let data = d.data;
294
295 G.fragmentobjs = data;
296 G.fragmentcount = d.count;
297
298 let ls = [];
299
300 for (let i = 0, l = data.length; i < l; i++)
301 {
302 ls.push(`
303 <div class="Color3 Pad50 Raised FragmentTile" data-objid="${data[i].rid}">${data[i].path}</div>
304 `);
305 }
306
307 P.HTML(resultsdiv, ls);
308
309 P.HTML('.EditFragmentTilesPageBar', P.PageBar(G.fragmentcount, G.fragmentstart, 20, G.fragmentobjs.length, 'G.ListFragments'));
310 }
311 );
312}
313
314G.displayfuncs['system_page'] = function(type, fields, item, ls, modellinks, data)
315{
316 let modelid = G.modelids['system_page'];
317 let modelinfo = G.modelfields['system_page'];
318
319 if (type == 'card')
320 {
321 ls.push(`<div class="blueg Center">${item['rid']}</div>`);
322 } else
323 {
324 ls.push(`<th">${item['rid']}</th>`);
325 }
326
327 for (let j = 1, m = fields.length; j < m; j++)
328 {
329 let k = fields[j];
330
331 let value = item[k];
332 let valuetype = modelinfo[k][0];
333
334 if (type == 'card')
335 {
336 if (k == 'content' || k == 'title')
337 {
338 ls.push('<div>' + StripTags(P.BestTranslation(value)) + '</div>');
339 } else if (valuetype == 'JSON')
340 {
341 ls.push('<pre>' + JSON.stringify(value, null, 2) + '</pre>');
342 } else
343 {
344 ls.push('<div>' + value + '</div>');
345 }
346 } else
347 {
348 if (k == 'content' || k == 'title')
349 {
350 ls.push('<td>' + StripTags(P.BestTranslation(value)) + '</td>');
351 } else if (valuetype == 'JSON')
352 {
353 ls.push('<td><pre>' + JSON.stringify(value, null, 2) + '</pre></td>');
354 } else
355 {
356 ls.push('<td>' + value + '</td>');
357 }
358 }
359 }
360}
361
362G.customeditfuncs['system_page'] = function(obj, editfields)
363{
364 let flagsrow = E('.DBObjFormForm .flagsRow');
365
366 if (flagsrow)
367 {
368 let value = obj.flags ? obj.flags : 0;
369
370 P.HTML(
371 '.DBObjFormForm .flagsRow',
372 `<div class="BitFlags flagsFlags"
373 data-name="flags"
374 data-value="${value}">
375 </div>`
376 );
377
378 P.MakeToggleButtons(
379 '.DBObjFormForm .flagsFlags',
380 [
381 ["Don't Cache", 'dontcache', 0x01, value & 0x01 ? 'on' : 'unset'],
382 ["Load Hooks", 'loadhooks', 0x02, value & 0x02 ? 'on' : 'unset']
383 ],
384 {
385 flags: value,
386 ontoggle: function(el, flagname, newstate)
387 {
388 let flagsel = E('.DBObjFormForm .flagsFlags');
389
390 flagsel.dataset.value = (newstate == 'on')
391 ? parseInt(flagsel.dataset.value) | parseInt(el.dataset.value)
392 : parseInt(flagsel.dataset.value) & (~parseInt(el.dataset.value));
393 }
394 }
395 );
396 }
397
398 P.OnClick(
399 '.DBObjFormForm input[type=number]',
400 P.LBUTTON,
401 function(e)
402 {
403 let targetname = e.target.getAttribute('name');
404
405 if (targetname.endsWith('id') && targetname != 'rid')
406 {
407 P.AddFullScreen(
408 'dpurpleg',
409 '',
410 `<h2 class="Center">Edit Fragment</h2>
411 <div class="ButtonBar EditFragmentBar">
412 <input type="text" class="FragmentFilter">
413 <a class="Color1" onclick="G.ListFragments()">Search</a>
414 </div>
415 <div class="FlexGrid20 Gap50 EditFragmentTiles"></div>
416 <div class="EditFragmentTilesPageBar"></div>
417 <div class="EditFragmentArea"></div>`
418 );
419
420 P.OnClick(
421 '.EditFragmentTiles',
422 P.LBUTTON,
423 function(e)
424 {
425 let tile = P.TargetClass(e, '.FragmentTile');
426
427 if (!tile)
428 return;
429
430 G.EditPageFragment(tile.dataset.objid);
431 }
432 )
433
434 G.fragmentinput = e.target;
435
436 G.EditPageFragment(e.target.value);
437 }
438 }
439 );
440}
441
442"""
443
444 # -------------------------------------------------------------------
445 def __init__(self):
446 super().__init__()
447
448 # -------------------------------------------------------------------
449 def RenderCSS(self, url):
450 """Converts a stylesheet URL into the full link tag.
451 """
452
453 if not url:
454 return ''
455
456 return f'<link rel="stylesheet" type="text/css" href="{url}" />'
457
458 # -------------------------------------------------------------------
459 def RenderJS(self, url):
460 """Converts a stylesheet URL into the full script tag.
461 """
462
463 return f'<script src="{url}"></script>'
464
465 # -------------------------------------------------------------------
466 def Render(self, g):
467 """Returns a string containing the full HTML of the page.
468 """
469 if not self.jsfiles:
470 self.jsfiles = []
471
472 if not self.cssfiles:
473 self.cssfiles = []
474
475 if self.translations:
476 for t in self.translations:
477 try:
478 appname, translationname = t.split('/')
479 langloaded = LoadTranslation(g, appname, translationname, g.lang)
480 self.jsfiles.insert(0, f'/{appname}/translations/{langloaded}/{translationname}.js')
481 except Exception as e:
482 print('!!!\tProblem loading translation ', t, e)
483
484 lang = g.session.lang
485
486 topbar = g.db.Load(system_pagefragment, self.topbarid) if self.topbarid else ''
487 bottombar = g.db.Load(system_pagefragment, self.bottombarid) if self.bottombarid else ''
488 leftbar = g.db.Load(system_pagefragment, self.leftbarid) if self.leftbarid else ''
489 rightbar = g.db.Load(system_pagefragment, self.rightbarid) if self.rightbarid else ''
490
491 layout = {}
492
493 if topbar:
494 layout['top'] = topbar.size
495
496 if bottombar:
497 layout['bottom'] = bottombar.size
498
499 if leftbar:
500 layout['left'] = leftbar.size
501
502 if rightbar:
503 layout['right'] = rightbar.size
504
505 title = BestTranslation(self.title, g.session.langcodes)
506
507 content = []
508
509 if self.headerid:
510 content.append(g.db.Load(system_pagefragment, self.headerid).Render(g))
511
512 if self.content:
513 content.append(BestTranslation(self.content, g.session.langcodes))
514
515 if self.contentid:
516 content.append(g.db.Load(system_pagefragment, self.contentid).Render(g))
517
518 if self.contentfunc:
519 try:
520 exec(self.contentfunc)
521 except Exception as e:
522 content.append(f'<div class="Error">{e}</div>')
523
524 if not g.db.dbflags & DB_PRODUCTION:
525 content.append('<pre>' + traceback.format_exc() + '</pre>')
526
527 # Add content from the python function if specified
528 if self.pyfile:
529 try:
530 parts = self.pyfile.split('.')
531
532 modfile = '.'.join(parts[0:-1])
533 renderfunc = parts[-1]
534
535 mod = importlib.import_module(modfile)
536
537 content.append(mod.ROUTES[renderfunc](g))
538 except Exception as e:
539 content.append(f'<div class="Error">{e}</div>')
540
541 if not g.db.dbflags & DB_PRODUCTION:
542 content.append('<pre>' + traceback.format_exc() + '</pre>')
543
544 if self.footerid:
545 content.append(g.db.Load(system_pagefragment, self.footerid).Render(g))
546
547 content = '\n'.join(content)
548
549 # Automatically load the corresponding JavaScript file if found
550 pagejsfile = 'static/' + g.apppath + '/' + g.pagename[:-5] + '.js'
551 pagejs = ''
552
553 if os.path.exists(pagejsfile):
554 pagejs = self.RenderJS(f'''/{g.apppath}/{g.pagename[:-5]}.js''')
555
556 if g.siteconfig['production']:
557 cssfiles = ['/system/all.min.css'] + self.cssfiles
558 else:
559 cssfiles = g.siteconfig['cssfiles'] + self.cssfiles
560
561 cssfiles = '\n'.join([self.RenderCSS(x) for x in cssfiles])
562
563 if g.siteconfig['production']:
564 jsfiles = ['/system/all.min.js'] + self.jsfiles
565 else:
566 jsfiles = g.siteconfig['jsfiles'] + self.jsfiles
567
568 hookjsfiles = []
569
570 if self.flags & PAGE_LOAD_HOOKS:
571 parts = os.path.split(self.path)
572
573 if len(parts) == 2:
574 appname, pagename = parts
575
576 # Remove leading slash
577 appname = appname[1:]
578
579 hookfile = os.path.splitext(pagename)[0] + '.js'
580
581 for dirname in os.listdir('static'):
582 if dirname == appname:
583 continue
584
585 if os.path.isdir(os.path.join('static', dirname)):
586 hookjs = os.path.join('static', dirname, hookfile)
587
588 if os.path.exists(hookjs):
589 hookjsfiles.append('/' + dirname + '/' + hookfile)
590
591 hookjsfiles = '\n'.join([self.RenderJS(x) for x in hookjsfiles])
592
593 jsfiles = '\n'.join([self.RenderJS(x) for x in jsfiles])
594
595 wallpaper = ''
596
597 if self.wallpaper:
598 wallpaper = f"""class="BackgroundCover" style="background-image: url('/system/wallpapers/{self.wallpaper}.webp')" """
599
600 rendered = f"""<!DOCTYPE html>
601<html>
602<head>
603
604 <meta charset="UTF-8">
605 <meta name="apple-mobile-web-app-capable" content="yes" />
606 <meta name="mobile-web-app-capable" content="yes" />
607 <meta name="viewport" content="width=device-width,initial-scale=1">
608
609 <link rel="manifest" href="/manifest.json">
610
611 <title>{title}</title>
612
613 <script>
614 // Preload translations
615 window.T = {{}}
616 </script>
617
618 {self.RenderJS('/system/translations/en/system.js')}
619
620 {cssfiles}
621 {jsfiles}
622
623 <link rel="stylesheet" type="text/css" href="/system/singlepageapp.css" />
624 <link id="ScreenSizeStylesheet" rel="stylesheet" type="text/css" href="/system/blank.css" />
625</head>
626<body {wallpaper}>
627
628<script>
629
630if (!document.addEventListener)
631{{
632 window.location = '/outdated.{lang}.html';
633}}
634
635</script>
636
637<div class="ViewportGrid">
638 <div class="TopBarGrid">{topbar.Render(g) if topbar else ''}</div>
639 <div class="LeftBarGrid">{leftbar.Render(g) if leftbar else ''}</div>
640 <div class="PageBodyGrid">
641 {content}
642 <pre class="DebugMessages white"></pre>
643 </div>
644 <div class="RightBarGrid">{rightbar.Render(g) if rightbar else ''}</div>
645 <div class="BottomBarGrid">{bottombar.Render(g) if bottombar else ''}</div>
646</div>
647
648<noscript>
649 <div class="Error FullScreen">This site uses advanced technologies in order to provide a faster and more efficient experience for our visitors. Please enable JavaScript to use this site properly.</div>
650</noscript>
651
652{self.RenderJS('/system/translations/' + g.session.lang + '/system.js')}
653{self.RenderJS('/system/sessionvars')}
654{pagejs}
655{hookjsfiles}
656
657<script>
658
659P.AddHandler('pageload', function() {{ P.SetLayout({json.dumps(layout)}); }});
660
661</script>
662
663</body>
664</html>"""
665
666 # Return proper encoding in utf-16
667 return rendered.encode('utf-16', 'surrogatepass').decode('utf-16')
Base class for all database models.
Definition: modelbase.py:20
Throws an exception on blank values.
Definition: validators.py:43
Represents a webpage in the system.
Definition: page.py:83
def Render(self, g)
Returns a string containing the full HTML of the page.
Definition: page.py:466
def RenderJS(self, url)
Converts a stylesheet URL into the full script tag.
Definition: page.py:459
def __init__(self)
Initialize all fields at creation like a good programmer should.
Definition: page.py:445
def RenderCSS(self, url)
Converts a stylesheet URL into the full link tag.
Definition: page.py:449
Fragments are parts of a page which can be reused across different pages.
Definition: page.py:20
def Render(self, g)
Returns the text representation of this fragment in the current language.
Definition: page.py:46
def __init__(self)
Initialize all fields at creation like a good programmer should.
Definition: page.py:42
Definition: db.py:1
def BestTranslation(translations, languages, defaultlang='en')
Returns the best translation found in a dict of different translations.
Definition: utils.py:104
def LoadTranslation(g, app, translation, languages)
Load the translation into the global language dict in g.T.
Definition: utils.py:129