openwrite 0.11.0__tar.gz → 0.12.0__tar.gz

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.
Files changed (82) hide show
  1. {openwrite-0.11.0/openwrite.egg-info → openwrite-0.12.0}/PKG-INFO +4 -1
  2. {openwrite-0.11.0 → openwrite-0.12.0}/README.md +3 -0
  3. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/__init__.py +14 -3
  4. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/routes/admin.py +49 -1
  5. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/routes/blog.py +55 -11
  6. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/routes/dashboard.py +166 -12
  7. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/routes/main.py +8 -2
  8. openwrite-0.12.0/openwrite/static/emoji.json +15627 -0
  9. openwrite-0.12.0/openwrite/static/script/nativeEmoji.js +389 -0
  10. openwrite-0.12.0/openwrite/static/style/nativeEmoji.min.css +1 -0
  11. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/static/style/style.css +27 -2
  12. openwrite-0.12.0/openwrite/static/style/themes/cyberpunk.css +391 -0
  13. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/static/style/themes/light.css +11 -1
  14. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/static/style/themes/stickynote.css +11 -1
  15. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/static/style/themes/terminal.css +9 -0
  16. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/static/style/themes/warmnight.css +5 -0
  17. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/static/style/themes/windows98.css +58 -3
  18. openwrite-0.12.0/openwrite/templates/blog.html +18 -0
  19. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/templates/blog_header.html +16 -1
  20. openwrite-0.12.0/openwrite/templates/blog_preview.html +57 -0
  21. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/templates/dashboard.html +1 -1
  22. openwrite-0.12.0/openwrite/templates/discover.html +11 -0
  23. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/templates/edit.html +38 -52
  24. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/templates/header.html +4 -0
  25. openwrite-0.12.0/openwrite/templates/new_page.html +218 -0
  26. openwrite-0.11.0/openwrite/templates/blog_preview.html → openwrite-0.12.0/openwrite/templates/page_preview.html +15 -11
  27. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/templates/translations.html +1 -0
  28. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/utils/create_db.py +1 -1
  29. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/utils/models.py +15 -0
  30. {openwrite-0.11.0 → openwrite-0.12.0/openwrite.egg-info}/PKG-INFO +4 -1
  31. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite.egg-info/SOURCES.txt +6 -0
  32. {openwrite-0.11.0 → openwrite-0.12.0}/pyproject.toml +1 -1
  33. openwrite-0.11.0/openwrite/templates/blog.html +0 -16
  34. openwrite-0.11.0/openwrite/templates/discover.html +0 -11
  35. {openwrite-0.11.0 → openwrite-0.12.0}/LICENSE +0 -0
  36. {openwrite-0.11.0 → openwrite-0.12.0}/MANIFEST.in +0 -0
  37. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/cli.py +0 -0
  38. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/db/__init__.py +0 -0
  39. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/db/base.py +0 -0
  40. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/gemini_multi.py +0 -0
  41. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/gemini_single.py +0 -0
  42. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/routes/auth.py +0 -0
  43. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/routes/federation.py +0 -0
  44. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/routes/upload.py +0 -0
  45. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/static/IBMPlexSans-Regular.ttf +0 -0
  46. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/static/Merriweather.ttf +0 -0
  47. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/static/avatar.png +0 -0
  48. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/static/favicon.ico +0 -0
  49. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/static/logo.png +0 -0
  50. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/static/script/chart.js +0 -0
  51. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/static/script/highlight.min.js +0 -0
  52. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/static/script/openwrite.js +0 -0
  53. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/static/script/papaparse.min.js +0 -0
  54. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/static/script/showdown.min.js +0 -0
  55. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/static/script/showdown.min.js.map +0 -0
  56. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/static/script/site.compat.min.js +0 -0
  57. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/static/script/site.min.js +0 -0
  58. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/static/style/hl.css +0 -0
  59. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/static/style/themes/obscura.css +0 -0
  60. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/templates/admin.html +0 -0
  61. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/templates/blog_footer.html +0 -0
  62. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/templates/changepw.html +0 -0
  63. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/templates/create.html +0 -0
  64. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/templates/footer.html +0 -0
  65. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/templates/import.html +0 -0
  66. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/templates/index.html +0 -0
  67. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/templates/instances.html +0 -0
  68. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/templates/login.html +0 -0
  69. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/templates/new_post.html +0 -0
  70. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/templates/post.html +0 -0
  71. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/templates/preview.html +0 -0
  72. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/templates/register.html +0 -0
  73. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/templates/settings.html +0 -0
  74. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/templates/stats.html +0 -0
  75. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/utils/db.py +0 -0
  76. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/utils/helpers.py +0 -0
  77. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite/utils/i18n.json +0 -0
  78. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite.egg-info/dependency_links.txt +0 -0
  79. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite.egg-info/entry_points.txt +0 -0
  80. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite.egg-info/requires.txt +0 -0
  81. {openwrite-0.11.0 → openwrite-0.12.0}/openwrite.egg-info/top_level.txt +0 -0
  82. {openwrite-0.11.0 → openwrite-0.12.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openwrite
3
- Version: 0.11.0
3
+ Version: 0.12.0
4
4
  License: GNU AFFERO GENERAL PUBLIC LICENSE
5
5
  Version 3, 19 November 2007
6
6
 
@@ -690,6 +690,7 @@ Dynamic: license-file
690
690
  ![logo](https://openwrite.io/static/logo.png)
691
691
  openwrite is a minimalist blogging platform built for writing freely, hosting independently, and publishing without noise.
692
692
 
693
+ ![PyPI - Downloads](https://img.shields.io/pypi/dw/openwrite)
693
694
  ![PyPI - Version](https://img.shields.io/pypi/v/openwrite)
694
695
  [![License: AGPL-3.0-or-later](https://img.shields.io/badge/License-AGPL--3.0--or--later-blue.svg)](LICENSE)
695
696
  ![pythonver](https://img.shields.io/badge/python%20version-3.11-blue)
@@ -719,6 +720,8 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
719
720
  - Gemini protocol
720
721
  - Statistics per post
721
722
  - Multilanguage (currently English and Polish)
723
+ - Custom favicon per blog
724
+ - Subpages per blog
722
725
 
723
726
 
724
727
  ## Installation
@@ -1,6 +1,7 @@
1
1
  ![logo](https://openwrite.io/static/logo.png)
2
2
  openwrite is a minimalist blogging platform built for writing freely, hosting independently, and publishing without noise.
3
3
 
4
+ ![PyPI - Downloads](https://img.shields.io/pypi/dw/openwrite)
4
5
  ![PyPI - Version](https://img.shields.io/pypi/v/openwrite)
5
6
  [![License: AGPL-3.0-or-later](https://img.shields.io/badge/License-AGPL--3.0--or--later-blue.svg)](LICENSE)
6
7
  ![pythonver](https://img.shields.io/badge/python%20version-3.11-blue)
@@ -30,6 +31,8 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
30
31
  - Gemini protocol
31
32
  - Statistics per post
32
33
  - Multilanguage (currently English and Polish)
34
+ - Custom favicon per blog
35
+ - Subpages per blog
33
36
 
34
37
 
35
38
  ## Installation
@@ -32,7 +32,7 @@ def create_app(test_config=None):
32
32
  file_cwd = "/".join(f_abs_path.split("/")[:-1])
33
33
  from .utils.create_db import init_db
34
34
  init_db(db_type, db_path)
35
- from .utils.models import User, Blog, Post, Settings, Home
35
+ from .utils.models import User, Blog, Post, Settings, Home, Page
36
36
  from .utils.db import init_engine, SessionLocal
37
37
  init_engine(db_type, db_path)
38
38
  from .utils.db import SessionLocal
@@ -68,15 +68,26 @@ def create_app(test_config=None):
68
68
  title=domain,
69
69
  index="on",
70
70
  access="domain",
71
- description_raw=f"![hello](https://openwrite.b-cdn.net/hello.jpg =500x258)\n\n# Hello there! 👋\n\nYou can edit your blog description in [dashboard](http://{domain}/dashboard/edit/default)",
72
- description_html=f"<p><img src=\"https://openwrite.b-cdn.net/hello.jpg\" width=\"500\" height=\"258\"></p><h1>Hello there! 👋</h1><p>You can edit your blog description in <a href=\"http://{domain}/dashboard/edit/default\">dashboard</a></p>",
73
71
  css="",
72
+ description_raw="x",
73
+ description_html="x",
74
74
  pub_key=public_pem,
75
75
  priv_key=private_pem,
76
76
  theme="default",
77
+ favicon="",
77
78
  created=now
78
79
  )
79
80
  SessionLocal.add(new_blog)
81
+
82
+ new_page = Page(
83
+ blog=1,
84
+ name="Home",
85
+ url="",
86
+ content_raw=f"![hello](https://openwrite.b-cdn.net/hello.jpg =500x258)\n\n# Hello there! 👋\n\nYou can edit your blog home page in [dashboard](https://{domain}/dashboard/page/1/edit)\n\n---\n### Posts\n\n{{posts}}",
87
+ content_html=f"<p><img src=\"https://openwrite.b-cdn.net/hello.jpg\" width=\"500\" height=\"258\"></p><h1>Hello there! 👋</h1><p>You can edit your blog home page in <a href=\"https://{domain}/dashboard/page/1/edit\">dashboard</a></p>\n\n<hr>\n<h3>Posts\n\n{{posts}}",
88
+ show="off"
89
+ )
90
+ SessionLocal.add(new_page)
80
91
 
81
92
  hometext_pl = """
82
93
  <h1 class="centered"><span style="color: #f2d2c8">cicha</span> przestrzeń na <span style="color: #5a36bf">głośne</span> myśli.</h1>
@@ -1,4 +1,5 @@
1
- from flask import Blueprint, render_template, redirect, g, request
1
+ from flask import Blueprint, render_template, redirect, g, request, make_response
2
+ import json
2
3
  from openwrite.utils.models import Blog, User, Settings, Home
3
4
  import re
4
5
  import bcrypt
@@ -13,6 +14,9 @@ ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "webp"}
13
14
 
14
15
  @admin_bp.route("/admin")
15
16
  def admin():
17
+ """
18
+ Admin dashboard to manage blogs and users.
19
+ """
16
20
  if g.user is None or g.isadmin == 0 or g.mode == "single":
17
21
  return redirect("/")
18
22
 
@@ -26,6 +30,9 @@ def admin():
26
30
 
27
31
  @admin_bp.route("/admin/delete_blog/<blog>")
28
32
  def admin_delete_blog(blog):
33
+ """
34
+ Delete a blog by its name.
35
+ """
29
36
  if g.user is None or g.isadmin == 0 or g.mode == "single":
30
37
  return redirect("/")
31
38
 
@@ -38,6 +45,9 @@ def admin_delete_blog(blog):
38
45
 
39
46
  @admin_bp.route("/admin/delete_user/<username>")
40
47
  def admin_delete_user(username):
48
+ """
49
+ Delete a user and all their blogs.
50
+ """
41
51
  if g.user is None or g.isadmin == 0 or g.mode == "single":
42
52
  return redirect("/")
43
53
 
@@ -59,6 +69,9 @@ def admin_delete_user(username):
59
69
 
60
70
  @admin_bp.route("/admin/make_admin/<username>")
61
71
  def admin_make_admin(username):
72
+ """
73
+ Grant admin privileges to a user.
74
+ """
62
75
  if g.user is None or g.isadmin == 0 or g.mode == "single":
63
76
  return redirect("/")
64
77
 
@@ -73,6 +86,9 @@ def admin_make_admin(username):
73
86
 
74
87
  @admin_bp.route("/admin/remove_admin/<username>")
75
88
  def admin_remove_admin(username):
89
+ """
90
+ Remove admin privileges from a user.
91
+ """
76
92
  if g.user is None or g.isadmin == 0 or g.mode == "single":
77
93
  return redirect("/")
78
94
 
@@ -90,6 +106,9 @@ def admin_remove_admin(username):
90
106
 
91
107
  @admin_bp.route("/admin/add_user", methods=['GET', 'POST'])
92
108
  def admin_add_user():
109
+ """
110
+ Add a new user to the system.
111
+ """
93
112
  if g.user is None or g.isadmin == 0 or g.mode == "single":
94
113
  return redirect("/")
95
114
  if request.method == "GET":
@@ -122,6 +141,9 @@ def admin_add_user():
122
141
 
123
142
  @admin_bp.route("/admin/settings", methods=['GET', 'POST'])
124
143
  def admin_settings():
144
+ """
145
+ Manage application settings.
146
+ """
125
147
  if g.user is None or g.isadmin == 0 or g.mode == "single":
126
148
  return redirect("/")
127
149
 
@@ -174,6 +196,14 @@ def admin_settings():
174
196
 
175
197
  @admin_bp.route("/admin/translations", methods=['GET', 'POST'])
176
198
  def translations():
199
+ """
200
+ Manage translations for the application.
201
+ Allows viewing, editing, and saving translations.
202
+ (I can't believe I didn't secured it earlier lol)
203
+ """
204
+ if g.user is None or g.isadmin == 0 or g.mode == "single":
205
+ return redirect("/")
206
+
177
207
  if request.method == "GET":
178
208
  trans = g.db.query(Home).filter_by(type="translation").all()
179
209
  grouped = defaultdict(dict)
@@ -198,3 +228,21 @@ def translations():
198
228
 
199
229
  g.db.commit()
200
230
  return redirect("/admin/translations")
231
+
232
+ @admin_bp.route("/admin/download_translations")
233
+ def download_translations():
234
+ """
235
+ Download all translations as a json file.
236
+ """
237
+ if g.user is None or g.isadmin == 0 or g.mode == "single":
238
+ return redirect("/")
239
+
240
+ translations = g.db.query(Home).filter_by(type="translation").all()
241
+ trans_dict = defaultdict(dict)
242
+ for t in translations:
243
+ trans_dict[t.language][t.name] = t.content
244
+
245
+ response = make_response(json.dumps(dict(trans_dict), ensure_ascii=False))
246
+ response.headers["Content-Disposition"] = "attachment; filename=i18n.json"
247
+ response.headers["Content-Type"] = "application/json"
248
+ return response
@@ -1,5 +1,5 @@
1
1
  from flask import Blueprint, render_template, redirect, request, g, Response, abort
2
- from openwrite.utils.models import Blog, Post, User, View, Like
2
+ from openwrite.utils.models import Blog, Post, User, View, Like, Page
3
3
  from openwrite.utils.helpers import gen_link, sanitize_html, anonymize, get_ip
4
4
  from feedgen.feed import FeedGenerator
5
5
  from sqlalchemy import desc
@@ -20,10 +20,14 @@ def show_blog(blog):
20
20
  if blog.access == "domain":
21
21
  return redirect(f"https://{blog.name}.{os.getenv('DOMAIN')}/")
22
22
 
23
- posts = g.db.query(Post).filter_by(blog=blog.id).order_by(desc(Post.id)).all()
23
+ pages = g.db.query(Page).filter_by(blog=blog.id).all()
24
24
  blog.url = f"/b/{blog.name}"
25
+ homepage = g.db.query(Page).filter(Page.blog == blog.id, Page.url == "").first()
26
+ if "{posts}" in homepage.content_raw:
27
+ posts = g.db.query(Post).filter_by(blog=blog.id).order_by(desc(Post.id)).all()
28
+ return render_template("blog.html", blog=blog, page=homepage, posts=posts, pages=pages)
25
29
 
26
- return render_template("blog.html", blog=blog, posts=posts)
30
+ return render_template("blog.html", blog=blog, page=homepage, pages=pages)
27
31
 
28
32
  @blog_bp.route("/", subdomain="<blog>")
29
33
  def show_subblog(blog):
@@ -36,9 +40,13 @@ def show_subblog(blog):
36
40
  if blog.access == "path":
37
41
  return redirect(f"https://{os.getenv('DOMAIN')}/b/{blog.name}")
38
42
 
39
- posts = g.db.query(Post).filter_by(blog=blog.id).order_by(desc(Post.id)).all()
43
+ pages = g.db.query(Page).filter_by(blog=blog.id).all()
44
+ homepage = g.db.query(Page).filter(Page.blog == blog.id, Page.url == "").first()
45
+ if "{posts}" in homepage.content_raw:
46
+ posts = g.db.query(Post).filter_by(blog=blog.id).order_by(desc(Post.id)).all()
47
+ return render_template("blog.html", blog=blog, page=homepage, posts=posts, pages=pages)
40
48
 
41
- return render_template("blog.html", blog=blog, posts=posts)
49
+ return render_template("blog.html", blog=blog, page=homepage, pages=pages)
42
50
 
43
51
  @blog_bp.route("/b/<blog>/<post>")
44
52
  def show_post(blog, post):
@@ -54,9 +62,21 @@ def show_post(blog, post):
54
62
  if blog.access == "domain":
55
63
  return redirect(f"https://{blog.name}.{os.getenv('DOMAIN')}/{post}")
56
64
 
65
+ blog.url = f"/b/{blog.name}"
66
+ pages = g.db.query(Page).filter_by(blog=blog.id).all()
57
67
  one_post = g.db.query(Post).filter(Post.blog == blog.id, Post.link == post).first()
58
68
  if not one_post:
59
- return redirect("/")
69
+ page = g.db.query(Page).filter(Page.blog == blog.id, Page.url == post).first()
70
+ if not page:
71
+ return redirect("/")
72
+
73
+ if "{posts}" in page.content_raw:
74
+ posts = g.db.query(Post).filter_by(blog=blog.id).order_by(desc(Post.id)).all()
75
+
76
+ return render_template("blog.html", blog=blog, page=page, posts=posts, pages=pages)
77
+
78
+ return render_template("blog.html", blog=blog, page=page, pages=pages)
79
+
60
80
 
61
81
  post_author = g.db.query(User).filter_by(id=blog.owner).first()
62
82
  one_post.authorname = post_author.username
@@ -76,7 +96,7 @@ def show_post(blog, post):
76
96
  one_post.liked = liked
77
97
 
78
98
  user = g.db.query(User).filter_by(id=g.user) if g.user else None
79
- return render_template("post.html", blog=blog, post=one_post, user=user, views=v, likes=likes)
99
+ return render_template("post.html", blog=blog, post=one_post, user=user, views=v, likes=likes, pages=pages)
80
100
 
81
101
  @blog_bp.route("/<post>", subdomain="<blog>")
82
102
  def show_subpost(blog, post):
@@ -92,9 +112,20 @@ def show_subpost(blog, post):
92
112
  if blog.access == "path":
93
113
  return redirect(f"https://{os.getenv('DOMAIN')}/b/{blog.name}/{post}")
94
114
 
115
+ pages = g.db.query(Page).filter_by(blog=blog.id).all()
95
116
  one_post = g.db.query(Post).filter(Post.blog == blog.id, Post.link == post).first()
117
+
96
118
  if not one_post:
97
- return redirect("/")
119
+ page = g.db.query(Page).filter(Page.blog == blog.id, Page.url == post).first()
120
+ if not page:
121
+ return redirect("/")
122
+
123
+ if "{posts}" in page.content_raw:
124
+ posts = g.db.query(Post).filter_by(blog=blog.id).order_by(desc(Post.id)).all()
125
+
126
+ return render_template("blog.html", blog=blog, page=page, posts=posts, pages=pages)
127
+
128
+ return render_template("blog.html", blog=blog, page=page, pages=pages)
98
129
 
99
130
  post_author = g.db.query(User).filter_by(id=blog.owner).first()
100
131
  one_post.authorname = post_author.username
@@ -115,7 +146,7 @@ def show_subpost(blog, post):
115
146
  one_post.liked = liked
116
147
 
117
148
  user = g.db.query(User).filter_by(id=g.user) if g.user else None
118
- return render_template("post.html", blog=blog, post=one_post, user=user, views=v)
149
+ return render_template("post.html", blog=blog, post=one_post, user=user, views=v, pages=pages)
119
150
 
120
151
  @blog_bp.route("/p/<post>")
121
152
  def single_showpost(post):
@@ -123,9 +154,22 @@ def single_showpost(post):
123
154
  return redirect("/")
124
155
 
125
156
  blog = g.db.query(Blog).filter_by(id=1).first()
157
+ blog.url = f"http://{g.main_domain}"
126
158
  one_post = g.db.query(Post).filter(Post.blog == 1, Post.link == post).first()
159
+
160
+ pages = g.db.query(Page).filter_by(blog=1).all()
161
+
127
162
  if not one_post:
128
- return redirect("/")
163
+ page = g.db.query(Page).filter(Page.blog == blog.id, Page.url == post).first()
164
+ if not page:
165
+ return redirect("/")
166
+
167
+ if "{posts}" in page.content_raw:
168
+ posts = g.db.query(Post).filter_by(blog=blog.id).order_by(desc(Post.id)).all()
169
+
170
+ return render_template("blog.html", blog=blog, page=page, posts=posts, pages=pages)
171
+
172
+ return render_template("blog.html", blog=blog, page=page, pages=pages)
129
173
 
130
174
  post_author = g.db.query(User).filter_by(id=1).first()
131
175
  one_post.authorname = post_author.username
@@ -145,7 +189,7 @@ def single_showpost(post):
145
189
  one_post.liked = liked
146
190
 
147
191
  user = g.db.query(User).filter_by(id=g.user) if g.user else None
148
- return render_template("post.html", blog=blog, post=one_post, user=user, views=v)
192
+ return render_template("post.html", blog=blog, post=one_post, user=user, views=v, pages=pages)
149
193
 
150
194
  @blog_bp.route("/rss")
151
195
  def single_rss():
@@ -1,5 +1,5 @@
1
1
  from flask import Blueprint, render_template, redirect, request, g, abort
2
- from openwrite.utils.models import Blog, Post, User, View
2
+ from openwrite.utils.models import Blog, Post, User, View, Page
3
3
  from openwrite.utils.helpers import sanitize_html, gen_link, safe_css, send_activity, is_html, get_themes
4
4
  import requests
5
5
  from sqlalchemy import desc
@@ -12,6 +12,7 @@ import bcrypt
12
12
  import markdown
13
13
  from bs4 import BeautifulSoup
14
14
  from user_agents import parse
15
+ import re
15
16
 
16
17
  dashboard_bp = Blueprint("dashboard", __name__)
17
18
 
@@ -79,16 +80,31 @@ def create_blog():
79
80
  name=form_url,
80
81
  index=form_index,
81
82
  access=form_access,
82
- description_raw=f"![hello](https://openwrite.b-cdn.net/hello.jpg =500x258)\n\n# Hello there! 👋\n\nYou can edit your blog description in [dashboard](https://{g.main_domain}/dashboard/edit/{form_url})",
83
- description_html=f"<p><img src=\"https://openwrite.b-cdn.net/hello.jpg\" width=\"500\" height=\"258\"></p><h1>Hello there! 👋</h1><p>You can edit your blog description in <a href=\"https://{g.main_domain}/dashboard/edit/{form_url}\">dashboard</a></p>",
84
83
  css="",
84
+ description_raw="x",
85
+ description_html="x",
85
86
  pub_key=public_pem,
86
87
  priv_key=private_pem,
87
88
  theme="default",
89
+ favicon="",
88
90
  created=now
89
91
  )
90
92
  g.db.add(new_blog)
91
93
  g.db.commit()
94
+
95
+ b_id = g.db.query(Blog).filter_by(name=form_url).first().id
96
+
97
+ new_page = Page(
98
+ blog=b_id,
99
+ name="Home",
100
+ url="",
101
+ content_raw=f"![hello](https://openwrite.b-cdn.net/hello.jpg =500x258)\n\n# Hello there! 👋\n\nYou can edit your blog home page in [dashboard](https://{g.main_domain}/dashboard/edit/{form_url})\n\n---\n### Posts\n\n{{posts}}",
102
+ content_html=f"<p><img src=\"https://openwrite.b-cdn.net/hello.jpg\" width=\"500\" height=\"258\"></p><h1>Hello there! 👋</h1><p>You can edit your blog home page in <a href=\"https://{g.main_domain}/dashboard/edit/{form_url}\">dashboard</a></p>\n\n<hr>\n<h3>Posts\n\n{{posts}}",
103
+ show="off"
104
+ )
105
+ g.db.add(new_page)
106
+ g.db.commit()
107
+
92
108
  return redirect("/dashboard")
93
109
  except Exception:
94
110
  return render_template("create.html", error=g.trans['error'])
@@ -122,27 +138,28 @@ def edit_blog(name):
122
138
  v = g.db.query(View).filter(View.post == p.id, View.blog == blog.id).count()
123
139
  p.views = v
124
140
 
141
+ pages = g.db.query(Page).filter_by(blog=blog.id).all()
142
+
125
143
  themes = get_themes()
126
144
  themes.append("default")
127
145
 
128
146
  if request.method == "GET":
129
- return render_template("edit.html", blog=blog, posts=posts, themes=themes)
147
+ return render_template("edit.html", blog=blog, posts=posts, themes=themes, pages=pages)
130
148
 
131
149
  now = datetime.now(timezone.utc).replace(microsecond=0)
132
- blog.description_raw = request.form.get("description_raw")
133
- blog.description_html = sanitize_html(request.form.get("description_html"))
134
150
  if len(request.form.get("title")) > 30:
135
- return render_template("edit.html", blog=blog, posts=posts, themes=themes, error="Title too long! Max 30 characters.")
151
+ return render_template("edit.html", blog=blog, posts=posts, themes=themes, pages=pages, error="Title too long! Max 30 characters.")
136
152
  blog.css = safe_css(request.form.get("css"))
137
153
  blog.updated = now
138
154
  blog.title = request.form.get("title")
155
+ blog.favicon = request.form.get("icon")[:10]
139
156
  selected_theme = request.form.get("theme")
140
157
  if selected_theme not in themes:
141
- return render_template("edit.html", blog=blog, posts=posts, themes=themes, error="Wrong theme!")
158
+ return render_template("edit.html", blog=blog, posts=posts, themes=themes, pages=pages, error="Wrong theme!")
142
159
  blog.theme = selected_theme
143
160
  g.db.commit()
144
161
 
145
- return render_template("edit.html", blog=blog, posts=posts, themes=themes)
162
+ return render_template("edit.html", blog=blog, posts=posts, themes=themes, pages=pages)
146
163
 
147
164
  @dashboard_bp.route("/dashboard/post/<name>", methods=['GET', 'POST'])
148
165
  def new_post(name):
@@ -164,7 +181,9 @@ def new_post(name):
164
181
  link = gen_link(title)
165
182
  if link == "rss":
166
183
  link = "p_rss"
167
- dupes = g.db.query(Post).filter(Post.link.startswith(link), Post.blog == blog.id).count()
184
+ dupes_posts = g.db.query(Post).filter(Post.link.startswith(link), Post.blog == blog.id).count()
185
+ dupes_pages = g.db.query(Page).filter(Page.url == link, Page.blog == blog.id).count()
186
+ dupes = dupes_posts + dupes_pages
168
187
  if dupes > 0:
169
188
  link += f"-{dupes + 1}"
170
189
 
@@ -246,12 +265,21 @@ def blog_preview():
246
265
  return redirect("/login")
247
266
 
248
267
  u = g.db.query(User).filter_by(id=g.user).first()
268
+ blog_name = request.referrer.split("/")[-1]
269
+ blog = g.db.query(Blog).filter_by(name=blog_name).first()
270
+ if not blog:
271
+ return redirect("/")
272
+
273
+ homepage = g.db.query(Page).filter(Page.blog == blog.id, Page.url == "").first()
249
274
  data = {
250
275
  'title': request.form.get('title'),
251
- 'content': sanitize_html(request.form.get('content')),
276
+ 'css': request.form.get('css'),
277
+ 'icon': request.form.get('icon'),
278
+ 'content': homepage.content_html,
252
279
  'theme': request.form.get('theme')
253
280
  }
254
281
 
282
+
255
283
  return render_template("blog_preview.html", data=data)
256
284
 
257
285
  @dashboard_bp.route("/dashboard/edit/<blog>/<post>", methods=['GET', 'POST'])
@@ -277,7 +305,9 @@ def edit_post(blog, post):
277
305
  link = gen_link(title)
278
306
  if link == "rss":
279
307
  link = "p_rss"
280
- dupes = g.db.query(Post).filter(Post.link.startswith(link), Post.blog == blog_obj.id).count()
308
+ dupes_posts = g.db.query(Post).filter(Post.link.startswith(link), Post.blog == blog_obj.id).count()
309
+ dupes_pages = g.db.query(Page).filter(Page.url == link, Page.blog == blog_obj.id).count()
310
+ dupes = dupes_posts + dupes_pages
281
311
  if dupes > 0 and link != post:
282
312
  link += f"-{dupes + 1}"
283
313
 
@@ -407,3 +437,127 @@ def get_stats(blog, post, limit):
407
437
  views_obj["views"].append([v.date, os, browser])
408
438
 
409
439
  return views_obj
440
+
441
+ @dashboard_bp.route("/dashboard/page/<blog>", methods=['GET', 'POST'])
442
+ def new_page(blog):
443
+ if g.user is None:
444
+ abort(403)
445
+ blog = g.db.query(Blog).filter_by(name=blog).first()
446
+
447
+ if not blog:
448
+ abort(404)
449
+
450
+ if blog.owner != g.user:
451
+ abort(403)
452
+
453
+ if request.method == "GET":
454
+ return render_template("new_page.html", blog=blog)
455
+
456
+ name = request.form.get("name")
457
+ url = request.form.get("url")
458
+ content_raw = request.form.get("content_raw")
459
+ content_html = request.form.get("content")
460
+ show = request.form.get("show")
461
+ if len(name) > 120:
462
+ return render_template("new_page.html", blog=blog, error="Name too long! Max 120 characters")
463
+ if not re.match(r"^\/[a-zA-Z0-9\-\_]+$", url):
464
+ return render_template("new_page.html", blog=blog, error="Invalid URL!")
465
+
466
+ url = url[1:]
467
+ if url == "rss":
468
+ url = "p_rss"
469
+ dupes_posts = g.db.query(Post).filter(Post.link.startswith(url), Post.blog == blog.id).count()
470
+ dupes_pages = g.db.query(Page).filter(Page.url == url, Page.blog == blog.id).count()
471
+ dupes = dupes_posts + dupes_pages
472
+ if dupes > 0:
473
+ url = f"{url}-{dupes + 1}"
474
+
475
+ new_page = Page(blog=blog.id, name=name, url=url, content_raw=content_raw, content_html=sanitize_html(content_html), show=show)
476
+
477
+ g.db.add(new_page)
478
+ g.db.commit()
479
+ return redirect(f"/dashboard/edit/{blog.name}")
480
+
481
+ @dashboard_bp.route("/dashboard/page/<page>/edit", methods=['GET', 'POST'])
482
+ def edit_page(page):
483
+ if g.user is None:
484
+ abort(403)
485
+
486
+ page = g.db.query(Page).filter_by(id=page).first()
487
+ if not page:
488
+ abort(404)
489
+ blog = g.db.query(Blog).filter_by(id=page.blog).first()
490
+
491
+ if not blog:
492
+ abort(404)
493
+
494
+ if blog.owner != g.user:
495
+ abort(403)
496
+ home = False
497
+ if page.url == "":
498
+ home = True
499
+
500
+ if request.method == 'GET':
501
+ return render_template("new_page.html", blog=blog, page=page, home=home)
502
+
503
+ name = request.form.get("name")
504
+ content_raw = request.form.get("content_raw")
505
+ content_html = request.form.get("content")
506
+ show = request.form.get("show")
507
+ url = request.form.get("url")
508
+ if len(name) > 120:
509
+ return render_template("new_page.html", blog=blog, page=page, home=home, error="Name too long!")
510
+ if home == False:
511
+ if not re.match(r"^\/[a-zA-Z0-9\-\_]+$", url):
512
+ return render_template("new_page.html", blog=blog, error="Invalid URL!")
513
+
514
+ url = url[1:]
515
+ if url == "rss":
516
+ url = "p_rss"
517
+ dupes_posts = g.db.query(Post).filter(Post.link.startswith(url), Post.blog == blog.id).count()
518
+ dupes_pages = g.db.query(Page).filter(Page.url == url, Page.blog == blog.id).count()
519
+ dupes = dupes_posts + dupes_pages
520
+ if dupes > 0 and url != page.url:
521
+ url = f"{url}-{dupes + 1}"
522
+
523
+ page.name = name
524
+ page.content_raw = content_raw
525
+ page.content_html = sanitize_html(content_html)
526
+ page.show = show
527
+ if home == False:
528
+ page.url = url
529
+
530
+ g.db.commit()
531
+ return redirect(f"/dashboard/edit/{blog.name}")
532
+
533
+ @dashboard_bp.route("/dashboard/page_preview", methods=['POST'])
534
+ def page_preview():
535
+ if g.user is None:
536
+ return redirect("/")
537
+
538
+ data = {
539
+ 'name': request.form.get('name'),
540
+ 'content': sanitize_html(request.form.get('content')),
541
+ 'blog_title': request.form.get('blog_title'),
542
+ 'blog_name': request.form.get('blog_name'),
543
+ 'theme': request.form.get('theme')
544
+ }
545
+
546
+ return render_template("page_preview.html", data=data)
547
+
548
+
549
+ @dashboard_bp.route("/dashboard/page/<page>/delete")
550
+ def page_delete(page):
551
+ if g.user is None:
552
+ return redirect("/")
553
+
554
+ page = g.db.query(Page).filter_by(id=page).first()
555
+ blog = g.db.query(Blog).filter_by(id=page.blog).first()
556
+
557
+ if blog.owner != g.user:
558
+ return abort(403)
559
+
560
+ g.db.delete(page)
561
+ g.db.commit()
562
+
563
+ return redirect(f"/dashboard/edit/{blog.name}")
@@ -3,7 +3,7 @@ import os
3
3
  import json
4
4
  import datetime
5
5
  import requests
6
- from openwrite.utils.models import Post, Blog, Home, Settings
6
+ from openwrite.utils.models import Post, Blog, Home, Settings, Page
7
7
  import time
8
8
  from openwrite import start_time
9
9
 
@@ -22,8 +22,14 @@ def index():
22
22
 
23
23
  elif g.mode == "single":
24
24
  blog = g.db.query(Blog).filter_by(name="default").first()
25
+ blog.url = f"http://{g.main_domain}"
26
+ page = g.db.query(Page).filter_by(blog=blog.id, url="").first()
27
+ pages = g.db.query(Page).filter_by(blog=blog.id).all()
28
+ if '{posts}' in page.content_raw:
29
+ posts = g.db.query(Post).filter_by(blog=blog.id).order_by(desc(Post.id)).all()
30
+ return render_template('blog.html', blog=blog, page=page, posts=posts, pages=pages)
25
31
  posts = g.db.query(Post).filter_by(blog=blog.id).all()
26
- return render_template('blog.html', blog=blog, posts=posts)
32
+ return render_template('blog.html', blog=blog, posts=posts, page=page, pages=pages)
27
33
 
28
34
  @main_bp.route("/set-lang/<lang_code>")
29
35
  def set_lang(lang_code):