Committee System in Danish Parliamentary OData API¶
The Danish Parliament (Folketinget) operates through a comprehensive committee system that handles the detailed examination of legislation, oversight functions, and specialized policy areas. This document provides a complete guide to understanding and working with committee data in the Parliamentary OData API.
Table of Contents¶
- Danish Parliamentary Committee System Overview
- Committee Types and Hierarchical Structure
- Committee Membership and Role Assignments
- Committee Meeting Patterns and Scheduling
- Case Assignment and Committee Workflows
- Committee Document Production and Publication
- Committee Voting and Recommendation Processes
- Inter-committee Relationships and Coordination
- Committee Performance Metrics and Analysis
- API Integration for Committee System Modeling
Danish Parliamentary Committee System Overview¶
The Danish Parliamentary committee system is the backbone of legislative work in the Folketinget. Committees examine bills in detail, conduct oversight of government ministries, and provide specialized expertise on policy areas.
Key Characteristics¶
- Specialized Focus: Each committee handles specific policy domains
- Cross-party Representation: Committee membership reflects parliamentary party strengths
- Legislative Function: All bills are referred to relevant committees for detailed examination
- Oversight Role: Committees monitor government ministries and agencies
- Public Transparency: Most committee meetings are open with published agendas and reports
Committee System in the API¶
In the Parliamentary OData API, committees are represented as Aktør entities with typeid = 3 (Udvalg). This classification allows committees to:
- Participate in cases through SagAktør relationships
- Hold meetings recorded in Møde entities
- Produce documents through DokumentAktør relationships
- Make recommendations through Afstemning (voting) records
Committee Types and Hierarchical Structure¶
Standing Committees (Faste Udvalg)¶
The Danish Parliament has approximately 25-30 standing committees that handle ongoing legislative and oversight work:
Major Policy Committees¶
# Get all active committees
curl "https://oda.ft.dk/api/Aktør?%24filter=typeid%20eq%203%20and%20slutdato%20gt%20datetime'2024-01-01'&%24orderby=navn"
Key Standing Committees:
- Finansudvalget (FIU) - Finance Committee
- Udenrigsudvalget (URU) - Foreign Affairs Committee
- Europaudvalget (EUU) - European Affairs Committee
- Retsudvalget (REU) - Legal Affairs Committee
- Forsvarsudvalget (FOU) - Defense Committee
- Børne- og Undervisningsudvalget (BUU) - Children and Education Committee
- Erhvervs-, Vækst- og Eksportudvalget (ERU) - Business, Growth and Export Committee
- Skatteudvalget (SAU) - Tax Committee
- Grønlandsudvalget (GRU) - Greenland Committee
Committee Naming Convention¶
Each committee has: - gruppenavnkort: 3-letter abbreviation (e.g., "FIU", "EUU") - navn: Full committee name in Danish - periodeid: Parliamentary session identifier
Specialized Committees¶
Some committees handle specific types of cases:
- Control Committees: Oversight of government agencies
- Ad Hoc Committees: Temporary committees for specific issues
- Joint Committees: Cross-parliamentary collaborations
Committee Hierarchy and Relationships¶
// Get committee structure with relationships
async function getCommitteeStructure() {
const response = await fetch(
'https://oda.ft.dk/api/Aktør?' +
'$filter=typeid eq 3&' +
'$expand=AktørAktør/Aktør&' +
'$orderby=gruppenavnkort'
);
return await response.json();
}
Committee Membership and Role Assignments¶
Committee Member Roles¶
Committee participation is tracked through several relationship entities:
SagAktør (Case-Actor) Relationships¶
Politicians participate in committee work through case assignments:
# Get committee members for a specific case
curl "https://oda.ft.dk/api/SagAktør?%24expand=Aktør&%24filter=sagid%20eq%20102903%20and%20Aktör/typeid%20eq%205"
MødeAktør (Meeting-Actor) Relationships¶
Meeting participation is tracked for each committee session:
// Get committee meeting attendance
async function getCommitteeMeetingAttendance(meetingId) {
const response = await fetch(
`https://oda.ft.dk/api/MødeAktör?$expand=Aktör&$filter=mødeid eq ${meetingId}`
);
return await response.json();
}
Role Types in Committee Work¶
The API tracks different roles through SagAktørRolle and other role entities:
- Committee Chairman (Formand)
- Committee Vice-Chairman (Næstformand)
- Regular Members (Medlemmer)
- Substitute Members (Suppleanter)
- Expert Advisors (Sagkyndige)
Committee Membership Analysis¶
import requests
def analyze_committee_membership(committee_id, period_id):
"""
Analyze committee membership patterns over time
"""
# Get committee cases
cases_url = f"https://oda.ft.dk/api/SagAktör?$expand=Sag,Aktör&$filter=aktörid eq {committee_id}"
cases_response = requests.get(cases_url)
cases_data = cases_response.json()
# Analyze member participation
member_participation = {}
for case_actor in cases_data['value']:
if case_actor['Aktör']['typeid'] == 5: # Person
member_id = case_actor['aktörid']
if member_id not in member_participation:
member_participation[member_id] = {
'name': case_actor['Aktör']['navn'],
'cases': []
}
member_participation[member_id]['cases'].append(case_actor['Sag'])
return member_participation
Committee Meeting Patterns and Scheduling¶
Meeting Types and Scheduling¶
Committee meetings are recorded in the Møde entity with specific characteristics:
Meeting Categories¶
- Regular Committee Meetings (
typeid = 2) - Plenary Sessions (
typeid = 1) - Special Hearings
- Joint Committee Meetings
Meeting Scheduling Patterns¶
# Get upcoming committee meetings for next 30 days
curl "https://oda.ft.dk/api/Møde?%24expand=MødeAktör/Aktör&%24filter=dato%20gt%20datetime'$(date -u +%Y-%m-%d)T00:00:00'%20and%20dato%20lt%20datetime'$(date -u -d '+30 days' +%Y-%m-%d)T00:00:00'&%24orderby=dato"
Regular Meeting Patterns¶
Based on API data analysis:
- European Affairs Committee (EUU): Meets bi-weekly (typically Thursday mornings)
- Finance Committee (FIU): Regular weekly meetings during budget season
- Legal Affairs Committee (REU): Meetings scheduled around legislative calendars
Meeting Data Structure¶
{
"id": 15170,
"titel": "Møde i Europaudvalget",
"lokale": "",
"nummer": null,
"dagsordenurl": "",
"starttidsbemærkning": "KL. 08.30",
"offentlighedskode": "O",
"dato": "2025-09-19T08:30:00",
"statusid": 1,
"typeid": 2,
"periodeid": 163,
"opdateringsdato": "2025-09-09T14:46:02.39",
"MødeAktør": [
{
"Aktör": {
"id": 20951,
"typeid": 3,
"gruppenavnkort": "EUU",
"navn": "Europaudvalget"
}
}
]
}
Advanced Meeting Analysis¶
class CommitteeMeetingAnalyzer {
constructor(baseUrl = 'https://oda.ft.dk/api/') {
this.baseUrl = baseUrl;
}
async getCommitteeSchedule(committeeId, startDate, endDate) {
const filter = `MødeAktör/any(ma: ma/aktörid eq ${committeeId}) and ` +
`dato ge datetime'${startDate}' and dato le datetime'${endDate}'`;
const params = new URLSearchParams({
'$filter': filter,
'$expand': 'MødeAktör/Aktör,Dagsordenspunkt',
'$orderby': 'dato'
});
const response = await fetch(`${this.baseUrl}Møde?${params}`);
return await response.json();
}
async analyzeCommitteeMeetingPatterns(committeeId, months = 12) {
const endDate = new Date();
const startDate = new Date();
startDate.setMonth(endDate.getMonth() - months);
const meetings = await this.getCommitteeSchedule(
committeeId,
startDate.toISOString().split('T')[0],
endDate.toISOString().split('T')[0]
);
return {
totalMeetings: meetings.value.length,
averagePerMonth: meetings.value.length / months,
meetingDays: this.analyzeMeetingDays(meetings.value),
meetingTimes: this.analyzeMeetingTimes(meetings.value)
};
}
analyzeMeetingDays(meetings) {
const dayCount = {};
meetings.forEach(meeting => {
const day = new Date(meeting.dato).getDay();
dayCount[day] = (dayCount[day] || 0) + 1;
});
return dayCount;
}
analyzeMeetingTimes(meetings) {
const timeCount = {};
meetings.forEach(meeting => {
const hour = new Date(meeting.dato).getHours();
timeCount[hour] = (timeCount[hour] || 0) + 1;
});
return timeCount;
}
}
Case Assignment and Committee Workflows¶
Legislative Process and Committee Role¶
Every bill (Lovforslag) and most parliamentary cases are assigned to relevant committees for detailed examination.
Case Assignment Process¶
- Initial Referral: Cases assigned after first reading
- Committee Examination: Detailed study and hearing process
- Report Production: Committee produces recommendation
- Parliamentary Return: Case returns to plenary with committee recommendation
Tracking Committee Case Assignments¶
# Get all cases assigned to Finance Committee
curl "https://oda.ft.dk/api/SagAktör?%24expand=Sag,Aktör&%24filter=aktörid%20eq%201%20and%20Aktör/typeid%20eq%203"
Case Types Handled by Committees¶
Different committees handle different types of parliamentary business:
def analyze_committee_caseload():
"""
Analyze the types of cases handled by different committees
"""
import requests
# Get committee case assignments
url = "https://oda.ft.dk/api/SagAktör?$expand=Sag,Aktör&$filter=Aktör/typeid eq 3"
response = requests.get(url)
data = response.json()
committee_caseload = {}
for assignment in data['value']:
committee_name = assignment['Aktör']['navn']
case_type = assignment['Sag']['typeid']
if committee_name not in committee_caseload:
committee_caseload[committee_name] = {}
if case_type not in committee_caseload[committee_name]:
committee_caseload[committee_name][case_type] = 0
committee_caseload[committee_name][case_type] += 1
return committee_caseload
Committee Workflow Stages¶
The API tracks different stages of committee work through Sagstrin (case steps):
- Henvist til udvalg - Referred to committee
- Udvalgsbehandling - Committee consideration
- Betænkning afgivet - Committee report submitted
- Udvalgsindstilling - Committee recommendation
# Track a case through committee process
curl "https://oda.ft.dk/api/Sagstrin?%24expand=Sag&%24filter=sagid%20eq%20102903&%24orderby=dato"
Committee Document Production and Publication¶
Committee Document Types¶
Committees produce various types of documents tracked in the API:
Primary Document Categories¶
- Betænkninger - Committee reports
- Indstillinger - Committee recommendations
- Spørgsmål og svar - Questions and answers
- Høringssvar - Hearing responses
- Arbejdsdokumenter - Working documents
Document Production Workflow¶
async function getCommitteeDocuments(committeeId, documentType = null) {
let filter = `DokumentAktör/any(da: da/aktörid eq ${committeeId})`;
if (documentType) {
filter += ` and typeid eq ${documentType}`;
}
const params = new URLSearchParams({
'$filter': filter,
'$expand': 'DokumentAktör/Aktör,Fil',
'$orderby': 'dato desc'
});
const response = await fetch(`https://oda.ft.dk/api/Dokument?${params}`);
return await response.json();
}
Document Publication Analysis¶
class CommitteeDocumentTracker:
def __init__(self):
self.base_url = "https://oda.ft.dk/api/"
def track_document_production(self, committee_id, start_date, end_date):
"""
Track document production by committee over time period
"""
filter_clause = (
f"DokumentAktör/any(da: da/aktörid eq {committee_id}) and "
f"dato ge datetime'{start_date}' and dato le datetime'{end_date}'"
)
params = {
'$filter': filter_clause,
'$expand': 'DokumentAktør/Aktör,Fil',
'$orderby': 'dato desc',
'$inlinecount': 'allpages'
}
# Implementation would make API call and analyze results
return self.analyze_document_timeline(params)
def analyze_document_timeline(self, params):
"""
Analyze document production patterns
"""
# Analyze document types, timing, and frequency
pass
def get_document_download_stats(self, document_id):
"""
Get file download information for committee documents
"""
url = f"{self.base_url}Fil?$filter=dokumentid eq {document_id}"
# Implementation would track file access patterns
pass
Committee Voting and Recommendation Processes¶
Committee Voting Records¶
Committee recommendations and decisions are recorded through the Afstemning (voting) entity:
Voting Types in Committee Context¶
- Udvalgsindstilling (
typeid = 2) - Committee recommendations - Ændringsforslag (
typeid = 4) - Amendment proposals - Endelig vedtagelse (
typeid = 1) - Final adoption votes
Committee Recommendation Analysis¶
# Get committee recommendations for recent cases
curl "https://oda.ft.dk/api/Afstemning?%24expand=Stemme/Aktör&%24filter=typeid%20eq%202&%24orderby=opdateringsdato%20desc&%24top=10"
Voting Patterns in Committees¶
class CommitteeVotingAnalyzer {
async getCommitteeVotingRecord(committeeId, periodId) {
// Get cases handled by committee
const casesResponse = await fetch(
`https://oda.ft.dk/api/SagAktör?` +
`$expand=Sag/Afstemning/Stemme/Aktör&` +
`$filter=aktörid eq ${committeeId}`
);
const cases = await casesResponse.json();
return this.analyzeVotingPatterns(cases.value);
}
analyzeVotingPatterns(cases) {
const patterns = {
unanimous: 0,
majority: 0,
split: 0,
partyLineVotes: 0
};
cases.forEach(caseData => {
if (caseData.Sag && caseData.Sag.Afstemning) {
caseData.Sag.Afstemning.forEach(vote => {
if (vote.Stemme && vote.Stemme.length > 0) {
patterns = this.categorizeVote(vote.Stemme, patterns);
}
});
}
});
return patterns;
}
categorizeVote(votes, patterns) {
const voteTypes = votes.map(v => v.typeid);
const uniqueVotes = [...new Set(voteTypes)];
if (uniqueVotes.length === 1) {
patterns.unanimous++;
} else if (uniqueVotes.length === 2) {
patterns.majority++;
} else {
patterns.split++;
}
return patterns;
}
}
Committee Recommendation Impact¶
Track how often committee recommendations are followed by the full parliament:
def analyze_committee_influence():
"""
Analyze how often parliamentary votes follow committee recommendations
"""
import requests
# Get committee recommendations and subsequent parliamentary votes
committee_recs_url = "https://oda.ft.dk/api/Afstemning?$expand=Stemme&$filter=typeid eq 2"
final_votes_url = "https://oda.ft.dk/api/Afstemning?$expand=Stemme&$filter=typeid eq 1"
# Compare recommendation outcomes with final votes
# Implementation would track correlation between committee and plenary
return {
'recommendation_success_rate': 0.0,
'dissent_cases': [],
'unanimous_recommendations': []
}
Inter-committee Relationships and Coordination¶
Joint Committee Work¶
Some complex cases require coordination between multiple committees:
Cross-Committee Case Handling¶
# Find cases with multiple committee involvement
curl "https://oda.ft.dk/api/Sag?%24expand=SagAktör/Aktör&%24filter=SagAktör/any(sa:%20sa/Aktör/typeid%20eq%203)&%24top=10" | jq '.value[] | select(.SagAktör | length > 1)'
Committee Relationship Mapping¶
async function mapInterCommitteeRelationships() {
// Get all cases with multiple committee involvement
const response = await fetch(
'https://oda.ft.dk/api/Sag?' +
'$expand=SagAktör/Aktör&' +
'$filter=SagAktör/any(sa: sa/Aktör/typeid eq 3)&' +
'$top=1000'
);
const cases = await response.json();
const relationships = {};
cases.value.forEach(case_ => {
const committees = case_.SagAktör
.filter(sa => sa.Aktör && sa.Aktör.typeid === 3)
.map(sa => sa.Aktör.gruppenavnkort);
if (committees.length > 1) {
committees.forEach((c1, i) => {
committees.slice(i + 1).forEach(c2 => {
const key = [c1, c2].sort().join('-');
relationships[key] = (relationships[key] || 0) + 1;
});
});
}
});
return relationships;
}
Committee Coordination Patterns¶
- Lead Committee: Primary responsibility with input from others
- Joint Hearings: Multiple committees meeting together
- Sequential Review: Cases passing between related committees
- Parallel Processing: Different aspects handled simultaneously
Committee Performance Metrics and Analysis¶
Key Performance Indicators¶
Case Processing Metrics¶
class CommitteePerformanceAnalyzer:
def __init__(self, api_base="https://oda.ft.dk/api/"):
self.api_base = api_base
def calculate_processing_time(self, committee_id, period_id):
"""
Calculate average time from case referral to committee report
"""
# Get committee cases with timestamps
cases_data = self.get_committee_cases(committee_id, period_id)
processing_times = []
for case in cases_data:
referral_date = self.get_referral_date(case['id'])
report_date = self.get_report_date(case['id'])
if referral_date and report_date:
processing_time = (report_date - referral_date).days
processing_times.append(processing_time)
return {
'average_days': sum(processing_times) / len(processing_times),
'median_days': sorted(processing_times)[len(processing_times)//2],
'max_days': max(processing_times),
'min_days': min(processing_times)
}
def analyze_committee_productivity(self, committee_id, period_id):
"""
Comprehensive productivity analysis
"""
return {
'cases_processed': self.count_cases_processed(committee_id, period_id),
'documents_produced': self.count_documents_produced(committee_id, period_id),
'meetings_held': self.count_meetings_held(committee_id, period_id),
'processing_efficiency': self.calculate_processing_time(committee_id, period_id),
'recommendation_success_rate': self.calculate_success_rate(committee_id, period_id)
}
def compare_committee_performance(self, committee_ids, period_id):
"""
Compare performance across multiple committees
"""
comparison = {}
for committee_id in committee_ids:
comparison[committee_id] = self.analyze_committee_productivity(
committee_id, period_id
)
return comparison
Workload Distribution Analysis¶
# Analyze committee workload distribution
curl "https://oda.ft.dk/api/SagAktör?%24expand=Aktör&%24filter=Aktör/typeid%20eq%203&%24inlinecount=allpages" | \
jq '[.value[] | .Aktör.gruppenavnkort] | group_by(.) | map({committee: .[0], cases: length}) | sort_by(.cases)'
Meeting Effectiveness Metrics¶
async function analyzeMeetingEffectiveness(committeeId, startDate, endDate) {
// Get meetings and associated decisions/documents
const meetings = await fetch(
`https://oda.ft.dk/api/Møde?` +
`$expand=MødeAktör,Dagsordenspunkt,Afstemning&` +
`$filter=MødeAktør/any(ma: ma/aktörid eq ${committeeId}) and ` +
`dato ge datetime'${startDate}' and dato le datetime'${endDate}'`
).then(r => r.json());
const effectiveness = {
totalMeetings: meetings.value.length,
decisionsPerMeeting: 0,
documentsPerMeeting: 0,
averageAgendaItems: 0
};
meetings.value.forEach(meeting => {
if (meeting.Dagsordenspunkt) {
effectiveness.averageAgendaItems += meeting.Dagsordenspunkt.length;
}
if (meeting.Afstemning) {
effectiveness.decisionsPerMeeting += meeting.Afstemning.length;
}
});
effectiveness.averageAgendaItems /= meetings.value.length;
effectiveness.decisionsPerMeeting /= meetings.value.length;
return effectiveness;
}
API Integration for Committee System Modeling¶
Complete Committee Management System¶
Here's a comprehensive example of building a committee management system using the Parliamentary OData API:
class ParliamentaryCommitteeSystem {
constructor(apiBase = 'https://oda.ft.dk/api/') {
this.apiBase = apiBase;
this.committees = new Map();
this.meetings = new Map();
this.cases = new Map();
}
// Initialize system with current committee data
async initialize(periodId = 163) {
await Promise.all([
this.loadCommittees(periodId),
this.loadRecentMeetings(),
this.loadActiveCases()
]);
}
async loadCommittees(periodId) {
const response = await fetch(
`${this.apiBase}Aktør?` +
`$filter=typeid eq 3 and periodeid eq ${periodId}&` +
`$expand=SagAktör/Sag,MødeAktör/Møde&` +
`$orderby=gruppenavnkort`
);
const data = await response.json();
data.value.forEach(committee => {
this.committees.set(committee.id, committee);
});
return this.committees;
}
async getCommitteeWorkload(committeeId) {
const committee = this.committees.get(committeeId);
if (!committee) return null;
return {
name: committee.navn,
abbreviation: committee.gruppenavnkort,
activeCases: committee.SagAktör ? committee.SagAktør.length : 0,
recentMeetings: committee.MødeAktør ? committee.MødeAktör.length : 0,
specialization: this.analyzeCommitteeSpecialization(committeeId),
efficiency: await this.calculateEfficiencyMetrics(committeeId)
};
}
analyzeCommitteeSpecialization(committeeId) {
const committee = this.committees.get(committeeId);
if (!committee || !committee.SagAktör) return {};
const caseTypes = {};
committee.SagAktör.forEach(sagAktor => {
if (sagAktor.Sag) {
const typeId = sagAktor.Sag.typeid;
caseTypes[typeId] = (caseTypes[typeId] || 0) + 1;
}
});
return caseTypes;
}
async calculateEfficiencyMetrics(committeeId) {
// Implementation would calculate:
// - Average case processing time
// - Meeting frequency and productivity
// - Document production rate
// - Recommendation success rate
return {
avgProcessingDays: 0,
meetingsPerMonth: 0,
documentsPerCase: 0,
recommendationSuccessRate: 0
};
}
// Real-time monitoring capabilities
async monitorCommitteeActivity(committeeId, callback) {
const lastUpdate = new Date();
setInterval(async () => {
const updates = await this.checkForUpdates(committeeId, lastUpdate);
if (updates.length > 0) {
callback(updates);
lastUpdate = new Date();
}
}, 60000); // Check every minute
}
async checkForUpdates(committeeId, since) {
const sinceStr = since.toISOString().replace('Z', '');
// Check for new cases
const newCases = await fetch(
`${this.apiBase}SagAktör?` +
`$expand=Sag&` +
`$filter=aktörid eq ${committeeId} and opdateringsdato gt datetime'${sinceStr}'`
).then(r => r.json());
// Check for new meetings
const newMeetings = await fetch(
`${this.apiBase}MødeAktør?` +
`$expand=Møde&` +
`$filter=aktörid eq ${committeeId} and opdateringsdato gt datetime'${sinceStr}'`
).then(r => r.json());
return {
newCases: newCases.value || [],
newMeetings: newMeetings.value || []
};
}
// Committee comparison and ranking
async rankCommitteesByActivity(metric = 'caseLoad') {
const rankings = [];
for (const [id, committee] of this.committees) {
const workload = await this.getCommitteeWorkload(id);
rankings.push({
id,
name: committee.navn,
abbreviation: committee.gruppenavnkort,
score: this.getMetricScore(workload, metric)
});
}
return rankings.sort((a, b) => b.score - a.score);
}
getMetricScore(workload, metric) {
switch (metric) {
case 'caseLoad': return workload.activeCases;
case 'meetings': return workload.recentMeetings;
case 'efficiency': return workload.efficiency.avgProcessingDays;
default: return workload.activeCases;
}
}
// Export committee data for external systems
async exportCommitteeData(format = 'json') {
const exportData = {
timestamp: new Date().toISOString(),
committees: Array.from(this.committees.values()),
summary: {
totalCommittees: this.committees.size,
totalActiveCases: 0,
totalMeetings: 0
}
};
// Calculate summaries
exportData.committees.forEach(committee => {
exportData.summary.totalActiveCases += committee.SagAktör?.length || 0;
exportData.summary.totalMeetings += committee.MødeAktör?.length || 0;
});
switch (format) {
case 'csv':
return this.convertToCSV(exportData);
case 'xml':
return this.convertToXML(exportData);
default:
return JSON.stringify(exportData, null, 2);
}
}
}
// Usage example
const committeeSystem = new ParliamentaryCommitteeSystem();
// Initialize and start monitoring
committeeSystem.initialize().then(() => {
console.log('Committee system initialized');
// Monitor specific committee activity
committeeSystem.monitorCommitteeActivity(20951, (updates) => {
console.log('Europaudvalget updates:', updates);
});
// Get workload analysis
committeeSystem.getCommitteeWorkload(1).then(workload => {
console.log('Finansudvalget workload:', workload);
});
// Rank committees by activity
committeeSystem.rankCommitteesByActivity('caseLoad').then(rankings => {
console.log('Most active committees:', rankings.slice(0, 5));
});
});
Committee Data Synchronization¶
For applications requiring real-time committee data:
import asyncio
import aiohttp
from datetime import datetime, timedelta
class CommitteeDataSync:
def __init__(self, api_base="https://oda.ft.dk/api/"):
self.api_base = api_base
self.last_sync = {}
self.change_callbacks = []
async def sync_committee_data(self, committee_ids=None):
"""
Synchronize committee data with local database/cache
"""
async with aiohttp.ClientSession() as session:
tasks = []
if committee_ids is None:
# Sync all committees
committee_ids = await self.get_active_committee_ids(session)
for committee_id in committee_ids:
tasks.append(self.sync_single_committee(session, committee_id))
results = await asyncio.gather(*tasks)
return results
async def sync_single_committee(self, session, committee_id):
"""
Sync data for a single committee
"""
last_update = self.last_sync.get(committee_id, datetime.min)
filter_date = last_update.isoformat().replace('+00:00', '')
# Sync committee cases
cases_url = (
f"{self.api_base}SagAktör?"
f"$expand=Sag,Aktör&"
f"$filter=aktörid eq {committee_id} and "
f"opdateringsdato gt datetime'{filter_date}'"
)
# Sync committee meetings
meetings_url = (
f"{self.api_base}MødeAktör?"
f"$expand=Møde&"
f"$filter=aktörid eq {committee_id} and "
f"opdateringsdato gt datetime'{filter_date}'"
)
cases_response = await session.get(cases_url)
meetings_response = await session.get(meetings_url)
cases_data = await cases_response.json()
meetings_data = await meetings_response.json()
changes = {
'committee_id': committee_id,
'new_cases': cases_data.get('value', []),
'new_meetings': meetings_data.get('value', []),
'timestamp': datetime.now()
}
self.last_sync[committee_id] = datetime.now()
# Notify callbacks of changes
for callback in self.change_callbacks:
await callback(changes)
return changes
def add_change_callback(self, callback):
"""
Add callback function to be notified of committee changes
"""
self.change_callbacks.append(callback)
async def continuous_sync(self, interval_minutes=5):
"""
Continuously sync committee data at specified interval
"""
while True:
try:
await self.sync_committee_data()
await asyncio.sleep(interval_minutes * 60)
except Exception as e:
print(f"Sync error: {e}")
await asyncio.sleep(30) # Short delay before retry
# Usage example
async def main():
sync_system = CommitteeDataSync()
# Add callback for handling changes
async def handle_committee_changes(changes):
print(f"Committee {changes['committee_id']} has {len(changes['new_cases'])} new cases")
print(f"Committee {changes['committee_id']} has {len(changes['new_meetings'])} new meetings")
sync_system.add_change_callback(handle_committee_changes)
# Start continuous synchronization
await sync_system.continuous_sync(interval_minutes=5)
# Run the sync system
# asyncio.run(main())
Committee Analytics Dashboard¶
Build a complete analytics dashboard for committee performance:
interface CommitteeMetrics {
id: number;
name: string;
abbreviation: string;
activeCases: number;
completedCases: number;
upcomingMeetings: number;
averageProcessingDays: number;
memberCount: number;
documentProductionRate: number;
recommendationSuccessRate: number;
}
interface CommitteeTrend {
date: string;
value: number;
metric: string;
}
class CommitteeAnalyticsDashboard {
private apiBase = 'https://oda.ft.dk/api/';
private cache = new Map<string, any>();
private cacheTTL = 300000; // 5 minutes
async getCommitteeMetrics(committeeId: number): Promise<CommitteeMetrics> {
const cacheKey = `metrics-${committeeId}`;
const cached = this.getCachedData(cacheKey);
if (cached) return cached;
const [caseData, meetingData, memberData] = await Promise.all([
this.fetchCommitteeCases(committeeId),
this.fetchCommitteeMeetings(committeeId),
this.fetchCommitteeMembers(committeeId)
]);
const metrics: CommitteeMetrics = {
id: committeeId,
name: caseData.committeeName,
abbreviation: caseData.committeeAbbr,
activeCases: caseData.activeCases,
completedCases: caseData.completedCases,
upcomingMeetings: meetingData.upcomingCount,
averageProcessingDays: this.calculateProcessingTime(caseData.cases),
memberCount: memberData.memberCount,
documentProductionRate: this.calculateDocumentRate(caseData.cases),
recommendationSuccessRate: this.calculateSuccessRate(caseData.cases)
};
this.setCachedData(cacheKey, metrics);
return metrics;
}
async getCommitteeTrends(
committeeId: number,
metric: string,
days: number = 30
): Promise<CommitteeTrend[]> {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(endDate.getDate() - days);
// Fetch historical data and calculate trends
const trends: CommitteeTrend[] = [];
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
const dateStr = d.toISOString().split('T')[0];
const value = await this.getMetricForDate(committeeId, metric, dateStr);
trends.push({
date: dateStr,
value,
metric
});
}
return trends;
}
async generateCommitteeReport(committeeId: number): Promise<string> {
const metrics = await this.getCommitteeMetrics(committeeId);
const trends = await this.getCommitteeTrends(committeeId, 'caseLoad', 90);
return `
# Committee Performance Report: ${metrics.name}
## Overview
- **Active Cases**: ${metrics.activeCases}
- **Completed Cases**: ${metrics.completedCases}
- **Average Processing Time**: ${metrics.averageProcessingDays} days
- **Success Rate**: ${(metrics.recommendationSuccessRate * 100).toFixed(1)}%
## Recent Activity
${this.generateTrendAnalysis(trends)}
## Recommendations
${this.generateRecommendations(metrics, trends)}
`;
}
private async fetchCommitteeCases(committeeId: number) {
const response = await fetch(
`${this.apiBase}SagAktör?` +
`$expand=Sag,Aktør&` +
`$filter=aktörid eq ${committeeId}`
);
const data = await response.json();
return {
committeeName: data.value[0]?.Aktør?.navn || '',
committeeAbbr: data.value[0]?.Aktør?.gruppenavnkort || '',
cases: data.value,
activeCases: data.value.filter((c: any) => c.Sag?.statusid === 1).length,
completedCases: data.value.filter((c: any) => c.Sag?.statusid === 3).length
};
}
private async fetchCommitteeMeetings(committeeId: number) {
const response = await fetch(
`${this.apiBase}MødeAktör?` +
`$expand=Møde&` +
`$filter=aktörid eq ${committeeId} and ` +
`Møde/dato gt datetime'${new Date().toISOString()}'`
);
const data = await response.json();
return {
upcomingCount: data.value.length,
meetings: data.value
};
}
private async fetchCommitteeMembers(committeeId: number) {
// Implementation would fetch committee membership data
return { memberCount: 0 };
}
private calculateProcessingTime(cases: any[]): number {
// Implementation would calculate average case processing time
return 0;
}
private calculateDocumentRate(cases: any[]): number {
// Implementation would calculate document production rate
return 0;
}
private calculateSuccessRate(cases: any[]): number {
// Implementation would calculate recommendation success rate
return 0;
}
private async getMetricForDate(committeeId: number, metric: string, date: string): Promise<number> {
// Implementation would fetch specific metric for date
return 0;
}
private generateTrendAnalysis(trends: CommitteeTrend[]): string {
// Implementation would analyze trends and generate narrative
return "Trend analysis would be generated here";
}
private generateRecommendations(metrics: CommitteeMetrics, trends: CommitteeTrend[]): string {
// Implementation would generate actionable recommendations
return "Recommendations would be generated here";
}
private getCachedData(key: string): any {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
return cached.data;
}
return null;
}
private setCachedData(key: string, data: any): void {
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
}
Summary¶
The Danish Parliamentary committee system is comprehensively modeled in the OData API through interconnected entities that track:
- Committee Structure: All standing and specialized committees with hierarchical relationships
- Meeting Patterns: Regular scheduling, attendance, and agenda management
- Case Processing: Legislative workflow from referral through recommendation
- Document Production: Committee reports, recommendations, and working documents
- Voting Records: Committee recommendations and their parliamentary impact
- Member Participation: Individual and party-level committee engagement
- Performance Metrics: Processing times, productivity, and success rates
The API provides real-time access to this data with UTF-8 support for Danish characters and comprehensive relationship modeling that enables sophisticated analysis of parliamentary committee operations. This makes it an invaluable resource for researchers, journalists, civic organizations, and technology developers building democratic transparency tools.
The examples provided demonstrate how to build comprehensive committee management systems, real-time monitoring applications, and analytical dashboards that can process and visualize the complex relationships and workflows within Denmark's parliamentary committee system.