DIR : /home/kozerus/public_html/go/dgs/features/lib_votes.php
/home/kozerus/public_html/go/dgs/features
<?php
/*
Dragon Go Server
Copyright (C) 2001- Erik Ouchterlony, Rod Ival, Jens-Uwe Gaspar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* Author: Jens-Uwe Gaspar */
$TranslateGroups[] = "Feature";
require_once 'include/std_classes.php';
require_once 'include/classlib_userquota.php'; // for FEATURE_POINTS_MAX_VALUE
require_once 'include/gui_functions.php';
/*!
* \file lib_votes.php
*
* \brief Functions for feature-voting.
*/
define('FEAT_SUBJECT_WRAPLEN', 55);
define('FEAT_DESCRIPTION_WRAPLEN', 100);
// Feature.Status, see 'specs/db/table-Voting.txt'
define('FEATSTAT_NEW', 'NEW');
define('FEATSTAT_VOTE', 'VOTE');
define('FEATSTAT_WORK', 'WORK');
define('FEATSTAT_DONE', 'DONE');
define('FEATSTAT_LIVE', 'LIVE');
define('FEATSTAT_NACK', 'NACK');
// Feature.Size, see 'specs/db/table-Voting.txt'
define('FEATSIZE_UNSET', '?');
define('FEATSIZE_EPIC', 'EPIC');
define('FEATSIZE_XXL', 'XXL');
define('FEATSIZE_XL', 'XL');
define('FEATSIZE_L', 'L');
define('FEATSIZE_M', 'M');
define('FEATSIZE_S', 'S');
// FeatureVote.Points
define('FEATVOTE_MAXPOINTS', 5);
// date-format for features
define('DATEFMT_FEATLIST', 'Y-m-d');
define('DATEFMT_VOTELIST', 'Y-m-d');
define('DATEFMT_FEATURE', 'Y-m-d&\n\b\s\p;H:i');
// conditions on user to allow voting
define('VOTE_MIN_FINISHED_GAMES', 5); // #games
define('VOTE_MIN_DAYS_LASTMOVED', 30); // #days
/*!
* \class Feature
*
* \brief Class to handle feature-list with subject and status
*/
class Feature
{
/*! \brief ID (PK from db). */
public $id;
/*! \brief Status (can be one of FEATSTAT_). */
public $status;
/*! \brief Size of feature (can be one of FEATSIZE_). */
public $size;
/*! \brief Subject-description of feature. */
public $subject;
/*! \brief Long description of feature. */
public $description;
/*! \brief user-id of feature-editor. */
private $editor;
/*! \brief Date when feature has been added (unix-time). */
public $created;
/*! \brief Date when feature has been last updated (unix-time). */
public $lastchanged;
/*! \brief FeatureVote-object, see func load_featurevote. */
private $featurevote;
/*!
* \brief Constructs Feature-object with specified arguments: created and lastchanged are in UNIX-time.
* $id may be 0 to add a new feature
*/
public function __construct( $id=0, $status=FEATSTAT_NEW, $size=FEATSIZE_UNSET, $subject='', $description='', $uid_editor=0,
$created=0, $lastchanged=0 )
{
if ( !is_numeric($uid_editor) || $uid_editor < 0 )
error('invalid_user', "Feature.construct.check.editor($id,$uid_editor)");
$this->id = (int) $id;
$this->set_status( $status );
$this->set_size( $size );
$this->set_subject( $subject );
$this->set_description( $description );
$this->editor = (int) $uid_editor;
$this->created = (int) $created;
$this->lastchanged = (int) $lastchanged;
}
/*! \brief Sets valid status */
public function set_status( $status )
{
if ( !preg_match( "/^(NEW|VOTE|WORK|DONE|LIVE|NACK)$/", $status ) )
error('invalid_args', "Feature.set_status($status)");
$this->status = $status;
}
/*! \brief Sets valid size */
public function set_size( $size )
{
if ( !preg_match( "/^(\\?|EPIC|XXL|XL|L|M|S)$/", $size ) )
error('invalid_args', "Feature.set_size($size)");
$this->size = $size;
}
/*! \brief Returns true, if changing status is allowed. */
public function edit_status_allowed( $is_super_admin, $new_status=null )
{
if ( $this->status == FEATSTAT_LIVE ) // final-status; NACK-status may be revived
return false;
if ( $is_super_admin )
return true;
if ( $this->status != FEATSTAT_NEW )
return false;
if ( is_null($new_status) )
return true;
else
return ( $new_status == FEATSTAT_NEW || $new_status == FEATSTAT_VOTE );
}//edit_status_allowed
/*! \brief Sets description */
public function set_description( $description )
{
if ( is_null($description) )
$this->description = '';
else
$this->description = preg_replace( "/(\r\n|\n|\r)/s", "\n", trim($description) );
}
/*!
* \brief Sets subject after doing some replacements
* (remove double-LFs, remove starting/trailing whitespaces and LFs, cut length to 255).
*/
public function set_subject( $subject )
{
if ( is_null($subject) )
$this->subject = '';
else
$this->subject = substr( preg_replace( "/(\r\n|\n|\r)+/s", " ", trim($subject) ), 0, 255);
}
/*! \brief Returns true, if feature can be voted on; no user-specific checks. */
public function allow_vote()
{
return (bool) ( $this->status == FEATSTAT_VOTE );
}
/*!
* \brief Returns true, if current (admin) user is allowed to edit this feature (NEW status).
* Always allowed for super-admin.
*/
public function allow_edit()
{
if ( self::is_super_admin() && $this->status != FEATSTAT_LIVE ) // feature-super-admin can always edit non-LIVE feature
return true;
if ( !self::is_admin() ) // only admin can edit
return false;
return (bool) ( $this->status == FEATSTAT_NEW );
}
/*!
* \brief Updates current Feature-data into database (may replace existing feature
* and set editor=current-user and lastchanged=NOW).
*/
public function update_feature()
{
global $player_row, $NOW;
$this->editor = @$player_row['ID'];
$this->lastchanged = $NOW;
$update_query = 'REPLACE INTO Feature SET'
. ' ID=' . (int)$this->id
. ", Status='" . mysql_addslashes($this->status) . "'"
. ", Size='" . mysql_addslashes($this->size) . "'"
. ", Subject='" . mysql_addslashes($this->subject) . "'"
. ", Description='" . mysql_addslashes($this->description) . "'"
. ', Editor_ID=' . (int)$this->editor
. ', Created=FROM_UNIXTIME(' . $this->created .')'
. ', Lastchanged=FROM_UNIXTIME(' . $this->lastchanged .')'
;
$result = db_query( "feature.update_feature({$this->id},{$this->subject})", $update_query );
if ( $result == 1 && !$this->id )
$this->id = mysql_insert_id();
return $result;
}//update_feature
/*! \brief Returns true, if delete-feature allowed (checks constraints). */
public function can_delete_feature()
{
// check if there are votes for this feature to delete
if ( $this->id )
{
$row = mysql_single_fetch( "feature.check_delete_feature({$this->id})",
"SELECT fid FROM FeatureVote WHERE fid={$this->id} LIMIT 1" );
$has_votes = (bool)$row;
}
else
$has_votes = false;
return !$has_votes;
}//can_delete_feature
/*!
* \brief Deletes current Feature from database if no votes found (only as admin).
* \return number of deleted rows
*/
public function delete_feature()
{
if ( !self::is_admin() )
error('feature_edit_not_allowed', "Feature.delete_feature({$this->id})");
if ( !$this->can_delete_feature() )
error('constraint_votes_delete_feature', "Feature.delete_feature({$this->id})");
$delete_query = "DELETE FROM Feature WHERE ID='{$this->id}' LIMIT 1";
db_query( "feature.delete_feature({$this->id})", $delete_query );
return mysql_affected_rows();
}//delete_feature
/*!
* \brief Loads featurevote for this Feature-object (if existing).
* \return null if no feature-vote found for feature.
*/
public function load_featurevote( $uid_voter )
{
$fvote = FeatureVote::load_featurevote( $this->id, $uid_voter );
$this->featurevote = $fvote;
return $fvote;
}
/*!
* \brief Updates current FeatureVote-data into database (may replace existing featurevote and lastchanged=NOW).
* \note IMPORTANT NOTE: caller needs to open TA with HOT-section if used with other db-writes!!
*
* \return number of points to add to UserQuota-feature-points (can be negative)
*/
public function update_vote( $uid_voter, $points )
{
$fpoints = 0; // consumed feature-points
if ( is_null($this->featurevote) )
{// no vote so far
$this->featurevote = new FeatureVote( $this->id, $uid_voter, $points );
$fpoints = -abs($points);
}
else
{// already voted
$old_points = $this->featurevote->get_points();
$this->featurevote->set_points( $points );
if ( $old_points != $points )
$fpoints = abs($old_points) - abs($points);
}
//error_log("F.update_vote: " . $this->to_string());
$this->featurevote->update_feature_vote();
return $fpoints;
}//update_vote
/*!
* \brief Fixes user user-quota feature-points on feature-status change.
*
* \note "return" all feature-points if feature rejected: !NACK -> NACK
* \note no "return" of feature-points for status-change back from rejected: NACK -> !NACK
* (because feature-points have already been returned for rejected features;
* votes are kept, principally allowing to increase ones points by "taking"
* back voted points => so better not undo feature-rejection)
* \note "return" min(own-vote,average-vote) of feature-points if feature: !LIVE -> LIVE
* \return sum of all "returned" feature-points
*/
public function fix_user_quota_feature_points( $old_status, $new_status )
{
if ( $old_status == $new_status ) // no status-change
return 0;
if ( $old_status == FEATSTAT_NACK ) // no "return" if NACK-status changed to something
return 0;
// feature rejected OR feature went live -> "return" all feature-points
if ( ( $old_status != FEATSTAT_NACK && $new_status == FEATSTAT_NACK )
|| ( $old_status != FEATSTAT_LIVE && $new_status == FEATSTAT_LIVE ) )
{
$row = FeatureVote::load_featurevote_summary( $this->id );
if ( !$row ) return 0;
$sumPoints = @$row['sumAbsPoints'] + 0;
// return all consumed points to respective voter
db_query( "Feature({$this->id}).fix_user_quota_feature_points($old_status,$new_status)",
'UPDATE UserQuota AS UQ '
. "INNER JOIN FeatureVote AS FV ON FV.Voter_ID=UQ.uid AND FV.fid={$this->id} AND FV.Points<>0 "
. 'SET UQ.FeaturePoints = LEAST('.FEATURE_POINTS_MAX_VALUE.', UQ.FeaturePoints + ABS(FV.Points))' );
return $sumPoints;
}
return 0;
}//fix_user_quota_feature_points
/*! \brief Returns string-representation of this object (for debugging purposes). */
public function to_string()
{
return "Feature(id={$this->id}): "
. "status=[{$this->status}], "
. "subject=[{$this->subject}], "
. "size=[{$this->size}], "
. "description=[{$this->description}], "
. "editor=[{$this->editor}], "
. "created=[{$this->created}], lastchanged=[{$this->lastchanged}]"
. ( ($this->featurevote ) ? $this->featurevote->to_string() : '');
}
// ---------- Static Class functions ----------------------------
/*!
* \brief Returns true, if current player has admin-rights for feature-functionality.
* \param $superadmin true=superadmin has ALL rights, false=feature-admin has add-rights
*/
public static function is_admin( $superadmin=false )
{
global $player_row;
$chk_adminlevel = ( $superadmin ) ? ADMIN_DEVELOPER : (ADMIN_FEATURE|ADMIN_DEVELOPER);
$is_admin = (bool) ( @$player_row['admin_level'] & $chk_adminlevel );
//return false; // for easy testing
return $is_admin;
}
/*! \brief Returns true, if current player has super-admin-rights for feature-functionality. */
public static function is_super_admin()
{
return self::is_admin(true);
}
/*! \brief Returns array used for filter on status for feature/votes-list. */
public static function build_filter_selection_status( $fname )
{
return array(
T_('All#featstat') => '',
T_('New#featstat') => "$fname='".FEATSTAT_NEW."'",
T_('Vote#featstat') => "$fname='".FEATSTAT_VOTE."'",
T_('Open#featstat') => "$fname IN ('".FEATSTAT_VOTE."','".FEATSTAT_WORK."','".FEATSTAT_DONE."')",
T_('In Work#featstat') => "$fname='".FEATSTAT_WORK."'",
T_('Done#featstat') => "$fname='".FEATSTAT_DONE."'",
T_('Online#featstat') => "$fname='".FEATSTAT_LIVE."'",
T_('Rejected#featstat') => "$fname='".FEATSTAT_NACK."'",
);
}
/*! \brief Returns array used for filter on size for feature/votes-list. */
public static function build_filter_selection_size( $fname )
{
return array(
T_('All#featsize') => '',
FEATSIZE_UNSET => "$fname='".FEATSIZE_UNSET."'",
FEATSIZE_S => "$fname='".FEATSIZE_S."'",
FEATSIZE_M => "$fname='".FEATSIZE_M."'",
FEATSIZE_L => "$fname='".FEATSIZE_L."'",
FEATSIZE_XL => "$fname='".FEATSIZE_XL."'",
FEATSIZE_XXL => "$fname='".FEATSIZE_XXL."'",
FEATSIZE_EPIC => "$fname='".FEATSIZE_EPIC."'",
);
}
/*! \brief Returns array with notes about feature-voting. */
public static function build_feature_notes( $deny_reason=null, $with_size=true, $intro=true )
{
$notes = array();
if ( !is_null($deny_reason) )
{
$notes[] = array( 'text' =>
span('VotingRestricted', T_('Voting restricted'), '%s:') . ' ' . make_html_safe($deny_reason, 'line') );
$notes[] = null; // empty line
}
if ( $intro )
{
$notes[] = T_('Feature or improvement suggestions are to be discussed in the <home forum/index.php>forums</home> first.');
$notes[] = T_('Features can only be added to this list by a feature-admin.');
$notes[] = null; // empty line
}
$notes_fstatus = array( T_('Feature Status') . ':',
sprintf( T_('%s = feature in edit-mode, only for feature-admin'), FEATSTAT_NEW ),
sprintf( T_('%s = new feature to vote on'), FEATSTAT_VOTE ),
sprintf( T_('%s = feature implementation started by developer'), FEATSTAT_WORK ),
sprintf( T_('%s = feature implemented (and tested), but not released yet'), FEATSTAT_DONE ),
sprintf( T_('%s = feature released and online'), FEATSTAT_LIVE ),
sprintf( T_('%s = feature rejected (will not or cannot be done)'), FEATSTAT_NACK ),
);
if ( $intro )
$notes_fstatus[] = sprintf( T_('%s = show status %s + %s + %s'), T_('Open#featstat'), FEATSTAT_VOTE, FEATSTAT_WORK, FEATSTAT_DONE );
$notes[] = $notes_fstatus;
if ( $with_size )
$notes[] = self::build_feature_notes_size();
return $notes;
}//build_feature_notes
public static function build_feature_notes_size()
{
$day_work = T_('ca. %s day(s) work#feature');
$month_work = T_('ca. %s month(s) work#feature');
$fmt = '<b>%s</b> = %s';
return array( T_('Size#feature') . ' (' . T_('estimated effort#feature') . '):',
sprintf($fmt, FEATSIZE_UNSET, T_('unknown size#feature') ),
sprintf($fmt, FEATSIZE_S, sprintf( $day_work, 3 ) ) . ', ' .
sprintf($fmt, FEATSIZE_M, sprintf( $day_work, 7 ) ) . ', ' .
sprintf($fmt, FEATSIZE_L, sprintf( $day_work, 14 ) ),
sprintf($fmt, FEATSIZE_XL, sprintf( $month_work, 1 ) ) . ', ' .
sprintf($fmt, FEATSIZE_XXL, sprintf( $month_work, 3 ) ) . ', ' .
sprintf($fmt, FEATSIZE_EPIC, T_('very big, >6 months work#feature') ),
);
}//build_feature_notes_size
/*!
* \brief Returns null if current user ($player_row) is allowed to participate in voting;
* otherwise return deny-reason.
*/
public static function allow_vote_check()
{
global $player_row, $NOW;
if ( @$player_row['ID'] <= GUESTS_ID_MAX )
return T_('Voting is not allowed for guest user.');
if ( @$player_row['AdminOptions'] & ADMOPT_DENY_FEATURE_VOTE )
return T_('Feature-Voting has been denied.');
// minimum X finished+rated games, moved within Y days
if ( @$player_row['Finished'] < VOTE_MIN_FINISHED_GAMES
|| @$player_row['X_LastMove'] < $NOW - VOTE_MIN_DAYS_LASTMOVED * SECS_PER_DAY )
{
return sprintf( T_("To be able to vote you have to finish %s games and \nactively play in games during the last %s days."),
VOTE_MIN_FINISHED_GAMES, VOTE_MIN_DAYS_LASTMOVED );
}
//return 'testing'; // for easy testing
return null;
}//allow_vote_check
/*!
* \brief Returns QuerySQL for feature-list page.
* \param $ftable: Table object
* \param $user_id: id of current logged-in user
*/
public static function build_query_feature_list( Table $ftable, $user_id )
{
if ( !is_numeric($user_id) )
error('invalid_user', "Feature:build_query_feature_list($user_id)");
// build SQL-query
$qsql = new QuerySQL();
$qsql->add_part_fields( self::get_query_fields() );
$qsql->add_part_fields( FeatureVote::get_query_fields() );
$qsql->add_part( SQLP_FROM, 'Feature AS F' );
$qsql->add_part( SQLP_FROM, "LEFT JOIN FeatureVote AS FV ON F.ID=FV.fid AND FV.Voter_ID='$user_id'" );
$query_ffilter = $ftable->get_query(); // clause-parts for filter
$qsql->merge( $query_ffilter );
return $qsql;
}//build_query_feature_list
/*! \brief Returns db-fields to be used for query of Feature-object. */
public static function get_query_fields( $short=false )
{
if ( $short )
return array( 'F.ID', 'F.Status', 'F.Size', 'F.Subject' );
return array(
'F.ID', 'F.Status', 'F.Size', 'F.Subject', 'F.Description', 'F.Editor_ID',
'IFNULL(UNIX_TIMESTAMP(F.Created),0) AS F_CreatedU',
'IFNULL(UNIX_TIMESTAMP(F.Lastchanged),0) AS F_LastchangedU'
);
}
/*! \brief Returns Feature-object for specified user $editor, created=$NOW set and all others in default-state. */
public static function new_feature( $uid_editor, $fid=0 )
{
global $NOW;
// id=set, status=NEW, size='', subject='', description='', editor=$editor, created=NOW, lastchanged
$feature = new Feature( $fid );
$feature->editor = $uid_editor;
$feature->created = $NOW;
return $feature;
}
/*! \brief Returns Feature-object created from specified (db-)row with fields defined by func fields_feature. */
public static function new_from_row( array $row )
{
$feature = new Feature(
$row['ID'],
$row['Status'],
$row['Size'],
$row['Subject'],
@$row['Description'],
@$row['Editor_ID'] + 0,
@$row['F_CreatedU'],
@$row['F_LastchangedU']
);
return $feature;
}
/*!
* \brief Returns Feature-object for specified feature-id $id.
* \return null if no feature found.
*/
public static function load_feature( $id )
{
if ( !is_numeric($id) )
error('invalid_args', "Feature:load_feature.check.id($id)");
$fields = implode(',', self::get_query_fields());
$row = mysql_single_fetch("Feature:load_feature.find($id)",
"SELECT $fields FROM Feature AS F WHERE F.ID='$id' LIMIT 1");
return ($row) ? self::new_from_row( $row ) : null;
}//load_feature
/*!
* \brief Updates or resets Players.CountFeatNew.
* \param $uid 0 update all users
* \param $diff null|omit to reset to -1 (=recalc later);
* COUNTNEW_RECALC to recalc now (for specific user-id only);
* otherwise increase or decrease counter
*/
public static function update_count_feature_new( $dbgmsg, $uid=0, $diff=null )
{
if ( !ALLOW_FEATURE_VOTE )
return;
$dbgmsg .= "Feature:update_count_feature_new($uid,$diff)";
if ( !is_numeric($uid) )
error('invalid_args', "$dbgmsg.check.uid" );
if ( is_null($diff) )
{
$query = "UPDATE Players SET CountFeatNew=-1 WHERE CountFeatNew>=0";
if ( $uid > 0 )
$query .= " AND ID=$uid LIMIT 1";
db_query( "$dbgmsg.reset", $query );
}
elseif ( is_numeric($diff) && $diff != 0 )
{
$query = "UPDATE Players SET CountFeatNew=CountFeatNew+($diff) WHERE CountFeatNew>=0";
if ( $uid > 0 )
$query .= " AND ID=$uid LIMIT 1";
db_query( "$dbgmsg.upd", $query );
}
elseif ( (string)$diff == COUNTNEW_RECALC && $uid > 0 )
{
global $player_row;
$count_new = count_feature_new( $uid );
if ( @$player_row['ID'] == $uid )
$player_row['CountFeatNew'] = $count_new;
db_query( "$dbgmsg.recalc",
"UPDATE Players SET CountFeatNew=$count_new WHERE ID=$uid LIMIT 1" );
}
}//update_count_feature_new
} // end of 'Feature'
/*!
* \class FeatureVote
*
* \brief Class to handle vote on feature
*/
class FeatureVote
{
/*! \brief feature-id (FK). */
private $fid;
/*! \brief user-id of feature-voter. */
private $voter;
/*! \brief vote points on feature: 0=neutral, 1=low, 9=high points, -1=not-wanted. */
public $points;
/*! \brief Date when feature has been last updated (unix-time). */
public $lastchanged;
/*!
* \brief Constructs Feature-object with specified arguments: created and lastchanged are in UNIX-time.
* $id may be 0 to add a new feature
*/
public function __construct( $fid=0, $uid_voter=0, $points=0, $lastchanged=0 )
{
if ( !is_numeric($uid_voter) || !is_numeric($uid_voter) || $uid_voter < 0 )
error('invalid_user', "FeatureVote.construct($fid,$uid_voter)");
$this->fid = (int) $fid;
$this->voter = (int) $uid_voter;
$this->set_points( $points );
$this->lastchanged = (int) $lastchanged;
}
/*! \brief Sets valid points (<0,0,>0). */
public function set_points( $points )
{
if ( !is_numeric($points) || abs($points) > FEATVOTE_MAXPOINTS )
error('invalid_args', "FeatureVote.set_points($points)");
$this->points = $points;
}
/*! \brief Returns points. */
public function get_points()
{
return $this->points;
}
/*! \brief Updates current FeatureVote-data into database (may replace existing featurevote, set lastchanged=NOW). */
public function update_feature_vote()
{
global $NOW;
$this->lastchanged = $NOW;
$update_query = 'REPLACE INTO FeatureVote SET'
. ' fid=' . (int)$this->fid
. ', Voter_ID=' . (int)$this->voter
. ', Points=' . (int)$this->points
. ', Lastchanged=FROM_UNIXTIME(' . $this->lastchanged .')'
;
db_query( "feature.update_feature_vote({$this->fid},{$this->voter},{$this->points})", $update_query );
}//update_feature_vote
/*! \brief Returns string-representation of this object (for debugging purposes). */
public function to_string()
{
return "FeatureVote(fid={$this->fid}): "
. "voter=[{$this->voter}], "
. "points=[{$this->points}], "
. "lastchanged=[{$this->lastchanged}]";
}
// ---------- Static Class functions ----------------------------
/*!
* \brief Returns error-message if points are invalid; otherwise return null (=points ok).
* \param $points the points to check
* \param $max_points max. amount of feature-points that can be used for voting;
* used if user less than FEATVOTE_MAXPOINTS remaining feature-points restricted by his quota.
*/
public static function check_points( $points, $max_points=FEATVOTE_MAXPOINTS )
{
if ( !is_numeric($points) )
return sprintf( T_('points [%s] must be numeric'), $points );
if ( abs($points) > FEATVOTE_MAXPOINTS )
return sprintf( T_('points [%1$s] must be in range [%2$s,%3$s]'),
$points, -FEATVOTE_MAXPOINTS, FEATVOTE_MAXPOINTS );
if ( abs($points) > $max_points )
return sprintf( T_('points [%1$s] must be in range [%2$s,%3$s] (restricted by feature-points quota)'),
$points, -$max_points, $max_points );
return null;
}//check_points
/*!
* \brief Returns QuerySQL for feature-vote-list page.
* \param $mquery QuerySQL object to merge
*/
public static function build_query_featurevote_list( $my_id, QuerySQL $mquery = null )
{
// build SQL-query
$qsql = new QuerySQL();
$qsql->add_part_fields( Feature::get_query_fields(true) );
$qsql->add_part( SQLP_FIELDS,
"MYFV.Points AS myPoints",
'SUM(FV.Points) AS sumPoints',
'COUNT(FV.fid) AS countVotes',
'SUM(IF(FV.Points>0,1,0)) AS countYes',
'SUM(IF(FV.Points<0,1,0)) AS countNo'
);
$qsql->add_part( SQLP_FROM, 'Feature AS F' );
$qsql->add_part( SQLP_FROM,
"LEFT JOIN FeatureVote AS MYFV on MYFV.fid=F.ID AND MYFV.Voter_ID=$my_id",
'LEFT JOIN FeatureVote AS FV ON F.ID=FV.fid' );
$qsql->add_part( SQLP_WHERE, 'FV.Points is not null' ); // abstention from voting
$qsql->add_part( SQLP_GROUP, 'FV.fid' );
$qsql->add_part( SQLP_HAVING, 'sumPoints is not null' );
if ( !is_null($mquery) )
$qsql->merge( $mquery );
return $qsql;
}//build_query_featurevote_list
/*!
* \brief Returns row-array with summary about feature-votes for given feature-id.
* \return for avgPoints=0: row( avgAbsPoints => average, sumAbsPoints => sum-points, cntVotes => vote-count )
* for avgPoints>0: row( sumAvgPoints => sum-points )
*/
public static function load_featurevote_summary( $fid, $avgPoints=0 )
{
if ( !is_numeric($fid) || $fid < 0 )
error('invalid_args', "FeatureVote:load_featurevote_summary.check.fid($fid)");
$qsql = new QuerySQL();
if ( $avgPoints > 0 )
$qsql->add_part( SQLP_FIELDS,
"SUM(IF($avgPoints<ABS(FV.Points),$avgPoints,ABS(FV.Points))) AS sumAvgPoints" );
else
$qsql->add_part( SQLP_FIELDS,
'COUNT(*) AS cntVotes',
'AVG(ABS(FV.Points)) AS avgAbsPoints',
'SUM(ABS(FV.Points)) AS sumAbsPoints' );
$qsql->add_part( SQLP_FROM, 'FeatureVote AS FV' );
$qsql->add_part( SQLP_WHERE,
"FV.fid='$fid'",
'FV.Points<>0' ); // abstention from voting
$query = $qsql->get_select() . ' LIMIT 1';
$row = mysql_single_fetch( "FeatureVote:load_featurevote_summary($fid)", $query );
return $row;
}//load_featurevote_summary
/*! \brief Returns db-fields to be used for query of Feature-object. */
public static function get_query_fields()
{
return array(
'FV.fid', 'FV.Voter_ID', 'FV.Points',
'IFNULL(UNIX_TIMESTAMP(FV.Lastchanged),0) AS FVLastchangedU',
);
}
/*!
* \brief Returns Feature-object created from specified (db-)row with fields defined by func fields_feature.
* \return null if row['fid']=0
*/
public static function new_from_row( array $row )
{
if ( $row['fid'] != 0 )
$fvote = new FeatureVote( $row['fid'], $row['Voter_ID'], $row['Points'], $row['FVLastchangedU'] );
else
$fvote = null;
return $fvote;
}
/*!
* \brief Returns FeatureVote-object for specified feature-id $id and voter $voter;
* \return null if no feature found.
*/
public static function load_featurevote( $fid, $uid_voter )
{
if ( !is_numeric($fid) )
error('invalid_args', "FeatureVote:load_featurevote.check.fid($fid,$uid_voter)");
$fields = implode(',', self::get_query_fields());
$row = mysql_single_fetch("FeatureVote:load_featurevote($fid,$uid_voter)",
"SELECT $fields FROM FeatureVote AS FV WHERE FV.fid='$fid' and FV.Voter_ID='$uid_voter' LIMIT 1");
return ($row) ? self::new_from_row( $row ) : null;
}//load_featurevote
/*! \brief Inserts new FeatureVote-entries into database with 0 points (neutral vote). */
public static function insert_feature_neutral_votes( $uid, array $feature_ids )
{
global $NOW;
if ( count($feature_ids) > 0 )
{
// (fid,Voter_ID,Points,Lastchanged)
$val_sets = array();
foreach ( $feature_ids as $fid )
$val_sets[] = sprintf('(%s,%s,0,FROM_UNIXTIME(%s))', $fid, $uid, $NOW );
// NOTE: must not use REPLACE as then we would need to handle existing Points<>0
db_query( "FeatureVote:insert_feature_neutral_votes($uid)",
'INSERT INTO FeatureVote (fid,Voter_ID,Points,Lastchanged) VALUES ' . implode(', ', $val_sets) );
}
}//insert_feature_neutral_votes
/*! \brief Returns text informing about remaining feature-points. */
public static function getFeaturePointsText( $points )
{
if ( $points > 0 )
$points = span('Positive', $points);
elseif ( $points < 0 )
$points = span('VotingRestricted', $points);
return sprintf( T_('You have %s points available for voting on features.'), $points );
}
public static function formatPoints( $points )
{
if ( $points < 0 )
return span('Negative', $points);
elseif ( $points > 0 )
return span('Positive', MINI_SPACING . $points);
else
return MINI_SPACING . $points;
}
public static function formatPointsText( $points )
{
if ( !is_numeric($points) )
return sprintf( T_('%s (no vote)'), NO_VALUE );
$point_str = sprintf( T_('%s points#feature'), ( $points > 0 ? '+' . $points : $points ) );
if ( $points > 0 )
$point_str = span('Positive', $point_str);
elseif ( $points < 0 )
$point_str = span('Negative', $point_str);
return $point_str;
}//formatPointsText
} // end of 'FeatureVote'
?>
//
koh5_pano
Drag mouse to navigate.
Navigation
- Left/Right Mouse drag: Changes camera heading.
- Up/Sown Mouse drag: Changes camera pitch.
- Scroll wheel: Changes camera field of view.
- I-Key: Displays Info panel with canvas size, image size and FPS.
17.Aug.2010, Martin Wengenmayer