*/ class ParTCP_Votings { public $fileSystem; public $baseDir; public $dataDir; public $eventsClass; public function __construct( $fileSystem, $dataDir ){ $this->fileSystem = $fileSystem; $this->dataDir = $dataDir; } public function set_base_dir( $baseDir ){ $this->baseDir = trim( $baseDir, '/' ); } public function get_dir( $id ){ return ( $this->baseDir ? "{$this->baseDir}/" : '' ) . "votings/{$id}"; } public function get_list( $status = NULL, $sortColumn = NULL, $sortDesc = FALSE ){ $dir = ( $this->baseDir ? "{$this->baseDir}/" : '' ) . 'votings'; $dirList = $this->fileSystem->get_listing( $dir ); $votings = array (); foreach ( $dirList as $item ){ if ( ! ( $data = $this->get_data( $item ) ) ){ continue; } if ( empty( $status ) || $status == 'all' || in_array( $data['status'], (array) $status ) ){ $votings[] = $data; } } if ( $votings && $sortColumn ){ $sortValues = array_column( $votings, $sortColumn ); $sortValues = array_pad( $sortValues, count( $votings ), NULL ); $sortOrder = $sortDesc ? SORT_DESC : SORT_ASC; array_multisort( $sortValues, $sortOrder, $votings ); } return $votings; } public function count(){ $votingList = $this->get_list(); $stats = [ 'idle' => 0, 'open' => 0, 'closed' => 0, 'finished' => 0 ]; foreach ( $votingList as $voting ){ $stats[ $voting['status'] ]++; } return $stats; } public function get_data( $id ){ $dir = $this->get_dir( $id ); if ( ! $this->fileSystem->exists( $dir ) ){ return FALSE; } $msg = $this->fileSystem->get_recent_contents( $dir, '[0-9]*' ); if ( $msg ){ $receipt = yaml_parse( $msg ); $data = $receipt['Voting-Data'] ?? FALSE; } else { $msg = $this->fileSystem->get_contents( "{$dir}/voting-definition" ); if ( $msg ){ $receipt = yaml_parse( $msg ); $data = $receipt['Voting-Data'] ?? FALSE; } } if ( empty( $data ) ){ return FALSE; } if ( ! empty( $data['link_target'] ) ){ $oldBaseDir = $this->baseDir; $this->set_base_dir( $this->eventsClass->get_dir( $data['link_target'] ) ); $targetData = $this->get_data( $id ); $this->baseDir = $oldBaseDir; unset( $targetData['event_id'], $targetData['administrable'], $targetData['voting_result'] ); $data = array_merge( $data, $targetData ); } else { $this->update_calculated_fields( $data ); } // Count ballots if ( is_object( $GLOBALS['Counter'] ) && $data['status'] != 'idle' ){ $GLOBALS['Counter']->set_base_dir( $dir ); $data['ballots_count'] = $GLOBALS['Counter']->get_value('ballots'); $data['ballots_received'] = $GLOBALS['Counter']->get_value('ballots_received'); } return $data; } public function purge_data( $data ){ $validKeys = [ 'id' => 0, 'name' => 0, 'type' => 0, 'title' => 0, 'short_description' => 0, 'description' => 0, 'link_url' => 0, 'sequence_number' => 0, 'prerequisites' => 0, 'selection_limit' => 0, 'single_vote_only' => 0, 'segments' => 0, 'confirm_segment_results' => 0, 'period_start' => 0, 'period_end' => 0, 'options' => 0, 'comment_rules' => 0, 'client_data' => 0 ]; return array_intersect_key( $data, $validKeys ); } public function update_calculated_fields( &$data ){ $now = time(); $start = empty( $data['period_start'] ) ? NULL : strtotime( $data['period_start'] ); $end = empty( $data['period_end'] ) ? NULL : strtotime( $data['period_end'] ); if ( ! $start || $start > $now ){ $data['status'] = 'idle'; } elseif ( ! $end || $end > $now ){ $data['status'] = 'open'; } elseif ( empty( $data['voting_result'] ) ){ $data['status'] = 'closed'; } else { $data['status'] = 'finished'; } } public function count_ballots( $votingId ){ $dir = $this->get_dir( $votingId ) . '/ballots'; $dirList = $this->fileSystem->get_listing( $dir ); return count( $dirList ?: [] ); } public function create_interim_count_confirmation( $votingId, $localId, $count ){ $votingDir = $this->get_dir( $votingId ); $dir = "{$votingDir}/interim"; $mtime = $this->fileSystem->get_mtime( $dir ); if ( $mtime && time() - $mtime <= 5 ){ return; } require_once 'lib/locker/locker.class.php'; $locker = new Locker( "{$this->dataDir}/{$votingDir}" ); if ( $locker->get_lock( 'interim', 5, 1 ) ){ $msg = new ParTCP_Outgoing_Message( NULL, $localId, 'interim-count-confirmation' ); $msg->set( 'Ballots-Received', $count ); $fileName = date('Ymd-His') . '-interim-count-confirmation'; $this->fileSystem->put_contents( "{$dir}/{$fileName}", $msg->dump( TRUE ) ); $locker->release_lock('interim'); } } public function count_votes( $votingId ){ $votingData = $this->get_data( $votingId ); if ( ! $votingData ){ throw new Exception('Unknown voting'); } $participantCount = 0; $invalidVoteCount = 0; $result = array (); $ballotsDir = $this->get_dir( $votingId ) . '/ballots'; $dirList = $this->fileSystem->get_listing( $ballotsDir ); set_time_limit(0); foreach ( $dirList as $participant ){ if ( $participant[0] == '.' ){ continue; } $participantCount++; $votes = $this->get_participant_votes( $ballotsDir, $participant ); if ( ! $votes || ! $this->votes_are_valid( $votes, $votingData ) ){ $invalidVoteCount++; continue; } foreach ( $votes as $vote ){ if ( empty( $vote['vote'] ) && $vote['vote'] !== 0 ){ continue; } $idxOption = 'option:' . $vote['id']; $idxVote = 'vote:' . $vote['vote']; if ( empty( $result[ $idxOption ][ $idxVote ] ) ){ $result[ $idxOption ][ $idxVote ] = 1; } else { $result[ $idxOption ][ $idxVote ]++; } } } return array ( 'participants' => $participantCount, 'invalid' => $invalidVoteCount, 'options' => $result ); } public function get_comments( $votingId ){ $result = [ 'general' => [], 'options' => [] ]; $ballotsDir = $this->get_dir( $votingId, TRUE ) . '/ballots'; $dirList = $this->fileSystem->get_listing( $ballotsDir ); set_time_limit(0); foreach ( $dirList as $participant ){ if ( $participant[0] == '.' ){ continue; } $comments = $this->get_participant_comments( $ballotsDir, $participant ); if ( ! $comments ){ continue; } $result = array_merge_recursive( $result, $comments ); } foreach ( $result['options'] as $key => $option ){ ksort( $option, SORT_NATURAL ); $result['options'][ $key ] = $option; } ksort( $result['options'] ); return $result; } public function get_all_votes( $votingId ){ $result = []; $ballotsDir = $this->get_dir( $votingId, TRUE ) . '/ballots'; $dirList = $this->fileSystem->get_listing( $ballotsDir ); set_time_limit(0); foreach ( $dirList as $participant ){ if ( $participant[0] == '.' ){ continue; } $votes = $this->get_participant_votes( $ballotsDir, $participant ); if ( ! $votes ){ continue; } $result[ $participant ] = $votes; } return $result; } public function get_participant_segments( $eventData, $ptcpId ){ $segments = []; $votes = []; foreach( $eventData['segments'] ?? [] as $segmentId => $segmentDef ){ $votingId = $segmentDef['voting_id']; $optionId = $segmentDef['option_id']; if ( ! isset( $votes[ $votingId ] ) ){ $ballotsDir = "{$this->get_dir( $votingId )}/ballots"; $ptcpVotes = $this->get_participant_votes( $ballotsDir, $ptcpId ); $votes[ $votingId ] = $ptcpVotes ? array_column( $ptcpVotes, 'data', 'id' ) : []; } $segments[ $segmentId ] = $votes[ $votingId ][ $optionId ] ?? NULL; } return $segments; } public function has_participant_voted( $votingId, $ptcpId ){ $dir = $this->get_dir( $votingId ) . "/ballots/{$ptcpId}"; return $this->fileSystem->is_not_empty( $dir ); } public function get_segment_results( $eventData, $votingData, $ptcpId ){ $segmentId = $votingData['confirm_segment_results']; $ptcpSegments = $this->get_participant_segments( $eventData, $ptcpId ); if ( empty( $ptcpSegments[ $segmentId ] ) ){ return NULL; } $dir = "{$this->dataDir}/event_segments/{$eventData['shortcode']}/" . "{$segmentId}/{$ptcpSegments[ $segmentId ]}"; $list = glob( "{$dir}/*" ) OR []; // collect voting options to be included in segment results //$options = array_filter( $votingData['options'], function( $o ){ // return ! empty( $o['confirm_segment_results'] ); //}); //$optionsIds = array_column( $options, 'id' ); $options = array_combine( array_column( $votingData['options'], 'id' ), $votingData['options'] ); // compile result sets $sets = []; $allVotes = []; //$setIdx = 97; // ASCII value of letter 'a' $i = 0; foreach ( $list as $path ){ //for ( $i = 0; $i < count( $list ); $i++ ){ $setIdx = chr( $i + 97 ); // 97 = ASCII value of letter 'a' //$ptcpId = basename( $list[ $i ] ); $ptcpId = basename( $path ); $votingDir = $this->get_dir( $votingData['id'] ); $votes = $this->get_participant_votes( "{$votingDir}/ballots", $ptcpId ); if ( ! $votes ){ continue; } //$votes = array_filter( $votes, function( $v ) use( $optionsIds ){ // return in_array( $v['id'], $optionsIds ); //}); //array_walk( $votes, function( &$v ) use( $optionsIds ){ // if ( ! in_array( $v['id'], $optionsIds ) ){ // $v['data'] = NULL; // } //}); array_walk( $votes, function( &$vote ) use( $options, $i ){ $rules = $options[ $vote['id'] ]['confirm_segment_results_rules'] ?? []; //var_dump( $rules ); foreach ( $rules as $rule ){ if ( $rule['round'] <= $i + 1 ){ $vote['data'] = $rule['value']; } } }); $keys = array_keys( $allVotes, $votes ); if ( $keys ){ array_push( $sets[ $keys[0] ], $ptcpId ); } else { $sets[ $setIdx ] = [ $ptcpId ]; $allVotes[ $setIdx ] = $votes; } $i++; } //var_dump( $allVotes ); // compile result options $options = []; for ( $i = 0; $i < count( $allVotes ); $i++ ){ $setIdx = chr( $i + 97 ); $votes = $allVotes[ $setIdx ] ?? []; foreach ( $votes as $key => $vote ){ if ( ! isset( $vote['vote'] ) || is_null( $vote['vote'] ) ){ continue; } $idxOption = "option:{$vote['id']}"; $idxSet = "set:{$setIdx}"; $options[ $idxOption ][ $idxSet ] = $vote['vote']; } } return [ 'result_sets' => $sets, 'options' => $options ]; } private function get_participant_votes( $ballotsDir, $ptcpId ){ $dir = "{$ballotsDir}/{$ptcpId}"; $listing = $this->fileSystem->get_listing( $dir, 'ballot*', TRUE ); foreach ( $listing as $fileName ){ $receipt = yaml_parse( $this->fileSystem->get_contents( "{$dir}/{$fileName}" ) ); if ( empty( $receipt['Original-Message'] ) || ! empty( $receipt['Error'] ) ){ continue; } $ballot = yaml_parse( $receipt['Original-Message'] ); if ( empty( $ballot['Votes'] ) ){ continue; } return $ballot['Votes']; } return FALSE; } private function get_participant_comments( $ballotsDir, $ptcpId ){ $dir = "{$ballotsDir}/{$ptcpId}"; $listing = $this->fileSystem->get_listing( $dir, 'ballot*', TRUE ); foreach ( $listing as $fileName ){ $receipt = yaml_parse( $this->fileSystem->get_contents( "{$dir}/{$fileName}" ) ); if ( empty( $receipt['Original-Message'] ) || ! empty( $receipt['Error'] ) ){ continue; } $ballot = yaml_parse( $receipt['Original-Message'] ); $general = empty( $ballot['Comment'] ) ? [] : [ $ballot['Comment'] ]; $options = []; foreach ( $ballot['Votes'] as $vote ){ if ( empty( $vote['comment'] ) ){ continue; } $idxOption = 'option:' . $vote['id']; $idxVote = 'vote:' . $vote['vote']; $options[ $idxOption ][ $idxVote ][] = $vote['comment']; } return compact( 'general', 'options' ); } return FALSE; } private function votes_are_valid( $votes, $votingData ){ $votes = array_column( $votes, NULL, 'id' ); $options = array_column( $votingData['options'], NULL, 'id' ); foreach ( $votes as $id => $data ){ $option = $options[ $id ] ?? NULL; if ( ! $option ){ return FALSE; } $type = $votingData['type'] == 'mixed' ? ( $option['type'] ?? NULL ) : $votingData['type']; if ( ! $type ){ return FALSE; } if ( $type == 'checkbox' && $data['vote'] !== 1 && $data['vote'] !== 0 ){ return FALSE; } } // TODO: bei multiple-choice prüfen, ob Anzahl der gewählten Optionen korrekt ist foreach ( $options as $id => $data ){ if ( ! empty( $data['required'] ) && empty( $votes[ $id ] ) ){ return FALSE; // required element is missing } } return TRUE; } } // end of file models/votings.class.php