Initial commit
This commit is contained in:
commit
e157eb4e0f
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
35
Dockerfile
Normal file
35
Dockerfile
Normal file
@ -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"]
|
||||||
816
app.py
Normal file
816
app.py
Normal file
@ -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/<int:document_id>/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/<int:document_id>')
|
||||||
|
@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/<int:document_id>/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/<int:document_id>/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/<int:document_id>/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/<int:document_id>/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/<share_token>')
|
||||||
|
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/<share_token>/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()
|
||||||
47
edit_document_route.py
Normal file
47
edit_document_route.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
@app.route('/document/<int:document_id>/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)
|
||||||
61
files_route.py
Normal file
61
files_route.py
Normal file
@ -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)
|
||||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@ -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
|
||||||
64
static/scripts.js
Normal file
64
static/scripts.js
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
70
static/styles.css
Normal file
70
static/styles.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
123
templates/admin.html
Normal file
123
templates/admin.html
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2 class="mb-4"><i class="bi bi-shield-lock"></i> Admin Panel</h2>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-file-earmark-text"></i> Document Statistics</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
Total Documents
|
||||||
|
<span class="badge bg-primary rounded-pill">{{ doc_stats.total_documents }}</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
Total Storage Used
|
||||||
|
<span class="badge bg-info rounded-pill">{{ (doc_stats.total_size / (1024*1024))|round(2) }} MB</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
Active Contributors
|
||||||
|
<span class="badge bg-success rounded-pill">{{ doc_stats.total_contributors }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-activity"></i> Activity Statistics</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
Total Actions
|
||||||
|
<span class="badge bg-primary rounded-pill">{{ activity_stats.total_actions }}</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
Active Users
|
||||||
|
<span class="badge bg-info rounded-pill">{{ activity_stats.active_users }}</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
Accessed Documents
|
||||||
|
<span class="badge bg-success rounded-pill">{{ activity_stats.accessed_documents }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-warning text-dark">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-gear"></i> System Actions</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button class="btn btn-outline-primary">
|
||||||
|
<i class="bi bi-arrow-repeat"></i> Rebuild Search Index
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-warning">
|
||||||
|
<i class="bi bi-trash"></i> Clear Expired Shares
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-danger">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i> System Maintenance
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-info text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-people"></i> User Management</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user in users %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ user.id }}</td>
|
||||||
|
<td>{{ user.username }}</td>
|
||||||
|
<td>{{ user.email }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-{{ 'danger' if user.role == 'admin' else 'primary' }}">
|
||||||
|
{{ user.role }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ user.created_at.split(' ')[0] }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<button class="btn btn-outline-primary">
|
||||||
|
<i class="bi bi-pencil"></i> Edit
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-danger" {% if user.role == 'admin' %}disabled{% endif %}>
|
||||||
|
<i class="bi bi-trash"></i> Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
194
templates/dashboard.html
Normal file
194
templates/dashboard.html
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2 class="mb-4"><i class="bi bi-speedometer2"></i> Dashboard</h2>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Category Summary -->
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-folder"></i> Document Categories</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if category_counts %}
|
||||||
|
<div class="row">
|
||||||
|
{% for category in category_counts %}
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-0">{{ category.category|capitalize }}</h6>
|
||||||
|
<small class="text-muted">{{ category.count }} document{% if category.count != 1 %}s{% endif %}</small>
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('files', category=category.category) }}" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-arrow-right"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle"></i> No documents found. Upload your first document to get started!
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<a href="{{ url_for('files') }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-table"></i> View All Documents
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-lightning"></i> Quick Actions</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<a href="{{ url_for('upload') }}" class="btn btn-outline-success w-100 h-100 d-flex flex-column justify-content-center align-items-center p-4">
|
||||||
|
<i class="bi bi-upload fs-2 mb-2"></i>
|
||||||
|
<span>Upload Document</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<a href="{{ url_for('files') }}" class="btn btn-outline-primary w-100 h-100 d-flex flex-column justify-content-center align-items-center p-4">
|
||||||
|
<i class="bi bi-table fs-2 mb-2"></i>
|
||||||
|
<span>View Files</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<a href="{{ url_for('files', category='admin') }}" class="btn btn-outline-secondary w-100 h-100 d-flex flex-column justify-content-center align-items-center p-4">
|
||||||
|
<i class="bi bi-briefcase fs-2 mb-2"></i>
|
||||||
|
<span>Admin Files</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<a href="{{ url_for('files', category='accounting') }}" class="btn btn-outline-info w-100 h-100 d-flex flex-column justify-content-center align-items-center p-4">
|
||||||
|
<i class="bi bi-calculator fs-2 mb-2"></i>
|
||||||
|
<span>Accounting Files</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Recent Documents -->
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header bg-info text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-clock-history"></i> Recent Documents</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if recent_documents %}
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
{% for doc in recent_documents %}
|
||||||
|
<a href="{{ url_for('view_document', document_id=doc.id) }}" class="list-group-item list-group-item-action">
|
||||||
|
<div class="d-flex w-100 justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
{% if doc.file_type == 'pdf' %}
|
||||||
|
<i class="bi bi-file-earmark-pdf text-danger me-2"></i>
|
||||||
|
{% elif doc.file_type == 'document' %}
|
||||||
|
<i class="bi bi-file-earmark-word text-primary me-2"></i>
|
||||||
|
{% elif doc.file_type == 'spreadsheet' %}
|
||||||
|
<i class="bi bi-file-earmark-excel text-success me-2"></i>
|
||||||
|
{% elif doc.file_type == 'image' %}
|
||||||
|
<i class="bi bi-file-earmark-image text-info me-2"></i>
|
||||||
|
{% elif doc.file_type == 'video' %}
|
||||||
|
<i class="bi bi-file-earmark-play text-warning me-2"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-file-earmark text-secondary me-2"></i>
|
||||||
|
{% endif %}
|
||||||
|
<h6 class="mb-0">{{ doc.custom_filename }}</h6>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">
|
||||||
|
<span class="badge bg-secondary">{{ doc.category|capitalize }}</span>
|
||||||
|
{{ (doc.file_size / 1024)|round(1) }} KB
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">{{ doc.created_at.split(' ')[0] }}</small>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info m-3">
|
||||||
|
<i class="bi bi-info-circle"></i> No documents found. Upload your first document to get started!
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if recent_documents %}
|
||||||
|
<div class="card-footer text-center">
|
||||||
|
<a href="{{ url_for('files') }}" class="btn btn-sm btn-outline-info">View All Documents</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Analytics (Admin Only) -->
|
||||||
|
{% if session.role == 'admin' and analytics %}
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header bg-warning text-dark">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-graph-up"></i> Analytics Overview</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h6>Most Viewed Documents</h6>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Document</th>
|
||||||
|
<th>Views</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for doc in analytics %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ doc.custom_filename }}</td>
|
||||||
|
<td>{{ doc.view_count }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if user_activity %}
|
||||||
|
<h6 class="mt-4">Most Active Users</h6>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user in user_activity %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ user.username }}</td>
|
||||||
|
<td>{{ user.action_count }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
77
templates/edit_document.html
Normal file
77
templates/edit_document.html
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2 class="mb-4"><i class="bi bi-pencil-square"></i> Edit Document</h2>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0">Edit Document Details</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" action="{{ url_for('edit_document', document_id=document.id) }}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="custom_filename" class="form-label">Document Name</label>
|
||||||
|
<input type="text" class="form-control" id="custom_filename" name="custom_filename"
|
||||||
|
value="{{ document.custom_filename }}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="category" class="form-label">Category</label>
|
||||||
|
<select class="form-select" id="category" name="category" required>
|
||||||
|
{% for category in categories %}
|
||||||
|
<option value="{{ category }}" {% if category == document.category %}selected{% endif %}>
|
||||||
|
{{ category|capitalize }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Visibility</label>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="visibility" id="private" value="private"
|
||||||
|
{% if document.visibility == 'private' %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="private">
|
||||||
|
<i class="bi bi-lock"></i> Private (Only you can access)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="visibility" id="public" value="public"
|
||||||
|
{% if document.visibility == 'public' %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="public">
|
||||||
|
<i class="bi bi-globe"></i> Public (Anyone can access)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="{{ url_for('view_document', document_id=document.id) }}" class="btn btn-secondary">
|
||||||
|
<i class="bi bi-x-circle"></i> Cancel
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-save"></i> Save Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-info text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Document Information</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p><strong>Original Filename:</strong> {{ document.original_filename }}</p>
|
||||||
|
<p><strong>File Type:</strong> {{ document.file_type|capitalize }}</p>
|
||||||
|
<p><strong>File Size:</strong> {{ (document.file_size / 1024)|round(1) }} KB</p>
|
||||||
|
<p><strong>Uploaded:</strong> {{ document.created_at.split(' ')[0] }}</p>
|
||||||
|
<p><strong>Last Modified:</strong> {{ document.updated_at.split(' ')[0] }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
49
templates/error.html
Normal file
49
templates/error.html
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Document Management System - Error</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.0/font/bootstrap-icons.css">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Navigation -->
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="{{ url_for('index') }}">
|
||||||
|
<i class="bi bi-folder2-open"></i> DMS
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="container mt-5 pt-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8 text-center">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h1 class="display-1 text-danger">{{ error_code }}</h1>
|
||||||
|
<h2 class="mb-4">{{ error_message }}</h2>
|
||||||
|
<p class="lead">Sorry, something went wrong.</p>
|
||||||
|
<a href="{{ url_for('index') }}" class="btn btn-primary mt-3">
|
||||||
|
<i class="bi bi-house"></i> Return to Home
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="bg-dark text-white py-4 mt-5">
|
||||||
|
<div class="container text-center">
|
||||||
|
<p>Document Management System</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="{{ url_for('static', filename='scripts.js') }}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
145
templates/files.html
Normal file
145
templates/files.html
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h2><i class="bi bi-table"></i> Files</h2>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 text-end">
|
||||||
|
<a href="{{ url_for('upload') }}" class="btn btn-success">
|
||||||
|
<i class="bi bi-upload"></i> Upload New Document
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-file-earmark"></i> Document Library</h5>
|
||||||
|
<form class="d-flex" action="{{ url_for('files') }}" method="get">
|
||||||
|
<select name="category" class="form-select form-select-sm me-2" style="width: 150px;" onchange="this.form.submit()">
|
||||||
|
<option value="all" {% if selected_category == 'all' %}selected{% endif %}>All Categories</option>
|
||||||
|
{% for category in categories %}
|
||||||
|
<option value="{{ category }}" {% if selected_category == category %}selected{% endif %}>
|
||||||
|
{{ category|capitalize }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control form-control-sm" placeholder="Search files..." name="search" value="{{ search_query }}">
|
||||||
|
<button class="btn btn-outline-light btn-sm" type="submit">
|
||||||
|
<i class="bi bi-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Category</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>Uploaded</th>
|
||||||
|
<th>Visibility</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% if documents %}
|
||||||
|
{% for doc in documents %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
{% if doc.file_type == 'pdf' %}
|
||||||
|
<i class="bi bi-file-earmark-pdf text-danger me-2"></i>
|
||||||
|
{% elif doc.file_type == 'document' %}
|
||||||
|
<i class="bi bi-file-earmark-word text-primary me-2"></i>
|
||||||
|
{% elif doc.file_type == 'spreadsheet' %}
|
||||||
|
<i class="bi bi-file-earmark-excel text-success me-2"></i>
|
||||||
|
{% elif doc.file_type == 'image' %}
|
||||||
|
<i class="bi bi-file-earmark-image text-info me-2"></i>
|
||||||
|
{% elif doc.file_type == 'video' %}
|
||||||
|
<i class="bi bi-file-earmark-play text-warning me-2"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-file-earmark text-secondary me-2"></i>
|
||||||
|
{% endif %}
|
||||||
|
<span>{{ doc.custom_filename }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td><span class="badge bg-secondary">{{ doc.category|capitalize }}</span></td>
|
||||||
|
<td>{{ doc.file_type|capitalize }}</td>
|
||||||
|
<td>{{ (doc.file_size / 1024)|round(1) }} KB</td>
|
||||||
|
<td>{{ doc.created_at.split(' ')[0] }}</td>
|
||||||
|
<td>
|
||||||
|
{% if doc.visibility == 'public' %}
|
||||||
|
<span class="badge bg-success">Public</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-warning text-dark">Private</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<a href="{{ url_for('view_document', document_id=doc.id) }}" class="btn btn-outline-primary" title="View">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('edit_document', document_id=doc.id) }}" class="btn btn-outline-secondary" title="Edit">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('share_document', document_id=doc.id) }}" class="btn btn-outline-info" title="Share">
|
||||||
|
<i class="bi bi-share"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('download_document', document_id=doc.id) }}" class="btn btn-outline-success" title="Download">
|
||||||
|
<i class="bi bi-download"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('delete_document', document_id=doc.id) }}" class="btn btn-outline-danger"
|
||||||
|
onclick="return confirm('Are you sure you want to delete this document?')" title="Delete">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="text-center py-4">
|
||||||
|
<div class="alert alert-info mb-0">
|
||||||
|
<i class="bi bi-info-circle"></i>
|
||||||
|
{% 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 %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if pagination and pagination.pages > 1 %}
|
||||||
|
<nav aria-label="Document pagination">
|
||||||
|
<ul class="pagination justify-content-center">
|
||||||
|
<li class="page-item {% if pagination.page == 1 %}disabled{% endif %}">
|
||||||
|
<a class="page-link" href="{{ url_for('files', page=pagination.page-1, search=search_query, category=selected_category) }}">Previous</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{% for p in range(1, pagination.pages + 1) %}
|
||||||
|
<li class="page-item {% if pagination.page == p %}active{% endif %}">
|
||||||
|
<a class="page-link" href="{{ url_for('files', page=p, search=search_query, category=selected_category) }}">{{ p }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<li class="page-item {% if pagination.page == pagination.pages %}disabled{% endif %}">
|
||||||
|
<a class="page-link" href="{{ url_for('files', page=pagination.page+1, search=search_query, category=selected_category) }}">Next</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
17
templates/index.html
Normal file
17
templates/index.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8 text-center">
|
||||||
|
<div class="card mt-5">
|
||||||
|
<div class="card-body">
|
||||||
|
<h1 class="display-4 mb-4">Document Management System</h1>
|
||||||
|
<p class="lead">A secure platform for managing, sharing, and collaborating on documents.</p>
|
||||||
|
<hr class="my-4">
|
||||||
|
<p>Please login to access your documents and start managing your files.</p>
|
||||||
|
<a href="{{ url_for('login') }}" class="btn btn-primary btn-lg">Login</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
92
templates/layout.html
Normal file
92
templates/layout.html
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Document Management System</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.0/font/bootstrap-icons.css">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Navigation -->
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="{{ url_for('index') }}">
|
||||||
|
<i class="bi bi-folder2-open"></i> DMS
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav me-auto">
|
||||||
|
{% if session.user_id %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('dashboard') }}">Dashboard</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('files') }}">Files</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('upload') }}">Upload</a>
|
||||||
|
</li>
|
||||||
|
{% if session.role == 'admin' %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('admin_panel') }}">Admin</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
{% if session.user_id %}
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown">
|
||||||
|
<i class="bi bi-person-circle"></i> {{ session.username }}
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('logout') }}">Logout</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('login') }}">Login</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Flash Messages -->
|
||||||
|
<div class="flash-messages">
|
||||||
|
{% 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 %}
|
||||||
|
<div class="alert {{ alert_class }} alert-dismissible fade show">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="container mt-4">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="bg-dark text-white py-4 mt-5">
|
||||||
|
<div class="container text-center">
|
||||||
|
<p>Document Management System</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="{{ url_for('static', filename='scripts.js') }}"></script>
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
33
templates/login.html
Normal file
33
templates/login.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card mt-5">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h4 class="mb-0"><i class="bi bi-person"></i> Login</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" action="{{ url_for('login') }}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username" class="form-label">Username</label>
|
||||||
|
<input type="text" class="form-control" id="username" name="username" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">Password</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary">Login</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<!-- <p class="text-muted">Demo Accounts:</p>
|
||||||
|
<p><small>Admin: username: <strong>admin</strong>, password: <strong>admin123</strong></small></p>
|
||||||
|
<p><small>User: username: <strong>user</strong>, password: <strong>user123</strong></small></p> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
163
templates/share_document.html
Normal file
163
templates/share_document.html
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h2><i class="bi bi-share"></i> Share Document: {{ document.original_filename }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 text-end">
|
||||||
|
<a href="{{ url_for('view_document', document_id=document.id) }}" class="btn btn-secondary">
|
||||||
|
<i class="bi bi-arrow-left"></i> Back to Document
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-person-plus"></i> Share with User</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" action="{{ url_for('share_document', document_id=document.id) }}">
|
||||||
|
<input type="hidden" name="share_type" value="user">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username" class="form-label">Username</label>
|
||||||
|
<select class="form-select" id="username" name="username" required>
|
||||||
|
<option value="" selected disabled>Select a user</option>
|
||||||
|
{% for user in users %}
|
||||||
|
<option value="{{ user.username }}">{{ user.username }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-share"></i> Share with User
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-info text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-link"></i> Create Shareable Link</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" action="{{ url_for('share_document', document_id=document.id) }}">
|
||||||
|
<input type="hidden" name="share_type" value="link">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="expiry" class="form-label">Link Expiry</label>
|
||||||
|
<select class="form-select" id="expiry" name="expiry">
|
||||||
|
<option value="1">1 day</option>
|
||||||
|
<option value="7" selected>7 days</option>
|
||||||
|
<option value="30">30 days</option>
|
||||||
|
<option value="90">90 days</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-info">
|
||||||
|
<i class="bi bi-link"></i> Generate Link
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-people"></i> Current Shares</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if shares %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Shared With</th>
|
||||||
|
<th>Date Shared</th>
|
||||||
|
<th>Expiry</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for share in shares %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{% if share.shared_with %}
|
||||||
|
<span class="badge bg-primary"><i class="bi bi-person"></i> User</span>
|
||||||
|
{% elif share.share_link %}
|
||||||
|
<span class="badge bg-info"><i class="bi bi-link"></i> Link</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if share.shared_with %}
|
||||||
|
{{ share.username }}
|
||||||
|
{% elif share.share_link %}
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<input type="text" class="form-control form-control-sm" value="{{ request.host_url }}shared/{{ share.share_link }}" readonly id="link-{{ share.id }}">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" onclick="copyLink('link-{{ share.id }}')">
|
||||||
|
<i class="bi bi-clipboard"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ share.created_at.split(' ')[0] }}</td>
|
||||||
|
<td>
|
||||||
|
{% if share.expires_at %}
|
||||||
|
{{ share.expires_at.split(' ')[0] }}
|
||||||
|
{% else %}
|
||||||
|
Never
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="#" class="btn btn-sm btn-danger">
|
||||||
|
<i class="bi bi-x-circle"></i> Revoke
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle"></i> This document hasn't been shared yet.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
function copyLink(elementId) {
|
||||||
|
var copyText = document.getElementById(elementId);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
140
templates/shared_view.html
Normal file
140
templates/shared_view.html
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle"></i> You are viewing a shared document.
|
||||||
|
{% if not session.user_id %}
|
||||||
|
<a href="{{ url_for('login') }}" class="alert-link">Login</a> to access more features.
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-file-earmark"></i> {{ document.original_filename }}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="document-viewer">
|
||||||
|
{% if document.file_type == 'pdf' %}
|
||||||
|
<div class="ratio ratio-16x9">
|
||||||
|
<iframe src="{{ url_for('download_shared', share_token=share_token) }}" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
{% elif document.file_type == 'image' %}
|
||||||
|
<img src="{{ url_for('download_shared', share_token=share_token) }}" class="img-fluid" alt="{{ document.original_filename }}">
|
||||||
|
{% elif document.file_type == 'video' %}
|
||||||
|
<div class="ratio ratio-16x9">
|
||||||
|
<video controls>
|
||||||
|
<source src="{{ url_for('download_shared', share_token=share_token) }}" type="video/mp4">
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle"></i> Preview not available for this file type. Please download the file to view its contents.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="{{ url_for('download_shared', share_token=share_token) }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-download"></i> Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-secondary text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Document Information</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
File Type
|
||||||
|
<span class="badge bg-primary rounded-pill">{{ document.file_type }}</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
Size
|
||||||
|
<span class="badge bg-secondary rounded-pill">{{ (document.file_size / 1024)|round(1) }} KB</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
Uploaded
|
||||||
|
<span class="badge bg-info rounded-pill">{{ document.created_at.split(' ')[0] }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-share"></i> Sharing Options</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>This document has been shared with you via a secure link.</p>
|
||||||
|
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input type="text" class="form-control" value="{{ request.url }}" readonly id="share-link">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" onclick="copyLink('share-link')">
|
||||||
|
<i class="bi bi-clipboard"></i> Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button class="btn btn-outline-primary" onclick="shareViaEmail()">
|
||||||
|
<i class="bi bi-envelope"></i> Share via Email
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-success" onclick="shareViaWhatsApp()">
|
||||||
|
<i class="bi bi-whatsapp"></i> Share via WhatsApp
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
function copyLink(elementId) {
|
||||||
|
var copyText = document.getElementById(elementId);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shareViaEmail() {
|
||||||
|
var subject = "Shared Document: {{ document.original_filename }}";
|
||||||
|
var body = "I'm sharing a document with you: {{ document.original_filename }}\\n\\nYou can access it here: " + window.location.href;
|
||||||
|
window.location.href = "mailto:?subject=" + encodeURIComponent(subject) + "&body=" + encodeURIComponent(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shareViaWhatsApp() {
|
||||||
|
var text = "I'm sharing a document with you: {{ document.original_filename }}\\n\\nYou can access it here: " + window.location.href;
|
||||||
|
window.open("https://wa.me/?text=" + encodeURIComponent(text), "_blank");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
98
templates/upload.html
Normal file
98
templates/upload.html
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2 class="mb-4"><i class="bi bi-upload"></i> Upload Document</h2>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<h5 class="mb-0">Upload New Document</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" action="{{ url_for('upload') }}" enctype="multipart/form-data">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="file" class="form-label">Select File</label>
|
||||||
|
<input type="file" class="form-control" id="file" name="file" required>
|
||||||
|
<div class="form-text">
|
||||||
|
Allowed file types: PDF, DOCX, XLSX, JPG, PNG, MP4, TXT
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="custom_filename" class="form-label">Custom File Name (Optional)</label>
|
||||||
|
<input type="text" class="form-control" id="custom_filename" name="custom_filename" placeholder="Enter a custom name for this file">
|
||||||
|
<div class="form-text">
|
||||||
|
If left blank, the original filename will be used
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="category" class="form-label">Category</label>
|
||||||
|
<select class="form-select" id="category" name="category" required>
|
||||||
|
{% for category in categories %}
|
||||||
|
<option value="{{ category }}" {% if category == 'general' %}selected{% endif %}>
|
||||||
|
{{ category|capitalize }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Visibility</label>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="visibility" id="private" value="private" checked>
|
||||||
|
<label class="form-check-label" for="private">
|
||||||
|
<i class="bi bi-lock"></i> Private (Only you can access)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="visibility" id="public" value="public">
|
||||||
|
<label class="form-check-label" for="public">
|
||||||
|
<i class="bi bi-globe"></i> Public (Anyone can access)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-success">
|
||||||
|
<i class="bi bi-upload"></i> Upload Document
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-info text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Information</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h6>File Size Limits</h6>
|
||||||
|
<p>Maximum file size: 16MB</p>
|
||||||
|
|
||||||
|
<h6>Supported File Types</h6>
|
||||||
|
<ul>
|
||||||
|
<li>Documents: PDF, DOCX, TXT</li>
|
||||||
|
<li>Spreadsheets: XLSX</li>
|
||||||
|
<li>Images: JPG, PNG</li>
|
||||||
|
<li>Videos: MP4</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h6>Categories</h6>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Admin:</strong> Administrative documents</li>
|
||||||
|
<li><strong>Accounting:</strong> Financial documents</li>
|
||||||
|
<li><strong>HR:</strong> Human resources documents</li>
|
||||||
|
<li><strong>Marketing:</strong> Marketing materials</li>
|
||||||
|
<li><strong>Legal:</strong> Legal documents</li>
|
||||||
|
<li><strong>General:</strong> General purpose documents</li>
|
||||||
|
<li><strong>Other:</strong> Miscellaneous documents</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
160
templates/view_document.html
Normal file
160
templates/view_document.html
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h2><i class="bi bi-file-earmark"></i> {{ document.original_filename }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 text-end">
|
||||||
|
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary">
|
||||||
|
<i class="bi bi-arrow-left"></i> Back to Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-eye"></i> Document Viewer</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="document-viewer">
|
||||||
|
{% if document.file_type == 'pdf' %}
|
||||||
|
<div class="ratio ratio-16x9">
|
||||||
|
<iframe src="{{ url_for('serve_document_file', document_id=document.id) }}" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
{% elif document.file_type == 'image' %}
|
||||||
|
<img src="{{ url_for('download_document', document_id=document.id) }}" class="img-fluid" alt="{{ document.original_filename }}">
|
||||||
|
{% elif document.file_type == 'video' %}
|
||||||
|
<div class="ratio ratio-16x9">
|
||||||
|
<video controls>
|
||||||
|
<source src="{{ url_for('download_document', document_id=document.id) }}" type="video/mp4">
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle"></i> Preview not available for this file type. Please download the file to view its contents.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="{{ url_for('download_document', document_id=document.id) }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-download"></i> Download
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('share_document', document_id=document.id) }}" class="btn btn-info">
|
||||||
|
<i class="bi bi-share"></i> Share
|
||||||
|
</a>
|
||||||
|
{% if document.user_id == session.user_id or session.role == 'admin' %}
|
||||||
|
<a href="{{ url_for('delete_document', document_id=document.id) }}" class="btn btn-danger"
|
||||||
|
onclick="return confirm('Are you sure you want to delete this document?')">
|
||||||
|
<i class="bi bi-trash"></i> Delete
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-info text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-clock-history"></i> Version History</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Version</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for version in versions %}
|
||||||
|
<tr>
|
||||||
|
<td>v{{ version.version_number }}</td>
|
||||||
|
<td>{{ version.created_at }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="#" class="btn btn-sm btn-primary">
|
||||||
|
<i class="bi bi-download"></i> Download
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-secondary text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Document Information</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
File Type
|
||||||
|
<span class="badge bg-primary rounded-pill">{{ document.file_type }}</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
Size
|
||||||
|
<span class="badge bg-secondary rounded-pill">{{ (document.file_size / 1024)|round(1) }} KB</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
Uploaded
|
||||||
|
<span class="badge bg-info rounded-pill">{{ document.created_at.split(' ')[0] }}</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
Visibility
|
||||||
|
<span class="badge bg-{{ 'success' if document.visibility == 'public' else 'warning' }} rounded-pill">
|
||||||
|
{{ document.visibility }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
Versions
|
||||||
|
<span class="badge bg-dark rounded-pill">{{ versions|length }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-share"></i> Sharing Information</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if shares %}
|
||||||
|
<h6>Shared With</h6>
|
||||||
|
<ul class="list-group list-group-flush mb-3">
|
||||||
|
{% for share in shares %}
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
{% if share.shared_with %}
|
||||||
|
<span><i class="bi bi-person"></i> {{ share.username }}</span>
|
||||||
|
{% elif share.share_link %}
|
||||||
|
<span><i class="bi bi-link"></i> Via Link</span>
|
||||||
|
<small class="text-muted">
|
||||||
|
{% if share.expires_at %}
|
||||||
|
Expires: {{ share.expires_at.split(' ')[0] }}
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p>This document hasn't been shared yet.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<a href="{{ url_for('share_document', document_id=document.id) }}" class="btn btn-success w-100">
|
||||||
|
<i class="bi bi-share"></i> Manage Sharing
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
74
uploads/8402731ae1994135823f9a7076255c78.pdf
Normal file
74
uploads/8402731ae1994135823f9a7076255c78.pdf
Normal file
@ -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<eY*rZ@\g#5>$*'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<e?)fT8%mZb>&?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:T<L`HO6.L3GA:]cf5kL\tR$jMZ=DndRT5g8DB*AV\\uLAT/^/KfT?HfLOuXED$sc*Oeu`g#lT9?27[52!:'0mG'P?XSD]-s0cm?nCZHC[t1N,o:W^Bp]lM%V]*7Ffgb!Yc+:%o#Pa\b"X!jKIf~>endstream
|
||||||
|
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
|
||||||
|
[<e4c6428afa3c84fcf9371b13758dd6b1><e4c6428afa3c84fcf9371b13758dd6b1>]
|
||||||
|
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
|
||||||
|
|
||||||
|
/Info 6 0 R
|
||||||
|
/Root 5 0 R
|
||||||
|
/Size 9
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
1555
|
||||||
|
%%EOF
|
||||||
BIN
uploads/930d548939884089a2ef119b8e181a3f.pdf
Normal file
BIN
uploads/930d548939884089a2ef119b8e181a3f.pdf
Normal file
Binary file not shown.
BIN
uploads/990c4a4b1a004a7090e221e401f5b4eb.pdf
Normal file
BIN
uploads/990c4a4b1a004a7090e221e401f5b4eb.pdf
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user