resumebuilder / app.py
sakthi07's picture
modified Dockerfile readme.md and app.py
b3d3e31
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'
@login_manager.user_loader
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"""
@login_required
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
@app.route('/')
def index():
if current_user.is_authenticated:
return redirect(url_for('profile'))
return redirect(url_for('signin'))
@app.route('/signin', methods=['GET', 'POST'])
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')
@app.route('/signup', methods=['GET', 'POST'])
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')
@app.route('/logout')
@login_required
def logout():
logout_user()
flash('You have been logged out successfully.', 'success')
return redirect(url_for('signin'))
@app.route('/profile')
@login_required
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)
@app.route('/forgot-password')
def forgot_password():
return render_template('forgot_password.html')
# GitHub OAuth Routes
@app.route('/auth/github')
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)
@app.route('/api/auth/github/callback')
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
@app.route('/profile/create', methods=['GET', 'POST'])
@login_required
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')
@app.route('/profile/create/introduction', methods=['GET', 'POST'])
@login_required
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)
@app.route('/profile/create/profile-summary', methods=['GET', 'POST'])
@login_required
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)
@app.route('/profile/create/generate-summary', methods=['POST'])
@login_required
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
@app.route('/profile/create/work-experience', methods=['GET', 'POST'])
@login_required
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)
@app.route('/profile/create/projects', methods=['GET', 'POST'])
@login_required
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)
@app.route('/profile/create/education', methods=['GET', 'POST'])
@login_required
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)
@app.route('/profile/create/skills', methods=['GET', 'POST'])
@login_required
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)
@app.route('/profile/create/achievements', methods=['GET', 'POST'])
@login_required
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)
@app.route('/profile/create/preview', methods=['GET', 'POST'])
@login_required
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)
@app.route('/ping')
def ping():
return {"ok": True, "msg": "pong from app.py"}
@app.cli.command()
def create_tables():
"""Create database tables"""
with app.app_context():
db.create_all()
print("Database tables created successfully!")
# Resume Generation Routes
@app.route('/profile/generate-resume/<format_type>')
@login_required
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
@app.route('/admin')
@admin_required
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
@app.route('/api/admin/users', methods=['GET'])
@admin_required
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
@app.route('/api/admin/users/<user_id>', methods=['DELETE'])
@admin_required
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)