diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc index ae90487..7da0fcd 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 3cc391e..70d7675 100644 Binary files a/__pycache__/models.cpython-313.pyc and b/__pycache__/models.cpython-313.pyc differ diff --git a/migrations/versions/add_help_articles_table.py b/migrations/versions/add_help_articles_table.py new file mode 100644 index 0000000..09cb6a2 --- /dev/null +++ b/migrations/versions/add_help_articles_table.py @@ -0,0 +1,56 @@ +"""add help articles table + +Revision ID: add_help_articles_table +Revises: c94c2b2b9f2e +Create Date: 2024-12-19 10:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy import text + + +# revision identifiers, used by Alembic. +revision = 'add_help_articles_table' +down_revision = 'c94c2b2b9f2e' +branch_labels = None +depends_on = None + + +def upgrade(): + conn = op.get_bind() + result = conn.execute(text(""" + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'help_articles' + ); + """)) + exists = result.scalar() + if not exists: + op.create_table('help_articles', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=200), nullable=False), + sa.Column('category', sa.String(length=50), nullable=False), + sa.Column('body', sa.Text(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=False), + sa.Column('is_published', sa.Boolean(), nullable=True, server_default='true'), + sa.Column('order_index', sa.Integer(), nullable=True, server_default='0'), + sa.ForeignKeyConstraint(['created_by'], ['user.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + + # Create indexes for better performance + op.create_index('idx_help_articles_category', 'help_articles', ['category']) + op.create_index('idx_help_articles_published', 'help_articles', ['is_published']) + op.create_index('idx_help_articles_order', 'help_articles', ['order_index']) + op.create_index('idx_help_articles_created_at', 'help_articles', ['created_at']) + + +def downgrade(): + op.drop_index('idx_help_articles_category', table_name='help_articles') + op.drop_index('idx_help_articles_published', table_name='help_articles') + op.drop_index('idx_help_articles_order', table_name='help_articles') + op.drop_index('idx_help_articles_created_at', table_name='help_articles') + op.drop_table('help_articles') \ No newline at end of file diff --git a/models.py b/models.py index 1d0c26e..2e39b4b 100644 --- a/models.py +++ b/models.py @@ -537,4 +537,53 @@ class Instance(db.Model): updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP'), onupdate=db.text('CURRENT_TIMESTAMP')) def __repr__(self): - return f'' \ No newline at end of file + return f'' + +class HelpArticle(db.Model): + __tablename__ = 'help_articles' + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(200), nullable=False) + category = db.Column(db.String(50), nullable=False) # getting-started, user-management, file-management, communication, security, administration + body = db.Column(db.Text, nullable=False) # Rich text content + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + created_by = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False) + is_published = db.Column(db.Boolean, default=True) + order_index = db.Column(db.Integer, default=0) # For ordering articles within categories + + # Relationships + creator = db.relationship('User', backref=db.backref('created_help_articles', cascade='all, delete-orphan'), foreign_keys=[created_by]) + + def __repr__(self): + return f'' + + @classmethod + def get_categories(cls): + """Get all available categories with their display names""" + return { + 'getting-started': 'Getting Started', + 'user-management': 'User Management', + 'file-management': 'File Management', + 'communication': 'Communication', + 'security': 'Security & Privacy', + 'administration': 'Administration' + } + + @classmethod + def get_articles_by_category(cls, category, published_only=True): + """Get articles for a specific category""" + query = cls.query.filter_by(category=category) + if published_only: + query = query.filter_by(is_published=True) + return query.order_by(cls.order_index.asc(), cls.created_at.desc()).all() + + @classmethod + def get_all_published(cls): + """Get all published articles grouped by category""" + articles = cls.query.filter_by(is_published=True).order_by(cls.order_index.asc(), cls.created_at.desc()).all() + grouped = {} + for article in articles: + if article.category not in grouped: + grouped[article.category] = [] + grouped[article.category].append(article) + return grouped \ No newline at end of file diff --git a/routes/__pycache__/admin.cpython-313.pyc b/routes/__pycache__/admin.cpython-313.pyc index 8037f5b..1c23c7e 100644 Binary files a/routes/__pycache__/admin.cpython-313.pyc and b/routes/__pycache__/admin.cpython-313.pyc differ diff --git a/routes/admin.py b/routes/admin.py index 2233d75..df1e99a 100644 --- a/routes/admin.py +++ b/routes/admin.py @@ -253,4 +253,189 @@ def get_usage_stats(): stats = DocuPulseSettings.get_usage_stats() return jsonify(stats) except Exception as e: - return jsonify({'error': str(e)}), 500 \ No newline at end of file + return jsonify({'error': str(e)}), 500 + +@admin.route('/help-articles/', methods=['PUT']) +@login_required +def update_help_article(article_id): + """Update a help article""" + if not current_user.is_admin: + return jsonify({'error': 'Unauthorized'}), 403 + + try: + article = HelpArticle.query.get_or_404(article_id) + + title = request.form.get('title') + category = request.form.get('category') + body = request.form.get('body') + order_index = int(request.form.get('order_index', 0)) + is_published = request.form.get('is_published') == 'true' + + if not title or not category or not body: + return jsonify({'error': 'Title, category, and body are required'}), 400 + + # Validate category + valid_categories = HelpArticle.get_categories().keys() + if category not in valid_categories: + return jsonify({'error': 'Invalid category'}), 400 + + article.title = title + article.category = category + article.body = body + article.order_index = order_index + article.is_published = is_published + article.updated_at = datetime.utcnow() + + db.session.commit() + + # Log the event + log_event( + event_type='help_article_update', + details={ + 'article_id': article.id, + 'title': article.title, + 'category': article.category, + 'updated_by': f"{current_user.username} {current_user.last_name}" + }, + user_id=current_user.id + ) + db.session.commit() + + return jsonify({'success': True, 'message': 'Article updated successfully'}) + + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + +@admin.route('/help-articles/', methods=['DELETE']) +@login_required +def delete_help_article(article_id): + """Delete a help article""" + if not current_user.is_admin: + return jsonify({'error': 'Unauthorized'}), 403 + + try: + article = HelpArticle.query.get_or_404(article_id) + + # Log the event before deletion + log_event( + event_type='help_article_delete', + details={ + 'article_id': article.id, + 'title': article.title, + 'category': article.category, + 'deleted_by': f"{current_user.username} {current_user.last_name}" + }, + user_id=current_user.id + ) + + db.session.delete(article) + db.session.commit() + + return jsonify({'success': True, 'message': 'Article deleted successfully'}) + + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + +# Help Articles API endpoints +@admin.route('/help-articles', methods=['GET']) +@login_required +def get_help_articles(): + """Get all help articles""" + if not current_user.is_admin: + return jsonify({'error': 'Unauthorized'}), 403 + + articles = HelpArticle.query.order_by(HelpArticle.category.asc(), HelpArticle.order_index.asc(), HelpArticle.created_at.desc()).all() + + articles_data = [] + for article in articles: + articles_data.append({ + 'id': article.id, + 'title': article.title, + 'category': article.category, + 'body': article.body, + 'created_at': article.created_at.isoformat() if article.created_at else None, + 'updated_at': article.updated_at.isoformat() if article.updated_at else None, + 'created_by': article.created_by, + 'is_published': article.is_published, + 'order_index': article.order_index + }) + + return jsonify({'articles': articles_data}) + +@admin.route('/help-articles', methods=['POST']) +@login_required +def create_help_article(): + """Create a new help article""" + if not current_user.is_admin: + return jsonify({'error': 'Unauthorized'}), 403 + + try: + title = request.form.get('title') + category = request.form.get('category') + body = request.form.get('body') + order_index = int(request.form.get('order_index', 0)) + is_published = request.form.get('is_published') == 'true' + + if not title or not category or not body: + return jsonify({'error': 'Title, category, and body are required'}), 400 + + # Validate category + valid_categories = HelpArticle.get_categories().keys() + if category not in valid_categories: + return jsonify({'error': 'Invalid category'}), 400 + + article = HelpArticle( + title=title, + category=category, + body=body, + order_index=order_index, + is_published=is_published, + created_by=current_user.id + ) + + db.session.add(article) + db.session.commit() + + # Log the event + log_event( + event_type='help_article_create', + details={ + 'article_id': article.id, + 'title': article.title, + 'category': article.category, + 'created_by': f"{current_user.username} {current_user.last_name}" + }, + user_id=current_user.id + ) + db.session.commit() + + return jsonify({'success': True, 'message': 'Article created successfully', 'article_id': article.id}) + + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + +@admin.route('/help-articles/', methods=['GET']) +@login_required +def get_help_article(article_id): + """Get a specific help article""" + if not current_user.is_admin: + return jsonify({'error': 'Unauthorized'}), 403 + + article = HelpArticle.query.get_or_404(article_id) + + article_data = { + 'id': article.id, + 'title': article.title, + 'category': article.category, + 'body': article.body, + 'created_at': article.created_at.isoformat() if article.created_at else None, + 'updated_at': article.updated_at.isoformat() if article.updated_at else None, + 'created_by': article.created_by, + 'is_published': article.is_published, + 'order_index': article.order_index + } + + return jsonify({'article': article_data}) \ No newline at end of file diff --git a/routes/admin_api.py b/routes/admin_api.py index 6b0bdf6..aa23fc9 100644 --- a/routes/admin_api.py +++ b/routes/admin_api.py @@ -2,9 +2,11 @@ from flask import Blueprint, jsonify, request, current_app, make_response, flash from functools import wraps from models import ( KeyValueSettings, User, Room, Conversation, RoomFile, - SiteSettings, DocuPulseSettings, Event, Mail, ManagementAPIKey, PasswordSetupToken, PasswordResetToken + SiteSettings, DocuPulseSettings, Event, Mail, ManagementAPIKey, PasswordSetupToken, PasswordResetToken, + HelpArticle ) from extensions import db, csrf +from utils import log_event from datetime import datetime, timedelta import os import jwt @@ -590,4 +592,5 @@ def get_version_info(current_user): 'git_commit': 'unknown', 'git_branch': 'unknown', 'deployed_at': 'unknown' - }), 500 \ No newline at end of file + }), 500 + diff --git a/routes/public.py b/routes/public.py index a75f53d..10ee477 100644 --- a/routes/public.py +++ b/routes/public.py @@ -1,5 +1,5 @@ -from flask import Blueprint, render_template, redirect, url_for -from models import SiteSettings +from flask import Blueprint, render_template, redirect, url_for, request +from models import SiteSettings, HelpArticle import os def init_public_routes(public_bp): @@ -38,6 +38,30 @@ def init_public_routes(public_bp): """Help Center page""" return render_template('public/help.html') + @public_bp.route('/help/articles') + def help_articles(): + """Display help articles by category""" + category = request.args.get('category', '') + + # Get all published articles grouped by category + all_articles = HelpArticle.get_all_published() + categories = HelpArticle.get_categories() + + # If a specific category is requested, filter to that category + if category and category in categories: + articles = HelpArticle.get_articles_by_category(category) + category_name = categories[category] + else: + articles = [] + category_name = None + + return render_template('public/help_articles.html', + articles=articles, + all_articles=all_articles, + categories=categories, + current_category=category, + category_name=category_name) + @public_bp.route('/contact') def contact(): """Contact page""" diff --git a/templates/admin/support_articles.html b/templates/admin/support_articles.html index b7dc0b8..5f2df94 100644 --- a/templates/admin/support_articles.html +++ b/templates/admin/support_articles.html @@ -2,6 +2,37 @@ {% block title %}Support Articles - DocuPulse{% endblock %} +{% block extra_css %} + + +{% endblock %} + {% block content %}
@@ -10,18 +41,373 @@

Support Articles

+
-
-
-
- -

Support Articles

-

Content coming soon...

-
-
+ +
+
+ + + + + + + + + +{% endblock %} + +{% block extra_js %} + + {% endblock %} \ No newline at end of file diff --git a/templates/public/help.html b/templates/public/help.html index 2055e8f..c56d14d 100644 --- a/templates/public/help.html +++ b/templates/public/help.html @@ -203,7 +203,7 @@

Getting Started

Learn the basics of DocuPulse and set up your workspace

- View Articles + View Articles
@@ -213,7 +213,7 @@

User Management

Manage users, permissions, and team collaboration

- View Articles + View Articles
@@ -223,7 +223,7 @@

File Management

Upload, organize, and manage your documents

- View Articles + View Articles
@@ -233,7 +233,7 @@

Communication

Use messaging and collaboration features

- View Articles + View Articles
@@ -243,7 +243,7 @@

Security & Privacy

Learn about security features and data protection

- View Articles + View Articles
@@ -253,7 +253,7 @@

Administration

Configure settings and manage your organization

- View Articles + View Articles @@ -296,7 +296,7 @@
- DocuPulse supports a wide range of file types including documents (PDF, DOCX, DOC, TXT, RTF), spreadsheets (XLSX, XLS, ODS), presentations (PPTX, PPT), images (JPG, PNG, GIF, SVG), archives (ZIP, RAR, 7Z), code files (PY, JS, HTML, CSS), audio/video files, and CAD/design files. Individual files can be up to 10MB, and the platform includes storage limits that can be configured by administrators. + DocuPulse supports a wide range of file types including documents (PDF, DOCX, DOC, TXT, RTF), spreadsheets (XLSX, XLS, ODS), presentations (PPTX, PPT), images (JPG, PNG, GIF, SVG), archives (ZIP, RAR, 7Z), code files (PY, JS, HTML, CSS), audio/video files, and CAD/design files.
diff --git a/templates/public/help_articles.html b/templates/public/help_articles.html new file mode 100644 index 0000000..9c48dfc --- /dev/null +++ b/templates/public/help_articles.html @@ -0,0 +1,294 @@ + + + + + + {% if category_name %}{{ category_name }} - {% endif %}Help Articles - DocuPulse + + + + + + + + {% include 'components/header_nav.html' %} + + +
+ +
+ + +
+
+ +
+
+
+ Categories +
+ + All Articles + + {% for category_key, category_name in categories.items() %} + + + {{ category_name }} + {% if all_articles.get(category_key) %} + {{ all_articles[category_key]|length }} + {% endif %} + + {% endfor %} +
+
+ + +
+ {% if category_name %} +
+

+ + {{ category_name }} +

+ + Back to Help Center + +
+ {% else %} +
+

+ All Help Articles +

+ + Back to Help Center + +
+ {% endif %} + + {% if articles %} + {% for article in articles %} +
+

{{ article.title }}

+
+ {{ article.body|safe }} +
+
+
+ + + Updated: {{ article.updated_at.strftime('%B %d, %Y') if article.updated_at else article.created_at.strftime('%B %d, %Y') }} + + + {{ categories[article.category] }} + +
+
+ {% endfor %} + {% else %} +
+ +

No Articles Found

+

+ {% if current_category %} + No articles are available in the "{{ category_name }}" category yet. + {% else %} + No help articles are available yet. + {% endif %} +

+ + Back to Help Center + +
+ {% endif %} +
+
+
+ + {% include 'components/footer_nav.html' %} + + + + \ No newline at end of file