openwrite 0.10.1__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.
- openwrite/__init__.py +134 -0
- openwrite/cli.py +340 -0
- openwrite/db/__init__.py +0 -0
- openwrite/db/base.py +3 -0
- openwrite/launch_gemini.py +8 -0
- openwrite/routes/admin.py +170 -0
- openwrite/routes/auth.py +77 -0
- openwrite/routes/blog.py +205 -0
- openwrite/routes/dashboard.py +408 -0
- openwrite/routes/federation.py +284 -0
- openwrite/routes/main.py +96 -0
- openwrite/routes/upload.py +81 -0
- openwrite/static/IBMPlexSans-Regular.ttf +0 -0
- openwrite/static/Merriweather.ttf +0 -0
- openwrite/static/avatar.png +0 -0
- openwrite/static/favicon.ico +0 -0
- openwrite/static/logo.png +0 -0
- openwrite/static/script/chart.js +14 -0
- openwrite/static/script/highlight.min.js +1244 -0
- openwrite/static/script/openwrite.js +88 -0
- openwrite/static/script/papaparse.min.js +7 -0
- openwrite/static/script/showdown.min.js +3 -0
- openwrite/static/script/showdown.min.js.map +1 -0
- openwrite/static/script/site.compat.min.js +7 -0
- openwrite/static/script/site.min.js +7 -0
- openwrite/static/style/hl.css +1 -0
- openwrite/static/style/style.css +611 -0
- openwrite/static/style/themes/light.css +28 -0
- openwrite/static/style/themes/obscura.css +27 -0
- openwrite/static/style/themes/stickynote.css +50 -0
- openwrite/static/style/themes/terminal.css +37 -0
- openwrite/static/style/themes/warmnight.css +73 -0
- openwrite/static/style/themes/windows98.css +132 -0
- openwrite/templates/admin.html +47 -0
- openwrite/templates/blog.html +16 -0
- openwrite/templates/blog_footer.html +6 -0
- openwrite/templates/blog_header.html +45 -0
- openwrite/templates/blog_preview.html +44 -0
- openwrite/templates/changepw.html +19 -0
- openwrite/templates/create.html +46 -0
- openwrite/templates/dashboard.html +45 -0
- openwrite/templates/discover.html +11 -0
- openwrite/templates/edit.html +190 -0
- openwrite/templates/footer.html +16 -0
- openwrite/templates/header.html +61 -0
- openwrite/templates/import.html +142 -0
- openwrite/templates/index.html +9 -0
- openwrite/templates/instances.html +14 -0
- openwrite/templates/login.html +18 -0
- openwrite/templates/new_post.html +207 -0
- openwrite/templates/post.html +53 -0
- openwrite/templates/preview.html +37 -0
- openwrite/templates/register.html +42 -0
- openwrite/templates/settings.html +63 -0
- openwrite/templates/stats.html +242 -0
- openwrite/utils/create_db.py +15 -0
- openwrite/utils/db.py +30 -0
- openwrite/utils/helpers.py +216 -0
- openwrite/utils/i18n.json +229 -0
- openwrite/utils/models.py +87 -0
- openwrite/utils/translations.py +6 -0
- openwrite-0.10.1.dist-info/METADATA +684 -0
- openwrite-0.10.1.dist-info/RECORD +67 -0
- openwrite-0.10.1.dist-info/WHEEL +5 -0
- openwrite-0.10.1.dist-info/entry_points.txt +2 -0
- openwrite-0.10.1.dist-info/licenses/LICENSE +661 -0
- openwrite-0.10.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{% include 'blog_header.html' %}
|
|
2
|
+
<h1 class="post-title">{{ post.title }}</h1>
|
|
3
|
+
<i class="post-date"><span class="datetime">{{ post.date }}</span> {% if post.author | int > 0 %} by {{ post.authorname }} {% endif %}</i>
|
|
4
|
+
<article class="post-content">
|
|
5
|
+
{{ post.content_html | safe }}
|
|
6
|
+
</article>
|
|
7
|
+
<div class="flex row small-gap to-right">
|
|
8
|
+
<span>{{ post.likes }}</span>
|
|
9
|
+
<svg id="like" width="20px" height="20px" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--twemoji" preserveAspectRatio="xMidYMid meet"><path style="{% if post.liked > 0 %}fill: #DD2E44;{% else %}fill: none{% endif %}" stroke="#DD2E44" stroke-width="2" d="M35.885 11.833c0-5.45-4.418-9.868-9.867-9.868c-3.308 0-6.227 1.633-8.018 4.129c-1.791-2.496-4.71-4.129-8.017-4.129c-5.45 0-9.868 4.417-9.868 9.868c0 .772.098 1.52.266 2.241C1.751 22.587 11.216 31.568 18 34.034c6.783-2.466 16.249-11.447 17.617-19.959c.17-.721.268-1.469.268-2.242z"></path></svg>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
<script nonce="{{ g.nonce }}">
|
|
14
|
+
const svg = document.querySelector("#like");
|
|
15
|
+
svg.addEventListener("mouseover", function() {
|
|
16
|
+
this.children[0].style.fill = "#DD2E44";
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
svg.addEventListener("mouseout", function() {
|
|
20
|
+
if(this.classList.contains("liked") === false) {
|
|
21
|
+
this.children[0].style.fill = "none";
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
svg.addEventListener("click", function() {
|
|
26
|
+
fetch('https://{{ g.main_domain }}/like', {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: {
|
|
29
|
+
"Content-Type": "application/json"
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
blog: {{ blog.id }},
|
|
33
|
+
post: {{ post.id}}
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
.then(response => {
|
|
37
|
+
var likes = parseInt(this.parentElement.children[0].innerText);
|
|
38
|
+
if(response.status == 201) {
|
|
39
|
+
likes++;
|
|
40
|
+
this.parentElement.children[0].innerText = likes;
|
|
41
|
+
this.children[0].style.fill = "#DD2E44";
|
|
42
|
+
this.classList.add("liked");
|
|
43
|
+
} else if(response.status == 204) {
|
|
44
|
+
likes--;
|
|
45
|
+
this.parentElement.children[0].innerText = likes;
|
|
46
|
+
this.children[0].style.fill = "none";
|
|
47
|
+
this.classList.remove("liked");
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
{% include 'blog_footer.html' %}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>{{ data.title }} | {{ data.blog_title }}</title>
|
|
7
|
+
<meta name="description" content="Minimalist, opensource blog. Write, freely.">
|
|
8
|
+
<link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
|
|
9
|
+
<link rel="stylesheet" href="{{ url_for('static', filename='style/hl.css') }}">
|
|
10
|
+
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
|
11
|
+
<script src="{{ url_for('static', filename='highlight.min.js') }}"></script>
|
|
12
|
+
<script nonce="{{ g.nonce }}">hljs.highlightAll();</script>
|
|
13
|
+
<script src="{{ url_for('static', filename='openwrite.js') }}"></script>
|
|
14
|
+
{% if data.theme != "default" %}
|
|
15
|
+
<link rel="stylesheet" href="{{ url_for('static', filename='style/themes/'+data.theme) }}.css">
|
|
16
|
+
{% endif %}
|
|
17
|
+
</head>
|
|
18
|
+
<body>
|
|
19
|
+
<div class="container">
|
|
20
|
+
<header class="header">
|
|
21
|
+
<h1 class="blog-name"><a href="#"><span>{{ data.blog_title }}</span></a></h1>
|
|
22
|
+
<div id="menu">
|
|
23
|
+
</div>
|
|
24
|
+
</header>
|
|
25
|
+
<article class="blog-content">
|
|
26
|
+
<h1 class="post-title">{{ data.title }}</h1>
|
|
27
|
+
<i class="post-date"><span class="datetime">{{ data.date }}</span> {% if data.author | int > 0 %} by {{ data.author_name }} {% endif %}</i>
|
|
28
|
+
<article class="post-content">
|
|
29
|
+
{{ data.content | safe }}
|
|
30
|
+
</article>
|
|
31
|
+
</article>
|
|
32
|
+
</div>
|
|
33
|
+
<footer class="footer">
|
|
34
|
+
Written with ♡ on <a target="_blank" href="https://{{ g.main_domain }}/">{{ g.main_domain }}</a>
|
|
35
|
+
</footer>
|
|
36
|
+
</body>
|
|
37
|
+
</html>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{% include 'header.html' %}
|
|
2
|
+
|
|
3
|
+
<h1 class="centered">{% if add %}{{ g.trans['add_user'] }}{% else %}{{ g.trans['register_on_this_instance'] }}{% endif %}</h1>
|
|
4
|
+
|
|
5
|
+
<article class="main-content">
|
|
6
|
+
{% if error %}
|
|
7
|
+
<div class="error">{{ error }}</div>
|
|
8
|
+
{% endif %}
|
|
9
|
+
{% if message %}
|
|
10
|
+
<div class="message">{{ message }}</div>
|
|
11
|
+
{% endif %}
|
|
12
|
+
<form method="post" action="{% if add %}/admin/add_user{% else %}/register{% endif %}">
|
|
13
|
+
<span class="form-field">{{ g.trans["username"] }}:</span>
|
|
14
|
+
<input type="text" name="username">
|
|
15
|
+
<span class="form-field">{{ g.trans["password"] }}:</span>
|
|
16
|
+
<input type="password" name="password">
|
|
17
|
+
<span class="form-field">{{ g.trans["password2"] }}:</span>
|
|
18
|
+
<input type="password" name="password2">
|
|
19
|
+
<input type="submit" name="sub" value="{% if add %}{{ g.trans['add_user'] }}{% else %}{{ g.trans['register'] }}{% endif %}" class="btn purple" style="margin-top: 1rem;">
|
|
20
|
+
{% if g.captcha and captcha != "0" %}
|
|
21
|
+
<div class="frc-captcha" data-sitekey="{{ g.fcaptcha_sitekey }}"></div>
|
|
22
|
+
{% endif %}
|
|
23
|
+
</form>
|
|
24
|
+
</article>
|
|
25
|
+
<script nonce="{{ g.nonce }}">
|
|
26
|
+
const form = document.querySelector("form");
|
|
27
|
+
const submit = form.querySelector('input[name="sub"]');
|
|
28
|
+
|
|
29
|
+
const username = form.querySelector('input[name="username"]');
|
|
30
|
+
const password = form.querySelector('input[name="password"]');
|
|
31
|
+
const password2 = form.querySelector('input[name="password2"]');
|
|
32
|
+
|
|
33
|
+
username.addEventListener("input", function() {
|
|
34
|
+
if((this.value.length > 29) || (this.value.match(/^[a-zA-Z0-9](?:[a-zA-Z0-9_-]{1,28}[a-zA-Z0-9])?$/) == null)) {
|
|
35
|
+
this.style.border = "1px solid #ff0000";
|
|
36
|
+
} else {
|
|
37
|
+
this.style.border = "";
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
</script>
|
|
42
|
+
{% include 'footer.html' %}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{% include 'header.html' %}
|
|
2
|
+
{% if error %}
|
|
3
|
+
<div class="error">{{ error }}</div>
|
|
4
|
+
{% endif %}
|
|
5
|
+
<h1 class="centered">{{ g.trans['settings'] }}</h1>
|
|
6
|
+
<form method="post" action="/admin/settings" enctype="multipart/form-data">
|
|
7
|
+
<h2>Logo</h2>
|
|
8
|
+
<img src="{{ settings['logo'] }}" style="max-width: 400px" id="logo">
|
|
9
|
+
<h3>Update logo</h3><input type="file" class="file_input" name="logo"><br><br>
|
|
10
|
+
<h2>Homepage text (HTML)</h2>
|
|
11
|
+
<div class="bloginfo">
|
|
12
|
+
<div style="display: flex; flex-direction: row; gap: 5px; align-items: center;">
|
|
13
|
+
{% for text in hometext %}
|
|
14
|
+
{% if text.language == "en" %}
|
|
15
|
+
<a class="small btn empty active" href="#" id="{{ text.language }}" data-action="{{ text.language }}">{{ text.language }}</a>
|
|
16
|
+
{% endif %}
|
|
17
|
+
{% endfor %}
|
|
18
|
+
{% for text in hometext %}
|
|
19
|
+
{% if text.language != "en" %}
|
|
20
|
+
<a class="small btn empty" href="#" id="{{ text.language }}" data-action="{{ text.language }}">{{ text.language }}</a>
|
|
21
|
+
{% endif %}
|
|
22
|
+
{% endfor %}
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
{% for text in hometext %}
|
|
26
|
+
{% if text.language == "en" %}
|
|
27
|
+
<textarea id="en" class="post_content" name="hometext_en" autocomplete="off" autocapitalize="none" autocorrect="off" spellcheck="false">{{ text.content }}</textarea>
|
|
28
|
+
{% endif %}
|
|
29
|
+
{% endfor %}
|
|
30
|
+
|
|
31
|
+
{% for text in hometext %}
|
|
32
|
+
{% if text.language != "en" %}
|
|
33
|
+
<textarea id="{{ text.language }}" class="post_content" style="display: none;" name="hometext_{{ text.language }}" autocomplete="off" autocapitalize="none" autocorrect="off" spellcheck="false">{{ text.content }}</textarea>
|
|
34
|
+
{% endif %}
|
|
35
|
+
{% endfor %}
|
|
36
|
+
|
|
37
|
+
<input type="submit" class="btn purple" value="Save">
|
|
38
|
+
</form>
|
|
39
|
+
|
|
40
|
+
<script>
|
|
41
|
+
|
|
42
|
+
document.querySelectorAll('[data-action]').forEach(btn => {
|
|
43
|
+
btn.addEventListener('click', function(e) {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
const action = this.getAttribute('data-action');
|
|
46
|
+
document.querySelectorAll('textarea:not(#' + action + ')').forEach(area => {
|
|
47
|
+
area.style.display = "none";
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
document.querySelector('textarea#' + action).style.display = "block";
|
|
51
|
+
|
|
52
|
+
document.querySelectorAll('[data-action]').forEach(btn2 => {
|
|
53
|
+
btn2.classList.remove("active");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
btn.classList.add("active");
|
|
57
|
+
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
{% include 'footer.html' %}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
{% include 'header.html' %}
|
|
2
|
+
<div><a href="/dashboard" class="action">Dashboard</a> > {{ blog.title }} - {{ g.trans['statistics'] }}</div>
|
|
3
|
+
<h1 class="centered">{{ g.trans['statistics'] }}</h1>
|
|
4
|
+
Post:
|
|
5
|
+
<select id="post" style="min-width: 14rem; min-height: 2rem; color: #fff; background: #211932;">
|
|
6
|
+
{% for post in posts %}
|
|
7
|
+
<option value="{{ post.id }}">{{ post.title }}</option>
|
|
8
|
+
{% endfor %}
|
|
9
|
+
</select>
|
|
10
|
+
<div class="buttons-list" style="margin-bottom: 2rem">
|
|
11
|
+
<a href="#" data-action="24" class="btn empty">1 day</a>
|
|
12
|
+
<a href="#" data-action="168" class="btn empty">7 days</a>
|
|
13
|
+
<a href="#" data-action="720" class="btn empty">30 days</a>
|
|
14
|
+
<a href="#" data-action="2160" class="btn empty">90 days</a>
|
|
15
|
+
</div>
|
|
16
|
+
<h3>{{ g.trans['views'] }}</h3>
|
|
17
|
+
<canvas id="stats" width="400" height="200"></canvas>
|
|
18
|
+
<br>
|
|
19
|
+
<h3>OS</h3>
|
|
20
|
+
<canvas id="os" width="400" height="200"></canvas>
|
|
21
|
+
<br>
|
|
22
|
+
<h3>Browsers</h3>
|
|
23
|
+
<canvas id="browsers" width="400" height="200"></canvas>
|
|
24
|
+
<script>
|
|
25
|
+
|
|
26
|
+
const ctx = document.getElementById("stats").getContext("2d");
|
|
27
|
+
const os_ctx = document.getElementById("os").getContext("2d");
|
|
28
|
+
const br_ctx = document.getElementById("browsers").getContext("2d");
|
|
29
|
+
|
|
30
|
+
const chart = new Chart(ctx, {
|
|
31
|
+
type: 'line',
|
|
32
|
+
data: {
|
|
33
|
+
labels: [],
|
|
34
|
+
datasets: [
|
|
35
|
+
{
|
|
36
|
+
label: '{{ g.trans['views'] }}',
|
|
37
|
+
data: [],
|
|
38
|
+
borderColor: 'rgba(75, 192, 192, 1)',
|
|
39
|
+
borderWidth: 1
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
options: {
|
|
44
|
+
scales: {
|
|
45
|
+
y: {
|
|
46
|
+
beginAtZero: true
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const os_chart = new Chart(os_ctx, {
|
|
53
|
+
type: 'bar',
|
|
54
|
+
data: {
|
|
55
|
+
labels: [],
|
|
56
|
+
datasets: [
|
|
57
|
+
{
|
|
58
|
+
label: 'OS',
|
|
59
|
+
data: [],
|
|
60
|
+
backgroundColor: [
|
|
61
|
+
'rgba(255, 99, 132, 0.3)',
|
|
62
|
+
'rgba(255, 159, 64, 0.3)',
|
|
63
|
+
'rgba(255, 205, 86, 0.3)',
|
|
64
|
+
'rgba(75, 192, 192, 0.3)',
|
|
65
|
+
'rgba(54, 162, 235, 0.3)',
|
|
66
|
+
'rgba(153, 102, 255, 0.3)',
|
|
67
|
+
'rgba(201, 203, 207, 0.3)'
|
|
68
|
+
],
|
|
69
|
+
borderColor: [
|
|
70
|
+
'rgb(255, 99, 132)',
|
|
71
|
+
'rgb(255, 159, 64)',
|
|
72
|
+
'rgb(255, 205, 86)',
|
|
73
|
+
'rgb(75, 192, 192)',
|
|
74
|
+
'rgb(54, 162, 235)',
|
|
75
|
+
'rgb(153, 102, 255)',
|
|
76
|
+
'rgb(201, 203, 207)'
|
|
77
|
+
],
|
|
78
|
+
borderWidth: 1
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
options: {
|
|
83
|
+
scales: {
|
|
84
|
+
y: {
|
|
85
|
+
beginAtZero: true
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const br_chart = new Chart(br_ctx, {
|
|
92
|
+
type: 'bar',
|
|
93
|
+
data: {
|
|
94
|
+
labels: [],
|
|
95
|
+
datasets: [
|
|
96
|
+
{
|
|
97
|
+
label: 'Browsers',
|
|
98
|
+
data: [],
|
|
99
|
+
backgroundColor: [
|
|
100
|
+
'rgba(54, 162, 235, 0.3)',
|
|
101
|
+
'rgba(153, 102, 255, 0.3)',
|
|
102
|
+
'rgba(201, 203, 207, 0.3)',
|
|
103
|
+
'rgba(255, 99, 132, 0.3)',
|
|
104
|
+
'rgba(255, 159, 64, 0.3)',
|
|
105
|
+
'rgba(255, 205, 86, 0.3)',
|
|
106
|
+
'rgba(75, 192, 192, 0.3)'
|
|
107
|
+
],
|
|
108
|
+
borderColor: [
|
|
109
|
+
'rgb(54, 162, 235)',
|
|
110
|
+
'rgb(153, 102, 255)',
|
|
111
|
+
'rgb(201, 203, 207)',
|
|
112
|
+
'rgb(255, 99, 132)',
|
|
113
|
+
'rgb(255, 159, 64)',
|
|
114
|
+
'rgb(255, 205, 86)',
|
|
115
|
+
'rgb(75, 192, 192)'
|
|
116
|
+
],
|
|
117
|
+
borderWidth: 1
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
},
|
|
121
|
+
options: {
|
|
122
|
+
scales: {
|
|
123
|
+
y: {
|
|
124
|
+
beginAtZero: true
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
function update_chart(action, response) {
|
|
131
|
+
const views = response.views;
|
|
132
|
+
var elements = 0;
|
|
133
|
+
var interval = 0;
|
|
134
|
+
var format = {};
|
|
135
|
+
if(action == "24") {
|
|
136
|
+
elements = 24 * 60 / 20;
|
|
137
|
+
interval = 1000 * 60 * 20;
|
|
138
|
+
format = { hour: '2-digit', minute: '2-digit' };
|
|
139
|
+
} else if(action == "168") {
|
|
140
|
+
elements = 84; // 168 / 2
|
|
141
|
+
interval = 1000 * 60 * 120;
|
|
142
|
+
format = { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' };
|
|
143
|
+
} else if(action == "720") {
|
|
144
|
+
elements = 60; // every 12 hours
|
|
145
|
+
interval = 1000 * 60 * 60 * 12;
|
|
146
|
+
format = { day: '2-digit', month: '2-digit' };
|
|
147
|
+
} else if(action == "2160") {
|
|
148
|
+
elements = 90;
|
|
149
|
+
interval = 1000 * 60 * 60 * 24;
|
|
150
|
+
format = { day: '2-digit', month: '2-digit' };
|
|
151
|
+
}
|
|
152
|
+
views.sort((a, b) => new Date(a[0]) - new Date(b[0]));
|
|
153
|
+
const cumulative = [];
|
|
154
|
+
let totalViews = response.start_from || 0;
|
|
155
|
+
let currentIndex = 0;
|
|
156
|
+
var labels = [];
|
|
157
|
+
const now = new Date();
|
|
158
|
+
for (let i = elements; i >= 0; i--) {
|
|
159
|
+
const bucketDate = new Date(now.getTime() - i * interval);
|
|
160
|
+
labels.push(bucketDate.toLocaleTimeString([], format));
|
|
161
|
+
|
|
162
|
+
while (currentIndex < views.length && new Date(views[currentIndex][0]) <= bucketDate) {
|
|
163
|
+
totalViews++;
|
|
164
|
+
currentIndex++;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
cumulative.push(totalViews);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
chart.data.labels = labels;
|
|
172
|
+
chart.data.datasets[0].data = cumulative;
|
|
173
|
+
chart.update();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function update_os_chart(response) {
|
|
177
|
+
const views = response.views;
|
|
178
|
+
const oss = {};
|
|
179
|
+
const browsers = {};
|
|
180
|
+
views.forEach(v => {
|
|
181
|
+
oss[v[1]] = oss[v[1]] + 1 || 1;
|
|
182
|
+
browsers[v[2]] = browsers[v[2]] + 1 || 1;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
os_chart.data.labels = Object.keys(oss);
|
|
186
|
+
os_chart.data.datasets[0].data = Object.values(oss);
|
|
187
|
+
br_chart.data.labels = Object.keys(browsers);
|
|
188
|
+
br_chart.data.datasets[0].data = Object.values(browsers);
|
|
189
|
+
|
|
190
|
+
os_chart.update();
|
|
191
|
+
br_chart.update();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
document.querySelectorAll('[data-action]').forEach(btn => {
|
|
195
|
+
btn.addEventListener('click', function(e) {
|
|
196
|
+
e.preventDefault();
|
|
197
|
+
document.querySelectorAll('[data-action]').forEach(bt => {
|
|
198
|
+
bt.classList.remove("active");
|
|
199
|
+
});
|
|
200
|
+
this.classList.add("active");
|
|
201
|
+
var views = [];
|
|
202
|
+
var e = document.getElementById("post");
|
|
203
|
+
var post_id = e.options[e.selectedIndex].value;
|
|
204
|
+
const action = this.getAttribute('data-action');
|
|
205
|
+
|
|
206
|
+
fetch('/dashboard/get_stats/{{ blog.id }}/' + post_id + '/' + action, {
|
|
207
|
+
method: "GET"
|
|
208
|
+
}).then(response => {
|
|
209
|
+
if(response.ok) {
|
|
210
|
+
return response.json();
|
|
211
|
+
}
|
|
212
|
+
}).then(json => {
|
|
213
|
+
update_chart(action, json);
|
|
214
|
+
update_os_chart(json);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
document.getElementById("post").addEventListener("change", function() {
|
|
220
|
+
var post_id = this.options[this.selectedIndex].value;
|
|
221
|
+
var action = 0;
|
|
222
|
+
document.querySelectorAll('[data-action]').forEach(btn => {
|
|
223
|
+
if(btn.classList.contains("active")) {
|
|
224
|
+
action = btn.getAttribute("data-action");
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
if(action != 0) {
|
|
228
|
+
fetch('/dashboard/get_stats/{{ blog.id }}/' + post_id + '/' + action, {
|
|
229
|
+
method: "GET"
|
|
230
|
+
}).then(response => {
|
|
231
|
+
if(response.ok) {
|
|
232
|
+
return response.json();
|
|
233
|
+
}
|
|
234
|
+
}).then(json => {
|
|
235
|
+
update_chart(action, json);
|
|
236
|
+
update_os_chart(json);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
</script>
|
|
242
|
+
{% include 'footer.html' %}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .db import init_engine
|
|
2
|
+
from .models import User, Blog, Post, View, Settings, Home, Like, Settings
|
|
3
|
+
from openwrite.db.base import Base
|
|
4
|
+
from sqlalchemy import create_engine
|
|
5
|
+
from sqlalchemy.orm import sessionmaker, declarative_base
|
|
6
|
+
from sqlalchemy.pool import StaticPool
|
|
7
|
+
import os
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def init_db(dbtype, dbpath):
|
|
13
|
+
engine = init_engine(dbtype, dbpath)
|
|
14
|
+
Base.metadata.create_all(bind=engine)
|
|
15
|
+
|
openwrite/utils/db.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from dotenv import load_dotenv
|
|
2
|
+
from sqlalchemy import create_engine
|
|
3
|
+
from sqlalchemy.orm import sessionmaker, declarative_base, scoped_session
|
|
4
|
+
from sqlalchemy.pool import StaticPool
|
|
5
|
+
import os
|
|
6
|
+
import json
|
|
7
|
+
from openwrite.db.base import Base
|
|
8
|
+
|
|
9
|
+
SessionLocal = None
|
|
10
|
+
engine = None
|
|
11
|
+
|
|
12
|
+
def init_engine(dbtype, dbpath):
|
|
13
|
+
global SessionLocal, engine
|
|
14
|
+
load_dotenv()
|
|
15
|
+
|
|
16
|
+
pwd = os.path.dirname(os.path.realpath(__file__))
|
|
17
|
+
|
|
18
|
+
if dbtype == "sqlite":
|
|
19
|
+
DB_URL = f"sqlite:///{dbpath}"
|
|
20
|
+
engine = create_engine(DB_URL, echo=False, future=True, connect_args={"check_same_thread": False}, poolclass=StaticPool)
|
|
21
|
+
elif dbtype == "mysql":
|
|
22
|
+
DB_URL = dbpath
|
|
23
|
+
engine = create_engine(DB_URL, echo=False, future=True, pool_size=20, max_overflow=30, pool_timeout=10, pool_recycle=1800)
|
|
24
|
+
else:
|
|
25
|
+
raise ValueError("Unsupported DB_TYPE. Use 'sqlite' or 'mysql'.")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
SessionLocal = scoped_session(sessionmaker(bind=engine, autocommit=False, autoflush=False))
|
|
29
|
+
return engine
|
|
30
|
+
|