From a5317978af96046c3def9fb63201bd966a282b60 Mon Sep 17 00:00:00 2001 From: elukjanovica <elukjanovica.e@rkg.lv> Date: Sun, 28 Apr 2024 20:04:50 +0300 Subject: [PATCH] --- instance/Picture_Puzzle_web.db | Bin 20480 -> 49152 bytes main.py | 148 +++++++++++++++++++++-------- templates/base.html | 3 +- templates/category.html | 1 + templates/create_post.html | 169 +++++++++++++++++++++++++++++++++ templates/forums.html | 4 + 6 files changed, 284 insertions(+), 41 deletions(-) create mode 100644 templates/create_post.html diff --git a/instance/Picture_Puzzle_web.db b/instance/Picture_Puzzle_web.db index fee74413216522ab7f0eae811aa110cf956b0e21..61ec0da54e38fedaef0004ee7f6fa67f0d58af7e 100644 GIT binary patch literal 49152 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 delta 171 zcmZo@U~X8zI6+!ajDdlH6^LPgX`+s?s2GD@Stl?54+a+Aw+#G@{O5V!^4-`hC{W2O z(U`%^E-os{*lb*qn3R)RkY8K^!Yq?d@m^tMnS7C7X7X-6E^gLFZ$_}Xrpn0|_zp2~ zX-+=RZ_bg%z<-i|0)N_OL4^qZ$?5W1Qszvo44SI8!MUaBPNhZZsYQPI`ALa+iABkq MSILJ7EOJl)01iSf$p8QV diff --git a/main.py b/main.py index 01ff867..0580b5e 100644 --- a/main.py +++ b/main.py @@ -1,14 +1,25 @@ from flask import Flask, render_template, redirect, request, session, url_for, g from flask_sqlalchemy import SQLAlchemy +from sqlalchemy.orm import relationship from flask_admin import Admin, AdminIndexView, expose, BaseView from flask_admin.contrib.sqla import ModelView +from flask_wtf import FlaskForm +from flask_wtf.file import FileField, FileAllowed +from wtforms import StringField, TextAreaField, SelectField +from wtforms.validators import InputRequired +from werkzeug.utils import secure_filename from functools import wraps +from datetime import datetime +import os +import logging app = Flask(__name__) app.secret_key = 'bebra' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///Picture_Puzzle_web.db' +app.config['UPLOAD_FOLDER'] = 'uploads' +app.config['ALLOWED_EXTENSIONS'] = {'png', 'jpg', 'jpeg', 'gif', 'mp4', 'waw'} db = SQLAlchemy(app) - +logging.basicConfig(level=logging.DEBUG) class User(db.Model): __tablename__ = 'user' id = db.Column(db.Integer, primary_key=True) @@ -25,29 +36,48 @@ class Post(db.Model): image = db.Column(db.String(100), nullable=False) class ForumCategory(db.Model): + __tablename__ = 'forumcategory' id = db.Column(db.Integer, primary_key=True) category_name = db.Column(db.String(100), nullable=False) description = db.Column(db.String(200)) class ForumPost(db.Model): + __tablename__ = 'forumpost' id = db.Column(db.Integer, primary_key=True) - category_id = db.Column(db.Integer, db.ForeignKey('forum_category.id'), nullable=False) + category_id = db.Column(db.Integer, db.ForeignKey('forumcategory.id'), nullable=False) post_name = db.Column(db.String(100), nullable=False) created_by = db.Column(db.String(100), nullable=False) - creation_date = db.Column(db.DateTime, nullable=False) - media = db.Column(db.String(100)) + creation_date = db.Column(db.DateTime, default=datetime.now) + media = db.relationship('Media', backref='forumpost', lazy=True) text = db.Column(db.Text, nullable=False) edited = db.Column(db.Boolean, default=False) class ForumComment(db.Model): + __tablename__ = 'forumcomment' id = db.Column(db.Integer, primary_key=True) - post_id = db.Column(db.Integer, db.ForeignKey('forum_post.id'), nullable=False) + post_id = db.Column(db.Integer, db.ForeignKey('forumpost.id'), nullable=False) created_by = db.Column(db.String(100), nullable=False) creation_date = db.Column(db.DateTime, nullable=False) - media = db.Column(db.String(100)) + media = db.Column(db.Integer) # Assuming 'media' is a column containing media IDs text = db.Column(db.Text, nullable=False) edited = db.Column(db.Boolean, default=False) + # Define a primaryjoin expression + post = relationship("ForumPost", primaryjoin="foreign(ForumComment.post_id) == remote(ForumPost.id)") + +class Media(db.Model): + __tablename__ = 'media' + id = db.Column(db.Integer, primary_key=True) + post_id = db.Column(db.Integer, db.ForeignKey('forumpost.id'), nullable=False) + filename = db.Column(db.String(100), nullable=False) + +class CreatePostForm(FlaskForm): + category_id = SelectField('Category', validators=[InputRequired()], coerce=int) + post_name = StringField('Post Title', validators=[InputRequired()]) + created_by = StringField('Your Name', validators=[InputRequired()]) + text = TextAreaField('Post Content', validators=[InputRequired()]) + media = FileField('Insert Media', validators=[FileAllowed(app.config['ALLOWED_EXTENSIONS'])]) + def admin_login_required(view_func): @wraps(view_func) def decorated_function(*args, **kwargs): @@ -64,13 +94,24 @@ class MyAdminIndexView(AdminIndexView): class UserAdminView(ModelView): column_exclude_list = ['password'] - form_excluded_columns = ['password'] can_export = True export_types = ['csv'] class PostAdminView(ModelView): can_export = True export_types = ['csv'] + +class ForumCategoryAdminView(ModelView): + can_export = True + export_types = ['csv'] + +class ForumPostAdminView(ModelView): + can_export = True + export_types = ['csv'] + +class ForumCommentAdminView(ModelView): + can_export = True + export_types = ['csv'] class LogoutView(BaseView): @expose('/') @@ -81,6 +122,9 @@ class LogoutView(BaseView): admin = Admin(app, name='Admin Panel', template_mode='bootstrap3', index_view=MyAdminIndexView()) admin.add_view(UserAdminView(User, db.session)) admin.add_view(PostAdminView(Post, db.session)) +admin.add_view(ForumCategoryAdminView(ForumCategory, db.session)) +admin.add_view(ForumPostAdminView(ForumPost, db.session)) +admin.add_view(ForumCommentAdminView(ForumComment, db.session)) admin.add_view(LogoutView(name='Logout', endpoint='admin_logout')) @app.before_request @@ -173,51 +217,77 @@ def post(alias): return render_template(f"{alias}.html", post_info=post_info) else: return "Post not found", 404 - + @app.route('/forums') def forums(): categories = ForumCategory.query.all() - posts = ForumPost.query.all() - comments = ForumComment.query.all() - return render_template('forums.html', categories=categories, posts=posts, comments=comments) + return render_template('forums.html', categories=categories) -@app.route('/forums/<int:category_id>') -def category(category_id): +@app.route('/forums/<category_name>') +def category(category_name): + category_name = category_name.capitalize() + print("Received category name:", category_name) + category = ForumCategory.query.filter_by(category_name=category_name).first() + if category: + posts = ForumPost.query.filter_by(category_id=category.id).limit(10).all() + return render_template('category.html', category=category, posts=posts) + else: + return "Category not found", 404 + +@app.route('/forums/<int:category_id>/create_post', methods=['GET', 'POST']) +def create_post(category_id): + form = CreatePostForm() category = ForumCategory.query.get_or_404(category_id) - posts = ForumPost.query.filter_by(category_id=category_id).all() - return render_template('category.html', category=category, posts=posts) - -@app.route('/forums/<int:post_id>') -def new_post(post_id): - post = ForumPost.query.get_or_404(post_id) - comments = ForumComment.query.filter_by(post_id=post_id).all() - return render_template('post.html', post=post, comments=comments) - -@app.route('/forums/create_post', methods=['GET', 'POST']) -def create_post(): - if request.method == 'POST': - category_id = request.form['category_id'] - post_name = request.form['post_name'] - created_by = request.form['created_by'] - text = request.form['text'] - new_post = ForumPost(category_id=category_id, post_name=post_name, created_by=created_by, text=text) # type: ignore + + # Provide choices for category_id field + form.category_id.choices = [(category.id, category.category_name) for category in ForumCategory.query.all()] + + if form.validate_on_submit(): + post_name = form.post_name.data + created_by = form.created_by.data + text = form.text.data + media_files = request.files.getlist('media') + + media_filenames = [] + for file in media_files: + if file: + filename = secure_filename(file.filename) # type: ignore + file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) + media_filenames.append(filename) + + new_post = ForumPost( + category_id=category_id, + post_name=post_name, + created_by=created_by, + text=text, + creation_date=datetime.now(), + edited=False + ) # type: ignore db.session.add(new_post) db.session.commit() - return redirect(url_for('forums')) - else: - categories = ForumCategory.query.all() - return render_template('create_post.html', categories=categories) + + for filename in media_filenames: + new_media = Media(post_id=new_post.id, filename=filename) # type: ignore + db.session.add(new_media) + + db.session.commit() + + return redirect(url_for('category', category_id=category_id)) + + return render_template('create_post.html', form=form, category=category) -@app.route('/forums/create_comment', methods=['POST']) # type: ignore -def create_comment(): +@app.route('/forums/<int:post_id>', methods=['GET', 'POST']) +def view_post(post_id): + post = ForumPost.query.get_or_404(post_id) + comments = ForumComment.query.filter_by(post_id=post_id).all() if request.method == 'POST': - post_id = request.form['post_id'] created_by = request.form['created_by'] text = request.form['text'] - new_comment = ForumComment(post_id=post_id, created_by=created_by, text=text) # type: ignore + new_comment = ForumComment(post_id=post_id, created_by=created_by, text=text) # type: ignore db.session.add(new_comment) db.session.commit() - return redirect(url_for('post', post_id=post_id)) + return redirect(url_for('view_post', post_id=post_id)) + return render_template('post.html', post=post, comments=comments) @app.route('/forums/upvote_post/<int:post_id>') def upvote_post(post_id): diff --git a/templates/base.html b/templates/base.html index 0495710..67d46f3 100644 --- a/templates/base.html +++ b/templates/base.html @@ -24,8 +24,7 @@ <a href="#" class="dropbtn">Docs</a> <div class="dropdown-content"> <a href="https://gitea.rkg.lv/elukjanovica/Picture_Puzzle">Source page</a> - <a href="#">Documentation</a> - <a href="#">Tutorials</a> + <a href="gitea.rkg.lv/elukjanovica/Picture_Puzzle/wiki">Documentation</a> </div> </li> <li class="dropdown"> diff --git a/templates/category.html b/templates/category.html index f28c151..f37d046 100644 --- a/templates/category.html +++ b/templates/category.html @@ -7,6 +7,7 @@ <div class="row"> <div class="col-md-12"> <h1>{{ category.category_name }}</h1> + <a href="{{ url_for('create_post', category_id=category.id) }}" class="btn btn-primary mb-3">Create Post</a> <div class="card mt-4"> <div class="card-body"> <div class="list-group"> diff --git a/templates/create_post.html b/templates/create_post.html new file mode 100644 index 0000000..a9d35b9 --- /dev/null +++ b/templates/create_post.html @@ -0,0 +1,169 @@ +{% extends 'base.html' %} + +{% block title %}Create Post{% endblock %} + +{% block content %} +<style> + .form-group { + margin-bottom: 20px; + position: relative; + } + + .container { + display: flex; + justify-content: center; + } + + form { + width: 120%; + padding: 20px; + border-radius: 5px; + background-color: #343a40; + box-shadow: 0 0 10px rgba(255, 255, 255, 0.1); + } + + input[type="text"], + textarea { + width: calc(100% - 24px); + padding: 10px; + border-radius: 5px; + cursor: text; + } + + input[type="file"] { + width: calc(100% - 24px); + padding: 10px; + border-radius: 5px; + position: absolute; + left: 0; + top: 0; + opacity: 0; + cursor: pointer; + } + + .btn-primary { + background-color: #007bff; + border: none; + color: #fff; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + box-shadow: rgba(45, 35, 66, 0.4) 0 2px 4px, rgba(45, 35, 66, 0.3) 0 7px 13px -3px, #0051a8 0 -3px 0 inset; + } + + button:focus { + box-shadow: #0051a8 0 0 0 1.5px inset, rgba(45, 35, 66, 0.4) 0 2px 4px, rgba(45, 35, 66, 0.3) 0 7px 13px -3px, #0051a8 0 -3px 0 inset; + } + + .btn-primary:hover { + box-shadow: rgba(45, 35, 66, 0.4) 0 4px 8px, rgba(45, 35, 66, 0.3) 0 7px 13px -3px, #0051a8 0 -3px 0 inset; + } + + .note { + font-style: italic; + color: #ccc; + } + + .custom-file-upload { + border: 1px solid #ccc; + display: inline-block; + padding: 10px; + cursor: pointer; + width: calc(100% - 44px); + border-radius: 5px; + background-color: #fff; + color: #333; + text-align: center; + } +</style> + +<div class="container"> + <div class="row"> + <div class="col-md-12"> + <h1>Create a New Post</h1> + <form method="POST" enctype="multipart/form-data"> + <div class="form-group"> + <input type="text" class="form-control" id="post_name" name="post_name" placeholder="Title" required> + </div> + <div class="form-group"> + <textarea class="form-control" id="text" name="text" rows="6" placeholder="Content" required></textarea> + </div> + <label for="media" class="custom-file-upload" id="drop-area"> + Click or drag & drop to upload media + </label> + <input type="file" id="media" name="media" style="display: none;" multiple><br> + <ul id="file-list" class="file-list"></ul> + <button type="submit" class="btn btn-primary">Submit</button> + </form> + <p class="note"><strong>Note:</strong> **bold** for bold text, *italic* for italic text</p> + </div> + </div> +</div> + +<script> + document.addEventListener('DOMContentLoaded', function() { + const textarea = document.getElementById('text'); + const fileList = document.getElementById('file-list'); + const dropArea = document.getElementById('drop-area'); + const fileInput = document.getElementById('media'); + + textarea.addEventListener('input', function() { + const text = textarea.value; + const boldRegex = /\*\*(.*?)\*\*/g; + const italicRegex = /\*(.*?)\*/g; + const newText = text.replace(boldRegex, '<strong>$1</strong>').replace(italicRegex, '<em>$1</em>'); + textarea.innerHTML = newText; + }); + + ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { + dropArea.addEventListener(eventName, preventDefaults, false); + }); + + function preventDefaults(e) { + e.preventDefault(); + e.stopPropagation(); + } + + ['dragenter', 'dragover'].forEach(eventName => { + dropArea.addEventListener(eventName, highlight, false); + }); + + ['dragleave', 'drop'].forEach(eventName => { + dropArea.addEventListener(eventName, unhighlight, false); + }); + + function highlight() { + dropArea.style.backgroundColor = '#f0f0f0'; + } + + function unhighlight() { + dropArea.style.backgroundColor = '#fff'; + } + + dropArea.addEventListener('drop', handleDrop, false); + + function handleDrop(e) { + const dt = e.dataTransfer; + const files = dt.files; + + handleFiles(files); + } + + function handleFiles(files) { + fileList.innerHTML = ''; + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const listItem = document.createElement('li'); + listItem.textContent = file.name; + listItem.classList.add('file-list-item'); + fileList.appendChild(listItem); + } + } + + fileInput.addEventListener('change', function() { + handleFiles(this.files); + }); + }); +</script> + +{% endblock %} diff --git a/templates/forums.html b/templates/forums.html index cef9744..85df388 100644 --- a/templates/forums.html +++ b/templates/forums.html @@ -12,13 +12,17 @@ <div class="card-header">{{ category.category_name }}</div> <div class="card-body"> <div class="list-group"> + {% set count = 0 %} {% for post in posts %} {% if post.category_id == category.id %} + {% if count < 10 %} <a href="{{ url_for('new_post', post_id=post.id) }}" class="list-group-item list-group-item-action"> <h5 class="mb-1">{{ post.post_name }}</h5> <p class="mb-1">{{ post.text }}</p> <small>Created by {{ post.created_by }} - {{ post.creation_date }}</small> </a> + {% set count = count + 1 %} + {% endif %} {% endif %} {% endfor %} </div>