# HG changeset patch # User bsw # Date 1581431076 -3600 # Node ID ed3c40911ae1bc6f41c628d1afa4eb823e19b375 # Parent 17e7082c377a6db8798087b037eec588c1cd42c0 Completed image attachments feature diff -r 17e7082c377a -r ed3c40911ae1 app/main/draft/_action/add.lua --- a/app/main/draft/_action/add.lua Mon Feb 10 21:10:49 2020 +0100 +++ b/app/main/draft/_action/add.lua Tue Feb 11 15:24:36 2020 +0100 @@ -1,24 +1,270 @@ -local draft_text = param.get("content") +local initiative +local new_initiative +local draft_id +local status + +if param.get("initiative_id", atom.integer) then + + local function donew() + local draft_text = param.get("content") + + if not draft_text then + return false + end + + local draft_text = util.wysihtml_preproc(draft_text) + + local valid_html, error_message = util.html_is_safe(draft_text) + if not valid_html then + slot.put_into("error", _("Draft contains invalid formatting or character sequence: #{error_message}", { error_message = error_message }) ) + return false + end + + if config.initiative_abstract then + local abstract = param.get("abstract") + if not abstract then + return false + end + abstract = encode.html(abstract) + draft_text = abstract .. "" .. draft_text + end + + draft_id = Draft:update_content( + app.session.member.id, + param.get("initiative_id", atom.integer), + param.get("formatting_engine"), + draft_text, + nil, + param.get("preview") or param.get("edit") + ) + return draft_id and true or false + end + + status = donew() + +else + + local function donew() + local issue + local area -if not draft_text then - return false -end + local issue_id = param.get("issue_id", atom.integer) + if issue_id then + issue = Issue:new_selector():add_where{"id=?",issue_id}:for_share():single_object_mode():exec() + if issue.closed then + slot.put_into("error", _"This issue is already closed.") + return false + elseif issue.fully_frozen then + slot.put_into("error", _"Voting for this issue has already begun.") + return false + elseif issue.phase_finished then + slot.put_into("error", _"Current phase is already closed.") + return false + end + area = issue.area + else + local area_id = param.get("area_id", atom.integer) + area = Area:new_selector():add_where{"id=?",area_id}:single_object_mode():exec() + if not area.active then + slot.put_into("error", "Invalid area.") + return false + end + end + + if not app.session.member:has_voting_right_for_unit_id(area.unit_id) then + return execute.view { module = "index", view = "403" } + end + + local policy_id = param.get("policy_id", atom.integer) + local policy + if policy_id then + policy = Policy:by_id(policy_id) + end -local draft_text = util.wysihtml_preproc(draft_text) + if not issue then + if policy_id == -1 then + slot.put_into("error", _"Please choose a policy") + return false + end + if not policy.active then + slot.put_into("error", "Invalid policy.") + return false + end + if policy.polling and not app.session.member:has_polling_right_for_unit_id(area.unit_id) then + return execute.view { module = "index", view = "403" } + end + if not area:get_reference_selector("allowed_policies") + :add_where{ "policy.id = ?", policy_id } + :optional_object_mode() + :exec() + then + slot.put_into("error", "policy not allowed") + return false + end + end + + local is_polling = (issue and param.get("polling", atom.boolean)) or (policy and policy.polling) or false + + local tmp = db:query({ "SELECT text_entries_left, initiatives_left FROM member_contingent_left WHERE member_id = ? AND polling = ?", app.session.member.id, is_polling }, "opt_object") + if not tmp or tmp.initiatives_left < 1 then + slot.put_into("error", _"Sorry, your contingent for creating initiatives has been used up. Please try again later.") + return false + end + if tmp and tmp.text_entries_left < 1 then + slot.put_into("error", _"Sorry, you have reached your personal flood limit. Please be slower...") + return false + end + + local name = param.get("name") + + local name = util.trim(name) + + if #name < 3 then + slot.put_into("error", _"Please enter a meaningful title for your initiative!") + return false + end + + if #name > 140 then + slot.put_into("error", _"This title is too long!") + return false + end -local valid_html, error_message = util.html_is_safe(draft_text) -if not valid_html then - slot.put_into("error", _("Draft contains invalid formatting or character sequence: #{error_message}", { error_message = error_message }) ) - return false -end + local timing + if not issue and policy.free_timeable then + local free_timing_string = util.trim(param.get("free_timing")) + if not free_timing_string or #free_timing_string < 1 then + slot.put_into("error", _"Choose timing") + return false + end + local available_timings + if config.free_timing and config.free_timing.available_func then + available_timings = config.free_timing.available_func(policy) + if available_timings == false then + slot.put_into("error", "error in free timing config") + return false + end + end + if available_timings then + local timing_available = false + for i, available_timing in ipairs(available_timings) do + if available_timing.id == free_timing_string then + timing_available = true + end + end + if not timing_available then + slot.put_into("error", _"Invalid timing") + return false + end + end + timing = config.free_timing.calculate_func(policy, free_timing_string) + if not timing then + slot.put_into("error", "error in free timing config") + return false + end + end + + local draft_text = param.get("content") + + if not draft_text then + slot.put_into("error", "no draft text") + return false + end + + local draft_text = util.wysihtml_preproc(draft_text) + + local valid_html, error_message = util.html_is_safe(draft_text) + if not valid_html then + slot.put_into("error", _("Draft contains invalid formatting or character sequence: #{error_message}", { error_message = error_message }) ) + return false + end + + if config.initiative_abstract then + local abstract = param.get("abstract") + if not abstract then + slot.put_into("error", "no abstract") + return false + end + abstract = encode.html(abstract) + draft_text = abstract .. "" .. draft_text + end + + local location = param.get("location") + if location == "" then + location = nil + end + + if param.get("preview") or param.get("edit") then + return false + end -if config.initiative_abstract then - local abstract = param.get("abstract") - if not abstract then - return false + initiative = Initiative:new() + + if not issue then + issue = Issue:new() + issue.area_id = area.id + issue.policy_id = policy_id + + if policy.polling then + issue.accepted = 'now' + issue.state = 'discussion' + initiative.polling = true + + if policy.free_timeable then + issue.discussion_time = timing.discussion + issue.verification_time = timing.verification + issue.voting_time = timing.voting + end + + end + + issue:save() + + if config.etherpad then + local result = net.curl( + config.etherpad.api_base + .. "api/1/createGroupPad?apikey=" .. config.etherpad.api_key + .. "&groupID=" .. config.etherpad.group_id + .. "&padName=Issue" .. tostring(issue.id) + .. "&text=" .. request.get_absolute_baseurl() .. "issue/show/" .. tostring(issue.id) .. ".html" + ) + end + end + + if param.get("polling", atom.boolean) and app.session.member:has_polling_right_for_unit_id(area.unit_id) then + initiative.polling = true + end + initiative.issue_id = issue.id + initiative.name = name + initiative:save() + + new_initiative = initiative + + local draft = Draft:new() + draft.initiative_id = initiative.id + draft.formatting_engine = formatting_engine + draft.content = draft_text + draft.location = location + draft.author_id = app.session.member.id + draft:save() + + draft_id = draft.id + + local initiator = Initiator:new() + initiator.initiative_id = initiative.id + initiator.member_id = app.session.member.id + initiator.accepted = true + initiator:save() + + if not is_polling then + local supporter = Supporter:new() + supporter.initiative_id = initiative.id + supporter.member_id = app.session.member.id + supporter.draft_id = draft.id + supporter:save() + end + end - abstract = encode.html(abstract) - draft_text = abstract .. "" .. draft_text + status = donew() end if config.attachments then @@ -80,76 +326,78 @@ fh:write(json.export(file_uploads)) fh:write("\n") fh:close() -end + + if draft_id then + local file_upload_session = param.get("file_upload_session") + file_upload_session = string.gsub(file_upload_session, "[^A-Za-z0-9]", "") -local draft_id = Draft:update_content( - app.session.member.id, - param.get("initiative_id", atom.integer), - param.get("formatting_engine"), - draft_text, - nil, - param.get("preview") or param.get("edit") -) + local draft_attachments = DraftAttachment:new_selector() + :add_where{ "draft_attachment.draft_id = ?", draft_id } + :exec() + + for i, draft_attachment in ipairs(draft_attachments) do + if param.get("file_delete_" .. draft_attachment.file_id, atom.boolean) then + draft_attachment:destroy() + end + end -if draft_id and config.attachments then - local file_upload_session = param.get("file_upload_session") - file_upload_session = string.gsub(file_upload_session, "[^A-Za-z0-9]", "") + local file_uploads = json.array() + local filename = encode.file_path(WEBMCP_BASE_PATH, 'tmp', "file_upload-" .. file_upload_session .. ".json") + local fh = io.open(filename, "r") + if fh then + file_uploads = json.import(fh:read("*a")) + end + for i, file_upload in ipairs(file_uploads) do + local filename = encode.file_path(WEBMCP_BASE_PATH, 'tmp', "file_upload-" .. file_upload_session .. "-" .. file_upload.id .. ".jpg") + local data + local fh = io.open(filename, "r") + if fh then + data = fh:read("*a") + end + local filename = encode.file_path(WEBMCP_BASE_PATH, 'tmp', "file_upload-" .. file_upload_session .. "-" .. file_upload.id .. ".preview.jpg") + local data_preview + local fh = io.open(filename, "r") + if fh then + data_preview = fh:read("*a") + end + + local hash = moonhash.sha3_512(data) - local draft_attachments = DraftAttachment:new_selector() - :add_where{ "draft_attachment.draft_id = ?", draft_id } - :exec() + local file = File:new_selector() + :add_where{ "hash = ?", hash } + :add_where{ "content_type = ?", "image/jpeg" } + :optional_object_mode() + :exec() - for i, draft_attachment in ipairs(draft_attachments) do - if param.get("file_delete_" .. draft_attachment.file_id, atom.boolean) then - draft_attachment:destroy() + if not file then + file = File:new() + file.content_type = "image/jpeg" + file.hash = hash + file.data = data + file.preview_content_type = "image/jpeg" + file.preview_data = data_preview + file:save() + end + + local draft_attachment = DraftAttachment:new() + draft_attachment.draft_id = draft_id + draft_attachment.file_id = file.id + draft_attachment.title = file_upload.title + draft_attachment.description = file_upload.description + draft_attachment:save() end end - local file_uploads = json.array() - local filename = encode.file_path(WEBMCP_BASE_PATH, 'tmp', "file_upload-" .. file_upload_session .. ".json") - local fh = io.open(filename, "r") - if fh then - file_uploads = json.import(fh:read("*a")) - end - for i, file_upload in ipairs(file_uploads) do - local filename = encode.file_path(WEBMCP_BASE_PATH, 'tmp', "file_upload-" .. file_upload_session .. "-" .. file_upload.id .. ".jpg") - local data - local fh = io.open(filename, "r") - if fh then - data = fh:read("*a") - end - local filename = encode.file_path(WEBMCP_BASE_PATH, 'tmp', "file_upload-" .. file_upload_session .. "-" .. file_upload.id .. ".preview.jpg") - local data_preview - local fh = io.open(filename, "r") - if fh then - data_preview = fh:read("*a") - end - - local hash = moonhash.sha3_512(data) - - local file = File:new_selector() - :add_where{ "hash = ?", hash } - :add_where{ "content_type = ?", "image/jpeg" } - :optional_object_mode() - :exec() - - if not file then - file = File:new() - file.content_type = "image/jpeg" - file.hash = hash - file.data = data - file.preview_content_type = "image/jpeg" - file.preview_data = data_preview - file:save() - end - - local draft_attachment = DraftAttachment:new() - draft_attachment.draft_id = draft_id - draft_attachment.file_id = file.id - draft_attachment.title = file_upload.title - draft_attachment.description = file_upload.description - draft_attachment:save() - end end -return draft_id and true or false +print(new_initiative, status) +if new_initiative and status ~= false then + request.redirect{ + module = "initiative", + view = "show", + id = new_initiative.id + } +end + +return status + diff -r 17e7082c377a -r ed3c40911ae1 app/main/draft/diff.lua --- a/app/main/draft/diff.lua Mon Feb 10 21:10:49 2020 +0100 +++ b/app/main/draft/diff.lua Tue Feb 11 15:24:36 2020 +0100 @@ -190,6 +190,98 @@ end end } + + local old_files = File:new_selector() + :left_join("draft_attachment", nil, "draft_attachment.file_id = file.id") + :add_where{ "draft_attachment.draft_id = ?", old_draft.id } + :reset_fields() + :add_field("file.id") + :add_field("draft_attachment.title") + :add_field("draft_attachment.description") + :add_order_by("draft_attachment.id") + :exec() + + local new_files = File:new_selector() + :left_join("draft_attachment", nil, "draft_attachment.file_id = file.id") + :add_where{ "draft_attachment.draft_id = ?", new_draft.id } + :reset_fields() + :add_field("file.id") + :add_field("draft_attachment.title") + :add_field("draft_attachment.description") + :add_order_by("draft_attachment.id") + :exec() + + local added_files = {} + for i, new_file in ipairs(new_files) do + local added = true + for j, old_file in ipairs(old_files) do + if + old_file.file_id == new_file.file_id + and old_file.title == new_file.title + and old_file.description == new_file.description + then + added = false + end + end + if added then + table.insert(added_files, new_file) + end + end + + if #added_files > 0 then + ui.container { + attr = { class = "mdl-card__content mdl-card--border" }, + content = function() + ui.container{ content = _"Added attachments" } + for i, file in ipairs(added_files) do + ui.image{ module = "file", view = "show.jpg", id = file.id, params = { preview = true } } + ui.container{ content = file.title or "" } + ui.container{ content = file.description or "" } + slot.put("

") + end + end + } + end + + local removed_files = {} + for i, old_file in ipairs(old_files) do + local removed = true + for j, new_file in ipairs(new_files) do + if + old_file.file_id == new_file.file_id + and old_file.title == new_file.title + and old_file.description == new_file.description + then + removed = false + end + end + if removed then + table.insert(removed_files, old_file) + end + end + + + if #removed_files > 0 then + ui.container { + attr = { class = "mdl-card__content mdl-card--border" }, + content = function() + ui.container{ content = _"Removed attachments" } + for i, file in ipairs(removed_files) do + ui.image{ module = "file", view = "show.jpg", id = file.id, params = { preview = true } } + ui.container{ content = file.title or "" } + ui.container{ content = file.description or "" } + slot.put("

") + end + end + } + end + + ui.container { + attr = { class = "draft mdl-card__content mdl-card--border" }, + content = function () + end + } + end } end } ui.cell_sidebar{ content = function() diff -r 17e7082c377a -r ed3c40911ae1 app/main/draft/new.lua --- a/app/main/draft/new.lua Mon Feb 10 21:10:49 2020 +0100 +++ b/app/main/draft/new.lua Tue Feb 11 15:24:36 2020 +0100 @@ -1,24 +1,68 @@ -local initiative = Initiative:by_id(param.get("initiative_id")) -initiative:load_everything_for_member_id(app.session.member_id) -initiative.issue:load_everything_for_member_id(app.session.member_id) +local issue +local area +local area_id -if initiative.issue.closed then - slot.put_into("error", _"This issue is already closed.") - return -elseif initiative.issue.half_frozen then - slot.put_into("error", _"This issue is already frozen.") - return -elseif initiative.issue.phase_finished then - slot.put_into("error", _"Current phase is already closed.") - return +local issue_id = param.get("issue_id", atom.integer) +if issue_id then + issue = Issue:new_selector():add_where{"id=?",issue_id}:single_object_mode():exec() + issue:load_everything_for_member_id(app.session.member_id) + area = issue.area +else + area_id = param.get("area_id", atom.integer) + if area_id then + area = Area:new_selector():add_where{"id=?",area_id}:single_object_mode():exec() + area:load_delegation_info_once_for_member_id(app.session.member_id) + end +end + +local polling = param.get("polling", atom.boolean) + +local policy_id = param.get("policy_id", atom.integer) +local policy + +local preview = param.get("preview") + +if #(slot.get_content("error")) > 0 then + preview = false +end + +if policy_id then + policy = Policy:by_id(policy_id) end -local draft = initiative.current_draft -if config.initiative_abstract then - draft.abstract = string.match(draft.content, "(.+)") - if draft.abstract then - draft.content = string.match(draft.content, "(.*)") + + + +local initiative_id = param.get("initiative_id") +local initiative = Initiative:by_id(initiative_id) +local draft +if initiative then + initiative:load_everything_for_member_id(app.session.member_id) + initiative.issue:load_everything_for_member_id(app.session.member_id) + + if initiative.issue.closed then + slot.put_into("error", _"This issue is already closed.") + return + elseif initiative.issue.half_frozen then + slot.put_into("error", _"This issue is already frozen.") + return + elseif initiative.issue.phase_finished then + slot.put_into("error", _"Current phase is already closed.") + return end + + draft = initiative.current_draft + if config.initiative_abstract then + draft.abstract = string.match(draft.content, "(.+)") + if draft.abstract then + draft.content = string.match(draft.content, "(.*)") + end + end +end + +if not initiative and not issue and not area then + ui.heading{ content = _"Missing parameter" } + return false end ui.form{ @@ -26,13 +70,17 @@ attr = { class = "vertical section", enctype = 'multipart/form-data' }, module = "draft", action = "add", - params = { initiative_id = initiative.id }, + params = { + area_id = area and area.id, + issue_id = issue and issue.id or nil, + initiative_id = initiative_id + }, routing = { ok = { mode = "redirect", module = "initiative", view = "show", - id = initiative.id + id = initiative_id } }, content = function() @@ -41,11 +89,30 @@ ui.cell_main{ content = function() ui.container{ attr = { class = "mdl-card mdl-shadow--2dp mdl-card__fullwidth" }, content = function() ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function() - ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = initiative.display_name } + if initiative then + ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = initiative.display_name } + elseif param.get("name") then + ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = param.get("name") } + elseif issue then + ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _("New competing initiative in issue '#{issue}'", { issue = issue.name }) } + elseif area then + ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _("New issue in area '#{area}'", { area = area.name }) } + end end } ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function() + +-- -------- PREVIEW if param.get("preview") then ui.sectionRow( function() + if not issue and not initiative then + ui.container { content = policy.name } + end + if param.get("free_timing") then + ui.container { content = param.get("free_timing") } + end + slot.put("
") + ui.field.hidden{ name = "policy_id", value = param.get("policy_id") } + ui.field.hidden{ name = "name", value = param.get("name") } if config.initiative_abstract then ui.field.hidden{ name = "abstract", value = param.get("abstract") } ui.container{ @@ -72,31 +139,35 @@ ) file_upload_session = string.gsub(file_upload_session, "[^A-Za-z0-9]", "") ui.field.hidden{ name = "file_upload_session", value = file_upload_session } - local files = File:new_selector() - :left_join("draft_attachment", nil, "draft_attachment.file_id = file.id") - :add_where{ "draft_attachment.draft_id = ?", initiative.current_draft.id } - :reset_fields() - :add_field("file.id") - :add_field("draft_attachment.title") - :add_field("draft_attachment.description") - :add_order_by("draft_attachment.id") - :exec() + if initiative then + local files = File:new_selector() + :left_join("draft_attachment", nil, "draft_attachment.file_id = file.id") + :add_where{ "draft_attachment.draft_id = ?", initiative.current_draft.id } + :reset_fields() + :add_field("file.id") + :add_field("draft_attachment.title") + :add_field("draft_attachment.description") + :add_order_by("draft_attachment.id") + :exec() - if #files > 0 then - ui.container { - content = function() - for i, file in ipairs(files) do - if param.get("file_delete_" .. file.id, atom.boolean) then - ui.field.hidden{ name = "file_delete_" .. file.id, value = "1" } - else - ui.image{ module = "file", view = "show.jpg", id = file.id, params = { preview = true } } - ui.container{ content = file.title or "" } - ui.container{ content = file.description or "" } - slot.put("

") + if #files > 0 then + ui.container { + content = function() + for i, file in ipairs(files) do + if param.get("file_delete_" .. file.id, atom.boolean) then + ui.field.hidden{ name = "file_delete_" .. file.id, value = "1" } + else + ui.image{ module = "file", view = "show.jpg", id = file.id, params = { preview = true } } + ui.container{ content = function() + ui.tag{ tag = "strong", content = file.title or "" } + end } + ui.container{ content = file.description or "" } + slot.put("

") + end end end - end - } + } + end end local filename = encode.file_path(WEBMCP_BASE_PATH, 'tmp', "file_upload-" .. file_upload_session .. ".json") local fh = io.open(filename, "r") @@ -106,7 +177,9 @@ ui.image{ module = "draft", view = "show_file_upload", params = { file_upload_session = file_upload_session, file_id = file_upload.id, preview = true } } - ui.container{ content = file_upload.title or "" } + ui.container{ content = function() + ui.tag{ tag = "strong", content = file_upload.title or "" } + end } ui.container{ content = file_upload.description or "" } slot.put("
") end @@ -139,47 +212,108 @@ ui.link{ attr = { class = "mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect" }, content = _"Cancel", - module = "initiative", + module = initiative and "initiative" or "area", view = "show", - id = initiative.id + id = initiative_id or area_id } end ) +-- -------- EDIT else - ui.sectionRow( function() - if config.initiative_abstract then - ui.container { content = _"Enter abstract:" } - ui.container{ attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--expandable mdl-textfield__fullwidth" }, content = function() + + if not issue_id and not initiative_id then + local tmp = { { id = -1, name = "" } } + for i, allowed_policy in ipairs(area.allowed_policies) do + if not allowed_policy.polling or app.session.member:has_polling_right_for_unit_id(area.unit_id) then + tmp[#tmp+1] = allowed_policy + end + end + ui.container{ content = _"Please choose a policy for the new issue:" } + ui.field.select{ + name = "policy_id", + foreign_records = tmp, + foreign_id = "id", + foreign_name = "name", + value = param.get("policy_id", atom.integer) or area.default_policy and area.default_policy.id + } + if policy and policy.free_timeable then + local available_timings + if config.free_timing and config.free_timing.available_func then + available_timings = config.free_timing.available_func(policy) + if available_timings == false then + slot.put_into("error", "error in free timing config") + return false + end + end + ui.heading{ level = 4, content = _"Free timing:" } + if available_timings then + ui.field.select{ + name = "free_timing", + foreign_records = available_timings, + foreign_id = "id", + foreign_name = "name", + value = param.get("free_timing") + } + else ui.field.text{ - name = "abstract", - multiline = true, - attr = { id = "abstract", style = "height: 20ex; width: 100%;" }, - value = param.get("abstract") + name = "free_timing", + value = param.get("free_timing") } - end } + end end - - ui.container { content = _"Enter your proposal and/or reasons:" } - ui.field.wysihtml{ - name = "content", - multiline = true, - attr = { id = "draft", style = "height: 50ex; width: 100%;" }, - value = param.get("content") - } - if not issue or issue.state == "admission" or issue.state == "discussion" then - ui.container { content = _"You can change your text again anytime during admission and discussion phase" } - else - ui.container { content = _"You cannot change your text again later, because this issue is already in verfication phase!" } - end + end + + if issue and issue.policy.polling and app.session.member:has_polling_right_for_unit_id(area.unit_id) then + slot.put("
") + ui.field.boolean{ name = "polling", label = _"No admission needed", value = polling } + end + + if not initiative then + ui.container{ attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label mdl-card__fullwidth" }, content = function () + ui.field.text{ + attr = { id = "lf-initiative__name", class = "mdl-textfield__input" }, + label_attr = { class = "mdl-textfield__label", ["for"] = "lf-initiative__name" }, + label = _"Title", + name = "name", + value = param.get("name") + } + end } + end - slot.put("
") - if config.attachments then - local file_upload_session = param.get("file_upload_session") or multirand.string( - 32, - '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' - ) - file_upload_session = string.gsub(file_upload_session, "[^A-Za-z0-9]", "") - ui.field.hidden{ name = "file_upload_session", value = file_upload_session } + if config.initiative_abstract then + ui.container { content = _"Enter abstract:" } + ui.container{ attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--expandable mdl-textfield__fullwidth" }, content = function() + ui.field.text{ + name = "abstract", + multiline = true, + attr = { id = "abstract", style = "height: 20ex; width: 100%;" }, + value = param.get("abstract") + } + end } + end + + ui.container { content = _"Enter your proposal and/or reasons:" } + ui.field.wysihtml{ + name = "content", + multiline = true, + attr = { id = "draft", style = "height: 50ex; width: 100%;" }, + value = param.get("content") + } + if not issue or issue.state == "admission" or issue.state == "discussion" then + ui.container { content = _"You can change your text again anytime during admission and discussion phase" } + else + ui.container { content = _"You cannot change your text again later, because this issue is already in verfication phase!" } + end + + slot.put("
") + if config.attachments then + local file_upload_session = param.get("file_upload_session") or multirand.string( + 32, + '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' + ) + file_upload_session = string.gsub(file_upload_session, "[^A-Za-z0-9]", "") + ui.field.hidden{ name = "file_upload_session", value = file_upload_session } + if initiative then local files = File:new_selector() :left_join("draft_attachment", nil, "draft_attachment.file_id = file.id") :add_where{ "draft_attachment.draft_id = ?", initiative.current_draft.id } @@ -195,7 +329,9 @@ content = function() for i, file in ipairs(files) do ui.image{ module = "file", view = "show.jpg", id = file.id, params = { preview = true } } - ui.container{ content = file.title or "" } + ui.container{ content = function() + ui.tag{ tag = "strong", content = file.title or "" } + end } ui.container{ content = file.description or "" } ui.field.boolean{ label = _"delete", name = "file_delete_" .. file.id, value = param.get("file_delete_" .. file.id) and true or false } slot.put("

") @@ -203,61 +339,63 @@ end } end - - local filename = encode.file_path(WEBMCP_BASE_PATH, 'tmp', "file_upload-" .. file_upload_session .. ".json") - local fh = io.open(filename, "r") - if fh then - local file_uploads = json.import(fh:read("*a")) - for i, file_upload in ipairs(file_uploads) do - ui.image{ module = "draft", view = "show_file_upload", params = { - file_upload_session = file_upload_session, file_id = file_upload.id, preview = true - } } - ui.container{ content = file_upload.title or "" } - ui.container{ content = file_upload.description or "" } - ui.field.boolean{ label = _"delete", name = "file_upload_delete_" .. file_upload.id } - slot.put("
") - end + end + local filename = encode.file_path(WEBMCP_BASE_PATH, 'tmp', "file_upload-" .. file_upload_session .. ".json") + local fh = io.open(filename, "r") + if fh then + local file_uploads = json.import(fh:read("*a")) + for i, file_upload in ipairs(file_uploads) do + ui.image{ module = "draft", view = "show_file_upload", params = { + file_upload_session = file_upload_session, file_id = file_upload.id, preview = true + } } + ui.container{ content = function() + ui.tag{ tag = "strong", content = file_upload.title or "" } + end } + ui.container{ content = file_upload.description or "" } + ui.field.boolean{ label = _"delete", name = "file_upload_delete_" .. file_upload.id } + slot.put("
") end - ui.container{ attr = { id = "file_upload_template", style = "display: none;" }, content = function() - ui.field.text{ label = _"Title", name = "__ID_title__" } - ui.field.text{ label = _"Description", name = "__ID_description__" } - ui.field.image{ field_name = "__ID_file__" } - end } - ui.container{ attr = { id = "file_upload" }, content = function() - end } - ui.field.hidden{ attr = { id = "file_upload_last_id" }, name = "file_upload_last_id" } - ui.script{ script = [[ var file_upload_id = 1; ]] } - ui.tag{ tag = "a", content = _"Attach image", attr = { - href = "#", - onclick = "var html = document.getElementById('file_upload_template').innerHTML; html = html.replace('__ID_file__', 'file_' + file_upload_id); html = html.replace('__ID_title__', 'title_' + file_upload_id); html = html.replace('__ID_description__', 'description_' + file_upload_id); var el = document.createElement('div'); el.innerHTML = html; document.getElementById('file_upload').appendChild(el); document.getElementById('file_upload_last_id').value = file_upload_id; file_upload_id++; return false;" - } } - slot.put("
") - - slot.put("
") + end + ui.container{ attr = { id = "file_upload_template", style = "display: none;" }, content = function() + ui.field.text{ label = _"Title", name = "__ID_title__" } + ui.field.text{ label = _"Description", name = "__ID_description__" } + ui.field.image{ field_name = "__ID_file__" } + end } + ui.container{ attr = { id = "file_upload" }, content = function() + end } + ui.field.hidden{ attr = { id = "file_upload_last_id" }, name = "file_upload_last_id" } + ui.script{ script = [[ var file_upload_id = 1; ]] } + ui.tag{ tag = "a", content = _"Attach image", attr = { + href = "#", + onclick = "var html = document.getElementById('file_upload_template').innerHTML; html = html.replace('__ID_file__', 'file_' + file_upload_id); html = html.replace('__ID_title__', 'title_' + file_upload_id); html = html.replace('__ID_description__', 'description_' + file_upload_id); var el = document.createElement('div'); el.innerHTML = html; document.getElementById('file_upload').appendChild(el); document.getElementById('file_upload_last_id').value = file_upload_id; file_upload_id++; return false;" + } } + slot.put("
") + + slot.put("
") - end + end - ui.tag{ - tag = "input", - attr = { - type = "submit", - name = "preview", - class = "mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--colored", - value = _'Preview' - }, - content = "" - } - slot.put("   ") - - ui.link{ - content = _"Cancel", - module = "initiative", - view = "show", - id = initiative.id, - attr = { class = "mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect" } - } - - end ) + ui.tag{ + tag = "input", + attr = { + type = "submit", + name = "preview", + class = "mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--colored", + value = _'Preview' + }, + content = "" + } + slot.put("   ") + + ui.link{ + content = _"Cancel", + module = initiative and "initiative" or issue and "issue" or "index", + view = area and "index" or "show", + id = initiative_id or issue_id, + params = { area = area_id, unit = area and area.unit_id or nil }, + attr = { class = "mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect" } + } + end end } end } diff -r 17e7082c377a -r ed3c40911ae1 app/main/index/_head.lua --- a/app/main/index/_head.lua Mon Feb 10 21:10:49 2020 +0100 +++ b/app/main/index/_head.lua Tue Feb 11 15:24:36 2020 +0100 @@ -116,7 +116,7 @@ if not config.voting_only and app.session.member_id and app.session.member:has_initiative_right_for_unit_id ( area.unit_id ) then ui.link { attr = { class = "mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--underlined" }, - module = "initiative", view = "new", + module = "draft", view = "new", params = { area_id = area.id }, content = function() ui.tag{ tag = "i", attr = { class = "material-icons" }, content = "add" } diff -r 17e7082c377a -r ed3c40911ae1 app/main/initiative/_action/create.lua --- a/app/main/initiative/_action/create.lua Mon Feb 10 21:10:49 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,219 +0,0 @@ -local issue -local area - -local issue_id = param.get("issue_id", atom.integer) -if issue_id then - issue = Issue:new_selector():add_where{"id=?",issue_id}:for_share():single_object_mode():exec() - if issue.closed then - slot.put_into("error", _"This issue is already closed.") - return false - elseif issue.fully_frozen then - slot.put_into("error", _"Voting for this issue has already begun.") - return false - elseif issue.phase_finished then - slot.put_into("error", _"Current phase is already closed.") - return false - end - area = issue.area -else - local area_id = param.get("area_id", atom.integer) - area = Area:new_selector():add_where{"id=?",area_id}:single_object_mode():exec() - if not area.active then - slot.put_into("error", "Invalid area.") - return false - end -end - -if not app.session.member:has_voting_right_for_unit_id(area.unit_id) then - return execute.view { module = "index", view = "403" } -end - -local policy_id = param.get("policy_id", atom.integer) -local policy -if policy_id then - policy = Policy:by_id(policy_id) -end - -if not issue then - if policy_id == -1 then - slot.put_into("error", _"Please choose a policy") - return false - end - if not policy.active then - slot.put_into("error", "Invalid policy.") - return false - end - if policy.polling and not app.session.member:has_polling_right_for_unit_id(area.unit_id) then - return execute.view { module = "index", view = "403" } - end - if not area:get_reference_selector("allowed_policies") - :add_where{ "policy.id = ?", policy_id } - :optional_object_mode() - :exec() - then - slot.put_into("error", "policy not allowed") - return false - end -end - -local is_polling = (issue and param.get("polling", atom.boolean)) or (policy and policy.polling) or false - -local tmp = db:query({ "SELECT text_entries_left, initiatives_left FROM member_contingent_left WHERE member_id = ? AND polling = ?", app.session.member.id, is_polling }, "opt_object") -if not tmp or tmp.initiatives_left < 1 then - slot.put_into("error", _"Sorry, your contingent for creating initiatives has been used up. Please try again later.") - return false -end -if tmp and tmp.text_entries_left < 1 then - slot.put_into("error", _"Sorry, you have reached your personal flood limit. Please be slower...") - return false -end - -local name = param.get("name") - -local name = util.trim(name) - -if #name < 3 then - slot.put_into("error", _"Please enter a meaningful title for your initiative!") - return false -end - -if #name > 140 then - slot.put_into("error", _"This title is too long!") - return false -end - -local timing -if not issue and policy.free_timeable then - local free_timing_string = util.trim(param.get("free_timing")) - if not free_timing_string or #free_timing_string < 1 then - slot.put_into("error", _"Choose timing") - return false - end - local available_timings - if config.free_timing and config.free_timing.available_func then - available_timings = config.free_timing.available_func(policy) - if available_timings == false then - slot.put_into("error", "error in free timing config") - return false - end - end - if available_timings then - local timing_available = false - for i, available_timing in ipairs(available_timings) do - if available_timing.id == free_timing_string then - timing_available = true - end - end - if not timing_available then - slot.put_into("error", _"Invalid timing") - return false - end - end - timing = config.free_timing.calculate_func(policy, free_timing_string) - if not timing then - slot.put_into("error", "error in free timing config") - return false - end -end - -local draft_text = param.get("draft") - -if not draft_text then - return false -end - -local draft_text = util.wysihtml_preproc(draft_text) - -local valid_html, error_message = util.html_is_safe(draft_text) -if not valid_html then - slot.put_into("error", _("Draft contains invalid formatting or character sequence: #{error_message}", { error_message = error_message }) ) - return false -end - -if config.initiative_abstract then - local abstract = param.get("abstract") - if not abstract then - return false - end - abstract = encode.html(abstract) - draft_text = abstract .. "" .. draft_text -end - -local location = param.get("location") -if location == "" then - location = nil -end - -if param.get("preview") or param.get("edit") then - return -end - -local initiative = Initiative:new() - -if not issue then - issue = Issue:new() - issue.area_id = area.id - issue.policy_id = policy_id - - if policy.polling then - issue.accepted = 'now' - issue.state = 'discussion' - initiative.polling = true - - if policy.free_timeable then - issue.discussion_time = timing.discussion - issue.verification_time = timing.verification - issue.voting_time = timing.voting - end - - end - - issue:save() - - if config.etherpad then - local result = net.curl( - config.etherpad.api_base - .. "api/1/createGroupPad?apikey=" .. config.etherpad.api_key - .. "&groupID=" .. config.etherpad.group_id - .. "&padName=Issue" .. tostring(issue.id) - .. "&text=" .. request.get_absolute_baseurl() .. "issue/show/" .. tostring(issue.id) .. ".html" - ) - end -end - -if param.get("polling", atom.boolean) and app.session.member:has_polling_right_for_unit_id(area.unit_id) then - initiative.polling = true -end -initiative.issue_id = issue.id -initiative.name = name -initiative:save() - -local draft = Draft:new() -draft.initiative_id = initiative.id -draft.formatting_engine = formatting_engine -draft.content = draft_text -draft.location = location -draft.author_id = app.session.member.id -draft:save() - -local initiator = Initiator:new() -initiator.initiative_id = initiative.id -initiator.member_id = app.session.member.id -initiator.accepted = true -initiator:save() - -if not is_polling then - local supporter = Supporter:new() - supporter.initiative_id = initiative.id - supporter.member_id = app.session.member.id - supporter.draft_id = draft.id - supporter:save() -end - -slot.put_into("notice", _"Initiative successfully created") - -request.redirect{ - module = "initiative", - view = "show", - id = initiative.id -} diff -r 17e7082c377a -r ed3c40911ae1 app/main/initiative/new.lua --- a/app/main/initiative/new.lua Mon Feb 10 21:10:49 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,279 +0,0 @@ -local issue -local area - -local issue_id = param.get("issue_id", atom.integer) -if issue_id then - issue = Issue:new_selector():add_where{"id=?",issue_id}:single_object_mode():exec() - issue:load_everything_for_member_id(app.session.member_id) - area = issue.area - -else - local area_id = param.get("area_id", atom.integer) - area = Area:new_selector():add_where{"id=?",area_id}:single_object_mode():exec() - area:load_delegation_info_once_for_member_id(app.session.member_id) -end - -local polling = param.get("polling", atom.boolean) - -local policy_id = param.get("policy_id", atom.integer) -local policy - -local preview = param.get("preview") - -if #(slot.get_content("error")) > 0 then - preview = false -end - -if policy_id then - policy = Policy:by_id(policy_id) -end - -if issue_id then - execute.view { - module = "issue", view = "_head", - params = { issue = issue, member = app.session.member } - } -else - --[[ - execute.view { - module = "area", view = "_head", - params = { area = area, member = app.session.member } - } - --]] - --[[ - execute.view { - module = "initiative", view = "_sidebar_policies", - params = { - area = area, - } - } - --]] -end - -ui.form{ - module = "initiative", - action = "create", - params = { - area_id = area.id, - issue_id = issue and issue.id or nil - }, - attr = { class = "vertical" }, - content = function() - ui.grid{ content = function() - ui.cell_main{ content = function() - ui.container{ attr = { class = "mdl-card mdl-shadow--2dp mdl-card__fullwidth" }, content = function() - ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function() - if preview then - ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _"Preview" } - elseif issue_id then - ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _"New competing initiative" } - else - ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _"Create a new issue" } - end - end } - - ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function() - - - if preview then - - ui.section( function() - ui.sectionHead( function() - ui.heading{ level = 1, content = encode.html(param.get("name")) } - if not issue then - ui.container { content = policy.name } - end - if param.get("free_timing") then - ui.container { content = param.get("free_timing") } - end - slot.put("
") - - local draft_text = param.get("draft") - local draft_text = util.wysihtml_preproc(draft_text) - - ui.field.hidden{ name = "policy_id", value = param.get("policy_id") } - ui.field.hidden{ name = "name", value = param.get("name") } - if config.initiative_abstract then - ui.field.hidden{ name = "abstract", value = param.get("abstract") } - ui.container{ - attr = { class = "abstract" }, - content = param.get("abstract") - } - slot.put("
") - end - ui.field.hidden{ name = "draft", value = draft_text } - ui.field.hidden{ name = "free_timing", value = param.get("free_timing") } - ui.field.hidden{ name = "polling", value = param.get("polling", atom.boolean) } - ui.field.hidden{ name = "location", value = param.get("location") } - local formatting_engine - if config.enforce_formatting_engine then - formatting_engine = config.enforce_formatting_engine - else - formatting_engine = param.get("formatting_engine") - end - ui.container{ - attr = { class = "draft" }, - content = function() - slot.put(draft_text) - end - } - slot.put("
") - - ui.tag{ - tag = "input", - attr = { - type = "submit", - class = "mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--colored", - value = _'Publish now' - }, - content = "" - } - slot.put("   ") - ui.tag{ - tag = "input", - attr = { - type = "submit", - name = "edit", - class = "mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect", - value = _'Edit again' - }, - content = "" - } - slot.put("   ") - local class = "mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect" - if issue then - ui.link{ content = _"Cancel", module = "issue", view = "show", id = issue.id, attr = { class = class } } - else - ui.link{ content = _"Cancel", module = "index", view = "index", params = { unit = area.unit_id, area = area.id }, attr = { class = class } } - end - end ) - end ) - else - - ui.sectionRow( function() - --[[ - if not preview and not issue_id then - ui.container { attr = { class = "section" }, content = _"Before creating a new issue, please check any existant issues before, if the topic is already in discussion." } - slot.put("
") - end - --]] - if not issue_id then - local tmp = { { id = -1, name = "" } } - for i, allowed_policy in ipairs(area.allowed_policies) do - if not allowed_policy.polling or app.session.member:has_polling_right_for_unit_id(area.unit_id) then - tmp[#tmp+1] = allowed_policy - end - end - ui.container{ content = _"Please choose a policy for the new issue:" } - ui.field.select{ - name = "policy_id", - foreign_records = tmp, - foreign_id = "id", - foreign_name = "name", - value = param.get("policy_id", atom.integer) or area.default_policy and area.default_policy.id - } - if policy and policy.free_timeable then - local available_timings - if config.free_timing and config.free_timing.available_func then - available_timings = config.free_timing.available_func(policy) - if available_timings == false then - slot.put_into("error", "error in free timing config") - return false - end - end - ui.heading{ level = 4, content = _"Free timing:" } - if available_timings then - ui.field.select{ - name = "free_timing", - foreign_records = available_timings, - foreign_id = "id", - foreign_name = "name", - value = param.get("free_timing") - } - else - ui.field.text{ - name = "free_timing", - value = param.get("free_timing") - } - end - end - end - - if issue and issue.policy.polling and app.session.member:has_polling_right_for_unit_id(area.unit_id) then - slot.put("
") - ui.field.boolean{ name = "polling", label = _"No admission needed", value = polling } - end - - ui.container{ attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label mdl-card__fullwidth" }, content = function () - ui.field.text{ - attr = { id = "lf-initiative__name", class = "mdl-textfield__input" }, - label_attr = { class = "mdl-textfield__label", ["for"] = "lf-initiative__name" }, - label = _"Title", - name = "name", - value = param.get("name") - } - end } - - if config.initiative_abstract then - ui.container { content = _"Enter abstract:" } - ui.container{ attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--expandable mdl-textfield__fullwidth" }, content = function() - ui.field.text{ - name = "abstract", - multiline = true, - attr = { id = "abstract", style = "height: 20ex; width: 100%;" }, - value = param.get("abstract") - } - end } - end - - ui.container { content = _"Enter your proposal and/or reasons:" } - ui.container{ attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--expandable mdl-textfield__fullwidth" }, content = function() - ui.field.wysihtml{ - name = "draft", - multiline = true, - attr = { id = "draft", style = "height: 50ex; width: 100%;" }, - value = param.get("draft") or config.draft_template - } - end } - if not issue or issue.state == "admission" or issue.state == "discussion" then - ui.container { content = _"You can change your text again anytime during admission and discussion phase" } - else - ui.container { content = _"You cannot change your text again later, because this issue is already in verfication phase!" } - end - slot.put("
") - - slot.put("
") - ui.tag{ - tag = "input", - attr = { - type = "submit", - name = "preview", - class = "mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--colored", - value = _'Preview' - }, - content = "" - } - slot.put("   ") - - local class = "mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect" - if issue then - - ui.link{ content = _"Cancel", module = "issue", view = "show", id = issue.id, attr = { class = class } } - else - ui.link{ content = _"Cancel", module = "index", view = "index", params = { unit = area.unit_id, area = area.id }, attr = { class = class } } - end - end ) - end - end } - end } - end } - if config.map or config.firstlife then - ui.cell_sidebar{ content = function() - ui.container{ attr = { class = "mdl-special-card map mdl-shadow--2dp" }, content = function() - ui.field.location{ name = "location", value = param.get("location") } - end } - end } - end - end } - end -} diff -r 17e7082c377a -r ed3c40911ae1 app/main/initiative/show.lua --- a/app/main/initiative/show.lua Mon Feb 10 21:10:49 2020 +0100 +++ b/app/main/initiative/show.lua Tue Feb 11 15:24:36 2020 +0100 @@ -155,7 +155,9 @@ ui.link{ module = "file", view = "show.jpg", id = file.id, content = function() ui.image{ module = "file", view = "show.jpg", id = file.id, params = { preview = true } } end } - ui.container{ content = file.title or "" } + ui.container{ content = function() + ui.tag{ tag = "strong", content = file.title or "" } + end } ui.container{ content = file.description or "" } slot.put("

") end diff -r 17e7082c377a -r ed3c40911ae1 app/main/issue/_sidebar_issue.lua --- a/app/main/issue/_sidebar_issue.lua Mon Feb 10 21:10:49 2020 +0100 +++ b/app/main/issue/_sidebar_issue.lua Tue Feb 11 15:24:36 2020 +0100 @@ -36,7 +36,7 @@ ui.container{ attr = { class = "mdl-card__actions mdl-card--border" }, content = function() ui.link { attr = { class = "mdl-button mdl-js-button" }, - module = "initiative", view = "new", + module = "draft", view = "new", params = { issue_id = issue.id }, content = _"start a new competing initiative" }