Current directory: /home/klas4s23/domains/585455.klas4s23.mid-ica.nl/public_html/Gastenboek/uploads
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Party Position Calculation System Documentation</title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<!-- Fallback language links (visible if JS not available or as a quick debug aid) -->
<div class="lang-toggle-noscript" aria-hidden="false">
<a href="stemwijzer_nl.html">NL</a>
<span aria-hidden="true"> | </span>
<a href="stemwijzer.html">EN</a>
</div>
<h1>Party Position Calculation System</h1>
<h2>Complete Technical Documentation</h2>
<!-- TOC placeholder: script.js will build the left sidebar and move content into the layout -->
<div id="doc-toc-placeholder" aria-hidden="true"></div>
<p><strong>Document Version:</strong> 1.0.0<br>
<strong>Last Updated:</strong> October 15, 2025<br>
<strong>Document Type:</strong> Technical Reference Guide</p>
<div class="page-break"></div>
<h2>Table of Contents</h2>
<ol>
<li>System Overview and Introduction</li>
<li>Political Axes Framework</li>
<li>Scoring Algorithm Detailed Explanation</li>
<li>API Architecture and Endpoints</li>
<li>Database Schema and Design</li>
<li>CMS Implementation Guide</li>
<li>Practical Calculation Examples</li>
<li>Error Handling Procedures</li>
<li>Performance Optimization</li>
<li>Security Considerations</li>
<li>System Requirements</li>
<li>Troubleshooting Guide</li>
</ol>
<div class="page-break"></div>
<h2>1. System Overview and Introduction</h2>
<h3>1.1 Purpose of This System</h3>
<p>The Party Position Calculation System is designed to automatically determine where a political party stands on a four-axis political compass based on their responses to a standardized quiz. This system eliminates the need for manual position assignment and ensures consistent, objective positioning across all parties.</p>
<p>The primary goal is to provide voters with a clear understanding of each party's political stance, making it easier to compare parties and make informed decisions during elections.</p>
<h3>1.2 How the System Works</h3>
<p>The system operates on a simple principle: parties answer a series of political questions, and their answers are automatically scored and converted into a political position. Each question is associated with a specific political axis (economic left/right or social progressive/conservative), and the party's stance on that question contributes to their overall position.</p>
<p>The calculation happens in real-time through a series of API calls that retrieve quiz answers, process them according to predefined scoring rules, and store the resulting position in a database for future reference.</p>
<h3>1.3 Key Benefits</h3>
<ul>
<li><strong>Objectivity:</strong> The system removes human bias from position assignment by using a consistent mathematical formula</li>
<li><strong>Transparency:</strong> Parties can see exactly how their answers contribute to their position</li>
<li><strong>Scalability:</strong> The system can handle unlimited parties and questions without manual intervention</li>
<li><strong>Real-time Updates:</strong> Positions are recalculated immediately when parties update their answers</li>
</ul>
<div class="note">
<strong>Note:</strong> This system is designed specifically for the Dutch political context, with axis labels in Dutch (links, rechts, progressief, conservatief). However, the underlying logic can be adapted for any political system.
</div>
<div class="page-break"></div>
<h2>2. Political Axes Framework</h2>
<h3>2.1 Understanding the Four-Axis Model</h3>
<p>Unlike simple left-right political spectrum models, this system uses a four-axis model that recognizes two independent dimensions of political thought: economic policy and social policy. This provides a more nuanced understanding of political positions.</p>
<h3>2.2 The Economic Axis</h3>
<p>The economic axis measures a party's stance on financial and economic matters, specifically regarding the role of government in the economy, taxation, social welfare, and market regulation.</p>
<h4>2.2.1 Links (Left)</h4>
<p><strong>Definition:</strong> Links represents left-wing economic policies that favor greater government intervention in the economy.</p>
<p><strong>Typical Positions:</strong></p>
<ul>
<li>Higher taxes on wealthy individuals and corporations</li>
<li>Expanded social welfare programs</li>
<li>Government regulation of markets</li>
<li>Public ownership of key industries</li>
<li>Wealth redistribution policies</li>
</ul>
<h4>2.2.2 Rechts (Right)</h4>
<p><strong>Definition:</strong> Rechts represents right-wing economic policies that favor free market capitalism with minimal government intervention.</p>
<p><strong>Typical Positions:</strong></p>
<ul>
<li>Lower taxes and reduced government spending</li>
<li>Limited social welfare programs</li>
<li>Deregulation of markets</li>
<li>Private ownership and entrepreneurship</li>
<li>Individual economic responsibility</li>
</ul>
<div class="important">
<strong>Important:</strong> Links and Rechts are opposites. A party cannot be simultaneously left and right on economic issues. When a party disagrees with a left-wing question, they automatically score points on the right-wing axis, and vice versa.
</div>
<h3>2.3 The Social Axis</h3>
<p>The social axis measures a party's stance on cultural, moral, and societal issues, including individual freedoms, traditional values, immigration, and social change.</p>
<h4>2.3.1 Progressief (Progressive)</h4>
<p><strong>Definition:</strong> Progressief represents progressive social policies that embrace change and prioritize individual freedoms.</p>
<p><strong>Typical Positions:</strong></p>
<ul>
<li>Support for LGBTQ+ rights and gender equality</li>
<li>Liberal immigration policies</li>
<li>Environmental protection and climate action</li>
<li>Secular government and separation of church and state</li>
<li>Social reform and modernization</li>
</ul>
<h4>2.3.2 Conservatief (Conservative)</h4>
<p><strong>Definition:</strong> Conservatief represents conservative social policies that favor traditional values and stability.</p>
<p><strong>Typical Positions:</strong></p>
<ul>
<li>Preservation of traditional family structures</li>
<li>Stricter immigration controls</li>
<li>Emphasis on national identity and cultural heritage</li>
<li>Traditional moral values, often influenced by religion</li>
<li>Cautious approach to social change</li>
</ul>
<div class="important">
<strong>Important:</strong> Progressief and Conservatief are opposites. A party cannot be simultaneously progressive and conservative on social issues. When a party disagrees with a progressive question, they automatically score points on the conservative axis, and vice versa.
</div>
<h3>2.4 Why Two Axes Matter</h3>
<p>Using two separate axes allows the system to distinguish between parties that might seem similar on one dimension but differ on another. For example:</p>
<div class="example">
<strong>Example Scenario:</strong><br><br>
Party A: Links-Progressief (left-wing economics, progressive social views)<br>
Party B: Links-Conservatief (left-wing economics, conservative social views)<br><br>
Both parties support similar economic policies (higher taxes, welfare programs) but have completely different views on social issues. A single-axis system would fail to capture this important distinction.
</div>
<div class="page-break"></div>
<h2>3. Scoring Algorithm Detailed Explanation</h2>
<h3>3.1 Algorithm Overview</h3>
<p>The scoring algorithm converts a party's quiz answers into a political position through a five-step process. This section explains each step in detail, including the mathematical logic and decision-making criteria.</p>
<h3>3.2 Step 1: Answer Processing</h3>
<h4>3.2.1 The Three Stance Types</h4>
<p>When answering a quiz question, parties can choose one of three stances:</p>
<table>
<tr>
<th>Stance</th>
<th>Dutch Term</th>
<th>Score Value</th>
<th>Meaning</th>
</tr>
<tr>
<td>Agree</td>
<td>eens</td>
<td>+1.0</td>
<td>Party fully agrees with the statement</td>
</tr>
<tr>
<td>Disagree</td>
<td>oneens</td>
<td>+1.0 (to opposite)</td>
<td>Party fully disagrees with the statement</td>
</tr>
<tr>
<td>Unsure</td>
<td>weet_niet</td>
<td>+0.5</td>
<td>Party is uncertain or neutral</td>
</tr>
</table>
<h4>3.2.2 Why These Values?</h4>
<p><strong>Agree (eens) = +1.0:</strong> When a party agrees with a question, they receive a full point toward that question's axis. This represents a clear, unambiguous position.</p>
<p><strong>Disagree (oneens) = +1.0 to opposite axis:</strong> This is the key to the opposite-axis logic. If a party disagrees with a left-wing economic question, they are by definition taking a right-wing position. Rather than giving them a negative score on the left axis, the system gives them a positive score on the opposite (right) axis. This ensures all scores remain positive and easier to interpret.</p>
<p><strong>Unsure (weet_niet) = +0.5:</strong> Uncertainty receives half a point. This acknowledges that the party leans slightly toward the question's axis but isn't committed. Over many questions, multiple "weet_niet" answers can still contribute to a position, but they carry less weight than definitive stances.</p>
<h4>3.2.3 Processing Examples</h4>
<div class="example">
<strong>Example 1: Agreement</strong><br><br>
<strong>Question:</strong> "Should the government increase taxes on wealthy individuals?"<br>
<strong>Assigned Axis:</strong> links (this is a left-wing economic question)<br>
<strong>Party Answer:</strong> eens (agree)<br><br>
<strong>Processing:</strong><br>
- The party agrees with a links question<br>
- Add 1.0 to the links counter<br>
- Links counter increases from 0.0 to 1.0<br><br>
<strong>Result:</strong> links += 1.0
</div>
<div class="example">
<strong>Example 2: Disagreement</strong><br><br>
<strong>Question:</strong> "Should the government increase taxes on wealthy individuals?"<br>
<strong>Assigned Axis:</strong> links (this is a left-wing economic question)<br>
<strong>Party Answer:</strong> oneens (disagree)<br><br>
<strong>Processing:</strong><br>
- The party disagrees with a links question<br>
- System identifies opposite of links = rechts<br>
- Add 1.0 to the rechts counter<br>
- Rechts counter increases from 0.0 to 1.0<br><br>
<strong>Result:</strong> rechts += 1.0
</div>
<div class="example">
<strong>Example 3: Uncertainty</strong><br><br>
<strong>Question:</strong> "Should the government increase taxes on wealthy individuals?"<br>
<strong>Assigned Axis:</strong> links (this is a left-wing economic question)<br>
<strong>Party Answer:</strong> weet_niet (unsure)<br><br>
<strong>Processing:</strong><br>
- The party is unsure about a links question<br>
- Add 0.5 to the links counter<br>
- Links counter increases from 0.0 to 0.5<br><br>
<strong>Result:</strong> links += 0.5
</div>
<h3>3.3 Step 2: Axis Count Aggregation</h3>
<h4>3.3.1 The Four Counters</h4>
<p>Throughout the quiz, the system maintains four running totals (counters), one for each axis. These counters start at zero and increase as questions are processed.</p>
<div class="code-block">Initial State:
{
"links": 0.0,
"rechts": 0.0,
"progressief": 0.0,
"conservatief": 0.0
}
After Processing Several Questions:
{
"links": 12.5,
"rechts": 3.0,
"progressief": 8.5,
"conservatief": 2.0
}</div>
<h4>3.3.2 Understanding Counter Values</h4>
<p><strong>Whole Numbers:</strong> Counters with whole numbers (12.0, 3.0) indicate all answers for that axis were definitive (either eens or oneens, no weet_niet responses).</p>
<p><strong>Half Numbers:</strong> Counters with .5 values (12.5, 8.5) indicate at least one weet_niet response contributed to that axis.</p>
<p><strong>Comparative Analysis:</strong> The relationship between opposite axes tells us about the party's clarity on that dimension. For example, links: 12.5 and rechts: 3.0 means the party has much stronger left-wing than right-wing tendencies.</p>
<div class="note">
<strong>Note:</strong> The sum of all four counters does not necessarily equal the total number of questions. This is because some questions generate weet_niet responses (worth 0.5) and some questions might not have been answered at all.
</div>
<h3>3.4 Step 3: Primary Axis Determination</h3>
<h4>3.4.1 Selection Criteria</h4>
<p>The primary axis is simply the axis with the highest counter value. This represents the party's dominant political characteristic.</p>
<div class="code-block">Axis Counts:
- links: 12.5 (HIGHEST - selected as primary)
- rechts: 3.0
- progressief: 8.5
- conservatief: 2.0
Primary Axis: links</div>
<h4>3.4.2 Handling Ties</h4>
<p>If two or more axes have exactly the same highest value, the system uses alphabetical ordering to break the tie. This ensures deterministic, reproducible results.</p>
<div class="example">
<strong>Tie Scenario:</strong><br><br>
Axis Counts:<br>
- links: 5.0 (tied for highest)<br>
- rechts: 2.0<br>
- progressief: 5.0 (tied for highest)<br>
- conservatief: 1.0<br><br>
<strong>Resolution:</strong> Both links and progressief have 5.0 points. Alphabetically, "links" comes before "progressief", so links is selected as the primary axis.
</div>
<div class="important">
<strong>Important:</strong> The primary axis always represents the party's strongest characteristic. Even if the margin is small (e.g., links: 5.1 vs progressief: 5.0), the highest scorer becomes the primary axis.
</div>
<h3>3.5 Step 4: Secondary Axis Qualification</h3>
<h4>3.5.1 Why Secondary Axes?</h4>
<p>Most political parties have positions on both economic and social issues. The secondary axis allows the system to capture this two-dimensional positioning. However, not every party qualifies for a secondary axis - they must demonstrate sufficient strength in a second dimension.</p>
<h4>3.5.2 Qualification Criterion 1: Minimum Threshold (50% Rule)</h4>
<p>The secondary axis must have a counter value of at least 50% of the total questions asked for that axis category.</p>
<p><strong>Formula:</strong> secondary_count >= (total_questions_for_axis * 0.5)</p>
<p><strong>Why 50%?</strong> This threshold ensures that the secondary axis represents a genuine characteristic of the party, not just random or minimal responses. A party must demonstrate consistent positions on at least half the questions for that dimension to be considered meaningfully positioned on it.</p>
<div class="example">
<strong>Threshold Calculation Example:</strong><br><br>
<strong>Scenario:</strong> The quiz contains 15 questions about the social axis (progressief/conservatief)<br><br>
Party's progressief count: 8.5<br>
Total social questions: 15<br>
Required threshold: 15 * 0.5 = 7.5<br><br>
<strong>Check:</strong> 8.5 >= 7.5 → YES, qualifies!<br><br>
<strong>Explanation:</strong> The party scored 8.5 points on progressief out of 15 possible social questions. Since 8.5 is more than half of 15, the party has demonstrated sufficient progressive tendencies to warrant including "progressief" as a secondary axis.
</div>
<h4>3.5.3 Qualification Criterion 2: Not Opposite of Primary</h4>
<p>The secondary axis cannot be the opposite of the primary axis. This rule prevents logical contradictions.</p>
<p><strong>Exclusion Rules:</strong></p>
<ul>
<li>If primary is links → rechts is excluded from secondary consideration</li>
<li>If primary is rechts → links is excluded from secondary consideration</li>
<li>If primary is progressief → conservatief is excluded from secondary consideration</li>
<li>If primary is conservatief → progressief is excluded from secondary consideration</li>
</ul>
<p><strong>Why This Rule Exists:</strong> A party cannot be simultaneously left-wing and right-wing, or simultaneously progressive and conservative. These are contradictory positions. If a party has high scores on both opposite axes, it indicates uncertainty or inconsistency, not a dual position. In such cases, only the primary (higher-scoring) axis should be used.</p>
<div class="example">
<strong>Opposite Exclusion Example:</strong><br><br>
Axis Counts:<br>
- links: 10.0 (selected as primary)<br>
- rechts: 8.0 (high score, but opposite of primary)<br>
- progressief: 6.0<br>
- conservatief: 1.0<br><br>
<strong>Secondary Selection Process:</strong><br>
1. Exclude primary: links is removed from consideration<br>
2. Exclude opposite of primary: rechts is removed (opposite of links)<br>
3. Remaining candidates: progressief (6.0), conservatief (1.0)<br>
4. Highest remaining: progressief (6.0)<br>
5. Check threshold: Assume 10 social questions, threshold = 5.0<br>
6. Check: 6.0 >= 5.0 → YES, qualifies!<br><br>
<strong>Result:</strong> Secondary axis is progressief<br>
<strong>Final Position:</strong> links-progressief
</div>
<h4>3.5.4 Secondary Axis Selection Process</h4>
<p>The system follows this step-by-step process to select a secondary axis:</p>
<ol>
<li><strong>Remove Primary Axis:</strong> The axis already selected as primary cannot also be secondary</li>
<li><strong>Remove Opposite of Primary:</strong> Apply the opposite exclusion rule</li>
<li><strong>Identify Highest Remaining:</strong> From the two remaining axes, select the one with the highest counter value</li>
<li><strong>Verify Threshold:</strong> Check if this axis meets the 50% threshold for its category</li>
<li><strong>Decision:</strong>
<ul>
<li>If threshold is met: This becomes the secondary axis</li>
<li>If threshold is not met: No secondary axis is assigned (position remains single-axis)</li>
</ul>
</li>
</ol>
<div class="example">
<strong>Complete Secondary Axis Selection Example:</strong><br><br>
<strong>Initial Axis Counts:</strong><br>
- links: 12.5<br>
- rechts: 3.0<br>
- progressief: 8.5<br>
- conservatief: 2.0<br><br>
<strong>Quiz Details:</strong><br>
- Total economic questions (links/rechts): 20<br>
- Total social questions (progressief/conservatief): 15<br><br>
<strong>Step-by-Step Selection:</strong><br><br>
<strong>Step 1:</strong> Determine Primary<br>
- Highest value: 12.5 (links)<br>
- Primary Axis: links<br><br>
<strong>Step 2:</strong> Begin Secondary Selection<br>
- Available axes: rechts, progressief, conservatief<br><br>
<strong>Step 3:</strong> Exclude Opposite<br>
- Primary is links<br>
- Opposite of links is rechts<br>
- Remove rechts from consideration<br>
- Remaining candidates: progressief, conservatief<br><br>
<strong>Step 4:</strong> Identify Highest Remaining<br>
- progressief: 8.5 (higher)<br>
- conservatief: 2.0<br>
- Candidate for secondary: progressief<br><br>
<strong>Step 5:</strong> Verify Threshold<br>
- Progressief count: 8.5<br>
- Total social questions: 15<br>
- Required threshold: 15 * 0.5 = 7.5<br>
- Check: 8.5 >= 7.5 → YES, qualifies!<br><br>
<strong>Result:</strong><br>
- Primary Axis: links<br>
- Secondary Axis: progressief<br>
- Final Position: links-progressief
</div>
<h3>3.6 Step 5: Position Label Generation</h3>
<h4>3.6.1 Label Format</h4>
<p>The final position is expressed as a simple text string following these formats:</p>
<p><strong>Single Axis Position:</strong> Just the primary axis name<br>
Examples: "links", "rechts", "progressief", "conservatief"</p>
<p><strong>Dual Axis Position:</strong> Primary axis, hyphen, secondary axis<br>
Examples: "links-progressief", "rechts-conservatief"</p>
<h4>3.6.2 Complete List of Possible Positions</h4>
<table>
<tr>
<th>Position Type</th>
<th>Position String</th>
<th>Meaning</th>
</tr>
<tr>
<td>Single Axis</td>
<td>links</td>
<td>Primarily left-wing economic</td>
</tr>
<tr>
<td>Single Axis</td>
<td>rechts</td>
<td>Primarily right-wing economic</td>
</tr>
<tr>
<td>Single Axis</td>
<td>progressief</td>
<td>Primarily progressive social</td>
</tr>
<tr>
<td>Single Axis</td>
<td>conservatief</td>
<td>Primarily conservative social</td>
</tr>
<tr>
<td>Dual Axis</td>
<td>links-progressief</td>
<td>Left-wing economics + progressive social views</td>
</tr>
<tr>
<td>Dual Axis</td>
<td>links-conservatief</td>
<td>Left-wing economics + conservative social views</td>
</tr>
<tr>
<td>Dual Axis</td>
<td>rechts-progressief</td>
<td>Right-wing economics + progressive social views</td>
</tr>
<tr>
<td>Dual Axis</td>
<td>rechts-conservatief</td>
<td>Right-wing economics + conservative social views</td>
</tr>
</table>
<div class="important">
<strong>Important - Impossible Positions:</strong> Due to the opposite exclusion rule, these positions can never occur:<br>
- links-rechts (economically left AND right - contradiction)<br>
- rechts-links (economically right AND left - contradiction)<br>
- progressief-conservatief (socially progressive AND conservative - contradiction)<br>
- conservatief-progressief (socially conservative AND progressive - contradiction)<br><br>
Total possible valid positions: 8 (4 single + 4 dual)
</div>
<div class="page-break"></div>
<h2>4. API Architecture and Endpoints</h2>
<h3>4.1 Architecture Overview</h3>
<p>The system uses a RESTful API architecture to separate the calculation logic (API server) from the presentation layer (CMS). This separation provides several benefits:</p>
<ul>
<li><strong>Modularity:</strong> The API can be used by multiple front-ends (web, mobile, etc.)</li>
<li><strong>Maintainability:</strong> Changes to calculation logic don't require CMS updates</li>
<li><strong>Scalability:</strong> The API server can be scaled independently</li>
<li><strong>Security:</strong> Database access is centralized in the API layer</li>
</ul>
<h3>4.2 System Flow Diagram</h3>
<div class="code-block">User Action: Party completes quiz
↓
[Database] Answers stored in party_answers table
↓
User Action: Party navigates to position page
↓
[CMS] PositionController::getPartyPositionData() called
↓
[CMS] PositionService::getPartyAnswers()
↓
[CMS] ApiClient::get('/endpoints/party_answers.php')
↓
[API] Endpoint receives request
↓
[API] Validates party_id and session
↓
[API] Queries database for answers
↓
[API] Returns JSON response with answers
↓
[CMS] Validates answer completeness
↓
[CMS] PositionService::calculateAndSavePosition()
↓
[CMS] ApiClient::get('/endpoints/calculate_position.php?save=1')
↓
[API] Endpoint receives request
↓
[API] Retrieves answers from database
↓
[API] Applies scoring algorithm
↓
[API] Calculates primary and secondary axes
↓
[API] Saves position to party_position table
↓
[API] Returns JSON response with position
↓
[CMS] PositionService::getPosition()
↓
[CMS] ApiClient::get('/endpoints/party_position.php')
↓
[API] Retrieves saved position from database
↓
[API] Returns JSON response
↓
[CMS] PositionController returns data to view
↓
[View] Displays position to user</div>
<h3>4.3 Endpoint 1: GET /endpoints/party_answers.php</h3>
<h4>4.3.1 Purpose</h4>
<p>This endpoint retrieves all quiz answers for a specific party, including the axis assignment for each question. It's the first step in the position calculation process and also serves to validate whether a party has completed the quiz.</p>
<h4>4.3.2 Request Parameters</h4>
<table>
<tr>
<th>Parameter</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
<tr>
<td>party_id</td>
<td>Integer</td>
<td>Yes</td>
<td>Unique identifier of the party</td>
</tr>
</table>
<h4>4.3.3 Request Example</h4>
<div class="code-block">GET /endpoints/party_answers.php?party_id=36</div>
<h4>4.3.4 Success Response Structure</h4>
<div class="code-block">{
"success": true,
"data": {
"party_id": 36,
"quiz_attempt_id": 5,
"total_questions": 20,
"total_answers": 20,
"answers": [
{
"question_id": 1,
"axis": "links",
"stance": "eens"
},
{
"question_id": 2,
"axis": "rechts",
"stance": "oneens"
},
{
"question_id": 3,
"axis": "progressief",
"stance": "weet_niet"
}
]
}
}</div>
<h4>4.3.5 Response Field Explanations</h4>
<table>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td>success</td>
<td>Boolean</td>
<td>Indicates if the request was successful</td>
</tr>
<tr>
<td>data.party_id</td>
<td>Integer</td>
<td>Echo of the requested party ID</td>
</tr>
<tr>
<td>data.quiz_attempt_id</td>
<td>Integer</td>
<td>The attempt number (for parties who retake the quiz)</td>
</tr>
<tr>
<td>data.total_questions</td>
<td>Integer</td>
<td>Total number of questions in the quiz</td>
</tr>
<tr>
<td>data.total_answers</td>
<td>Integer</td>
<td>Number of questions the party has answered</td>
</tr>
<tr>
<td>data.answers</td>
<td>Array</td>
<td>List of all answers with question ID, axis, and stance</td>
</tr>
</table>
<div class="note">
<strong>Quiz Completion Check:</strong> The CMS compares total_questions with total_answers. If they don't match, the quiz is incomplete and position calculation should not proceed.
</div>
<h4>4.3.6 Database Queries</h4>
<p>The endpoint executes three database queries:</p>
<div class="code-block">Query 1: Get Latest Quiz Attempt
SELECT MAX(quiz_attempt_id) AS latest_attempt
FROM party_answers
WHERE party_id = ?
Purpose: Identifies the most recent quiz attempt. This is important for parties who may have retaken the quiz multiple times.
Query 2: Get Answers with Axis Information
SELECT
pa.question_id,
q.axis,
pa.stance
FROM party_answers pa
JOIN questions q ON pa.question_id = q.id
WHERE pa.party_id = ? AND pa.quiz_attempt_id = ?
ORDER BY pa.question_id
Purpose: Retrieves all answers for the latest attempt, joined with question data to include the axis assignment.
Query 3: Get Total Question Count
SELECT COUNT(*) as total FROM questions
Purpose: Provides the total number of questions to enable completion checking.</div>
<h3>4.4 Endpoint 2: GET /endpoints/calculate_position.php</h3>
<h4>4.4.1 Purpose</h4>
<p>This is the core endpoint that performs the actual position calculation. It retrieves the party's answers, applies the scoring algorithm, determines the primary and secondary axes, and optionally saves the result to the database.</p>
<h4>4.4.2 Request Parameters</h4>
<table>
<tr>
<th>Parameter</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
<tr>
<td>party_id</td>
<td>Integer</td>
<td>Yes*</td>
<td>Unique identifier of the party</td>
</tr>
<tr>
<td>session_id</td>
<td>String</td>
<td>Yes*</td>
<td>Alternative to party_id for user sessions</td>
</tr>
<tr>
<td>save</td>
<td>String</td>
<td>No</td>
<td>Set to '1' to save results to database</td>
</tr>
</table>
<p>* Either party_id or session_id must be provided, but not both</p>
<h4>4.4.3 Request Examples</h4>
<div class="code-block">For Parties (with save):
GET /endpoints/calculate_position.php?party_id=36&save=1
For Users (calculation only, no save):
GET /endpoints/calculate_position.php?session_id=abc123</div>
<h4>4.4.4 Success Response Structure</h4>
<div class="code-block">{
"success": true,
"data": {
"party_id": 36,
"position": "links-progressief",
"axis_counts": {
"links": 12.5,
"rechts": 3.0,
"progressief": 8.5,
"conservatief": 2.0
},
"total_questions": 20
}
}</div>
<h4>4.4.5 Response Field Explanations</h4>
<table>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td>success</td>
<td>Boolean</td>
<td>Indicates if the request was successful</td>
</tr>
<tr>
<td>data.party_id</td>
<td>Integer</td>
<td>The party whose position was calculated</td>
</tr>
<tr>
<td>data.position</td>
<td>String</td>
<td>The calculated position string</td>
</tr>
<tr>
<td>data.axis_counts</td>
<td>Object</td>
<td>Raw scores for all four axes</td>
</tr>
<tr>
<td>data.total_questions</td>
<td>Integer</td>
<td>Number of questions processed</td>
</tr>
</table>
<h4>4.4.6 Calculation Process Details</h4>
<p>The endpoint follows this exact process:</p>
<ol>
<li><strong>Retrieve Answers:</strong> Fetch all answers from party_answers table (same query as party_answers.php endpoint)</li>
<li><strong>Initialize Counters:</strong> Create object with all four axes set to 0.0</li>
<li><strong>Process Each Answer:</strong>
<ul>
<li>If stance is "eens": Add 1.0 to the question's axis</li>
<li>If stance is "oneens": Add 1.0 to the opposite axis</li>
<li>If stance is "weet_niet": Add 0.5 to the question's axis</li>
</ul>
</li>
<li><strong>Identify Primary Axis:</strong> Find axis with highest count (alphabetical tiebreaker)</li>
<li><strong>Identify Secondary Axis:</strong>
<ul>
<li>Exclude primary axis</li>
<li>Exclude opposite of primary axis</li>
<li>Select highest remaining axis</li>
<li>Verify 50% threshold</li>
</ul>
</li>
<li><strong>Generate Position String:</strong> Format as "primary" or "primary-secondary"</li>
<li><strong>Save (if requested):</strong> If save=1, insert/update party_position table</li>
<li><strong>Return Result:</strong> Send JSON response with position and axis counts</li>
</ol>
<h4>4.4.7 Database Operation (when save=1)</h4>
<div class="code-block">INSERT INTO party_position
(party_id, position, axis_counts, created_at)
VALUES
(?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE
position = VALUES(position),
axis_counts = VALUES(axis_counts),
created_at = NOW()
Parameters:
- party_id: Integer
- position: String (e.g., "links-progressief")
- axis_counts: JSON string (e.g., '{"links":12.5,"rechts":3.0,"progressief":8.5,"conservatief":2.0}')
Note: The ON DUPLICATE KEY UPDATE clause ensures that recalculation overwrites the previous position rather than causing an error.</div>
<h3>4.5 Endpoint 3: GET /endpoints/party_position.php</h3>
<h4>4.5.1 Purpose</h4>
<p>This endpoint retrieves a previously calculated and saved position from the database. It's faster than recalculating and provides access to the timestamp of when the position was last updated.</p>
<h4>4.5.2 Request Parameters</h4>
<table>
<tr>
<th>Parameter</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
<tr>
<td>party_id</td>
<td>Integer</td>
<td>Yes</td>
<td>Unique identifier of the party</td>
</tr>
</table>
<h4>4.5.3 Request Example</h4>
<div class="code-block">GET /endpoints/party_position.php?party_id=36</div>
<h4>4.5.4 Success Response Structure</h4>
<div class="code-block">{
"success": true,
"data": {
"party_id": 36,
"position": "links-progressief",
"created_at": "2025-10-15 10:30:00",
"axis_counts": {
"links": 12.5,
"rechts": 3.0,
"progressief": 8.5,
"conservatief": 2.0
}
}
}</div>
<h4>4.5.5 Response Field Explanations</h4>
<table>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td>success</td>
<td>Boolean</td>
<td>Indicates if the request was successful</td>
</tr>
<tr>
<td>data.party_id</td>
<td>Integer</td>
<td>The party whose position was retrieved</td>
</tr>
<tr>
<td>data.position</td>
<td>String</td>
<td>The stored position string</td>
</tr>
<tr>
<td>data.created_at</td>
<td>Timestamp</td>
<td>When the position was last calculated and saved</td>
</tr>
<tr>
<td>data.axis_counts</td>
<td>Object</td>
<td>The raw axis scores from the calculation</td>
</tr>
</table>
<h4>4.5.6 Database Query</h4>
<div class="code-block">SELECT
party_id,
position,
created_at,
axis_counts
FROM party_position
WHERE party_id = ?
Purpose: Simple retrieval of the stored position data for display purposes.</div>
<h4>4.5.7 Error Scenarios</h4>
<div class="code-block">No Position Found:
{
"success": false,
"error": "No position found for party ID 36"
}
This occurs when a party has never had their position calculated, or when the position record has been deleted.</div>
<div class="page-break"></div>
<h2>5. Database Schema and Design</h2>
<h3>5.1 Schema Overview</h3>
<p>The database schema consists of three primary tables that work together to store quiz questions, party answers, and calculated positions. The design prioritizes data integrity through foreign key relationships and uses modern features like JSON columns for flexible data storage.</p>
<h3>5.2 Table 1: questions</h3>
<h4>5.2.1 Purpose</h4>
<p>This table stores all quiz questions along with their axis assignments. Each question is permanently associated with one of the four political axes.</p>
<h4>5.2.2 Schema Definition</h4>
<div class="code-block">CREATE TABLE questions (
id INT AUTO_INCREMENT PRIMARY KEY,
axis VARCHAR(20) NOT NULL,
question_text TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_axis (axis)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;</div>
<h4>5.2.3 Field Descriptions</h4>
<table>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
<th>Constraints</th>
</tr>
<tr>
<td>id</td>
<td>INT</td>
<td>Unique question identifier</td>
<td>PRIMARY KEY, AUTO_INCREMENT</td>
</tr>
<tr>
<td>axis</td>
<td>VARCHAR(20)</td>
<td>Political axis: links, rechts, progressief, conservatief</td>
<td>NOT NULL, INDEXED</td>
</tr>
<tr>
<td>question_text</td>
<td>TEXT</td>
<td>The actual question content</td>
<td>NOT NULL</td>
</tr>
<tr>
<td>created_at</td>
<td>TIMESTAMP</td>
<td>When question was created</td>
<td>DEFAULT NOW()</td>
</tr>
<tr>
<td>updated_at</td>
<td>TIMESTAMP</td>
<td>Last modification time</td>
<td>AUTO UPDATE</td>
</tr>
</table>
<h4>5.2.4 Sample Data</h4>
<div class="code-block">id | axis | question_text
---+-------------+-----------------------------------------------
1 | links | Should taxes be increased for the wealthy?
2 | rechts | Should corporate tax rates be reduced?
3 | progressief | Should same-sex marriage be legal?
4 | conservatief| Should religious education be mandatory?</div>
<h3>5.3 Table 2: party_answers</h3>
<h4>5.3.1 Purpose</h4>
<p>This table stores all answers provided by parties to quiz questions. It supports multiple quiz attempts, allowing parties to retake the quiz if needed.</p>
<h4>5.3.2 Schema Definition</h4>
<div class="code-block">CREATE TABLE party_answers (
id INT AUTO_INCREMENT PRIMARY KEY,
party_id INT NOT NULL,
quiz_attempt_id INT NOT NULL DEFAULT 1,
question_id INT NOT NULL,
stance VARCHAR(20) NOT NULL,
answered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (party_id) REFERENCES partys(id) ON DELETE CASCADE,
FOREIGN KEY (question_id) REFERENCES questions(id) ON DELETE CASCADE,
UNIQUE KEY unique_answer (party_id, quiz_attempt_id, question_id),
INDEX idx_party_attempt (party_id, quiz_attempt_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;</div>
<h4>5.3.3 Field Descriptions</h4>
<table>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
<th>Constraints</th>
</tr>
<tr>
<td>id</td>
<td>INT</td>
<td>Unique answer record identifier</td>
<td>PRIMARY KEY, AUTO_INCREMENT</td>
</tr>
<tr>
<td>party_id</td>
<td>INT</td>
<td>References the party who answered</td>
<td>FOREIGN KEY, NOT NULL</td>
</tr>
<tr>
<td>quiz_attempt_id</td>
<td>INT</td>
<td>Attempt number (1, 2, 3...)</td>
<td>NOT NULL, DEFAULT 1</td>
</tr>
<tr>
<td>question_id</td>
<td>INT</td>
<td>References the question answered</td>
<td>FOREIGN KEY, NOT NULL</td>
</tr>
<tr>
<td>stance</td>
<td>VARCHAR(20)</td>
<td>Answer: eens, oneens, weet_niet</td>
<td>NOT NULL</td>
</tr>
<tr>
<td>answered_at</td>
<td>TIMESTAMP</td>
<td>When answer was submitted</td>
<td>DEFAULT NOW()</td>
</tr>
</table>
<h4>5.3.4 Important Constraints</h4>
<p><strong>Unique Answer Constraint:</strong> The UNIQUE KEY ensures a party cannot answer the same question multiple times within the same attempt. However, they can provide different answers across different attempts.</p>
<p><strong>Cascade Deletion:</strong> ON DELETE CASCADE means if a party is deleted, all their answers are automatically deleted. Similarly, if a question is removed from the system, all answers to that question are deleted.</p>
<h4>5.3.5 Sample Data</h4>
<div class="code-block">party_id | quiz_attempt_id | question_id | stance | answered_at
---------+-----------------+-------------+-------------+--------------------
36 | 1 | 1 | eens | 2025-10-15 09:00:00
36 | 1 | 2 | oneens | 2025-10-15 09:01:00
36 | 1 | 3 | weet_niet | 2025-10-15 09:02:00
42 | 1 | 1 | oneens | 2025-10-15 10:00:00
42 | 2 | 1 | eens | 2025-10-15 11:00:00 (retake)</div>
<h3>5.4 Table 3: party_position</h3>
<h4>5.4.1 Purpose</h4>
<p>This table stores the calculated political positions for each party. It serves as a cache to avoid recalculating positions on every page load, and provides a historical record of when positions were last calculated.</p>
<h4>5.4.2 Schema Definition</h4>
<div class="code-block">CREATE TABLE party_position (
id INT AUTO_INCREMENT PRIMARY KEY,
party_id INT NOT NULL,
position VARCHAR(50) NOT NULL,
axis_counts JSON NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_party (party_id),
FOREIGN KEY (party_id) REFERENCES partys(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;</div>
<h4>5.4.3 Field Descriptions</h4>
<table>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
<th>Constraints</th>
</tr>
<tr>
<td>id</td>
<td>INT</td>
<td>Unique record identifier</td>
<td>PRIMARY KEY, AUTO_INCREMENT</td>
</tr>
<tr>
<td>party_id</td>
<td>INT</td>
<td>References the party</td>
<td>FOREIGN KEY, UNIQUE, NOT NULL</td>
</tr>
<tr>
<td>position</td>
<td>VARCHAR(50)</td>
<td>Calculated position string</td>
<td>NOT NULL</td>
</tr>
<tr>
<td>axis_counts</td>
<td>JSON</td>
<td>Raw axis scores as JSON object</td>
<td>NOT NULL</td>
</tr>
<tr>
<td>created_at</td>
<td>TIMESTAMP</td>
<td>When position was calculated</td>
<td>DEFAULT NOW()</td>
</tr>
</table>
<h4>5.4.4 JSON Structure for axis_counts</h4>
<p>The axis_counts field stores a JSON object with exactly four keys, one for each axis:</p>
<div class="code-block">Example JSON:
{
"links": 12.5,
"rechts": 3.0,
"progressief": 8.5,
"conservatief": 2.0
}
All four keys must always be present, even if the value is 0.0</div>
<h4>5.4.5 Sample Data</h4>
<div class="code-block">id | party_id | position | axis_counts | created_at
---+----------+--------------------+--------------------------------+--------------------
1 | 36 | links-progressief | {"links":12.5,"rechts":3.0,... | 2025-10-15 10:30:00
2 | 42 | rechts | {"links":2.0,"rechts":15.0,... | 2025-10-15 11:45:00</div>
<h4>5.4.6 Unique Party Constraint</h4>
<p>The UNIQUE KEY on party_id ensures each party can have only one position record. When recalculating, the system uses INSERT ... ON DUPLICATE KEY UPDATE to replace the existing record rather than creating a new one.</p>
<div class="page-break"></div>
<h2>6. CMS Implementation Guide</h2>
<h3>6.1 Three-Tier Architecture</h3>
<p>The CMS follows a three-tier architecture pattern to maintain clean separation of concerns:</p>
<ul>
<li><strong>View Layer:</strong> Handles presentation and user interface</li>
<li><strong>Controller Layer:</strong> Orchestrates business logic and data flow</li>
<li><strong>Service Layer:</strong> Manages API communication and data transformation</li>
</ul>
<h3>6.2 Service Layer: PositionService.php</h3>
<h4>6.2.1 Purpose</h4>
<p>The PositionService class acts as a wrapper around API calls, providing a clean interface for the controller layer to interact with the position calculation system.</p>
<h4>6.2.2 Complete Implementation</h4>
<div class="code-block">class PositionService
{
/**
* Retrieve party's quiz answers with axis information
*
* @param int $party_id The party identifier
* @return array API response containing answers
* @throws Exception if API call fails
*/
public static function getPartyAnswers($party_id)
{
return ApiClient::get('/endpoints/party_answers.php', [
'party_id' => $party_id
]);
}
/**
* Calculate position and save to database
*
* @param int $party_id The party identifier
* @return array API response containing calculated position
* @throws Exception if API call fails
*/
public static function calculateAndSavePosition($party_id)
{
return ApiClient::get('/endpoints/calculate_position.php', [
'party_id' => $party_id,
'save' => '1'
]);
}
/**
* Retrieve saved position from database
*
* @param int $party_id The party identifier
* @return array API response containing stored position
* @throws Exception if API call fails
*/
public static function getPosition($party_id)
{
return ApiClient::get('/endpoints/party_position.php', [
'party_id' => $party_id
]);
}
}</div>
<h4>6.2.3 Method Descriptions</h4>
<p><strong>getPartyAnswers():</strong> This method is called first to retrieve all quiz answers. The response includes total_questions and total_answers, allowing the controller to verify quiz completion before proceeding with calculation.</p>
<p><strong>calculateAndSavePosition():</strong> This method triggers the position calculation algorithm on the API server. The save=1 parameter ensures the result is persisted to the party_position table.</p>
<p><strong>getPosition():</strong> This method retrieves the previously calculated and saved position. It's faster than recalculation and is typically used when displaying positions on list pages or public-facing views.</p>
<h3>6.3 Controller Layer: PositionController.php</h3>
<h4>6.3.1 Purpose</h4>
<p>The PositionController orchestrates the entire position calculation and display workflow. It handles authentication, validates data completeness, manages error conditions, and prepares data for the view layer.</p>
<h4>6.3.2 Complete Implementation</h4>
<div class="code-block">class PositionController
{
/**
* Main method to get all data needed for position display
*
* @return array Structured data for the view layer
*/
public static function getPartyPositionData()
{
try {
// Step 1: Authenticate and get party ID
$currentUser = AuthController::getCurrentUser();
if (!$currentUser || !isset($currentUser['party_id'])) {
return self::returnError('User not authenticated');
}
$party_id = $currentUser['party_id'];
// Step 2: Fetch party profile information
$profile = PartyService::getById($party_id);
if (!$profile || !$profile['success']) {
return self::returnError('Party profile not found');
}
// Step 3: Fetch quiz answers
$answersData = PositionService::getPartyAnswers($party_id);
if (!$answersData || !$answersData['success']) {
return self::returnError('Unable to retrieve quiz answers');
}
$data = $answersData['data'];
// Step 4: Validate quiz completion
if ($data['total_answers'] < $data['total_questions']) {
$remaining = $data['total_questions'] - $data['total_answers'];
return self::returnError(
"Quiz incomplete. Please answer the remaining {$remaining} questions."
);
}
// Step 5: Calculate and save position
$calculatedPosition = PositionService::calculateAndSavePosition($party_id);
if (!$calculatedPosition || !$calculatedPosition['success']) {
return self::returnError('Position calculation failed');
}
// Step 6: Retrieve saved position for display
$partyPosition = PositionService::getPosition($party_id);
if (!$partyPosition || !$partyPosition['success']) {
return self::returnError('Unable to retrieve saved position');
}
// Step 7: Return structured data
return [
'profile' => $profile['data'],
'partyPosition' => $partyPosition['data'],
'axisCounts' => $partyPosition['data']['axis_counts'],
'errorMessage' => '',
'showPosition' => true
];
} catch (Exception $e) {
return self::returnError('System error: ' . $e->getMessage());
}
}
/**
* Helper method to format error responses
*
* @param string $message Error message
* @return array Error data structure
*/
private static function returnError($message)
{
return [
'profile' => null,
'partyPosition' => null,
'axisCounts' => [
'links' => 0,
'rechts' => 0,
'progressief' => 0,
'conservatief' => 0
],
'errorMessage' => $message,
'showPosition' => false
];
}
}</div>
<h4>6.3.3 Workflow Explanation</h4>
<p><strong>Step 1 - Authentication:</strong> Verifies the user is logged in and retrieves their party_id. This ensures parties can only view their own position.</p>
<p><strong>Step 2 - Profile Retrieval:</strong> Fetches the party's basic information (name, logo, etc.) for display purposes.</p>
<p><strong>Step 3 - Answer Retrieval:</strong> Gets all quiz answers along with completion statistics.</p>
<p><strong>Step 4 - Completion Check:</strong> Compares total_answers with total_questions. If incomplete, returns an error message indicating how many questions remain.</p>
<p><strong>Step 5 - Calculation:</strong> Triggers the position calculation algorithm with save enabled. This ensures the database is updated with the latest position.</p>
<p><strong>Step 6 - Position Retrieval:</strong> Fetches the newly saved position data for display. While this might seem redundant after step 5, it ensures consistency with the database state.</p>
<p><strong>Step 7 - Data Return:</strong> Packages all data into a structured array for the view layer.</p>
<h3>6.4 View Layer: party-positions.php</h3>
<h4>6.4.1 Purpose</h4>
<p>The view layer is responsible for rendering the position data to the user. It includes conditional logic to display either the position information or error messages based on the data received from the controller.</p>
<h4>6.4.2 Complete Implementation</h4>
<div class="code-block"><?php
// Get data from controller
$positionData = PositionController::getPartyPositionData();
// Extract variables for easier access
$profile = $positionData['profile'] ?? [
'name' => 'Unknown Party',
'logo' => ''
];
$partyPosition = $positionData['partyPosition'] ?? null;
$axisCounts = $positionData['axisCounts'] ?? [
'links' => 0,
'rechts' => 0,
'progressief' => 0,
'conservatief' => 0
];
$errorMessage = $positionData['errorMessage'] ?? '';
$showPosition = $positionData['showPosition'] ?? false;
?>
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<title>Party Position - <?php echo htmlspecialchars($profile['name']); ?></title>
</head>
<body>
<div class="container">
<h1>Political Position for <?php echo htmlspecialchars($profile['name']); ?></h1>
<?php if ($showPosition && $partyPosition): ?>
<!-- Position Result Component -->
<?php include 'components/position_result.php'; ?>
<!-- Axis Scores Component -->
<?php include 'components/axis_scores.php'; ?>
<!-- Political Compass Visualization Component -->
<?php include 'components/political_compass.php'; ?>
<?php else: ?>
<!-- Error Message Component -->
<?php include 'components/position_error.php'; ?>
<?php endif; ?>
</div>
</body>
</html></div>
<h4>6.4.3 Component Files</h4>
<p>The view includes several component files for modular rendering:</p>
<p><strong>components/position_result.php:</strong> Displays the calculated position string prominently with a human-readable explanation.</p>
<p><strong>components/axis_scores.php:</strong> Shows the raw axis counts as a table or chart, allowing parties to understand how their position was calculated.</p>
<p><strong>components/political_compass.php:</strong> Visual representation of the position on a two-dimensional compass diagram.</p>
<p><strong>components/position_error.php:</strong> Displays the error message when position calculation fails or quiz is incomplete.</p>
<div class="page-break"></div>
<h2>7. Practical Calculation Examples</h2>
<h3>7.1 Example 1: Clear Dual-Axis Position</h3>
<h4>7.1.1 Scenario</h4>
<p>A party with strong left-wing economic views and progressive social views answers 20 quiz questions.</p>
<h4>7.1.2 Quiz Distribution</h4>
<ul>
<li>Economic questions (links/rechts): 10 questions</li>
<li>Social questions (progressief/conservatief): 10 questions</li>
</ul>
<h4>7.1.3 Party Answers</h4>
<div class="code-block">Economic Questions:
Q1 (links): eens → links += 1.0
Q2 (links): eens → links += 1.0
Q3 (links): eens → links += 1.0
Q4 (links): eens → links += 1.0
Q5 (links): eens → links += 1.0
Q6 (rechts): oneens → links += 1.0
Q7 (rechts): oneens → links += 1.0
Q8 (rechts): oneens → links += 1.0
Q9 (rechts): oneens → links += 1.0
Q10 (rechts): oneens → links += 1.0
Social Questions:
Q11 (progressief): eens → progressief += 1.0
Q12 (progressief): eens → progressief += 1.0
Q13 (progressief): eens → progressief += 1.0
Q14 (progressief): eens → progressief += 1.0
Q15 (progressief): eens → progressief += 1.0
Q16 (conservatief): oneens → progressief += 1.0
Q17 (conservatief): oneens → progressief += 1.0
Q18 (conservatief): oneens → progressief += 1.0
Q19 (conservatief): oneens → progressief += 1.0
Q20 (conservatief): oneens → progressief += 1.0</div>
<h4>7.1.4 Axis Count Results</h4>
<div class="code-block">links: 10.0
rechts: 0.0
progressief: 10.0
conservatief: 0.0</div>
<h4>7.1.5 Position Calculation</h4>
<p><strong>Primary Axis Selection:</strong></p>
<ul>
<li>links: 10.0 (tied for highest)</li>
<li>progressief: 10.0 (tied for highest)</li>
<li>Alphabetical order: "links" comes before "progressief"</li>
<li><strong>Primary Axis: links</strong></li>
</ul>
<p><strong>Secondary Axis Selection:</strong></p>
<ul>
<li>Exclude primary: links removed</li>
<li>Exclude opposite: rechts removed (opposite of links)</li>
<li>Remaining: progressief (10.0), conservatief (0.0)</li>
<li>Highest remaining: progressief (10.0)</li>
<li>Threshold check: 10.0 >= (10 * 0.5 = 5.0) → YES</li>
<li><strong>Secondary Axis: progressief</strong></li>
</ul>
<p><strong>Final Position: links-progressief</strong></p>
<h4>7.1.6 Interpretation</h4>
<p>This party has a clear and consistent political position. They strongly support left-wing economic policies and progressive social values, with no conflicting answers.</p>
<h3>7.2 Example 2: Single-Axis Position (Secondary Fails Threshold)</h3>
<h4>7.2.1 Scenario</h4>
<p>A party with very strong left-wing economic views but uncertain social views.</p>
<h4>7.2.2 Party Answers</h4>
<div class="code-block">Economic Questions (10 total):
Q1-Q5 (links): eens → links += 5.0
Q6-Q10 (rechts): oneens → links += 5.0
Social Questions (10 total):
Q11-Q15 (progressief): weet_niet → progressief += 2.5
Q16-Q20 (conservatief): weet_niet → conservatief += 2.5</div>
<h4>7.2.3 Axis Count Results</h4>
<div class="code-block">links: 10.0
rechts: 0.0
progressief: 2.5
conservatief: 2.5</div>
<h4>7.2.4 Position Calculation</h4>
<p><strong>Primary Axis:</strong> links (10.0 - highest)</p>
<p><strong>Secondary Axis Check:</strong></p>
<ul>
<li>Exclude primary: links removed</li>
<li>Exclude opposite: rechts removed</li>
<li>Remaining: progressief (2.5), conservatief (2.5)</li>
<li>Highest remaining: progressief (2.5) - alphabetical tiebreaker</li>
<li>Threshold check: 2.5 >= (10 * 0.5 = 5.0) → NO, FAILS</li>
<li><strong>No secondary axis qualifies</strong></li>
</ul>
<p><strong>Final Position: links</strong></p>
<h4>7.2.5 Interpretation</h4>
<p>This party has clear economic positions but lacks a defined social stance. Their uncertainty on social questions (all weet_niet answers) means they don't meet the threshold for a secondary axis.</p>
<h3>7.3 Example 3: Mixed Answers with Half-Point Values</h3>
<h4>7.3.1 Scenario</h4>
<p>A realistic party with a mix of definitive and uncertain answers.</p>
<h4>7.3.2 Party Answers</h4>
<div class="code-block">Economic Questions (12 total):
Q1 (links): eens → links += 1.0
Q2 (links): eens → links += 1.0
Q3 (links): weet_niet → links += 0.5
Q4 (links): weet_niet → links += 0.5
Q5 (links): eens → links += 1.0
Q6 (links): eens → links += 1.0
Q7 (rechts): oneens → links += 1.0
Q8 (rechts): oneens → links += 1.0
Q9 (rechts): weet_niet → rechts += 0.5
Q10 (rechts): eens → rechts += 1.0
Q11 (rechts): eens → rechts += 1.0
Q12 (rechts): eens → rechts += 1.0
Social Questions (8 total):
Q13 (progressief): eens → progressief += 1.0
Q14 (progressief): eens → progressief += 1.0
Q15 (progressief): eens → progressief += 1.0
Q16 (progressief): eens → progressief += 1.0
Q17 (conservatief): oneens → progressief += 1.0
Q18 (conservatief): oneens → progressief += 1.0
Q19 (conservatief): weet_niet → conservatief += 0.5
Q20 (conservatief): weet_niet → conservatief += 0.5</div>
<h4>7.2.3 Axis Count Results</h4>
<div class="code-block">links: 7.0
rechts: 3.5
progressief: 6.0
conservatief: 1.0</div>
<h4>7.3.4 Position Calculation</h4>
<p><strong>Primary Axis:</strong> links (7.0 - highest)</p>
<p><strong>Secondary Axis Check:</strong></p>
<ul>
<li>Exclude primary: links removed</li>
<li>Exclude opposite: rechts removed</li>
<li>Remaining: progressief (6.0), conservatief (1.0)</li>
<li>Highest remaining: progressief (6.0)</li>
<li>Threshold check: 6.0 >= (8 * 0.5 = 4.0) → YES</li>
<li><strong>Secondary Axis: progressief</strong></li>
</ul>
<p><strong>Final Position: links-progressief</strong></p>
<h4>7.3.5 Interpretation</h4>
<p>Despite some uncertainty and a few right-wing answers, this party qualifies as links-progressief. The half-point values from weet_niet answers are reflected in the axis counts but don't prevent position qualification.</p>
<h3>7.4 Example 4: Opposite Exclusion in Action</h3>
<h4>7.4.1 Scenario</h4>
<p>A party with conflicting economic answers but clear social positions.</p>
<h4>7.4.2 Party Answers</h4>
<div class="code-block">Economic Questions (10 total):
Q1-Q5 (links): eens → links += 5.0
Q6-Q10 (rechts): eens → rechts += 5.0
Social Questions (10 total):
Q11-Q18 (progressief): eens → progressief += 8.0
Q19-Q20 (conservatief): oneens → progressief += 2.0</div>
<h4>7.4.3 Axis Count Results</h4>
<div class="code-block">links: 5.0
rechts: 5.0
progressief: 10.0
conservatief: 0.0</div>
<h4>7.4.4 Position Calculation</h4>
<p><strong>Primary Axis Selection:</strong></p>
<ul>
<li>Highest value: progressief (10.0)</li>
<li><strong>Primary Axis: progressief</strong></li>
</ul>
<p><strong>Secondary Axis Check:</strong></p>
<ul>
<li>Exclude primary: progressief removed</li>
<li>Exclude opposite: conservatief removed (opposite of progressief)</li>
<li>Remaining: links (5.0), rechts (5.0)</li>
<li>Highest remaining: links (5.0) - alphabetical tiebreaker</li>
<li>Threshold check: 5.0 >= (10 * 0.5 = 5.0) → YES</li>
<li><strong>Secondary Axis: links</strong></li>
</ul>
<p><strong>Final Position: progressief-links</strong></p>
<h4>7.4.5 Interpretation</h4>
<p>Notice the order: progressief-links (not links-progressief) because progressief had the highest score and became the primary axis. The party's conflicting economic answers (equal links and rechts scores) were resolved by the primary axis being social rather than economic.</p>
<div class="page-break"></div>
<h2>8. Error Handling Procedures</h2>
<h3>8.1 Error Response Standards</h3>
<p>All API endpoints follow a consistent error response format to ensure predictable error handling:</p>
<div class="code-block">{
"success": false,
"error": "Human-readable error message"
}</div>
<h3>8.2 Common Error Scenarios and Responses</h3>
<h4>8.2.1 No Answers Found</h4>
<p><strong>Cause:</strong> Party has never taken the quiz or all answers were deleted.</p>
<div class="code-block">API Response:
{
"success": false,
"error": "No quiz answers found for party ID 36"
}
CMS Handling:
- Display message: "You haven't completed the quiz yet. Please take the quiz to see your position."
- Show link to quiz page
- Set showPosition = false</div>
<h4>8.2.2 Incomplete Quiz</h4>
<p><strong>Cause:</strong> Party started but didn't finish the quiz.</p>
<div class="code-block">API Response:
{
"success": true,
"data": {
"total_questions": 20,
"total_answers": 15,
...
}
}
CMS Handling:
- Calculate remaining: 20 - 15 = 5
- Display message: "Quiz incomplete. Please answer the remaining 5 questions."
- Show progress: "15/20 questions answered"
- Show "Continue Quiz" button
- Set showPosition = false</div>
<h4>8.2.3 Database Connection Error</h4>
<p><strong>Cause:</strong> API cannot connect to database.</p>
<div class="code-block">API Response:
{
"success": false,
"error": "Database connection failed"
}
CMS Handling:
- Display message: "System temporarily unavailable. Please try again later."
- Log error for admin review
- Show contact support information
- Set showPosition = false</div>
<h4>8.2.4 Invalid Party ID</h4>
<p><strong>Cause:</strong> Party ID doesn't exist or is malformed.</p>
<div class="code-block">API Response:
{
"success": false,
"error": "Party not found"
}
CMS Handling:
- Display message: "Party profile not found. Please contact support."
- Log potential security issue
- Redirect to dashboard
- Set showPosition = false</div>
<h4>8.2.5 No Position Record</h4>
<p><strong>Cause:</strong> Position has never been calculated or was deleted.</p>
<div class="code-block">API Response:
{
"success": false,
"error": "No position found for party ID 36"
}
CMS Handling:
- Automatically trigger calculateAndSavePosition()
- If that succeeds, retry getPosition()
- If that fails, display: "Unable to calculate position. Please contact support."
- Set showPosition = false</div>
<h3>8.3 CMS Error Handling Strategy</h3>
<h4>8.3.1 Graceful Degradation</h4>
<p>The CMS never shows raw error messages or technical details to users. Instead, it:</p>
<ul>
<li>Translates technical errors into user-friendly language</li>
<li>Provides actionable next steps (e.g., "Continue Quiz" button)</li>
<li>Maintains page structure even when data is unavailable</li>
<li>Shows contact information for unresolvable errors</li>
</ul>
<h4>8.3.2 Error Data Structure</h4>
<p>When errors occur, the controller returns this structure:</p>
<div class="code-block">[
'profile' => null or default values,
'partyPosition' => null,
'axisCounts' => [
'links' => 0,
'rechts' => 0,
'progressief' => 0,
'conservatief' => 0
],
'errorMessage' => "User-friendly error explanation",
'showPosition' => false
]</div>
<h4>8.3.3 Conditional Rendering</h4>
<p>The view checks the showPosition flag to determine what to display:</p>
<div class="code-block">if ($showPosition) {
// Display position components
include 'components/position_result.php';
include 'components/axis_scores.php';
include 'components/political_compass.php';
} else {
// Display error component
include 'components/position_error.php';
// Error message comes from $errorMessage variable
}</div>
<div class="page-break"></div>
<h2>9. Performance Optimization</h2>
<h3>9.1 Caching Strategy</h3>
<h4>9.1.1 Position Persistence</h4>
<p>The primary performance optimization is storing calculated positions in the party_position table. This means:</p>
<ul>
<li>Position calculation happens once per quiz completion</li>
<li>Subsequent page loads fetch from database (fast query)</li>
<li>No repeated calculation of the same data</li>
<li>Recalculation only when explicitly triggered</li>
</ul>
<h4>9.1.2 Latest Attempt Selection</h4>
<p>When retrieving answers, the API uses MAX(quiz_attempt_id) to find the most recent attempt:</p>
<div class="code-block">SELECT MAX(quiz_attempt_id) AS latest_attempt
FROM party_answers
WHERE party_id = ?</div>
<p>This ensures historical quiz attempts are ignored, reducing the dataset size and processing time.</p>
<h3>9.2 Database Optimization</h3>
<h4>9.2.1 Index Strategy</h4>
<p>Strategic indexes improve query performance:</p>
<table>
<tr>
<th>Table</th>
<th>Index</th>
<th>Purpose</th>
</tr>
<tr>
<td>party_answers</td>
<td>INDEX (party_id, quiz_attempt_id)</td>
<td>Fast retrieval of answers for specific party/attempt</td>
</tr>
<tr>
<td>party_position</td>
<td>UNIQUE INDEX (party_id)</td>
<td>Fast lookup and prevents duplicate positions</td>
</tr>
<tr>
<td>questions</td>
<td>INDEX (id, axis)</td>
<td>Fast JOIN operations and axis filtering</td>
</tr>
</table>
<h4>9.2.2 Query Optimization</h4>
<p><strong>Single JOIN Query:</strong> The system fetches answers with axis data in one query rather than multiple queries:</p>
<div class="code-block">-- Optimized (used)
SELECT pa.question_id, q.axis, pa.stance
FROM party_answers pa
JOIN questions q ON pa.question_id = q.id
WHERE pa.party_id = ? AND pa.quiz_attempt_id = ?
-- Unoptimized (NOT used)
-- This would require N+1 queries
SELECT * FROM party_answers WHERE party_id = ? AND quiz_attempt_id = ?
-- Then for each answer:
SELECT axis FROM questions WHERE id = ?</div>
<h4>9.2.3 JSON Storage Benefits</h4>
<p>Storing axis_counts as JSON in a single column provides several advantages:</p>
<ul>
<li>Reduces table columns from 4 to 1</li>
<li>Simpler schema maintenance</li>
<li>Easy to add new axes in the future</li>
<li>Single read operation retrieves all axis data</li>
</ul>
<h3>9.3 API Call Efficiency</h3>
<h4>9.3.1 Call Sequence Optimization</h4>
<p>The controller makes three API calls in sequence:</p>
<ol>
<li>getPartyAnswers() - Validates completion (lightweight)</li>
<li>calculateAndSavePosition() - Heavy calculation (only if complete)</li>
<li>getPosition() - Fast retrieval (cached result)</li>
</ol>
<p>This sequence ensures expensive calculations only happen when necessary.</p>
<h4>9.3.2 Save Parameter Usage</h4>
<p>The calculate_position.php endpoint supports calculation without saving:</p>
<div class="code-block">// For testing or user simulations (no save)
GET /endpoints/calculate_position.php?session_id=abc123
// For party official positions (with save)
GET /endpoints/calculate_position.php?party_id=36&save=1</div>
<p>This allows the same endpoint to serve different purposes without creating duplicate database records.</p>
<div class="page-break"></div>
<h2>10. Security Considerations</h2>
<h3>10.1 Authentication and Authorization</h3>
<h4>10.1.1 Session-Based Authentication</h4>
<p>All API endpoints verify valid user sessions before processing requests:</p>
<div class="code-block">// API-side authentication check
session_start();
if (!isset($_SESSION['user_id']) || !isset($_SESSION['party_id'])) {
http_response_code(401);
echo json_encode([
'success' => false,
'error' => 'Authentication required'
]);
exit;
}</div>
<h4>10.1.2 Party ID Validation</h4>
<p>The API verifies that the requested party_id matches the authenticated session:</p>
<div class="code-block">$requested_party_id = $_GET['party_id'];
$session_party_id = $_SESSION['party_id'];
if ($requested_party_id != $session_party_id) {
// Exception for super admins
if ($_SESSION['role'] != 'super_admin') {
http_response_code(403);
echo json_encode([
'success' => false,
'error' => 'Access denied'
]);
exit;
}
}</div>
<h4>10.1.3 Authorization Levels</h4>
<table>
<tr>
<th>User Type</th>
<th>Permissions</th>
</tr>
<tr>
<td>Party User</td>
<td>Can only view/calculate own party's position</td>
</tr>
<tr>
<td>Super Admin</td>
<td>Can view all party positions</td>
</tr>
<tr>
<td>Public User</td>
<td>No access to position data</td>
</tr>
</table>
<h3>10.2 Data Integrity Protection</h3>
<h4>10.2.1 SQL Injection Prevention</h4>
<p>All database queries use prepared statements with parameter binding:</p>
<div class="code-block">// SECURE (used)
$stmt = $pdo->prepare("SELECT * FROM party_answers WHERE party_id = ?");
$stmt->execute([$party_id]);
// INSECURE (NEVER used)
$query = "SELECT * FROM party_answers WHERE party_id = " . $_GET['party_id'];
$result = $pdo->query($query);</div>
<h4>10.2.2 Input Validation</h4>
<p>All inputs are validated before use:</p>
<div class="code-block">// Validate party_id
$party_id = filter_input(INPUT_GET, 'party_id', FILTER_VALIDATE_INT);
if ($party_id === false || $party_id <= 0) {
http_response_code(400);
echo json_encode([
'success' => false,
'error' => 'Invalid party ID'
]);
exit;
}
// Validate stance
$valid_stances = ['eens', 'oneens', 'weet_niet'];
if (!in_array($stance, $valid_stances)) {
throw new Exception('Invalid stance value');
}
// Validate axis
$valid_axes = ['links', 'rechts', 'progressief', 'conservatief'];
if (!in_array($axis, $valid_axes)) {
throw new Exception('Invalid axis value');
}</div>
<h4>10.2.3 Output Sanitization</h4>
<p>All output is sanitized to prevent XSS attacks:</p>
<div class="code-block">// In view layer
<h1><?php echo htmlspecialchars($profile['name'], ENT_QUOTES, 'UTF-8'); ?></h1>
// Position strings are validated against whitelist
$valid_positions = [
'links', 'rechts', 'progressief', 'conservatief',
'links-progressief', 'links-conservatief',
'rechts-progressief', 'rechts-conservatief'
];
if (!in_array($position, $valid_positions)) {
throw new Exception('Invalid position value');
}</div>
<h3>10.3 Database Security</h3>
<h4>10.3.1 Foreign Key Constraints</h4>
<p>Foreign keys ensure referential integrity:</p>
<div class="code-block">FOREIGN KEY (party_id) REFERENCES partys(id) ON DELETE CASCADE
FOREIGN KEY (question_id) REFERENCES questions(id) ON DELETE CASCADE</div>
<p>Benefits:</p>
<ul>
<li>Prevents orphaned records</li>
<li>Automatic cleanup when parent records are deleted</li>
<li>Database-level integrity enforcement</li>
</ul>
<h4>10.3.2 Unique Constraints</h4>
<p>Unique constraints prevent data duplication:</p>
<div class="code-block">UNIQUE KEY unique_answer (party_id, quiz_attempt_id, question_id)
UNIQUE KEY unique_party (party_id) in party_position</div>
<p>Benefits:</p>
<ul>
<li>One position per party</li>
<li>One answer per question per attempt</li>
<li>Prevents accidental duplicate submissions</li>
</ul>
<div class="page-break"></div>
<h2>11. System Requirements</h2>
<h3>11.1 API Server Requirements</h3>
<table>
<tr>
<th>Component</th>
<th>Requirement</th>
<th>Purpose</th>
</tr>
<tr>
<td>PHP Version</td>
<td>7.4 or higher</td>
<td>Modern PHP features and security patches</td>
</tr>
<tr>
<td>Database</td>
<td>MySQL 5.7+ or MariaDB 10.2+</td>
<td>JSON column support</td>
</tr>
<tr>
<td>PDO Extension</td>
<td>Enabled</td>
<td>Database connectivity and prepared statements</td>
</tr>
<tr>
<td>JSON Extension</td>
<td>Enabled</td>
<td>JSON encoding/decoding</td>
</tr>
<tr>
<td>Session Support</td>
<td>Enabled</td>
<td>User authentication</td>
</tr>
</table>
<h3>11.2 CMS Server Requirements</h3>
<table>
<tr>
<th>Component</th>
<th>Requirement</th>
<th>Purpose</th>
</tr>
<tr>
<td>PHP Version</td>
<td>7.4 or higher</td>
<td>Compatibility with modern frameworks</td>
</tr>
<tr>
<td>cURL Extension</td>
<td>Enabled</td>
<td>API communication</td>
</tr>
<tr>
<td>JSON Extension</td>
<td>Enabled</td>
<td>API response parsing</td>
</tr>
<tr>
<td>Session Support</td>
<td>Enabled</td>
<td>User session management</td>
</tr>
</table>
<h3>11.3 Database Requirements</h3>
<table>
<tr>
<th>Feature</th>
<th>Requirement</th>
<th>Purpose</th>
</tr>
<tr>
<td>Storage Engine</td>
<td>InnoDB</td>
<td>Transaction support and foreign keys</td>
</tr>
<tr>
<td>Character Set</td>
<td>utf8mb4</td>
<td>Full Unicode support including emojis</td>
</tr>
<tr>
<td>Collation</td>
<td>utf8mb4_unicode_ci</td>
<td>Case-insensitive Unicode sorting</td>
</tr>
<tr>
<td>JSON Support</td>
<td>Native JSON type</td>
<td>Efficient storage of axis_counts</td>
</tr>
</table>
<div class="page-break"></div>
<h2>12. Troubleshooting Guide</h2>
<h3>12.1 Position Not Calculating</h3>
<h4>12.1.1 Symptoms</h4>
<ul>
<li>Error message: "Position calculation failed"</li>
<li>Page shows error instead of position</li>
<li>Position remains null in database</li>
</ul>
<h4>12.1.2 Possible Causes and Solutions</h4>
<p><strong>Cause 1: Incomplete Quiz</strong></p>
<div class="code-block">Check: Compare total_answers with total_questions
Solution: Direct party to complete remaining questions
Verification: SELECT COUNT(*) FROM party_answers WHERE party_id = X AND quiz_attempt_id = Y</div>
<p><strong>Cause 2: Invalid Axis Values</strong></p>
<div class="code-block">Check: Verify all questions have valid axis assignments
Solution: Update questions table with correct axis values
Verification: SELECT DISTINCT axis FROM questions
Should return only: links, rechts, progressief, conservatief</div>
<p><strong>Cause 3: Missing Question Data</strong></p>
<div class="code-block">Check: Ensure questions table has entries
Solution: Import question data
Verification: SELECT COUNT(*) FROM questions
Should return > 0</div>
<h3>12.2 Wrong Position Calculated</h3>
<h4>12.2.1 Symptoms</h4>
<ul>
<li>Position doesn't match expected result</li>
<li>Party disputes their calculated position</li>
</ul>
<h4>12.2.2 Debugging Process</h4>
<p><strong>Step 1: Verify Answer Data</strong></p>
<div class="code-block">Query:
SELECT q.axis, pa.stance, COUNT(*) as count
FROM party_answers pa
JOIN questions q ON pa.question_id = q.id
WHERE pa.party_id = X AND pa.quiz_attempt_id = Y
GROUP BY q.axis, pa.stance
Review output to ensure answers match expectations</div>
<p><strong>Step 2: Manual Calculation</strong></p>
<div class="code-block">Calculate axis counts manually:
- Count all "eens" for each axis
- Count all "oneens" and add to opposite axis
- Count all "weet_niet" as 0.5 for each axis
Compare with axis_counts in database</div>
<p><strong>Step 3: Check Threshold Calculations</strong></p>
<div class="code-block">Verify secondary axis threshold:
- Count total questions for each axis category
- Calculate 50% of that number
- Verify secondary axis count meets threshold</div>
<h3>12.3 API Connection Errors</h3>
<h4>12.3.1 Symptoms</h4>
<ul>
<li>Error message: "Unable to retrieve quiz answers"</li>
<li>Page times out or shows generic error</li>
</ul>
<h4>12.3.2 Solutions</h4>
<p><strong>Check 1: API Server Status</strong></p>
<div class="code-block">Test: Access API endpoint directly in browser
URL: https://your-domain.com/endpoints/party_answers.php?party_id=1
Expected: JSON response (success or error)
If fails: Check web server configuration and PHP errors</div>
<p><strong>Check 2: Database Connection</strong></p>
<div class="code-block">Test: Check database credentials in API config
Verify: Database server is running
Check: Database user has proper permissions</div>
<p><strong>Check 3: Network Issues</strong></p>
<div class="code-block">Test: Ping API server from CMS server
Verify: Firewall allows communication
Check: SSL certificates are valid (if using HTTPS)</div>
<h3>12.4 Performance Issues</h3>
<h4>12.4.1 Symptoms</h4>
<ul>
<li>Page loads slowly</li>
<li>Position calculation takes excessive time</li>
</ul>
<h4>12.4.2 Solutions</h4>
<p><strong>Solution 1: Verify Indexes</strong></p>
<div class="code-block">Check: SHOW INDEX FROM party_answers
Verify: Indexes exist on (party_id, quiz_attempt_id)
Add if missing: CREATE INDEX idx_party_attempt ON party_answers(party_id, quiz_attempt_id)</div>
<p><strong>Solution 2: Check Query Performance</strong></p>
<div class="code-block">Test: EXPLAIN SELECT statement
Look for: "Using index" in Extra column
Avoid: "Using filesort" or "Using temporary"</div>
<p><strong>Solution 3: Enable Query Caching</strong></p>
<div class="code-block">Check: SHOW VARIABLES LIKE 'query_cache%'
Enable: query_cache_type = 1
Size: query_cache_size = 16M (adjust as needed)</div>
<div class="page-break"></div>
<h2>Conclusion</h2>
<p>This documentation provides a complete technical reference for the Party Position Calculation System. The system successfully automates the process of determining political positions based on quiz responses, using a sophisticated four-axis model with weighted scoring.</p>
<h3>Key Takeaways</h3>
<ul>
<li><strong>Objectivity:</strong> The mathematical algorithm ensures consistent, bias-free position assignments</li>
<li><strong>Nuance:</strong> The four-axis model captures both economic and social dimensions of political thought</li>
<li><strong>Flexibility:</strong> The 50% threshold rule ensures secondary axes are only assigned when genuinely warranted</li>
<li><strong>Performance:</strong> Position caching and optimized queries ensure fast page loads</li>
<li><strong>Security:</strong> Multi-layered authentication and validation protect against unauthorized access and data corruption</li>
</ul>
<h3>System Summary</h3>
<table>
<tr>
<th>Component</th>
<th>Technology</th>
<th>Purpose</th>
</tr>
<tr>
<td>Calculation Engine</td>
<td>PHP API</td>
<td>Processes quiz answers and calculates positions</td>
</tr>
<tr>
<td>Data Storage</td>
<td>MySQL/MariaDB</td>
<td>Stores questions, answers, and calculated positions</td>
</tr>
<tr>
<td>Presentation Layer</td>
<td>PHP CMS</td>
<td>Displays positions to party users</td>
</tr>
<tr>
<td>Communication</td>
<td>RESTful API</td>
<td>Connects CMS to calculation engine</td>
</tr>
</table>
<h3>Support and Maintenance</h3>
<p>For technical support, system updates, or questions about this documentation, please contact the development team.</p>
<p><strong>Document Version:</strong> 1.0.0<br>
<strong>Last Updated:</strong> October 15, 2025<br>
<strong>Next Review Date:</strong> January 15, 2026</p>
<script src="assets/js/toc.js"></script>
<script src="assets/js/search.js"></script>
</body>
</html>