Spaces:
Configuration error
Configuration error
| import os | |
| from dotenv import load_dotenv | |
| import json | |
| # Load environment variables FIRST | |
| load_dotenv() | |
| from flask import Flask, render_template, redirect, url_for, flash, session, request, jsonify, send_file | |
| from flask_login import LoginManager, login_user, logout_user, login_required, current_user | |
| from werkzeug.security import generate_password_hash, check_password_hash | |
| import requests | |
| import uuid | |
| from urllib.parse import urlencode | |
| import re | |
| from datetime import datetime | |
| from sqlalchemy.orm import joinedload | |
| # Import models and database | |
| from models import db, User, Introduction, ProfileSummary, WorkExperience, Project, Education, Skill, Achievement, ProfileSectionOrder | |
| app = Flask(__name__) | |
| # Configure Flask | |
| app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production') | |
| app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL') or os.environ.get('SQLALCHEMY_DATABASE_URI') | |
| app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False | |
| # Initialize extensions | |
| db.init_app(app) | |
| # Note: CSRFProtect disabled for now, using simple token validation | |
| # Configure Flask-Login | |
| login_manager = LoginManager() | |
| login_manager.init_app(app) | |
| login_manager.login_view = 'signin' | |
| login_manager.login_message = 'Please sign in to access this page.' | |
| login_manager.login_message_category = 'info' | |
| def load_user(user_id): | |
| return db.session.get(User, str(user_id)) | |
| # Admin required decorator | |
| def admin_required(f): | |
| """Decorator to require admin access""" | |
| def decorated_function(*args, **kwargs): | |
| if not current_user.is_admin: | |
| flash('You do not have permission to access this page.', 'error') | |
| return redirect(url_for('profile')) | |
| return f(*args, **kwargs) | |
| decorated_function.__name__ = f.__name__ | |
| return decorated_function | |
| # Note: CSRF protection disabled for simplicity in this development version | |
| # GitHub OAuth Configuration | |
| GITHUB_CLIENT_ID = os.environ.get('GITHUB_CLIENT_ID') | |
| GITHUB_CLIENT_SECRET = os.environ.get('GITHUB_CLIENT_SECRET') | |
| # Auto-detect the correct callback URL based on environment | |
| SPACE_HOST = os.environ.get('SPACE_HOST') | |
| if SPACE_HOST: | |
| GITHUB_REDIRECT_URI = f'https://{SPACE_HOST}/api/auth/github/callback' | |
| else: | |
| GITHUB_REDIRECT_URI = os.environ.get('GITHUB_OAUTH_BACKEND_REDIRECT', 'http://127.0.0.1:5000/auth/github/callback') | |
| # GitHub OAuth Functions | |
| def generate_github_auth_url(): | |
| """Generate GitHub OAuth authorization URL""" | |
| params = { | |
| 'client_id': GITHUB_CLIENT_ID, | |
| 'redirect_uri': GITHUB_REDIRECT_URI, | |
| 'scope': 'user:email', | |
| 'state': str(uuid.uuid4()) # Simple CSRF protection | |
| } | |
| return f"https://github.com/login/oauth/authorize?{urlencode(params)}" | |
| def exchange_code_for_token(code): | |
| """Exchange authorization code for access token""" | |
| data = { | |
| 'client_id': GITHUB_CLIENT_ID, | |
| 'client_secret': GITHUB_CLIENT_SECRET, | |
| 'code': code, | |
| 'redirect_uri': GITHUB_REDIRECT_URI, | |
| } | |
| headers = { | |
| 'Accept': 'application/json' | |
| } | |
| response = requests.post('https://github.com/login/oauth/access_token', | |
| data=data, headers=headers) | |
| if response.status_code == 200: | |
| result = response.json() | |
| return result.get('access_token') | |
| return None | |
| def get_github_user_info(access_token): | |
| """Get user information from GitHub API""" | |
| headers = { | |
| 'Authorization': f'token {access_token}', | |
| 'Accept': 'application/json' | |
| } | |
| response = requests.get('https://api.github.com/user', headers=headers) | |
| if response.status_code == 200: | |
| return response.json() | |
| return None | |
| def get_github_user_email(access_token): | |
| """Get user's primary email from GitHub API""" | |
| headers = { | |
| 'Authorization': f'token {access_token}', | |
| 'Accept': 'application/json' | |
| } | |
| response = requests.get('https://api.github.com/user/emails', headers=headers) | |
| if response.status_code == 200: | |
| emails = response.json() | |
| # Find primary email | |
| for email in emails: | |
| if email.get('primary') and email.get('verified'): | |
| return email.get('email') | |
| # If no primary verified email, use the first one | |
| if emails: | |
| return emails[0].get('email') | |
| return None | |
| # Email validation function | |
| def validate_email_format(email): | |
| """Validate email format with flexible rules""" | |
| import re | |
| basic_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' | |
| if re.match(basic_pattern, email): | |
| return None | |
| return "Invalid email format" | |
| # Routes | |
| def index(): | |
| if current_user.is_authenticated: | |
| return redirect(url_for('profile')) | |
| return redirect(url_for('signin')) | |
| def signin(): | |
| if current_user.is_authenticated: | |
| return redirect(url_for('profile')) | |
| if request.method == 'POST': | |
| email = request.form.get('email', '').strip() | |
| password = request.form.get('password', '') | |
| if not email or not password: | |
| flash('Please enter both email and password.', 'error') | |
| return render_template('signin.html') | |
| user = User.query.filter_by(email=email).first() | |
| if user and user.check_password(password): | |
| # Check if user should be admin | |
| admin_email = os.environ.get('ADMIN_EMAIL') | |
| if admin_email and email.lower() == admin_email.lower(): | |
| if not user.is_admin: | |
| user.is_admin = True | |
| user.role = 'Admin' | |
| db.session.commit() | |
| flash('Admin access granted!', 'success') | |
| login_user(user) | |
| next_page = request.args.get('next') | |
| return redirect(next_page or url_for('profile')) | |
| else: | |
| flash('Invalid email or password.', 'error') | |
| return render_template('signin.html') | |
| def signup(): | |
| if current_user.is_authenticated: | |
| return redirect(url_for('profile')) | |
| if request.method == 'POST': | |
| name = request.form.get('name', '').strip() | |
| email = request.form.get('email', '').strip() | |
| password = request.form.get('password', '') | |
| password_confirm = request.form.get('password_confirm', '') | |
| # Validation | |
| errors = [] | |
| if not name: | |
| errors.append('Name is required') | |
| if not email: | |
| errors.append('Email is required') | |
| else: | |
| email_error = validate_email_format(email) | |
| if email_error: | |
| errors.append(email_error) | |
| if not password: | |
| errors.append('Password is required') | |
| elif len(password) < 8: | |
| errors.append('Password must be at least 8 characters long') | |
| if not password_confirm: | |
| errors.append('Please confirm your password') | |
| elif password != password_confirm: | |
| errors.append('Passwords do not match') | |
| # Check if email already exists | |
| if email and User.query.filter_by(email=email).first(): | |
| errors.append('Email already registered') | |
| if errors: | |
| for error in errors: | |
| flash(error, 'error') | |
| return render_template('signup.html') | |
| # Create new user | |
| new_user = User( | |
| id=uuid.uuid4(), | |
| name=name, | |
| email=email | |
| ) | |
| new_user.set_password(password) | |
| # Check if user should be admin | |
| admin_email = os.environ.get('ADMIN_EMAIL') | |
| if admin_email and email.lower() == admin_email.lower(): | |
| new_user.is_admin = True | |
| new_user.role = 'Admin' | |
| try: | |
| db.session.add(new_user) | |
| db.session.commit() | |
| if new_user.is_admin: | |
| flash('Admin account created successfully! Please sign in.', 'success') | |
| else: | |
| flash('Account created successfully! Please sign in.', 'success') | |
| return redirect(url_for('signin')) | |
| except Exception as e: | |
| db.session.rollback() | |
| flash('An error occurred while creating your account. Please try again.', 'error') | |
| return render_template('signup.html') | |
| def logout(): | |
| logout_user() | |
| flash('You have been logged out successfully.', 'success') | |
| return redirect(url_for('signin')) | |
| def profile(): | |
| # Check if user has a profile | |
| intro = Introduction.query.filter_by(user_id=current_user.id).first() | |
| has_profile = intro is not None | |
| if has_profile: | |
| # Get all profile data | |
| summary = ProfileSummary.query.filter_by(user_id=current_user.id).first() | |
| work_experiences = WorkExperience.query.filter_by(user_id=current_user.id).order_by(WorkExperience.order).all() | |
| projects = Project.query.filter_by(user_id=current_user.id).order_by(Project.order).all() | |
| educations = Education.query.filter_by(user_id=current_user.id).order_by(Education.order).all() | |
| skills = Skill.query.filter_by(user_id=current_user.id).order_by(Skill.order).all() | |
| achievements = Achievement.query.filter_by(user_id=current_user.id).order_by(Achievement.order).all() | |
| # Get section order if exists | |
| section_order_obj = ProfileSectionOrder.query.filter_by(user_id=current_user.id).first() | |
| section_order = section_order_obj.section_order if section_order_obj else [ | |
| 'introduction', 'profile_summary', 'work_experience', | |
| 'projects', 'education', 'skills', 'achievements' | |
| ] | |
| return render_template('profile.html', | |
| has_profile=True, | |
| intro=intro, | |
| summary=summary, | |
| work_experiences=work_experiences, | |
| projects=projects, | |
| educations=educations, | |
| skills=skills, | |
| achievements=achievements, | |
| section_order=section_order) | |
| else: | |
| return render_template('profile.html', has_profile=False) | |
| def forgot_password(): | |
| return render_template('forgot_password.html') | |
| # GitHub OAuth Routes | |
| def github_auth(): | |
| """Initiate GitHub OAuth flow""" | |
| if not GITHUB_CLIENT_ID or not GITHUB_CLIENT_SECRET: | |
| flash('GitHub OAuth is not configured. Please check your environment variables.', 'error') | |
| return redirect(url_for('signin')) | |
| auth_url = generate_github_auth_url() | |
| return redirect(auth_url) | |
| def github_callback(): | |
| """Handle GitHub OAuth callback""" | |
| # Check for errors | |
| error = request.args.get('error') | |
| if error: | |
| flash(f'GitHub authentication failed: {error}', 'error') | |
| return redirect(url_for('signin')) | |
| # Get authorization code and state | |
| code = request.args.get('code') | |
| state = request.args.get('state') | |
| if not code: | |
| flash('Authorization code not received from GitHub.', 'error') | |
| return redirect(url_for('signin')) | |
| # Exchange code for access token | |
| access_token = exchange_code_for_token(code) | |
| if not access_token: | |
| flash('Failed to exchange authorization code for access token.', 'error') | |
| return redirect(url_for('signin')) | |
| # Get user information from GitHub | |
| github_user = get_github_user_info(access_token) | |
| if not github_user: | |
| flash('Failed to retrieve user information from GitHub.', 'error') | |
| return redirect(url_for('signin')) | |
| # Get user email | |
| email = get_github_user_email(access_token) | |
| if not email: | |
| flash('Could not retrieve email from GitHub. Please ensure your email is public and verified.', 'error') | |
| return redirect(url_for('signin')) | |
| # Check if user exists | |
| user = User.query.filter_by(email=email).first() | |
| if user: | |
| # Existing user - check if they should be admin | |
| admin_email = os.environ.get('ADMIN_EMAIL') | |
| if admin_email and email.lower() == admin_email.lower(): | |
| if not user.is_admin: | |
| user.is_admin = True | |
| user.role = 'Admin' | |
| db.session.commit() | |
| flash('Admin access granted!', 'success') | |
| login_user(user) | |
| flash(f'Welcome back, {user.name}!', 'success') | |
| return redirect(url_for('profile')) | |
| else: | |
| # New user - create account | |
| # Generate a random password for GitHub users | |
| import secrets | |
| random_password = secrets.token_urlsafe(32) | |
| new_user = User( | |
| id=uuid.uuid4(), | |
| name=github_user.get('name', github_user.get('login', 'GitHub User')), | |
| email=email | |
| ) | |
| new_user.set_password(random_password) | |
| # Check if user should be admin | |
| admin_email = os.environ.get('ADMIN_EMAIL') | |
| if admin_email and email.lower() == admin_email.lower(): | |
| new_user.is_admin = True | |
| new_user.role = 'Admin' | |
| try: | |
| db.session.add(new_user) | |
| db.session.commit() | |
| login_user(new_user) | |
| if new_user.is_admin: | |
| flash(f'Admin account created successfully! Welcome, {new_user.name}!', 'success') | |
| else: | |
| flash(f'Account created successfully! Welcome, {new_user.name}!', 'success') | |
| return redirect(url_for('profile')) | |
| except Exception as e: | |
| db.session.rollback() | |
| flash('Failed to create account. Please try again.', 'error') | |
| return redirect(url_for('signin')) | |
| # Profile Creation Routes | |
| def create_profile(): | |
| """Start profile creation process""" | |
| # Check if user already has a profile | |
| if current_user.introduction: | |
| flash('You already have a profile. View or edit it from your profile page.', 'info') | |
| return redirect(url_for('profile')) | |
| return render_template('create_profile.html') | |
| def create_introduction(): | |
| """Create introduction section""" | |
| form_data = {} | |
| form_errors = {} | |
| if request.method == 'POST': | |
| name = request.form.get('name', '').strip() | |
| email = request.form.get('email', '').strip() | |
| phone = request.form.get('phone', '').strip() | |
| linkedin = request.form.get('linkedin', '').strip() | |
| github = request.form.get('github', '').strip() | |
| website = request.form.get('website', '').strip() | |
| # Store form data | |
| form_data = { | |
| 'name': name, | |
| 'email': email, | |
| 'phone': phone, | |
| 'linkedin': linkedin, | |
| 'github': github, | |
| 'website': website | |
| } | |
| # Validation | |
| if not name: | |
| form_errors['name'] = ['Name is required'] | |
| if not email: | |
| form_errors['email'] = ['Email is required'] | |
| elif not re.match(r'^[^@]+@[^@]+\.[^@]+$', email): | |
| form_errors['email'] = ['Invalid email format'] | |
| if not phone: | |
| form_errors['phone'] = ['Phone is required'] | |
| if form_errors: | |
| return render_template('create_introduction.html', | |
| form_data=form_data, | |
| form_errors=form_errors) | |
| # Create introduction | |
| introduction = Introduction( | |
| user_id=current_user.id, | |
| name=name, | |
| email=email, | |
| phone=phone, | |
| linkedin=linkedin or None, | |
| github=github or None, | |
| website=website or None | |
| ) | |
| try: | |
| db.session.add(introduction) | |
| db.session.commit() | |
| return redirect(url_for('create_profile_summary')) | |
| except Exception as e: | |
| db.session.rollback() | |
| flash('An error occurred while saving your introduction. Please try again.', 'error') | |
| return render_template('create_introduction.html', | |
| form_data=form_data, | |
| form_errors=form_errors) | |
| def create_profile_summary(): | |
| """Create profile summary section""" | |
| form_data = {} | |
| form_errors = {} | |
| if not current_user.introduction: | |
| flash('Please complete your introduction first.', 'error') | |
| return redirect(url_for('create_introduction')) | |
| if request.method == 'POST': | |
| summary = request.form.get('summary', '').strip() | |
| # Store form data | |
| form_data = { | |
| 'summary': summary | |
| } | |
| # Validation | |
| if not summary: | |
| form_errors['summary'] = ['Profile summary is required'] | |
| if form_errors: | |
| return render_template('create_profile_summary.html', | |
| form_data=form_data, | |
| form_errors=form_errors) | |
| # Create or update profile summary | |
| profile_summary = ProfileSummary.query.filter_by(user_id=current_user.id).first() | |
| if not profile_summary: | |
| profile_summary = ProfileSummary(user_id=current_user.id) | |
| profile_summary.summary = summary | |
| profile_summary.ai_generated = False | |
| try: | |
| db.session.add(profile_summary) | |
| db.session.commit() | |
| return redirect(url_for('create_work_experience')) | |
| except Exception as e: | |
| db.session.rollback() | |
| flash('An error occurred while saving your profile summary. Please try again.', 'error') | |
| # For GET request or if returning from POST with error | |
| # Get existing summary if any | |
| existing_summary = ProfileSummary.query.filter_by(user_id=current_user.id).first() | |
| if existing_summary: | |
| form_data['summary'] = existing_summary.summary | |
| return render_template('create_profile_summary.html', | |
| form_data=form_data, | |
| form_errors=form_errors) | |
| def generate_ai_summary(): | |
| """Generate AI-powered profile summary using OpenAI""" | |
| if not current_user.introduction: | |
| return jsonify({'error': 'Please complete your introduction first'}), 400 | |
| try: | |
| # Get user's introduction and other relevant info | |
| intro = current_user.introduction | |
| # Get additional profile data if available | |
| work_experiences = WorkExperience.query.filter_by(user_id=current_user.id).all() | |
| projects = Project.query.filter_by(user_id=current_user.id).all() | |
| educations = Education.query.filter_by(user_id=current_user.id).all() | |
| skills = Skill.query.filter_by(user_id=current_user.id).all() | |
| # Prepare context for AI | |
| context = f""" | |
| Personal Information: | |
| - Name: {intro.name} | |
| - Email: {intro.email} | |
| - Phone: {intro.phone} | |
| - LinkedIn: {intro.linkedin or 'Not provided'} | |
| - GitHub: {intro.github or 'Not provided'} | |
| - Website: {intro.website or 'Not provided'} | |
| """ | |
| if work_experiences: | |
| context += "\n\nWork Experience:\n" | |
| for exp in work_experiences[:3]: # Limit to first 3 experiences | |
| context += f"- {exp.title} at {exp.organization} ({exp.start_month}/{exp.start_year} - {'Present' if not exp.end_month else f'{exp.end_month}/{exp.end_year}'})\n" | |
| if educations: | |
| context += "\nEducation:\n" | |
| for edu in educations[:2]: # Limit to first 2 educations | |
| context += f"- {edu.title} at {edu.organization} ({edu.start_month}/{edu.start_year} - {'Present' if not edu.end_month else f'{edu.end_month}/{edu.end_year}'})\n" | |
| if skills: | |
| skill_list = [s.skill for s in skills[:10]] # Limit to first 10 skills | |
| context += f"\nSkills: {', '.join(skill_list)}" | |
| # Using OpenAI API directly | |
| def generate_with_openai(): | |
| from openai import OpenAI | |
| client = OpenAI(api_key=os.environ.get('OPENAI_API_KEY')) | |
| prompt = f""" | |
| Create a professional profile summary based on this information: | |
| {context} | |
| Requirements: | |
| - Write in third person | |
| - Keep it concise (3-5 sentences) | |
| - Highlight professional strengths and achievements | |
| - Make it engaging and professional | |
| - Focus on what makes this person unique | |
| """ | |
| response = client.chat.completions.create( | |
| model=os.environ.get('OPENAI_MODEL', 'gpt-4o'), | |
| messages=[ | |
| {"role": "system", "content": "You are an expert resume writer and career coach. Write compelling professional profiles that stand out."}, | |
| {"role": "user", "content": prompt} | |
| ], | |
| max_tokens=250, | |
| temperature=0.8 | |
| ) | |
| return response.choices[0].message.content.strip() | |
| # Generate the summary | |
| ai_summary = generate_with_openai() | |
| # Save the AI-generated summary | |
| profile_summary = ProfileSummary.query.filter_by(user_id=current_user.id).first() | |
| if not profile_summary: | |
| profile_summary = ProfileSummary(user_id=current_user.id) | |
| profile_summary.summary = ai_summary | |
| profile_summary.ai_generated = True | |
| db.session.add(profile_summary) | |
| db.session.commit() | |
| return jsonify({ | |
| 'success': True, | |
| 'summary': ai_summary, | |
| 'message': 'Profile summary generated successfully!' | |
| }) | |
| except Exception as e: | |
| db.session.rollback() | |
| return jsonify({'success': False, 'error': f'Failed to generate summary: {str(e)}'}), 500 | |
| def create_work_experience(): | |
| """Create work experience section""" | |
| if not current_user.introduction: | |
| flash('Please complete your introduction first.', 'error') | |
| return redirect(url_for('create_introduction')) | |
| if request.method == 'POST': | |
| # Clear existing work experiences for this user | |
| WorkExperience.query.filter_by(user_id=current_user.id).delete() | |
| # Get form data | |
| organizations = request.form.getlist('organization[]') | |
| titles = request.form.getlist('title[]') | |
| start_months = request.form.getlist('start_month[]') | |
| start_years = request.form.getlist('start_year[]') | |
| end_months = request.form.getlist('end_month[]') | |
| end_years = request.form.getlist('end_year[]') | |
| is_present_list = request.form.getlist('is_present[]') | |
| remarks_list = request.form.getlist('remarks[]') | |
| # Find the maximum length among all lists to handle inconsistent lengths | |
| max_length = max(len(organizations), len(titles), len(start_months), | |
| len(start_years), len(end_months), len(end_years), | |
| len(remarks_list)) | |
| # Save each work experience | |
| for i in range(max_length): | |
| org = organizations[i] if i < len(organizations) else '' | |
| title = titles[i] if i < len(titles) else '' | |
| if org.strip(): # Only save if organization is provided | |
| work_exp = WorkExperience( | |
| user_id=current_user.id, | |
| organization=org.strip(), | |
| title=title.strip(), | |
| start_month=int(start_months[i]) if i < len(start_months) and start_months[i] else None, | |
| start_year=int(start_years[i]) if i < len(start_years) and start_years[i] else None, | |
| end_month=int(end_months[i]) if i < len(end_months) and end_months[i] and str(i) not in is_present_list else None, | |
| end_year=int(end_years[i]) if i < len(end_years) and end_years[i] and str(i) not in is_present_list else None, | |
| remarks=remarks_list[i].strip() if i < len(remarks_list) and remarks_list[i] else None, | |
| order=i | |
| ) | |
| db.session.add(work_exp) | |
| try: | |
| db.session.commit() | |
| flash('Work experience saved successfully.', 'success') | |
| return redirect(url_for('create_projects')) | |
| except Exception as e: | |
| db.session.rollback() | |
| flash('An error occurred while saving work experience.', 'error') | |
| # GET request - show form | |
| form_data = { | |
| 'work_experiences': WorkExperience.query.filter_by(user_id=current_user.id).order_by(WorkExperience.order).all() | |
| } | |
| return render_template('create_work_experience.html', | |
| form_data=form_data, | |
| current_year=datetime.now().year) | |
| def create_projects(): | |
| """Create projects section""" | |
| if not current_user.introduction: | |
| flash('Please complete your introduction first.', 'error') | |
| return redirect(url_for('create_introduction')) | |
| if request.method == 'POST': | |
| # Clear existing projects for this user | |
| Project.query.filter_by(user_id=current_user.id).delete() | |
| # Get form data | |
| organizations = request.form.getlist('organization[]') | |
| titles = request.form.getlist('title[]') | |
| start_months = request.form.getlist('start_month[]') | |
| start_years = request.form.getlist('start_year[]') | |
| end_months = request.form.getlist('end_month[]') | |
| end_years = request.form.getlist('end_year[]') | |
| is_present_list = request.form.getlist('is_present[]') | |
| remarks_list = request.form.getlist('remarks[]') | |
| # Find the maximum length among all lists to handle inconsistent lengths | |
| max_length = max(len(organizations), len(titles), len(start_months), | |
| len(start_years), len(end_months), len(end_years), | |
| len(remarks_list)) | |
| # Save each project | |
| for i in range(max_length): | |
| title = titles[i] if i < len(titles) else '' | |
| org = organizations[i] if i < len(organizations) else '' | |
| if title.strip(): # Only save if title is provided | |
| project = Project( | |
| user_id=current_user.id, | |
| organization=org.strip() if org else None, | |
| title=title.strip(), | |
| start_month=int(start_months[i]) if i < len(start_months) and start_months[i] else None, | |
| start_year=int(start_years[i]) if i < len(start_years) and start_years[i] else None, | |
| end_month=int(end_months[i]) if i < len(end_months) and end_months[i] and str(i) not in is_present_list else None, | |
| end_year=int(end_years[i]) if i < len(end_years) and end_years[i] and str(i) not in is_present_list else None, | |
| remarks=remarks_list[i].strip() if i < len(remarks_list) and remarks_list[i] else None, | |
| order=i | |
| ) | |
| db.session.add(project) | |
| try: | |
| db.session.commit() | |
| flash('Projects saved successfully.', 'success') | |
| return redirect(url_for('create_education')) | |
| except Exception as e: | |
| db.session.rollback() | |
| flash('An error occurred while saving projects.', 'error') | |
| # GET request - show form | |
| form_data = { | |
| 'projects': Project.query.filter_by(user_id=current_user.id).order_by(Project.order).all() | |
| } | |
| return render_template('create_projects.html', | |
| form_data=form_data, | |
| current_year=datetime.now().year) | |
| def create_education(): | |
| """Create education section""" | |
| if not current_user.introduction: | |
| flash('Please complete your introduction first.', 'error') | |
| return redirect(url_for('create_introduction')) | |
| if request.method == 'POST': | |
| # Clear existing education for this user | |
| Education.query.filter_by(user_id=current_user.id).delete() | |
| # Get form data | |
| organizations = request.form.getlist('organization[]') | |
| titles = request.form.getlist('title[]') | |
| start_months = request.form.getlist('start_month[]') | |
| start_years = request.form.getlist('start_year[]') | |
| end_months = request.form.getlist('end_month[]') | |
| end_years = request.form.getlist('end_year[]') | |
| is_present_list = request.form.getlist('is_present[]') | |
| remarks_list = request.form.getlist('remarks[]') | |
| # Find the maximum length among all lists to handle inconsistent lengths | |
| max_length = max(len(organizations), len(titles), len(start_months), | |
| len(start_years), len(end_months), len(end_years), | |
| len(remarks_list)) | |
| # Save each education | |
| for i in range(max_length): | |
| org = organizations[i] if i < len(organizations) else '' | |
| title = titles[i] if i < len(titles) else '' | |
| if org.strip(): # Only save if organization is provided | |
| education = Education( | |
| user_id=current_user.id, | |
| organization=org.strip(), | |
| title=title.strip(), | |
| start_month=int(start_months[i]) if i < len(start_months) and start_months[i] else None, | |
| start_year=int(start_years[i]) if i < len(start_years) and start_years[i] else None, | |
| end_month=int(end_months[i]) if i < len(end_months) and end_months[i] and str(i) not in is_present_list else None, | |
| end_year=int(end_years[i]) if i < len(end_years) and end_years[i] and str(i) not in is_present_list else None, | |
| remarks=remarks_list[i].strip() if i < len(remarks_list) and remarks_list[i] else None, | |
| order=i | |
| ) | |
| db.session.add(education) | |
| try: | |
| db.session.commit() | |
| flash('Education saved successfully.', 'success') | |
| return redirect(url_for('create_skills')) | |
| except Exception as e: | |
| db.session.rollback() | |
| flash('An error occurred while saving education.', 'error') | |
| # GET request - show form | |
| form_data = { | |
| 'educations': Education.query.filter_by(user_id=current_user.id).order_by(Education.order).all() | |
| } | |
| return render_template('create_education.html', | |
| form_data=form_data, | |
| current_year=datetime.now().year) | |
| def create_skills(): | |
| """Create skills section""" | |
| form_data = {} | |
| form_errors = {} | |
| if not current_user.introduction: | |
| flash('Please complete your introduction first.', 'error') | |
| return redirect(url_for('create_introduction')) | |
| if request.method == 'POST': | |
| # Get skills from form | |
| skills_text = request.form.get('skills', '').strip() | |
| # Store form data | |
| form_data = { | |
| 'skills': skills_text, | |
| 'skills_preview': [skill.strip() for skill in skills_text.split(',') if skill.strip()] if skills_text else [] | |
| } | |
| # Validation - skills are optional but if provided, they should be valid | |
| if skills_text and len(skills_text.split(',')) > 50: | |
| form_errors['skills'] = ['You can add up to 50 skills maximum'] | |
| if form_errors: | |
| return render_template('create_skills.html', | |
| form_data=form_data, | |
| form_errors=form_errors) | |
| # Clear existing skills for this user | |
| Skill.query.filter_by(user_id=current_user.id).delete() | |
| if skills_text: | |
| skills_list = [skill.strip() for skill in skills_text.split(',') if skill.strip()] | |
| # Save each skill | |
| for i, skill in enumerate(skills_list): | |
| new_skill = Skill( | |
| user_id=current_user.id, | |
| skill=skill, | |
| order=i | |
| ) | |
| db.session.add(new_skill) | |
| try: | |
| db.session.commit() | |
| flash('Skills saved successfully.', 'success') | |
| return redirect(url_for('create_achievements')) | |
| except Exception as e: | |
| db.session.rollback() | |
| flash('An error occurred while saving skills.', 'error') | |
| # GET request - show form | |
| existing_skills = Skill.query.filter_by(user_id=current_user.id).order_by(Skill.order).all() | |
| skills_text = ', '.join([skill.skill for skill in existing_skills]) | |
| form_data = { | |
| 'skills': skills_text, | |
| 'skills_preview': [skill.skill for skill in existing_skills] | |
| } | |
| return render_template('create_skills.html', | |
| form_data=form_data, | |
| form_errors=form_errors) | |
| def create_achievements(): | |
| """Create achievements section""" | |
| form_data = {} | |
| form_errors = {} | |
| if not current_user.introduction: | |
| flash('Please complete your introduction first.', 'error') | |
| return redirect(url_for('create_introduction')) | |
| if request.method == 'POST': | |
| # Get achievements from form | |
| achievements_text = request.form.get('achievements', '').strip() | |
| # Store form data | |
| form_data = { | |
| 'achievements': achievements_text, | |
| 'achievements_preview': [achievement.strip() for achievement in achievements_text.split(',') if achievement.strip()] if achievements_text else [] | |
| } | |
| # Validation - achievements are optional but if provided, they should be valid | |
| if achievements_text and len(achievements_text.split(',')) > 50: | |
| form_errors['achievements'] = ['You can add up to 50 achievements maximum'] | |
| if form_errors: | |
| return render_template('create_achievements.html', | |
| form_data=form_data, | |
| form_errors=form_errors) | |
| # Clear existing achievements for this user | |
| Achievement.query.filter_by(user_id=current_user.id).delete() | |
| if achievements_text: | |
| achievements_list = [achievement.strip() for achievement in achievements_text.split(',') if achievement.strip()] | |
| # Save each achievement | |
| for i, achievement in enumerate(achievements_list): | |
| new_achievement = Achievement( | |
| user_id=current_user.id, | |
| achievement=achievement, | |
| order=i | |
| ) | |
| db.session.add(new_achievement) | |
| try: | |
| db.session.commit() | |
| flash('Achievements saved successfully.', 'success') | |
| return redirect(url_for('create_preview')) | |
| except Exception as e: | |
| db.session.rollback() | |
| flash('An error occurred while saving achievements.', 'error') | |
| # GET request - show form | |
| existing_achievements = Achievement.query.filter_by(user_id=current_user.id).order_by(Achievement.order).all() | |
| achievements_text = ', '.join([achievement.achievement for achievement in existing_achievements]) | |
| form_data = { | |
| 'achievements': achievements_text, | |
| 'achievements_preview': [achievement.achievement for achievement in existing_achievements] | |
| } | |
| return render_template('create_achievements.html', | |
| form_data=form_data, | |
| form_errors=form_errors) | |
| def create_preview(): | |
| """Preview and finalize profile""" | |
| if not current_user.introduction: | |
| flash('Please complete your introduction first.', 'error') | |
| return redirect(url_for('create_introduction')) | |
| if request.method == 'POST': | |
| action = request.form.get('action') | |
| if action == 'submit': | |
| # Save section order | |
| section_order = request.form.get('section_order', '[]') | |
| try: | |
| order_data = json.loads(section_order) | |
| # Update or create section order | |
| profile_order = ProfileSectionOrder.query.filter_by(user_id=current_user.id).first() | |
| if not profile_order: | |
| profile_order = ProfileSectionOrder(user_id=current_user.id) | |
| profile_order.section_order = order_data | |
| db.session.add(profile_order) | |
| db.session.commit() | |
| flash('Profile created successfully!', 'success') | |
| return redirect(url_for('profile')) | |
| except Exception as e: | |
| db.session.rollback() | |
| flash('An error occurred while saving your profile.', 'error') | |
| elif action == 'clear': | |
| # Delete all profile data | |
| try: | |
| # Delete all profile sections | |
| if current_user.introduction: | |
| db.session.delete(current_user.introduction) | |
| if current_user.profile_summary: | |
| db.session.delete(current_user.profile_summary) | |
| if current_user.section_order: | |
| db.session.delete(current_user.section_order) | |
| # Delete collections | |
| WorkExperience.query.filter_by(user_id=current_user.id).delete() | |
| Project.query.filter_by(user_id=current_user.id).delete() | |
| Education.query.filter_by(user_id=current_user.id).delete() | |
| Skill.query.filter_by(user_id=current_user.id).delete() | |
| Achievement.query.filter_by(user_id=current_user.id).delete() | |
| db.session.commit() | |
| flash('Profile cleared successfully.', 'success') | |
| return redirect(url_for('profile')) | |
| except Exception as e: | |
| db.session.rollback() | |
| flash('An error occurred while clearing your profile.', 'error') | |
| # Get all profile data | |
| intro = current_user.introduction | |
| summary = current_user.profile_summary | |
| work_experiences = WorkExperience.query.filter_by(user_id=current_user.id).order_by(WorkExperience.order).all() | |
| projects = Project.query.filter_by(user_id=current_user.id).order_by(Project.order).all() | |
| educations = Education.query.filter_by(user_id=current_user.id).order_by(Education.order).all() | |
| skills = Skill.query.filter_by(user_id=current_user.id).order_by(Skill.order).all() | |
| achievements = Achievement.query.filter_by(user_id=current_user.id).order_by(Achievement.order).all() | |
| # Get default section order | |
| default_order = ['introduction', 'profile_summary', 'work_experience', 'projects', 'education', 'skills', 'achievements'] | |
| return render_template('create_preview.html', | |
| intro=intro, | |
| summary=summary, | |
| work_experiences=work_experiences, | |
| projects=projects, | |
| educations=educations, | |
| skills=skills, | |
| achievements=achievements, | |
| default_order=default_order) | |
| def ping(): | |
| return {"ok": True, "msg": "pong from app.py"} | |
| def create_tables(): | |
| """Create database tables""" | |
| with app.app_context(): | |
| db.create_all() | |
| print("Database tables created successfully!") | |
| # Resume Generation Routes | |
| def generate_resume(format_type): | |
| """Generate resume in specified format""" | |
| # Check if user has profile data | |
| intro = Introduction.query.filter_by(user_id=current_user.id).first() | |
| if not intro: | |
| flash('Please create your profile first.', 'error') | |
| return redirect(url_for('profile')) | |
| # Get all profile data | |
| summary = ProfileSummary.query.filter_by(user_id=current_user.id).first() | |
| work_experiences = WorkExperience.query.filter_by(user_id=current_user.id).order_by(WorkExperience.order).all() | |
| projects = Project.query.filter_by(user_id=current_user.id).order_by(Project.order).all() | |
| educations = Education.query.filter_by(user_id=current_user.id).order_by(Education.order).all() | |
| skills = Skill.query.filter_by(user_id=current_user.id).order_by(Skill.order).all() | |
| achievements = Achievement.query.filter_by(user_id=current_user.id).order_by(Achievement.order).all() | |
| # Get section order | |
| section_order_obj = ProfileSectionOrder.query.filter_by(user_id=current_user.id).first() | |
| section_order = section_order_obj.section_order if section_order_obj else [ | |
| 'introduction', 'profile_summary', 'work_experience', | |
| 'projects', 'education', 'skills', 'achievements' | |
| ] | |
| try: | |
| if format_type == 'word': | |
| return generate_word_resume( | |
| intro, summary, work_experiences, projects, | |
| educations, skills, achievements, section_order | |
| ) | |
| elif format_type == 'pdf-standard': | |
| return generate_pdf_resume( | |
| intro, summary, work_experiences, projects, | |
| educations, skills, achievements, section_order, 'standard' | |
| ) | |
| elif format_type == 'pdf-modern': | |
| return generate_pdf_resume( | |
| intro, summary, work_experiences, projects, | |
| educations, skills, achievements, section_order, 'modern' | |
| ) | |
| else: | |
| flash('Invalid resume format.', 'error') | |
| return redirect(url_for('profile')) | |
| except Exception as e: | |
| import traceback | |
| app.logger.error(f"Error generating resume: {str(e)}") | |
| app.logger.error(traceback.format_exc()) | |
| flash(f'An error occurred while generating your resume: {str(e)}', 'error') | |
| return redirect(url_for('profile')) | |
| def generate_word_resume(intro, summary, work_experiences, projects, educations, skills, achievements, section_order): | |
| """Generate Word document resume""" | |
| try: | |
| from docx import Document | |
| from docx.shared import Pt, Inches, RGBColor | |
| from docx.enum.text import WD_ALIGN_PARAGRAPH | |
| # Create document | |
| doc = Document() | |
| # Set default font | |
| style = doc.styles['Normal'] | |
| font = style.font | |
| font.name = 'Calibri' | |
| font.size = Pt(11) | |
| # Header section | |
| name_para = doc.add_paragraph() | |
| name_para.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
| name_run = name_para.add_run(intro.name.upper()) | |
| name_run.bold = True | |
| name_run.size = Pt(16) | |
| # Contact info | |
| contact_para = doc.add_paragraph() | |
| contact_para.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
| contact_para.add_run(f"Email: {intro.email} | Phone: {intro.phone}") | |
| if intro.linkedin: | |
| contact_para.add_run(" | LinkedIn") | |
| if intro.github: | |
| contact_para.add_run(" | GitHub") | |
| if intro.website: | |
| contact_para.add_run(" | Website") | |
| doc.add_paragraph() | |
| # Profile summary | |
| if summary: | |
| doc.add_heading('Professional Summary', level=1) | |
| summary_para = doc.add_paragraph(summary.summary) | |
| if summary.ai_generated: | |
| ai_para = doc.add_paragraph("AI Generated") | |
| ai_para.italic = True | |
| ai_para.runs[0].font.size = Pt(9) | |
| doc.add_paragraph() | |
| for section_name in section_order: | |
| if section_name == 'work_experience' and work_experiences: | |
| doc.add_heading('Work Experience', level=1) | |
| for exp in work_experiences: | |
| # Title and organization | |
| exp_para = doc.add_paragraph() | |
| exp_para.add_run(exp.title).bold = True | |
| exp_para.add_run(f" at {exp.organization}").italic = True | |
| # Date | |
| date_para = doc.add_paragraph() | |
| date_text = f"{exp.start_month}/{exp.start_year} - " | |
| if exp.end_year: | |
| date_text += f"{exp.end_month}/{exp.end_year}" | |
| else: | |
| date_text += "Present" | |
| date_para.add_run(date_text) | |
| # Remarks | |
| if exp.remarks: | |
| remarks_para = doc.add_paragraph(exp.remarks) | |
| doc.add_paragraph() | |
| elif section_name == 'projects' and projects: | |
| doc.add_heading('Projects', level=1) | |
| for project in projects: | |
| # Title and organization | |
| proj_para = doc.add_paragraph() | |
| proj_para.add_run(project.title).bold = True | |
| if project.organization: | |
| proj_para.add_run(f" at {project.organization}").italic = True | |
| # Date | |
| date_para = doc.add_paragraph() | |
| date_text = f"{project.start_month}/{project.start_year} - " | |
| if project.end_year: | |
| date_text += f"{project.end_month}/{project.end_year}" | |
| else: | |
| date_text += "Present" | |
| date_para.add_run(date_text) | |
| # Remarks | |
| if project.remarks: | |
| remarks_para = doc.add_paragraph(project.remarks) | |
| doc.add_paragraph() | |
| elif section_name == 'education' and educations: | |
| doc.add_heading('Education', level=1) | |
| for edu in educations: | |
| # Title and organization | |
| edu_para = doc.add_paragraph() | |
| edu_para.add_run(edu.title).bold = True | |
| edu_para.add_run(f" at {edu.organization}").italic = True | |
| # Date | |
| date_para = doc.add_paragraph() | |
| date_text = f"{edu.start_month}/{edu.start_year} - " | |
| if edu.end_year: | |
| date_text += f"{edu.end_month}/{edu.end_year}" | |
| else: | |
| date_text += "Present" | |
| date_para.add_run(date_text) | |
| # Remarks | |
| if edu.remarks: | |
| remarks_para = doc.add_paragraph(edu.remarks) | |
| doc.add_paragraph() | |
| elif section_name == 'skills' and skills: | |
| doc.add_heading('Skills', level=1) | |
| skills_text = ", ".join([skill.skill for skill in skills]) | |
| doc.add_paragraph(skills_text) | |
| doc.add_paragraph() | |
| elif section_name == 'achievements' and achievements: | |
| doc.add_heading('Achievements', level=1) | |
| for achievement in achievements: | |
| doc.add_paragraph(f"• {achievement.achievement}") | |
| doc.add_paragraph() | |
| # Save to bytes | |
| from io import BytesIO | |
| doc_buffer = BytesIO() | |
| doc.save(doc_buffer) | |
| doc_buffer.seek(0) | |
| # Return as downloadable file | |
| username = intro.name.replace(' ', '_') | |
| return send_file( | |
| doc_buffer, | |
| mimetype='application/vnd.openxmlformats-officedocument.wordprocessingml.document', | |
| as_attachment=True, | |
| download_name=f'{username}_resume.docx' | |
| ) | |
| except Exception as e: | |
| app.logger.error(f"Error generating Word resume: {str(e)}") | |
| raise | |
| def generate_pdf_resume(intro, summary, work_experiences, projects, educations, skills, achievements, section_order, template_type): | |
| """Generate PDF resume using ReportLab""" | |
| try: | |
| from pdf_generator import create_pdf_resume | |
| from io import BytesIO | |
| import calendar | |
| def format_date(month, year): | |
| """Format month and year as 'Month Year'""" | |
| if month and year: | |
| try: | |
| month_name = calendar.month_name[int(month)] | |
| return f"{month_name[:3]} {year}" | |
| except: | |
| return f"{month}/{year}" | |
| return "" | |
| # Prepare data for PDF generation | |
| work_exp_list = [] | |
| if work_experiences: | |
| for exp in work_experiences: | |
| start_date = format_date(exp.start_month, exp.start_year) | |
| end_date = "Present" if not exp.end_month or not exp.end_year else format_date(exp.end_month, exp.end_year) | |
| work_exp_list.append({ | |
| 'title': exp.title, | |
| 'organization': exp.organization, | |
| 'start_date': start_date, | |
| 'end_date': end_date, | |
| 'remarks': exp.remarks or '' | |
| }) | |
| projects_list = [] | |
| if projects: | |
| for proj in projects: | |
| start_date = format_date(proj.start_month, proj.start_year) | |
| end_date = "Present" if not proj.end_month or not proj.end_year else format_date(proj.end_month, proj.end_year) | |
| projects_list.append({ | |
| 'title': proj.title, | |
| 'organization': proj.organization, | |
| 'start_date': start_date, | |
| 'end_date': end_date, | |
| 'remarks': proj.remarks or '' | |
| }) | |
| education_list = [] | |
| if educations: | |
| for edu in educations: | |
| start_date = format_date(edu.start_month, edu.start_year) | |
| end_date = "Present" if not edu.end_month or not edu.end_year else format_date(edu.end_month, edu.end_year) | |
| education_list.append({ | |
| 'title': edu.title, | |
| 'organization': edu.organization, | |
| 'start_date': start_date, | |
| 'end_date': end_date, | |
| 'remarks': edu.remarks or '' | |
| }) | |
| # Convert skills and achievements to comma-separated strings | |
| skills_text = ', '.join([skill.skill for skill in skills]) if skills else '' | |
| achievements_text = ', '.join([achievement.achievement for achievement in achievements]) if achievements else '' | |
| # Create data dictionary | |
| data = { | |
| 'name': intro.name, | |
| 'email': intro.email, | |
| 'phone': intro.phone, | |
| 'linkedin': intro.linkedin, | |
| 'github': intro.github, | |
| 'website': intro.website, | |
| 'summary': summary.summary if summary else '', | |
| 'work_experience': work_exp_list, | |
| 'projects': projects_list, | |
| 'education': education_list, | |
| 'skills': skills_text, | |
| 'achievements': achievements_text, | |
| 'sections_order': section_order | |
| } | |
| # Generate PDF | |
| pdf_bytes = create_pdf_resume(data, template_type) | |
| if pdf_bytes: | |
| # Return as downloadable file | |
| username = intro.name.replace(' ', '_') | |
| filename = f'{username}_resume.pdf' if template_type == 'standard' else f'{username}_resume_modern.pdf' | |
| pdf_buffer = BytesIO(pdf_bytes) | |
| return send_file( | |
| pdf_buffer, | |
| mimetype='application/pdf', | |
| as_attachment=True, | |
| download_name=filename | |
| ) | |
| else: | |
| raise Exception("PDF generation failed") | |
| except Exception as e: | |
| app.logger.error(f"Error generating PDF resume: {str(e)}") | |
| raise | |
| # Admin Panel Route | |
| def admin_panel(): | |
| """Admin panel to manage users""" | |
| users = User.query.order_by(User.created_at.desc()).all() | |
| return render_template('admin.html', users=users) | |
| # Admin API Endpoints | |
| def api_get_users(): | |
| """Get list of all users""" | |
| try: | |
| users = User.query.order_by(User.created_at.desc()).all() | |
| users_data = [] | |
| for user in users: | |
| users_data.append({ | |
| 'id': str(user.id), | |
| 'email': user.email, | |
| 'name': user.name, | |
| 'created_at': user.created_at.isoformat() if user.created_at else None, | |
| 'is_admin': user.is_admin, | |
| 'role': user.role | |
| }) | |
| return jsonify({ | |
| 'success': True, | |
| 'users': users_data | |
| }) | |
| except Exception as e: | |
| app.logger.error(f"Error fetching users: {str(e)}") | |
| return jsonify({ | |
| 'success': False, | |
| 'error': str(e) | |
| }), 500 | |
| def api_delete_user(user_id): | |
| """Delete a user and all their profile data""" | |
| try: | |
| # Find the user and store email before deletion | |
| user = User.query.get(user_id) | |
| if not user: | |
| return jsonify({ | |
| 'success': False, | |
| 'error': 'User not found' | |
| }), 404 | |
| # Store user email for success message | |
| user_email = user.email | |
| # Don't allow deleting admin users | |
| if user.is_admin: | |
| return jsonify({ | |
| 'success': False, | |
| 'error': 'Cannot delete admin users' | |
| }), 400 | |
| # Don't allow deleting self | |
| if str(user.id) == str(current_user.id): | |
| return jsonify({ | |
| 'success': False, | |
| 'error': 'Cannot delete your own account' | |
| }), 400 | |
| # Use direct SQL to delete all profile data first | |
| # This ensures we handle any duplicate data that might exist | |
| # Delete from all profile tables | |
| from sqlalchemy import text | |
| # Delete introductions (handle potential duplicates) | |
| db.session.execute(text("DELETE FROM introductions WHERE user_id = :user_id"), {'user_id': str(user_id)}) | |
| # Delete profile summaries | |
| db.session.execute(text("DELETE FROM profile_summaries WHERE user_id = :user_id"), {'user_id': str(user_id)}) | |
| # Delete work experiences | |
| db.session.execute(text("DELETE FROM work_experiences WHERE user_id = :user_id"), {'user_id': str(user_id)}) | |
| # Delete projects | |
| db.session.execute(text("DELETE FROM projects WHERE user_id = :user_id"), {'user_id': str(user_id)}) | |
| # Delete education | |
| db.session.execute(text("DELETE FROM educations WHERE user_id = :user_id"), {'user_id': str(user_id)}) | |
| # Delete skills | |
| db.session.execute(text("DELETE FROM skills WHERE user_id = :user_id"), {'user_id': str(user_id)}) | |
| # Delete achievements | |
| db.session.execute(text("DELETE FROM achievements WHERE user_id = :user_id"), {'user_id': str(user_id)}) | |
| # Delete section order | |
| db.session.execute(text("DELETE FROM profile_section_orders WHERE user_id = :user_id"), {'user_id': str(user_id)}) | |
| # Finally delete the user | |
| db.session.execute(text("DELETE FROM users WHERE id = :user_id"), {'user_id': str(user_id)}) | |
| # Expunge the user object from session to avoid the deleted instance warning | |
| db.session.expunge(user) | |
| db.session.commit() | |
| return jsonify({ | |
| 'success': True, | |
| 'message': f'User {user_email} and all their profile data have been deleted successfully' | |
| }) | |
| except Exception as e: | |
| db.session.rollback() | |
| app.logger.error(f"Error deleting user: {str(e)}") | |
| return jsonify({ | |
| 'success': False, | |
| 'error': str(e) | |
| }), 500 | |
| def create_tables_if_needed(): | |
| """Create database tables if they don't exist""" | |
| with app.app_context(): | |
| # Check if tables exist | |
| inspector = db.inspect(db.engine) | |
| table_names = inspector.get_table_names() | |
| if not table_names or 'users' not in table_names: | |
| print("Creating database tables...") | |
| db.create_all() | |
| print("Database tables created successfully!") | |
| # Run admin migration if needed | |
| try: | |
| from sqlalchemy import text | |
| columns = [column['name'] for column in inspector.get_columns('users')] | |
| if 'is_admin' not in columns: | |
| print("Running admin migration...") | |
| db.session.execute(text('ALTER TABLE users ADD COLUMN is_admin BOOLEAN DEFAULT FALSE')) | |
| db.session.execute(text('ALTER TABLE users ADD COLUMN role VARCHAR(50) DEFAULT \'User\'')) | |
| db.session.commit() | |
| print("Admin migration completed!") | |
| except Exception as e: | |
| print(f"Migration error: {e}") | |
| # Initialize database only when running directly, not when imported | |
| if __name__ == "__main__": | |
| create_tables_if_needed() | |
| port = int(os.environ.get('PORT', 5000)) | |
| # Railway requires binding to 0.0.0.0 | |
| # Check if we're running on Railway | |
| is_railway = os.environ.get('RAILWAY_ENVIRONMENT') or os.environ.get('RAILWAY_ENVIRONMENT_NAME') | |
| host = '0.0.0.0' if is_railway else '127.0.0.1' | |
| print(f"Flask running at http://{host}:{port}") | |
| # Disable debug mode in production | |
| debug = not is_railway | |
| app.run(host=host, port=port, debug=debug) | |