Spaces:
Paused
Paused
Commit
·
3c4bd31
1
Parent(s):
9a7d4db
updated
Browse files- app.py +18 -0
- backend/models/database.py +21 -0
- backend/routes/auth.py +7 -0
- backend/templates/apply.html +4 -4
- backend/templates/base.html +10 -4
- backend/templates/job_detail.html +14 -6
- backend/templates/jobs.html +6 -2
- backend/templates/my_applications.html +91 -0
app.py
CHANGED
@@ -41,6 +41,24 @@ app = Flask(
|
|
41 |
|
42 |
app.config['SECRET_KEY'] = 'your-secret-key'
|
43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
# Configure the database connection
|
45 |
# Use /tmp directory for database in Hugging Face Spaces
|
46 |
# Note: Data will be lost when the space restarts
|
|
|
41 |
|
42 |
app.config['SECRET_KEY'] = 'your-secret-key'
|
43 |
|
44 |
+
# -----------------------------------------------------------------------------
|
45 |
+
# Cookie configuration for Hugging Face Spaces
|
46 |
+
#
|
47 |
+
# When running this app inside an iframe (as is typical on Hugging Face Spaces),
|
48 |
+
# browsers will drop cookies that have the default SameSite policy of ``Lax``.
|
49 |
+
# This prevents the Flask session cookie from being stored and means that
|
50 |
+
# ``login_user()`` will appear to have no effect – the user will be redirected
|
51 |
+
# back to the home page but remain anonymous. By explicitly setting the
|
52 |
+
# SameSite policy to ``None`` and enabling the ``Secure`` flag, we allow the
|
53 |
+
# session and remember cookies to be sent even when the app is embedded in an
|
54 |
+
# iframe. Without these settings the sign‑up and login flows work locally
|
55 |
+
# but silently fail in Spaces, causing the "redirect to home page without
|
56 |
+
# anything" behaviour reported by users.
|
57 |
+
app.config['SESSION_COOKIE_SAMESITE'] = 'None'
|
58 |
+
app.config['SESSION_COOKIE_SECURE'] = True
|
59 |
+
app.config['REMEMBER_COOKIE_SAMESITE'] = 'None'
|
60 |
+
app.config['REMEMBER_COOKIE_SECURE'] = True
|
61 |
+
|
62 |
# Configure the database connection
|
63 |
# Use /tmp directory for database in Hugging Face Spaces
|
64 |
# Note: Data will be lost when the space restarts
|
backend/models/database.py
CHANGED
@@ -21,6 +21,21 @@ class Job(db.Model):
|
|
21 |
recruiter_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
|
22 |
recruiter = db.relationship('User', backref='posted_jobs')
|
23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
def __repr__(self):
|
25 |
return f"<Job {self.role} at {self.company}>"
|
26 |
|
@@ -41,6 +56,12 @@ class Application(db.Model):
|
|
41 |
|
42 |
user = db.relationship('User', backref='applications')
|
43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
def __repr__(self):
|
45 |
return f"Application('{self.name}', '{self.email}', Job ID: {self.job_id})"
|
46 |
|
|
|
21 |
recruiter_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
|
22 |
recruiter = db.relationship('User', backref='posted_jobs')
|
23 |
|
24 |
+
@property
|
25 |
+
def skills_list(self):
|
26 |
+
"""Return a list of skills parsed from the JSON string stored in ``skills``.
|
27 |
+
|
28 |
+
The ``skills`` column stores a JSON encoded list of skills (e.g. '["Python", "Flask"]').
|
29 |
+
In templates it is convenient to work with a Python list so that skills can be joined
|
30 |
+
or iterated over. If parsing fails for any reason an empty list is returned.
|
31 |
+
"""
|
32 |
+
try:
|
33 |
+
# Import json lazily to avoid circular imports at module import time.
|
34 |
+
import json as _json
|
35 |
+
return _json.loads(self.skills) if self.skills else []
|
36 |
+
except Exception:
|
37 |
+
return []
|
38 |
+
|
39 |
def __repr__(self):
|
40 |
return f"<Job {self.role} at {self.company}>"
|
41 |
|
|
|
56 |
|
57 |
user = db.relationship('User', backref='applications')
|
58 |
|
59 |
+
# Set up a relationship back to the Job so that templates can access
|
60 |
+
# ``application.job`` directly. Without this relationship you'd need to
|
61 |
+
# query the Job model manually in the route or template, which is less
|
62 |
+
# convenient and can lead to additional database queries.
|
63 |
+
job = db.relationship('Job', backref='applications', lazy='joined')
|
64 |
+
|
65 |
def __repr__(self):
|
66 |
return f"Application('{self.name}', '{self.email}', Job ID: {self.job_id})"
|
67 |
|
backend/routes/auth.py
CHANGED
@@ -40,10 +40,17 @@ def handle_resume_upload(file):
|
|
40 |
@auth_bp.route('/login', methods=['GET', 'POST'])
|
41 |
def login():
|
42 |
form = LoginForm()
|
|
|
|
|
|
|
|
|
|
|
43 |
if form.validate_on_submit():
|
44 |
user = User.query.filter_by(email=form.email.data).first()
|
45 |
if user and user.check_password(form.password.data):
|
46 |
login_user(user)
|
|
|
|
|
47 |
return redirect(url_for('index'))
|
48 |
else:
|
49 |
flash('Invalid credentials', 'danger')
|
|
|
40 |
@auth_bp.route('/login', methods=['GET', 'POST'])
|
41 |
def login():
|
42 |
form = LoginForm()
|
43 |
+
# When the form is submitted and passes validation, attempt to authenticate
|
44 |
+
# the user. If the email/password pair is valid, log them in and provide
|
45 |
+
# a success flash message so the user has feedback. Without this message
|
46 |
+
# the application silently redirects to the home page, which can be
|
47 |
+
# confusing. If authentication fails, flash an error message.
|
48 |
if form.validate_on_submit():
|
49 |
user = User.query.filter_by(email=form.email.data).first()
|
50 |
if user and user.check_password(form.password.data):
|
51 |
login_user(user)
|
52 |
+
# ✅ Provide a success message so users know the login worked
|
53 |
+
flash('You have been logged in successfully!', 'success')
|
54 |
return redirect(url_for('index'))
|
55 |
else:
|
56 |
flash('Invalid credentials', 'danger')
|
backend/templates/apply.html
CHANGED
@@ -1,13 +1,13 @@
|
|
1 |
{% extends "base.html" %}
|
2 |
|
3 |
-
{% block title %}Apply for {{ job.
|
4 |
|
5 |
{% block hero %}
|
6 |
<section class="hero" style="padding: 3rem 1rem;">
|
7 |
<div class="container">
|
8 |
<div class="hero-content">
|
9 |
-
<h1>Apply for {{ job.
|
10 |
-
<p>{{ job.company }} • {{ job.
|
11 |
</div>
|
12 |
</div>
|
13 |
</section>
|
@@ -18,7 +18,7 @@
|
|
18 |
<ul class="breadcrumbs">
|
19 |
<li><a href="{{ url_for('index') }}">Home</a></li>
|
20 |
<li><a href="{{ url_for('jobs') }}">Jobs</a></li>
|
21 |
-
<li><a href="{{ url_for('job_detail', job_id=job.id) }}">{{ job.
|
22 |
<li>Apply</li>
|
23 |
</ul>
|
24 |
|
|
|
1 |
{% extends "base.html" %}
|
2 |
|
3 |
+
{% block title %}Apply for {{ job.role }} - Codingo{% endblock %}
|
4 |
|
5 |
{% block hero %}
|
6 |
<section class="hero" style="padding: 3rem 1rem;">
|
7 |
<div class="container">
|
8 |
<div class="hero-content">
|
9 |
+
<h1>Apply for {{ job.role }}</h1>
|
10 |
+
<p>{{ job.company }}{% if job.seniority %} • {{ job.seniority }}{% endif %}</p>
|
11 |
</div>
|
12 |
</div>
|
13 |
</section>
|
|
|
18 |
<ul class="breadcrumbs">
|
19 |
<li><a href="{{ url_for('index') }}">Home</a></li>
|
20 |
<li><a href="{{ url_for('jobs') }}">Jobs</a></li>
|
21 |
+
<li><a href="{{ url_for('job_detail', job_id=job.id) }}">{{ job.role }}</a></li>
|
22 |
<li>Apply</li>
|
23 |
</ul>
|
24 |
|
backend/templates/base.html
CHANGED
@@ -735,11 +735,17 @@
|
|
735 |
</a>
|
736 |
<div class="login-buttons">
|
737 |
{% if current_user.is_authenticated %}
|
738 |
-
|
739 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
740 |
{% else %}
|
741 |
-
|
742 |
-
|
743 |
{% endif %}
|
744 |
</div>
|
745 |
|
|
|
735 |
</a>
|
736 |
<div class="login-buttons">
|
737 |
{% if current_user.is_authenticated %}
|
738 |
+
<!-- Show a link to the user's dashboard based on their role -->
|
739 |
+
{% if current_user.role == 'unemployed' %}
|
740 |
+
<a href="{{ url_for('my_applications') }}" class="btn btn-outline">My Applications</a>
|
741 |
+
{% elif current_user.role == 'recruiter' %}
|
742 |
+
<a href="{{ url_for('jobs') }}" class="btn btn-outline">Browse Candidates</a>
|
743 |
+
{% endif %}
|
744 |
+
<span class="welcome-message">Welcome, {{ current_user.username }}</span>
|
745 |
+
<a href="{{ url_for('auth.logout') }}" class="btn btn-logout">Logout</a>
|
746 |
{% else %}
|
747 |
+
<a href="{{ url_for('auth.login') }}" class="btn btn-outline">Log In</a>
|
748 |
+
<a href="{{ url_for('auth.signup') }}" class="btn btn-primary">Sign Up</a>
|
749 |
{% endif %}
|
750 |
</div>
|
751 |
|
backend/templates/job_detail.html
CHANGED
@@ -1,21 +1,21 @@
|
|
1 |
{% extends "base.html" %}
|
2 |
|
3 |
-
{% block title %}{{ job.
|
4 |
|
5 |
{% block content %}
|
6 |
<section class="content-section">
|
7 |
<ul class="breadcrumbs">
|
8 |
<li><a href="{{ url_for('index') }}">Home</a></li>
|
9 |
<li><a href="{{ url_for('jobs') }}">Jobs</a></li>
|
10 |
-
<li>{{ job.
|
11 |
</ul>
|
12 |
|
13 |
<div class="card">
|
14 |
<div class="card-header">
|
15 |
-
<h2>{{ job.
|
16 |
<div style="display: flex; justify-content: space-between; margin-top: 0.5rem;">
|
17 |
<span>{{ job.company }}</span>
|
18 |
-
<span>{{ job.
|
19 |
</div>
|
20 |
</div>
|
21 |
<div class="card-body">
|
@@ -25,8 +25,16 @@
|
|
25 |
</div>
|
26 |
|
27 |
<div style="margin-bottom: 2rem;">
|
28 |
-
<h3 style="color: var(--primary); margin-bottom: 1rem;">
|
29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
30 |
</div>
|
31 |
|
32 |
<div style="text-align: center; margin-top: 2rem;">
|
|
|
1 |
{% extends "base.html" %}
|
2 |
|
3 |
+
{% block title %}{{ job.role }} - Codingo{% endblock %}
|
4 |
|
5 |
{% block content %}
|
6 |
<section class="content-section">
|
7 |
<ul class="breadcrumbs">
|
8 |
<li><a href="{{ url_for('index') }}">Home</a></li>
|
9 |
<li><a href="{{ url_for('jobs') }}">Jobs</a></li>
|
10 |
+
<li>{{ job.role }}</li>
|
11 |
</ul>
|
12 |
|
13 |
<div class="card">
|
14 |
<div class="card-header">
|
15 |
+
<h2>{{ job.role }}</h2>
|
16 |
<div style="display: flex; justify-content: space-between; margin-top: 0.5rem;">
|
17 |
<span>{{ job.company }}</span>
|
18 |
+
<span>{{ job.seniority }}</span>
|
19 |
</div>
|
20 |
</div>
|
21 |
<div class="card-body">
|
|
|
25 |
</div>
|
26 |
|
27 |
<div style="margin-bottom: 2rem;">
|
28 |
+
<h3 style="color: var(--primary); margin-bottom: 1rem;">Required Skills</h3>
|
29 |
+
<!-- Display the skills as a comma separated list. The Job model exposes
|
30 |
+
a ``skills_list`` property to parse the JSON stored in the database. -->
|
31 |
+
<p>
|
32 |
+
{% if job.skills_list %}
|
33 |
+
{{ job.skills_list | join(', ') }}
|
34 |
+
{% else %}
|
35 |
+
Not specified
|
36 |
+
{% endif %}
|
37 |
+
</p>
|
38 |
</div>
|
39 |
|
40 |
<div style="text-align: center; margin-top: 2rem;">
|
backend/templates/jobs.html
CHANGED
@@ -30,10 +30,14 @@
|
|
30 |
{% for job in jobs %}
|
31 |
<div class="job-card">
|
32 |
<div class="job-header">
|
33 |
-
|
|
|
|
|
|
|
|
|
34 |
<div class="job-info">
|
35 |
<span>{{ job.company }}</span>
|
36 |
-
<span>{{ job.
|
37 |
</div>
|
38 |
</div>
|
39 |
<div class="job-body">
|
|
|
30 |
{% for job in jobs %}
|
31 |
<div class="job-card">
|
32 |
<div class="job-header">
|
33 |
+
<!-- Use the Job model's fields instead of undefined 'title' and 'location'.
|
34 |
+
Each job has a 'role' (job title), 'company' and 'seniority' (e.g. Junior/Mid/Senior).
|
35 |
+
The previous template referenced 'job.title' and 'job.location' which do not exist on
|
36 |
+
our SQLAlchemy model and caused rendering errors. -->
|
37 |
+
<h3>{{ job.role }}</h3>
|
38 |
<div class="job-info">
|
39 |
<span>{{ job.company }}</span>
|
40 |
+
<span>{{ job.seniority }}</span>
|
41 |
</div>
|
42 |
</div>
|
43 |
<div class="job-body">
|
backend/templates/my_applications.html
ADDED
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends "base.html" %}
|
2 |
+
|
3 |
+
{% block title %}My Applications - Codingo{% endblock %}
|
4 |
+
|
5 |
+
{% block content %}
|
6 |
+
<section class="content-section">
|
7 |
+
<div class="section-title">
|
8 |
+
<h2>My Applications</h2>
|
9 |
+
<p>Your submitted job applications are listed below.</p>
|
10 |
+
</div>
|
11 |
+
|
12 |
+
<ul class="breadcrumbs">
|
13 |
+
<li><a href="{{ url_for('index') }}">Home</a></li>
|
14 |
+
<li>My Applications</li>
|
15 |
+
</ul>
|
16 |
+
|
17 |
+
<div class="application-list">
|
18 |
+
{% if applications %}
|
19 |
+
{% for application in applications %}
|
20 |
+
<div class="application-card">
|
21 |
+
<div class="application-header">
|
22 |
+
<h3>{{ application.job.role if application.job else 'Unknown Role' }}</h3>
|
23 |
+
<div class="application-info">
|
24 |
+
<span>{{ application.job.company if application.job else '' }}</span>
|
25 |
+
<span>Status: {{ application.status }}</span>
|
26 |
+
</div>
|
27 |
+
</div>
|
28 |
+
<div class="application-body">
|
29 |
+
<p>Applied on {{ application.date_applied.strftime('%B %d, %Y') }}</p>
|
30 |
+
{% if application.job %}
|
31 |
+
<p>{{ application.job.description[:150] }}{% if application.job.description|length > 150 %}...{% endif %}</p>
|
32 |
+
{% endif %}
|
33 |
+
</div>
|
34 |
+
<div class="application-footer">
|
35 |
+
{% if application.job %}
|
36 |
+
<a href="{{ url_for('job_detail', job_id=application.job.id) }}" class="btn btn-outline">View Job</a>
|
37 |
+
{% endif %}
|
38 |
+
{% if application.extracted_features %}
|
39 |
+
<!-- Offer to take the interview if CV data exists -->
|
40 |
+
<a href="{{ url_for('interview_page', job_id=application.job.id) }}" class="btn btn-primary">Take Interview</a>
|
41 |
+
{% endif %}
|
42 |
+
</div>
|
43 |
+
</div>
|
44 |
+
{% endfor %}
|
45 |
+
{% else %}
|
46 |
+
<div class="card">
|
47 |
+
<div class="card-body">
|
48 |
+
<p>You haven't applied to any jobs yet. Browse available positions on the <a href="{{ url_for('jobs') }}">jobs page</a>.</p>
|
49 |
+
</div>
|
50 |
+
</div>
|
51 |
+
{% endif %}
|
52 |
+
</div>
|
53 |
+
</section>
|
54 |
+
|
55 |
+
<style>
|
56 |
+
.application-list {
|
57 |
+
display: grid;
|
58 |
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
59 |
+
gap: 1.5rem;
|
60 |
+
}
|
61 |
+
|
62 |
+
.application-card {
|
63 |
+
background-color: var(--light);
|
64 |
+
border: 1px solid #eee;
|
65 |
+
border-radius: 8px;
|
66 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
67 |
+
display: flex;
|
68 |
+
flex-direction: column;
|
69 |
+
justify-content: space-between;
|
70 |
+
padding: 1rem;
|
71 |
+
}
|
72 |
+
|
73 |
+
.application-header h3 {
|
74 |
+
margin-bottom: 0.5rem;
|
75 |
+
color: var(--primary);
|
76 |
+
}
|
77 |
+
|
78 |
+
.application-info span {
|
79 |
+
margin-right: 1rem;
|
80 |
+
color: var(--dark);
|
81 |
+
font-weight: 500;
|
82 |
+
}
|
83 |
+
|
84 |
+
.application-footer {
|
85 |
+
margin-top: 1rem;
|
86 |
+
display: flex;
|
87 |
+
justify-content: flex-end;
|
88 |
+
gap: 0.5rem;
|
89 |
+
}
|
90 |
+
</style>
|
91 |
+
{% endblock %}
|