openwrite 0.12.0__tar.gz → 0.13.1__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 (81) hide show
  1. {openwrite-0.12.0 → openwrite-0.13.1}/MANIFEST.in +3 -0
  2. {openwrite-0.12.0/openwrite.egg-info → openwrite-0.13.1}/PKG-INFO +2 -1
  3. {openwrite-0.12.0 → openwrite-0.13.1}/README.md +1 -0
  4. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/__init__.py +1 -1
  5. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/gemini_multi.py +33 -12
  6. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/gemini_single.py +37 -12
  7. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/routes/admin.py +24 -9
  8. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/routes/blog.py +12 -9
  9. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/routes/dashboard.py +81 -31
  10. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/routes/federation.py +176 -24
  11. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/routes/main.py +5 -3
  12. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/style/style.css +6 -0
  13. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/create.html +7 -1
  14. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/dashboard.html +5 -2
  15. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/discover.html +1 -1
  16. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/edit.html +13 -6
  17. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/footer.html +1 -0
  18. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/header.html +0 -1
  19. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/new_page.html +7 -1
  20. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/new_post.html +97 -14
  21. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/post.html +7 -3
  22. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/settings.html +7 -1
  23. openwrite-0.13.1/openwrite/utils/i18n.json +1 -0
  24. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/utils/models.py +1 -0
  25. {openwrite-0.12.0 → openwrite-0.13.1/openwrite.egg-info}/PKG-INFO +2 -1
  26. {openwrite-0.12.0 → openwrite-0.13.1}/pyproject.toml +4 -1
  27. openwrite-0.12.0/openwrite/utils/i18n.json +0 -227
  28. {openwrite-0.12.0 → openwrite-0.13.1}/LICENSE +0 -0
  29. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/cli.py +0 -0
  30. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/db/__init__.py +0 -0
  31. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/db/base.py +0 -0
  32. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/routes/auth.py +0 -0
  33. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/routes/upload.py +0 -0
  34. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/IBMPlexSans-Regular.ttf +0 -0
  35. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/Merriweather.ttf +0 -0
  36. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/avatar.png +0 -0
  37. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/emoji.json +0 -0
  38. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/favicon.ico +0 -0
  39. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/logo.png +0 -0
  40. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/script/chart.js +0 -0
  41. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/script/highlight.min.js +0 -0
  42. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/script/nativeEmoji.js +0 -0
  43. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/script/openwrite.js +0 -0
  44. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/script/papaparse.min.js +0 -0
  45. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/script/showdown.min.js +0 -0
  46. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/script/showdown.min.js.map +0 -0
  47. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/script/site.compat.min.js +0 -0
  48. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/script/site.min.js +0 -0
  49. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/style/hl.css +0 -0
  50. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/style/nativeEmoji.min.css +0 -0
  51. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/style/themes/cyberpunk.css +0 -0
  52. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/style/themes/light.css +0 -0
  53. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/style/themes/obscura.css +0 -0
  54. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/style/themes/stickynote.css +0 -0
  55. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/style/themes/terminal.css +0 -0
  56. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/style/themes/warmnight.css +0 -0
  57. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/static/style/themes/windows98.css +0 -0
  58. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/admin.html +0 -0
  59. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/blog.html +0 -0
  60. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/blog_footer.html +0 -0
  61. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/blog_header.html +0 -0
  62. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/blog_preview.html +0 -0
  63. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/changepw.html +0 -0
  64. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/import.html +0 -0
  65. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/index.html +0 -0
  66. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/instances.html +0 -0
  67. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/login.html +0 -0
  68. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/page_preview.html +0 -0
  69. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/preview.html +0 -0
  70. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/register.html +0 -0
  71. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/stats.html +0 -0
  72. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/templates/translations.html +0 -0
  73. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/utils/create_db.py +0 -0
  74. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/utils/db.py +0 -0
  75. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite/utils/helpers.py +0 -0
  76. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite.egg-info/SOURCES.txt +0 -0
  77. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite.egg-info/dependency_links.txt +0 -0
  78. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite.egg-info/entry_points.txt +0 -0
  79. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite.egg-info/requires.txt +0 -0
  80. {openwrite-0.12.0 → openwrite-0.13.1}/openwrite.egg-info/top_level.txt +0 -0
  81. {openwrite-0.12.0 → openwrite-0.13.1}/setup.cfg +0 -0
@@ -1,3 +1,6 @@
1
1
  include openwrite/utils/i18n.json
2
2
  recursive-include openwrite/templates *
3
3
  recursive-include openwrite/static *
4
+ exclude logs/*
5
+ exclude gemini/*
6
+ exclude uploads/*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openwrite
3
- Version: 0.12.0
3
+ Version: 0.13.1
4
4
  License: GNU AFFERO GENERAL PUBLIC LICENSE
5
5
  Version 3, 19 November 2007
6
6
 
@@ -704,6 +704,7 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
704
704
  - Single-blog mode
705
705
  - Supports sqlite and mysql databases
706
706
  - Upload images to local storage or bunny cdn
707
+ - Drafts
707
708
  - Simple markdown editor in posting
708
709
  - Discover section
709
710
  - Privacy:
@@ -15,6 +15,7 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
15
15
  - Single-blog mode
16
16
  - Supports sqlite and mysql databases
17
17
  - Upload images to local storage or bunny cdn
18
+ - Drafts
18
19
  - Simple markdown editor in posting
19
20
  - Discover section
20
21
  - Privacy:
@@ -85,7 +85,7 @@ def create_app(test_config=None):
85
85
  url="",
86
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
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"
88
+ show="0"
89
89
  )
90
90
  SessionLocal.add(new_page)
91
91
 
@@ -1,10 +1,11 @@
1
1
  import os, subprocess
2
2
  from dotenv import load_dotenv
3
3
  from openwrite.utils.db import init_engine, SessionLocal
4
- from openwrite.utils.models import Blog, Post, User, View
4
+ from openwrite.utils.models import Blog, Post, User, View, Page
5
5
  from openwrite.utils.helpers import anonymize
6
6
  from md2gemini import md2gemini
7
7
  from datetime import datetime, timezone
8
+ import re
8
9
  session = None
9
10
 
10
11
  openwrite_logo = """
@@ -25,7 +26,7 @@ def root(req):
25
26
  resp += "In here you can read posts published.\n"
26
27
  resp += "\n## 🗞️ Latest posts:\n"
27
28
  try:
28
- posts = (session.query(Post).filter_by(feed="1").order_by(Post.id.desc()).limit(10).all())
29
+ posts = (session.query(Post).filter(Post.feed == "1", Post.isdraft == "0").order_by(Post.id.desc()).limit(10).all())
29
30
  for p in posts:
30
31
  blog = session.query(Blog).filter_by(id=p.blog).first()
31
32
  resp += f"\n{p.date} - 📓 {blog.title}\n"
@@ -45,15 +46,24 @@ def blog_index(req):
45
46
  if not blog:
46
47
  return "not found"
47
48
  posts = (session.query(Post)
48
- .filter_by(blog=blog.id)
49
- .order_by(Post.id.desc())
50
- .all())
49
+ .filter_by(blog=blog.id, isdraft="0")
50
+ .order_by(Post.id.desc())
51
+ .all())
52
+ pages = (session.query(Page)
53
+ .filter(Page.blog == blog.id, Page.show == "1").all())
54
+ homepage = session.query(Page).filter(Page.blog == blog.id, Page.url == "").first()
51
55
  body = f"# 📓 {blog.title}\n\n"
52
- body += md2gemini(blog.description_raw.replace("#", ""))
53
- body += "\n--------------------------------------------------\n"
54
- body += "### Posts:\n\n"
56
+ posts_data = ""
55
57
  for post in posts:
56
- body += f"=> /b/{blogname}/{post.link} {post.title}\n"
58
+ posts_data += f"=> /b/{blogname}/{post.link} {post.title}\n\n"
59
+ pages_data = ""
60
+ for page in pages:
61
+ pages_data += f"=> /b/{blogname}/{page.url} {page.name}\n\n"
62
+ content = pages_data + "\n"
63
+ content += homepage.content_raw.replace('{posts}', f"\n{posts_data}")
64
+ content = re.sub(r'!\[(.*?)\]\((.*?)\s*=\d+x\d+\)', r'![\1](\2)', content)
65
+ body += md2gemini(content)
66
+
57
67
  return body
58
68
  finally:
59
69
  session.close()
@@ -67,10 +77,19 @@ def blog_index(req):
67
77
  return "not found"
68
78
 
69
79
  post = (session.query(Post)
70
- .filter_by(blog=blog.id, link=slug)
80
+ .filter_by(blog=blog.id, link=slug, isdraft="0")
71
81
  .first())
72
82
  if not post:
73
- return "not found"
83
+ page = (session.query(Page)
84
+ .filter_by(blog=blog.id, url=slug)
85
+ .first())
86
+ if page:
87
+ content = f"=> /b/{blog.name} 📓 {blog.title}\n\n"
88
+ content += page.content_raw
89
+ content = re.sub(r'!\[(.*?)\]\((.*?)\s*=\d+x\d+\)', r'![\1](\2)', content)
90
+ return md2gemini(content)
91
+ else:
92
+ return "not found"
74
93
  user = session.query(User).filter_by(id=blog.owner).first()
75
94
 
76
95
  now = datetime.now(timezone.utc)
@@ -88,7 +107,9 @@ def blog_index(req):
88
107
  if post.author != "0":
89
108
  gemtext += f"by {post.authorname}"
90
109
  gemtext += "\n--------------------------------------------------\n"
91
- gemtext += f"\n\n{md2gemini(post.content_raw)}"
110
+ content = post.content_raw
111
+ content = re.sub(r'!\[(.*?)\]\((.*?)\s*=\d+x\d+\)', r'![\1](\2)', content)
112
+ gemtext += f"\n\n{md2gemini(content)}\n\n"
92
113
  return gemtext
93
114
 
94
115
  finally:
@@ -11,17 +11,28 @@ def blog_index(req):
11
11
  global session
12
12
  try:
13
13
  blog = session.query(Blog).filter_by(name="default").first()
14
- posts = (session.query(Post)
14
+ if not blog:
15
+ return "not found"
16
+ posts = (session.query(Post)
15
17
  .filter_by(blog=blog.id)
16
18
  .order_by(Post.id.desc())
17
19
  .all())
18
- body = f"# 📓 {blog.title}\n\n"
19
- body += md2gemini(blog.description_raw.replace("#", ""))
20
- body += "\n--------------------------------------------------\n"
21
- body += "### Posts:\n\n"
22
- for post in posts:
23
- body += f"=> /p/{post.link} {post.title}\n"
24
- return body
20
+ pages = (session.query(Page)
21
+ .filter(Page.blog == blog.id, Page.show == "1").all())
22
+ homepage = session.query(Page).filter(Page.blog == blog.id, Page.url == "").first()
23
+ body = f"# 📓 {blog.title}\n\n"
24
+ posts_data = ""
25
+ for post in posts:
26
+ posts_data += f"=> /b/{blogname}/{post.link} {post.title}\n\n"
27
+ pages_data = ""
28
+ for page in pages:
29
+ pages_data += f"=> /b/{blogname}/{page.url} {page.name}\n\n"
30
+ content = pages_data + "\n"
31
+ content += homepage.content_raw.replace('{posts}', f"\n{posts_data}")
32
+ content = re.sub(r'!\[(.*?)\]\((.*?)\s*=\d+x\d+\)', r'![\1](\2)', content)
33
+ body += md2gemini(content)
34
+
35
+ return body
25
36
  finally:
26
37
  session.close()
27
38
 
@@ -33,11 +44,23 @@ def blog_post(req):
33
44
  slug = path.split("/")[2]
34
45
  try:
35
46
  blog = session.query(Blog).filter_by(name="default").first()
47
+ if not blog:
48
+ return "not found"
49
+
36
50
  post = (session.query(Post)
37
- .filter_by(blog=blog.id, link=slug)
38
- .first())
51
+ .filter_by(blog=blog.id, link=slug, isdraft="0")
52
+ .first())
39
53
  if not post:
40
- return "not found"
54
+ page = (session.query(Page)
55
+ .filter_by(blog=blog.id, url=slug)
56
+ .first())
57
+ if page:
58
+ content = f"=> /b/{blog.name} 📓 {blog.title}\n\n"
59
+ content += page.content_raw
60
+ content = re.sub(r'!\[(.*?)\]\((.*?)\s*=\d+x\d+\)', r'![\1](\2)', content)
61
+ return md2gemini(content)
62
+ else:
63
+ return "not found"
41
64
  user = session.query(User).filter_by(id=blog.owner).first()
42
65
 
43
66
  now = datetime.now(timezone.utc)
@@ -55,7 +78,9 @@ def blog_post(req):
55
78
  if post.author != "0":
56
79
  gemtext += f"by {post.authorname}"
57
80
  gemtext += "\n--------------------------------------------------\n"
58
- gemtext += f"\n\n{md2gemini(post.content_raw)}"
81
+ content = post.content_raw
82
+ content = re.sub(r'!\[(.*?)\]\((.*?)\s*=\d+x\d+\)', r'![\1](\2)', content)
83
+ gemtext += f"\n\n{md2gemini(content)}\n\n"
59
84
  return gemtext
60
85
 
61
86
  finally:
@@ -1,6 +1,6 @@
1
1
  from flask import Blueprint, render_template, redirect, g, request, make_response
2
2
  import json
3
- from openwrite.utils.models import Blog, User, Settings, Home
3
+ from openwrite.utils.models import Blog, User, Settings, Home, Post
4
4
  import re
5
5
  import bcrypt
6
6
  from werkzeug.utils import secure_filename
@@ -116,16 +116,16 @@ def admin_add_user():
116
116
 
117
117
  form_username = request.form.get('username')
118
118
  if not re.match(r"^[a-zA-Z0-9](?:[a-zA-Z0-9_-]{1,28}[a-zA-Z0-9])?$", form_username):
119
- return render_template('register.html', error=g.trans['wrong_username'], add="1", captcha="0")
119
+ return render_template('register.html', error='wrong_username', add="1", captcha="0")
120
120
  form_password = request.form.get('password')
121
121
  form_password2 = request.form.get('password2')
122
122
 
123
123
  if form_password != form_password2:
124
- return render_template("register.html", error=g.trans['password_dont_match'], add="1", captcha="0")
124
+ return render_template("register.html", error='password_dont_match', add="1", captcha="0")
125
125
 
126
126
  user = g.db.query(User).filter_by(username=form_username).first()
127
127
  if user:
128
- return render_template('register.html', error=g.trans["user_exists"], add="1", captcha="0")
128
+ return render_template('register.html', error="user_exists", add="1", captcha="0")
129
129
 
130
130
  try:
131
131
  hashed = bcrypt.hashpw(form_password.encode('utf-8'), bcrypt.gensalt())
@@ -136,7 +136,7 @@ def admin_add_user():
136
136
  except Exception as e:
137
137
  print(e)
138
138
  g.db.rollback()
139
- return render_template('register.html', error=g.trans["error"], add="1", captcha="0")
139
+ return render_template('register.html', error='error', add="1", captcha="0")
140
140
 
141
141
 
142
142
  @admin_bp.route("/admin/settings", methods=['GET', 'POST'])
@@ -166,17 +166,17 @@ def admin_settings():
166
166
  file = request.files['logo']
167
167
  filename = secure_filename(file.filename)
168
168
  if "." not in filename:
169
- return render_template("settings.html", settings=settings_dict, hometext=hometext, error="Wrong file!")
169
+ return render_template("settings.html", settings=settings_dict, hometext=hometext, error='invalid_filename')
170
170
 
171
171
  extension = filename.rsplit('.', 1)[1].lower()
172
172
  if extension not in ALLOWED_EXTENSIONS:
173
- return render_template("settings.html", settings=settings_dict, hometext=hometext, error="Wrong extension!")
173
+ return render_template("settings.html", settings=settings_dict, hometext=hometext, error='unsupported_filetype')
174
174
 
175
175
  try:
176
176
  img = Image.open(file.stream)
177
177
  img.verify()
178
178
  except Exception:
179
- return render_template("settings.html", settings=settings_dict, hometext=hometext, error="File is not image!")
179
+ return render_template("settings.html", settings=settings_dict, hometext=hometext, error="not_image")
180
180
 
181
181
  file.stream.seek(0)
182
182
 
@@ -245,4 +245,19 @@ def download_translations():
245
245
  response = make_response(json.dumps(dict(trans_dict), ensure_ascii=False))
246
246
  response.headers["Content-Disposition"] = "attachment; filename=i18n.json"
247
247
  response.headers["Content-Type"] = "application/json"
248
- return response
248
+ return response
249
+
250
+ @admin_bp.route("/admin/remove_from_feed/<int:post_id>")
251
+ def remove_from_feed(post_id):
252
+ """
253
+ Remove a post from the discover feed.
254
+ """
255
+ if g.user is None or g.isadmin == 0 or g.mode == "single":
256
+ return redirect("/")
257
+
258
+ post = g.db.query(Post).filter_by(id=post_id).first()
259
+ if post:
260
+ post.feed = "0"
261
+ g.db.commit()
262
+
263
+ return redirect("/discover")
@@ -24,7 +24,7 @@ def show_blog(blog):
24
24
  blog.url = f"/b/{blog.name}"
25
25
  homepage = g.db.query(Page).filter(Page.blog == blog.id, Page.url == "").first()
26
26
  if "{posts}" in homepage.content_raw:
27
- posts = g.db.query(Post).filter_by(blog=blog.id).order_by(desc(Post.id)).all()
27
+ posts = g.db.query(Post).filter(Post.blog == blog.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
28
28
  return render_template("blog.html", blog=blog, page=homepage, posts=posts, pages=pages)
29
29
 
30
30
  return render_template("blog.html", blog=blog, page=homepage, pages=pages)
@@ -40,10 +40,11 @@ def show_subblog(blog):
40
40
  if blog.access == "path":
41
41
  return redirect(f"https://{os.getenv('DOMAIN')}/b/{blog.name}")
42
42
 
43
+ blog.url = f"https://{blog.name}.{os.getenv('DOMAIN')}"
43
44
  pages = g.db.query(Page).filter_by(blog=blog.id).all()
44
45
  homepage = g.db.query(Page).filter(Page.blog == blog.id, Page.url == "").first()
45
46
  if "{posts}" in homepage.content_raw:
46
- posts = g.db.query(Post).filter_by(blog=blog.id).order_by(desc(Post.id)).all()
47
+ posts = g.db.query(Post).filter(Post.blog == blog.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
47
48
  return render_template("blog.html", blog=blog, page=homepage, posts=posts, pages=pages)
48
49
 
49
50
  return render_template("blog.html", blog=blog, page=homepage, pages=pages)
@@ -64,14 +65,14 @@ def show_post(blog, post):
64
65
 
65
66
  blog.url = f"/b/{blog.name}"
66
67
  pages = g.db.query(Page).filter_by(blog=blog.id).all()
67
- one_post = g.db.query(Post).filter(Post.blog == blog.id, Post.link == post).first()
68
+ one_post = g.db.query(Post).filter(Post.blog == blog.id, Post.link == post, Post.isdraft == "0").first()
68
69
  if not one_post:
69
70
  page = g.db.query(Page).filter(Page.blog == blog.id, Page.url == post).first()
70
71
  if not page:
71
72
  return redirect("/")
72
73
 
73
74
  if "{posts}" in page.content_raw:
74
- posts = g.db.query(Post).filter_by(blog=blog.id).order_by(desc(Post.id)).all()
75
+ posts = g.db.query(Post).filter(Post.blog == blog.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
75
76
 
76
77
  return render_template("blog.html", blog=blog, page=page, posts=posts, pages=pages)
77
78
 
@@ -112,8 +113,10 @@ def show_subpost(blog, post):
112
113
  if blog.access == "path":
113
114
  return redirect(f"https://{os.getenv('DOMAIN')}/b/{blog.name}/{post}")
114
115
 
116
+ blog.url = f"https://{blog.name}.{os.getenv('DOMAIN')}"
117
+
115
118
  pages = g.db.query(Page).filter_by(blog=blog.id).all()
116
- one_post = g.db.query(Post).filter(Post.blog == blog.id, Post.link == post).first()
119
+ one_post = g.db.query(Post).filter(Post.blog == blog.id, Post.link == post, Post.isdraft == "0").first()
117
120
 
118
121
  if not one_post:
119
122
  page = g.db.query(Page).filter(Page.blog == blog.id, Page.url == post).first()
@@ -121,7 +124,7 @@ def show_subpost(blog, post):
121
124
  return redirect("/")
122
125
 
123
126
  if "{posts}" in page.content_raw:
124
- posts = g.db.query(Post).filter_by(blog=blog.id).order_by(desc(Post.id)).all()
127
+ posts = g.db.query(Post).filter(Post.blog == blog.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
125
128
 
126
129
  return render_template("blog.html", blog=blog, page=page, posts=posts, pages=pages)
127
130
 
@@ -155,7 +158,7 @@ def single_showpost(post):
155
158
 
156
159
  blog = g.db.query(Blog).filter_by(id=1).first()
157
160
  blog.url = f"http://{g.main_domain}"
158
- one_post = g.db.query(Post).filter(Post.blog == 1, Post.link == post).first()
161
+ one_post = g.db.query(Post).filter(Post.blog == 1, Post.link == post, Post.isdraft == "0").first()
159
162
 
160
163
  pages = g.db.query(Page).filter_by(blog=1).all()
161
164
 
@@ -165,7 +168,7 @@ def single_showpost(post):
165
168
  return redirect("/")
166
169
 
167
170
  if "{posts}" in page.content_raw:
168
- posts = g.db.query(Post).filter_by(blog=blog.id).order_by(desc(Post.id)).all()
171
+ posts = g.db.query(Post).filter(Post.blog == blog.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
169
172
 
170
173
  return render_template("blog.html", blog=blog, page=page, posts=posts, pages=pages)
171
174
 
@@ -230,7 +233,7 @@ def like():
230
233
 
231
234
 
232
235
  def _generate_rss(blog):
233
- posts = g.db.query(Post).filter_by(blog=blog.id).all()
236
+ posts = g.db.query(Post).filter(Post.blog == blog.id, Post.isdraft == "0").all()
234
237
  fg = FeedGenerator()
235
238
  fg.title(blog.title)
236
239
  fg.link(href=f"https://{os.getenv('DOMAIN')}/b/{blog.name}", rel="alternate")
@@ -1,4 +1,4 @@
1
- from flask import Blueprint, render_template, redirect, request, g, abort
1
+ from flask import Blueprint, render_template, redirect, request, g, abort, Response
2
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
@@ -13,6 +13,8 @@ import markdown
13
13
  from bs4 import BeautifulSoup
14
14
  from user_agents import parse
15
15
  import re
16
+ import csv
17
+ from io import StringIO
16
18
 
17
19
  dashboard_bp = Blueprint("dashboard", __name__)
18
20
 
@@ -36,7 +38,7 @@ def create_blog():
36
38
  if int(g.blog_limit) > 0:
37
39
  count = g.db.query(Blog).filter_by(owner=g.user).count()
38
40
  if count >= int(g.blog_limit):
39
- return render_template("create.html", error="Blog limit reached!")
41
+ return render_template("create.html", error="blog-limit-reached")
40
42
 
41
43
  if request.method == "GET":
42
44
  return render_template("create.html")
@@ -44,17 +46,17 @@ def create_blog():
44
46
  form_name = request.form.get("name")
45
47
  form_url = gen_link(request.form.get("url"))
46
48
  if len(form_name) > 30:
47
- return render_template("create.html", error="Title too long! Max 30 characters.")
49
+ return render_template("create.html", error="title-too-long")
48
50
  if len(form_url) > 30:
49
- return render_template("create.html", error="URL too long! Max 30 characters.")
51
+ return render_template("create.html", error="url-too-long")
50
52
  blog = g.db.query(Blog).filter_by(name=form_url).first()
51
53
  if blog:
52
- return render_template("create.html", error="This URL already exists!")
54
+ return render_template("create.html", error="url-exists")
53
55
 
54
56
  form_index = request.form.get("index") or "off"
55
57
  form_access = request.form.get("access")
56
58
  if form_access not in ("path", "domain"):
57
- return render_template("create.html", error="Wrong access!")
59
+ return render_template("create.html", error="wrong-access")
58
60
 
59
61
  key = rsa.generate_private_key(
60
62
  public_exponent=65537,
@@ -99,15 +101,15 @@ def create_blog():
99
101
  name="Home",
100
102
  url="",
101
103
  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
+ 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</h3>\n\n{{posts}}",
105
+ show="0"
104
106
  )
105
107
  g.db.add(new_page)
106
108
  g.db.commit()
107
109
 
108
110
  return redirect("/dashboard")
109
111
  except Exception:
110
- return render_template("create.html", error=g.trans['error'])
112
+ return render_template("create.html", error="error")
111
113
 
112
114
  @dashboard_bp.route("/dashboard/delete/<name>")
113
115
  def delete_blog(name):
@@ -118,7 +120,7 @@ def delete_blog(name):
118
120
 
119
121
  blog = g.db.query(Blog).filter_by(name=name).first()
120
122
  if blog is None or blog.owner != g.user:
121
- return redirect("/dashboard")
123
+ abort(403)
122
124
 
123
125
  g.db.delete(blog)
124
126
  g.db.commit()
@@ -131,7 +133,7 @@ def edit_blog(name):
131
133
 
132
134
  blog = g.db.query(Blog).filter_by(name=name).first()
133
135
  if blog is None or blog.owner != g.user:
134
- return redirect("/dashboard")
136
+ abort(403)
135
137
 
136
138
  posts = g.db.query(Post).filter_by(blog=blog.id).all()
137
139
  for p in posts:
@@ -148,14 +150,14 @@ def edit_blog(name):
148
150
 
149
151
  now = datetime.now(timezone.utc).replace(microsecond=0)
150
152
  if len(request.form.get("title")) > 30:
151
- return render_template("edit.html", blog=blog, posts=posts, themes=themes, pages=pages, error="Title too long! Max 30 characters.")
153
+ return render_template("edit.html", blog=blog, posts=posts, themes=themes, pages=pages, error="title-too-long")
152
154
  blog.css = safe_css(request.form.get("css"))
153
155
  blog.updated = now
154
156
  blog.title = request.form.get("title")
155
157
  blog.favicon = request.form.get("icon")[:10]
156
158
  selected_theme = request.form.get("theme")
157
159
  if selected_theme not in themes:
158
- return render_template("edit.html", blog=blog, posts=posts, themes=themes, pages=pages, error="Wrong theme!")
160
+ return render_template("edit.html", blog=blog, posts=posts, themes=themes, pages=pages, error="wrong-theme")
159
161
  blog.theme = selected_theme
160
162
  g.db.commit()
161
163
 
@@ -168,7 +170,7 @@ def new_post(name):
168
170
 
169
171
  blog = g.db.query(Blog).filter_by(name=name).first()
170
172
  if blog is None or blog.owner != g.user:
171
- return redirect("/dashboard")
173
+ abort(403)
172
174
 
173
175
  if request.method == "GET":
174
176
  return render_template("new_post.html", blog=blog)
@@ -176,8 +178,11 @@ def new_post(name):
176
178
  u = g.db.query(User).filter_by(id=g.user).first()
177
179
  title = request.form.get('title')
178
180
  if len(title) > 120:
179
- return render_template("new_post.html", blog=blog, error="Title too long! Max 120 characters.")
180
-
181
+ return render_template("new_post.html", blog=blog, error="title-too-long")
182
+
183
+ if len(title) < 1:
184
+ return render_template("new_post.html", blog=blog, error="title-empty")
185
+
181
186
  link = gen_link(title)
182
187
  if link == "rss":
183
188
  link = "p_rss"
@@ -198,7 +203,8 @@ def new_post(name):
198
203
  author=request.form.get('author'),
199
204
  link=link,
200
205
  date=now,
201
- feed=request.form.get('feed')
206
+ feed=request.form.get('feed'),
207
+ isdraft=request.form.get('draft', '0')
202
208
  )
203
209
  g.db.add(post)
204
210
  g.db.commit()
@@ -268,7 +274,7 @@ def blog_preview():
268
274
  blog_name = request.referrer.split("/")[-1]
269
275
  blog = g.db.query(Blog).filter_by(name=blog_name).first()
270
276
  if not blog:
271
- return redirect("/")
277
+ abort(404)
272
278
 
273
279
  homepage = g.db.query(Page).filter(Page.blog == blog.id, Page.url == "").first()
274
280
  data = {
@@ -289,7 +295,7 @@ def edit_post(blog, post):
289
295
 
290
296
  blog_obj = g.db.query(Blog).filter_by(name=blog).first()
291
297
  if blog_obj is None or blog_obj.owner != g.user:
292
- return redirect("/dashboard")
298
+ abort(403)
293
299
 
294
300
  e_post = g.db.query(Post).filter_by(link=post).first()
295
301
  if request.method == "GET":
@@ -297,11 +303,11 @@ def edit_post(blog, post):
297
303
 
298
304
  p = g.db.query(Post).filter_by(link=post, blog=blog_obj.id).first()
299
305
  if not p:
300
- return redirect("/dashboard")
306
+ abort(404)
301
307
 
302
308
  title = request.form.get("title")
303
309
  if len(title) > 120:
304
- return render_template("new_post.html", blog=blog_obj, error="Title too long! Max 120 characters.")
310
+ return render_template("new_post.html", blog=blog_obj, error="title-too-long")
305
311
  link = gen_link(title)
306
312
  if link == "rss":
307
313
  link = "p_rss"
@@ -319,6 +325,7 @@ def edit_post(blog, post):
319
325
  p.feed = request.form.get("feed")
320
326
  p.link = link
321
327
  p.updated = now
328
+ p.isdraft = request.form.get("draft", "0")
322
329
  g.db.commit()
323
330
  return redirect(f"/dashboard/edit/{blog_obj.name}")
324
331
 
@@ -329,7 +336,7 @@ def delete_post(blog, post):
329
336
 
330
337
  blog_obj = g.db.query(Blog).filter_by(name=blog).first()
331
338
  if blog_obj is None or blog_obj.owner != g.user:
332
- return redirect("/dashboard")
339
+ abort(403)
333
340
 
334
341
  p = g.db.query(Post).filter_by(link=post, blog=blog_obj.id).first()
335
342
  if p:
@@ -353,14 +360,14 @@ def changepw():
353
360
  user = g.db.query(User).filter_by(id=g.user).first()
354
361
  if user and bcrypt.checkpw(old_pw.encode('utf-8'), user.password_hash.encode('utf-8')):
355
362
  if new_pass != new_pass2:
356
- return render_template("changepw.html", error=g.trans['passwords_dont_match'])
363
+ return render_template("changepw.html", error='passwords_dont_match')
357
364
  hashed = bcrypt.hashpw(new_pass.encode('utf-8'), bcrypt.gensalt())
358
365
  user.password_hash = hashed
359
366
  return redirect("/dashboard")
360
367
 
361
368
  else:
362
- return render_template("changepw.html", error=g.trans['invalid_password'])
363
-
369
+ return render_template("changepw.html", error='invalid_password')
370
+
364
371
 
365
372
  @dashboard_bp.route("/dashboard/import", methods=['GET', 'POST'])
366
373
  def migrate():
@@ -377,7 +384,7 @@ def migrate():
377
384
 
378
385
  blog = g.db.query(Blog).filter_by(name=blog_d).first()
379
386
  if blog.owner != g.user:
380
- return redirect("/dashboard")
387
+ abort(403)
381
388
 
382
389
  for post in posts:
383
390
  title = post['title']
@@ -404,7 +411,7 @@ def stats(blog):
404
411
 
405
412
  blog = g.db.query(Blog).filter_by(name=blog).first()
406
413
  if blog.owner != g.user:
407
- return redirect("/dashboard")
414
+ abort(403)
408
415
 
409
416
  posts = g.db.query(Post).filter_by(blog=blog.id).all()
410
417
 
@@ -459,9 +466,9 @@ def new_page(blog):
459
466
  content_html = request.form.get("content")
460
467
  show = request.form.get("show")
461
468
  if len(name) > 120:
462
- return render_template("new_page.html", blog=blog, error="Name too long! Max 120 characters")
469
+ return render_template("new_page.html", blog=blog, error="name-too-long")
463
470
  if not re.match(r"^\/[a-zA-Z0-9\-\_]+$", url):
464
- return render_template("new_page.html", blog=blog, error="Invalid URL!")
471
+ return render_template("new_page.html", blog=blog, error="invalid-url")
465
472
 
466
473
  url = url[1:]
467
474
  if url == "rss":
@@ -506,10 +513,10 @@ def edit_page(page):
506
513
  show = request.form.get("show")
507
514
  url = request.form.get("url")
508
515
  if len(name) > 120:
509
- return render_template("new_page.html", blog=blog, page=page, home=home, error="Name too long!")
516
+ return render_template("new_page.html", blog=blog, page=page, home=home, error="name-too-long")
510
517
  if home == False:
511
518
  if not re.match(r"^\/[a-zA-Z0-9\-\_]+$", url):
512
- return render_template("new_page.html", blog=blog, error="Invalid URL!")
519
+ return render_template("new_page.html", blog=blog, error="invalid-url")
513
520
 
514
521
  url = url[1:]
515
522
  if url == "rss":
@@ -557,7 +564,50 @@ def page_delete(page):
557
564
  if blog.owner != g.user:
558
565
  return abort(403)
559
566
 
567
+ if page.url == "":
568
+ return redirect("/dashboard")
569
+
560
570
  g.db.delete(page)
561
571
  g.db.commit()
562
572
 
563
573
  return redirect(f"/dashboard/edit/{blog.name}")
574
+
575
+ @dashboard_bp.route("/dashboard/export/<blog>")
576
+ def export_posts(blog):
577
+ if g.user is None:
578
+ return redirect("/login")
579
+
580
+ blog_obj = g.db.query(Blog).filter_by(name=blog).first()
581
+ if blog_obj is None or blog_obj.owner != g.user:
582
+ return redirect("/dashboard")
583
+
584
+ posts = g.db.query(Post).filter_by(blog=blog_obj.id).all()
585
+
586
+
587
+ output = StringIO()
588
+ writer = csv.writer(output)
589
+
590
+ writer.writerow(['Title', 'Content Raw', 'Content HTML', 'Author', 'Link', 'Date', 'Updated', 'Feed', 'Is Draft'])
591
+
592
+ for post in posts:
593
+ writer.writerow([
594
+ post.title,
595
+ post.content_raw,
596
+ post.content_html,
597
+ post.author,
598
+ post.link,
599
+ post.date.isoformat() if post.date else '',
600
+ post.updated.isoformat() if post.updated else '',
601
+ post.feed,
602
+ post.isdraft
603
+ ])
604
+
605
+ output.seek(0)
606
+
607
+ return Response(
608
+ output.getvalue(),
609
+ mimetype='text/csv',
610
+ headers={
611
+ 'Content-Disposition': f'attachment; filename={blog}_posts.csv'
612
+ }
613
+ )