Kaadan commited on
Commit
5ae03a4
·
1 Parent(s): 92cfefe

adding score to DB

Browse files
backend/alembic/versions/290ee4ce077e_add_score_and_question_scores_columns_.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Add score and question_scores columns to applications table
2
+
3
+ Revision ID: 290ee4ce077e
4
+ Revises: f9f1aa7380ab
5
+ Create Date: 2026-02-09 11:29:38.655897
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = '290ee4ce077e'
16
+ down_revision: Union[str, Sequence[str], None] = 'f9f1aa7380ab'
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade schema."""
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ op.add_column('applications', sa.Column('score', sa.Float(), nullable=True))
25
+ op.add_column('applications', sa.Column('question_scores', sa.Text(), nullable=True))
26
+ # ### end Alembic commands ###
27
+
28
+
29
+ def downgrade() -> None:
30
+ """Downgrade schema."""
31
+ # ### commands auto generated by Alembic - please adjust! ###
32
+ op.drop_column('applications', 'question_scores')
33
+ op.drop_column('applications', 'score')
34
+ # ### end Alembic commands ###
backend/api/application_routes.py CHANGED
@@ -53,6 +53,15 @@ def get_applications_list(jid: str, aid: str, page: int = 1, limit: int = 10, db
53
  from services.user_service import get_user
54
  user = get_user(db, application.user_id)
55
 
 
 
 
 
 
 
 
 
 
56
  # Create response object that matches technical requirements exactly
57
  application_response = {
58
  'id': application.id,
@@ -62,6 +71,7 @@ def get_applications_list(jid: str, aid: str, page: int = 1, limit: int = 10, db
62
  'answers': [], # Not including answers in the list view for performance
63
  'score': score,
64
  'passing_score': assessment.passing_score,
 
65
  'assessment_details': {
66
  'id': assessment.id,
67
  'title': assessment.title,
@@ -202,6 +212,15 @@ def get_application_detail(jid: str, aid: str, id: str, db: Session = Depends(ge
202
  logger.error(f"Error creating assessment details: {str(e)}")
203
  assessment_details_obj = None
204
 
 
 
 
 
 
 
 
 
 
205
  application_detail = ApplicationDetailedResponse(
206
  id=application.id,
207
  job_id=application.job_id,
@@ -210,6 +229,7 @@ def get_application_detail(jid: str, aid: str, id: str, db: Session = Depends(ge
210
  answers=enriched_answers,
211
  score=score,
212
  passing_score=assessment.passing_score,
 
213
  assessment_details=assessment_details_obj,
214
  user={
215
  'id': user.id if user else None,
@@ -416,6 +436,15 @@ def get_my_application(id: str, db: Session = Depends(get_db), current_user: Use
416
  logger.error(f"Error creating assessment details: {str(e)}")
417
  assessment_details_obj = None
418
 
 
 
 
 
 
 
 
 
 
419
  application_detail = ApplicationDetailedResponse(
420
  id=application.id,
421
  job_id=application.job_id,
@@ -424,6 +453,7 @@ def get_my_application(id: str, db: Session = Depends(get_db), current_user: Use
424
  answers=enriched_answers,
425
  score=score,
426
  passing_score=assessment.passing_score,
 
427
  assessment_details=assessment_details_obj,
428
  user={
429
  'id': user.id if user else None,
 
53
  from services.user_service import get_user
54
  user = get_user(db, application.user_id)
55
 
56
+ # Parse question scores from JSON string
57
+ question_scores = []
58
+ if application.question_scores:
59
+ try:
60
+ question_scores = json.loads(application.question_scores)
61
+ except json.JSONDecodeError:
62
+ logger.warning(f"Failed to parse question scores for application ID: {application.id}")
63
+ question_scores = []
64
+
65
  # Create response object that matches technical requirements exactly
66
  application_response = {
67
  'id': application.id,
 
71
  'answers': [], # Not including answers in the list view for performance
72
  'score': score,
73
  'passing_score': assessment.passing_score,
74
+ 'question_scores': question_scores, # Include individual question scores
75
  'assessment_details': {
76
  'id': assessment.id,
77
  'title': assessment.title,
 
212
  logger.error(f"Error creating assessment details: {str(e)}")
213
  assessment_details_obj = None
214
 
215
+ # Parse question scores from JSON string
216
+ question_scores = []
217
+ if application.question_scores:
218
+ try:
219
+ question_scores = json.loads(application.question_scores)
220
+ except json.JSONDecodeError:
221
+ logger.warning(f"Failed to parse question scores for application ID: {application.id}")
222
+ question_scores = []
223
+
224
  application_detail = ApplicationDetailedResponse(
225
  id=application.id,
226
  job_id=application.job_id,
 
229
  answers=enriched_answers,
230
  score=score,
231
  passing_score=assessment.passing_score,
232
+ question_scores=question_scores, # Include individual question scores
233
  assessment_details=assessment_details_obj,
234
  user={
235
  'id': user.id if user else None,
 
436
  logger.error(f"Error creating assessment details: {str(e)}")
437
  assessment_details_obj = None
438
 
439
+ # Parse question scores from JSON string
440
+ question_scores = []
441
+ if application.question_scores:
442
+ try:
443
+ question_scores = json.loads(application.question_scores)
444
+ except json.JSONDecodeError:
445
+ logger.warning(f"Failed to parse question scores for application ID: {application.id}")
446
+ question_scores = []
447
+
448
  application_detail = ApplicationDetailedResponse(
449
  id=application.id,
450
  job_id=application.job_id,
 
453
  answers=enriched_answers,
454
  score=score,
455
  passing_score=assessment.passing_score,
456
+ question_scores=question_scores, # Include individual question scores
457
  assessment_details=assessment_details_obj,
458
  user={
459
  'id': user.id if user else None,
backend/models/application.py CHANGED
@@ -1,4 +1,4 @@
1
- from sqlalchemy import Column, String, Text, ForeignKey, DateTime
2
  from sqlalchemy.sql import func
3
  from .base import Base
4
  import uuid
@@ -11,5 +11,7 @@ class Application(Base):
11
  assessment_id = Column(String, ForeignKey("assessments.id"), nullable=False)
12
  user_id = Column(String, ForeignKey("users.id"), nullable=False)
13
  answers = Column(Text) # Stored as JSON string
 
 
14
  created_at = Column(DateTime(timezone=True), server_default=func.now())
15
  updated_at = Column(DateTime(timezone=True), onupdate=func.now())
 
1
+ from sqlalchemy import Column, String, Text, ForeignKey, DateTime, Float
2
  from sqlalchemy.sql import func
3
  from .base import Base
4
  import uuid
 
11
  assessment_id = Column(String, ForeignKey("assessments.id"), nullable=False)
12
  user_id = Column(String, ForeignKey("users.id"), nullable=False)
13
  answers = Column(Text) # Stored as JSON string
14
+ score = Column(Float) # Overall application score
15
+ question_scores = Column(Text) # Individual question scores stored as JSON string
16
  created_at = Column(DateTime(timezone=True), server_default=func.now())
17
  updated_at = Column(DateTime(timezone=True), onupdate=func.now())
backend/schemas/application.py CHANGED
@@ -23,6 +23,11 @@ class ApplicationQuestion(BaseModel):
23
  options: Optional[List[dict]] = [] # Using dict for simplicity
24
  correct_options: Optional[List[str]] = []
25
 
 
 
 
 
 
26
  class ApplicationAnswerWithQuestion(ApplicationAnswer):
27
  question_text: str = Field(..., min_length=1, max_length=1000)
28
  weight: int = Field(..., ge=1, le=5) # range 1-5
@@ -31,6 +36,7 @@ class ApplicationAnswerWithQuestion(ApplicationAnswer):
31
  question_options: Optional[List[dict]] = [] # Options for the question
32
  correct_options: Optional[List[str]] = []
33
  rationale: str = Field(..., min_length=1, max_length=1000)
 
34
 
35
  class ApplicationBase(BaseSchema):
36
  job_id: str = Field(..., min_length=1)
@@ -52,9 +58,10 @@ class ApplicationAssessment(BaseModel):
52
 
53
  class ApplicationResponse(ApplicationBase):
54
  id: str
55
- score: Optional[float] = None
56
  passing_score: Optional[float] = None
57
  assessment_details: Optional[ApplicationAssessment] = None
 
58
 
59
  class Config:
60
  from_attributes = True
 
23
  options: Optional[List[dict]] = [] # Using dict for simplicity
24
  correct_options: Optional[List[str]] = []
25
 
26
+ class ApplicationAnswerScore(BaseModel):
27
+ question_id: str
28
+ score: float # Score between 0 and 1
29
+ rationale: str
30
+
31
  class ApplicationAnswerWithQuestion(ApplicationAnswer):
32
  question_text: str = Field(..., min_length=1, max_length=1000)
33
  weight: int = Field(..., ge=1, le=5) # range 1-5
 
36
  question_options: Optional[List[dict]] = [] # Options for the question
37
  correct_options: Optional[List[str]] = []
38
  rationale: str = Field(..., min_length=1, max_length=1000)
39
+ score: Optional[float] = None # Score for this specific answer
40
 
41
  class ApplicationBase(BaseSchema):
42
  job_id: str = Field(..., min_length=1)
 
58
 
59
  class ApplicationResponse(ApplicationBase):
60
  id: str
61
+ score: Optional[float] = None # Overall application score
62
  passing_score: Optional[float] = None
63
  assessment_details: Optional[ApplicationAssessment] = None
64
+ question_scores: Optional[List[ApplicationAnswerScore]] = None # Individual question scores
65
 
66
  class Config:
67
  from_attributes = True
backend/services/application_service.py CHANGED
@@ -51,19 +51,25 @@ def get_applications_by_user(db: Session, user_id: str, skip: int = 0, limit: in
51
  return applications
52
 
53
  def create_application(db: Session, application: ApplicationCreate) -> Application:
54
- """Create a new application"""
55
  logger.info(f"Creating new application for job ID: {application.job_id}, assessment ID: {application.assessment_id}, user ID: {application.user_id}")
 
 
 
 
56
  db_application = Application(
57
  id=str(uuid.uuid4()),
58
  job_id=application.job_id,
59
  assessment_id=application.assessment_id,
60
  user_id=application.user_id,
61
- answers=json.dumps([ans.dict() for ans in application.answers]) # Store as JSON string
 
 
62
  )
63
  db.add(db_application)
64
  db.commit()
65
  db.refresh(db_application)
66
- logger.info(f"Successfully created application with ID: {db_application.id}")
67
  return db_application
68
 
69
  def update_application(db: Session, application_id: str, **kwargs) -> Optional[Application]:
@@ -95,6 +101,106 @@ def delete_application(db: Session, application_id: str) -> bool:
95
  logger.warning(f"Failed to delete application - application not found: {application_id}")
96
  return False
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  def calculate_application_score(db: Session, application_id: str) -> float:
99
  """Calculate the score for an application"""
100
  logger.debug(f"Calculating score for application ID: {application_id}")
 
51
  return applications
52
 
53
  def create_application(db: Session, application: ApplicationCreate) -> Application:
54
+ """Create a new application and calculate scores"""
55
  logger.info(f"Creating new application for job ID: {application.job_id}, assessment ID: {application.assessment_id}, user ID: {application.user_id}")
56
+
57
+ # Calculate scores for the application
58
+ score, question_scores = calculate_detailed_application_score(db, application)
59
+
60
  db_application = Application(
61
  id=str(uuid.uuid4()),
62
  job_id=application.job_id,
63
  assessment_id=application.assessment_id,
64
  user_id=application.user_id,
65
+ answers=json.dumps([ans.dict() for ans in application.answers]), # Store as JSON string
66
+ score=score, # Store the overall application score
67
+ question_scores=json.dumps([qs.dict() for qs in question_scores]) # Store individual question scores as JSON
68
  )
69
  db.add(db_application)
70
  db.commit()
71
  db.refresh(db_application)
72
+ logger.info(f"Successfully created application with ID: {db_application.id} and overall score: {score}")
73
  return db_application
74
 
75
  def update_application(db: Session, application_id: str, **kwargs) -> Optional[Application]:
 
101
  logger.warning(f"Failed to delete application - application not found: {application_id}")
102
  return False
103
 
104
+ def calculate_detailed_application_score(db: Session, application_create: ApplicationCreate):
105
+ """Calculate detailed scores for an application including individual question scores"""
106
+ from models.assessment import Assessment
107
+ from schemas.application import ApplicationAnswerScore
108
+
109
+ logger.debug(f"Calculating detailed scores for application - job ID: {application_create.job_id}, assessment ID: {application_create.assessment_id}")
110
+
111
+ # Get the associated assessment to compare answers with correct answers
112
+ assessment = db.query(Assessment).filter(Assessment.id == application_create.assessment_id).first()
113
+ if not assessment:
114
+ logger.warning(f"Assessment not found for ID: {application_create.assessment_id}")
115
+ return 0.0, []
116
+
117
+ # Parse the questions
118
+ import json
119
+ try:
120
+ questions = json.loads(assessment.questions) if assessment.questions else []
121
+ except json.JSONDecodeError:
122
+ logger.error(f"Failed to parse questions for assessment ID: {application_create.assessment_id}")
123
+ return 0.0, []
124
+
125
+ # Create a mapping of question_id to question for easy lookup
126
+ question_map = {q['id']: q for q in questions}
127
+
128
+ # Calculate the scores
129
+ total_points = 0
130
+ earned_points = 0
131
+ question_scores = []
132
+
133
+ for answer in application_create.answers:
134
+ question_id = answer.question_id
135
+ if not question_id or question_id not in question_map:
136
+ continue
137
+
138
+ question_data = question_map[question_id]
139
+
140
+ # Calculate weighted score
141
+ question_weight = question_data.get('weight', 1) # Default weight is 1
142
+ total_points += question_weight
143
+
144
+ # Initialize question score object
145
+ question_score_obj = ApplicationAnswerScore(
146
+ question_id=question_id,
147
+ score=0.0,
148
+ rationale="No rationale available"
149
+ )
150
+
151
+ # For multiple choice questions, score directly without AI
152
+ if question_data['type'] in ['choose_one', 'choose_many']:
153
+ correct_options = set(question_data.get('correct_options', []))
154
+ selected_options = set(answer.options or [])
155
+
156
+ # Check if the selected options match the correct options exactly
157
+ if selected_options == correct_options:
158
+ earned_points += question_weight # Full points for correct answer
159
+ question_score_obj.score = 1.0 # Perfect score
160
+ question_score_obj.rationale = "Correct answer"
161
+ else:
162
+ question_score_obj.score = 0.0 # No points for incorrect answer
163
+ question_score_obj.rationale = f"Incorrect. Correct options: {list(correct_options)}, Selected: {list(selected_options)}"
164
+
165
+ # For text-based questions, use AI to evaluate the answer
166
+ elif question_data['type'] == 'text_based':
167
+ # Convert the question data to an AssessmentQuestion object
168
+ from schemas.assessment import AssessmentQuestion, AssessmentQuestionOption
169
+ from schemas.enums import QuestionType
170
+ question_obj = AssessmentQuestion(
171
+ id=question_data['id'],
172
+ text=question_data['text'],
173
+ weight=question_data['weight'],
174
+ skill_categories=question_data['skill_categories'],
175
+ type=QuestionType(question_data['type']),
176
+ options=[AssessmentQuestionOption(text=opt['text'], value=opt['value']) for opt in question_data.get('options', [])],
177
+ correct_options=question_data.get('correct_options', [])
178
+ )
179
+
180
+ # Use AI service to score the text-based answer
181
+ from services.ai_service import score_answer
182
+ score_result = score_answer(
183
+ question=question_obj,
184
+ answer_text=answer.text or '',
185
+ selected_options=answer.options or []
186
+ )
187
+
188
+ earned_points += score_result['score'] * question_weight
189
+ question_score_obj.score = score_result['score']
190
+ question_score_obj.rationale = score_result['rationale']
191
+
192
+ question_scores.append(question_score_obj)
193
+
194
+ # Calculate percentage score
195
+ if total_points > 0:
196
+ overall_score = (earned_points / total_points) * 100
197
+ else:
198
+ overall_score = 0.0
199
+
200
+ logger.debug(f"Calculated detailed scores: overall {overall_score}% ({earned_points}/{total_points} points), {len(question_scores)} questions scored")
201
+ return round(overall_score, 2), question_scores
202
+
203
+
204
  def calculate_application_score(db: Session, application_id: str) -> float:
205
  """Calculate the score for an application"""
206
  logger.debug(f"Calculating score for application ID: {application_id}")