From f4ef6b01b1c0745caf9407f7f1bead179f9eccec Mon Sep 17 00:00:00 2001 From: Felix Sargent Date: Thu, 25 Jul 2024 10:41:38 -0700 Subject: [PATCH] Improve and verify poll pages. (#213) --- .github/workflows/django.yml | 1 + .trunk/configs/.bandit | 2 + approval_polls/settings.py | 52 ++-- approval_polls/staticfiles/create.js | 392 ++++++------------------- approval_polls/staticfiles/detail.js | 300 ++++++++----------- approval_polls/staticfiles/edit.js | 1 - approval_polls/staticfiles/my_polls.js | 139 +++------ approval_polls/templates/base.html | 6 +- approval_polls/templates/create.html | 283 ++++++++---------- approval_polls/templates/detail.html | 263 ++++++----------- approval_polls/templates/edit.html | 301 ------------------- approval_polls/templates/index.html | 12 +- approval_polls/templates/my_info.html | 16 +- approval_polls/templates/my_polls.html | 6 - approval_polls/tests.py | 183 +----------- approval_polls/urls.py | 3 +- approval_polls/views.py | 368 +++++++++++------------ pyproject.toml | 4 + 18 files changed, 711 insertions(+), 1621 deletions(-) create mode 100644 .trunk/configs/.bandit delete mode 100644 approval_polls/staticfiles/edit.js delete mode 100644 approval_polls/templates/edit.html diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 020c180..7a61c73 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -27,4 +27,5 @@ jobs: - run: poetry install - name: Run Tests run: | + poetry run python manage.py compress poetry run pytest diff --git a/.trunk/configs/.bandit b/.trunk/configs/.bandit new file mode 100644 index 0000000..99a68b1 --- /dev/null +++ b/.trunk/configs/.bandit @@ -0,0 +1,2 @@ +[bandit] +exclude = *_test.py, */test_*.py,*/tests.py \ No newline at end of file diff --git a/approval_polls/settings.py b/approval_polls/settings.py index 84d6abe..ceef30a 100644 --- a/approval_polls/settings.py +++ b/approval_polls/settings.py @@ -25,18 +25,41 @@ SECRET_KEY = env("SECRET_KEY") db_path = "/data/prod.sqlite3" + +# Hosts/domain names that are valid for this site; required if DEBUG is False +# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts +APP_NAME = env("FLY_APP_NAME", str, "") +ALLOWED_HOSTS = [f"{APP_NAME}.fly.dev", "vote.electionscience.org"] # ← Updated! + if DEBUG: db_path = os.path.join(BASE_DIR, "db.sqlite3") + CSRF_TRUSTED_ORIGINS = ["http://localhost:8000", "http://127.0.0.1:8000"] + CSRF_ALLOWED_ORIGINS = ["http://localhost:8000", "http://127.0.0.1:8000"] + CORS_ORIGINS_WHITELIST = ["http://localhost:8000", "http://127.0.0.1:8000"] + ALLOWED_HOSTS.extend(["localhost", "0.0.0.0", "127.0.0.1"]) # trunk-ignore(bandit) if not DEBUG: COMPRESS_OFFLINE = True LIBSASS_OUTPUT_STYLE = "compressed" + sentry_sdk.init( + dsn="https://78856604267db99554868743d5eb61e5@o4506681396625408.ingest.sentry.io/4506681396756480", + # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + traces_sample_rate=1.0, + # Set profiles_sample_rate to 1.0 to profile 100% + # of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=1.0, + ) + CSRF_TRUSTED_ORIGINS = ["https://vote.electionscience.org"] + CSRF_ALLOWED_ORIGINS = ["https://vote.electionscience.org"] + CORS_ORIGINS_WHITELIST = ["https://vote.electionscience.org"] + -if "test" in sys.argv: +if "test" in sys.argv or "pytest" in sys.argv: COMPRESS_OFFLINE = False COMPRESS_ENABLED = False - DATABASES = { "default": { # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. @@ -45,16 +68,6 @@ } } -# Hosts/domain names that are valid for this site; required if DEBUG is False -# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts -APP_NAME = env("FLY_APP_NAME", str, "") -ALLOWED_HOSTS = [f"{APP_NAME}.fly.dev", "vote.electionscience.org"] # ← Updated! -if DEBUG: - ALLOWED_HOSTS.extend(["localhost", "0.0.0.0", "127.0.0.1"]) - -CSRF_TRUSTED_ORIGINS = ["https://vote.electionscience.org"] -CSRF_ALLOWED_ORIGINS = ["https://vote.electionscience.org"] -CORS_ORIGINS_WHITELIST = ["https://vote.electionscience.org"] # The following settings are required for the activation emails in the # registration module to work. @@ -136,7 +149,7 @@ # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( - "django.template.loaders.filesystem.Loader", + "django.template.loaders.filesystem.Loader" "django.template.loaders.app_directories.Loader", ) @@ -149,6 +162,7 @@ "django_ajax.middleware.AJAXMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", "allauth.account.middleware.AccountMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", ) ROOT_URLCONF = "approval_polls.urls" @@ -274,15 +288,3 @@ # ACCOUNT_LOGIN_ATTEMPTS_LIMIT = 5 # or any reasonable number ACCOUNT_RATE_LIMITS = False ACCOUNT_FORMS = {"signup": "approval_polls.forms.CustomSignupForm"} - - -sentry_sdk.init( - dsn="https://78856604267db99554868743d5eb61e5@o4506681396625408.ingest.sentry.io/4506681396756480", - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - traces_sample_rate=1.0, - # Set profiles_sample_rate to 1.0 to profile 100% - # of sampled transactions. - # We recommend adjusting this value in production. - profiles_sample_rate=1.0, -) diff --git a/approval_polls/staticfiles/create.js b/approval_polls/staticfiles/create.js index bb226e0..24d6d89 100644 --- a/approval_polls/staticfiles/create.js +++ b/approval_polls/staticfiles/create.js @@ -1,337 +1,115 @@ $(function () { - var that = this; - this.lastId = undefined; - var changeDateLogic, roundMinutes, setDefaultOptions, changeDisabledOptions; - var validateTokenField; - /* Add an extra textfield for a poll choice on the poll creation page. */ - function addChoiceField(numChoiceFields) { - var formGroup, input; - numChoiceFields++; - that.lastId = numChoiceFields; - formGroup = $("
"); - - input = $( - "
\ - \ - \ - \ -
", - ); - - formGroup.append(input); - - $(".form-group").last().after(formGroup); - $("[data-toggle=tooltip]").tooltip(); - } - $("button#add-choice").on("click", function () { - addChoiceField(that.lastId || 4); - }); - - $("button#add-choice-edit").on("click", function () { - if (that.lastId == undefined) { - that.lastId = parseInt($("#LastId").val()); - } - addChoiceField(that.lastId); - }); - - $("[data-toggle=tooltip]").tooltip(); + const pollOptions = { + container: $("#poll-options"), + addButton: $("#add-choice"), + removeSelector: ".remove-choice", + }; - /* Allow user to attach an external link to an option. */ + // Initialize lastId based on the number of existing options + let lastId = pollOptions.container.children().length; - // Event delegation to capture dynamically added links - $(".row") - .on("click", "a.add-link", function (e) { + const initializePollOptions = () => { + pollOptions.container.on("click", pollOptions.removeSelector, function (e) { e.preventDefault(); - var alertDiv, alertDivId, currentUrl; - alertDivId = $(this).attr("id"); - alertDivId = alertDivId.split("-").pop(); - - alertDiv = - "
" + - "

" + - "" + - " " + - "

"; - - if ($("#alert-" + alertDivId).length === 0) { - // Remove all previous alerts - $(".alert").remove(); - // Append the alert box before selected option - $("#div-" + alertDivId).before(alertDiv); - // Populate textbox with the last 'valid and inserted' URL - currentUrl = $("#linkurl-" + alertDivId).val(); - $("#url-" + alertDivId).val(currentUrl); - } + $(this).closest(".poll-option").remove(); + updateOptionNumbers(); + }); - $('button[id^="confirm-link-"]').click(function () { - var buttonId, linkUrl, validUrl, urlPattern; - buttonId = $(this).attr("id"); - buttonId = buttonId.split("-").pop(); - linkUrl = $("#url-" + buttonId).val(); - linkUrl = $.trim(linkUrl); - // Check if URL begins with http or https or ftp - // If not, prepend 'http://' - urlPattern = new RegExp("^(http|https|ftp)://", "i"); - if (!urlPattern.test(linkUrl)) { - linkUrl = "http://" + linkUrl; - } - // Source: https://github.com/jzaefferer/jquery-validation/blob/master/src/core.js - validUrl = - /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test( - linkUrl, - ); - if (!validUrl) { - // If URL is not valid, change class of alert box - $("#alert-" + buttonId).attr("class", "alert alert-info has-error"); - // If URL is not valid, show an error message - $('label[for="url-' + buttonId + '"]').remove(); - $("#alert-" + buttonId).prepend( - "", - ); - } else { - // Reset class of alert box - $("#alert-" + buttonId).attr("class", "alert alert-info"); - // Remove any error message - $("label[for='url-" + buttonId + "']").remove(); - // Update value of hidden input field - $("#linkurl-" + alertDivId).val(linkUrl); - // Remove alert box - $("#alert-" + buttonId).remove(); - // Change color of link to show a valid insertion - $("#link-" + buttonId).addClass("text-success"); - } - }); + pollOptions.addButton.on("click", addChoiceField); + }; - $('button[id^="cancel-link-"]').click(function () { - var buttonId; - buttonId = $(this).attr("id"); - buttonId = buttonId.split("-").pop(); - // Remove alert box - $("#alert-" + buttonId).remove(); - }); + const addChoiceField = () => { + lastId++; + const newOption = ` +
+ +
+ + +
+
+ `; + pollOptions.container.append(newOption); + }; - $('button[id^="remove-link-"]').click(function () { - var buttonId; - buttonId = $(this).attr("id"); - buttonId = buttonId.split("-").pop(); - // Reset value of hidden input field to empty string - $("#linkurl-" + buttonId).val(""); - // Reset value of textbox - $("#url-" + alertDivId).val(""); - // Reset class of alert box - $("#alert-" + buttonId).attr("class", "alert alert-info"); - // Remove any error message - $("label[for='url-" + buttonId + "']").remove(); - // Reset color of link to show no current insertion - $("#link-" + buttonId).removeClass("text-success"); - }); - // To prevent navigation - return false; - }) - .on("click", "a.remove-choice", function (e) { - e.preventDefault(); - var container = $(e.currentTarget).closest(".form-group"); - container.remove(); - return false; + const updateOptionNumbers = () => { + pollOptions.container.children(".poll-option").each(function (index) { + const newIndex = index + 1; + $(this).find("label").text(`Option ${newIndex}`); + $(this) + .find("input") + .attr("id", `choice${newIndex}`) + .attr("name", `choice${newIndex}`); }); + lastId = pollOptions.container.children().length; + }; - // $('a.remove-choice').click(function(e) { - // e.preventDefault(); - // var container = $(e.currentTarget).closest('.form-group'); - // container.remove(); - // return false; - // }); + const initializeTokenFields = () => { + const allTags = $("#allTags").length ? $("#allTags").val().split(",") : []; - /* Allow user to select a poll closing date and time from a Jquery - DateTime picker. */ + emailValidation.tokenField + .on("tokenfield:createtoken", tokenize) + .on("tokenfield:createdtoken", validateEmailToken) + .on("tokenfield:removedtoken", validateTokenField) + .tokenfield(); - roundMinutes = function (today) { - var hr, min, time; - min = today.getMinutes(); - if (min >= 0 && min < 30) { - today.setMinutes(30); - } else if (min >= 30 && min < 60) { - today.setHours(today.getHours() + 1); - today.setMinutes(0); - } - hr = ("0" + today.getHours()).slice(-2); - min = ("0" + today.getMinutes()).slice(-2); - time = hr + ":" + min; - return [time, today]; + $("#tokenTagField") + .on("tokenfield:createtoken", tokenize) + .tokenfield() + .tokenfield("setTokens", allTags); }; - setDefaultOptions = function () { - var options, roundDateTime, roundDate, roundTime; - options = {}; - roundDateTime = roundMinutes(new Date()); - roundTime = roundDateTime[0]; - roundDate = roundDateTime[1]; - options["defaultDate"] = roundDate; - options["minDate"] = roundDate; - options["defaultTime"] = roundTime; - options["minTime"] = roundTime; - return options; + const tokenize = (e) => { + const data = e.attrs.value.split("|"); + e.attrs.value = data[1] || data[0]; + e.attrs.label = data[1] ? `${data[0]} (${data[1]})` : data[0]; }; - changeDateLogic = function (ct, $i) { - var selected, current, roundDate; - roundDate = roundMinutes(new Date())[1]; - selected = new Date(ct.dateFormat("Y/m/d")); - current = new Date(roundDate.dateFormat("Y/m/d")); - if (selected.getTime() == current.getTime()) { - if (ct.dateFormat("H:i") < roundDate.dateFormat("H:i")) { - $("#datetimepicker").val(""); - } - $i.datetimepicker(setDefaultOptions()); - } else if (selected.getTime() > current.getTime()) { - $i.datetimepicker({ - minTime: false, - }); - } else if (selected.getTime() < current.getTime()) { - $("#datetimepicker").val(""); - $i.datetimepicker({ - defaultDate: false, - minTime: "23:59", - }); + const validateEmailToken = (e) => { + const valid = emailValidation.regex.test(e.attrs.value); + if (!valid) { + $(e.relatedTarget).addClass("invalid"); } - $("#datetimepicker").change(); + validateTokenField(); }; - $("#datetimepicker").datetimepicker(setDefaultOptions()); - - $("#datetimepicker").datetimepicker({ - step: 30, - todayButton: false, - onShow: changeDateLogic, - onSelectDate: changeDateLogic, - onChangeMonth: changeDateLogic, - }); - - $("#datetimepicker").keydown(function (e) { - if (e.keyCode == 8 || e.keyCode == 46) { - $(this).val(""); - $(this).change(); - e.preventDefault(); - } - }); + const validateTokenField = () => { + const existingTokens = emailValidation.tokenField.tokenfield("getTokens"); + const tokensValid = existingTokens.every((token) => + emailValidation.regex.test(token.value), + ); - changeDisabledOptions = function () { - if ($("#datetimepicker").val() == "") { - $("#checkbox1").attr("disabled", true); - $("#checkbox2").attr("disabled", true); - } else { - $("#checkbox1").prop("disabled", false); - $("#checkbox2").prop("disabled", false); - } + emailValidation.errorElement.toggle( + !tokensValid && existingTokens.length > 0, + ); }; - $("#datetimepicker").change(function () { - changeDisabledOptions(); - }); - - $("#datetimepicker").change(); - - /* Validate the token field for the email list. */ - validateTokenField = function () { - var re = /\S+@\S+\.\S+/; - var existingTokens; - var tokensValid = true; - existingTokens = $("#tokenEmailField").tokenfield("getTokens"); + const initializeEmailPollDisplay = () => { + if ($("#poll-vtype").val() == 3) { + emailPollDisplay(); + } - if (existingTokens.length === 0) { - $("#email-error").hide(); - } else { - $.each(existingTokens, function (index, token) { - if (!re.test(token.value)) { - tokensValid = false; - } - }); - if (tokensValid) { - $("#email-error").hide(); + $("input[name=radio-poll-type]:radio").on("click", function () { + if ($(this).val() == 3) { + emailPollDisplay(); } else { - $("#email-error").show(); + $("#email-input, #existing-emails").hide(); + $("#poll-visibility").prop("checked", true); } - } + }); }; - $("#tokenEmailField") - .on("tokenfield:createtoken", function (e) { - tokenize(e); - }) - .on("tokenfield:createdtoken", function (e) { - // Simple E-mail validation - var re = /\S+@\S+\.\S+/; - var valid = re.test(e.attrs.value); - if (!valid) { - $(e.relatedTarget).addClass("invalid"); - } - validateTokenField(); - }) - .on("tokenfield:removedtoken", function (e) { - validateTokenField(); - }) - .tokenfield(); - - var allTags = []; - if ($("#allTags").length) { - allTags = $("#allTags").val().split(","); - } - - $("#tokenTagField") - .on("tokenfield:createtoken", function (e) { - tokenize(e); - }) - .tokenfield(); - $("#tokenTagField").tokenfield("setTokens", allTags); - - // For edit page, display email text field if poll.vtype is 3 - if ($("#poll-vtype").val() == 3) { - emailPollDisplay(); - } - // Toggle the visibility of the email input - $("input[name=radio-poll-type]:radio").click(function () { - if ($(this).attr("value") == 3) { - emailPollDisplay(); - } else { - $("#email-input").hide(); - $("#poll-visibility").prop("checked", true); - $("#existing-emails").hide(); - } - }); - - function emailPollDisplay() { - $("#email-input").show(); - if ($("#poll-id") == undefined) { + const emailPollDisplay = () => { + $("#email-input, #existing-emails").show(); + if ($("#poll-id").length === 0) { $("#poll-visibility").prop("checked", false); } - $("#existing-emails").show(); - } + }; - function tokenize(e) { - var data = e.attrs.value.split("|"); - e.attrs.value = data[1] || data[0]; - e.attrs.label = data[1] ? data[0] + " (" + data[1] + ")" : data[0]; - } + // Initialize everything + initializePollOptions(); + initializeTokenFields(); + initializeEmailPollDisplay(); }); diff --git a/approval_polls/staticfiles/detail.js b/approval_polls/staticfiles/detail.js index f95274d..8672acc 100644 --- a/approval_polls/staticfiles/detail.js +++ b/approval_polls/staticfiles/detail.js @@ -1,200 +1,136 @@ $(function () { + // Social sharing $("#share").jsSocials({ showLabel: false, showCount: false, shares: ["email", "twitter", "facebook", "linkedin", "pinterest"], }); - var invitation_key, invitation_email; - - this.numChoiceFields = $("input:checkbox").length; - - this.addChoiceField = function () { - var checkBox, input; - - this.numChoiceFields++; - checkBox = $("
"); - input = $( - "", - ); - - checkBox.append(input); - - $(".checkbox").last().after(checkBox); - $("[data-toggle=tooltip]").tooltip(); + // Poll options + const pollOptions = { + container: $("#poll-options"), + addButton: $("#add-option"), + removeSelector: ".remove-choice", }; - $("[data-toggle=tooltip]").tooltip(); - $("button#add-option").click($.proxy(this.addChoiceField, this)); - - /* Allow user to attach an external link to an option. */ - - // Event delegation to capture dynamically added links - $(".row-fluid").on("click", "a", function () { - var alertDiv, alertDivId, currentUrl; - alertDivId = $(this).attr("id"); - alertDivId = alertDivId.split("-").pop(); - - alertDiv = - "
" + - "

" + - "" + - " " + - "

"; - - if ($("#alert-" + alertDivId).length === 0) { - // Remove all previous alerts - $(".alert").remove(); - // Append the alert box before selected option - $("#label-" + alertDivId).before(alertDiv); - // Populate textbox with the last 'valid and inserted' URL - currentUrl = $("#linkurl-" + alertDivId).val(); - $("#url-" + alertDivId).val(currentUrl); - } + let lastId = pollOptions.container.children().length; + + const addChoiceField = () => { + lastId++; + const newOption = ` +
+ + +
+ `; + pollOptions.container.append(newOption); + }; - $('button[id^="confirm-link-"]').click(function () { - var buttonId, linkUrl, validUrl, urlPattern; - buttonId = $(this).attr("id"); - buttonId = buttonId.split("-").pop(); - linkUrl = $("#url-" + buttonId).val(); - linkUrl = $.trim(linkUrl); - // Check if URL begins with http or https or ftp - // If not, prepend 'http://' - urlPattern = new RegExp("^(http|https|ftp)://", "i"); - if (!urlPattern.test(linkUrl)) { - linkUrl = "http://" + linkUrl; - } - // Source: https://github.com/jzaefferer/jquery-validation/blob/master/src/core.js - validUrl = - /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test( - linkUrl, - ); - if (!validUrl) { - // If URL is not valid, change class of alert box - $("#alert-" + buttonId).attr("class", "alert alert-info has-error"); - // If URL is not valid, show an error message - $('label[for="url-' + buttonId + '"]').remove(); - $("#alert-" + buttonId).prepend( - "", - ); - } else { - // Reset class of alert box - $("#alert-" + buttonId).attr("class", "alert alert-info"); - // Remove any error message - $("label[for='url-" + buttonId + "']").remove(); - // Update value of hidden input field - $("#linkurl-" + alertDivId).val(linkUrl); - // Remove alert box - $("#alert-" + buttonId).remove(); - // Change color of link to show a valid insertion - $("#link-" + buttonId).attr("class", "text-success"); - } - }); - - $('button[id^="cancel-link-"]').click(function () { - var buttonId; - buttonId = $(this).attr("id"); - buttonId = buttonId.split("-").pop(); - // Remove alert box - $("#alert-" + buttonId).remove(); - }); - - $('button[id^="remove-link-"]').click(function () { - var buttonId; - buttonId = $(this).attr("id"); - buttonId = buttonId.split("-").pop(); - // Reset value of hidden input field to empty string - $("#linkurl-" + buttonId).val(""); - // Reset value of textbox - $("#url-" + alertDivId).val(""); - // Reset class of alert box - $("#alert-" + buttonId).attr("class", "alert alert-info"); - // Remove any error message - $("label[for='url-" + buttonId + "']").remove(); - // Reset color of link to show no current insertion - $("#link-" + buttonId).removeAttr("class"); - }); - // To prevent navigation - return false; + pollOptions.addButton.on("click", addChoiceField); + + // Link handling + pollOptions.container.on("click", ".add-link", function (e) { + e.preventDefault(); + const choiceId = $(this) + .closest(".form-check") + .find('input[type="checkbox"]') + .attr("id"); + const alertDiv = createAlertDiv(choiceId); + $(this).closest(".form-check").before(alertDiv); }); - var convertSeconds, onZero, time_difference; - - convertSeconds = function (total_seconds) { - var days, hours, minutes, seconds, currentTime; - - days = Math.floor(total_seconds / 86400); - hours = Math.floor((total_seconds % 86400) / 3600); - minutes = Math.floor(((total_seconds % 86400) % 3600) / 60); - seconds = Math.floor(((total_seconds % 86400) % 3600) % 60); - hours = ("0" + hours).slice(-2); - minutes = ("0" + minutes).slice(-2); - seconds = ("0" + seconds).slice(-2); - currentTime = days + "d:" + hours + "h:" + minutes + "m:" + seconds + "s"; - - return currentTime; - }; + function createAlertDiv(choiceId) { + return ` +
+ + + + +
+ `; + } + + pollOptions.container.on("click", "[id^='confirm-link-']", function () { + const choiceId = this.id.split("-").pop(); + const linkUrl = $(`#url-${choiceId}`).val().trim(); + if (validateUrl(linkUrl)) { + $(`#linkurl-${choiceId}`).val(linkUrl); + $(`#alert-${choiceId}`).remove(); + $(`#${choiceId}`) + .closest(".form-check") + .find(".add-link") + .addClass("btn-success"); + } else { + $(`#alert-${choiceId}`) + .addClass("alert-danger") + .removeClass("alert-info") + .prepend('

Please enter a valid URL

'); + } + }); - $.fn.countdown = function (callback, duration) { - var currentTimeString, container, countdown, message; + pollOptions.container.on("click", "[id^='remove-link-']", function () { + const choiceId = this.id.split("-").pop(); + $(`#linkurl-${choiceId}`).val(""); + $(`#url-${choiceId}`).val(""); + $(`#alert-${choiceId}`).removeClass("alert-danger").addClass("alert-info"); + $(`#${choiceId}`) + .closest(".form-check") + .find(".add-link") + .removeClass("btn-success"); + }); - message = "before poll closes"; - currentTimeString = convertSeconds(duration); - container = $(this[0]).html(currentTimeString + " " + message); + pollOptions.container.on("click", "[id^='cancel-link-']", function () { + const choiceId = this.id.split("-").pop(); + $(`#alert-${choiceId}`).remove(); + }); - countdown = setInterval(function () { - if (--duration) { - currentTimeString = convertSeconds(duration); - container.html(currentTimeString + " " + message); - } else { - clearInterval(countdown); - callback.call(container); + function validateUrl(url) { + const pattern = + /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i; + return pattern.test(url); + } + + // Countdown timer + const timeDifference = $("#time_difference").val(); + if (timeDifference) { + startCountdown(Math.ceil(timeDifference)); + } + + function startCountdown(duration) { + const timer = $("#timer"); + const message = "before poll closes"; + + function updateTimer() { + const timeString = formatTime(duration); + timer.html(`${timeString} ${message}`); + + if (--duration < 0) { + clearInterval(interval); + window.location.reload(); } - }, 1000); - }; - - onZero = function () { - window.location.reload(); - }; - - time_difference = - document.getElementById("time_difference") && - document.getElementById("time_difference").value; + } - $("#timer").countdown(onZero, Math.ceil(time_difference)); + updateTimer(); + const interval = setInterval(updateTimer, 1000); + } + + function formatTime(totalSeconds) { + const days = Math.floor(totalSeconds / 86400); + const hours = Math.floor((totalSeconds % 86400) / 3600); + const minutes = Math.floor(((totalSeconds % 86400) % 3600) / 60); + const seconds = Math.floor(((totalSeconds % 86400) % 3600) % 60); + return `${days}d:${padZero(hours)}h:${padZero(minutes)}m:${padZero(seconds)}s`; + } + + function padZero(num) { + return num.toString().padStart(2, "0"); + } }); diff --git a/approval_polls/staticfiles/edit.js b/approval_polls/staticfiles/edit.js deleted file mode 100644 index ad7629d..0000000 --- a/approval_polls/staticfiles/edit.js +++ /dev/null @@ -1 +0,0 @@ -$(function () {}); diff --git a/approval_polls/staticfiles/my_polls.js b/approval_polls/staticfiles/my_polls.js index b4dc72b..4882369 100644 --- a/approval_polls/staticfiles/my_polls.js +++ b/approval_polls/staticfiles/my_polls.js @@ -1,120 +1,63 @@ $(function () { - var csrfSafeMethod; - - csrfSafeMethod = function (method) { - // these HTTP methods do not require CSRF protection + var csrfSafeMethod = function (method) { return /^(GET|HEAD|OPTIONS|TRACE)$/.test(method); }; - $('a[id^="delete-poll-"]').click(function (event) { + $('button[id^="delete-poll-"]').click(function (event) { disableAction(event.target, "Delete"); }); - $('a[id^="suspend-poll-"]').click(function (event) { - disableAction(event.target, "Suspend"); - }); - $('a[id^="unsuspend-poll-"]').click(function (event) { - disableAction(event.target, "Unsuspend"); - }); function disableAction(target, action) { - _action = action.toLowerCase(); - var alertDiv, alertDivId; - alertDivId = $(target).attr("id"); - alertDivId = alertDivId.split("-").pop(); - - alertDiv = + var pollId = $(target).attr("id").split("-").pop(); + var alertDiv = "
" + - "

This poll will be " + - verbalizeAction(_action) + - "." + - (action == "Suspend" - ? "Suspending the poll will not allow any voting on it." - : "") + - " " + - " " + + "" + "

"; - if ($("#alert" + alertDivId).length == 0) { - $(".well").css("border-color", "#dcdcdc"); - $(".alert").remove(); - $("#well" + alertDivId).before(alertDiv); - $("#well" + alertDivId).css("border-color", "red"); + if ($("#alert" + pollId).length == 0) { + $("#poll-" + pollId).before(alertDiv); } - function confirmAction() { - var csrfToken, buttonId; - buttonId = $(target).attr("id"); - buttonId = buttonId.split("-").pop(); - csrfToken = $("#csrfmiddlewaretoken").val(); - $("#well" + buttonId).css("border-color", "#dcdcdc"); - $("#alert" + buttonId).remove(); - if (action == "Delete") { - $.ajax({ - method: "DELETE", - url: "/approval_polls/" + buttonId + "/", - beforeSend: function (xhr, settings) { - if (!csrfSafeMethod(settings.type) && !this.crossDomain) { - xhr.setRequestHeader("X-CSRFToken", csrfToken); - } - }, - success: function (data) { - $("#well" + buttonId).remove(); - window.location.reload(); - }, - error: function (data) {}, - }); - } else { - $.ajax({ - method: "PUT", - url: "/approval_polls/" + buttonId + "/change_suspension/", - beforeSend: function (xhr, settings) { - if (!csrfSafeMethod(settings.type) && !this.crossDomain) { - xhr.setRequestHeader("X-CSRFToken", csrfToken); - } - }, - success: function (data) { - window.location.reload(); - }, - }); - } - } - $('button[id^="confirm-delete-"]').click(confirmAction); - $('button[id^="confirm-suspend-"]').click(confirmAction); - $('button[id^="confirm-unsuspend-"]').click(confirmAction); + $("#confirm-delete-" + pollId).click(function () { + confirmAction(pollId); + }); - $('button[id^="cancel-delete-"]').click(cancelAction); - $('button[id^="cancel-suspend-"]').click(cancelAction); - $('button[id^="cancel-unsuspend-"]').click(cancelAction); + $("#cancel-delete-" + pollId).click(function () { + cancelAction(pollId); + }); } - function cancelAction() { - var buttonId; - buttonId = $(this).attr("id"); - buttonId = buttonId.split("-").pop(); - $("#well" + buttonId).css("border-color", "#dcdcdc"); - $("#alert" + buttonId).remove(); + function confirmAction(pollId) { + var csrfToken = $("#csrfmiddlewaretoken").val(); + console.info(csrfToken); + $.ajax({ + method: "POST", + url: "/" + pollId + "/delete/", + headers: { + "X-CSRFToken": csrfToken, + }, + success: function (data) { + if (data.status === "success") { + $("#poll-" + pollId).remove(); + $("#alert" + pollId).remove(); + } + }, + error: function (xhr, status, error) { + console.error("Error deleting poll:", error); + alert("An error occurred while deleting the poll."); + }, + }); } - function verbalizeAction(action) { - switch (action) { - case "delete": - return "permanently deleted"; - case "unsuspend": - return "unsuspended"; - case "suspend": - return "suspended"; - } + function cancelAction(pollId) { + $("#alert" + pollId).remove(); } }); diff --git a/approval_polls/templates/base.html b/approval_polls/templates/base.html index af209f9..050d62b 100644 --- a/approval_polls/templates/base.html +++ b/approval_polls/templates/base.html @@ -105,13 +105,14 @@ Learn why it's better and create your own election.

+
Election Science Logo + style="max-width: 200px" /> @@ -127,6 +128,7 @@ $('[data-toggle="popover"]').popover(); }); - {% block extra_js %}{% endblock %} + {% block extra_js %} + {% endblock extra_js %} diff --git a/approval_polls/templates/create.html b/approval_polls/templates/create.html index 7f4ab19..3e98770 100644 --- a/approval_polls/templates/create.html +++ b/approval_polls/templates/create.html @@ -1,183 +1,134 @@ {% extends 'base.html' %} +{% load static %} {% load filters %} -{% block head %} - {% load static %} +{% block extra_css %} - - - + href="https://cdn.jsdelivr.net/npm/bootstrap-tokenfield@0.12.0/dist/css/bootstrap-tokenfield.min.css"> +{% endblock %} +{% block extra_js %} + {% endblock %} {% block content %}
-
-
-
Create a Poll
-
- {% if question_error %}{% endif %} - - -
- {% for i in 5|get_range %} -
- {% if i == 1 and choice_error %}{% endif %} - -
- - - - -
+

Create a Poll

+ + {% csrf_token %} +
+
+

Poll Question

+
+ + + {% if question_error %}
{{ question_error }}
{% endif %}
- {% endfor %} - -
-
-

- -

-
-
-
Who should be allowed to vote in this poll?
-
- - -
-
- - -
-
-
Poll Visibility
-
- - -
-
-
-
Closing date and time for this poll, if any
- - -
-
- - -
-
- - -
-
-
Customize Poll
-
- - -
-
- - -
-
- - -
-
-
-
(Enter a comma after each tag)
- +

Poll Options

+
+ {% for i in 3|get_range %} +
+ +
+ class="form-control {% if i == 1 and choice_error %}is-invalid{% endif %}" + id="choice{{ i }}" + name="choice{{ i }}" + maxlength="100" + placeholder="Option Name"> +
+ {% if i == 1 and choice_error %}
{{ choice_error }}
{% endif %}
+ {% endfor %} +
+ +
+
+
+
+

Poll Settings

+
+
+

Who should be allowed to vote in this poll?

+
+
+ + +
+
+ +
+

Poll Visibility

+
+ + +
+

Customize Poll

+
+
+ + +
+
+ + +
+
+ + +
+
+

Tags

+
+ + +
- {% csrf_token %} - -
+
+
+ +
{% endblock %} diff --git a/approval_polls/templates/detail.html b/approval_polls/templates/detail.html index 0e81962..7999e25 100644 --- a/approval_polls/templates/detail.html +++ b/approval_polls/templates/detail.html @@ -1,109 +1,74 @@ -{% extends 'base.html' %} -{% block head %} - {% load static %} +{% extends "base.html" %} +{% load static %} +{% block extra_js %} -{% endblock %} +{% endblock extra_js %} {% block content %} -
-
-

{{ poll.question }}

-
-
- {% if error_message %} -
-
{{ error_message }}
-
- {% endif %} -
-
-
- {% csrf_token %} - Choose as many options as you wish - {% for choice in poll.choice_set.all %} -
- -
- {% endfor %} - {% if poll.show_write_in and not poll.is_closed %} -

- -

- {% endif %} -
+ {% endif %} {% if poll.vtype == 3 and not poll.is_closed %} {% if vote_invitation %} - - + + + {% elif not vote_authorized %} +
Sorry! You are not authorized to vote in this poll.
{% else %} - - {% if not vote_authorized %} -
-
Sorry! You are not authorized to vote in this poll.
-
- {% else %} - - {% endif %} + {% endif %} {% endif %} - {% if poll.vtype == 2 and not user.is_authenticated %} - - {% else %} - {% if poll.is_closed %} -
-
Sorry! This poll is closed.
-
- {% endif %} - {% if poll.is_suspended %} -
-
Sorry! This poll has been temporarily suspended.
-
- {% endif %} - {% with message="Sign me up to receive email communication about efforts to create fairer, more representative
elections through approval voting." %} - {% if poll.vtype == 3 %} - {% if poll.show_email_opt_in %} -
- - {{ message }} -
-
- {% endif %} - - {% elif poll.vtype == 2 %} - {% if poll.show_email_opt_in %} -
- - {{ message }} -
-
- {% endif %} -
{% endif %} + {% if poll.is_suspended %} +
Sorry! This poll has been temporarily suspended.
+ {% endif %} + {% if poll.show_email_opt_in %} +
+ + +
+ {% endif %} +
+ {% if poll.vtype == 2 and not user.is_authenticated %} + + {% else %} + - {% elif poll.vtype == 1 %} - {% if already_voted %} -
-
You have already voted on this poll.
-
- {% elif poll.show_email_opt_in %} -
- - {{ message }} My email address is - - -
-
- {% endif %} - {% endif %} - {% endwith %} - {% endif %} -
-
-
-
-
-
- {% if num_tags > 0 %} -

- Tags: - {% for tag in tags %} - {{ tag }} - {% endfor %} - {% endif %} -

-
-
-
-
- {% if poll.show_close_date %} -
-

Closing Date: {{ poll.close_date|date:"N j, Y, P e" }}

+
+
- {% endif %} - {% if poll.show_countdown and time_difference %} -
-

- + - {% endif %} -
-

Creator: {{ poll.user.username }}

-

- See Results -

-
-
-
-
-

- This poll uses approval voting, instead of the more common plurality system. - Learn why it's better and - create your own poll. -

-
-
-
-
-{% endblock %} +{% endblock content %} diff --git a/approval_polls/templates/edit.html b/approval_polls/templates/edit.html deleted file mode 100644 index b196ab2..0000000 --- a/approval_polls/templates/edit.html +++ /dev/null @@ -1,301 +0,0 @@ -{% extends 'base.html' %} -{% load filters %} -{% block head %} - {% load static %} - - - - - - -{% endblock %} -{% block content %} -
-
- - -
- Edit: {{ poll.question }} -

- {% if not can_edit_poll %}You cannot edit the questions and choices as this poll has got ballots on it!{% endif %} -

- {% csrf_token %} -
- {% if question_error %}{% endif %} - -
- - {% if choice_blank_error %} -
- Please ensure you've entered all choices correctly -
- {% endif %} - {% with existing_choice_texts_existing=existing_choice_texts|get_hash_item:'existing' %} - {% with existing_choice_links_existing=existing_choice_links|get_hash_item:'existing' %} - {% for choice in choices %} - {% with choice_id=choice.id %} - {% with box_text=existing_choice_texts_existing|get_item:choice_id %} - {% with link_text=existing_choice_links_existing|get_item:choice_id %} -
- {% if choice_blank_error and choice_id in blank_choices %} - - {% endif %} -
- - {% if can_edit_poll %} - - - - - - - - - - - {% endif %} - -
-
- {% endwith %} - {% endwith %} - {% endwith %} - {% endfor %} - {% endwith %} - {% endwith %} - {% with existing_choice_texts_new=existing_choice_texts|get_hash_item:'new' %} - {% with existing_choice_links_new=existing_choice_links|get_hash_item:'new' %} - {% for id, text in existing_choice_texts_new.items %} - {% with link_text=existing_choice_links_new|get_item:id %} -
-
- - - - - - - - - - - - -
-
- {% endwith %} - {% endfor %} - {% endwith %} - {% endwith %} -

- -

-

-

- -
- -
-
- -
-
- Who should be allowed to vote in this poll ? -
- -
-
- -
-
- -
-
-

Poll Visibility

-
- -
-
-
-

Please select a closing date and time for this poll, if any

- -
-
- -
-
- -
-
-

Customize Poll

-
- -
-
- -
-
- -
-
-
-
-
- -
-
-
- {% endblock %} diff --git a/approval_polls/templates/index.html b/approval_polls/templates/index.html index 6e32812..5985950 100644 --- a/approval_polls/templates/index.html +++ b/approval_polls/templates/index.html @@ -2,7 +2,13 @@ {% block content %}
{% if latest_poll_list %} -

Latest Public Polls

+

+ {% if tag %} + Tag: {{ tag.tag_text }} + {% else %} + Latest Public Polls + {% endif %} +

{% for poll in latest_poll_list %} @@ -26,7 +32,7 @@

- Previous + Previous {% endif %} @@ -38,7 +44,7 @@

- Next + Next {% endif %} diff --git a/approval_polls/templates/my_info.html b/approval_polls/templates/my_info.html index 275c0c0..55057b8 100644 --- a/approval_polls/templates/my_info.html +++ b/approval_polls/templates/my_info.html @@ -50,6 +50,13 @@

Personal Information

My Polls

+ {% if messages %} +
+ {% for message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} {% if latest_poll_list %} {% for poll in latest_poll_list %}
@@ -59,8 +66,13 @@

Published on {{ poll.pub_date|date:"N j, Y, P e" }}

- Edit - + {% comment %} Edit {% endcomment %} +
+ {% csrf_token %} + +
diff --git a/approval_polls/templates/my_polls.html b/approval_polls/templates/my_polls.html index 4acf887..5eb506c 100644 --- a/approval_polls/templates/my_polls.html +++ b/approval_polls/templates/my_polls.html @@ -19,12 +19,6 @@

Published on {{ poll.pub_date|date:"N j, Y, P e" }}

- Edit - {% comment %} - {% with suspend_text=poll.is_suspended|get_suspend_text %} - - {% endwith %} - {% endcomment %}
diff --git a/approval_polls/tests.py b/approval_polls/tests.py index 499656c..c6c2891 100644 --- a/approval_polls/tests.py +++ b/approval_polls/tests.py @@ -462,7 +462,9 @@ def test_poll_details_different_user(self): self.client.logout() User.objects.create_user("user2", "user2@example.com", "password123") self.client.login( - username="user2", email="user2@example.com", password="password123" + username="user2", + email="user2@example.com", + password="password123", ) response = self.client.get("/1/") self.assertContains(response, "Vote", status_code=200) @@ -484,7 +486,9 @@ def test_poll_details_closed_poll(self): close_date=timezone.now() + datetime.timedelta(days=-10), ) self.client.login( - username="user2", email="user2@example.com", password="password123" + username="user2", + email="user2@example.com", + password="password123", ) response = self.client.get(reverse("detail", args=(poll_closed.id,))) self.assertContains(response, "Sorry! This poll is closed.", status_code=200) @@ -507,7 +511,7 @@ def setUp(self): self.client.login(username="user1", email="user1@example.com", password="test") def test_delete_one_poll(self): - self.client.delete("/1/", follow=True) + self.client.delete("/1/delete", follow=True) response = self.client.get(reverse("my_polls")) self.assertEqual(response.status_code, 200) @@ -530,8 +534,8 @@ def test_delete_one_poll(self): self.assertEqual(response.status_code, 404) def test_delete_all_polls(self): - self.client.delete("/1/", follow=True) - self.client.delete("/2/", follow=True) + self.client.delete("/1/delete", follow=True) + self.client.delete("/2/delete", follow=True) response = self.client.get(reverse("my_polls")) self.assertEqual(response.status_code, 200) self.assertQuerySetEqual(response.context["latest_poll_list"], []) @@ -602,168 +606,6 @@ def test_private_poll_different_user(self): ) -class PollEditTests(TestCase): - - def setUp(self): - self.client = Client() - - self.poll = create_poll( - question="Create Sample Poll.", - close_date=timezone.now() + datetime.timedelta(days=3), - vtype=3, - ) - self.client.login(username="user1", password="test") - - create_vote_invitation(self.poll, email="test1@test1.com") - self.choice = self.poll.choice_set.create(choice_text="Choice 1.") - - def test_edit_view_with_invalid_poll(self): - """ - Requesting the edit page of a non-existent poll should - return a 404 not found error. - """ - response = self.client.get(reverse("edit", args=(10000,))) - self.assertEqual(response.status_code, 404) - - def test_edit_view_visible_to_other_user(self): - """ - The edit page of a poll belonging to one user should not be - visible to another user. It should return a permission denied (403) error. - """ - User.objects.create_user("user2", "user2@example.com", "test") - self.client.logout() - self.client.login(username="user2", password="test") - response = self.client.get(reverse("edit", args=(self.poll.id,))) - self.assertEqual(response.status_code, 403) - - def test_email_invitees_are_returned(self): - """ - The poll's edit page should list email invitees if poll.vtype is 3 - """ - response = self.client.get(reverse("edit", args=(self.poll.id,))) - self.assertEqual(response.context["invited_emails"], "test1@test1.com") - - def test_new_choices_are_added(self): - """ - New choices should be added to the poll and existing ones should be updated. - """ - self.client.post( - reverse("edit", args=(self.poll.id,)), - { - "choice1": "xxx", - "linkurl-choice1": "xxx", - "choice1000": "BBBBB", - "linkurl-choice1000": "BBBBBBBB", - "close-datetime": "bb", - "question": "q", - "token-tags": "", - }, - ) - - # Refresh poll and choice objects from the database - self.poll.refresh_from_db() - self.choice.refresh_from_db() - - # Check if the existing choice was updated - self.assertEqual(self.choice.choice_text, "xxx") - - # Check if the new choice was added - new_choice = self.poll.choice_set.get(choice_text="BBBBB") - self.assertIsNotNone(new_choice) - self.assertEqual(new_choice.choice_text, "BBBBB") - - # Verify the response contains the expected HTML elements - response = self.client.get(reverse("edit", args=(self.poll.id,))) - - logger.info(response.content) - - self.assertContains( - response, - "", - None, - 200, - "", - html=True, - ) - - def test_can_not_edit_poll(self): - """ - If ballots are on the poll, editing should not happen - """ - create_ballot(self.poll) - response = self.client.get(reverse("edit", args=(1,))) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.context["can_edit_poll"], False) - self.assertContains( - response, - "You cannot edit the questions and choices as this poll has got ballots on it!", - ) - self.client.post( - reverse("edit", args=(1,)), - { - "choice1": "xxx", - "linkurl-choice1": "xxx", - "choice1000": "BBBBB", - "linkurl-choice1000": "BBBBBBBB", - "close-datetime": "bb", - "question": "q", - "token-tags": "", - }, - ) - self.assertEqual(Poll.objects.get(id=self.poll.id).choice_set.count(), 1) - self.assertEqual(Choice.objects.get(id=self.choice.id).choice_text, "Choice 1.") - - -# class SuspendPollTests(TestCase): -# def setUp(self): -# self.client = Client() -# self.poll = create_poll( -# question="Create Sample Poll.", -# close_date=timezone.now() + datetime.timedelta(days=3), -# vtype=3, -# is_suspended=True, -# ) -# self.poll.choice_set.create(choice_text="Choice 1.") -# self.choice = Choice.objects.get(poll_id=self.poll.id) -# self.client.login(username="user1", email="user1@example.com", password="test") - -# def test_suspend_tests(self): -# response = self.client.get(reverse("my_polls")) -# self.assertEqual(response.status_code, 200) - -# # Debugging: print response content -# print(response.content.decode()) - -# self.assertContains(response, "id='unsuspend-poll-1'> unsuspend ") - -# # Unsuspend the poll and check again -# self.poll.is_suspended = False -# self.poll.save() - -# response = self.client.get(reverse("my_polls")) - -# # Debugging: print response content -# print(response.content.decode()) - -# self.assertContains(response, "id='suspend-poll-1'> suspend ") - -# def test_suspended_tests_cannot_vote(self): -# response = self.client.get(reverse("detail", args=(self.poll.id,))) -# self.assertEqual(response.status_code, 200) - -# # Debugging: print response content -# print(response.content.decode()) - -# self.assertContains( -# response, "Sorry! This poll has been temporarily suspended." -# ) -# self.assertContains( -# response, -# "", -# html=True, -# ) - - class TagCloudTests(TestCase): def setUp(self): self.client = Client() @@ -780,16 +622,19 @@ def setUp(self): def test_poll_tag_exists(self): response = self.client.get(reverse("detail", args=(1,))) self.assertEqual(response.status_code, 200) - self.assertContains(response, "new york") + self.assertContains(response, "/tag/new%20york/") + self.assertContains(response, "new york") def test_poll_tags_index(self): # print [pt.tag_text for pt in self.poll.polltag_set.all()] response = self.client.get(reverse("tagged_polls", args=("New York",))) self.assertEqual(response.status_code, 200) + print(response.content) self.assertContains(response, 'Create Sample Poll.') def test_poll_delete(self): self.poll.polltag_set.clear() response = self.client.get(reverse("detail", args=(1,))) self.assertEqual(response.status_code, 200) - self.assertNotContains(response, "new york") + self.assertNotContains(response, "/", views.DetailView.as_view(), name="detail"), + path("/delete/", views.delete_poll, name="delete_poll"), path("/results/", views.ResultsView.as_view(), name="results"), path( "/embed_instructions/", @@ -16,7 +17,7 @@ name="embed_instructions", ), path("/vote/", views.vote, name="vote"), - path("/edit/", views.EditView.as_view(), name="edit"), + # path("/edit/", views.EditView.as_view(), name="edit"), path( "/change_suspension/", views.change_suspension, diff --git a/approval_polls/views.py b/approval_polls/views.py index 41cd761..a245c59 100644 --- a/approval_polls/views.py +++ b/approval_polls/views.py @@ -3,28 +3,21 @@ import re import structlog +from django.contrib import messages from django.contrib.auth import authenticate, login, logout from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User -from django.core.exceptions import PermissionDenied from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.db.models import Count -from django.http import Http404, HttpResponseRedirect -from django.shortcuts import get_object_or_404, render +from django.http import HttpResponseRedirect, HttpResponseServerError +from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils import timezone from django.utils.decorators import method_decorator from django.views import generic from django.views.decorators.http import require_http_methods -from approval_polls.models import ( - Ballot, - Choice, - Poll, - PollTag, - Subscription, - VoteInvitation, -) +from approval_polls.models import Ballot, Poll, PollTag, Subscription, VoteInvitation from .forms import ManageSubscriptionsForm, NewUsernameForm @@ -129,7 +122,7 @@ def my_polls(request): def tagged_polls(request, tag): t = PollTag.objects.get(tag_text=tag.lower()) poll_list = t.polls.all() - return get_polls(request, poll_list, "index.html") + return get_polls(request, poll_list, "index.html", tag=t) @login_required @@ -159,7 +152,7 @@ def set_user_timezone(request): return HttpResponseRedirect(redirect_url) -def get_polls(request, poll_list, render_page): +def get_polls(request, poll_list, render_page, tag: str = ""): paginator = Paginator(poll_list, 5) page = request.GET.get("page") try: @@ -168,7 +161,7 @@ def get_polls(request, poll_list, render_page): polls = paginator.page(1) except EmptyPage: polls = paginator.page(paginator.num_pages) - return render(request, render_page, {"latest_poll_list": polls}) + return render(request, render_page, {"latest_poll_list": polls, "tag": tag}) def change_suspension(request, poll_id): @@ -260,10 +253,19 @@ def get_context_data(self, **kwargs): context["time_difference"] = time_diff.total_seconds() return context - def delete(self, request, *args, **kwargs): - poll_id = self.get_object().id - Poll.objects.filter(id=poll_id).delete() - return HttpResponseRedirect("/my-polls/") + +@login_required +def delete_poll(request, poll_id): + logger.debug(f"Attempting to delete poll {poll_id}") + try: + poll = get_object_or_404(Poll, id=poll_id, user=request.user) + logger.debug(f"Found poll: {poll}") + poll.delete() + messages.success(request, "Poll deleted successfully.") + return redirect("my_polls") # Redirect to the list of user's polls + except Exception as e: + logger.error(f"Error deleting poll: {str(e)}") + return HttpResponseServerError(f"An error occurred: {str(e)}") class ResultsView(generic.DetailView): @@ -668,171 +670,171 @@ def post(self, request, *args, **kwargs): return HttpResponseRedirect(reverse("embed_instructions", args=(p.id,))) -class EditView(generic.View): - @method_decorator(login_required) - def get(self, request, *args, **kwargs): - try: - poll = Poll.objects.get(id=kwargs["poll_id"]) - except Poll.DoesNotExist: - raise Http404("Poll does not exist") - - if request.user != poll.user and not request.user.is_staff: - raise PermissionDenied - - choices = Choice.objects.filter(poll=kwargs["poll_id"]) - # convert closedatetime to localtime. - if poll.close_date: - closedatetime = timezone.localtime(poll.close_date) - return render( - request, - "edit.html", - { - "poll": poll, - "choices": choices, - "closedatetime": ( - closedatetime.strftime("%Y/%m/%d %H:%M") if poll.close_date else "" - ), - "can_edit_poll": poll.can_edit(), - "choices_count": Choice.objects.last().id, - "blank_choices": [], - "choice_blank_error": False, - "existing_choice_texts": {"new": {}, "existing": {}}, - "existing_choice_links": {"new": {}, "existing": {}}, - "invited_emails": ",".join([str(r) for r in poll.invited_emails()]), - "all_tags": poll.all_tags(), - }, - ) - - @method_decorator(login_required) - def post(self, request, *args, **kwargs): - existing_choice_texts = {} - existing_choice_links = {} - tags_to_add = [] - tags_to_delete = [] - poll = Poll.objects.get(id=kwargs["poll_id"]) - closedatetime = request.POST["close-datetime"] - try: - original_close_date = poll.close_date - closedatetime = datetime.datetime.strptime(closedatetime, "%Y/%m/%d %H:%M") - current_datetime = timezone.localtime(timezone.now()) - current_tzinfo = current_datetime.tzinfo - closedatetime = closedatetime.replace(tzinfo=current_tzinfo) - poll.close_date = closedatetime - except ValueError: - poll.close_date = original_close_date - poll.show_close_date = "show-close-date" in request.POST - poll.show_countdown = "show-countdown" in request.POST - poll.is_private = "public-poll-visibility" not in request.POST - poll.show_write_in = "show-write-in" in request.POST - poll.show_lead_color = "show-lead-color" in request.POST - poll.show_email_opt_in = "show-email-opt-in" in request.POST - if "radio-poll-type" in request.POST: - poll.vtype = int(request.POST["radio-poll-type"]) - poll.save() - if "token-emails" in request.POST: - poll.send_vote_invitations(request.POST["token-emails"]) - existing_tags_set = set(poll.all_tags().split(",")) - if len(request.POST["token-tags"]) > 0: - request_tags_set = set( - [tag.strip() for tag in request.POST["token-tags"].split(",")] - ) - else: - request_tags_set = set([]) - tags_to_add = list(request_tags_set - existing_tags_set) - tags_to_delete = list(existing_tags_set - request_tags_set) - if len(tags_to_add) > 0: - poll.add_tags(tags_to_add) - if len(tags_to_delete) > 0: - poll.delete_tags(tags_to_delete) - if poll.can_edit(): - if poll.question != request.POST["question"]: - poll.question = request.POST["question"].strip() - choices = Choice.objects.filter(poll=kwargs["poll_id"]) - request_choice_ids = [] - create_data_for_text = {} - create_data_for_link = {} - update_data_for_text = {} - update_data_for_link = {} - choice_blank = False - for k in list(request.POST.keys()): - m = re.search(r"choice(\d+)", k) - if m and m.group(1): - id = m.group(1) - request_choice_ids.append(int(id)) - poll_choice_ids = [choice.id for choice in choices] - request_choice_ids_set = set(request_choice_ids) - poll_choice_ids_set = set(poll_choice_ids) - choice_ids_for_create = request_choice_ids_set - poll_choice_ids_set - choice_ids_for_delete = poll_choice_ids_set - request_choice_ids_set - choice_ids_for_update = poll_choice_ids_set & request_choice_ids_set - new_choice_len = len(choice_ids_for_create) - update_choice_len = len(choice_ids_for_update) - delete_choice_len = len(choice_ids_for_delete) - if new_choice_len > 0: - choice_ids_for_create_dup = choice_ids_for_create.copy() - for i in choice_ids_for_create_dup: - create_text = request.POST["choice" + (str(i))] - if len(create_text) == 0: - choice_ids_for_create.remove(i) - continue - else: - create_data_for_text[i] = create_text - create_data_for_link[i] = request.POST[ - "linkurl-choice" + (str(i)) - ] - existing_choice_texts["new"] = create_data_for_text - existing_choice_links["new"] = create_data_for_link - if update_choice_len > 0: - blank_choices = [] - for i in choice_ids_for_update: - update_text = request.POST["choice" + (str(i))] - if len(update_text) == 0: - choice_blank = True - blank_choices.append(i) - else: - update_data_for_text[i] = update_text - update_data_for_link[i] = request.POST[ - "linkurl-choice" + (str(i)) - ] - existing_choice_texts["existing"] = update_data_for_text - existing_choice_links["existing"] = update_data_for_link - # If any current poll choices are left blank by user - if choice_blank: - ccount = Choice.objects.last().id + new_choice_len - return render( - request, - "edit.html", - { - "poll": poll, - "choices": choices, - "choice_blank_error": choice_blank, - "choices_count": ccount, - "can_edit_poll": poll.can_edit(), - "blank_choices": blank_choices, - "existing_choice_texts": existing_choice_texts, - "existing_choice_links": existing_choice_links, - "invited_emails": ",".join( - [str(r) for r in poll.invited_emails()] - ), - "all_tags": poll.all_tags(), - }, - ) - - # No current poll choices are blank, so go ahead and update, create, delete choices - if new_choice_len > 0: - poll.add_choices( - choice_ids_for_create, create_data_for_text, create_data_for_link - ) - if update_choice_len > 0: - poll.update_choices( - choice_ids_for_update, update_data_for_text, update_data_for_link - ) - if delete_choice_len > 0: - poll.delete_choices(choice_ids_for_delete) - - poll.save() - - return HttpResponseRedirect(reverse("my_polls")) +# class EditView(generic.View): +# @method_decorator(login_required) +# def get(self, request, *args, **kwargs): +# try: +# poll = Poll.objects.get(id=kwargs["poll_id"]) +# except Poll.DoesNotExist: +# raise Http404("Poll does not exist") + +# if request.user != poll.user and not request.user.is_staff: +# raise PermissionDenied + +# choices = Choice.objects.filter(poll=kwargs["poll_id"]) +# # convert closedatetime to localtime. +# if poll.close_date: +# closedatetime = timezone.localtime(poll.close_date) +# return render( +# request, +# "edit.html", +# { +# "poll": poll, +# "choices": choices, +# "closedatetime": ( +# closedatetime.strftime("%Y/%m/%d %H:%M") if poll.close_date else "" +# ), +# "can_edit_poll": poll.can_edit(), +# "choices_count": Choice.objects.last().id, +# "blank_choices": [], +# "choice_blank_error": False, +# "existing_choice_texts": {"new": {}, "existing": {}}, +# "existing_choice_links": {"new": {}, "existing": {}}, +# "invited_emails": ",".join([str(r) for r in poll.invited_emails()]), +# "all_tags": poll.all_tags(), +# }, +# ) + +# @method_decorator(login_required) +# def post(self, request, *args, **kwargs): +# existing_choice_texts = {} +# existing_choice_links = {} +# tags_to_add = [] +# tags_to_delete = [] +# poll = Poll.objects.get(id=kwargs["poll_id"]) +# closedatetime = request.POST["close-datetime"] +# try: +# original_close_date = poll.close_date +# closedatetime = datetime.datetime.strptime(closedatetime, "%Y/%m/%d %H:%M") +# current_datetime = timezone.localtime(timezone.now()) +# current_tzinfo = current_datetime.tzinfo +# closedatetime = closedatetime.replace(tzinfo=current_tzinfo) +# poll.close_date = closedatetime +# except ValueError: +# poll.close_date = original_close_date +# poll.show_close_date = "show-close-date" in request.POST +# poll.show_countdown = "show-countdown" in request.POST +# poll.is_private = "public-poll-visibility" not in request.POST +# poll.show_write_in = "show-write-in" in request.POST +# poll.show_lead_color = "show-lead-color" in request.POST +# poll.show_email_opt_in = "show-email-opt-in" in request.POST +# if "radio-poll-type" in request.POST: +# poll.vtype = int(request.POST["radio-poll-type"]) +# poll.save() +# if "token-emails" in request.POST: +# poll.send_vote_invitations(request.POST["token-emails"]) +# existing_tags_set = set(poll.all_tags().split(",")) +# if len(request.POST["token-tags"]) > 0: +# request_tags_set = set( +# [tag.strip() for tag in request.POST["token-tags"].split(",")] +# ) +# else: +# request_tags_set = set([]) +# tags_to_add = list(request_tags_set - existing_tags_set) +# tags_to_delete = list(existing_tags_set - request_tags_set) +# if len(tags_to_add) > 0: +# poll.add_tags(tags_to_add) +# if len(tags_to_delete) > 0: +# poll.delete_tags(tags_to_delete) +# if poll.can_edit(): +# if poll.question != request.POST["question"]: +# poll.question = request.POST["question"].strip() +# choices = Choice.objects.filter(poll=kwargs["poll_id"]) +# request_choice_ids = [] +# create_data_for_text = {} +# create_data_for_link = {} +# update_data_for_text = {} +# update_data_for_link = {} +# choice_blank = False +# for k in list(request.POST.keys()): +# m = re.search(r"choice(\d+)", k) +# if m and m.group(1): +# id = m.group(1) +# request_choice_ids.append(int(id)) +# poll_choice_ids = [choice.id for choice in choices] +# request_choice_ids_set = set(request_choice_ids) +# poll_choice_ids_set = set(poll_choice_ids) +# choice_ids_for_create = request_choice_ids_set - poll_choice_ids_set +# choice_ids_for_delete = poll_choice_ids_set - request_choice_ids_set +# choice_ids_for_update = poll_choice_ids_set & request_choice_ids_set +# new_choice_len = len(choice_ids_for_create) +# update_choice_len = len(choice_ids_for_update) +# delete_choice_len = len(choice_ids_for_delete) +# if new_choice_len > 0: +# choice_ids_for_create_dup = choice_ids_for_create.copy() +# for i in choice_ids_for_create_dup: +# create_text = request.POST["choice" + (str(i))] +# if len(create_text) == 0: +# choice_ids_for_create.remove(i) +# continue +# else: +# create_data_for_text[i] = create_text +# create_data_for_link[i] = request.POST[ +# "linkurl-choice" + (str(i)) +# ] +# existing_choice_texts["new"] = create_data_for_text +# existing_choice_links["new"] = create_data_for_link +# if update_choice_len > 0: +# blank_choices = [] +# for i in choice_ids_for_update: +# update_text = request.POST["choice" + (str(i))] +# if len(update_text) == 0: +# choice_blank = True +# blank_choices.append(i) +# else: +# update_data_for_text[i] = update_text +# update_data_for_link[i] = request.POST[ +# "linkurl-choice" + (str(i)) +# ] +# existing_choice_texts["existing"] = update_data_for_text +# existing_choice_links["existing"] = update_data_for_link +# # If any current poll choices are left blank by user +# if choice_blank: +# ccount = Choice.objects.last().id + new_choice_len +# return render( +# request, +# "edit.html", +# { +# "poll": poll, +# "choices": choices, +# "choice_blank_error": choice_blank, +# "choices_count": ccount, +# "can_edit_poll": poll.can_edit(), +# "blank_choices": blank_choices, +# "existing_choice_texts": existing_choice_texts, +# "existing_choice_links": existing_choice_links, +# "invited_emails": ",".join( +# [str(r) for r in poll.invited_emails()] +# ), +# "all_tags": poll.all_tags(), +# }, +# ) + +# # No current poll choices are blank, so go ahead and update, create, delete choices +# if new_choice_len > 0: +# poll.add_choices( +# choice_ids_for_create, create_data_for_text, create_data_for_link +# ) +# if update_choice_len > 0: +# poll.update_choices( +# choice_ids_for_update, update_data_for_text, update_data_for_link +# ) +# if delete_choice_len > 0: +# poll.delete_choices(choice_ids_for_delete) + +# poll.save() + +# return HttpResponseRedirect(reverse("my_polls")) def logoutView(request): diff --git a/pyproject.toml b/pyproject.toml index 3b9fbc6..e231c44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,3 +37,7 @@ build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] DJANGO_SETTINGS_MODULE = "approval_polls.settings" python_files = "tests.py test_*.py *_tests.py" + +[tool.bandit] +exclude_dirs = ['*_test.py', '*/test_*.py', '*/tests.py'] +skips = ["B106"]