diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc index 1ffc7ea..3f1a0c3 100644 Binary files a/__pycache__/app.cpython-313.pyc and b/__pycache__/app.cpython-313.pyc differ diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc index 4f74996..7136ff4 100644 Binary files a/__pycache__/models.cpython-313.pyc and b/__pycache__/models.cpython-313.pyc differ diff --git a/app.py b/app.py index ec7e857..5f32566 100644 --- a/app.py +++ b/app.py @@ -24,7 +24,7 @@ from flask_migrate import Migrate from models import ( db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType, Category, UserMindmap, UserMindmapNode, MindmapNote, - node_thought_association, user_thought_bookmark, node_relationship + node_thought_association, user_thought_bookmark, node_relationship, ForumCategory, ForumPost ) # Lade .env-Datei @@ -190,6 +190,31 @@ def create_default_categories(): db.session.commit() print("Standard-Kategorien wurden erstellt!") +def create_forum_categories(): + """Erstellt Forum-Kategorien basierend auf Hauptknotenpunkten der Mindmap""" + # Hauptknotenpunkte abrufen (nur die, die keine Elternknoten haben) + main_nodes = MindMapNode.query.filter(~MindMapNode.id.in_( + db.session.query(node_relationship.c.child_id) + )).all() + + for node in main_nodes: + # Prüfen, ob eine Forum-Kategorie für diesen Knoten bereits existiert + existing_category = ForumCategory.query.filter_by(node_id=node.id).first() + if existing_category: + continue + + # Neue Kategorie erstellen + forum_category = ForumCategory( + node_id=node.id, + title=node.name, + description=node.description, + is_active=True + ) + db.session.add(forum_category) + + db.session.commit() + print("Forum-Kategorien wurden für alle Hauptknotenpunkte erstellt!") + def initialize_database(): """Initialisiert die Datenbank mit Grunddaten, falls diese leer ist""" try: @@ -198,97 +223,34 @@ def initialize_database(): # Erstelle alle Tabellen db.create_all() - # Prüfe, ob bereits Benutzer existieren - if User.query.count() == 0: - print("Erstelle Admin-Benutzer...") - admin = User( - username="admin", - email="admin@example.com", - is_admin=True - ) - admin.set_password("admin123") # In echter Umgebung ein sicheres Passwort verwenden! - db.session.add(admin) + # Prüfen, ob bereits Kategorien existieren + categories_count = Category.query.count() + users_count = User.query.count() - # Prüfe, ob bereits Kategorien existieren - if Category.query.count() == 0: - print("Erstelle Standard-Kategorien...") + # Erstelle Standarddaten, wenn es keine Kategorien gibt + if categories_count == 0: create_default_categories() - # Stelle sicher, dass die Standard-Knoten für die öffentliche Mindmap existieren - if MindMapNode.query.count() == 0: - print("Erstelle Standard-Knoten für die Mindmap...") - - # Hauptknoten: Wissen - root_node = MindMapNode( - name="Wissen", - description="Zentrale Wissensbasis", - color_code="#4299E1", - is_public=True + # Admin-Benutzer erstellen, wenn keine Benutzer vorhanden sind + if users_count == 0: + admin_user = User( + username="admin", + email="admin@example.com", + role="admin", + is_active=True ) - db.session.add(root_node) - db.session.flush() # Um die ID zu generieren - - # Verwandte Kategorien finden - philosophy = Category.query.filter_by(name="Philosophie").first() - science = Category.query.filter_by(name="Wissenschaft").first() - technology = Category.query.filter_by(name="Technologie").first() - arts = Category.query.filter_by(name="Künste").first() - - # Erstelle Hauptthemenknoten - nodes = [ - MindMapNode( - name="Philosophie", - description="Philosophisches Denken", - color_code="#9F7AEA", - category=philosophy, - is_public=True - ), - MindMapNode( - name="Wissenschaft", - description="Wissenschaftliche Erkenntnisse", - color_code="#48BB78", - category=science, - is_public=True - ), - MindMapNode( - name="Technologie", - description="Technologische Entwicklungen", - color_code="#ED8936", - category=technology, - is_public=True - ), - MindMapNode( - name="Künste", - description="Künstlerische Ausdrucksformen", - color_code="#ED64A6", - category=arts, - is_public=True - ) - ] - - # Füge Knoten zur Datenbank hinzu - for node in nodes: - db.session.add(node) - + admin_user.set_password("admin123") # Sicheres Passwort in der Produktion verwenden! + db.session.add(admin_user) db.session.commit() - - # Nachdem wir die IDs haben, füge die Verbindungen hinzu - all_nodes = MindMapNode.query.all() - root = MindMapNode.query.filter_by(name="Wissen").first() - - if root: - for node in all_nodes: - if node.id != root.id: - root.children.append(node) - - # Speichere die Änderungen - db.session.commit() - - print("Datenbankinitialisierung abgeschlossen.") + print("Admin-Benutzer wurde erstellt!") + + # Forum-Kategorien erstellen + create_forum_categories() + + return True except Exception as e: - print(f"Fehler bei der Datenbankinitialisierung: {str(e)}") - db.session.rollback() - raise + print(f"Fehler bei Datenbank-Initialisierung: {e}") + return False # Instead of before_first_request, which is deprecated in newer Flask versions # Use a function to initialize the database that will be called during app creation @@ -1521,6 +1483,251 @@ def refresh_mindmap(): def mindmap_page(): return render_template('mindmap.html') +# Community-Forum-Routen +@app.route('/community') +@login_required +def community(): + """Hauptseite des Community-Forums""" + forum_categories = ForumCategory.query.filter_by(is_active=True).all() + + # Statistiken für jede Kategorie berechnen + categories_data = [] + for category in forum_categories: + total_posts = ForumPost.query.filter_by(category_id=category.id, parent_id=None).count() + total_replies = ForumPost.query.filter( + ForumPost.category_id == category.id, + ForumPost.parent_id != None + ).count() + latest_post = ForumPost.query.filter_by(category_id=category.id)\ + .order_by(ForumPost.created_at.desc()).first() + + categories_data.append({ + 'category': category, + 'total_posts': total_posts, + 'total_replies': total_replies, + 'latest_post': latest_post + }) + + return render_template('community/index.html', categories_data=categories_data) + +@app.route('/community/category/') +@login_required +def forum_category(category_id): + """Zeigt alle Themen in einer bestimmten Kategorie an""" + category = ForumCategory.query.get_or_404(category_id) + + # Haupt-Beiträge (Threads) in dieser Kategorie + threads = ForumPost.query.filter_by( + category_id=category_id, + parent_id=None + ).order_by(ForumPost.is_pinned.desc(), ForumPost.created_at.desc()).all() + + # Zähle Antworten und hole den neuesten Beitrag für jeden Thread + threads_data = [] + for thread in threads: + reply_count = ForumPost.query.filter_by(parent_id=thread.id).count() + latest_reply = ForumPost.query.filter_by(parent_id=thread.id)\ + .order_by(ForumPost.created_at.desc()).first() + + threads_data.append({ + 'thread': thread, + 'reply_count': reply_count, + 'latest_reply': latest_reply + }) + + return render_template('community/category.html', + category=category, + threads_data=threads_data, + node=category.node) + +@app.route('/community/post/') +@login_required +def forum_post(post_id): + """Zeigt einen Beitrag und alle seine Antworten an""" + post = ForumPost.query.get_or_404(post_id) + + # Wenn es sich um eine Antwort handelt, leite zur übergeordneten Beitragsseite weiter + if post.parent_id: + return redirect(url_for('forum_post', post_id=post.parent_id)) + + # Erhöhe die Ansichtszahl + post.view_count += 1 + db.session.commit() + + # Hole alle Antworten zu diesem Beitrag + replies = ForumPost.query.filter_by(parent_id=post_id)\ + .order_by(ForumPost.created_at).all() + + return render_template('community/post.html', + post=post, + replies=replies, + category=post.category) + +@app.route('/community/new-post/', methods=['GET', 'POST']) +@login_required +def new_post(category_id): + """Erstellt einen neuen Beitrag in einer Kategorie""" + category = ForumCategory.query.get_or_404(category_id) + + if request.method == 'POST': + title = request.form.get('title') + content = request.form.get('content') + + if not title or not content: + flash('Bitte fülle alle Pflichtfelder aus.', 'error') + return redirect(url_for('new_post', category_id=category_id)) + + # Neuen Beitrag erstellen + post = ForumPost( + title=title, + content=content, + user_id=current_user.id, + category_id=category_id + ) + db.session.add(post) + db.session.commit() + + flash('Dein Beitrag wurde erfolgreich erstellt.', 'success') + return redirect(url_for('forum_post', post_id=post.id)) + + return render_template('community/new_post.html', category=category) + +@app.route('/community/reply/', methods=['POST']) +@login_required +def reply_to_post(post_id): + """Antwortet auf einen bestehenden Beitrag""" + parent_post = ForumPost.query.get_or_404(post_id) + + # Stelle sicher, dass der Beitrag nicht gesperrt ist + if parent_post.is_locked: + flash('Dieser Beitrag ist gesperrt und kann keine Antworten mehr erhalten.', 'error') + return redirect(url_for('forum_post', post_id=post_id)) + + content = request.form.get('content') + + if not content: + flash('Die Antwort darf nicht leer sein.', 'error') + return redirect(url_for('forum_post', post_id=post_id)) + + # Erstelle eine Antwort + reply = ForumPost( + title=f"Re: {parent_post.title}", + content=content, + user_id=current_user.id, + category_id=parent_post.category_id, + parent_id=post_id + ) + db.session.add(reply) + db.session.commit() + + flash('Deine Antwort wurde erfolgreich hinzugefügt.', 'success') + return redirect(url_for('forum_post', post_id=post_id)) + +@app.route('/community/edit-post/', methods=['GET', 'POST']) +@login_required +def edit_post(post_id): + """Bearbeitet einen bestehenden Beitrag""" + post = ForumPost.query.get_or_404(post_id) + + # Überprüfe, ob der Benutzer der Autor ist oder Admin-Rechte hat + if post.user_id != current_user.id and current_user.role != 'admin': + flash('Du hast keine Berechtigung, diesen Beitrag zu bearbeiten.', 'error') + return redirect(url_for('forum_post', post_id=post.parent_id or post.id)) + + if request.method == 'POST': + title = request.form.get('title') + content = request.form.get('content') + + if not title or not content: + flash('Bitte fülle alle Pflichtfelder aus.', 'error') + return redirect(url_for('edit_post', post_id=post_id)) + + # Aktualisiere den Beitrag + post.title = title + post.content = content + post.updated_at = datetime.utcnow() + db.session.commit() + + flash('Dein Beitrag wurde erfolgreich aktualisiert.', 'success') + return redirect(url_for('forum_post', post_id=post.parent_id or post.id)) + + return render_template('community/edit_post.html', post=post) + +@app.route('/community/delete-post/', methods=['POST']) +@login_required +def delete_post(post_id): + """Löscht einen Beitrag""" + post = ForumPost.query.get_or_404(post_id) + + # Überprüfe, ob der Benutzer der Autor ist oder Admin-Rechte hat + if post.user_id != current_user.id and current_user.role != 'admin': + flash('Du hast keine Berechtigung, diesen Beitrag zu löschen.', 'error') + return redirect(url_for('forum_post', post_id=post.parent_id or post.id)) + + # Bestimme, wohin nach dem Löschen weitergeleitet wird + if post.parent_id: + redirect_url = url_for('forum_post', post_id=post.parent_id) + else: + redirect_url = url_for('forum_category', category_id=post.category_id) + + # Lösche den Beitrag und seine Antworten + if not post.parent_id: # Wenn es ein Hauptbeitrag ist, lösche auch alle Antworten + ForumPost.query.filter_by(parent_id=post_id).delete() + + db.session.delete(post) + db.session.commit() + + flash('Der Beitrag wurde erfolgreich gelöscht.', 'success') + return redirect(redirect_url) + +@app.route('/community/toggle-pin/', methods=['POST']) +@login_required +def toggle_pin_post(post_id): + """Fixiert oder löst einen Beitrag von der Fixierung""" + # Nur Admins und Moderatoren können Beiträge fixieren + if current_user.role not in ['admin', 'moderator']: + flash('Du hast keine Berechtigung, Beiträge zu fixieren.', 'error') + return redirect(url_for('forum_post', post_id=post_id)) + + post = ForumPost.query.get_or_404(post_id) + + # Nur Hauptbeiträge können fixiert werden + if post.parent_id: + flash('Nur Hauptbeiträge können fixiert werden.', 'error') + return redirect(url_for('forum_post', post_id=post.parent_id)) + + # Ändere den Status + post.is_pinned = not post.is_pinned + db.session.commit() + + status = 'fixiert' if post.is_pinned else 'nicht mehr fixiert' + flash(f'Der Beitrag ist jetzt {status}.', 'success') + return redirect(url_for('forum_post', post_id=post_id)) + +@app.route('/community/toggle-lock/', methods=['POST']) +@login_required +def toggle_lock_post(post_id): + """Sperrt oder entsperrt einen Beitrag für weitere Antworten""" + # Nur Admins und Moderatoren können Beiträge sperren + if current_user.role not in ['admin', 'moderator']: + flash('Du hast keine Berechtigung, Beiträge zu sperren.', 'error') + return redirect(url_for('forum_post', post_id=post_id)) + + post = ForumPost.query.get_or_404(post_id) + + # Nur Hauptbeiträge können gesperrt werden + if post.parent_id: + flash('Nur Hauptbeiträge können gesperrt werden.', 'error') + return redirect(url_for('forum_post', post_id=post.parent_id)) + + # Ändere den Status + post.is_locked = not post.is_locked + db.session.commit() + + status = 'gesperrt' if post.is_locked else 'entsperrt' + flash(f'Der Beitrag ist jetzt {status}.', 'success') + return redirect(url_for('forum_post', post_id=post_id)) + # Fehlerbehandlung @app.errorhandler(404) def not_found(e): diff --git a/templates/base.html b/templates/base.html index ec774bd..b68067b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -278,6 +278,13 @@ : '{{ 'nav-link-light-active' if request.endpoint == 'mindmap' else 'nav-link-light' }}'"> Mindmap + + Community + Mindmap + + Community + Mindmap + + Community + {% if current_user.is_authenticated %} diff --git a/templates/community/category.html b/templates/community/category.html new file mode 100644 index 0000000..4cea15e --- /dev/null +++ b/templates/community/category.html @@ -0,0 +1,192 @@ +{% extends 'base.html' %} + +{% block title %}{{ category.title }} - Forum{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+ +
+ + Forum + + / + {{ category.title }} +
+ + +
+
+ +
+ +
+ + +
+

{{ category.title }}

+

{{ category.description }}

+
+
+ + + + + Neues Thema + +
+ + + + + +
+
+ +
+
+

Mindmap-Knotenpunkt: {{ node.name }}

+

In der Mindmap findest du weitere Informationen zu diesem Themenbereich.

+
+ +
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/templates/community/edit_post.html b/templates/community/edit_post.html new file mode 100644 index 0000000..8c77501 --- /dev/null +++ b/templates/community/edit_post.html @@ -0,0 +1,344 @@ +{% extends 'base.html' %} + +{% block title %}Beitrag bearbeiten{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+ +
+ + Forum + + / + + {{ post.category.title }} + + / + {% if post.parent_id %} + + {{ post.parent.title }} + + / + {% endif %} + Beitrag bearbeiten +
+ + +
+

Beitrag bearbeiten

+

+ {% if post.parent_id %} + Antwort auf {{ post.parent.title }} + {% else %} + in der Kategorie {{ post.category.title }} + {% endif %} +

+
+ + +
+
+ + Beitrag bearbeiten +
+
+
+
+ +
+ +
+
+ +
+
+ +
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+
+

Du kannst Knotenpunkte der Mindmap durch @Knotenname verlinken.

+

Dieser Editor unterstützt Markdown-Formatierung:

+
+ + + + + + + + + +
+
+
+
+ +
+ + Abbrechen + + +
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/templates/community/index.html b/templates/community/index.html new file mode 100644 index 0000000..aef5a13 --- /dev/null +++ b/templates/community/index.html @@ -0,0 +1,125 @@ +{% extends 'base.html' %} + +{% block title %}Community Forum{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+ +
+

Community Forum

+

Diskutiere mit anderen Nutzern über die Hauptthemenbereiche der Mindmap

+
+ + + + + +
+

+ + So funktioniert das Forum +

+

Das Community-Forum ist nach den Hauptknotenpunkten der Systades-Mindmap strukturiert. + In deinen Beiträgen kannst du Knotenpunkte mit @Knotenname verlinken.

+
+
+
+

Fachliche Diskussionen

+

Tausche dich mit anderen zu spezifischen Themen aus

+
+
+
+

Wissensvernetzung

+

Verknüpfe Inhalte durch Knotenreferenzen

+
+
+
+

Markdown Support

+

Formatiere deine Beiträge mit Markdown

+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/templates/community/new_post.html b/templates/community/new_post.html new file mode 100644 index 0000000..df4f115 --- /dev/null +++ b/templates/community/new_post.html @@ -0,0 +1,355 @@ +{% extends 'base.html' %} + +{% block title %}Neues Thema - {{ category.title }}{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+ +
+ + Forum + + / + + {{ category.title }} + + / + Neues Thema +
+ + +
+

Neues Thema erstellen

+

in der Kategorie {{ category.title }}

+
+ + +
+
+ + Neues Thema +
+
+
+
+ +
+ +
+
+ +
+
+ +
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+
+

Du kannst Knotenpunkte der Mindmap durch @Knotenname verlinken.

+

Dieser Editor unterstützt Markdown-Formatierung:

+
+ + + + + + + + + +
+
+
+
+ +
+ + Abbrechen + + +
+
+
+
+ + +
+
+ +
+
+

Mindmap-Knotenpunkt: {{ category.node.name }}

+

Dieser Diskussionsbereich ist mit dem Mindmap-Knotenpunkt "{{ category.node.name }}" verknüpft.

+
+ +
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/templates/community/post.html b/templates/community/post.html new file mode 100644 index 0000000..695b4db --- /dev/null +++ b/templates/community/post.html @@ -0,0 +1,491 @@ +{% extends 'base.html' %} + +{% block title %}{{ post.title }} - Forum{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+ +
+ + Forum + + / + + {{ category.title }} + + / + {{ post.title }} +
+ + +
+

{{ post.title }}

+
+ {{ post.created_at.strftime('%d.%m.%Y, %H:%M') }} + {{ post.view_count }} Aufrufe + {{ replies|length }} Antworten + + {% if post.is_pinned or post.is_locked %} +
+ {% if post.is_pinned %} + + Angepinnt + + {% endif %} + {% if post.is_locked %} + + Gesperrt + + {% endif %} +
+ {% endif %} +
+
+ + +
+ +
+
+
+ +
+ {% if post.author.avatar %} + {{ post.author.username }} + {% else %} + {{ post.author.username[0].upper() }} + {% endif %} +
+ + +
+
{{ post.author.username }}
+
Erstellt am {{ post.created_at.strftime('%d.%m.%Y, %H:%M') }}
+
+
+ + +
+ {% if current_user.id == post.user_id or current_user.role == 'admin' %} + + + +
+ +
+ {% endif %} + + + {% if current_user.role in ['admin', 'moderator'] %} +
+
+ +
+
+ +
+ {% endif %} +
+
+
+ + +
+
+ {{ post.content|safe }} +
+ + {% if post.updated_at and post.updated_at != post.created_at %} +
+ Zuletzt bearbeitet: {{ post.updated_at.strftime('%d.%m.%Y, %H:%M') }} +
+ {% endif %} +
+
+ + +
+

+ + {{ replies|length }} Antworten +

+ + + {% if replies %} + {% for reply in replies %} +
+ +
+
+
+ +
+ {% if reply.author.avatar %} + {{ reply.author.username }} + {% else %} + {{ reply.author.username[0].upper() }} + {% endif %} +
+ + +
+
{{ reply.author.username }}
+
{{ reply.created_at.strftime('%d.%m.%Y, %H:%M') }}
+
+
+ + +
+ {% if current_user.id == reply.user_id or current_user.role == 'admin' %} + + + +
+ +
+ {% endif %} +
+
+
+ + +
+
+ {{ reply.content|safe }} +
+ + {% if reply.updated_at and reply.updated_at != reply.created_at %} +
+ Zuletzt bearbeitet: {{ reply.updated_at.strftime('%d.%m.%Y, %H:%M') }} +
+ {% endif %} +
+
+ {% endfor %} + {% else %} +
+
+

Noch keine Antworten

+

Sei der Erste, der auf diesen Beitrag antwortet!

+
+ {% endif %} +
+ + + {% if not post.is_locked %} +
+
+ + Antworten +
+
+
+
+ +
+ +
+
+

Du kannst Knotenpunkte der Mindmap durch @Knotenname verlinken.

+

Dieser Editor unterstützt Markdown-Formatierung:

+
+ + + + + + + + + +
+
+
+
+ +
+
+
+
+ {% else %} +
+ + Dieser Beitrag ist geschlossen. Es können keine neuen Antworten mehr verfasst werden. +
+ {% endif %} +
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file