Skip to main content

Overview

After installation, ION Career requires minimal configuration. The app automatically installs custom fields, web forms, and scripts through fixtures. This guide covers how to verify and customize these configurations.

Module Information

App Name

ion_career

Module

ION Career

Publisher

ARD

License

MIT

Automatic Fixtures

ION Career uses Frappe’s fixture system to automatically install configurations. These are defined in hooks.py:19-36:
fixtures = [
    {
        "doctype": "Custom Field",
        "filters": [["module", "=", "ION Career"]]
    },
    {
        "doctype": "Client Script",
        "filters": [["module", "=", "ION Career"]]
    },
    {
        "doctype": "Server Script",
        "filters": [["module", "=", "ION Career"]]
    },
    {
        "doctype": "Web Form",
        "filters": [["module", "=", "ION Career"]]
    },
]
Fixtures are automatically installed during bench install-app ion_career. You don’t need to manually configure these unless customizing the app.

Custom Fields Configuration

The app adds custom fields to two core ERPNext doctypes:

Job Opening Fields

custom_job_question_set
Link
required
Links to a Job Question Set doctype. This field is required when creating job openings.Properties:
  • Inserted after: designation
  • Link to: Job Question Set
  • Module: ION Career

Job Applicant Fields

custom_questions
Tab Break
Creates a dedicated “Questions” tab in the Job Applicant formProperties:
  • Inserted after: upper_range
  • Label: “Questions”
custom_score
Data
default:"0"
Displays the calculated applicant score (0-10 scale)Properties:
  • Read-only: Yes
  • Inserted after: applicant_rating
  • Calculated automatically by handlers.py:process_job_questions
custom_job_question_answers
Text
Stores raw JSON of applicant answers (hidden field)Properties:
  • Hidden: Yes
  • Used by: handlers.py to process answers
custom_question_answers
Table
Child table showing structured question-answer pairsProperties:
  • Child DocType: Job Applicant Question Answer
  • Shows: question, fieldname, answer, job_opening
custom_questions_html
HTML
HTML field for custom question rendering in forms

Document Event Hooks

The app registers a document event hook in hooks.py:13-17:
doc_events = {
    "Job Applicant": {
        "after_insert": "ion_career.handlers.process_job_questions"
    }
}

How It Works

1

Trigger

Automatically runs after a Job Applicant record is created
2

Processing

The process_job_questions handler:
  1. Reads answers from custom_job_question_answers JSON field
  2. Fetches the associated Job Question Set
  3. Creates child table entries in custom_question_answers
  4. Calculates the applicant score
3

Scoring Algorithm

# From handlers.py:37-38
score_multiplier = 10 / len(qset.questions)
doc.custom_score = score_multiplier * total_answered
Scores are based on “Yes” answers to questions (0-10 scale)
The document event triggers on after_insert only. If you manually edit the custom_job_question_answers field, you must recalculate scores manually or trigger a save operation.

API Endpoints

The app exposes one whitelisted API endpoint for web form integration:

get_job_questions

File: api.py:2-18
@frappe.whitelist()
def get_job_questions(job_opening):
    jo = frappe.get_doc("Job Opening", job_opening)
    qset_name = jo.custom_job_question_set
    
    if not qset_name:
        return []
    
    qset = frappe.get_doc("Job Question Set", qset_name)
    
    return [{
        "question": q.question,
        "fieldname": q.fieldname,
        "input_type": q.input_type,
        "required": q.required
    } for q in qset.questions]
Usage:
frappe.call({
    method: 'ion_career.api.get_job_questions',
    args: { job_opening: 'JOB-OPENING-00001' },
    callback: function(r) {
        console.log(r.message); // Array of questions
    }
});
Returns:
[
    {
        "question": "Do you have 5+ years of Python experience?",
        "fieldname": "python_exp_5y",
        "input_type": "Select",
        "required": true
    }
]

DocType Configurations

Job Question Set

File: ion_career/doctype/job_question_set/job_question_set.json
autoname
field:title
Automatically names documents using the title field
allow_rename
boolean
default:"true"
Question sets can be renamed after creation
Fields:
  • title (Data, unique): Display name of the question set
  • questions (Table): Child table of Job Question records
Permissions:
  • System Manager: Full access (create, read, write, delete)

Job Question

File: ion_career/doctype/job_question/job_question.json
istable
boolean
default:"true"
Child table doctype (cannot exist independently)
editable_grid
boolean
default:"true"
Allows inline editing in the parent form
Fields:
  • question (Data, required): The screening question text
  • fieldname (Data, hidden): Auto-generated field identifier
  • input_type (Select, default=“Select”): Input type (Checkbox/Select)
  • required (Check, default=0): Whether answer is mandatory
  • order (Int): Display order in forms

Job Applicant Question Answer

File: ion_career/doctype/job_applicant_question_answer/ Child table storing individual answers: Fields:
  • question (Data): Copy of the question text
  • fieldname (Data): Field identifier from Job Question
  • answer (Text): Applicant’s answer
  • job_opening (Link): Reference to Job Opening

Web Form Configuration

The app includes a web form fixture for public job applications: Name: ION Job Application
1

Access web form settings

Navigate to Website > Web Form > ION Job Application
2

Enable the form

Set “Published” to Yes to make it publicly accessible
3

Configure settings

  • DocType: Job Applicant
  • Route: /ion-job-application (or customize)
  • Login Required: No (for public applications)
  • Allow Edit: No
4

Customize fields

Add standard Job Applicant fields plus dynamic question loading via custom script
The web form uses the get_job_questions API to dynamically load questions based on the selected job opening.

Score Calculation Details

The scoring system is implemented in handlers.py:23-40:
total_answered = 0

for q in qset.questions:
    doc.append("custom_question_answers", {
        "question": q.question,
        "fieldname": q.fieldname,
        "answer": answers.get(q.fieldname),
        "job_opening": job_opening
    })
    
    if answers.get(q.fieldname) == "Yes":
        total_answered += 1

score_multiplier = 10 / len(qset.questions)
doc.custom_score = score_multiplier * total_answered
Scoring Logic:
  1. Each “Yes” answer counts as 1 point
  2. Total points are normalized to a 0-10 scale
  3. Formula: (number_of_yes_answers / total_questions) * 10
Example:
  • Question Set: 5 questions
  • Applicant answers “Yes” to 3 questions
  • Score: (3 / 5) * 10 = 6.0
The current implementation only counts “Yes” answers. Other answer types (No, Maybe, numeric values) are not factored into scoring. Customize handlers.py to implement different scoring logic.

Validation Rules

The app includes answer validation in handlers.py:43-62:
def validate(doc, method):
    if not doc.custom_job_question_answers:
        return
    
    answers = json.loads(doc.custom_job_question_answers)
    qset_name = frappe.db.get_value(
        "Job Opening",
        doc.job_opening,
        "custom_job_question_set"
    )
    qset = frappe.get_doc("Job Question Set", qset_name)
    
    for q in qset.questions:
        if q.required and q.fieldname not in answers:
            frappe.throw(
                f"Missing answer for required question: {q.question}"
            )
This validation function is defined but not currently hooked in hooks.py. To enable validation, add it to the doc_events configuration:
doc_events = {
    "Job Applicant": {
        "after_insert": "ion_career.handlers.process_job_questions",
        "validate": "ion_career.handlers.validate"  # Add this line
    }
}

Customization Examples

Custom Scoring Logic

To implement weighted scoring, edit handlers.py:37-38:
# Original
score_multiplier = 10 / len(qset.questions)
doc.custom_score = score_multiplier * total_answered

# Weighted version
total_weight = sum(q.weight for q in qset.questions)  # Add weight field to Job Question
weighted_score = sum(
    q.weight for q in qset.questions 
    if answers.get(q.fieldname) == "Yes"
)
doc.custom_score = (weighted_score / total_weight) * 10

Adding Question Types

Extend the input_type options in job_question.json:36:
"options": "Checkbox\nSelect\nRating\nText\nNumber"
Then update scoring logic in handlers.py to handle new types.

Troubleshooting Configuration

Manually reimport fixtures:
bench --site your-site-name migrate
bench --site your-site-name import-fixtures ion_career
Check module assignment:
  1. Go to Customize Form
  2. Select Job Opening or Job Applicant
  3. Verify custom fields have “ION Career” as module
  4. Clear cache: bench --site your-site clear-cache
Verify:
  • Document event hook is registered in hooks.py:13-17
  • custom_job_question_answers field contains valid JSON
  • Job Opening has a linked Question Set
  • Check Error Log for exceptions in handlers.py
Ensure:
  • api.py has @frappe.whitelist() decorator
  • App is properly installed and migrated
  • User has permissions to access Job Opening doctype
  • Call path is ion_career.api.get_job_questions (not /api/method/...)

Next Steps

Create Question Sets

Start building screening questions for your job openings

Customize Scoring

Implement weighted or custom scoring algorithms

Extend Fields

Add custom fields or modify existing configurations

API Integration

Build external integrations using the API endpoints

Build docs developers (and LLMs) love