Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 45 additions & 9 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g, Response, send_file, abort
from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, CompanySettings, UserPreferences, WorkRegion, AccountType, ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, TaskDependency, Sprint, SprintStatus, Announcement, SystemEvent, WidgetType, UserDashboard, DashboardWidget, WidgetTemplate, Comment, CommentVisibility, BrandingSettings, CompanyInvitation
from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, CompanySettings, UserPreferences, WorkRegion, AccountType, ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, TaskDependency, Sprint, SprintStatus, Announcement, SystemEvent, WidgetType, UserDashboard, DashboardWidget, WidgetTemplate, Comment, CommentVisibility, BrandingSettings, CompanyInvitation, Note, NoteFolder, NoteShare
from data_formatting import (
format_duration, prepare_export_data, prepare_team_hours_export_data,
format_table_data, format_graph_data, format_team_data, format_burndown_data
Expand All @@ -20,9 +20,10 @@
from werkzeug.security import check_password_hash

# Import blueprints
# from routes.notes import notes_bp
# from routes.notes_download import notes_download_bp
# from routes.notes_api import notes_api_bp
from routes.notes import notes_bp
from routes.notes_download import notes_download_bp
from routes.notes_api import notes_api_bp
from routes.notes_public import notes_public_bp
from routes.tasks import tasks_bp, get_filtered_tasks_for_burndown
from routes.tasks_api import tasks_api_bp
from routes.sprints import sprints_bp
Expand All @@ -39,6 +40,7 @@
from routes.announcements import announcements_bp
from routes.export import export_bp
from routes.export_api import export_api_bp
from routes.organization import organization_bp

# Import auth decorators from routes.auth
from routes.auth import login_required, admin_required, system_admin_required, role_required, company_required
Expand Down Expand Up @@ -84,9 +86,10 @@
db.init_app(app)

# Register blueprints
# app.register_blueprint(notes_bp)
# app.register_blueprint(notes_download_bp)
# app.register_blueprint(notes_api_bp)
app.register_blueprint(notes_bp)
app.register_blueprint(notes_download_bp)
app.register_blueprint(notes_api_bp)
app.register_blueprint(notes_public_bp)
app.register_blueprint(tasks_bp)
app.register_blueprint(tasks_api_bp)
app.register_blueprint(sprints_bp)
Expand All @@ -103,6 +106,7 @@
app.register_blueprint(announcements_bp)
app.register_blueprint(export_bp)
app.register_blueprint(export_api_bp)
app.register_blueprint(organization_bp)

# Import and register invitations blueprint
from routes.invitations import invitations_bp
Expand Down Expand Up @@ -829,8 +833,10 @@ def verify_email(token):
@role_required(Role.TEAM_MEMBER)
@company_required
def dashboard():
"""User dashboard with configurable widgets."""
return render_template('dashboard.html', title='Dashboard')
"""User dashboard with configurable widgets - DISABLED due to widget issues."""
# Redirect to home page instead of dashboard
flash('Dashboard is temporarily disabled. Redirecting to home page.', 'info')
return redirect(url_for('index'))


@app.route('/profile', methods=['GET', 'POST'])
Expand Down Expand Up @@ -2666,6 +2672,36 @@ def search_sprints():
logger.error(f"Error in search_sprints: {str(e)}")
return jsonify({'success': False, 'message': str(e)})

@app.route('/api/render-markdown', methods=['POST'])
@login_required
def render_markdown():
"""Render markdown content to HTML for preview"""
try:
data = request.get_json()
content = data.get('content', '')

if not content:
return jsonify({'html': '<p class="preview-placeholder">Start typing to see the preview...</p>'})

# Parse frontmatter and extract body
from frontmatter_utils import parse_frontmatter
metadata, body = parse_frontmatter(content)

# Render markdown to HTML
try:
import markdown
# Use extensions for better markdown support
html = markdown.markdown(body, extensions=['extra', 'codehilite', 'toc', 'tables', 'fenced_code'])
except ImportError:
# Fallback if markdown not installed
html = f'<pre>{body}</pre>'

return jsonify({'html': html})

except Exception as e:
logger.error(f"Error rendering markdown: {str(e)}")
return jsonify({'html': '<p class="error">Error rendering markdown</p>'})

if __name__ == '__main__':
port = int(os.environ.get('PORT', 5000))
app.run(debug=True, host='0.0.0.0', port=port)
22 changes: 12 additions & 10 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,20 @@ services:

timetrack:
build: .
environment:
FLASK_ENV: ${FLASK_ENV:-production}
SECRET_KEY: ${SECRET_KEY}
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
MAIL_SERVER: ${MAIL_SERVER}
MAIL_PORT: ${MAIL_PORT}
MAIL_USE_TLS: ${MAIL_USE_TLS}
MAIL_USERNAME: ${MAIL_USERNAME}
MAIL_PASSWORD: ${MAIL_PASSWORD}
MAIL_DEFAULT_SENDER: ${MAIL_DEFAULT_SENDER}
ports:
- "${TIMETRACK_PORT:-5000}:5000"
environment:
- DATABASE_URL=${DATABASE_URL}
- POSTGRES_HOST=db
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- FLASK_ENV=${FLASK_ENV}
- SECRET_KEY=${SECRET_KEY}
- MAIL_SERVER=${MAIL_SERVER}
- MAIL_USERNAME=${MAIL_USERNAME}
- MAIL_PASSWORD=${MAIL_PASSWORD}
- MAIL_DEFAULT_SENDER=${MAIL_DEFAULT_SENDER}
depends_on:
db:
condition: service_healthy
Expand Down
70 changes: 70 additions & 0 deletions frontmatter_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import yaml
import re
from datetime import datetime

def parse_frontmatter(content):
"""
Parse YAML frontmatter from markdown content.
Returns a tuple of (metadata dict, content without frontmatter)
"""
if not content or not content.strip().startswith('---'):
return {}, content

# Match frontmatter pattern
pattern = r'^---\s*\n(.*?)\n---\s*\n(.*)$'
match = re.match(pattern, content, re.DOTALL)

if not match:
return {}, content

try:
# Parse YAML frontmatter
metadata = yaml.safe_load(match.group(1)) or {}
content_body = match.group(2)
return metadata, content_body
except yaml.YAMLError:
# If YAML parsing fails, return original content
return {}, content

def create_frontmatter(metadata):
"""
Create YAML frontmatter from metadata dict.
"""
if not metadata:
return ""

# Filter out None values and empty strings
filtered_metadata = {k: v for k, v in metadata.items() if v is not None and v != ''}

if not filtered_metadata:
return ""

return f"---\n{yaml.dump(filtered_metadata, default_flow_style=False, sort_keys=False)}---\n\n"

def update_frontmatter(content, metadata):
"""
Update or add frontmatter to content.
"""
_, body = parse_frontmatter(content)
frontmatter = create_frontmatter(metadata)
return frontmatter + body

def extract_title_from_content(content):
"""
Extract title from content, checking frontmatter first, then first line.
"""
metadata, body = parse_frontmatter(content)

# Check if title is in frontmatter
if metadata.get('title'):
return metadata['title']

# Otherwise extract from first line of body
lines = body.strip().split('\n')
for line in lines:
line = line.strip()
if line:
# Remove markdown headers if present
return re.sub(r'^#+\s*', '', line)

return 'Untitled Note'
20 changes: 20 additions & 0 deletions migrations/add_cascade_delete_note_links.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-- Migration to add CASCADE delete to note_link foreign keys
-- This ensures that when a note is deleted, all links to/from it are also deleted

-- For PostgreSQL
-- Drop existing foreign key constraints
ALTER TABLE note_link DROP CONSTRAINT IF EXISTS note_link_source_note_id_fkey;
ALTER TABLE note_link DROP CONSTRAINT IF EXISTS note_link_target_note_id_fkey;

-- Add new foreign key constraints with CASCADE
ALTER TABLE note_link
ADD CONSTRAINT note_link_source_note_id_fkey
FOREIGN KEY (source_note_id)
REFERENCES note(id)
ON DELETE CASCADE;

ALTER TABLE note_link
ADD CONSTRAINT note_link_target_note_id_fkey
FOREIGN KEY (target_note_id)
REFERENCES note(id)
ON DELETE CASCADE;
25 changes: 25 additions & 0 deletions migrations/add_cascade_delete_note_links_sqlite.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- SQLite migration for cascade delete on note_link
-- SQLite doesn't support ALTER TABLE for foreign keys, so we need to recreate the table

-- Create new table with CASCADE delete
CREATE TABLE note_link_new (
id INTEGER PRIMARY KEY,
source_note_id INTEGER NOT NULL,
target_note_id INTEGER NOT NULL,
link_type VARCHAR(50) DEFAULT 'related',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
created_by_id INTEGER NOT NULL,
FOREIGN KEY (source_note_id) REFERENCES note(id) ON DELETE CASCADE,
FOREIGN KEY (target_note_id) REFERENCES note(id) ON DELETE CASCADE,
FOREIGN KEY (created_by_id) REFERENCES user(id),
UNIQUE(source_note_id, target_note_id)
);

-- Copy data from old table
INSERT INTO note_link_new SELECT * FROM note_link;

-- Drop old table
DROP TABLE note_link;

-- Rename new table
ALTER TABLE note_link_new RENAME TO note_link;
5 changes: 5 additions & 0 deletions migrations/add_folder_to_notes.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- Add folder column to notes table
ALTER TABLE note ADD COLUMN IF NOT EXISTS folder VARCHAR(100);

-- Create an index on folder for faster filtering
CREATE INDEX IF NOT EXISTS idx_note_folder ON note(folder) WHERE folder IS NOT NULL;
17 changes: 17 additions & 0 deletions migrations/add_note_folder_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- Create note_folder table for tracking folders independently of notes
CREATE TABLE IF NOT EXISTS note_folder (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
path VARCHAR(500) NOT NULL,
parent_path VARCHAR(500),
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by_id INTEGER NOT NULL REFERENCES "user"(id),
company_id INTEGER NOT NULL REFERENCES company(id),
CONSTRAINT uq_folder_path_company UNIQUE (path, company_id)
);

-- Create indexes for better performance
CREATE INDEX IF NOT EXISTS idx_note_folder_company ON note_folder(company_id);
CREATE INDEX IF NOT EXISTS idx_note_folder_parent_path ON note_folder(parent_path);
CREATE INDEX IF NOT EXISTS idx_note_folder_created_by ON note_folder(created_by_id);
21 changes: 21 additions & 0 deletions migrations/add_note_sharing.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-- Add note_share table for public note sharing functionality
CREATE TABLE IF NOT EXISTS note_share (
id SERIAL PRIMARY KEY,
note_id INTEGER NOT NULL REFERENCES note(id) ON DELETE CASCADE,
token VARCHAR(64) UNIQUE NOT NULL,
expires_at TIMESTAMP,
password_hash VARCHAR(255),
view_count INTEGER DEFAULT 0,
max_views INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by_id INTEGER NOT NULL REFERENCES "user"(id),
last_accessed_at TIMESTAMP
);

-- Create indexes for better performance
CREATE INDEX IF NOT EXISTS idx_note_share_token ON note_share(token);
CREATE INDEX IF NOT EXISTS idx_note_share_note_id ON note_share(note_id);
CREATE INDEX IF NOT EXISTS idx_note_share_created_by ON note_share(created_by_id);

-- Add comment
COMMENT ON TABLE note_share IS 'Public sharing links for notes with optional password protection and view limits';
Loading
Loading