platzky 0.1.18__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- platzky/blog/__init__.py +0 -0
- platzky/blog/blog.py +72 -42
- platzky/blog/comment_form.py +8 -4
- platzky/config.py +63 -57
- platzky/db/__init__.py +0 -0
- platzky/db/db.py +107 -0
- platzky/db/db_loader.py +32 -0
- platzky/db/google_json_db.py +33 -10
- platzky/db/graph_ql_db.py +117 -27
- platzky/db/json_db.py +83 -19
- platzky/db/json_file_db.py +25 -9
- platzky/models.py +64 -0
- platzky/platzky.py +122 -61
- platzky/plugin_loader.py +31 -34
- platzky/plugins/redirections/entrypoint.py +41 -19
- platzky/plugins/sendmail/entrypoint.py +32 -11
- platzky/seo/seo.py +37 -26
- platzky/static/blog.css +5 -12
- platzky/templates/base.html +137 -22
- platzky/templates/blog.html +3 -2
- platzky/templates/body_meta.html +4 -21
- platzky/templates/feed.xml +5 -5
- platzky/templates/head_meta.html +5 -15
- platzky/templates/page.html +2 -2
- platzky/templates/post.html +2 -2
- platzky/www_handler.py +7 -4
- platzky-0.2.0.dist-info/METADATA +39 -0
- platzky-0.2.0.dist-info/RECORD +34 -0
- {platzky-0.1.18.dist-info → platzky-0.2.0.dist-info}/WHEEL +1 -1
- platzky/blog/db.py +0 -18
- platzky/blog/post_formatter.py +0 -16
- platzky/db_loader.py +0 -25
- platzky-0.1.18.dist-info/METADATA +0 -39
- platzky-0.1.18.dist-info/RECORD +0 -32
platzky/plugin_loader.py
CHANGED
|
@@ -1,42 +1,39 @@
|
|
|
1
|
+
import importlib.util
|
|
1
2
|
import os
|
|
2
3
|
import sys
|
|
3
|
-
from os.path import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
from os.path import abspath, dirname
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def find_plugin(plugin_name):
|
|
8
|
+
"""Find plugin by name and return it as module.
|
|
9
|
+
:param plugin_name: name of plugin to find
|
|
10
|
+
:return: module of plugin
|
|
11
|
+
"""
|
|
12
|
+
plugins_dir = os.path.join(dirname(abspath(__file__)), "plugins")
|
|
13
|
+
module_name = plugin_name.removesuffix(".py")
|
|
14
|
+
spec = importlib.util.spec_from_file_location(
|
|
15
|
+
module_name, os.path.join(plugins_dir, plugin_name, "entrypoint.py")
|
|
16
|
+
)
|
|
17
|
+
assert spec is not None
|
|
18
|
+
plugin = importlib.util.module_from_spec(spec)
|
|
19
|
+
sys.modules[module_name] = plugin
|
|
20
|
+
assert spec.loader is not None
|
|
21
|
+
spec.loader.exec_module(plugin)
|
|
22
|
+
return plugin
|
|
13
23
|
|
|
14
24
|
|
|
15
|
-
def
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
raise Exception(f"Plugins {selected_not_enabled} has been selected in config, but has not been installed.")
|
|
21
|
-
|
|
22
|
-
for plugin_dir in enabled_plugins:
|
|
23
|
-
module_name = plugin_dir.removesuffix('.py')
|
|
24
|
-
spec = importlib.util.spec_from_file_location(module_name,
|
|
25
|
-
os.path.join(plugins_dir, plugin_dir, "entrypoint.py"))
|
|
26
|
-
plugin = importlib.util.module_from_spec(spec)
|
|
27
|
-
sys.modules[module_name] = plugin
|
|
28
|
-
spec.loader.exec_module(plugin)
|
|
29
|
-
plugins.append(plugin)
|
|
30
|
-
|
|
31
|
-
for finder, name, ispkg in pkgutil.iter_modules():
|
|
32
|
-
if name.startswith('platzky_'):
|
|
33
|
-
plugins.append(importlib.import_module(name))
|
|
25
|
+
def plugify(app):
|
|
26
|
+
"""Load plugins and run their entrypoints.
|
|
27
|
+
:param app: Flask app
|
|
28
|
+
:return: Flask app
|
|
29
|
+
"""
|
|
34
30
|
|
|
35
|
-
|
|
31
|
+
plugins_data = app.db.get_plugins_data()
|
|
36
32
|
|
|
33
|
+
for plugin_data in plugins_data:
|
|
34
|
+
plugin_config = plugin_data["config"]
|
|
35
|
+
plugin_name = plugin_data["name"]
|
|
36
|
+
plugin = find_plugin(plugin_name)
|
|
37
|
+
plugin.process(app, plugin_config)
|
|
37
38
|
|
|
38
|
-
def plugify(app):
|
|
39
|
-
plugins = set(app.config["PLUGINS"])
|
|
40
|
-
for plugin in find_plugins(plugins):
|
|
41
|
-
plugin.process(app)
|
|
42
39
|
return app
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
from flask import redirect
|
|
2
|
-
from functools import partial
|
|
3
2
|
from gql import gql
|
|
3
|
+
from pydantic import BaseModel
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
def
|
|
6
|
+
def json_db_get_redirections(self):
|
|
7
7
|
return self.data.get("redirections", {})
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def
|
|
10
|
+
def json_file_db_get_redirections(self):
|
|
11
|
+
return json_db_get_redirections(self)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def google_json_db_get_redirections(self):
|
|
11
15
|
return self.data.get("redirections", {})
|
|
12
16
|
|
|
13
17
|
|
|
14
|
-
def
|
|
18
|
+
def graph_ql_db_get_redirections(self):
|
|
15
19
|
redirections = gql(
|
|
16
20
|
"""
|
|
17
21
|
query MyQuery{
|
|
@@ -22,25 +26,43 @@ def graphql_get_redirections(self):
|
|
|
22
26
|
}
|
|
23
27
|
"""
|
|
24
28
|
)
|
|
25
|
-
return {
|
|
29
|
+
return {
|
|
30
|
+
x["source"]: x["destination"]
|
|
31
|
+
for x in self.client.execute(redirections)["redirections"]
|
|
32
|
+
}
|
|
26
33
|
|
|
27
34
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"graph_ql": graphql_get_redirections,
|
|
32
|
-
"google_json": google_get_redirections
|
|
35
|
+
class Redirection(BaseModel):
|
|
36
|
+
source: str
|
|
37
|
+
destiny: str
|
|
33
38
|
|
|
34
|
-
}
|
|
35
|
-
return redirections[db_type]
|
|
36
39
|
|
|
40
|
+
def parse_redirections(config: dict) -> list[Redirection]:
|
|
41
|
+
return [
|
|
42
|
+
Redirection(source=source, destiny=destiny)
|
|
43
|
+
for source, destiny in config.items()
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def setup_routes(app, redirections):
|
|
48
|
+
for redirection in redirections:
|
|
49
|
+
func = redirect_with_name(
|
|
50
|
+
redirection.destiny,
|
|
51
|
+
code=301,
|
|
52
|
+
name=f"{redirection.source}-{redirection.destiny}",
|
|
53
|
+
)
|
|
54
|
+
app.route(rule=redirection.source)(func)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def redirect_with_name(destiny, code, name):
|
|
58
|
+
def named_redirect(*args, **kwargs):
|
|
59
|
+
return redirect(destiny, code, *args, **kwargs)
|
|
60
|
+
|
|
61
|
+
named_redirect.__name__ = name
|
|
62
|
+
return named_redirect
|
|
37
63
|
|
|
38
|
-
def process(app):
|
|
39
|
-
app.db.get_redirections = get_proper_redirections(app.config["DB"]["TYPE"])
|
|
40
|
-
redirects = app.db.get_redirections(app.db)
|
|
41
|
-
for source, destiny in redirects.items():
|
|
42
|
-
func = partial(redirect, destiny, code=301)
|
|
43
|
-
func.__name__ = f"{source}-{destiny}"
|
|
44
|
-
app.route(rule=source)(func)
|
|
45
64
|
|
|
65
|
+
def process(app, config: dict) -> object:
|
|
66
|
+
redirections = parse_redirections(config)
|
|
67
|
+
setup_routes(app, redirections)
|
|
46
68
|
return app
|
|
@@ -1,22 +1,43 @@
|
|
|
1
1
|
import smtplib
|
|
2
|
-
from functools import partial
|
|
3
2
|
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
4
|
|
|
5
|
-
def send_mail(sender_email, password, smtp_server, port, receiver_email, subject, message):
|
|
6
|
-
full_message = f'From: {sender_email}\nTo: {receiver_email}\nSubject: {subject}\n\n{message}'
|
|
7
5
|
|
|
8
|
-
|
|
6
|
+
def send_mail(
|
|
7
|
+
sender_email, password, smtp_server, port, receiver_email, subject, message
|
|
8
|
+
):
|
|
9
|
+
full_message = (
|
|
10
|
+
f"From: {sender_email}\nTo: {receiver_email}\nSubject: {subject}\n\n{message}"
|
|
11
|
+
)
|
|
12
|
+
server = smtplib.SMTP_SSL(smtp_server, port)
|
|
9
13
|
server.ehlo()
|
|
10
14
|
server.login(sender_email, password)
|
|
11
15
|
server.sendmail(sender_email, receiver_email, full_message)
|
|
12
16
|
server.close()
|
|
13
17
|
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
class SendMailConfig(BaseModel):
|
|
20
|
+
user: str = Field(alias="sender_email")
|
|
21
|
+
password: str = Field(alias="password")
|
|
22
|
+
server: str = Field(alias="smtp_server")
|
|
23
|
+
port: int = Field(alias="port")
|
|
24
|
+
receiver: str = Field(alias="receiver_email")
|
|
25
|
+
subject: str = Field(alias="subject")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def process(app, config):
|
|
29
|
+
plugin_config = SendMailConfig.model_validate(config)
|
|
30
|
+
|
|
31
|
+
def notify(message):
|
|
32
|
+
send_mail(
|
|
33
|
+
sender_email=plugin_config.user,
|
|
34
|
+
password=plugin_config.password,
|
|
35
|
+
smtp_server=plugin_config.server,
|
|
36
|
+
port=plugin_config.port,
|
|
37
|
+
receiver_email=plugin_config.receiver,
|
|
38
|
+
subject=plugin_config.subject,
|
|
39
|
+
message=message,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
app.add_notifier(notify)
|
|
22
43
|
return app
|
platzky/seo/seo.py
CHANGED
|
@@ -1,26 +1,34 @@
|
|
|
1
|
+
import typing as t
|
|
1
2
|
import urllib.parse
|
|
2
|
-
from
|
|
3
|
+
from os.path import dirname
|
|
4
|
+
from flask import Blueprint, current_app, make_response, render_template, request
|
|
3
5
|
|
|
4
6
|
|
|
5
|
-
def create_seo_blueprint(db, config):
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
def create_seo_blueprint(db, config: dict[str, t.Any]):
|
|
8
|
+
seo = Blueprint(
|
|
9
|
+
"seo",
|
|
10
|
+
__name__,
|
|
11
|
+
url_prefix=config["SEO_PREFIX"],
|
|
12
|
+
template_folder=f"{dirname(__file__)}/../templates",
|
|
13
|
+
)
|
|
9
14
|
|
|
10
15
|
@seo.route("/robots.txt")
|
|
11
16
|
def robots():
|
|
12
|
-
robots_response = render_template(
|
|
17
|
+
robots_response = render_template(
|
|
18
|
+
"robots.txt", domain=request.host, mimetype="text/plain"
|
|
19
|
+
)
|
|
13
20
|
response = make_response(robots_response)
|
|
14
21
|
response.headers["Content-Type"] = "text/plain"
|
|
15
22
|
return response
|
|
16
23
|
|
|
17
24
|
@seo.route("/sitemap.xml")
|
|
18
25
|
def main_sitemap():
|
|
19
|
-
if domain_to_lang := config
|
|
20
|
-
|
|
21
|
-
return sitemap(domains_lang)
|
|
26
|
+
if domain_to_lang := config["DOMAIN_TO_LANG"]:
|
|
27
|
+
return sitemap(domain_to_lang[request.host])
|
|
22
28
|
else:
|
|
23
|
-
return sitemap(
|
|
29
|
+
return sitemap(
|
|
30
|
+
config.get("TRANSLATION_DIRECTORIES")
|
|
31
|
+
) # TODO should be based on localization not on config
|
|
24
32
|
|
|
25
33
|
def sitemap(lang):
|
|
26
34
|
"""
|
|
@@ -35,30 +43,33 @@ def create_seo_blueprint(db, config):
|
|
|
35
43
|
|
|
36
44
|
# Static routes with static content
|
|
37
45
|
static_urls = list()
|
|
38
|
-
for rule in
|
|
46
|
+
for rule in current_app.url_map.iter_rules():
|
|
39
47
|
if not str(rule).startswith("/admin") and not str(rule).startswith("/user"):
|
|
40
|
-
if
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
if (
|
|
49
|
+
rule.methods is not None
|
|
50
|
+
and "GET" in rule.methods
|
|
51
|
+
and len(rule.arguments) == 0
|
|
52
|
+
):
|
|
53
|
+
url = {"loc": f"{host_base}{str(rule)}"}
|
|
54
|
+
static_urls.append(url)
|
|
45
55
|
|
|
46
56
|
# Dynamic routes with dynamic content
|
|
47
57
|
dynamic_urls = list()
|
|
48
58
|
seo_posts = db.get_all_posts(lang)
|
|
49
59
|
for post in seo_posts:
|
|
50
|
-
slug = post[
|
|
51
|
-
datet = post[
|
|
52
|
-
url = {
|
|
53
|
-
"loc": f"{host_base}/seo/{slug}",
|
|
54
|
-
"lastmod": datet
|
|
55
|
-
}
|
|
60
|
+
slug = post["slug"]
|
|
61
|
+
datet = post["date"].split("T")[0]
|
|
62
|
+
url = {"loc": f"{host_base}/{slug}", "lastmod": datet}
|
|
56
63
|
dynamic_urls.append(url)
|
|
57
64
|
|
|
58
|
-
statics = list({v[
|
|
59
|
-
dynamics = list({v[
|
|
60
|
-
xml_sitemap = render_template(
|
|
61
|
-
|
|
65
|
+
statics = list({v["loc"]: v for v in static_urls}.values())
|
|
66
|
+
dynamics = list({v["loc"]: v for v in dynamic_urls}.values())
|
|
67
|
+
xml_sitemap = render_template(
|
|
68
|
+
"sitemap.xml",
|
|
69
|
+
static_urls=statics,
|
|
70
|
+
dynamic_urls=dynamics,
|
|
71
|
+
host_base=host_base,
|
|
72
|
+
)
|
|
62
73
|
response = make_response(xml_sitemap)
|
|
63
74
|
response.headers["Content-Type"] = "application/xml"
|
|
64
75
|
return response
|
platzky/static/blog.css
CHANGED
|
@@ -81,29 +81,22 @@ img::-moz-selection {
|
|
|
81
81
|
background: transparent;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
#mainNav {
|
|
85
|
-
# position: absolute;
|
|
86
|
-
# border-bottom: 1px solid #e9ecef;
|
|
87
|
-
# background-color: white;
|
|
88
|
-
font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
84
|
#mainNav .navbar-brand {
|
|
92
|
-
font-weight:
|
|
85
|
+
font-weight: 1000;
|
|
93
86
|
color: #343a40;
|
|
94
87
|
}
|
|
95
88
|
|
|
96
89
|
#mainNav .navbar-toggler {
|
|
97
|
-
font-size:
|
|
90
|
+
font-size: 20px;
|
|
98
91
|
font-weight: 800;
|
|
99
|
-
padding:
|
|
92
|
+
padding: 10px;
|
|
100
93
|
text-transform: uppercase;
|
|
101
94
|
color: #343a40;
|
|
102
95
|
}
|
|
103
96
|
|
|
104
97
|
#mainNav .navbar-nav > li.nav-item > a {
|
|
105
|
-
font-size:
|
|
106
|
-
font-weight:
|
|
98
|
+
font-size: 15px;
|
|
99
|
+
font-weight: 1000;
|
|
107
100
|
letter-spacing: 1px;
|
|
108
101
|
text-transform: uppercase;
|
|
109
102
|
}
|
platzky/templates/base.html
CHANGED
|
@@ -1,16 +1,110 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
2
|
<html lang="{{ language }}">
|
|
3
3
|
<head>
|
|
4
|
+
{% block head_meta %}
|
|
4
5
|
{% include "head_meta.html" %}
|
|
6
|
+
|
|
7
|
+
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"
|
|
8
|
+
integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
|
|
9
|
+
crossorigin=""/>
|
|
10
|
+
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css" />
|
|
11
|
+
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css" />
|
|
12
|
+
<link rel="stylesheet" href="{{ font.url }}">
|
|
13
|
+
{% endblock %}
|
|
5
14
|
<title>{% block title %}{% endblock %}</title>
|
|
6
15
|
<meta name="description" content=" {% block description %} {% endblock %} ">
|
|
16
|
+
<style>
|
|
17
|
+
html,
|
|
18
|
+
body {
|
|
19
|
+
height: 100%;
|
|
20
|
+
|
|
21
|
+
}
|
|
22
|
+
body {
|
|
23
|
+
font-family: {{ font.name }}
|
|
24
|
+
}
|
|
25
|
+
.logo {
|
|
26
|
+
max-height: 6rem;
|
|
27
|
+
max-width: 50vw;
|
|
28
|
+
}
|
|
29
|
+
.header-row {
|
|
30
|
+
z-index: 1;
|
|
31
|
+
background-color: {{ primary_color }};
|
|
32
|
+
height: min-content;
|
|
33
|
+
display: flex;
|
|
34
|
+
justify-content: center;
|
|
35
|
+
align-items: center;
|
|
36
|
+
}
|
|
37
|
+
#main-row {
|
|
38
|
+
position: relative;
|
|
39
|
+
flex-grow: 1;
|
|
40
|
+
z-index: 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.left-panel-contents {
|
|
44
|
+
padding: 16px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.left-panel {
|
|
48
|
+
background-color: {{ secondary_color }};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#left-panel {
|
|
52
|
+
background-color: {{ secondary_color }};
|
|
53
|
+
position: absolute;
|
|
54
|
+
height: 100%;
|
|
55
|
+
max-width: 80vw;
|
|
56
|
+
overflow-y: auto;
|
|
57
|
+
color: white;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.offcanvas-lg.offcanvas-start {
|
|
61
|
+
top: auto;
|
|
62
|
+
width: auto;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#filter-form form > div {
|
|
66
|
+
padding-bottom: 1rem;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#filter-form form div > span {
|
|
70
|
+
font-weight: bold;
|
|
71
|
+
padding-bottom: 0.5rem;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.dropdown-menu {
|
|
75
|
+
z-index: 1;
|
|
76
|
+
}
|
|
77
|
+
.btn {
|
|
78
|
+
padding: 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.btn-close {
|
|
82
|
+
opacity: 1;
|
|
83
|
+
}
|
|
84
|
+
i.fi {
|
|
85
|
+
margin: 0 0.5em;
|
|
86
|
+
}
|
|
87
|
+
</style>
|
|
7
88
|
</head>
|
|
8
89
|
<body>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
90
|
+
{% block body_meta %}
|
|
91
|
+
{% include "body_meta.html" %}
|
|
92
|
+
{% endblock %}
|
|
93
|
+
<div class="container-fluid d-flex flex-column h-100 g-0">
|
|
94
|
+
<div class="row header-row bg-light g-0">
|
|
95
|
+
<nav class="navbar navbar-expand-lg navbar-light p-3" id="mainNav">
|
|
96
|
+
{% if self.left_panel() %}
|
|
97
|
+
<button class="navbar-toggler" type="button" data-bs-toggle="offcanvas" data-bs-target="#left-panel" aria-label="Toggle left panel">
|
|
98
|
+
<i class="fas fa-sliders-h"></i>
|
|
99
|
+
</button>
|
|
100
|
+
{% endif %}
|
|
101
|
+
<a class="navbar-brand" href="/">
|
|
102
|
+
{% if logo_url %}
|
|
103
|
+
<img src="{{ logo_url }}" class="logo" >
|
|
104
|
+
{% else %}
|
|
105
|
+
{{ _(app_name) }}
|
|
106
|
+
{% endif %}
|
|
107
|
+
</a>
|
|
14
108
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
|
|
15
109
|
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
|
16
110
|
<i class="fas fa-bars"></i>
|
|
@@ -19,34 +113,55 @@
|
|
|
19
113
|
<ul class="navbar-nav">
|
|
20
114
|
{% for menu_item in menu_items %}
|
|
21
115
|
<li class="nav-item">
|
|
22
|
-
<a class="nav-link" href={{menu_item
|
|
116
|
+
<a class="nav-link" href={{menu_item.url}} >{{_(menu_item.name)}}</a>
|
|
23
117
|
</li>
|
|
24
118
|
{% endfor %}
|
|
25
119
|
</ul>
|
|
26
120
|
<ul class="navbar-nav ms-auto">
|
|
27
121
|
<li class="nav-item dropdown">
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
122
|
+
<div id="language-dropdown" class="btn-group">
|
|
123
|
+
<button type="button" class="nav-link dropdown-toggle btn btn-link" id="languages-menu" role="button"
|
|
124
|
+
data-bs-toggle="dropdown" aria-expanded="false">
|
|
125
|
+
<i class="fi fi-{{ current_flag }}"></i>
|
|
126
|
+
</button>
|
|
127
|
+
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="languages-menu">
|
|
128
|
+
{% for lg,items in languages.items() %}
|
|
129
|
+
<a class="dropdown-item" href={{url_for('change_language', lang=lg, _external=True)}}>
|
|
130
|
+
<i class="fi fi-{{ items['flag'] }} "></i>{{ items['name'] }}</a>
|
|
131
|
+
{% endfor %}
|
|
132
|
+
</ul>
|
|
133
|
+
</div>
|
|
37
134
|
</li>
|
|
38
135
|
</ul>
|
|
39
136
|
</div>
|
|
40
|
-
</
|
|
41
|
-
</
|
|
42
|
-
|
|
137
|
+
</nav>
|
|
138
|
+
</div>
|
|
139
|
+
{% block header %}{% endblock %}
|
|
43
140
|
|
|
44
|
-
|
|
45
|
-
{%
|
|
141
|
+
<div class="row g-0" id="main-row">
|
|
142
|
+
{% if self.left_panel() %}
|
|
143
|
+
<div class="col-lg-2 left-panel" >
|
|
144
|
+
<div id="left-panel" class="left-panel offcanvas-lg offcanvas-start" aria-modal="true" role="dialog">
|
|
145
|
+
<div class="offcanvas-body d-flex">
|
|
146
|
+
<div class="left-panel-contents">
|
|
147
|
+
{% block left_panel %}{% endblock %}
|
|
148
|
+
</div>
|
|
149
|
+
<div class="offcanvas-header justify-content-between align-items-start">
|
|
150
|
+
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" data-bs-target="#left-panel" ></button>
|
|
151
|
+
</div>
|
|
46
152
|
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
47
155
|
|
|
48
|
-
|
|
49
|
-
{%
|
|
156
|
+
</div>
|
|
157
|
+
{% endif %}
|
|
158
|
+
<div class="h-100 {% if self.left_panel() %} col-lg-10 {% else %} col-lg-12 {% endif %}">
|
|
159
|
+
<main class="h-100">
|
|
160
|
+
{% block content %}{% endblock %}
|
|
161
|
+
</main>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
50
164
|
|
|
165
|
+
</div>
|
|
51
166
|
</body>
|
|
52
167
|
</html>
|
platzky/templates/blog.html
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
{% endblock %}
|
|
10
10
|
|
|
11
11
|
{% block content %}
|
|
12
|
-
<div class="
|
|
12
|
+
<div class="blog-contents mx-auto w-75">
|
|
13
13
|
<h1>{% block title %}Blog {{ subtitle|default('', true) }} {% endblock %}</h1>
|
|
14
14
|
{% for post in posts %}
|
|
15
|
+
|
|
15
16
|
<div class="row align-items-center">
|
|
16
17
|
<div class="col-3">
|
|
17
|
-
<img src="{{ post.coverImage.
|
|
18
|
+
<img src="{{ post.coverImage.url }}" alt="{{ post.coverImage.alternateText }}" class="img-thumbnail align-middle">
|
|
18
19
|
</div>
|
|
19
20
|
<div class="col-8 mx-auto">
|
|
20
21
|
<div class="post-preview">
|
platzky/templates/body_meta.html
CHANGED
|
@@ -1,24 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-MRQ7FDB"
|
|
4
|
-
height="0" width="0" style="display:none;visibility:hidden"></iframe>
|
|
5
|
-
</noscript>
|
|
6
|
-
<!-- End Google Tag Manager (noscript) -->
|
|
1
|
+
{{ dynamic_body | safe }}
|
|
2
|
+
|
|
7
3
|
<!-- Optional JavaScript -->
|
|
8
4
|
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
|
|
9
|
-
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
|
|
10
|
-
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
|
|
11
|
-
crossorigin="anonymous"></script>
|
|
12
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.6/umd/popper.min.js"
|
|
13
|
-
integrity="sha384-wHAiFfRlMFy6i5SRaxvfOCifBUQy1xHdJ/yoi7FRNXMRBu5WHdZYu1hA6ZOblgut"
|
|
14
|
-
crossorigin="anonymous"></script>
|
|
15
|
-
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
|
|
16
|
-
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
|
|
17
|
-
crossorigin="anonymous"></script>
|
|
18
5
|
|
|
19
|
-
|
|
20
|
-
<
|
|
21
|
-
type="text/javascript"
|
|
22
|
-
src="https://cdnjs.cloudflare.com/ajax/libs/mdb-ui-kit/3.0.0/mdb.min.js"
|
|
23
|
-
></script>
|
|
24
|
-
<!-- End MDB -->
|
|
6
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
|
|
7
|
+
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" crossorigin="">
|
platzky/templates/feed.xml
CHANGED
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
<rss version="2.0">
|
|
3
3
|
|
|
4
4
|
<channel>
|
|
5
|
-
<title>{{_(
|
|
6
|
-
<link>{{ url_for("
|
|
5
|
+
<title>{{_(app_name)}}</title>
|
|
6
|
+
<link>{{ url_for("blog.all_posts", _external=True) }}</link>
|
|
7
7
|
<description>{{_("Everything about mailings and newsletters")}}</description>
|
|
8
8
|
{% for post in posts %}
|
|
9
9
|
<item>
|
|
10
10
|
<title>{{ post.title }}</title>
|
|
11
|
-
<link>{{ url_for("get_post", post_slug=post.slug, _external=True) }}</link>
|
|
11
|
+
<link>{{ url_for("blog.get_post", post_slug=post.slug, _external=True) }}</link>
|
|
12
12
|
<description>{{ post.excerpt }}</description>
|
|
13
13
|
<enclosure url="{{ post.coverImage.url }}" type="image/jpeg" />
|
|
14
|
-
<guid isPermaLink="false">{{ url_for("get_post", post_slug=post.slug, _external=True) }}</guid>
|
|
14
|
+
<guid isPermaLink="false">{{ url_for("blog.get_post", post_slug=post.slug, _external=True) }}</guid>
|
|
15
15
|
<pubDate>{{ post.createdAt }}</pubDate>
|
|
16
|
-
<source url="{{ url_for('get_post', post_slug=post.slug, _external=True) }}">{{_(
|
|
16
|
+
<source url="{{ url_for('blog.get_post', post_slug=post.slug, _external=True) }}">{{_(app_name)}} feed</source>
|
|
17
17
|
</item>
|
|
18
18
|
{% endfor %}
|
|
19
19
|
|
platzky/templates/head_meta.html
CHANGED
|
@@ -7,32 +7,22 @@
|
|
|
7
7
|
<link rel="canonical" href="{{ request.base_url }}"/>
|
|
8
8
|
|
|
9
9
|
<!-- Bootstrap CSS -->
|
|
10
|
-
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.
|
|
11
|
-
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
|
10
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
|
|
12
11
|
|
|
13
|
-
<!--
|
|
14
|
-
<link href="https://
|
|
15
|
-
<!-- End
|
|
12
|
+
<!-- Flag icons -->
|
|
13
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@6.6.6/css/flag-icons.min.css"/>
|
|
14
|
+
<!-- End Flag icons -->
|
|
16
15
|
|
|
17
16
|
<!-- Font Awesome -->
|
|
18
17
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/css/all.min.css" rel="stylesheet">
|
|
19
18
|
<!-- End Font Awesome -->
|
|
20
19
|
|
|
21
|
-
|
|
22
20
|
<!-- Custom fonts for this template -->
|
|
23
21
|
<link href='https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic' rel='stylesheet' type='text/css'>
|
|
24
22
|
<link
|
|
25
23
|
href='https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800'
|
|
26
24
|
rel='stylesheet' type='text/css'>
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
|
30
|
-
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
|
31
|
-
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
|
32
|
-
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
|
33
|
-
})(window,document,'script','dataLayer','GTM-MRQ7FDB');
|
|
34
|
-
</script>
|
|
35
|
-
<!-- End Google Tag Manager -->
|
|
36
|
-
|
|
26
|
+
{{ dynamic_head | safe }}
|
|
37
27
|
|
|
38
28
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='blog.css') }}">
|
platzky/templates/page.html
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<div class="row">
|
|
8
8
|
<div class="col-lg-8 col-md-10 mx-auto">
|
|
9
9
|
<div class="post-heading position-relative">
|
|
10
|
-
<h1>{% block title %}{{
|
|
10
|
+
<h1>{% block title %}{{ page.title }}{% endblock %}</h1>
|
|
11
11
|
</div>
|
|
12
12
|
</div>
|
|
13
13
|
</div>
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
<div class="container">
|
|
19
19
|
<div class="row">
|
|
20
20
|
<div class="col-lg-8 col-md-10 mx-auto post-content">
|
|
21
|
-
{{
|
|
21
|
+
{{page.contentInMarkdown | markdown}}
|
|
22
22
|
</div>
|
|
23
23
|
</div>
|
|
24
24
|
</div>
|