husseinelsaadi commited on
Commit
ce04e48
·
1 Parent(s): 27994de

Updated version

Browse files
app.py CHANGED
@@ -28,6 +28,9 @@ from backend.models.database import db, Job, Application, init_db
28
  from backend.models.user import User
29
  from backend.routes.auth import auth_bp, handle_resume_upload
30
  from backend.routes.interview_api import interview_api
 
 
 
31
  # Initialize Flask app
32
  app = Flask(
33
  __name__,
@@ -229,6 +232,138 @@ def interview_page(job_id):
229
  cv_data = json.loads(application.extracted_features)
230
  return render_template("interview.html", job=job, cv=cv_data)
231
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  if __name__ == '__main__':
233
  print("Starting Codingo application...")
234
  with app.app_context():
 
28
  from backend.models.user import User
29
  from backend.routes.auth import auth_bp, handle_resume_upload
30
  from backend.routes.interview_api import interview_api
31
+ # Import additional utilities
32
+ import re
33
+ import json
34
  # Initialize Flask app
35
  app = Flask(
36
  __name__,
 
232
  cv_data = json.loads(application.extracted_features)
233
  return render_template("interview.html", job=job, cv=cv_data)
234
 
235
+
236
+ # -----------------------------------------------------------------------------
237
+ # Recruiter job posting route
238
+ #
239
+ # Authenticated users with a recruiter or admin role can access this page to
240
+ # create new job listings. Posted jobs are associated with the current
241
+ # recruiter via the ``recruiter_id`` foreign key on the ``Job`` model.
242
+ @app.route('/post_job', methods=['GET', 'POST'])
243
+ @login_required
244
+ def post_job():
245
+ # Only allow recruiters and admins to post jobs
246
+ if current_user.role not in ('recruiter', 'admin'):
247
+ flash('You do not have permission to post jobs.', 'warning')
248
+ return redirect(url_for('jobs'))
249
+
250
+ if request.method == 'POST':
251
+ # Extract fields from the form
252
+ role_title = request.form.get('role', '').strip()
253
+ description = request.form.get('description', '').strip()
254
+ seniority = request.form.get('seniority', '').strip()
255
+ skills_input = request.form.get('skills', '').strip()
256
+ company = request.form.get('company', '').strip()
257
+
258
+ # Validate required fields
259
+ errors = []
260
+ if not role_title:
261
+ errors.append('Job title is required.')
262
+ if not description:
263
+ errors.append('Job description is required.')
264
+ if not seniority:
265
+ errors.append('Seniority level is required.')
266
+ if not skills_input:
267
+ errors.append('Skills are required.')
268
+ if not company:
269
+ errors.append('Company name is required.')
270
+
271
+ if errors:
272
+ for err in errors:
273
+ flash(err, 'danger')
274
+ return render_template('post_job.html')
275
+
276
+ # Normalise the skills input into a JSON encoded list. Users can
277
+ # separate entries with commas, semicolons or newlines.
278
+ skills_list = [s.strip() for s in re.split(r'[\n,;]+', skills_input) if s.strip()]
279
+ skills_json = json.dumps(skills_list)
280
+
281
+ # Create and persist the new job
282
+ new_job = Job(
283
+ role=role_title,
284
+ description=description,
285
+ seniority=seniority,
286
+ skills=skills_json,
287
+ company=company,
288
+ recruiter_id=current_user.id
289
+ )
290
+ db.session.add(new_job)
291
+ db.session.commit()
292
+
293
+ flash('Job posted successfully!', 'success')
294
+ return redirect(url_for('jobs'))
295
+
296
+ # GET request returns the form
297
+ return render_template('post_job.html')
298
+
299
+
300
+ # -----------------------------------------------------------------------------
301
+ # Recruiter dashboard route
302
+ #
303
+ # Displays a list of candidates who applied to jobs posted by the current
304
+ # recruiter. Candidates are sorted by a simple skill match score computed
305
+ # against the job requirements. A placeholder download button is provided
306
+ # for future PDF report functionality.
307
+ @app.route('/dashboard')
308
+ @login_required
309
+ def dashboard():
310
+ # Only recruiters and admins can view the dashboard
311
+ if current_user.role not in ('recruiter', 'admin'):
312
+ flash('You do not have permission to access the dashboard.', 'warning')
313
+ return redirect(url_for('index'))
314
+
315
+ # Fetch jobs posted by the current recruiter
316
+ posted_jobs = Job.query.filter_by(recruiter_id=current_user.id).all()
317
+ job_ids = [job.id for job in posted_jobs]
318
+
319
+ candidates_with_scores = []
320
+ if job_ids:
321
+ # Fetch applications associated with these job IDs
322
+ candidate_apps = Application.query.filter(Application.job_id.in_(job_ids)).all()
323
+
324
+ # Helper to compute a match score based on skills overlap
325
+ def compute_score(application):
326
+ try:
327
+ # Extract candidate skills from stored JSON
328
+ candidate_features = json.loads(application.extracted_features) if application.extracted_features else {}
329
+ candidate_skills = candidate_features.get('skills', [])
330
+ # Retrieve the job's required skills and parse from JSON
331
+ job_skills = json.loads(application.job.skills) if application.job and application.job.skills else []
332
+ if not job_skills:
333
+ return ('Medium', 2) # Default when job specifies no skills
334
+
335
+ # Compute case‑insensitive intersection
336
+ candidate_set = {s.lower() for s in candidate_skills}
337
+ job_set = {s.lower() for s in job_skills}
338
+ common = candidate_set & job_set
339
+ ratio = len(common) / len(job_set) if job_set else 0
340
+
341
+ # Map ratio to qualitative score
342
+ if ratio >= 0.75:
343
+ return ('Excellent', 4)
344
+ elif ratio >= 0.5:
345
+ return ('Good', 3)
346
+ elif ratio >= 0.25:
347
+ return ('Medium', 2)
348
+ else:
349
+ return ('Poor', 1)
350
+ except Exception:
351
+ return ('Medium', 2)
352
+
353
+ # Build a list of candidate applications with computed scores
354
+ for app_record in candidate_apps:
355
+ score_label, score_value = compute_score(app_record)
356
+ candidates_with_scores.append({
357
+ 'application': app_record,
358
+ 'score_label': score_label,
359
+ 'score_value': score_value
360
+ })
361
+
362
+ # Sort candidates from highest to lowest score
363
+ candidates_with_scores.sort(key=lambda item: item['score_value'], reverse=True)
364
+
365
+ return render_template('dashboard.html', candidates=candidates_with_scores)
366
+
367
  if __name__ == '__main__':
368
  print("Starting Codingo application...")
369
  with app.app_context():
backend/models/database.py CHANGED
@@ -75,37 +75,12 @@ def init_db(app):
75
  db.init_app(app)
76
  with app.app_context():
77
  db.create_all()
78
- # Add sample data...
79
-
80
-
81
- # Add sample data if jobs table is empty
82
- if Job.query.count() == 0:
83
- sample_jobs = [
84
- Job(
85
- role='Senior Python Developer',
86
- description='Experienced developer needed for backend systems.',
87
- seniority='Senior',
88
- skills=json.dumps(['Python', 'Flask', 'SQL', 'AWS']),
89
- company='TechCorp'
90
- ),
91
- Job(
92
- role='Data Scientist',
93
- description='ML model development and statistical analysis.',
94
- seniority='Mid',
95
- skills=json.dumps(['Python', 'scikit-learn', 'Pandas', 'Spark']),
96
- company='DataInsights'
97
- ),
98
- Job(
99
- role='Frontend Developer',
100
- description='Modern web frontend development with React.',
101
- seniority='Junior',
102
- skills=json.dumps(['HTML', 'CSS', 'JavaScript', 'React']),
103
- company='WebSolutions'
104
- ),
105
- ]
106
-
107
- for job in sample_jobs:
108
- db.session.add(job)
109
-
110
- db.session.commit()
111
- print("✅ Sample jobs added to database.")
 
75
  db.init_app(app)
76
  with app.app_context():
77
  db.create_all()
78
+ # Database tables are created on application start. We intentionally do not
79
+ # seed any sample data here. Previously a block of code inserted dummy
80
+ # job listings whenever the jobs table was empty. In production, jobs
81
+ # should only be added by authenticated recruiters via the job posting
82
+ # interface. Leaving the seeding logic in place would result in fake
83
+ # positions appearing every time the application starts, which is
84
+ # undesirable for a live recruitment platform. If your environment
85
+ # requires initial data for testing, insert it manually via the
86
+ # database or through the new recruiter job posting page.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/templates/apply.html CHANGED
@@ -40,8 +40,8 @@
40
  <!--
41
  Collect the candidate's skills, experience and education manually.
42
  These fields allow applicants to highlight their background even when resume
43
- parsing is disabled. Entries can be separated by commas or newlines; the
44
- backend will normalise them into lists.
45
  -->
46
  <div class="form-group">
47
  <label for="skills">Skills</label>
@@ -56,6 +56,22 @@
56
  <textarea name="education" id="education" class="form-control" rows="3" placeholder="e.g. B.Sc. in Computer Science, M.Sc. in Data Science" required></textarea>
57
  </div>
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  <div class="application-actions" style="margin-top: 2rem;">
60
  <button type="submit" class="btn btn-primary">Submit Application</button>
61
  </div>
 
40
  <!--
41
  Collect the candidate's skills, experience and education manually.
42
  These fields allow applicants to highlight their background even when resume
43
+ parsing is disabled. Entries can be separated by commas, semicolons or newlines;
44
+ the backend will normalise them into lists.
45
  -->
46
  <div class="form-group">
47
  <label for="skills">Skills</label>
 
56
  <textarea name="education" id="education" class="form-control" rows="3" placeholder="e.g. B.Sc. in Computer Science, M.Sc. in Data Science" required></textarea>
57
  </div>
58
 
59
+ <!-- Interview guidelines displayed directly above the submit button. These
60
+ instructions help applicants prepare for the AI interview. They are
61
+ intentionally placed within the form so that they appear close to
62
+ the submit action, ensuring visibility without disrupting the flow
63
+ of the application fields. -->
64
+ <div class="interview-guidelines" style="background-color: #f8f9fa; border-left: 4px solid var(--primary); padding: 1rem; margin-top: 1.5rem; border-radius: 6px;">
65
+ <h3 style="margin-top: 0; color: var(--primary); margin-bottom: 0.75rem;">Important Interview Guidelines</h3>
66
+ <ul style="margin-left: 1rem; padding-left: 1rem; list-style-type: disc; line-height: 1.5;">
67
+ <li>The interview can be taken only once, so please be prepared.</li>
68
+ <li>Make sure you are in a quiet environment with a stable internet connection.</li>
69
+ <li>This is not a final job interview, but it helps the company shortlist the most relevant candidates.</li>
70
+ <li>The interview is customized based on your CV and the job requirements.</li>
71
+ <li>It takes 10 to 15 minutes and includes both general and skill-based questions.</li>
72
+ </ul>
73
+ </div>
74
+
75
  <div class="application-actions" style="margin-top: 2rem;">
76
  <button type="submit" class="btn btn-primary">Submit Application</button>
77
  </div>
backend/templates/dashboard.html ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Recruiter Dashboard - Codingo{% endblock %}
4
+
5
+ {#
6
+ This dashboard lists all candidates who have applied to jobs posted by the
7
+ currently logged in recruiter. Candidates are sorted from highest to
8
+ lowest matching score. The score is calculated on the fly by comparing
9
+ the candidate's self‑reported skills to the job's required skills. A
10
+ placeholder button is provided for downloading a PDF report; the
11
+ functionality will be implemented in a future iteration.
12
+ #}
13
+
14
+ {% block content %}
15
+ <section class="content-section">
16
+ <ul class="breadcrumbs">
17
+ <li><a href="{{ url_for('index') }}">Home</a></li>
18
+ <li>Dashboard</li>
19
+ </ul>
20
+
21
+ <div class="section-title">
22
+ <h2>Interviewed Candidates</h2>
23
+ <p>Review candidates who have applied to your job postings</p>
24
+ </div>
25
+
26
+ {% if candidates %}
27
+ <div class="card">
28
+ <div class="card-body" style="overflow-x: auto;">
29
+ <table class="dashboard-table" style="width: 100%; border-collapse: collapse;">
30
+ <thead>
31
+ <tr style="background-color: var(--primary); color: white; text-align: left;">
32
+ <th style="padding: 0.75rem;">Name</th>
33
+ <th style="padding: 0.75rem;">Email</th>
34
+ <th style="padding: 0.75rem;">Job Applied</th>
35
+ <th style="padding: 0.75rem;">Interview Score</th>
36
+ <th style="padding: 0.75rem;">Action</th>
37
+ </tr>
38
+ </thead>
39
+ <tbody>
40
+ {% for item in candidates %}
41
+ <tr style="border-bottom: 1px solid #ddd;">
42
+ <td style="padding: 0.75rem;">{{ item.application.name }}</td>
43
+ <td style="padding: 0.75rem;">{{ item.application.email }}</td>
44
+ <td style="padding: 0.75rem;">{{ item.application.job.role }}</td>
45
+ <td style="padding: 0.75rem; font-weight: 600; color: var(--secondary);">{{ item.score_label }}</td>
46
+ <td style="padding: 0.75rem;">
47
+ <button class="btn btn-outline" disabled style="cursor: not-allowed;">Download Report (PDF)</button>
48
+ </td>
49
+ </tr>
50
+ {% endfor %}
51
+ </tbody>
52
+ </table>
53
+ </div>
54
+ </div>
55
+ {% else %}
56
+ <div class="card">
57
+ <div class="card-body">
58
+ <p>No candidate applications found for your job postings.</p>
59
+ </div>
60
+ </div>
61
+ {% endif %}
62
+ </section>
63
+
64
+ <style>
65
+ .dashboard-table th, .dashboard-table td {
66
+ text-align: left;
67
+ }
68
+
69
+ .dashboard-table th {
70
+ font-weight: 600;
71
+ }
72
+
73
+ .dashboard-table tr:nth-child(even) {
74
+ background-color: #f5f5f5;
75
+ }
76
+
77
+ .dashboard-table tr:hover {
78
+ background-color: #eef5ff;
79
+ }
80
+ </style>
81
+ {% endblock %}
backend/templates/post_job.html ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Post a New Job - Codingo{% endblock %}
4
+
5
+ {#
6
+ This template allows authenticated recruiters to create new job postings. It
7
+ follows the same card‑based design used throughout the platform to ensure
8
+ visual consistency. All fields are required, and any validation errors
9
+ will be flashed by the corresponding view function. Upon successful
10
+ submission the recruiter is redirected back to the jobs listing.
11
+ #}
12
+
13
+ {% block content %}
14
+ <section class="content-section">
15
+ <ul class="breadcrumbs">
16
+ <li><a href="{{ url_for('index') }}">Home</a></li>
17
+ <li><a href="{{ url_for('jobs') }}">Jobs</a></li>
18
+ <li>Post Job</li>
19
+ </ul>
20
+
21
+ <div class="card">
22
+ <div class="card-header">
23
+ <h2>Post a New Job</h2>
24
+ <p>Fill out the details below to create a new job listing.</p>
25
+ </div>
26
+ <div class="card-body">
27
+ <form method="POST">
28
+ <div class="form-group">
29
+ <label for="role">Job Title</label>
30
+ <input type="text" name="role" id="role" class="form-control" required placeholder="e.g. Senior Python Developer">
31
+ </div>
32
+ <div class="form-group">
33
+ <label for="company">Company</label>
34
+ <input type="text" name="company" id="company" class="form-control" required placeholder="e.g. TechCorp">
35
+ </div>
36
+ <div class="form-group">
37
+ <label for="seniority">Seniority</label>
38
+ <input type="text" name="seniority" id="seniority" class="form-control" required placeholder="e.g. Junior, Mid, Senior">
39
+ </div>
40
+ <div class="form-group">
41
+ <label for="skills">Required Skills</label>
42
+ <textarea name="skills" id="skills" class="form-control" rows="3" required placeholder="e.g. Python, Flask, SQL"></textarea>
43
+ </div>
44
+ <div class="form-group">
45
+ <label for="description">Job Description</label>
46
+ <textarea name="description" id="description" class="form-control" rows="5" required placeholder="Describe the responsibilities and requirements for this position"></textarea>
47
+ </div>
48
+ <div class="application-actions" style="margin-top: 2rem; text-align: center;">
49
+ <button type="submit" class="btn btn-primary">Post Job</button>
50
+ </div>
51
+ </form>
52
+ </div>
53
+ </div>
54
+ </section>
55
+
56
+ <style>
57
+ /* Reuse form styling from other pages for consistency */
58
+ .form-group label {
59
+ font-weight: 600;
60
+ color: var(--primary);
61
+ margin-bottom: 0.5rem;
62
+ display: block;
63
+ }
64
+
65
+ .form-control {
66
+ width: 100%;
67
+ padding: 0.75rem;
68
+ font-size: 1rem;
69
+ border-radius: 6px;
70
+ border: 1px solid #ccc;
71
+ }
72
+
73
+ .application-actions {
74
+ text-align: center;
75
+ }
76
+
77
+ .btn-primary {
78
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
79
+ color: white;
80
+ padding: 0.75rem 1.5rem;
81
+ font-weight: 500;
82
+ border: none;
83
+ border-radius: 6px;
84
+ cursor: pointer;
85
+ }
86
+
87
+ .btn-primary:hover {
88
+ opacity: 0.9;
89
+ }
90
+ </style>
91
+ {% endblock %}