From 248a6fd50cc92a8964567929b83cb1910d349738 Mon Sep 17 00:00:00 2001 From: Jordie <4343468+Jordie0608@users.noreply.github.com> Date: Mon, 11 May 2020 12:34:51 +1000 Subject: [PATCH] Datum based poll creation and vote handling update (#50843) An almost complete rework of how creating polls, their data and voting on them is handled. Instead of repeatedly querying for poll data, running polls are loaded at runtime into poll_question and poll_option datums that stores all the needed variables for both. This datum is then used for creating, editing and accessing poll data. The database is only contacted when saving changes or votes. Creating polls and options is now done with a html window instead of a series of popups akin to how the banning panel works. The form data is parsed and error-checked before passing to be saved. This is done in two stages, first time a poll's details are entered and it must be initialized (created as a datum) before options can be added and all of both are saved to the database. Instructions about how this work are shown on the poll creation window. A new field for polls has been added, subtitles, which is text only shown when a poll is opened by a player instead of on the list of polls. Intended so the actual question text can be kept to only a name and important information about a poll goes in a subtitle. All polls can now have revoting enabled on them. Polls can have a starting datetime specified, this can be in the past but why would you do that? Polls and options can be edited once created, excluding the type of a poll. Doing so will by default clear all existing poll votes. Votes can also be cleared by a button. The handling of how votes are processed has been adapted for the datum system but is on the whole not functionally that different aside from poll validation not being roughly copypasted across each poll type's vote proc All poll tables now have a deleted column for retaining 'deleted' data. poll_question has also gained the columns created_datetime, subtitle, allow_revoting, their function explained above, and a change of idx_pquest_time_admin to idx_pquest_time_deleted_id. A stored procedure set_poll_deleted has been added. This is called when setting a poll as deleted to avoid needing 4 separate queries from the server or one fairly long 4-way joined Create Poll verb is renamed to Server Poll Management --- SQL/database_changelog.txt | 47 +- SQL/tgstation_schema.sql | 48 +- SQL/tgstation_schema_prefixed.sql | 48 +- code/__DEFINES/subsystems.dm | 2 +- code/_globalvars/misc.dm | 8 + code/game/world.dm | 1 + code/modules/admin/admin_verbs.dm | 9 +- code/modules/admin/create_poll.dm | 150 --- code/modules/admin/poll_management.dm | 735 +++++++++++ code/modules/admin/sql_ban_system.dm | 3 +- code/modules/admin/topic.dm | 49 + .../modules/mob/dead/new_player/new_player.dm | 109 +- code/modules/mob/dead/new_player/poll.dm | 1080 ++++++++--------- html/admin/admin_panels.css | 42 + ...anpanel_css3.css => admin_panels_css3.css} | 0 html/admin/banpanel.css | 33 - tgstation.dme | 2 +- 17 files changed, 1460 insertions(+), 906 deletions(-) delete mode 100644 code/modules/admin/create_poll.dm create mode 100644 code/modules/admin/poll_management.dm create mode 100644 html/admin/admin_panels.css rename html/admin/{banpanel_css3.css => admin_panels_css3.css} (100%) diff --git a/SQL/database_changelog.txt b/SQL/database_changelog.txt index 2684cf032a8..c204cbaaf92 100644 --- a/SQL/database_changelog.txt +++ b/SQL/database_changelog.txt @@ -1,15 +1,56 @@ Any time you make a change to the schema files, remember to increment the database schema version. Generally increment the minor number, major should be reserved for significant changes to the schema. Both values go up to 255. -The latest database version is 5.8; The query to update the schema revision table is: +The latest database version is 5.9; The query to update the schema revision table is: -INSERT INTO `schema_revision` (`major`, `minor`) VALUES (5, 8); +INSERT INTO `schema_revision` (`major`, `minor`) VALUES (5, 9); or -INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (5, 8); +INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (5, 9); In any query remember to add a prefix to the table names if you use one. ----------------------------------------------------- +Version 5.9, 19 April 2020, by Jordie0608 +Updates and improvements to poll handling. +Added the `deleted` column to tables 'poll_option', 'poll_textreply' and 'poll_vote' and the columns `created_datetime`, `subtitle`, `allow_revoting` and `deleted` to 'poll_question'. +Changes table 'poll_question' column `createdby_ckey` to be NOT NULL and index `idx_pquest_time_admin` to be `idx_pquest_time_deleted_id` and 'poll_textreply' column `adminrank` to have no default. +Added procedure `set_poll_deleted` that's called when deleting a poll to set deleted to true on each poll table where rows matching a poll_id argument. + +ALTER TABLE `poll_option` + ADD COLUMN `deleted` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `default_percentage_calc`; + +ALTER TABLE `poll_question` + CHANGE COLUMN `createdby_ckey` `createdby_ckey` VARCHAR(32) NOT NULL AFTER `multiplechoiceoptions`, + ADD COLUMN `created_datetime` datetime NOT NULL AFTER `polltype`, + ADD COLUMN `subtitle` VARCHAR(255) NULL DEFAULT NULL AFTER `question`, + ADD COLUMN `allow_revoting` TINYINT(1) UNSIGNED NOT NULL AFTER `dontshow`, + ADD COLUMN `deleted` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `allow_revoting`, + DROP INDEX `idx_pquest_time_admin`, + ADD INDEX `idx_pquest_time_deleted_id` (`starttime`, `endtime`, `deleted`, `id`); + +ALTER TABLE `poll_textreply` + CHANGE COLUMN `adminrank` `adminrank` varchar(32) NOT NULL AFTER `replytext`, + ADD COLUMN `deleted` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `adminrank`; + +ALTER TABLE `poll_vote` + ADD COLUMN `deleted` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `rating`; + +DELIMITER $$ +CREATE PROCEDURE `set_poll_deleted`( + IN `poll_id` INT +) +SQL SECURITY INVOKER +BEGIN +UPDATE `poll_question` SET deleted = 1 WHERE id = poll_id; +UPDATE `poll_option` SET deleted = 1 WHERE pollid = poll_id; +UPDATE `poll_vote` SET deleted = 1 WHERE pollid = poll_id; +UPDATE `poll_textreply` SET deleted = 1 WHERE pollid = poll_id; +END +$$ +DELIMITER ; + +----------------------------------------------------- + Version 5.8, 7 April 2020, by Jordie0608 Modified table `messages`, adding column `deleted_ckey` to record who deleted a message. diff --git a/SQL/tgstation_schema.sql b/SQL/tgstation_schema.sql index f03788e14c4..1f5edef8c57 100644 --- a/SQL/tgstation_schema.sql +++ b/SQL/tgstation_schema.sql @@ -346,6 +346,7 @@ CREATE TABLE `poll_option` ( `descmid` varchar(32) DEFAULT NULL, `descmax` varchar(32) DEFAULT NULL, `default_percentage_calc` tinyint(1) unsigned NOT NULL DEFAULT '1', + `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `idx_pop_pollid` (`pollid`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; @@ -361,17 +362,21 @@ DROP TABLE IF EXISTS `poll_question`; CREATE TABLE `poll_question` ( `id` int(11) NOT NULL AUTO_INCREMENT, `polltype` enum('OPTION','TEXT','NUMVAL','MULTICHOICE','IRV') NOT NULL, + `created_datetime` datetime NOT NULL, `starttime` datetime NOT NULL, `endtime` datetime NOT NULL, `question` varchar(255) NOT NULL, + `subtitle` varchar(255) DEFAULT NULL, `adminonly` tinyint(1) unsigned NOT NULL, `multiplechoiceoptions` int(2) DEFAULT NULL, - `createdby_ckey` varchar(32) DEFAULT NULL, + `createdby_ckey` varchar(32) NOT NULL, `createdby_ip` int(10) unsigned NOT NULL, `dontshow` tinyint(1) unsigned NOT NULL, + `allow_revoting` tinyint(1) unsigned NOT NULL, + `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `idx_pquest_question_time_ckey` (`question`,`starttime`,`endtime`,`createdby_ckey`,`createdby_ip`), - KEY `idx_pquest_time_admin` (`starttime`,`endtime`,`adminonly`), + KEY `idx_pquest_time_deleted_id` (`starttime`,`endtime`, `deleted`, `id`), KEY `idx_pquest_id_time_type_admin` (`id`,`starttime`,`endtime`,`polltype`,`adminonly`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; @@ -390,7 +395,8 @@ CREATE TABLE `poll_textreply` ( `ckey` varchar(32) NOT NULL, `ip` int(10) unsigned NOT NULL, `replytext` varchar(2048) NOT NULL, - `adminrank` varchar(32) NOT NULL DEFAULT 'Player', + `adminrank` varchar(32) NOT NULL, + `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `idx_ptext_pollid_ckey` (`pollid`,`ckey`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; @@ -412,6 +418,7 @@ CREATE TABLE `poll_vote` ( `ip` int(10) unsigned NOT NULL, `adminrank` varchar(32) NOT NULL, `rating` int(2) DEFAULT NULL, + `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `idx_pvote_pollid_ckey` (`pollid`,`ckey`), KEY `idx_pvote_optionid_ckey` (`optionid`,`ckey`) @@ -455,18 +462,6 @@ CREATE TABLE `schema_revision` ( PRIMARY KEY (`major`, `minor`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; -DELIMITER $$ -CREATE TRIGGER `role_timeTlogupdate` AFTER UPDATE ON `role_time` FOR EACH ROW BEGIN INSERT into role_time_log (ckey, job, delta) VALUES (NEW.CKEY, NEW.job, NEW.minutes-OLD.minutes); -END -$$ -CREATE TRIGGER `role_timeTloginsert` AFTER INSERT ON `role_time` FOR EACH ROW BEGIN INSERT into role_time_log (ckey, job, delta) VALUES (NEW.ckey, NEW.job, NEW.minutes); -END -$$ -CREATE TRIGGER `role_timeTlogdelete` AFTER DELETE ON `role_time` FOR EACH ROW BEGIN INSERT into role_time_log (ckey, job, delta) VALUES (OLD.ckey, OLD.job, 0-OLD.minutes); -END -$$ -DELIMITER ; - -- -- Table structure for table `stickyban` -- @@ -556,6 +551,29 @@ CREATE TABLE `ticket` ( PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +DELIMITER $$ +CREATE PROCEDURE `set_poll_deleted`( + IN `poll_id` INT +) +SQL SECURITY INVOKER +BEGIN +UPDATE `poll_question` SET deleted = 1 WHERE id = poll_id; +UPDATE `poll_option` SET deleted = 1 WHERE pollid = poll_id; +UPDATE `poll_vote` SET deleted = 1 WHERE pollid = poll_id; +UPDATE `poll_textreply` SET deleted = 1 WHERE pollid = poll_id; +END +$$ +CREATE TRIGGER `role_timeTlogupdate` AFTER UPDATE ON `role_time` FOR EACH ROW BEGIN INSERT into role_time_log (ckey, job, delta) VALUES (NEW.CKEY, NEW.job, NEW.minutes-OLD.minutes); +END +$$ +CREATE TRIGGER `role_timeTloginsert` AFTER INSERT ON `role_time` FOR EACH ROW BEGIN INSERT into role_time_log (ckey, job, delta) VALUES (NEW.ckey, NEW.job, NEW.minutes); +END +$$ +CREATE TRIGGER `role_timeTlogdelete` AFTER DELETE ON `role_time` FOR EACH ROW BEGIN INSERT into role_time_log (ckey, job, delta) VALUES (OLD.ckey, OLD.job, 0-OLD.minutes); +END +$$ +DELIMITER ; + /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; diff --git a/SQL/tgstation_schema_prefixed.sql b/SQL/tgstation_schema_prefixed.sql index 8a4de26c263..cf4818c30c9 100644 --- a/SQL/tgstation_schema_prefixed.sql +++ b/SQL/tgstation_schema_prefixed.sql @@ -346,6 +346,7 @@ CREATE TABLE `SS13_poll_option` ( `descmid` varchar(32) DEFAULT NULL, `descmax` varchar(32) DEFAULT NULL, `default_percentage_calc` tinyint(1) unsigned NOT NULL DEFAULT '1', + `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `idx_pop_pollid` (`pollid`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; @@ -361,17 +362,21 @@ DROP TABLE IF EXISTS `SS13_poll_question`; CREATE TABLE `SS13_poll_question` ( `id` int(11) NOT NULL AUTO_INCREMENT, `polltype` enum('OPTION','TEXT','NUMVAL','MULTICHOICE','IRV') NOT NULL, + `created_datetime` datetime NOT NULL, `starttime` datetime NOT NULL, `endtime` datetime NOT NULL, `question` varchar(255) NOT NULL, + `subtitle` varchar(255) DEFAULT NULL, `adminonly` tinyint(1) unsigned NOT NULL, `multiplechoiceoptions` int(2) DEFAULT NULL, - `createdby_ckey` varchar(32) DEFAULT NULL, + `createdby_ckey` varchar(32) NOT NULL, `createdby_ip` int(10) unsigned NOT NULL, `dontshow` tinyint(1) unsigned NOT NULL, + `allow_revoting` tinyint(1) unsigned NOT NULL, + `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `idx_pquest_question_time_ckey` (`question`,`starttime`,`endtime`,`createdby_ckey`,`createdby_ip`), - KEY `idx_pquest_time_admin` (`starttime`,`endtime`,`adminonly`), + KEY `idx_pquest_time_deleted_id` (`starttime`,`endtime`, `deleted`, `id`), KEY `idx_pquest_id_time_type_admin` (`id`,`starttime`,`endtime`,`polltype`,`adminonly`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; @@ -390,7 +395,8 @@ CREATE TABLE `SS13_poll_textreply` ( `ckey` varchar(32) NOT NULL, `ip` int(10) unsigned NOT NULL, `replytext` varchar(2048) NOT NULL, - `adminrank` varchar(32) NOT NULL DEFAULT 'Player', + `adminrank` varchar(32) NOT NULL, + `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `idx_ptext_pollid_ckey` (`pollid`,`ckey`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; @@ -412,6 +418,7 @@ CREATE TABLE `SS13_poll_vote` ( `ip` int(10) unsigned NOT NULL, `adminrank` varchar(32) NOT NULL, `rating` int(2) DEFAULT NULL, + `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `idx_pvote_pollid_ckey` (`pollid`,`ckey`), KEY `idx_pvote_optionid_ckey` (`optionid`,`ckey`) @@ -455,18 +462,6 @@ CREATE TABLE `SS13_schema_revision` ( PRIMARY KEY (`major`,`minor`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; -DELIMITER $$ -CREATE TRIGGER `SS13_role_timeTlogupdate` AFTER UPDATE ON `SS13_role_time` FOR EACH ROW BEGIN INSERT into SS13_role_time_log (ckey, job, delta) VALUES (NEW.CKEY, NEW.job, NEW.minutes-OLD.minutes); -END -$$ -CREATE TRIGGER `SS13_role_timeTloginsert` AFTER INSERT ON `SS13_role_time` FOR EACH ROW BEGIN INSERT into SS13_role_time_log (ckey, job, delta) VALUES (NEW.ckey, NEW.job, NEW.minutes); -END -$$ -CREATE TRIGGER `SS13_role_timeTlogdelete` AFTER DELETE ON `SS13_role_time` FOR EACH ROW BEGIN INSERT into SS13_role_time_log (ckey, job, delta) VALUES (OLD.ckey, OLD.job, 0-OLD.minutes); -END -$$ -DELIMITER ; - -- -- Table structure for table `SS13_stickyban` -- @@ -556,6 +551,29 @@ CREATE TABLE `SS13_ticket` ( PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +DELIMITER $$ +CREATE PROCEDURE `set_poll_deleted`( + IN `poll_id` INT +) +SQL SECURITY INVOKER +BEGIN +UPDATE `SS13_poll_question` SET deleted = 1 WHERE id = poll_id; +UPDATE `SS13_poll_option` SET deleted = 1 WHERE pollid = poll_id; +UPDATE `SS13_poll_vote` SET deleted = 1 WHERE pollid = poll_id; +UPDATE `SS13_poll_textreply` SET deleted = 1 WHERE pollid = poll_id; +END +$$ +CREATE TRIGGER `SS13_role_timeTlogupdate` AFTER UPDATE ON `SS13_role_time` FOR EACH ROW BEGIN INSERT into SS13_role_time_log (ckey, job, delta) VALUES (NEW.CKEY, NEW.job, NEW.minutes-OLD.minutes); +END +$$ +CREATE TRIGGER `SS13_role_timeTloginsert` AFTER INSERT ON `SS13_role_time` FOR EACH ROW BEGIN INSERT into SS13_role_time_log (ckey, job, delta) VALUES (NEW.ckey, NEW.job, NEW.minutes); +END +$$ +CREATE TRIGGER `SS13_role_timeTlogdelete` AFTER DELETE ON `SS13_role_time` FOR EACH ROW BEGIN INSERT into SS13_role_time_log (ckey, job, delta) VALUES (OLD.ckey, OLD.job, 0-OLD.minutes); +END +$$ +DELIMITER ; + /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 6efb38b7785..515f9f485fb 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -20,7 +20,7 @@ * * make sure you add an update to the schema_version stable in the db changelog */ -#define DB_MINOR_VERSION 8 +#define DB_MINOR_VERSION 9 //! ## Timing subsystem diff --git a/code/_globalvars/misc.dm b/code/_globalvars/misc.dm index 70ac73929a2..1de2cd22fca 100644 --- a/code/_globalvars/misc.dm +++ b/code/_globalvars/misc.dm @@ -17,4 +17,12 @@ GLOBAL_VAR_INIT(bsa_unlock, FALSE) //BSA unlocked by head ID swipes GLOBAL_LIST_EMPTY(player_details) // ckey -> /datum/player_details +///All currently running polls held as datums +GLOBAL_LIST_EMPTY(polls) +GLOBAL_PROTECT(polls) + +///All poll option datums of running polls +GLOBAL_LIST_EMPTY(poll_options) +GLOBAL_PROTECT(poll_options) + GLOBAL_VAR_INIT(internal_tick_usage, 0.2 * world.tick_lag) diff --git a/code/game/world.dm b/code/game/world.dm index 4e290ea9b1f..f7526730771 100644 --- a/code/game/world.dm +++ b/code/game/world.dm @@ -50,6 +50,7 @@ GLOBAL_VAR(restart_counter) SSdbcore.CheckSchemaVersion() SSdbcore.SetRoundID() SetupLogs() + load_poll_data() #ifndef USE_CUSTOM_ERROR_HANDLER world.log = file("[GLOB.log_directory]/dd.log") diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 3a8730a39af..39973b2f755 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -171,7 +171,7 @@ GLOBAL_LIST_INIT(admin_verbs_possess, list(/proc/possess, /proc/release)) GLOBAL_PROTECT(admin_verbs_possess) GLOBAL_LIST_INIT(admin_verbs_permissions, list(/client/proc/edit_admin_permissions)) GLOBAL_PROTECT(admin_verbs_permissions) -GLOBAL_LIST_INIT(admin_verbs_poll, list(/client/proc/create_poll)) +GLOBAL_LIST_INIT(admin_verbs_poll, list(/client/proc/poll_panel)) GLOBAL_PROTECT(admin_verbs_poll) //verbs which can be hidden - needs work @@ -401,6 +401,13 @@ GLOBAL_PROTECT(admin_verbs_hideable) holder.Secrets() SSblackbox.record_feedback("tally", "admin_verb", 1, "Secrets Panel") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! +/client/proc/poll_panel() + set name = "Server Poll Management" + set category = "Admin" + if(!check_rights(R_POLL)) + return + holder.poll_list_panel() + SSblackbox.record_feedback("tally", "admin_verb", 1, "Server Poll Management") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! /client/proc/findStealthKey(txt) if(txt) diff --git a/code/modules/admin/create_poll.dm b/code/modules/admin/create_poll.dm deleted file mode 100644 index 495ee420bfa..00000000000 --- a/code/modules/admin/create_poll.dm +++ /dev/null @@ -1,150 +0,0 @@ -/client/proc/create_poll() - set name = "Create Poll" - set category = "Admin" - if(!check_rights(R_POLL)) - return - if(!SSdbcore.Connect()) - to_chat(src, "Failed to establish database connection.", confidential = TRUE) - return - var/polltype = input("Choose poll type.","Poll Type") as null|anything in list("Single Option","Text Reply","Rating","Multiple Choice", "Instant Runoff Voting") - var/choice_amount = 0 - switch(polltype) - if("Single Option") - polltype = POLLTYPE_OPTION - if("Text Reply") - polltype = POLLTYPE_TEXT - if("Rating") - polltype = POLLTYPE_RATING - if("Multiple Choice") - polltype = POLLTYPE_MULTI - choice_amount = input("How many choices should be allowed?","Select choice amount") as num|null - switch(choice_amount) - if(0) - to_chat(src, "Multiple choice poll must have at least one choice allowed.", confidential = TRUE) - return - if(1) - polltype = POLLTYPE_OPTION - if(null) - return - if ("Instant Runoff Voting") - polltype = POLLTYPE_IRV - else - return 0 - var/starttime = SQLtime() - var/endtime = input("Set end time for poll as format YYYY-MM-DD HH:MM:SS. All times in server time. HH:MM:SS is optional and 24-hour. Must be later than starting time for obvious reasons.", "Set end time", SQLtime()) as text|null - if(!endtime) - return - endtime = sanitizeSQL(endtime) - var/datum/DBQuery/query_validate_time = SSdbcore.NewQuery("SELECT IF(STR_TO_DATE('[endtime]','%Y-%c-%d %T') > NOW(), STR_TO_DATE('[endtime]','%Y-%c-%d %T'), 0)") - if(!query_validate_time.warn_execute() || QDELETED(usr) || !src) - qdel(query_validate_time) - return - if(query_validate_time.NextRow()) - var/checktime = text2num(query_validate_time.item[1]) - if(!checktime) - to_chat(src, "Datetime entered is improperly formatted or not later than current server time.", confidential = TRUE) - qdel(query_validate_time) - return - endtime = query_validate_time.item[1] - qdel(query_validate_time) - var/adminonly - switch(alert("Admin only poll?",,"Yes","No","Cancel")) - if("Yes") - adminonly = 1 - if("No") - adminonly = 0 - else - return - var/dontshow - switch(alert("Hide poll results from tracking until completed?",,"Yes","No","Cancel")) - if("Yes") - dontshow = 1 - if("No") - dontshow = 0 - else - return - var/sql_ckey = sanitizeSQL(ckey) - var/question = input("Write your question","Question") as message|null - if(!question) - return - question = sanitizeSQL(question) - var/list/sql_option_list = list() - if(polltype != POLLTYPE_TEXT) - var/add_option = 1 - while(add_option) - var/option = input("Write your option","Option") as message|null - if(!option) - return - option = sanitizeSQL(option) - var/default_percentage_calc = 0 - if(polltype != POLLTYPE_IRV) - switch(alert("Should this option be included by default when poll result percentages are generated?",,"Yes","No","Cancel")) - if("Yes") - default_percentage_calc = 1 - if("No") - default_percentage_calc = 0 - else - return - var/minval = 0 - var/maxval = 0 - var/descmin = "" - var/descmid = "" - var/descmax = "" - if(polltype == POLLTYPE_RATING) - minval = input("Set minimum rating value.","Minimum rating") as num|null - if(minval) - minval = sanitizeSQL(minval) - else if(minval == null) - return - maxval = input("Set maximum rating value.","Maximum rating") as num|null - if(maxval) - maxval = sanitizeSQL(maxval) - if(minval >= maxval) - to_chat(src, "Maximum rating value can't be less than or equal to minimum rating value", confidential = TRUE) - continue - else if(maxval == null) - return - descmin = input("Optional: Set description for minimum rating","Minimum rating description") as message|null - if(descmin) - descmin = sanitizeSQL(descmin) - else if(descmin == null) - return - descmid = input("Optional: Set description for median rating","Median rating description") as message|null - if(descmid) - descmid = sanitizeSQL(descmid) - else if(descmid == null) - return - descmax = input("Optional: Set description for maximum rating","Maximum rating description") as message|null - if(descmax) - descmax = sanitizeSQL(descmax) - else if(descmax == null) - return - sql_option_list += list(list("text" = "'[option]'", "minval" = "'[minval]'", "maxval" = "'[maxval]'", "descmin" = "'[descmin]'", "descmid" = "'[descmid]'", "descmax" = "'[descmax]'", "default_percentage_calc" = "'[default_percentage_calc]'")) - switch(alert(" ",,"Add option","Finish", "Cancel")) - if("Add option") - add_option = 1 - if("Finish") - add_option = 0 - else - return 0 - var/m1 = "[key_name(usr)] has created a new server poll. Poll type: [polltype] - Admin Only: [adminonly ? "Yes" : "No"] - Question: [question]" - var/m2 = "[key_name_admin(usr)] has created a new server poll. Poll type: [polltype] - Admin Only: [adminonly ? "Yes" : "No"]
Question: [question]" - var/datum/DBQuery/query_polladd_question = SSdbcore.NewQuery("INSERT INTO [format_table_name("poll_question")] (polltype, starttime, endtime, question, adminonly, multiplechoiceoptions, createdby_ckey, createdby_ip, dontshow) VALUES ('[polltype]', '[starttime]', '[endtime]', '[question]', '[adminonly]', '[choice_amount]', '[sql_ckey]', INET_ATON('[address]'), '[dontshow]')") - if(!query_polladd_question.warn_execute()) - qdel(query_polladd_question) - return - qdel(query_polladd_question) - if(polltype != POLLTYPE_TEXT) - var/pollid = 0 - var/datum/DBQuery/query_get_id = SSdbcore.NewQuery("SELECT LAST_INSERT_ID()") - if(!query_get_id.warn_execute()) - qdel(query_get_id) - return - if(query_get_id.NextRow()) - pollid = query_get_id.item[1] - qdel(query_get_id) - for(var/list/i in sql_option_list) - i |= list("pollid" = "'[pollid]'") - SSdbcore.MassInsert(format_table_name("poll_option"), sql_option_list, warn = 1) - log_admin(m1) - message_admins(m2) diff --git a/code/modules/admin/poll_management.dm b/code/modules/admin/poll_management.dm new file mode 100644 index 00000000000..f7062ad1722 --- /dev/null +++ b/code/modules/admin/poll_management.dm @@ -0,0 +1,735 @@ +/** + * Datum which holds details of a running poll loaded from the database and supplementary info. + * + * Used to minimize the need for querying this data every time it's needed. + * + */ +/datum/poll_question + ///Reference list of the options for this poll, not used by text response polls. + var/list/options = list() + ///Table id of this poll, will be null until poll has been created. + var/poll_id + ///The type of poll to be created, must be POLLTYPE_OPTION, POLLTYPE_TEXT, POLLTYPE_RATING, POLLTYPE_MULTI or POLLTYPE_IRV. + var/poll_type + ///Count of how many players have voted or responded to this poll. + var/poll_votes + ///Ckey of the poll's original author + var/created_by + ///Date and time the poll opens, timestamp format is YYYY-MM-DD HH:MM:SS. + var/start_datetime + ///Date and time the poll will run until, timestamp format is YYYY-MM-DD HH:MM:SS. + var/end_datetime + ///The title text of the poll, shows up on the list of polls. + var/question + ///Supplementary text displayed only when responding to a poll. + var/subtitle + ///Hides the poll from any client without a holder datum. + var/admin_only + ///The number of responses allowed in a multiple-choice poll, more can be selected but won't be recorded. + var/options_allowed + ///Hint for statbus, not used by the game; Stops the results of a poll from being displayed until the end_datetime is reached. + var/dont_show + ///Allows a player to change their vote to a poll they've already voted on, off by default. + var/allow_revoting + ///Indicates if a poll has been submitted or loaded from the DB so the management panel will open with edit functions. + var/edit_ready = FALSE + ///Holds duration data when creating or editing a poll and refreshing the poll creation window. + var/duration + ///Holds interval data when creating or editing a poll and refreshing the poll creation window. + var/interval + ///Indicates a poll is set to not start in the future, still visible for editing but not voting on. + var/future_poll + +/** + * Datum which holds details of a poll option loaded from the database. + * + * Used to minimize the need for querying this data every time it's needed. + * + */ +/datum/poll_option + ///Reference to the poll this option belongs to + var/datum/poll_question/parent_poll + ///Table id of this option, will be null until poll has been created. + var/option_id + ///Description/name of this option + var/text + ///For rating polls, the minimum selectable value allowed; Supported value range is -2147483648 to 2147483647 + var/min_val + ///For rating polls, the maximum selectable value allowed; Supported value range is -2147483648 to 2147483647 + var/max_val + ///Optional for rating polls, description shown next to the minimum value + var/desc_min = "" + ///Optional for rating polls, description shown next to the rounded whole middle value + var/desc_mid = "" + ///Optional for rating polls, description shown next to the maximum value + var/desc_max = "" + ///Hint for statbus, not used by the game; If this option should be included by default when calculating the resulting percentages of all options for this poll + var/default_percentage_calc + +/** + * Shows a list of all current and future polls and buttons to edit or delete them or create a new poll. + * + */ +/datum/admins/proc/poll_list_panel() + var/list/output = list("Current and future polls
Note when editing polls or their options changes are not saved until you press Submit Poll.
New PollReload Polls
") + for(var/p in GLOB.polls) + var/datum/poll_question/poll = p + output += {"[poll.question] + Edit + Delete + "} + if(poll.subtitle) + output += "
[poll.subtitle]" + output += "
[poll.future_poll ? "Starts" : "Started"] at [poll.start_datetime] | Ends at [poll.end_datetime]" + if(poll.admin_only) + output += " | Admin only" + if(poll.dont_show) + output += " | Hidden from tracking until complete" + output += " | [poll.poll_votes] players have [poll.poll_type == POLLTYPE_TEXT ? "responded" : "voted"]
" + var/datum/browser/panel = new(usr, "plpanel", "Poll list Panel", 700, 400) + panel.set_content(jointext(output, "")) + panel.open() + +/** + * Show the options for creating a poll or editing its parameters along with its linked options. + * + */ +/datum/admins/proc/poll_management_panel(datum/poll_question/poll) + var/list/output = list("
[HrefTokenFormField()]") + output += {"Poll type +
+ +
+ Question + + Multiple-choice options allowed + +
+ + + +
+
+
+ Duration +
+ + +
+ +
+
+ + +
+
+ Start +
+ +
+
+ + +
+
+
+ Subtitle (Optional) +
+ +
+
+ "} + var/option_count = 0 + if(!poll) + output += {" + +
+
+
+ First enter the poll question details and press Initialize Question. +
+ Then add poll options and press Submit Poll to save and create the question and options. No options are required for Text Reply polls. +
+ Which poll type should I use? + "} + else + output += "" + if(poll.edit_ready) + output += {" +
+ "} + if(poll.poll_type == POLLTYPE_TEXT) + output += "Clear poll responses [poll.poll_votes] players have responded" + else + output += "Clear poll votes [poll.poll_votes] players have voted" + if(poll.poll_type == POLLTYPE_TEXT) + output += "" + else + output += "
Add Option
" + if(length(poll.options)) + for(var/o in poll.options) + var/datum/poll_option/option = o + option_count++ + output += {"Option [option_count] + Edit + Delete +
[option.text] + "} + if(poll.poll_type == POLLTYPE_RATING) + output += {"
Minimum value: [option.min_val] | Maximum value: [option.max_val] +
Minimum description: [option.desc_min] +
Middle description: [option.desc_mid] +
Maximum description: [option.desc_max] + "} + output += "
" + var/datum/browser/panel = new(usr, "pmpanel", "Poll Management Panel", 780, 640) + panel.add_stylesheet("admin_panelscss", 'html/admin/admin_panels.css') + if(usr.client.prefs.tgui_fancy) //some browsers (IE8) have trouble with unsupported css3 elements that break the panel's functionality, so we won't load those if a user is in no frills tgui mode since that's for similar compatability support + panel.add_stylesheet("admin_panelscss3", 'html/admin/admin_panels_css3.css') + panel.set_content(jointext(output, "")) + panel.open() + +/** + * Processes topic data from poll management panel. + * + * Reads through returned form data and assigns data to the poll datum, creating a new one if required, before passing it to be saved. + * Also does some simple error checking to ensure the poll will be valid before creation. + * + */ +/datum/admins/proc/poll_parse_href(list/href_list, datum/poll_question/poll) + if(!check_rights(R_POLL)) + return + if(!SSdbcore.Connect()) + to_chat(usr, "Failed to establish database connection.", confidential = TRUE) + return + var/list/error_state = list() + var/new_poll = FALSE + var/clear_votes = FALSE + var/submit_ready = FALSE + if(!poll) + poll = new(creator = usr.client.ckey) + new_poll = TRUE + if(new_poll) + poll.poll_type = href_list["polltype"] + switch(href_list["radioduration"]) + if("runfor") + poll.duration = text2num(href_list["duration"]) + poll.interval = href_list["durationtype"] + if("rununtil") + if(href_list["enddatetimetext"] != "YYYY-MM-DD HH:MM:SS") + poll.duration = href_list["enddatetimetext"] + if(new_poll) + poll.end_datetime = poll.duration + if(!poll.duration) + error_state += "No duration was provided." + switch(href_list["radiostart"]) + if("startnow") + poll.start_datetime = null + if("startdatetime") + if(href_list["startdatetimetext"] && href_list["startdatetimetext"] != "YYYY-MM-DD HH:MM:SS") + poll.start_datetime = href_list["startdatetimetext"] + else + error_state += "Start datetime was selected but none was provided." + if(href_list["question"]) + poll.question = href_list["question"] + else + error_state += "No question was provided." + poll.subtitle = href_list["subtitle"] + if(href_list["adminonly"]) + poll.admin_only = TRUE + else + poll.admin_only = FALSE + if(href_list["dontshow"]) + poll.dont_show = TRUE + else + poll.dont_show = FALSE + if(href_list["allowrevoting"]) + poll.allow_revoting = TRUE + else + poll.allow_revoting = FALSE + if(href_list["clearvotesedit"]) + clear_votes = TRUE + if(href_list["submitpoll"]) + submit_ready = TRUE + if(poll.poll_type == POLLTYPE_MULTI) + if(text2num(href_list["optionsallowed"])) + poll.options_allowed = text2num(href_list["optionsallowed"]) + if(poll.options_allowed == 1) + error_state += "Multiple choice polls require more than one option allowed, use a standard option poll for singlular voting." + if(poll.options_allowed < 0) + error_state += "Multiple choice options allowed cannot be negative." + else + error_state += "Multiple choice poll was selected but no number of allowed options was provided." + if(submit_ready && poll.poll_type != POLLTYPE_TEXT && !length(poll.options)) + error_state += "This poll type requires at least one option." + if(error_state.len) + if(poll.edit_ready) + to_chat(usr, "Not all edits were applied because the following errors were present:\n[error_state.Join("\n")]", confidential = TRUE) + else + to_chat(usr, "Poll not [new_poll ? "initialized" : "submitted"] because the following errors were present:\n[error_state.Join("\n")]", confidential = TRUE) + if(new_poll) + qdel(poll) + return + if(submit_ready) + var/db = poll.edit_ready //if the poll is new it will need its options inserted for the first time + poll.save_poll_data(clear_votes) + if(!db) + poll.save_all_options() + poll_management_panel(poll) + +/datum/poll_question/New(id, polltype, starttime, endtime, question, subtitle, adminonly, multiplechoiceoptions, dontshow, allow_revoting, vote_count, creator, future, dbload = FALSE) + poll_id = text2num(id) + poll_type = polltype + start_datetime = starttime + end_datetime = endtime + src.question = question + src.subtitle = subtitle + admin_only = text2num(adminonly) + options_allowed = text2num(multiplechoiceoptions) + dont_show = text2num(dontshow) + src.allow_revoting = text2num(allow_revoting) + poll_votes = text2num(vote_count) || 0 + created_by = creator + future_poll = text2num(future) + edit_ready = dbload + GLOB.polls += src + +/datum/poll_question/Destroy() + GLOB.polls -= src + return ..() + +/** + * Sets a poll and its associated data as deleted in the database. + * + * Calls the procedure set_poll_deleted to set the deleted column to 1 for each row in the poll_ tables matching the poll id used. + * Then deletes each option datum and finally the poll itself. + * + */ +/datum/poll_question/proc/delete_poll() + if(!check_rights(R_POLL)) + return + if(!SSdbcore.Connect()) + to_chat(usr, "Failed to establish database connection.", confidential = TRUE) + return + var/datum/DBQuery/query_delete_poll = SSdbcore.NewQuery("CALL set_poll_deleted('[sanitizeSQL(poll_id)]')") + if(!query_delete_poll.warn_execute()) + qdel(query_delete_poll) + return + qdel(query_delete_poll) + for(var/o in options) + var/datum/poll_option/option = o + qdel(option) + GLOB.polls -= src + qdel(src) + +/** + * Inserts or updates a poll question to the database. + * + * Uses INSERT ON DUPLICATE KEY UPDATE to handle both inserting and updating at once. + * The start and end datetimes and poll id for new polls is then retrieved for the poll datum. + * Arguments: + * * clear_votes - When true will call clear_poll_votes() to delete all votes matching this poll id. + * + */ +/datum/poll_question/proc/save_poll_data(clear_votes) + if(!check_rights(R_POLL)) + return + if(!SSdbcore.Connect()) + to_chat(usr, "Failed to establish database connection.", confidential = TRUE) + return + var/poll_id_sql = "[sanitizeSQL(poll_id)]" + var/new_poll = FALSE + if(!poll_id_sql) + poll_id_sql = "NULL" + new_poll = TRUE + var/poll_type_sql = sanitizeSQL(poll_type) + var/question_sql = sanitizeSQL(question) + var/subtitle_sql = sanitizeSQL(subtitle) + var/admin_only_sql = sanitizeSQL(admin_only) + var/options_allowed_sql = "[sanitizeSQL(options_allowed)]" + if(poll_type != POLLTYPE_MULTI) + options_allowed_sql = "NULL" + var/dont_show_sql = sanitizeSQL(dont_show) + var/allow_revoting_sql = sanitizeSQL(allow_revoting) + var/admin_ckey = sanitizeSQL(created_by) + var/admin_ip = sanitizeSQL(usr.client.address) + var/end_datetime_sql + if(interval) + end_datetime_sql = "NOW() + INTERVAL [sanitizeSQL(duration)] [sanitizeSQL(interval)]" + else + end_datetime_sql = "'[sanitizeSQL(duration)]'" + var/start_datetime_sql + if(!start_datetime) + start_datetime_sql = "NOW()" + else + start_datetime_sql = "'[sanitizeSQL(start_datetime)]'" + var/kn = key_name(usr) + var/kna = key_name_admin(usr) + var/datum/DBQuery/query_save_poll = SSdbcore.NewQuery("INSERT INTO [format_table_name("poll_question")] (id, polltype, created_datetime, starttime, endtime, question, subtitle, adminonly, multiplechoiceoptions, createdby_ckey, createdby_ip, dontshow, allow_revoting) VALUES ([poll_id_sql], '[poll_type_sql]', NOW(), [start_datetime_sql], [end_datetime_sql], '[question_sql]', '[subtitle_sql]', '[admin_only_sql]', [options_allowed_sql], '[admin_ckey]', INET_ATON('[admin_ip]'), '[dont_show_sql]', '[allow_revoting_sql]') ON DUPLICATE KEY UPDATE starttime = [start_datetime_sql], endtime = [end_datetime_sql], question = '[question_sql]', subtitle = '[subtitle_sql]', adminonly = '[admin_only_sql]', multiplechoiceoptions = [options_allowed_sql], dontshow = '[dont_show_sql]', allow_revoting = '[allow_revoting_sql]'") + if(!query_save_poll.warn_execute()) + qdel(query_save_poll) + return + qdel(query_save_poll) + if(poll_id_sql == "NULL") + poll_id_sql = "LAST_INSERT_ID()" + var/datum/DBQuery/query_get_poll_id_start_endtime = SSdbcore.NewQuery("SELECT LAST_INSERT_ID(), starttime, endtime, IF(starttime > NOW(), 1, 0) FROM [format_table_name("poll_question")] WHERE id = [poll_id_sql]") + if(!query_get_poll_id_start_endtime.warn_execute()) + qdel(query_get_poll_id_start_endtime) + return + if(query_get_poll_id_start_endtime.NextRow()) + if(!poll_id) + poll_id = text2num(query_get_poll_id_start_endtime.item[1]) + start_datetime = query_get_poll_id_start_endtime.item[2] + end_datetime = query_get_poll_id_start_endtime.item[3] + future_poll = text2num(query_get_poll_id_start_endtime.item[4]) + qdel(query_get_poll_id_start_endtime) + if(clear_votes) + clear_poll_votes() + edit_ready = TRUE + var/msg = "has [new_poll ? "created a new" : "edited a"][admin_only ? " admin only" : ""] server poll. Question: [question]" + if(admin_only) + log_admin_private("[kn] [msg]") + else + log_admin("[kn] [msg]") + message_admins("[kna] [msg]") + +/** + * Saves all options of a poll to the database. + * + * Saves all the created options for a poll when it's submitted to the DB for the first time and associated an id with the options. + * Insertion and id querying for each option is done separately to ensure data integrity; this is less performant, but not significantly. + * Using MassInsert() would mean having to query a list of rows by poll_id or matching by fields afterwards, which doesn't guarantee accuracy. + * + */ +/datum/poll_question/proc/save_all_options() + if(!SSdbcore.Connect()) + to_chat(usr, "Failed to establish database connection.", confidential = TRUE) + return + for(var/o in options) + var/datum/poll_option/option = o + option.save_option() + var/datum/DBQuery/query_get_option_id = SSdbcore.NewQuery("SELECT LAST_INSERT_ID()") + if(!query_get_option_id.warn_execute()) + qdel(query_get_option_id) + return + if(query_get_option_id.NextRow()) + option.option_id = text2num(query_get_option_id.item[1]) + qdel(query_get_option_id) + +/** + * Deletes all votes or text replies for this poll, depending on its type. + * + */ +/datum/poll_question/proc/clear_poll_votes() + if(!check_rights(R_POLL)) + return + if(!SSdbcore.Connect()) + to_chat(usr, "Failed to establish database connection.", confidential = TRUE) + return + var/table = "poll_vote" + if(poll_type == POLLTYPE_TEXT) + table = "poll_textreply" + var/datum/DBQuery/query_clear_poll_votes = SSdbcore.NewQuery("UPDATE [format_table_name("[table]")] SET deleted = 1 WHERE pollid = [sanitizeSQL(poll_id)]") + if(!query_clear_poll_votes.warn_execute()) + qdel(query_clear_poll_votes) + return + qdel(query_clear_poll_votes) + poll_votes = 0 + to_chat(usr, "Poll [poll_type == POLLTYPE_TEXT ? "responses" : "votes"] cleared.", confidential = TRUE) + +/** + * Show the options for creating a poll option or editing its parameters. + * + */ +/datum/admins/proc/poll_option_panel(datum/poll_question/poll, datum/poll_option/option) + var/list/output = list("
[HrefTokenFormField()]") + output += {" Option for poll [poll.question] +
+ +
+ "} + if(poll.poll_type == POLLTYPE_RATING) + output += {"Minimum value + + Maximum Value + +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ "} + output += {"
+ + + + "} + var/panel_height = 180 + if(poll.poll_type == POLLTYPE_RATING) + panel_height = 320 + var/datum/browser/panel = new(usr, "popanel", "Poll Option Panel", 370, panel_height) + panel.add_stylesheet("admin_panelscss", 'html/admin/admin_panels.css') + if(usr.client.prefs.tgui_fancy) //some browsers (IE8) have trouble with unsupported css3 elements that break the panel's functionality, so we won't load those if a user is in no frills tgui mode since that's for similar compatability support + panel.add_stylesheet("admin_panelscss3", 'html/admin/admin_panels_css3.css') + panel.set_content(jointext(output, "")) + panel.open() + +/** + * Processes topic data from poll option panel. + * + * Reads through returned form data and assigns data to the option datum, creating a new one if required, before passing it to be saved. + * Also does some simple error checking to ensure the option will be valid before creation. + * + */ +/datum/admins/proc/poll_option_parse_href(list/href_list, datum/poll_question/poll, datum/poll_option/option) + if(!check_rights(R_POLL)) + return + if(!SSdbcore.Connect()) + to_chat(usr, "Failed to establish database connection.", confidential = TRUE) + return + var/list/error_state = list() + var/new_option = FALSE + if(!option) + option = new() + new_option = TRUE + if(href_list["optiontext"]) + option.text = href_list["optiontext"] + else + error_state += "No option text was provided." + if(href_list["defpercalc"]) + option.default_percentage_calc = TRUE + else + option.default_percentage_calc = FALSE + if(poll.poll_type == POLLTYPE_RATING) + var/value_in_range = text2num(href_list["minval"]) + if(href_list["minval"]) + if(ISINRANGE(value_in_range, -2147483647, 2147483647)) + option.min_val = value_in_range + else + error_state += "Minimum value out of range." + else + error_state += "No minimum value was provided." + value_in_range = text2num(href_list["maxval"]) + if(href_list["maxval"]) + if(ISINRANGE(value_in_range, -2147483647, 2147483647)) + if(value_in_range < option.min_val) + error_state += "Maximum value is less than minimum value." + else + option.max_val = value_in_range + else + error_state += "Maximum value out of range." + else + error_state += "No maximum value was provided." + if(href_list["descmincheck"]) + if(href_list["descmintext"]) + option.desc_min = href_list["descmintext"] + else + error_state += "Minimum value description was selected but not provided." + else + option.desc_min = null + if(href_list["descmidcheck"]) + if(href_list["descmidtext"]) + option.desc_mid = href_list["descmidtext"] + else + error_state += "Middle value description was selected but not provided." + else + option.desc_mid = null + if(href_list["descmaxcheck"]) + if(href_list["descmaxtext"]) + option.desc_max = href_list["descmaxtext"] + else + error_state += "Maximum value description was selected but not provided." + else + option.desc_max = null + if(error_state.len) + if(new_option) + to_chat(usr, "Option not added because the following errors were present:\n[error_state.Join("\n")]", confidential = TRUE) + qdel(option) + else + to_chat(usr, "Not all edits were applied because the following errors were present:\n[error_state.Join("\n")]", confidential = TRUE) + return + if(new_option) + poll.options += option + option.parent_poll = poll + if(poll.edit_ready) + option.save_option() + poll_management_panel(poll) + +/datum/poll_option/New(id, text, minval, maxval, descmin, descmid, descmax, default_percentage_calc) + option_id = text2num(id) + src.text = text + min_val = text2num(minval) + max_val = text2num(maxval) + desc_min = descmin + desc_mid = descmid + desc_max = descmax + src.default_percentage_calc = text2num(default_percentage_calc) + GLOB.poll_options += src + +/datum/poll_option/Destroy() + parent_poll.options -= src + parent_poll = null + GLOB.poll_options -= src + return ..() + +/** + * Inserts or updates a poll option to the database. + * + * Uses INSERT ON DUPLICATE KEY UPDATE to handle both inserting and updating at once. + * The list of columns and values is built dynamically to avoid excess data being sent when not a rating type poll. + * + */ +/datum/poll_option/proc/save_option() + if(!check_rights(R_POLL)) + return + if(!SSdbcore.Connect()) + to_chat(usr, "Failed to establish database connection.", confidential = TRUE) + return + var/list/columns = list("text", "default_percentage_calc", "pollid", "id") + var/list/values = list("'[sanitizeSQL(text)]'", "[sanitizeSQL(default_percentage_calc)]", "[sanitizeSQL(parent_poll.poll_id)]") + if(option_id) + values += "[sanitizeSQL(option_id)]" + else + values += "NULL" + if(parent_poll.poll_type == POLLTYPE_RATING) + columns.Add("minval", "maxval", "descmin", "descmid", "descmax") + values.Add("[sanitizeSQL(min_val)]", "[sanitizeSQL(max_val)]") + if(desc_min) + values += "'[sanitizeSQL(desc_min)]'" + else + values += "NULL" + if(desc_mid) + values += "'[sanitizeSQL(desc_mid)]'" + else + values += "NULL" + if(desc_max) + values += "'[sanitizeSQL(desc_max)]'" + else + values += "NULL" + var/list/update_data = list() + var/count = 0 + for(var/i in columns) + count++ + if(i == "pollid" || i == "id") //we don't want to update the pollid or option id so skip including those + continue + update_data += "[i] = [values[count]]" + var/datum/DBQuery/query_update_poll_option = SSdbcore.NewQuery("INSERT INTO [format_table_name("poll_option")] ([jointext(columns, ",")]) VALUES ([jointext(values, ",")]) ON DUPLICATE KEY UPDATE [jointext(update_data, ", ")]") + if(!query_update_poll_option.warn_execute()) + qdel(query_update_poll_option) + return + qdel(query_update_poll_option) + +/** + * Sets a poll option and its votes as deleted in the database then deletes its datum. + * + */ +/datum/poll_option/proc/delete_option() + if(!check_rights(R_POLL)) + return + . = parent_poll + if(option_id) + if(!SSdbcore.Connect()) + to_chat(usr, "Failed to establish database connection.", confidential = TRUE) + return + var/datum/DBQuery/query_delete_poll_option = SSdbcore.NewQuery("UPDATE [format_table_name("poll_option")] AS o INNER JOIN [format_table_name("poll_vote")] AS v ON o.id = v.optionid SET o.deleted = 1, v.deleted = 1 WHERE o.id = [sanitizeSQL(option_id)]") + if(!query_delete_poll_option.warn_execute()) + qdel(query_delete_poll_option) + return + qdel(query_delete_poll_option) + qdel(src) + +/** + * Loads all current and future server polls and their options to store both as datums. + * + */ +/proc/load_poll_data() + if(!SSdbcore.Connect()) + to_chat(usr, "Failed to establish database connection.", confidential = TRUE) + return + var/datum/DBQuery/query_load_polls = SSdbcore.NewQuery("SELECT id, polltype, starttime, endtime, question, subtitle, adminonly, multiplechoiceoptions, dontshow, allow_revoting, IF(polltype='TEXT',(SELECT COUNT(ckey) FROM [format_table_name("poll_textreply")] AS t WHERE t.pollid = q.id AND deleted = 0), (SELECT COUNT(DISTINCT ckey) FROM [format_table_name("poll_vote")] AS v WHERE v.pollid = q.id AND deleted = 0)), IFNULL((SELECT byond_key FROM [format_table_name("player")] AS p WHERE p.ckey = q.createdby_ckey), createdby_ckey), IF(starttime > NOW(), 1, 0) FROM [format_table_name("poll_question")] AS q WHERE NOW() < endtime AND deleted = 0") + if(!query_load_polls.Execute()) + qdel(query_load_polls) + return + var/list/poll_ids = list() + while(query_load_polls.NextRow()) + new /datum/poll_question(query_load_polls.item[1], query_load_polls.item[2], query_load_polls.item[3], query_load_polls.item[4], query_load_polls.item[5], query_load_polls.item[6], query_load_polls.item[7], query_load_polls.item[8], query_load_polls.item[9], query_load_polls.item[10], query_load_polls.item[11], query_load_polls.item[12], query_load_polls.item[13], TRUE) + poll_ids += query_load_polls.item[1] + qdel(query_load_polls) + if(length(poll_ids)) + var/datum/DBQuery/query_load_poll_options = SSdbcore.NewQuery("SELECT id, text, minval, maxval, descmin, descmid, descmax, default_percentage_calc, pollid FROM [format_table_name("poll_option")] WHERE pollid IN ([jointext(poll_ids, ",")])") + if(!query_load_poll_options.Execute()) + qdel(query_load_poll_options) + return + while(query_load_poll_options.NextRow()) + var/datum/poll_option/option = new(query_load_poll_options.item[1], query_load_poll_options.item[2], query_load_poll_options.item[3], query_load_poll_options.item[4], query_load_poll_options.item[5], query_load_poll_options.item[6], query_load_poll_options.item[7], query_load_poll_options.item[8]) + var/option_poll_id = text2num(query_load_poll_options.item[9]) + for(var/q in GLOB.polls) + var/datum/poll_question/poll = q + if(poll.poll_id == option_poll_id) + poll.options += option + option.parent_poll = poll + qdel(query_load_poll_options) diff --git a/code/modules/admin/sql_ban_system.dm b/code/modules/admin/sql_ban_system.dm index afdebdc9f9d..733e8f06ebc 100644 --- a/code/modules/admin/sql_ban_system.dm +++ b/code/modules/admin/sql_ban_system.dm @@ -86,9 +86,10 @@ if(edit_id) panel_height = 240 var/datum/browser/panel = new(usr, "banpanel", "Banning Panel", 910, panel_height) + panel.add_stylesheet("admin_panelscss", 'html/admin/admin_panels.css') panel.add_stylesheet("banpanelcss", 'html/admin/banpanel.css') if(usr.client.prefs.tgui_fancy) //some browsers (IE8) have trouble with unsupported css3 elements and DOM methods that break the panel's functionality, so we won't load those if a user is in no frills tgui mode since that's for similar compatability support - panel.add_stylesheet("banpanelcss3", 'html/admin/banpanel_css3.css') + panel.add_stylesheet("admin_panelscss3", 'html/admin/admin_panels_css3.css') panel.add_script("banpaneljs", 'html/admin/banpanel.js') var/list/output = list("[HrefTokenFormField()]") output += {" diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index 0d3563f55bf..78e58bf998a 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -2202,6 +2202,55 @@ else if(href_list["beakerpanel"]) beaker_panel_act(href_list) + else if(href_list["reloadpolls"]) + GLOB.polls.Cut() + GLOB.poll_options.Cut() + load_poll_data() + poll_list_panel() + + else if(href_list["newpoll"]) + poll_management_panel() + + else if(href_list["editpoll"]) + var/datum/poll_question/poll = locate(href_list["editpoll"]) in GLOB.polls + poll_management_panel(poll) + + else if(href_list["deletepoll"]) + var/datum/poll_question/poll = locate(href_list["deletepoll"]) in GLOB.polls + poll.delete_poll() + poll_list_panel() + + else if(href_list["initializepoll"]) + poll_parse_href(href_list) + + else if(href_list["submitpoll"]) + var/datum/poll_question/poll = locate(href_list["submitpoll"]) in GLOB.polls + poll_parse_href(href_list, poll) + + else if(href_list["clearpollvotes"]) + var/datum/poll_question/poll = locate(href_list["clearpollvotes"]) in GLOB.polls + poll.clear_poll_votes() + poll_management_panel(poll) + + else if(href_list["addpolloption"]) + var/datum/poll_question/poll = locate(href_list["addpolloption"]) in GLOB.polls + poll_option_panel(poll) + + else if(href_list["editpolloption"]) + var/datum/poll_option/option = locate(href_list["editpolloption"]) in GLOB.poll_options + var/datum/poll_question/poll = locate(href_list["parentpoll"]) in GLOB.polls + poll_option_panel(poll, option) + + else if(href_list["deletepolloption"]) + var/datum/poll_option/option = locate(href_list["deletepolloption"]) in GLOB.poll_options + var/datum/poll_question/poll = option.delete_option() + poll_management_panel(poll) + + else if(href_list["submitoption"]) + var/datum/poll_option/option = locate(href_list["submitoption"]) in GLOB.poll_options + var/datum/poll_question/poll = locate(href_list["submitoptionpoll"]) in GLOB.polls + poll_option_parse_href(href_list, poll, option) + /datum/admins/proc/HandleCMode() if(!check_rights(R_ADMIN)) return diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm index ad3ebb912fb..86ab731eb15 100644 --- a/code/modules/mob/dead/new_player/new_player.dm +++ b/code/modules/mob/dead/new_player/new_player.dm @@ -58,20 +58,19 @@ if(!IsGuestKey(src.key)) if (SSdbcore.Connect()) - var/isadmin = 0 - if(src.client && src.client.holder) - isadmin = 1 - var/datum/DBQuery/query_get_new_polls = SSdbcore.NewQuery("SELECT id FROM [format_table_name("poll_question")] WHERE [(isadmin ? "" : "adminonly = false AND")] Now() BETWEEN starttime AND endtime AND id NOT IN (SELECT pollid FROM [format_table_name("poll_vote")] WHERE ckey = \"[sanitizeSQL(ckey)]\") AND id NOT IN (SELECT pollid FROM [format_table_name("poll_textreply")] WHERE ckey = \"[sanitizeSQL(ckey)]\")") + var/isadmin = FALSE + if(client?.holder) + isadmin = TRUE + var/sql_ckey = sanitizeSQL(ckey) + var/datum/DBQuery/query_get_new_polls = SSdbcore.NewQuery("SELECT id FROM [format_table_name("poll_question")] WHERE [(isadmin ? "" : "adminonly = 0 AND")] Now() BETWEEN starttime AND endtime AND deleted = 0 AND id NOT IN (SELECT pollid FROM [format_table_name("poll_vote")] WHERE ckey = '[sql_ckey]' AND deleted = 0) AND id NOT IN (SELECT pollid FROM [format_table_name("poll_textreply")] WHERE ckey = '[sql_ckey]' AND deleted = 0)") var/rs = REF(src) - if(query_get_new_polls.Execute()) - var/newpoll = 0 - if(query_get_new_polls.NextRow()) - newpoll = 1 - - if(newpoll) - output += "

Show Player Polls (NEW!)

" - else - output += "

Show Player Polls

" + if(!query_get_new_polls.Execute()) + qdel(query_get_new_polls) + return + if(query_get_new_polls.NextRow()) + output += "

Show Player Polls (NEW!)

" + else + output += "

Show Player Polls

" qdel(query_get_new_polls) if(QDELETED(src)) return @@ -174,85 +173,13 @@ handle_player_polling() return - if(href_list["pollid"]) - var/pollid = href_list["pollid"] - if(istext(pollid)) - pollid = text2num(pollid) - if(isnum(pollid) && ISINTEGER(pollid)) - src.poll_player(pollid) - return + if(href_list["viewpoll"]) + var/datum/poll_question/poll = locate(href_list["viewpoll"]) in GLOB.polls + poll_player(poll) - if(href_list["votepollid"] && href_list["votetype"]) - var/pollid = text2num(href_list["votepollid"]) - var/votetype = href_list["votetype"] - //lets take data from the user to decide what kind of poll this is, without validating it - //what could go wrong - switch(votetype) - if(POLLTYPE_OPTION) - var/optionid = text2num(href_list["voteoptionid"]) - if(vote_on_poll(pollid, optionid)) - to_chat(usr, "Vote successful.") - else - to_chat(usr, "Vote failed, please try again or contact an administrator.") - if(POLLTYPE_TEXT) - var/replytext = href_list["replytext"] - if(log_text_poll_reply(pollid, replytext)) - to_chat(usr, "Feedback logging successful.") - else - to_chat(usr, "Feedback logging failed, please try again or contact an administrator.") - if(POLLTYPE_RATING) - var/id_min = text2num(href_list["minid"]) - var/id_max = text2num(href_list["maxid"]) - - if( (id_max - id_min) > 100 ) //Basic exploit prevention - //(protip, this stops no exploits) - to_chat(usr, "The option ID difference is too big. Please contact administration or the database admin.") - return - - for(var/optionid = id_min; optionid <= id_max; optionid++) - if(!isnull(href_list["o[optionid]"])) //Test if this optionid was replied to - var/rating - if(href_list["o[optionid]"] == "abstain") - rating = null - else - rating = text2num(href_list["o[optionid]"]) - if(!isnum(rating) || !ISINTEGER(rating)) - return - - if(!vote_on_numval_poll(pollid, optionid, rating)) - to_chat(usr, "Vote failed, please try again or contact an administrator.") - return - to_chat(usr, "Vote successful.") - if(POLLTYPE_MULTI) - var/id_min = text2num(href_list["minoptionid"]) - var/id_max = text2num(href_list["maxoptionid"]) - - if( (id_max - id_min) > 100 ) //Basic exploit prevention - to_chat(usr, "The option ID difference is too big. Please contact administration or the database admin.") - return - - for(var/optionid = id_min; optionid <= id_max; optionid++) - if(!isnull(href_list["option_[optionid]"])) //Test if this optionid was selected - var/i = vote_on_multi_poll(pollid, optionid) - switch(i) - if(0) - continue - if(1) - to_chat(usr, "Vote failed, please try again or contact an administrator.") - return - if(2) - to_chat(usr, "Maximum replies reached.") - break - to_chat(usr, "Vote successful.") - if(POLLTYPE_IRV) - if (!href_list["IRVdata"]) - to_chat(src, "No ordering data found. Please try again or contact an administrator.") - return - var/list/votelist = splittext(href_list["IRVdata"], ",") - if (!vote_on_irv_poll(pollid, votelist)) - to_chat(src, "Vote failed, please try again or contact an administrator.") - return - to_chat(src, "Vote successful.") + if(href_list["votepollref"]) + var/datum/poll_question/poll = locate(href_list["votepollref"]) in GLOB.polls + vote_on_poll_handler(poll, href_list) //When you cop out of the round (NB: this HAS A SLEEP FOR PLAYER INPUT IN IT) /mob/dead/new_player/proc/make_me_an_observer() diff --git a/code/modules/mob/dead/new_player/poll.dm b/code/modules/mob/dead/new_player/poll.dm index c729fb7fc1e..869ce4cad8f 100644 --- a/code/modules/mob/dead/new_player/poll.dm +++ b/code/modules/mob/dead/new_player/poll.dm @@ -1,620 +1,510 @@ -/datum/polloption - var/optionid - var/optiontext - +/** + * Shows a list of currently running polls a player can vote/has voted on + * + */ /mob/dead/new_player/proc/handle_player_polling() - if(!SSdbcore.IsConnected()) - to_chat(usr, "Failed to establish database connection.") - return - var/datum/DBQuery/query_poll_get = SSdbcore.NewQuery("SELECT id, question FROM [format_table_name("poll_question")] WHERE Now() BETWEEN starttime AND endtime [(client.holder ? "" : "AND adminonly = false")]") - if(!query_poll_get.warn_execute()) - qdel(query_poll_get) - return - var/output = "
Player polls
" - var/i = 0 + var/list/output = list("
Player polls
") var/rs = REF(src) - while(query_poll_get.NextRow()) - var/pollid = query_poll_get.item[1] - var/pollquestion = query_poll_get.item[2] - output += "" - i++ - qdel(query_poll_get) + for(var/p in GLOB.polls) + var/datum/poll_question/poll = p + if((poll.admin_only && !client.holder) || poll.future_poll) + continue + output += "" output += "
[pollquestion]
[poll.question]
" - if(!QDELETED(src)) - src << browse(output,"window=playerpolllist;size=500x300") + src << browse(jointext(output, ""),"window=playerpolllist;size=500x300") -/mob/dead/new_player/proc/poll_player(pollid) - if(!pollid) +/** + * Redirects a player to the correct poll window based on poll type. + * + */ +/mob/dead/new_player/proc/poll_player(datum/poll_question/poll) + if(!poll) return - if (!SSdbcore.Connect()) - to_chat(usr, "Failed to establish database connection.") + if(!SSdbcore.Connect()) + to_chat(src, "Failed to establish database connection.") return - var/datum/DBQuery/query_poll_get_details = SSdbcore.NewQuery("SELECT starttime, endtime, question, polltype, multiplechoiceoptions FROM [format_table_name("poll_question")] WHERE id = [pollid]") - if(!query_poll_get_details.warn_execute()) - qdel(query_poll_get_details) - return - var/pollstarttime = "" - var/pollendtime = "" - var/pollquestion = "" - var/polltype = "" - var/multiplechoiceoptions = 0 - if(query_poll_get_details.NextRow()) - pollstarttime = query_poll_get_details.item[1] - pollendtime = query_poll_get_details.item[2] - pollquestion = query_poll_get_details.item[3] - polltype = query_poll_get_details.item[4] - multiplechoiceoptions = text2num(query_poll_get_details.item[5]) - qdel(query_poll_get_details) - switch(polltype) + switch(poll.poll_type) if(POLLTYPE_OPTION) - var/datum/DBQuery/query_option_get_votes = SSdbcore.NewQuery("SELECT optionid FROM [format_table_name("poll_vote")] WHERE pollid = [pollid] AND ckey = '[ckey]'") - if(!query_option_get_votes.warn_execute()) - qdel(query_option_get_votes) - return - var/votedoptionid = 0 - if(query_option_get_votes.NextRow()) - votedoptionid = text2num(query_option_get_votes.item[1]) - qdel(query_option_get_votes) - var/list/datum/polloption/options = list() - var/datum/DBQuery/query_option_options = SSdbcore.NewQuery("SELECT id, text FROM [format_table_name("poll_option")] WHERE pollid = [pollid]") - if(!query_option_options.warn_execute()) - qdel(query_option_options) - return - while(query_option_options.NextRow()) - var/datum/polloption/PO = new() - PO.optionid = text2num(query_option_options.item[1]) - PO.optiontext = query_option_options.item[2] - options += PO - qdel(query_option_options) - var/output = "
Player poll
" - output += "Question: [pollquestion]
" - output += "Poll runs from [pollstarttime] until [pollendtime]

" - if(!votedoptionid) - output += "" - output += "" - output += "" - output += "" - output += "
" - for(var/datum/polloption/O in options) - if(O.optionid && O.optiontext) - if(votedoptionid) - if(votedoptionid == O.optionid) - output += "[O.optiontext]
" - else - output += "[O.optiontext]
" - else - output += "[O.optiontext]
" - output += "
" - if(!votedoptionid) - output += "

" - output += "

" - output += "
" - src << browse(null ,"window=playerpolllist") - src << browse(output,"window=playerpoll;size=500x250") + poll_player_option(poll) if(POLLTYPE_TEXT) - var/datum/DBQuery/query_text_get_votes = SSdbcore.NewQuery("SELECT replytext FROM [format_table_name("poll_textreply")] WHERE pollid = [pollid] AND ckey = '[ckey]'") - if(!query_text_get_votes.warn_execute()) - qdel(query_text_get_votes) - return - var/vote_text = "" - if(query_text_get_votes.NextRow()) - vote_text = query_text_get_votes.item[1] - qdel(query_text_get_votes) - var/output = "
Player poll
" - output += "Question: [pollquestion]
" - output += "Feedback gathering runs from [pollstarttime] until [pollendtime]

" - output += "

" - output += "" - output += "" - output += "" - output += "Please provide feedback below. You can use any letters of the English alphabet, numbers and the symbols: . , ! ? : ; -
" - output += "" - output += "

" - output += "
" - output += "" - output += "" - output += "" - output += "" - output += "
" - - src << browse(null ,"window=playerpolllist") - src << browse(output,"window=playerpoll;size=500x500") + poll_player_text(poll) if(POLLTYPE_RATING) - var/datum/DBQuery/query_rating_get_votes = SSdbcore.NewQuery("SELECT o.text, v.rating FROM [format_table_name("poll_option")] o, [format_table_name("poll_vote")] v WHERE o.pollid = [pollid] AND v.ckey = '[ckey]' AND o.id = v.optionid") - if(!query_rating_get_votes.warn_execute()) - qdel(query_rating_get_votes) - return - var/output = "
Player poll
" - output += "Question: [pollquestion]
" - output += "Poll runs from [pollstarttime] until [pollendtime]

" - var/rating - while(query_rating_get_votes.NextRow()) - var/optiontext = query_rating_get_votes.item[1] - rating = query_rating_get_votes.item[2] - output += "
[optiontext] - [rating]" - qdel(query_rating_get_votes) - if(!rating) - output += "

" - output += "" - output += "" - output += "" - var/minid = 999999 - var/maxid = 0 - var/datum/DBQuery/query_rating_options = SSdbcore.NewQuery("SELECT id, text, minval, maxval, descmin, descmid, descmax FROM [format_table_name("poll_option")] WHERE pollid = [pollid]") - if(!query_rating_options.warn_execute()) - qdel(query_rating_options) - return - while(query_rating_options.NextRow()) - var/optionid = text2num(query_rating_options.item[1]) - var/optiontext = query_rating_options.item[2] - var/minvalue = text2num(query_rating_options.item[3]) - var/maxvalue = text2num(query_rating_options.item[4]) - var/descmin = query_rating_options.item[5] - var/descmid = query_rating_options.item[6] - var/descmax = query_rating_options.item[7] - if(optionid < minid) - minid = optionid - if(optionid > maxid) - maxid = optionid - var/midvalue = round( (maxvalue + minvalue) / 2) - output += "
[optiontext]: " - qdel(query_rating_options) - output += "" - output += "" - output += "

" - if(!QDELETED(src)) - src << browse(null ,"window=playerpolllist") - src << browse(output,"window=playerpoll;size=500x500") + poll_player_rating(poll) if(POLLTYPE_MULTI) - var/datum/DBQuery/query_multi_get_votes = SSdbcore.NewQuery("SELECT optionid FROM [format_table_name("poll_vote")] WHERE pollid = [pollid] AND ckey = '[ckey]'") - if(!query_multi_get_votes.warn_execute()) - qdel(query_multi_get_votes) - return - var/list/votedfor = list() - while(query_multi_get_votes.NextRow()) - votedfor.Add(text2num(query_multi_get_votes.item[1])) - qdel(query_multi_get_votes) - var/list/datum/polloption/options = list() - var/maxoptionid = 0 - var/minoptionid = 0 - var/datum/DBQuery/query_multi_options = SSdbcore.NewQuery("SELECT id, text FROM [format_table_name("poll_option")] WHERE pollid = [pollid]") - if(!query_multi_options.warn_execute()) - qdel(query_multi_options) - return - while(query_multi_options.NextRow()) - var/datum/polloption/PO = new() - PO.optionid = text2num(query_multi_options.item[1]) - PO.optiontext = query_multi_options.item[2] - if(PO.optionid > maxoptionid) - maxoptionid = PO.optionid - if(PO.optionid < minoptionid || !minoptionid) - minoptionid = PO.optionid - options += PO - qdel(query_multi_options) - var/output = "
Player poll
" - output += "Question: [pollquestion]
You can select up to [multiplechoiceoptions] options. If you select more, the first [multiplechoiceoptions] will be saved.
" - output += "Poll runs from [pollstarttime] until [pollendtime]

" - if(!votedfor.len) - output += "

" - output += "" - output += "" - output += "" - output += "" - output += "" - output += "
" - for(var/datum/polloption/O in options) - if(O.optionid && O.optiontext) - if(votedfor.len) - if(O.optionid in votedfor) - output += "[O.optiontext]
" - else - output += "[O.optiontext]
" - else - output += "[O.optiontext]
" - output += "
" - if(!votedfor.len) - output += "

" - output += "
" - src << browse(null ,"window=playerpolllist") - src << browse(output,"window=playerpoll;size=500x250") + poll_player_multi(poll) if(POLLTYPE_IRV) - var/datum/asset/irv_assets = get_asset_datum(/datum/asset/group/IRV) - irv_assets.send(src) + poll_player_irv(poll) - var/datum/DBQuery/query_irv_get_votes = SSdbcore.NewQuery("SELECT optionid FROM [format_table_name("poll_vote")] WHERE pollid = [pollid] AND ckey = '[ckey]'") - if(!query_irv_get_votes.warn_execute()) - qdel(query_irv_get_votes) - return - - var/list/votedfor = list() - while(query_irv_get_votes.NextRow()) - votedfor.Add(text2num(query_irv_get_votes.item[1])) - qdel(query_irv_get_votes) - - var/list/datum/polloption/options = list() - - var/datum/DBQuery/query_irv_options = SSdbcore.NewQuery("SELECT id, text FROM [format_table_name("poll_option")] WHERE pollid = [pollid]") - if(!query_irv_options.warn_execute()) - qdel(query_irv_options) - return - while(query_irv_options.NextRow()) - var/datum/polloption/PO = new() - PO.optionid = text2num(query_irv_options.item[1]) - PO.optiontext = query_irv_options.item[2] - options["[PO.optionid]"] += PO - qdel(query_irv_options) - - //if they already voted, use their sort - if (votedfor.len) - var/list/datum/polloption/newoptions = list() - for (var/V in votedfor) - var/datum/polloption/PO = options["[V]"] - if(PO) - newoptions["[V]"] = PO - options -= "[V]" - //add any options that they didn't vote on (some how, some way) - options = shuffle(options) - for (var/V in options) - newoptions["[V]"] = options["[V]"] - options = newoptions - //otherwise, lets shuffle it. - else - var/list/datum/polloption/newoptions = list() - while (options.len) - var/list/local_options = options.Copy() - var/key - //the jist is we randomly remove all options from a copy of options until only one reminds, - // move that over to our new list - // and repeat until we've moved all of them - while (local_options.len) - key = local_options[rand(1, local_options.len)] - local_options -= key - var/value = options[key] - options -= key - newoptions[key] = value - options = newoptions - - var/output = {" - - - - - - - - - - -
Player poll
- Question: [pollquestion]
Please sort the options in the order of most preferred to least preferred
- Revoting has been enabled on this poll, if you think you made a mistake, simply revote
- Poll runs from [pollstarttime] until [pollendtime]

-

-
- - - - -
-
Most Preferred
-
    - "} - for(var/O in options) - var/datum/polloption/PO = options["[O]"] - if(PO.optionid && PO.optiontext) - output += "
  1. [PO.optiontext]
  2. \n" - output += {" -
-
Least Preferred

-
-

- "} - src << browse(null ,"window=playerpolllist") - src << browse(output,"window=playerpoll;size=500x500") - return - -//Returns null on failure, TRUE if already voted, FALSE if not voted yet. -/mob/dead/new_player/proc/poll_check_voted(pollid, text = FALSE, silent = FALSE) - var/table = "poll_vote" - if (text) - table = "poll_textreply" - if (!SSdbcore.Connect()) - to_chat(usr, "Failed to establish database connection.") +/** + * Shows voting window for an option type poll, listing its options and relevant details. + * + * If already voted on, the option a player voted for is pre-selected. + * + */ +/mob/dead/new_player/proc/poll_player_option(datum/poll_question/poll) + var/datum/DBQuery/query_option_get_voted = SSdbcore.NewQuery("SELECT optionid FROM [format_table_name("poll_vote")] WHERE pollid = [sanitizeSQL(poll.poll_id)] AND ckey = '[sanitizeSQL(ckey)]' AND deleted = 0") + if(!query_option_get_voted.warn_execute()) + qdel(query_option_get_voted) return - var/datum/DBQuery/query_hasvoted = SSdbcore.NewQuery("SELECT id FROM `[format_table_name(table)]` WHERE pollid = [pollid] AND ckey = '[ckey]'") - if(!query_hasvoted.warn_execute()) - qdel(query_hasvoted) + var/voted_option_id = 0 + if(query_option_get_voted.NextRow()) + voted_option_id = text2num(query_option_get_voted.item[1]) + qdel(query_option_get_voted) + var/list/output = list("
Player poll
Question: [poll.question]
") + if(poll.subtitle) + output += "[poll.subtitle]
" + output += "Poll runs from [poll.start_datetime] until [poll.end_datetime]
" + if(poll.allow_revoting) + output += "Revoting is enabled." + if(!voted_option_id || poll.allow_revoting) + output += {"
+ + + "} + output += "
" + for(var/o in poll.options) + var/datum/poll_option/option = o + output += "