commit e157eb4e0f8f96f573c409f4bf6203f3672cd06c Author: admin Date: Thu May 1 00:24:26 2025 +0800 Initial commit diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..8b27833 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Current File", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..20f56ab --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# Use the official Python image from the Docker Hub +FROM python:3.10-slim + +# Install dependencies (Including SQLite libraries and timezone data) +RUN apt-get update && \ + apt-get install -y \ + iputils-ping \ + curl \ + apt-transport-https \ + sqlite3 \ + libsqlite3-dev \ + tzdata && \ + rm -rf /var/lib/apt/lists/* + +# Set the timezone environment variable +ENV TZ="Asia/Manila" + +# Set the working directory in the container +WORKDIR /app + +# Copy the requirements.txt file into the container +COPY requirements.txt . + +# Install the dependencies +RUN pip install --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt + +# Copy the rest of your application code into the container +COPY . . + +# Expose port 1000 to the outside world +EXPOSE 1000 + +# Define the command to run your application (app.py as primary file) +CMD ["python", "app.py"] diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..ca6e941 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: gunicorn app:app diff --git a/app.py b/app.py new file mode 100644 index 0000000..967e865 --- /dev/null +++ b/app.py @@ -0,0 +1,816 @@ +import os +import sqlite3 +import uuid +import hashlib +import datetime +from flask import Flask, render_template, request, redirect, url_for, flash, session, send_from_directory +from werkzeug.utils import secure_filename +from functools import wraps + +app = Flask(__name__) +app.secret_key = os.environ.get('SECRET_KEY', os.urandom(24)) + +# Configuration +UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads') +ALLOWED_EXTENSIONS = {'pdf', 'docx', 'xlsx', 'jpg', 'png', 'mp4', 'txt'} +DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'dms.db') + +# Create upload folder if it doesn't exist +if not os.path.exists(UPLOAD_FOLDER): + os.makedirs(UPLOAD_FOLDER) + +app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER +app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max upload size + +# Production configuration +app.config['PREFERRED_URL_SCHEME'] = 'https' +app.config['SESSION_COOKIE_SECURE'] = False +app.config['SESSION_COOKIE_HTTPONLY'] = True +app.config['PERMANENT_SESSION_LIFETIME'] = datetime.timedelta(days=1) + +# Database initialization +def init_db(): + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # Create users table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + email TEXT UNIQUE NOT NULL, + role TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Create documents table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS documents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + filename TEXT NOT NULL, + original_filename TEXT NOT NULL, + custom_filename TEXT NOT NULL, + file_type TEXT NOT NULL, + file_size INTEGER NOT NULL, + category TEXT NOT NULL, + user_id INTEGER NOT NULL, + visibility TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) + ) + ''') + + # Create document_shares table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS document_shares ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + document_id INTEGER NOT NULL, + shared_by INTEGER NOT NULL, + shared_with INTEGER, + share_link TEXT, + qr_code TEXT, + expires_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (document_id) REFERENCES documents (id), + FOREIGN KEY (shared_by) REFERENCES users (id), + FOREIGN KEY (shared_with) REFERENCES users (id) + ) + ''') + + # Create document_versions table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS document_versions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + document_id INTEGER NOT NULL, + filename TEXT NOT NULL, + version_number INTEGER NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (document_id) REFERENCES documents (id) + ) + ''') + + # Create analytics table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS analytics ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + document_id INTEGER NOT NULL, + action TEXT NOT NULL, + user_id INTEGER, + ip_address TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (document_id) REFERENCES documents (id), + FOREIGN KEY (user_id) REFERENCES users (id) + ) + ''') + + # Insert default admin user if not exists + cursor.execute("SELECT * FROM users WHERE username = 'admin'") + if not cursor.fetchone(): + # Password: admin123 + hashed_password = hashlib.sha256("admin123".encode()).hexdigest() + cursor.execute("INSERT INTO users (username, password, email, role) VALUES (?, ?, ?, ?)", + ("admin", hashed_password, "admin@example.com", "admin")) + + # Insert default regular user if not exists + cursor.execute("SELECT * FROM users WHERE username = 'user'") + if not cursor.fetchone(): + # Password: user123 + hashed_password = hashlib.sha256("user123".encode()).hexdigest() + cursor.execute("INSERT INTO users (username, password, email, role) VALUES (?, ?, ?, ?)", + ("user", hashed_password, "user@example.com", "user")) + + conn.commit() + conn.close() + +# Helper functions +def allowed_file(filename): + return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + +def get_file_type(filename): + ext = filename.rsplit('.', 1)[1].lower() + if ext in ['pdf']: + return 'pdf' + elif ext in ['docx', 'doc']: + return 'document' + elif ext in ['xlsx', 'xls']: + return 'spreadsheet' + elif ext in ['jpg', 'jpeg', 'png', 'gif']: + return 'image' + elif ext in ['mp4', 'avi', 'mov']: + return 'video' + else: + return 'other' + +def get_db_connection(): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + return conn + +# Authentication decorator +def login_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if 'user_id' not in session: + flash('Please log in to access this page', 'error') + return redirect(url_for('login')) + return f(*args, **kwargs) + return decorated_function + +def admin_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if 'user_id' not in session: + flash('Please log in to access this page', 'error') + return redirect(url_for('login')) + + conn = get_db_connection() + user = conn.execute('SELECT * FROM users WHERE id = ?', (session['user_id'],)).fetchone() + conn.close() + + if user['role'] != 'admin': + flash('Admin access required', 'error') + return redirect(url_for('dashboard')) + + return f(*args, **kwargs) + return decorated_function + +# Routes +@app.route('/') +def index(): + if 'user_id' in session: + return redirect(url_for('dashboard')) + return render_template('index.html') + +@app.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + hashed_password = hashlib.sha256(password.encode()).hexdigest() + + conn = get_db_connection() + user = conn.execute('SELECT * FROM users WHERE username = ? AND password = ?', + (username, hashed_password)).fetchone() + conn.close() + + if user: + session['user_id'] = user['id'] + session['username'] = user['username'] + session['role'] = user['role'] + flash(f'Welcome back, {username}!', 'success') + return redirect(url_for('dashboard')) + else: + flash('Invalid username or password', 'error') + return render_template('login.html') + else: + return render_template('login.html') + +@app.route('/logout') +def logout(): + session.clear() + flash('You have been logged out', 'info') + return redirect(url_for('index')) + +@app.route('/dashboard') +@login_required +def dashboard(): + conn = get_db_connection() + + # Get document counts by category + category_counts = conn.execute(''' + SELECT category, COUNT(*) as count + FROM documents + WHERE user_id = ? OR visibility = "public" + OR id IN (SELECT document_id FROM document_shares WHERE shared_with = ?) + GROUP BY category + ''', (session['user_id'], session['user_id'])).fetchall() + + # Get recent documents (limited to 5) + recent_documents = conn.execute(''' + SELECT * FROM documents + WHERE user_id = ? OR visibility = "public" + OR id IN (SELECT document_id FROM document_shares WHERE shared_with = ?) + ORDER BY created_at DESC + LIMIT 5 + ''', (session['user_id'], session['user_id'])).fetchall() + + # Get analytics data for admin + if session['role'] == 'admin': + analytics = conn.execute(''' + SELECT documents.custom_filename, COUNT(analytics.id) as view_count + FROM documents + LEFT JOIN analytics ON documents.id = analytics.document_id + WHERE analytics.action = 'view' + GROUP BY documents.id + ORDER BY view_count DESC + LIMIT 5 + ''').fetchall() + + # Get user activity + user_activity = conn.execute(''' + SELECT users.username, COUNT(analytics.id) as action_count + FROM analytics + JOIN users ON analytics.user_id = users.id + GROUP BY analytics.user_id + ORDER BY action_count DESC + LIMIT 5 + ''').fetchall() + else: + analytics = None + user_activity = None + + conn.close() + + return render_template('dashboard.html', + recent_documents=recent_documents, + category_counts=category_counts, + analytics=analytics, + user_activity=user_activity) + +@app.route('/upload', methods=['GET', 'POST']) +@login_required +def upload(): + if request.method == 'POST': + # Check if the post request has the file part + if 'file' not in request.files: + flash('No file part', 'error') + return redirect(request.url) + + file = request.files['file'] + visibility = request.form.get('visibility', 'private') + custom_filename = request.form.get('custom_filename', '').strip() + category = request.form.get('category', 'general') + + # If user does not select file, browser also + # submit an empty part without filename + if file.filename == '': + flash('No selected file', 'error') + return redirect(request.url) + + if file and allowed_file(file.filename): + # Generate unique filename + original_filename = secure_filename(file.filename) + file_extension = original_filename.rsplit('.', 1)[1].lower() + unique_filename = f"{uuid.uuid4().hex}.{file_extension}" + + # Use custom filename if provided, otherwise use original + if not custom_filename: + custom_filename = os.path.splitext(original_filename)[0] + + # Save the file + file_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename) + file.save(file_path) + + # Get file size + file_size = os.path.getsize(file_path) + + # Save to database + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO documents (filename, original_filename, custom_filename, file_type, file_size, category, user_id, visibility) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ''', (unique_filename, original_filename, custom_filename, get_file_type(original_filename), file_size, category, session['user_id'], visibility)) + + document_id = cursor.lastrowid + + # Add initial version + cursor.execute(''' + INSERT INTO document_versions (document_id, filename, version_number) + VALUES (?, ?, ?) + ''', (document_id, unique_filename, 1)) + + conn.commit() + conn.close() + + flash('File successfully uploaded', 'success') + return redirect(url_for('files')) + else: + flash(f'Allowed file types are: {", ".join(ALLOWED_EXTENSIONS)}', 'error') + + # Get available categories + categories = ['admin', 'accounting', 'hr', 'marketing', 'legal', 'general', 'other'] + + return render_template('upload.html', categories=categories) + +@app.route('/files') +@login_required +def files(): + # Get query parameters + search_query = request.args.get('search', '') + selected_category = request.args.get('category', 'all') + page = request.args.get('page', 1, type=int) + per_page = 10 # Number of documents per page + + conn = get_db_connection() + + # Base query + query = ''' + SELECT * FROM documents + WHERE (user_id = ? OR visibility = "public" + OR id IN (SELECT document_id FROM document_shares WHERE shared_with = ?)) + ''' + params = [session['user_id'], session['user_id']] + + # Add search condition if search query provided + if search_query: + query += ' AND (custom_filename LIKE ? OR original_filename LIKE ?)' + params.extend(['%' + search_query + '%', '%' + search_query + '%']) + + # Add category filter if specific category selected + if selected_category != 'all': + query += ' AND category = ?' + params.append(selected_category) + + # Count total matching documents + count_query = query.replace('SELECT *', 'SELECT COUNT(*)') + total_docs = conn.execute(count_query, params).fetchone()[0] + + # Add pagination + query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?' + offset = (page - 1) * per_page + params.extend([per_page, offset]) + + # Execute final query + documents = conn.execute(query, params).fetchall() + + # Get available categories for filter dropdown + categories = ['admin', 'accounting', 'hr', 'marketing', 'legal', 'general', 'other'] + + # Calculate pagination info + total_pages = (total_docs + per_page - 1) // per_page # Ceiling division + pagination = { + 'page': page, + 'per_page': per_page, + 'total': total_docs, + 'pages': total_pages + } + + conn.close() + + return render_template('files.html', + documents=documents, + categories=categories, + selected_category=selected_category, + search_query=search_query, + pagination=pagination) + +@app.route('/document//edit', methods=['GET', 'POST']) +@login_required +def edit_document(document_id): + conn = get_db_connection() + + # Get document + document = conn.execute('SELECT * FROM documents WHERE id = ?', (document_id,)).fetchone() + + if not document: + conn.close() + flash('Document not found', 'error') + return redirect(url_for('files')) + + # Check ownership + if document['user_id'] != session['user_id'] and session['role'] != 'admin': + conn.close() + flash('You do not have permission to edit this document', 'error') + return redirect(url_for('files')) + + # Get available categories + categories = ['admin', 'accounting', 'hr', 'marketing', 'legal', 'general', 'other'] + + if request.method == 'POST': + custom_filename = request.form.get('custom_filename', '').strip() + category = request.form.get('category', 'general') + visibility = request.form.get('visibility', 'private') + + # Use custom filename if provided, otherwise keep existing + if not custom_filename: + custom_filename = document['custom_filename'] + + # Update document + conn.execute(''' + UPDATE documents + SET custom_filename = ?, category = ?, visibility = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + ''', (custom_filename, category, visibility, document_id)) + + conn.commit() + conn.close() + + flash('Document updated successfully', 'success') + return redirect(url_for('view_document', document_id=document_id)) + + conn.close() + + return render_template('edit_document.html', document=document, categories=categories) + +@app.route('/document/') +@login_required +def view_document(document_id): + conn = get_db_connection() + + # Get document + document = conn.execute('SELECT * FROM documents WHERE id = ?', (document_id,)).fetchone() + + if not document: + conn.close() + flash('Document not found', 'error') + return redirect(url_for('dashboard')) + + # Check permissions + if document['visibility'] != 'public' and document['user_id'] != session['user_id']: + # Check if shared with user + shared = conn.execute(''' + SELECT * FROM document_shares + WHERE document_id = ? AND shared_with = ? + ''', (document_id, session['user_id'])).fetchone() + + if not shared and session['role'] != 'admin': + conn.close() + flash('You do not have permission to view this document', 'error') + return redirect(url_for('dashboard')) + + # Record view in analytics + conn.execute(''' + INSERT INTO analytics (document_id, action, user_id, ip_address) + VALUES (?, ?, ?, ?) + ''', (document_id, 'view', session['user_id'], request.remote_addr)) + + # Get document versions + versions = conn.execute(''' + SELECT * FROM document_versions + WHERE document_id = ? + ORDER BY version_number DESC + ''', (document_id,)).fetchall() + + # Get share information + shares = conn.execute(''' + SELECT document_shares.*, users.username + FROM document_shares + LEFT JOIN users ON document_shares.shared_with = users.id + WHERE document_id = ? + ''', (document_id,)).fetchall() + + conn.commit() + conn.close() + + return render_template('view_document.html', document=document, versions=versions, shares=shares) + + +@app.route('/document//file') +@login_required +def serve_document_file(document_id): + conn = get_db_connection() + + # Get document + document = conn.execute('SELECT * FROM documents WHERE id = ?', (document_id,)).fetchone() + + if not document: + conn.close() + flash('Document not found', 'error') + return redirect(url_for('dashboard')) + + # Check permissions + if document['visibility'] != 'public' and document['user_id'] != session['user_id']: + shared = conn.execute(''' + SELECT * FROM document_shares + WHERE document_id = ? AND shared_with = ? + ''', (document_id, session['user_id'])).fetchone() + + if not shared and session['role'] != 'admin': + conn.close() + flash('You do not have permission to view this document', 'error') + return redirect(url_for('dashboard')) + + try: + # Serve the document without download (viewable in browser) + return send_from_directory(app.config['UPLOAD_FOLDER'], document['filename'], + as_attachment=False, download_name=document['original_filename']) + except FileNotFoundError: + flash('Document file not found', 'error') + conn.close() + return redirect(url_for('dashboard')) + finally: + conn.close() + + +@app.route('/document//download') +@login_required +def download_document(document_id): + conn = get_db_connection() + + # Get document + document = conn.execute('SELECT * FROM documents WHERE id = ?', (document_id,)).fetchone() + + if not document: + conn.close() + flash('Document not found', 'error') + return redirect(url_for('dashboard')) + + # Check permissions + if document['visibility'] != 'public' and document['user_id'] != session['user_id']: + # Check if shared with user + shared = conn.execute(''' + SELECT * FROM document_shares + WHERE document_id = ? AND shared_with = ? + ''', (document_id, session['user_id'])).fetchone() + + if not shared and session['role'] != 'admin': + conn.close() + flash('You do not have permission to download this document', 'error') + return redirect(url_for('dashboard')) + + # Record download in analytics + conn.execute(''' + INSERT INTO analytics (document_id, action, user_id, ip_address) + VALUES (?, ?, ?, ?) + ''', (document_id, 'download', session['user_id'], request.remote_addr)) + + conn.commit() + conn.close() + + return send_from_directory(app.config['UPLOAD_FOLDER'], document['filename'], + as_attachment=True, download_name=document['original_filename']) + +@app.route('/document//share', methods=['GET', 'POST']) +@login_required +def share_document(document_id): + conn = get_db_connection() + + # Get document + document = conn.execute('SELECT * FROM documents WHERE id = ?', (document_id,)).fetchone() + + if not document: + conn.close() + flash('Document not found', 'error') + return redirect(url_for('dashboard')) + + # Check ownership + if document['user_id'] != session['user_id'] and session['role'] != 'admin': + conn.close() + flash('You do not have permission to share this document', 'error') + return redirect(url_for('dashboard')) + + if request.method == 'POST': + share_type = request.form.get('share_type') + + if share_type == 'user': + # Share with specific user + username = request.form.get('username') + user = conn.execute('SELECT * FROM users WHERE username = ?', (username,)).fetchone() + + if not user: + flash(f'User {username} not found', 'error') + else: + # Check if already shared + existing = conn.execute(''' + SELECT * FROM document_shares + WHERE document_id = ? AND shared_with = ? + ''', (document_id, user['id'])).fetchone() + + if existing: + flash(f'Document already shared with {username}', 'info') + else: + conn.execute(''' + INSERT INTO document_shares (document_id, shared_by, shared_with) + VALUES (?, ?, ?) + ''', (document_id, session['user_id'], user['id'])) + flash(f'Document shared with {username}', 'success') + + elif share_type == 'link': + # Generate shareable link + expiry_days = int(request.form.get('expiry', 7)) + expires_at = datetime.datetime.now() + datetime.timedelta(days=expiry_days) + share_token = uuid.uuid4().hex + + conn.execute(''' + INSERT INTO document_shares (document_id, shared_by, share_link, expires_at) + VALUES (?, ?, ?, ?) + ''', (document_id, session['user_id'], share_token, expires_at)) + + flash(f'Shareable link created (expires in {expiry_days} days)', 'success') + + conn.commit() + + # Get users for sharing + users = conn.execute('SELECT * FROM users WHERE id != ?', (session['user_id'],)).fetchall() + + # Get existing shares + shares = conn.execute(''' + SELECT document_shares.*, users.username + FROM document_shares + LEFT JOIN users ON document_shares.shared_with = users.id + WHERE document_id = ? + ''', (document_id,)).fetchall() + + conn.close() + + return render_template('share_document.html', document=document, users=users, shares=shares) + +@app.route('/document//delete') +@login_required +def delete_document(document_id): + conn = get_db_connection() + + # Get document + document = conn.execute('SELECT * FROM documents WHERE id = ?', (document_id,)).fetchone() + + if not document: + conn.close() + flash('Document not found', 'error') + return redirect(url_for('dashboard')) + + # Check ownership + if document['user_id'] != session['user_id'] and session['role'] != 'admin': + conn.close() + flash('You do not have permission to delete this document', 'error') + return redirect(url_for('dashboard')) + + # Get all versions + versions = conn.execute('SELECT * FROM document_versions WHERE document_id = ?', (document_id,)).fetchall() + + # Delete files + for version in versions: + file_path = os.path.join(app.config['UPLOAD_FOLDER'], version['filename']) + if os.path.exists(file_path): + os.remove(file_path) + + # Delete from database (cascade will handle related records) + conn.execute('DELETE FROM analytics WHERE document_id = ?', (document_id,)) + conn.execute('DELETE FROM document_shares WHERE document_id = ?', (document_id,)) + conn.execute('DELETE FROM document_versions WHERE document_id = ?', (document_id,)) + conn.execute('DELETE FROM documents WHERE id = ?', (document_id,)) + + conn.commit() + conn.close() + + flash('Document deleted successfully', 'success') + return redirect(url_for('dashboard')) + +@app.route('/admin') +@admin_required +def admin_panel(): + conn = get_db_connection() + + # Get all users + users = conn.execute('SELECT * FROM users').fetchall() + + # Get document statistics + doc_stats = conn.execute(''' + SELECT + COUNT(*) as total_documents, + SUM(file_size) as total_size, + COUNT(DISTINCT user_id) as total_contributors + FROM documents + ''').fetchone() + + # Get activity statistics + activity_stats = conn.execute(''' + SELECT + COUNT(*) as total_actions, + COUNT(DISTINCT user_id) as active_users, + COUNT(DISTINCT document_id) as accessed_documents + FROM analytics + ''').fetchone() + + conn.close() + + return render_template('admin.html', users=users, doc_stats=doc_stats, activity_stats=activity_stats) + +@app.route('/shared/') +def access_shared(share_token): + conn = get_db_connection() + + # Get share information + share = conn.execute(''' + SELECT document_shares.*, documents.* + FROM document_shares + JOIN documents ON document_shares.document_id = documents.id + WHERE document_shares.share_link = ? + ''', (share_token,)).fetchone() + + if not share: + conn.close() + flash('Invalid or expired share link', 'error') + return redirect(url_for('index')) + + # Check if expired + if share['expires_at'] and datetime.datetime.strptime(share['expires_at'], '%Y-%m-%d %H:%M:%S.%f') < datetime.datetime.now(): + conn.close() + flash('This share link has expired', 'error') + return redirect(url_for('index')) + + # Record view in analytics + user_id = session.get('user_id') + conn.execute(''' + INSERT INTO analytics (document_id, action, user_id, ip_address) + VALUES (?, ?, ?, ?) + ''', (share['document_id'], 'shared_view', user_id, request.remote_addr)) + + conn.commit() + + # Get document versions + versions = conn.execute(''' + SELECT * FROM document_versions + WHERE document_id = ? + ORDER BY version_number DESC + ''', (share['document_id'],)).fetchall() + + conn.close() + + return render_template('shared_view.html', document=share, versions=versions, share_token=share_token) + +@app.route('/shared//download') +def download_shared(share_token): + conn = get_db_connection() + + # Get share information + share = conn.execute(''' + SELECT document_shares.*, documents.* + FROM document_shares + JOIN documents ON document_shares.document_id = documents.id + WHERE document_shares.share_link = ? + ''', (share_token,)).fetchone() + + if not share: + conn.close() + flash('Invalid or expired share link', 'error') + return redirect(url_for('index')) + + # Check if expired + if share['expires_at'] and datetime.datetime.strptime(share['expires_at'], '%Y-%m-%d %H:%M:%S.%f') < datetime.datetime.now(): + conn.close() + flash('This share link has expired', 'error') + return redirect(url_for('index')) + + # Record download in analytics + user_id = session.get('user_id') + conn.execute(''' + INSERT INTO analytics (document_id, action, user_id, ip_address) + VALUES (?, ?, ?, ?) + ''', (share['document_id'], 'shared_download', user_id, request.remote_addr)) + + conn.commit() + conn.close() + + return send_from_directory(app.config['UPLOAD_FOLDER'], share['filename'], + as_attachment=True, download_name=share['original_filename']) + +# Error handlers +@app.errorhandler(404) +def page_not_found(e): + return render_template('error.html', error_code=404, error_message="Page not found"), 404 + +@app.errorhandler(500) +def internal_server_error(e): + return render_template('error.html', error_code=500, error_message="Internal server error"), 500 + +# Initialize database and start app +if __name__ == '__main__': + init_db() + app.run(host='0.0.0.0', port=1000, debug=False) +else: + # For production WSGI servers + init_db() diff --git a/dms.db b/dms.db new file mode 100644 index 0000000..7a1d629 Binary files /dev/null and b/dms.db differ diff --git a/edit_document_route.py b/edit_document_route.py new file mode 100644 index 0000000..4d9ffe5 --- /dev/null +++ b/edit_document_route.py @@ -0,0 +1,47 @@ +@app.route('/document//edit', methods=['GET', 'POST']) +@login_required +def edit_document(document_id): + conn = get_db_connection() + + # Get document + document = conn.execute('SELECT * FROM documents WHERE id = ?', (document_id,)).fetchone() + + if not document: + conn.close() + flash('Document not found', 'error') + return redirect(url_for('files')) + + # Check ownership + if document['user_id'] != session['user_id'] and session['role'] != 'admin': + conn.close() + flash('You do not have permission to edit this document', 'error') + return redirect(url_for('files')) + + # Get available categories + categories = ['admin', 'accounting', 'hr', 'marketing', 'legal', 'general', 'other'] + + if request.method == 'POST': + custom_filename = request.form.get('custom_filename', '').strip() + category = request.form.get('category', 'general') + visibility = request.form.get('visibility', 'private') + + # Use custom filename if provided, otherwise keep existing + if not custom_filename: + custom_filename = document['custom_filename'] + + # Update document + conn.execute(''' + UPDATE documents + SET custom_filename = ?, category = ?, visibility = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + ''', (custom_filename, category, visibility, document_id)) + + conn.commit() + conn.close() + + flash('Document updated successfully', 'success') + return redirect(url_for('view_document', document_id=document_id)) + + conn.close() + + return render_template('edit_document.html', document=document, categories=categories) diff --git a/files_route.py b/files_route.py new file mode 100644 index 0000000..f5d243f --- /dev/null +++ b/files_route.py @@ -0,0 +1,61 @@ +@app.route('/files') +@login_required +def files(): + # Get query parameters + search_query = request.args.get('search', '') + selected_category = request.args.get('category', 'all') + page = request.args.get('page', 1, type=int) + per_page = 10 # Number of documents per page + + conn = get_db_connection() + + # Base query + query = ''' + SELECT * FROM documents + WHERE (user_id = ? OR visibility = "public" + OR id IN (SELECT document_id FROM document_shares WHERE shared_with = ?)) + ''' + params = [session['user_id'], session['user_id']] + + # Add search condition if search query provided + if search_query: + query += ' AND (custom_filename LIKE ? OR original_filename LIKE ?)' + params.extend(['%' + search_query + '%', '%' + search_query + '%']) + + # Add category filter if specific category selected + if selected_category != 'all': + query += ' AND category = ?' + params.append(selected_category) + + # Count total matching documents + count_query = query.replace('SELECT *', 'SELECT COUNT(*)') + total_docs = conn.execute(count_query, params).fetchone()[0] + + # Add pagination + query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?' + offset = (page - 1) * per_page + params.extend([per_page, offset]) + + # Execute final query + documents = conn.execute(query, params).fetchall() + + # Get available categories for filter dropdown + categories = ['admin', 'accounting', 'hr', 'marketing', 'legal', 'general', 'other'] + + # Calculate pagination info + total_pages = (total_docs + per_page - 1) // per_page # Ceiling division + pagination = { + 'page': page, + 'per_page': per_page, + 'total': total_docs, + 'pages': total_pages + } + + conn.close() + + return render_template('files.html', + documents=documents, + categories=categories, + selected_category=selected_category, + search_query=search_query, + pagination=pagination) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c6410d7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +Flask==2.3.3 +Werkzeug==2.3.7 +Jinja2==3.1.2 +MarkupSafe==2.1.3 +itsdangerous==2.1.2 +click==8.1.7 +gunicorn==21.2.0 diff --git a/static/scripts.js b/static/scripts.js new file mode 100644 index 0000000..69c5fa4 --- /dev/null +++ b/static/scripts.js @@ -0,0 +1,64 @@ +// Document Management System JavaScript +document.addEventListener('DOMContentLoaded', function() { + // Auto-dismiss flash messages after 5 seconds + window.setTimeout(function() { + document.querySelectorAll('.alert').forEach(function(alert) { + if (bootstrap && bootstrap.Alert) { + var bsAlert = new bootstrap.Alert(alert); + bsAlert.close(); + } else { + alert.style.display = 'none'; + } + }); + }, 5000); + + // Copy link functionality + window.copyLink = function(elementId) { + var copyText = document.getElementById(elementId); + if (copyText) { + copyText.select(); + copyText.setSelectionRange(0, 99999); + navigator.clipboard.writeText(copyText.value); + + // Show a temporary tooltip + var tooltip = document.createElement("div"); + tooltip.innerHTML = "Copied!"; + tooltip.style.position = "fixed"; + tooltip.style.backgroundColor = "#4CAF50"; + tooltip.style.color = "white"; + tooltip.style.padding = "5px 10px"; + tooltip.style.borderRadius = "5px"; + tooltip.style.zIndex = "1000"; + tooltip.style.top = (event.clientY - 30) + "px"; + tooltip.style.left = event.clientX + "px"; + document.body.appendChild(tooltip); + + setTimeout(function() { + document.body.removeChild(tooltip); + }, 2000); + } + }; + + // Email sharing functionality + window.shareViaEmail = function(filename, url) { + var subject = "Shared Document: " + filename; + var body = "I'm sharing a document with you: " + filename + "\n\nYou can access it here: " + url; + window.location.href = "mailto:?subject=" + encodeURIComponent(subject) + "&body=" + encodeURIComponent(body); + }; + + // WhatsApp sharing functionality + window.shareViaWhatsApp = function(filename, url) { + var text = "I'm sharing a document with you: " + filename + "\n\nYou can access it here: " + url; + window.open("https://wa.me/?text=" + encodeURIComponent(text), "_blank"); + }; + + // Confirm delete + var deleteButtons = document.querySelectorAll('[data-confirm]'); + deleteButtons.forEach(function(button) { + button.addEventListener('click', function(e) { + if (!confirm(this.getAttribute('data-confirm'))) { + e.preventDefault(); + } + }); + }); +}); diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 0000000..bd67a1c --- /dev/null +++ b/static/styles.css @@ -0,0 +1,70 @@ +/* Main Styles */ +body { + padding-top: 60px; + background-color: #f8f9fa; +} + +.card { + margin-bottom: 20px; + box-shadow: 0 4px 6px rgba(0,0,0,.1); +} + +.document-card { + transition: transform 0.2s; +} + +.document-card:hover { + transform: translateY(-5px); +} + +.file-icon { + font-size: 2.5rem; + margin-bottom: 10px; +} + +.pdf-icon { color: #e74c3c; } +.document-icon { color: #3498db; } +.spreadsheet-icon { color: #2ecc71; } +.image-icon { color: #9b59b6; } +.video-icon { color: #e67e22; } +.other-icon { color: #95a5a6; } + +.flash-messages { + position: fixed; + top: 70px; + right: 20px; + z-index: 1000; + max-width: 300px; +} + +/* Dashboard Styles */ +.analytics-card { + background-color: #f8f9fa; + border-left: 4px solid #007bff; +} + +/* Document Viewer Styles */ +.document-viewer { + min-height: 400px; + background-color: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 0.25rem; + padding: 1rem; +} + +/* Responsive Adjustments */ +@media (max-width: 768px) { + .card-body { + padding: 1rem; + } + + .btn-group { + display: flex; + flex-direction: column; + } + + .btn-group .btn { + margin-bottom: 0.25rem; + border-radius: 0.25rem !important; + } +} diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..ec093d3 --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,123 @@ +{% extends "layout.html" %} + +{% block content %} +

Admin Panel

+ +
+
+
+
+
Document Statistics
+
+
+
    +
  • + Total Documents + {{ doc_stats.total_documents }} +
  • +
  • + Total Storage Used + {{ (doc_stats.total_size / (1024*1024))|round(2) }} MB +
  • +
  • + Active Contributors + {{ doc_stats.total_contributors }} +
  • +
+
+
+
+ +
+
+
+
Activity Statistics
+
+
+
    +
  • + Total Actions + {{ activity_stats.total_actions }} +
  • +
  • + Active Users + {{ activity_stats.active_users }} +
  • +
  • + Accessed Documents + {{ activity_stats.accessed_documents }} +
  • +
+
+
+
+ +
+
+
+
System Actions
+
+
+
+ + + +
+
+
+
+
+ +
+
+
User Management
+
+
+
+ + + + + + + + + + + + + {% for user in users %} + + + + + + + + + {% endfor %} + +
IDUsernameEmailRoleCreatedActions
{{ user.id }}{{ user.username }}{{ user.email }} + + {{ user.role }} + + {{ user.created_at.split(' ')[0] }} +
+ + +
+
+
+
+
+{% endblock %} diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..cd07db6 --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,194 @@ +{% extends "layout.html" %} + +{% block content %} +

Dashboard

+ +
+ +
+
+
+
Document Categories
+
+
+ {% if category_counts %} +
+ {% for category in category_counts %} +
+
+
+
+
+
{{ category.category|capitalize }}
+ {{ category.count }} document{% if category.count != 1 %}s{% endif %} +
+ + + +
+
+
+
+ {% endfor %} +
+ {% else %} +
+ No documents found. Upload your first document to get started! +
+ {% endif %} + +
+
+
+ + + +
+ +
+ + + + + {% if session.role == 'admin' and analytics %} +
+
+
+
Analytics Overview
+
+
+
Most Viewed Documents
+
+ + + + + + + + + {% for doc in analytics %} + + + + + {% endfor %} + +
DocumentViews
{{ doc.custom_filename }}{{ doc.view_count }}
+
+ + {% if user_activity %} +
Most Active Users
+
+ + + + + + + + + {% for user in user_activity %} + + + + + {% endfor %} + +
UserActions
{{ user.username }}{{ user.action_count }}
+
+ {% endif %} +
+
+
+ {% endif %} +
+{% endblock %} diff --git a/templates/edit_document.html b/templates/edit_document.html new file mode 100644 index 0000000..5c6d0a0 --- /dev/null +++ b/templates/edit_document.html @@ -0,0 +1,77 @@ +{% extends "layout.html" %} + +{% block content %} +

Edit Document

+ +
+
+
+
+
Edit Document Details
+
+
+
+
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+ + +
+
+ +
+ + Cancel + + +
+
+
+
+
+ +
+
+
+
Document Information
+
+
+

Original Filename: {{ document.original_filename }}

+

File Type: {{ document.file_type|capitalize }}

+

File Size: {{ (document.file_size / 1024)|round(1) }} KB

+

Uploaded: {{ document.created_at.split(' ')[0] }}

+

Last Modified: {{ document.updated_at.split(' ')[0] }}

+
+
+
+
+{% endblock %} diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 0000000..25e3660 --- /dev/null +++ b/templates/error.html @@ -0,0 +1,49 @@ + + + + + + Document Management System - Error + + + + + + + + + +
+
+
+
+
+

{{ error_code }}

+

{{ error_message }}

+

Sorry, something went wrong.

+ + Return to Home + +
+
+
+
+
+ + +
+
+

Document Management System

+
+
+ + + + + diff --git a/templates/files.html b/templates/files.html new file mode 100644 index 0000000..7cc28c5 --- /dev/null +++ b/templates/files.html @@ -0,0 +1,145 @@ +{% extends "layout.html" %} + +{% block content %} +
+
+

Files

+
+ +
+ +
+
+
Document Library
+
+ +
+ + +
+
+
+
+
+ + + + + + + + + + + + + + {% if documents %} + {% for doc in documents %} + + + + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
NameCategoryTypeSizeUploadedVisibilityActions
+
+ {% if doc.file_type == 'pdf' %} + + {% elif doc.file_type == 'document' %} + + {% elif doc.file_type == 'spreadsheet' %} + + {% elif doc.file_type == 'image' %} + + {% elif doc.file_type == 'video' %} + + {% else %} + + {% endif %} + {{ doc.custom_filename }} +
+
{{ doc.category|capitalize }}{{ doc.file_type|capitalize }}{{ (doc.file_size / 1024)|round(1) }} KB{{ doc.created_at.split(' ')[0] }} + {% if doc.visibility == 'public' %} + Public + {% else %} + Private + {% endif %} + + +
+
+ + {% if search_query %} + No documents found matching your search criteria. + {% elif selected_category != 'all' %} + No documents found in the selected category. + {% else %} + No documents found. Upload your first document to get started! + {% endif %} +
+
+
+
+
+ +{% if pagination and pagination.pages > 1 %} + +{% endif %} +{% endblock %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..fc52ca8 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,17 @@ +{% extends "layout.html" %} + +{% block content %} +
+
+
+
+

Document Management System

+

A secure platform for managing, sharing, and collaborating on documents.

+
+

Please login to access your documents and start managing your files.

+ Login +
+
+
+
+{% endblock %} diff --git a/templates/layout.html b/templates/layout.html new file mode 100644 index 0000000..d105f97 --- /dev/null +++ b/templates/layout.html @@ -0,0 +1,92 @@ + + + + + + Document Management System + + + + + + + + + +
+ {% for category, message in get_flashed_messages(with_categories=true) %} + {% set alert_class = 'alert-info' %} + {% if category == 'success' %} + {% set alert_class = 'alert-success' %} + {% elif category == 'error' %} + {% set alert_class = 'alert-danger' %} + {% endif %} +
+ {{ message }} + +
+ {% endfor %} +
+ + +
+ {% block content %}{% endblock %} +
+ + +
+
+

Document Management System

+
+
+ + + + {% block scripts %}{% endblock %} + + diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..c9a1d8c --- /dev/null +++ b/templates/login.html @@ -0,0 +1,33 @@ +{% extends "layout.html" %} + +{% block content %} +
+
+
+
+

Login

+
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+
+
+
+
+{% endblock %} diff --git a/templates/share_document.html b/templates/share_document.html new file mode 100644 index 0000000..c2e3c30 --- /dev/null +++ b/templates/share_document.html @@ -0,0 +1,163 @@ +{% extends "layout.html" %} + +{% block content %} +
+
+

Share Document: {{ document.original_filename }}

+
+ +
+ +
+
+
+
+
Share with User
+
+
+
+ +
+ + +
+
+ +
+
+
+
+
+ +
+
+
+
Create Shareable Link
+
+
+
+ +
+ + +
+
+ +
+
+
+
+
+
+ +
+
+
Current Shares
+
+
+ {% if shares %} +
+ + + + + + + + + + + + {% for share in shares %} + + + + + + + + {% endfor %} + +
TypeShared WithDate SharedExpiryActions
+ {% if share.shared_with %} + User + {% elif share.share_link %} + Link + {% endif %} + + {% if share.shared_with %} + {{ share.username }} + {% elif share.share_link %} +
+ + +
+ {% endif %} +
{{ share.created_at.split(' ')[0] }} + {% if share.expires_at %} + {{ share.expires_at.split(' ')[0] }} + {% else %} + Never + {% endif %} + + + Revoke + +
+
+ {% else %} +
+ This document hasn't been shared yet. +
+ {% endif %} +
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/shared_view.html b/templates/shared_view.html new file mode 100644 index 0000000..12e3455 --- /dev/null +++ b/templates/shared_view.html @@ -0,0 +1,140 @@ +{% extends "layout.html" %} + +{% block content %} +
+
+
+ You are viewing a shared document. + {% if not session.user_id %} + Login to access more features. + {% endif %} +
+
+
+ +
+
+
+
+
{{ document.original_filename }}
+
+
+
+ {% if document.file_type == 'pdf' %} +
+ +
+ {% elif document.file_type == 'image' %} + {{ document.original_filename }} + {% elif document.file_type == 'video' %} +
+ +
+ {% else %} +
+ Preview not available for this file type. Please download the file to view its contents. +
+ {% endif %} +
+ + +
+
+
+ +
+
+
+
Document Information
+
+
+
    +
  • + File Type + {{ document.file_type }} +
  • +
  • + Size + {{ (document.file_size / 1024)|round(1) }} KB +
  • +
  • + Uploaded + {{ document.created_at.split(' ')[0] }} +
  • +
+
+
+ +
+
+
Sharing Options
+
+
+

This document has been shared with you via a secure link.

+ +
+ + +
+ +
+ + +
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/upload.html b/templates/upload.html new file mode 100644 index 0000000..279678a --- /dev/null +++ b/templates/upload.html @@ -0,0 +1,98 @@ +{% extends "layout.html" %} + +{% block content %} +

Upload Document

+ +
+
+
+
+
Upload New Document
+
+
+
+
+ + +
+ Allowed file types: PDF, DOCX, XLSX, JPG, PNG, MP4, TXT +
+
+ +
+ + +
+ If left blank, the original filename will be used +
+
+ +
+ + +
+ +
+ +
+ + +
+
+ + +
+
+ +
+ +
+
+
+
+
+ +
+
+
+
Information
+
+
+
File Size Limits
+

Maximum file size: 16MB

+ +
Supported File Types
+
    +
  • Documents: PDF, DOCX, TXT
  • +
  • Spreadsheets: XLSX
  • +
  • Images: JPG, PNG
  • +
  • Videos: MP4
  • +
+ +
Categories
+
    +
  • Admin: Administrative documents
  • +
  • Accounting: Financial documents
  • +
  • HR: Human resources documents
  • +
  • Marketing: Marketing materials
  • +
  • Legal: Legal documents
  • +
  • General: General purpose documents
  • +
  • Other: Miscellaneous documents
  • +
+
+
+
+
+{% endblock %} diff --git a/templates/view_document.html b/templates/view_document.html new file mode 100644 index 0000000..e509065 --- /dev/null +++ b/templates/view_document.html @@ -0,0 +1,160 @@ +{% extends "layout.html" %} + +{% block content %} +
+
+

{{ document.original_filename }}

+
+ +
+ +
+
+
+
+
Document Viewer
+
+
+
+ {% if document.file_type == 'pdf' %} +
+ +
+ {% elif document.file_type == 'image' %} + {{ document.original_filename }} + {% elif document.file_type == 'video' %} +
+ +
+ {% else %} +
+ Preview not available for this file type. Please download the file to view its contents. +
+ {% endif %} +
+ +
+ + Download + + + Share + + {% if document.user_id == session.user_id or session.role == 'admin' %} + + Delete + + {% endif %} +
+
+
+ +
+
+
Version History
+
+
+
+ + + + + + + + + + {% for version in versions %} + + + + + + {% endfor %} + +
VersionDateActions
v{{ version.version_number }}{{ version.created_at }} + + Download + +
+
+
+
+
+ +
+
+
+
Document Information
+
+
+
    +
  • + File Type + {{ document.file_type }} +
  • +
  • + Size + {{ (document.file_size / 1024)|round(1) }} KB +
  • +
  • + Uploaded + {{ document.created_at.split(' ')[0] }} +
  • +
  • + Visibility + + {{ document.visibility }} + +
  • +
  • + Versions + {{ versions|length }} +
  • +
+
+
+ +
+
+
Sharing Information
+
+
+ {% if shares %} +
Shared With
+
    + {% for share in shares %} +
  • + {% if share.shared_with %} + {{ share.username }} + {% elif share.share_link %} + Via Link + + {% if share.expires_at %} + Expires: {{ share.expires_at.split(' ')[0] }} + {% endif %} + + {% endif %} +
  • + {% endfor %} +
+ {% else %} +

This document hasn't been shared yet.

+ {% endif %} + + + Manage Sharing + +
+
+
+
+{% endblock %} diff --git a/uploads/8402731ae1994135823f9a7076255c78.pdf b/uploads/8402731ae1994135823f9a7076255c78.pdf new file mode 100644 index 0000000..79c46d2 --- /dev/null +++ b/uploads/8402731ae1994135823f9a7076255c78.pdf @@ -0,0 +1,74 @@ +%PDF-1.4 +%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com +1 0 obj +<< +/F1 2 0 R /F2 3 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/Contents 8 0 R /MediaBox [ 0 0 612 792 ] /Parent 7 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +5 0 obj +<< +/PageMode /UseNone /Pages 7 0 R /Type /Catalog +>> +endobj +6 0 obj +<< +/Author (\(anonymous\)) /CreationDate (D:20250419002510-06'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20250419002510-06'00') /Producer (ReportLab PDF Library - www.reportlab.com) + /Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False +>> +endobj +7 0 obj +<< +/Count 1 /Kids [ 4 0 R ] /Type /Pages +>> +endobj +8 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 529 +>> +stream +Gat='gMYb"%"6H'SB&'?;G!(UO2I8!__@a?J&$#f$*'2bAXE&8YLFaE%h_F"gc[^)I0%X)Pc5hu_FE>V7;%?hdLCrlk.^1WtmrJhE7H7@Uo];&Ff-6nsG%#k0re&L-;G\@#YN=BTFDp9ifP=K.j6rY!a+WiH,?8*`<7RKoKQCQb9A(BZdV?r=iBJ"o'dC9KP">m\>811tXQ$SYV=Uu^knO09pVM#ZD&?k_>AG8SQa&"d2VRuiZY\8C2G'57>>o'CP=IOs]eQpANnCr;]'2E^joo27AAD=.,*N>RiD-=WOId&C`12"KYe?bV@?"Uao/':&Y&I]D@Rg.udZNXeAjg2S-&i6?/h62Q@A'Kn:Tendstream +endobj +xref +0 9 +0000000000 65535 f +0000000073 00000 n +0000000114 00000 n +0000000221 00000 n +0000000333 00000 n +0000000526 00000 n +0000000594 00000 n +0000000877 00000 n +0000000936 00000 n +trailer +<< +/ID +[] +% ReportLab generated PDF document -- digest (http://www.reportlab.com) + +/Info 6 0 R +/Root 5 0 R +/Size 9 +>> +startxref +1555 +%%EOF diff --git a/uploads/930d548939884089a2ef119b8e181a3f.pdf b/uploads/930d548939884089a2ef119b8e181a3f.pdf new file mode 100644 index 0000000..a37272d Binary files /dev/null and b/uploads/930d548939884089a2ef119b8e181a3f.pdf differ diff --git a/uploads/990c4a4b1a004a7090e221e401f5b4eb.pdf b/uploads/990c4a4b1a004a7090e221e401f5b4eb.pdf new file mode 100644 index 0000000..a37272d Binary files /dev/null and b/uploads/990c4a4b1a004a7090e221e401f5b4eb.pdf differ