From f7feb18de391c62daa405559699da4df78c37926 Mon Sep 17 00:00:00 2001 From: m Date: Tue, 17 Mar 2026 09:14:50 +0100 Subject: [PATCH] logic move out --- .gitignore | 4 +- app.py | 86 +++++++++++++++++++++++-------------- flask_logic.py/logic.py | 93 +++++++++++++++++++++++++++++++++++++++++ readme.md | 87 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 237 insertions(+), 33 deletions(-) create mode 100644 flask_logic.py/logic.py create mode 100644 readme.md diff --git a/.gitignore b/.gitignore index 9df8b3f..20242a2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ docker-compose.yml Dockerfile Caddyfile -deploy.sh \ No newline at end of file +deploy.sh +.vscode/ +.ruff_cache/ \ No newline at end of file diff --git a/app.py b/app.py index 53e0cab..e950c94 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,6 @@ from flask import Flask, render_template, request, redirect, url_for from content.posts import BLOG_POSTS -from content.logic import get_enriched_post, get_comments_for_post, save_comment +from flask_logic.logic import get_enriched_post, get_comments_for_post, save_comment # 🌟 IMPORT THE content from separate files. from content.posts import BLOG_POSTS @@ -16,15 +16,6 @@ COMMENT_FILE = 'content/comments.csv' MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024 # 5 Megabytes POSTS_PER_PAGE = 5 # posts per page limit here -""" -import os -from flask import send_from_directory - -@app.route('/content/images/') -def custom_static(filename): - # This serves files from your private submodule folder - return send_from_directory('content/images', filename) -""" @app.route('/post//comment', methods=['POST']) def post_comment(post_id): @@ -43,35 +34,66 @@ def post_comment(post_id): # --- Routes --- +def calculate_pagination(posts, posts_per_page, page): + """ + Calculate pagination parameters for blog posts. + + Args: + posts (list): Full list of blog posts + posts_per_page (int): Number of posts per page + page (int): Current page number (1-indexed) + + Returns: + dict: Pagination data including: + - posts_to_show: Posts for current page + - prev_url: Previous page URL or None + - next_url: Next page URL or None + - current_page: Current page number + - total_pages: Total number of pages + - total_posts: Total number of posts + """ + # Sort posts by ID newest first + sorted_posts = sorted(posts, key=lambda x: x['id'], reverse=True) + + total_posts = len(sorted_posts) + total_pages = math.ceil(total_posts / posts_per_page) + + # Clamp page to valid range + page = max(1, min(page, total_pages)) + + start = (page - 1) * posts_per_page + end = start + posts_per_page + + posts_to_show = sorted_posts[start:end] + + prev_url = url_for('home', page=page - 1) if page > 1 else None + next_url = url_for('home', page=page + 1) if end < total_posts else None + + return { + 'posts_to_show': posts_to_show, + 'prev_url': prev_url, + 'next_url': next_url, + 'current_page': page, + 'total_pages': total_pages, + 'total_posts': total_posts + } + @app.route('/') def home(): + """Home page with paginated blog posts.""" page = request.args.get('page', 1, type=int) blog_title = TITLE - - # 2. Sort posts by ID newest first - all_posts = sorted(BLOG_POSTS, key=lambda x: x['id'], reverse=True) - # Calculate totals - total_posts = len(all_posts) - total_pages = math.ceil(total_posts / POSTS_PER_PAGE) - # 3. Calculate start and end indices - start = (page - 1) * POSTS_PER_PAGE - end = start + POSTS_PER_PAGE - # 4. Slice the list for the current page - posts_to_show = all_posts[start:end] - - # 5. Determine if there is a next or previous page - prev_url = url_for('home', page=page - 1) if page > 1 else None - next_url = url_for('home', page=page + 1) if end < len(all_posts) else None + pagination = calculate_pagination(BLOG_POSTS, POSTS_PER_PAGE, page) return render_template('index.html', - posts=posts_to_show, - prev_url=prev_url, - next_url=next_url, - current_page=page, - blog_title=blog_title, - total_pages=total_pages) - + posts=pagination['posts_to_show'], + prev_url=pagination['prev_url'], + next_url=pagination['next_url'], + current_page=pagination['current_page'], + total_pages=pagination['total_pages'], + blog_title=blog_title, + total_posts=pagination['total_posts']) diff --git a/flask_logic.py/logic.py b/flask_logic.py/logic.py new file mode 100644 index 0000000..77db759 --- /dev/null +++ b/flask_logic.py/logic.py @@ -0,0 +1,93 @@ +import os +import csv + +COMMENT_FILE = os.path.join(os.path.dirname(__file__), '../content/comments.csv') +MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024 # 5MB example + +import datetime + +def save_comment(post_id, author, content): + """Handles the validation and saving of comments to the private CSV.""" + # 1. Validation logic moved here + if not content or len(content) > 1000: + return False + + author = (author or "Anonymous").strip()[:50] + content = content.strip() + + # 2. Path & Size Checks + file_exists = os.path.isfile(COMMENT_FILE) + if file_exists and os.path.getsize(COMMENT_FILE) > MAX_FILE_SIZE_BYTES: + return False # Or raise a specific error + + # 3. Write to CSV + with open(COMMENT_FILE, 'a', newline='', encoding='utf-8') as f: + writer = csv.DictWriter(f, fieldnames=['post_id', 'author', 'content', 'date']) + if not file_exists: + writer.writeheader() + + writer.writerow({ + 'post_id': post_id, + 'author': author, + 'content': content, + 'date': datetime.datetime.now().strftime("%B %d, %Y at %I:%M %p") + }) + return True + + + +def load_snippet(filename): + """Helper to read HTML snippets from the data folder.""" + base_path = os.path.join(os.path.dirname(__file__), 'data') + file_path = os.path.join(base_path, filename) + + if os.path.exists(file_path): + with open(file_path, 'r', encoding='utf-8') as f: + return f.read() + return "" + +def get_enriched_post(post_id, BLOG_POSTS): + post = next((p for p in BLOG_POSTS if p.get('id') == post_id), None) + if not post: + return None + + # Mapping config: keeps logic.py tiny + configs = { + 10: { + "template": "components/timeline.html", + "timeline_file": "post_10_timeline.html" + }, + 8: { + "template": "components/christmas_post.html", + "timeline": None + }, + } + + if post_id in configs: + conf = configs[post_id] + post_template = conf.get("template") + if post_template: + post["template"] = post_template + + # Only load the file if a filename is provided + t_file = conf.get("timeline_file") + if t_file: + post["timeline"] = load_snippet(t_file) + + return post + + +def get_comments_for_post(post_id): + comments = [] + if not os.path.exists(COMMENT_FILE): + return comments + + if os.path.getsize(COMMENT_FILE) > MAX_FILE_SIZE_BYTES: + return ["Comment section full."] # Handle error in app.py + + with open(COMMENT_FILE, 'r', encoding='utf-8') as f: + reader = csv.DictReader(f) + for row in reader: + if row['post_id'] == str(post_id): + comments.append(row) + return comments \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..8d8db4e --- /dev/null +++ b/readme.md @@ -0,0 +1,87 @@ +# Flask Blog Templates + +A minimal **Flask template set** for my blog. +This project adapts and evolves the original [simple-blog-template](https://github.com/earlbread/simple-blog-template) by [Seunghun Lee](https://github.com/earlbread). + +## Git Tag + +Current version: + +```bash +git tag -a v1.0 -m "Initial release of Flask Blog Templates" +git push origin v1.0 +``` + +--- + +## Overview + +It provides a growing collection of styling templates and is designed to run with two additional files (see below). + +In the current setup, blog content is defined using Python dictionaries (example below), while comments are stored in a CSV file at `content/comments.csv`. + +A basic blog structure is provided by the existing templates. Additional per-article styling can be applied either by embedding raw HTML directly in the post content or by using template expansion via Jinja: + +```jinja2 +{% if post.template %} + {% include post.template %} +{% endif %} +``` + +For example, `templates/components/timeline.html` demonstrates a timeline component. + +An example data structure is provided below. + +--- + +## Required Files + +This two files should be included in program folder to run the app. + +### `content/about.py` + +provides text for about page and blog title + +```python +ABOUT = { + "content": """ +

Hello here is the about page

+ """ +} + +TITLE = { + "title": "Hello World" +} +``` + +### `content/posts.py` + +provides articles in blog: + +```python +BLOG_POSTS = [ + { + 'id': 7, + 'title': "title", + 'subtitle': "subtitle", + 'date': "December 10, 2024", + 'content': """can use raw html string for style, will be rendered as html + """, + 'displayall': False + }, + ] +``` + +## Template Evolution + +- Originally based on the Simple Blog Template project. + +- Adapted for Jinja2 rendering (Flask compatibility). + +- CSS rewritten for flexibility and modular styling. + +- Template design will continue accumulating for themes and post customization. + +## License + +- This project is distributed under the [MIT License](https://github.com/earlbread/simple-blog-template/blob/master/LICENSE).