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>