diff --git a/Gemfile b/Gemfile index 05d1379c5..a5ec32008 100644 --- a/Gemfile +++ b/Gemfile @@ -66,6 +66,9 @@ gem 'turnout', '~> 2.4.0' gem 'redcarpet', '~> 3.4.0' gem 'paperclip' +gem 'rails-assets-markdown-it', source: 'https://rails-assets.org' + +gem 'cocoon' group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console diff --git a/Gemfile.lock b/Gemfile.lock index 0989276ef..b1540cfe1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,5 +1,6 @@ GEM remote: https://rubygems.org/ + remote: https://rails-assets.org/ specs: actionmailer (4.2.8) actionpack (= 4.2.8) @@ -101,6 +102,7 @@ GEM cliver (0.3.2) cocaine (0.5.8) climate_control (>= 0.0.3, < 1.0) + cocoon (1.2.9) coffee-rails (4.2.1) coffee-script (>= 2.2.0) railties (>= 4.0.0, < 5.2.x) @@ -318,6 +320,7 @@ GEM bundler (>= 1.3.0, < 2.0) railties (= 4.2.8) sprockets-rails + rails-assets-markdown-it (8.2.1) rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) rails-dom-testing (1.0.8) @@ -482,6 +485,7 @@ DEPENDENCIES capistrano3-delayed-job (~> 1.7.3) capybara (~> 2.14.0) ckeditor (~> 4.2.3) + cocoon coffee-rails (~> 4.2.1) coveralls (~> 0.8.21) daemons @@ -519,6 +523,7 @@ DEPENDENCIES poltergeist (~> 1.15.0) quiet_assets rails (= 4.2.8) + rails-assets-markdown-it! redcarpet (~> 3.4.0) responders (~> 2.4.0) rinku (~> 2.0.2) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 928f06edd..d736be793 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -50,6 +50,15 @@ //= require banners //= require social_share //= require checkbox_toggle +//= require markdown-it +//= require markdown_editor +//= require cocoon +//= require legislation_admin +//= require legislation +//= require legislation_allegations +//= require legislation_annotatable +//= require watch_form_changes +//= require tree_navigator //= require custom var initialize_modules = function() { @@ -76,6 +85,14 @@ var initialize_modules = function() { App.Banners.initialize(); App.SocialShare.initialize(); App.CheckboxToggle.initialize(); + App.MarkdownEditor.initialize(); + App.LegislationAdmin.initialize(); + App.LegislationAllegations.initialize(); + App.Legislation.initialize(); + if ( $(".legislation-annotatable").length ) + App.LegislationAnnotatable.initialize(); + App.WatchFormChanges.initialize(); + App.TreeNavigator.initialize(); }; $(function(){ diff --git a/app/assets/javascripts/legislation.js.coffee b/app/assets/javascripts/legislation.js.coffee new file mode 100644 index 000000000..3eb53e6d2 --- /dev/null +++ b/app/assets/javascripts/legislation.js.coffee @@ -0,0 +1,25 @@ +App.Legislation = + + initialize: -> + $('#js-toggle-debate').on + click: -> + $('#debate-show').toggle() + + $('#js-toggle-small-debate').on + click: -> + $('#debate-show').toggle() + $('span').toggleClass('icon-angle-up') + + $('form#new_legislation_answer input.button').hide() + $('form#new_legislation_answer input[type=radio]').on + click: -> + $('form#new_legislation_answer').submit() + + $('form#draft_version_go_to_version input.button').hide() + $('form#draft_version_go_to_version select').on + change: -> + $('form#draft_version_go_to_version').submit() + + $('#js-toggle-legislation-process-header').on + click: -> + $('[data-target="legislation-header-full"]').toggle() diff --git a/app/assets/javascripts/legislation_admin.js.coffee b/app/assets/javascripts/legislation_admin.js.coffee new file mode 100644 index 000000000..c61d9cea1 --- /dev/null +++ b/app/assets/javascripts/legislation_admin.js.coffee @@ -0,0 +1,14 @@ +App.LegislationAdmin = + + initialize: -> + $("input[type='checkbox'][data-disable-date]").on + change: -> + checkbox = $(this) + parent = $(this).parents('.row:eq(0)') + date_selector = $(this).data('disable-date') + parent.find("input[type='text'][id^='"+date_selector+"']").each -> + if checkbox.is(':checked') + $(this).removeAttr("disabled") + else + $(this).val("") + diff --git a/app/assets/javascripts/legislation_allegations.js.coffee b/app/assets/javascripts/legislation_allegations.js.coffee new file mode 100644 index 000000000..e5c8ea62f --- /dev/null +++ b/app/assets/javascripts/legislation_allegations.js.coffee @@ -0,0 +1,25 @@ +App.LegislationAllegations = + + toggle_comments: -> + if !App.LegislationAnnotatable.isMobile() + $('.draft-allegation').toggleClass('comments-on') + $('#comments-box').html('').hide() + + show_comments: -> + if !App.LegislationAnnotatable.isMobile() + $('.draft-allegation').addClass('comments-on') + + initialize: -> + $('.js-toggle-allegations .draft-panel').on + click: (e) -> + e.preventDefault() + e.stopPropagation() + if !App.LegislationAnnotatable.isMobile() + App.LegislationAllegations.toggle_comments() + + $('.js-toggle-allegations').on + click: (e) -> + # Toggle comments when the section title is visible + if !App.LegislationAnnotatable.isMobile() + if $(this).find('.draft-panel .panel-title:visible').length == 0 + App.LegislationAllegations.toggle_comments() diff --git a/app/assets/javascripts/legislation_annotatable.js.coffee b/app/assets/javascripts/legislation_annotatable.js.coffee new file mode 100644 index 000000000..6b7e2e603 --- /dev/null +++ b/app/assets/javascripts/legislation_annotatable.js.coffee @@ -0,0 +1,215 @@ +_t = (key) -> new Gettext().gettext(key) + +App.LegislationAnnotatable = + + makeEditableAndHighlight: (colour) -> + sel = window.getSelection() + if sel.rangeCount and sel.getRangeAt + range = sel.getRangeAt(0) + document.designMode = 'on' + if range + sel.removeAllRanges() + sel.addRange range + # Use HiliteColor since some browsers apply BackColor to the whole block + if !document.execCommand('HiliteColor', false, colour) + document.execCommand 'BackColor', false, colour + document.designMode = 'off' + return + + highlight: (colour) -> + if window.getSelection + # IE9 and non-IE + try + if !document.execCommand('BackColor', false, colour) + App.LegislationAnnotatable.makeEditableAndHighlight colour + catch ex + App.LegislationAnnotatable.makeEditableAndHighlight colour + else if document.selection and document.selection.createRange + # IE <= 8 case + range = document.selection.createRange() + range.execCommand 'BackColor', false, colour + return + + remove_highlight: -> + $('[data-legislation-draft-version-id] span[style]').replaceWith(-> + return $(this).contents() + ) + return + + renderAnnotationComments: (event) -> + if event.offset + $("#comments-box").css({top: event.offset - $('.calc-comments').offset().top}) + + if App.LegislationAnnotatable.isMobile() + return + + $.ajax + method: "GET" + url: event.annotation_url + "/annotations/" + event.annotation_id + "/comments" + dataType: 'script' + + onClick: (event) -> + event.preventDefault() + event.stopPropagation() + + if App.LegislationAnnotatable.isMobile() + annotation_url = $(event.target).closest(".legislation-annotatable").data("legislation-annotatable-base-url") + window.location.href = annotation_url + "/annotations/" + $(this).data('annotation-id') + return + + $('[data-annotation-id]').removeClass('current-annotation') + + target = $(this) + + parents = target.parents('.annotator-hl') + parents_ids = parents.map (_, elem) -> + $(elem).data("annotation-id") + + annotation_id = target.data('annotation-id') + $('[data-annotation-id="'+annotation_id+'"]').addClass('current-annotation') + + $('#comments-box').html('') + App.LegislationAllegations.show_comments() + $("#comments-box").show() + + $.event.trigger + type: "renderLegislationAnnotation" + annotation_id: target.data("annotation-id") + annotation_url: target.closest(".legislation-annotatable").data("legislation-annotatable-base-url") + offset: target.offset()["top"] + + parents_ids.each (i, pid) -> + $.event.trigger + type: "renderLegislationAnnotation" + annotation_id: pid + annotation_url: target.closest(".legislation-annotatable").data("legislation-annotatable-base-url") + + isMobile: () -> + return window.innerWidth <= 652 + + viewerExtension: (viewer) -> + viewer._onHighlightMouseover = (event) -> + return + + customShow: (position) -> + $(@element).html '' + # Clean comments section and open it + $('#comments-box').html '' + App.LegislationAllegations.show_comments() + $('#comments-box').show() + + annotation_url = $('[data-legislation-annotatable-base-url]').data('legislation-annotatable-base-url') + $.ajax( + method: 'GET' + url: annotation_url + '/annotations/new' + dataType: 'script').done (-> + $('#new_legislation_annotation #legislation_annotation_quote').val(@annotation.quote) + $('#new_legislation_annotation #legislation_annotation_ranges').val(JSON.stringify(@annotation.ranges)) + $('#comments-box').css({top: position.top - $('.calc-comments').offset().top}) + + unless $('[data-legislation-open-phase]').data('legislation-open-phase') == false + App.LegislationAnnotatable.highlight('#7fff9a') + $('#comments-box textarea').focus() + + $("#new_legislation_annotation").on("ajax:complete", (e, data, status, xhr) -> + App.LegislationAnnotatable.app.destroy() + if data.status == 200 + App.LegislationAnnotatable.remove_highlight() + $("#comments-box").html("").hide() + $.ajax + method: "GET" + url: annotation_url + "/annotations/" + data.responseJSON.id + "/comments" + dataType: 'script' + else + $(e.target).find('label').addClass('error') + $('' + data.responseJSON[0] + '').insertAfter($(e.target).find('textarea')) + return true + ) + return + ).bind(this) + + editorExtension: (editor) -> + editor.show = App.LegislationAnnotatable.customShow + + scrollToAnchor: -> + annotationsLoaded: (annotations) -> + anchor = $(location).attr('hash') + if anchor && anchor.startsWith('#annotation') + ann_id = anchor.split("-")[-1..] + + checkExist = setInterval((-> + if $("span[data-annotation-id='" + ann_id + "']").length + el = $("span[data-annotation-id='" + ann_id + "']") + el.addClass('current-annotation') + $('#comments-box').html('') + App.LegislationAllegations.show_comments() + $('html,body').animate({scrollTop: el.offset().top}) + $.event.trigger + type: "renderLegislationAnnotation" + annotation_id: ann_id + annotation_url: el.closest(".legislation-annotatable").data("legislation-annotatable-base-url") + offset: el.offset()["top"] + clearInterval checkExist + return + ), 100) + + propotionalWeight: (v, max) -> + Math.floor(v*5/(max+1)) + 1 + + addWeightClasses: -> + annotationsLoaded: (annotations) -> + return if annotations.length == 0 + weights = annotations.map (ann) -> ann.weight + max_weight = Math.max.apply(null, weights) + last_annotation = annotations[annotations.length - 1] + + checkExist = setInterval((-> + if $("span[data-annotation-id='" + last_annotation.id + "']").length + for annotation in annotations + ann_weight = App.LegislationAnnotatable.propotionalWeight(annotation.weight, max_weight) + el = $("span[data-annotation-id='" + annotation.id + "']") + el.addClass('weight-' + ann_weight) + clearInterval checkExist + return + ), 100) + + initialize: -> + $(document).off("renderLegislationAnnotation").on("renderLegislationAnnotation", App.LegislationAnnotatable.renderAnnotationComments) + $(document).off('click', '[data-annotation-id]').on('click', '[data-annotation-id]', App.LegislationAnnotatable.onClick) + $(document).off('click', '[data-cancel-annotation]').on('click', '[data-cancel-annotation]', (e) -> + e.preventDefault() + $('#comments-box').html('') + $('#comments-box').hide() + App.LegislationAnnotatable.remove_highlight() + return + ) + + current_user_id = $('html').data('current-user-id') + + $(".legislation-annotatable").each -> + $this = $(this) + ann_type = "legislation_draft_version" + ann_id = $this.data("legislation-draft-version-id") + base_url = $this.data("legislation-annotatable-base-url") + + App.LegislationAnnotatable.app = new annotator.App() + .include -> + beforeAnnotationCreated: (ann) -> + ann["legislation_draft_version_id"] = ann_id + ann.permissions = ann.permissions || {} + ann.permissions.admin = [] + .include(annotator.ui.main, { + element: this, + viewerExtensions: [App.LegislationAnnotatable.viewerExtension], + editorExtensions: [App.LegislationAnnotatable.editorExtension] + }) + .include(App.LegislationAnnotatable.scrollToAnchor) + .include(App.LegislationAnnotatable.addWeightClasses) + .include(annotator.storage.http, { prefix: base_url, urls: { search: "/annotations/search" } }) + + App.LegislationAnnotatable.app.start().then -> + App.LegislationAnnotatable.app.ident.identity = current_user_id + + options = {} + options["legislation_draft_version_id"] = ann_id + App.LegislationAnnotatable.app.annotations.load(options) diff --git a/app/assets/javascripts/markdown_editor.js.coffee b/app/assets/javascripts/markdown_editor.js.coffee new file mode 100644 index 000000000..510bf83ec --- /dev/null +++ b/app/assets/javascripts/markdown_editor.js.coffee @@ -0,0 +1,39 @@ +App.MarkdownEditor = + + refresh_preview: (element, md) -> + textarea_content = element.find('textarea').val() + result = md.render(textarea_content) + element.find('#markdown-preview').html(result) + + initialize: -> + $('.markdown-editor').each -> + md = window.markdownit({ + html: true, + breaks: true, + typographer: true, + }) + + App.MarkdownEditor.refresh_preview($(this), md) + + $(this).on 'change input paste keyup', -> + App.MarkdownEditor.refresh_preview($(this), md) + $('.legislation-draft-versions-edit .warning').show() + return + + $(this).find('textarea').on 'scroll', -> + $('#markdown-preview').scrollTop($(this).scrollTop()) + + $(this).find('.fullscreen-toggle').on 'click', -> + $('.markdown-editor').toggleClass('fullscreen') + $('.fullscreen-container').toggleClass('medium-8', 'medium-12') + span = $(this).find('span') + current_html = span.html() + if(current_html == span.data('open-text')) + span.html(span.data('closed-text')) + else + span.html(span.data('open-text')) + + if $('.markdown-editor').hasClass('fullscreen') + $('.markdown-editor textarea').height($(window).height() - 100) + else + $('.markdown-editor textarea').height("10em") diff --git a/app/assets/javascripts/tree_navigator.js.coffee b/app/assets/javascripts/tree_navigator.js.coffee new file mode 100644 index 000000000..1fdc50ba2 --- /dev/null +++ b/app/assets/javascripts/tree_navigator.js.coffee @@ -0,0 +1,37 @@ +App.TreeNavigator = + setNodes: (nodes) -> + children = nodes.children('ul') + + if(children.length == 0) + return + + children.each -> + link = $(this).prev('a') + $('').insertBefore(link) + App.TreeNavigator.setNodes($(this).children()) + + initialize: -> + elem = $('[data-tree-navigator]') + if(elem.length == 0) + return + + ul = elem.find('ul:eq(0)') + if(ul.length && ul.children().length) + App.TreeNavigator.setNodes(ul.children()) + + $('[data-tree-navigator] span').on + click: (e) -> + elem = $(this) + if(elem.hasClass('open')) + elem.removeClass('open').addClass('closed') + elem.siblings('ul').hide() + else if(elem.hasClass('closed')) + elem.removeClass('closed').addClass('open') + elem.siblings('ul').show() + + if anchor = $(location).attr('hash') + if link = elem.find('a[href="'+anchor+'"]') + link.parents('ul').each -> + $(this).show() + $(this).siblings('span').removeClass('closed').addClass('open') + diff --git a/app/assets/javascripts/votes.js.coffee b/app/assets/javascripts/votes.js.coffee index b842c3ffc..0f6dfcfc5 100644 --- a/app/assets/javascripts/votes.js.coffee +++ b/app/assets/javascripts/votes.js.coffee @@ -13,4 +13,6 @@ App.Votes = initialize: -> App.Votes.hoverize "div.votes" App.Votes.hoverize "div.supports" + App.Votes.hoverize "div.debate-questions" + App.Votes.hoverize "div.comment-footer" false diff --git a/app/assets/javascripts/watch_form_changes.js.coffee b/app/assets/javascripts/watch_form_changes.js.coffee new file mode 100644 index 000000000..eaf125ded --- /dev/null +++ b/app/assets/javascripts/watch_form_changes.js.coffee @@ -0,0 +1,30 @@ +App.WatchFormChanges = + forms: -> + return $('form[data-watch-changes]') + + msg: -> + if($('[data-watch-form-message]').length) + return $('[data-watch-form-message]').data('watch-form-message') + + checkChanges: (event) -> + changes = false + App.WatchFormChanges.forms().each -> + form = $(this) + if form.serialize() != form.data('watchChanges') + changes = true + if changes + return confirm(App.WatchFormChanges.msg()) + else + return true + + initialize: -> + if App.WatchFormChanges.forms().length == 0 || App.WatchFormChanges.msg() == undefined + return + + $(document).off('page:before-change').on('page:before-change', (e) -> App.WatchFormChanges.checkChanges(e)) + + App.WatchFormChanges.forms().each -> + form = $(this) + form.data('watchChanges', form.serialize()) + + false diff --git a/app/assets/stylesheets/_settings.scss b/app/assets/stylesheets/_settings.scss index b804d62ba..6ae95791b 100644 --- a/app/assets/stylesheets/_settings.scss +++ b/app/assets/stylesheets/_settings.scss @@ -164,6 +164,7 @@ $header-font-family: $body-font-family; $header-font-weight: $global-weight-normal; $header-font-style: normal; $font-family-monospace: Consolas, 'Liberation Mono', Courier, monospace; +$font-family-serif: Georgia, 'Times New Roman', Times, serif; $header-sizes: ( small: ( 'h1': 34, diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss index fd6ed0815..13f946cde 100644 --- a/app/assets/stylesheets/admin.scss +++ b/app/assets/stylesheets/admin.scss @@ -6,6 +6,8 @@ // 04. Stats // 05. Management // 06. Polls +// 07. Legislation +// 08. CMS // // 01. Global styles @@ -538,7 +540,75 @@ table { } } -// 07. CMS +// 07. Legislation +// -------------- + +// Markdown Editor +// --------------- + +.markdown-editor { + background-color: white; + + .markdown-area, + #markdown-preview { + display: none; + } +} + +.markdown-editor #markdown-preview { + overflow-y: auto; + height: 15em; +} + +.markdown-editor textarea { + height: 15em; +} + +.markdown-editor.fullscreen { + z-index: 9999; + width: 100%; + height: 100%; + position: fixed; + top: 0; + left: 0; +} + +.markdown-editor.fullscreen #markdown-preview { + height: 99%; +} + +.edit_legislation_draft_version .row { + margin-bottom: 2rem; +} + +.legislation-admin { + .menu .active > a { + background: none; + } +} + +.legislation-process-save { + @include breakpoint(medium) { + float: right; + } +} + +.legislation-question-delete { + @include breakpoint(medium) { + text-align: right; + } +} + +.legislation-process-index { + + .legislation-process-new { + @include breakpoint(medium) { + text-align: right; + } + } +} + +// 08. CMS // -------------- .cms_page_list { @@ -547,3 +617,289 @@ table { vertical-align: middle; } } + +.legislation-process-edit { + + .edit_legislation_process { + + small { + color: $text-medium; + } + + input[type]:not([type="submit"]):not([type="file"]):not([type="checkbox"]):not([type="radio"]) { + background: $white; + } + + .legislation-process-start, .legislation-process-end { + @include breakpoint(medium) { + line-height: 3rem; + } + } + + .legislation-process-end { + @include breakpoint(medium) { + text-align: right; + } + } + } +} + +.legislation-draft-versions-index { + + .legislation-process-question { + @include breakpoint(medium) { + text-align: right; + } + } + + table tr td { + padding: 0.25rem 0.375rem; + } +} + +.legislation-questions-form { + + input[type]:not([type="submit"]):not([type="file"]):not([type="checkbox"]):not([type="radio"]) { + background: $white; + margin-bottom: 0; + + @include breakpoint(medium) { + margin-bottom: 1rem; + } + } + + input::-webkit-input-placeholder { + font-style: italic; + } + + input:-moz-placeholder { /* Firefox 18- */ + font-style: italic; + } + + input::-moz-placeholder { /* Firefox 19+ */ + font-style: italic; + } + + input:-ms-input-placeholder { + font-style: italic; + } + + .legislation-questions-answers { + margin-bottom: 1rem; + } + + .field { + margin-bottom: 1rem; + + @include breakpoint(medium) { + margin-bottom: 0; + } + + a { + line-height: 3rem; + color: $delete; + + span { + text-decoration: underline; + } + + .icon-x { + vertical-align: sub; + text-decoration: none; + line-height: 0; + } + + &:active, + &:focus, + &:hover { + text-decoration: none; + } + } + } +} + +.legislation-draft-versions-form { + + .legislation-process-version { + @include breakpoint(medium) { + text-align: right; + } + } + + input[type]:not([type="submit"]):not([type="file"]):not([type="checkbox"]):not([type="radio"]) { + background: $white; + } + + .control { + cursor: pointer; + margin-bottom: 1rem; + + small { + display: block; + margin-top: -1rem; + color: $text-medium; + + @include breakpoint(medium) { + margin-left: 0.25rem; + display: inline-block; + margin-top: 0; + } + } + } + + .fullscreen-container { + text-align: center; + background: #ccdbe6; + + .markdown-editor-header, + .markdown-editor-buttons { + display: none; + } + + a { + line-height: 8rem; + + span { + text-decoration: none; + font-size: $small-font-size; + } + + .icon-expand { + margin-left: 0.25rem; + vertical-align: sub; + text-decoration: none; + line-height: 0; + } + + &:active, + &:focus, + &:hover { + text-decoration: none; + } + } + } + + #legislation_draft_version_body { + font-family: $font-family-serif; + background: #f5f5f5; + height: 16em; + + &:focus { + border: 1px solid #cacaca; + box-shadow: inset 0 1px 2px rgba(34, 34, 34, 0.1); + } + } + + #markdown-preview { + font-family: $font-family-serif; + border: 1px solid #cacaca; + margin-bottom: 2rem; + + h1, h2, h3, h4, h5, h6 { + font-family: $font-family-serif !important; + font-size: 1rem; + line-height: 1.625rem; + margin-bottom: 0; + } + + p { + font-size: 1rem; + line-height: 1.625rem; + } + } + + .fullscreen { + + .markdown-area, + #markdown-preview { + display: block; + } + + .column { + padding: 0; + } + + .fullscreen-container { + text-align: left; + background: $admin-color; + padding: 0.5rem 1rem; + margin-bottom: 0; + + a { + line-height: 3rem; + + @include breakpoint(medium) { + float: right; + } + } + + .markdown-editor-header { + vertical-align: top; + display: inline-block; + color: $white; + + @include breakpoint(medium) { + line-height: 3rem; + } + } + + .truncate { + @include breakpoint(medium) { + width: 40vw; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + .markdown-editor-buttons { + display: block; + + @include breakpoint(medium) { + display: inline-block; + float: right; + padding-left: 1rem; + } + + input { + font-size: $small-font-size; + padding: 0.5em 1em; + margin-left: 0; + margin-bottom: 0; + margin-top: 1rem; + + @include breakpoint(medium) { + margin: 0.5rem; + } + } + } + + a { + color: $white; + } + } + + #legislation_draft_version_body { + border-radius: 0; + padding: 1rem; + border: none; + + @include breakpoint(medium) { + padding: 1rem 2rem; + } + + &:focus { + border: none; + } + } + + #markdown-preview { + padding: 1rem; + border: none; + + @include breakpoint(medium) { + padding: 1rem 2rem; + } + } + } +} diff --git a/app/assets/stylesheets/annotator_overrides.scss b/app/assets/stylesheets/annotator_overrides.scss index 962cdefd7..5132d68eb 100644 --- a/app/assets/stylesheets/annotator_overrides.scss +++ b/app/assets/stylesheets/annotator_overrides.scss @@ -97,3 +97,28 @@ } } } + +.annotator-hl { + cursor: pointer; + background: rgba(255, 255, 10, 0.2); +} + +.annotator-hl.weight-1 { + background: #FFF9DA; +} +.annotator-hl.weight-2 { + background: #FFF5BC; +} +.annotator-hl.weight-3 { + background: #FFF1A2; +} +.annotator-hl.weight-4 { + background: #FFEF8C; +} +.annotator-hl.weight-5 { + background: #FFE95F; +} + +.current-annotation { + text-decoration: underline; +} diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 0720c103f..c67d85471 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -7,6 +7,8 @@ @import "layout"; @import "participation"; @import "pages"; +@import "legislation"; +@import "legislation_process"; @import "custom"; @import "c3"; @import "annotator.min"; diff --git a/app/assets/stylesheets/icons.scss b/app/assets/stylesheets/icons.scss index cde59e594..5805fb090 100644 --- a/app/assets/stylesheets/icons.scss +++ b/app/assets/stylesheets/icons.scss @@ -1,5 +1,4 @@ @charset "UTF-8"; - @font-face { font-family: 'icons'; src: font-url('icons.eot'); @@ -10,7 +9,6 @@ font-weight: normal; font-style: normal; } - [data-icon]:before { font-family: "icons" !important; content: attr(data-icon); @@ -23,7 +21,6 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } - [class^="icon-"]:before, [class*=" icon-"]:before { font-family: "icons" !important; @@ -36,7 +33,6 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } - .icon-angle-down:before { content: "\61"; } @@ -184,12 +180,18 @@ .icon-arrow-top:before { content: "\57"; } -.icon-help-1:before { - content: "\58"; -} .icon-checkmark-circle:before { content: "\59"; } +.icon-minus-square:before { + content: "\58"; +} +.icon-plus-square:before { + content: "\5a"; +} +.icon-expand:before { + content: "\30"; +} .icon-telegram:before { content: "\31"; } diff --git a/app/assets/stylesheets/legislation.scss b/app/assets/stylesheets/legislation.scss new file mode 100644 index 000000000..950f9ce23 --- /dev/null +++ b/app/assets/stylesheets/legislation.scss @@ -0,0 +1,131 @@ +// Table of Contents +// +// 01. Hero +// 02. Sidebar menu +// 03. Legislation cards +// + +// 01. Hero +// ----------------- +.brand-heading { + background: $brand; + margin-bottom: 5rem; + + .column { + padding-top: 10rem; + padding-bottom: 10rem; + + h4 { + font-weight: 400; + text-align: center; + color: white; + } + } +} + +// 02. Sidebar menu +// ----------------- +.legislation-categories { + + .menu.simple { + border-bottom: none; + list-style: none; + padding-left: 0; + margin-left: 0; + margin-top: 0; + + @include breakpoint(medium) { + margin: 1.5rem 0; + } + + li { + display: block; + cursor: pointer; + margin-bottom: 1rem; + + @include breakpoint(medium) { + margin-bottom: 2rem; + max-width: 80%; + } + } + + li.active { + font-weight: 700; + } + } +} + +// 03. Legislation cards +// ----------------- +.legislation { + margin: 0 0 5rem 0; + background: white; + border: 1px solid; + border-color: #e5e6e9 #dfe0e4 #d0d1d5; + border-radius: 0; + box-shadow: 0px 1px 3px 0 #DEE0E3; + min-height: 12rem; + padding: 2rem 0 0 0; +} + +.button-legislation { + background: white; + border: 1px solid #2C9BE5; + color: #2C9BE5; + display: inline-block; + font-weight: 700; + margin-top: rem-calc(12); + + .icon-comments { + margin-right: 0.5rem; + color: $text-medium; + transition: color 0.25s ease-out, color 0.25s ease-out; + } + + &:hover, &:active { + border: 1px solid lighten(#2C9BE5, 25%); + cursor: pointer; + } + + &:hover .icon-comments, &:active .icon-comments { + color: white; + } +} + +.legislation-text { + margin-bottom: 1rem; + + h3 a { + color: $black; + } +} + +.legislation-calendar-info p { + font-size: $small-font-size; + color: $text-medium; + margin-bottom: 0; +} + +.legislation-calendar { + background: #E5ECF2; + padding-top: 1rem; + + h5 { + margin-left: 0.25rem; + margin-bottom: 0; + color: #61686E; + + @include breakpoint(medium) { + margin-left: 0; + } + } + + p { + margin-left: 0.25rem; + font-size: $small-font-size; + + @include breakpoint(medium) { + margin-left: 0; + } + } +} diff --git a/app/assets/stylesheets/legislation_process.scss b/app/assets/stylesheets/legislation_process.scss new file mode 100644 index 000000000..fb2bdecad --- /dev/null +++ b/app/assets/stylesheets/legislation_process.scss @@ -0,0 +1,1176 @@ +// Table of Contents +// +// 01. Utils +// 02. Hero +// 03. Legislation process navigation +// 04. Debate list +// 05. Debate quiz +// 06. Legislation draft +// 07. Legislation allegations +// 08. Legislation changes +// 09. Legislation comments +// 10. Legislation draft comment +// + +// 01. Utils +// ----------------- +.grey { + color: #878787; +} + +.grey-heading { + background: #E6E6E6; +} + +.center { + text-align: center; +} + +.right { + text-align: right; +} + +.strong { + font-weight: 700; +} + +$epigraph-font-size: rem-calc(15); +$epigraph-line-height: rem-calc(22); + +// 02. Hero +// ----------------- +.legislation-hero { + padding-top: 1.5rem; + + @include breakpoint(medium) { + padding-top: 3.5rem; + } + + h4 { + color: #878787; + text-transform: uppercase; + font-weight: 400; + } + + ul { + list-style: none; + margin-left: 0; + + li:before { + vertical-align: text-bottom; + padding-right: 0.5rem; + content: "■"; + color: #8AA8BE; + } + } + + #debate-show { + display: none; + } + + .debate-add-info { + margin-top: 3rem; + padding-top: 4rem; + border-top: 1px solid darken($border, 10%); + + @include breakpoint(medium) { + margin-bottom: 2rem; + } + + .debate-info-wrapper { + + h2 { + font-size: $lead-font-size; + + @include breakpoint(medium) { + float: left; + } + } + + p { + font-size: $epigraph-font-size; + line-height: $epigraph-line-height; + + @include breakpoint(medium) { + margin-left: 25%; + } + } + } + } + + .half-gradient { + /* Permalink - use to edit and share this gradient: http://colorzilla.com/gradient-editor/#e6e6e6+0,e6e6e6+50,ffffff+50 */ + background: #e6e6e6; /* Old browsers */ + background: -moz-linear-gradient(top, #e6e6e6 0%, #e6e6e6 50%, #ffffff 50%); /* FF3.6-15 */ + background: -webkit-linear-gradient(top, #e6e6e6 0%,#e6e6e6 50%,#ffffff 50%); /* Chrome10-25,Safari5.1-6 */ + background: linear-gradient(to bottom, #e6e6e6 0%,#e6e6e6 50%,#ffffff 50%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#e6e6e6', endColorstr='#ffffff',GradientType=0 ); /* IE6-9 */ + } + + .center .button { + background: $brand; + margin-bottom: 0; + } + + .headline { + margin-bottom: 1rem; + + @include breakpoint(medium) { + margin-bottom: 4rem; + + } + } + + .description { + margin-bottom: 1rem; + + p { + font-size: $epigraph-font-size; + line-height: $epigraph-line-height; + } + + ul { + font-size: $epigraph-font-size; + line-height: $epigraph-line-height; + + li { + margin-bottom: 1rem; + + p { + display: inline; + margin-bottom: 0; + } + } + } + + h4 { + font-size: $base-font-size; + } + } + + .button-subscribe { + margin-top: 1rem; + + h3 { + margin-bottom: 0; + } + + p { + margin-bottom: 0; + font-size: $small-font-size; + } + + &:hover h3 { + color: white; + } + + @include breakpoint(medium) { + padding: 0.5em 1em; + margin-top: 3rem; + } + } +} + +// 03. Legislation process navigation +// ----------------- +.legislation-process-categories { + position: relative; + + .legislation-process-list { + border-bottom: 1px solid $medium-gray; + margin: 0 1rem 1rem 1rem; + + @include breakpoint(medium) { + margin-left: 0; + } + + ul { + position: relative; + max-width: 75rem; + margin-left: auto; + margin-right: auto; + list-style: none; + padding-top: 4rem; + padding-left: 0; + margin-bottom: 0; + + @include breakpoint(medium) { + padding-left: 1rem; + } + + svg { + position: absolute; + top: 1.25rem; + + @include breakpoint(1280px) { + transform: rotate(-6deg); + left: -1rem; + } + } + + li { + cursor: pointer; + display: inline-block; + margin: 0 1rem 1rem 0; + transition: all 0.4s; + border-bottom: 2px solid transparent; + + &:first-of-type { + margin-left: 0; + } + + &:hover, + &:active, + &:focus { + border-bottom: 2px solid $brand; + } + + @media (min-width: 950px) { + margin: 0 0 0 3rem; + } + + a, + h4 { + display: block; + color: #6D6D6D; + margin-bottom: 0; + } + + a { + &:hover, &:active { + text-decoration: none; + } + + p { + margin-bottom: 0; + + @include breakpoint(medium) { + margin-bottom: 1rem; + } + } + } + } + + .active { + border-bottom: 2px solid $brand; + } + } + } +} + +// 04. Debate list +// ----------------- +.debate-chooser { + padding: 2rem 1rem; + + @include breakpoint(medium) { + .debate-chooser { + padding: 2rem 3rem; + } + } + + .debate-block { + margin-bottom: 2.5rem; + + .debate-type { + text-transform: uppercase; + color: #178DCC; + font-weight: 700; + font-size: $small-font-size; + + .icon-debates { + margin-left: 0.2rem; + } + } + + .debate-title a { + color: $brand; + } + + .debate-meta, + .debate-meta a { + font-size: $small-font-size; + color: #6D6D6D; + + .icon-comments { + margin-right: 0.2rem; + } + } + } + + .debate-info { + padding: 1rem; + background: #F4F4F4; + } +} + +// 05. Debate quiz +// ----------------- +.debate-questions { + .comments { + margin-top: 4rem; + } + + .quiz-header { + margin-bottom: 2rem; + + .quiz-title, .quiz-next { + padding: 1rem; + height: 6rem; + } + + .quiz-title { + background: #E5ECF2; + + .quiz-header-title { + margin-bottom: 0; + text-transform: uppercase; + color: #979B9F; + font-weight: 700; + font-size: $small-font-size; + } + } + + h4 a { + color: $brand; + } + + h4 a:hover { + text-decoration: none; + } + + .quiz-next-link { + display: block; + + &:hover, &:active { + text-decoration: none; + } + + .quiz-next { + background: #CCDBE5; + font-weight: 700; + color: $brand; + font-size: $small-font-size; + text-align: right; + text-transform: uppercase; + transition: background 0.25s ease-out, background 0.25s ease-out; + + .icon-angle-right { + vertical-align: sub; + } + + &:hover, &:active { + text-decoration: none; + background: $brand; + color: white; + + .icon-angle-right { + color: white; + } + } + } + } + } + + .quiz-question { + margin-bottom: 2rem; + } + + .debate-questions { + position: relative; + list-style: none; + + .participation-not-allowed { + padding-bottom: 3rem; + } + + .control { + position: relative; + display: inline-block; + color: #555; + cursor: pointer; + background: #fff; + border: 1px solid $border; + border-radius: 4px; + padding: 0.75rem 2.5rem; + margin-right: 1.5rem; + margin-bottom: 0.5rem; + } + + .active { + background: #CCDBE6; + border: none; + } + + .control input { + position: absolute; + opacity: 0; + z-index: -1; + } + + .control input:checked ~ .control-indicator { + background-color: $brand; + border: none; + } + + .radio .control-indicator { + border-radius: 50%; + } + + .control-indicator { + position: absolute; + top: 0.95rem; + left: 0.95rem; + display: block; + width: 1rem; + height: 1rem; + line-height: 1rem; + font-size: 65%; + text-align: center; + border: 2px solid #9C9C9C; + pointer-events: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + } +} + +// 06. Legislation draft +// ----------------- +.debate-draft { + padding: 10rem 2rem 15rem 2rem; + display: block; + background: #F2F2F2; + + button { + height: 90px; + + h3 { + margin-bottom: 0; + } + + p { + margin-bottom: 0; + font-size: $small-font-size; + } + } +} + +// 07. Legislation allegations +// ----------------- +.legislation-allegation { + padding-top: 1rem; + + #debate-show { + margin-top: 2rem; + } + + .headline { + margin-bottom: 0; + + .headline-small { + padding-top: 0.75rem; + } + } + + .button-circle { + line-height: 0; + padding: 0em; + width: 30px; + height: 30px; + border-radius: 50%; + + span { + padding-left: 1px; + + &:before { + line-height: 1.55; + } + } + } + + .icon-checkmark-circle { + font-size: 1.5rem; + vertical-align: bottom; + color: $text-medium; + margin-right: 0.5rem; + } + + .button-subscribed { + margin-top: 1rem; + border: 1px solid #D1D1D1; + background: #F2F2F2; + + h3 { + display: inline-block; + color: $text; + margin-bottom: 0; + } + + p { + margin-bottom: 0; + font-size: $small-font-size; + } + + &:hover h3 { + color: $text; + } + + @include breakpoint(medium) { + padding: 0.5em 1em; + } + } +} + +.draft-panels { + position: relative; + padding: 2rem 0; + + .draft-chooser { + margin-bottom: 2rem; + + h3 { + vertical-align: top; + display: inline-block; + font-weight: 400; + margin-right: 0.5rem; + } + + span { + margin-left: 0.25rem; + font-style: italic; + font-size: $small-font-size; + color: $text-medium; + vertical-align: top; + line-height: 2.35rem; + } + + .select-box { + position: relative; + + @include breakpoint(medium) { + display: inline-block; + } + + select { + margin-bottom: 0; + display: block; + } + + span { + vertical-align: inherit; + font-style: normal; + + a { + text-decoration: underline; + color: $text-medium + } + } + } + + .button { + margin-top: 1rem; + + @include breakpoint(medium) { + margin-top: 0; + } + } + } + + .draft-allegation { + @include breakpoint(medium) { + display: flex; + padding-left: 0.9375rem; + padding-right: 0.9375rem; + } + + .calc-index { + .draft-panel { + cursor: pointer; + } + + .draft-index-rotated { + display: none; + } + } + + // Panel calcs for desktop + @media screen and (min-width: 40em) { + .calc-index { + width: calc(35% - 25px); + } + + .calc-text { + width: calc(65% - 25px) + } + + .calc-comments { + cursor: pointer; + background: #F2F2F2; + width: 50px; + + .draft-panel { + .panel-title { + display: none; + } + } + } + } + + .border-right { + @include breakpoint(medium) { + border-right: 1px solid $border; + } + } + + .border-left { + @include breakpoint(medium) { + border-left: 1px solid $border; + } + } + + .draft-panel { + text-transform: uppercase; + font-weight: 700; + padding: 0.5rem 1rem; + color: #696969; + background: #F2F2F2; + font-size: $small-font-size; + + .icon-comments { + margin-right: 0.25rem; + } + + .icon-banner { + line-height: 0; + color: $text-medium; + vertical-align: sub; + margin-right: 0.25rem; + } + } + + .draft-index { + ul:first-child { + font-size: 1rem; + text-decoration: underline; + margin-left: 2.25rem; + margin-top: 1.5rem; + } + + ul { + list-style: none; + + li { + margin-bottom: 1rem; + } + .open::before { + cursor: pointer; + position: absolute; + margin-left: -1.25rem; + font-family: "icons"; + content: "\58"; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + .closed::before { + cursor: pointer; + position: absolute; + margin-left: -1.25rem; + font-family: "icons"; + content: "\5a"; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + } + } + + .draft-text { + position: relative; + padding: 1rem; + + @include breakpoint(medium) { + padding: 2rem 2rem 2rem 3rem; + }; + + h2 { + font-weight: 400; + margin-bottom: 2rem; + margin-top: 2rem; + } + + h3 { + font-weight: 400; + margin-bottom: 1rem; + } + + .anchor::before { + display: none; + content: "#"; + color: $text-medium; + position: absolute; + left: 0; + + @include breakpoint(medium) { + left: 1.5rem; + } + } + + a { + color: $text; + + &:hover, + &:active, + &:focus { + text-decoration: none; + + h3 { + color: $text; + } + + .anchor::before { + display: block; + } + } + } + } + + .calc-comments { + position: relative; + + .comment-box { + display: none; + } + + .draft-comments-rotated { + @include breakpoint(medium) { + font-size: $small-font-size; + text-transform: uppercase; + font-weight: 700; + color: #696969; + margin-top: 4rem; + -webkit-transform: rotate(-90deg); + -moz-transform: rotate(-90deg); + -ms-transform: rotate(-90deg); + -o-transform: rotate(-90deg); + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); + } + } + + } + } + + .comments-on { + .calc-index { + width: 50px; + background: #F2F2F2; + cursor: pointer; + + .panel-title { + display: none; + } + + .draft-index { + display: none; + } + + .draft-index-rotated { + @include breakpoint(medium) { + display: block; + font-size: $small-font-size; + text-transform: uppercase; + font-weight: 700; + color: #696969; + margin-top: 1rem; + -webkit-transform: rotate(-90deg); + -moz-transform: rotate(-90deg); + -ms-transform: rotate(-90deg); + -o-transform: rotate(-90deg); + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); + + .panel-title { + display: block; + } + } + } + } + + .calc-text { + width: calc(65% - 25px); + border-right: none; + + .show-comments { + width: 105%; + background: #FAFAFA; + padding: 0.25rem 2.5rem 0.25rem 0.25rem; + border: 1px solid $border; + margin-bottom: 1rem; + + p { + margin-bottom: 0; + } + } + + } + + .calc-comments { + background: white; + cursor: auto; + width: calc(35% - 25px); + + .draft-panel { + cursor: pointer; + } + + .draft-comments-rotated { + display: none; + } + + #comments-box { + position: absolute; + top: 230px; + } + + .comment-box { + width: 375px; + padding: 1rem; + background: #F9F9F9; + border: 1px solid $border; + display: block; + margin-bottom: 2rem; + + .button { + font-size: $small-font-size; + padding: 0.5em 1em; + } + + .publish-comment { + float: right; + } + + .comment-header { + color: #838383; + padding-bottom: 0.5rem; + margin-bottom: 1rem; + border-bottom: 1px solid $border; + + .comment-number { + color: $text; + display: inline-block; + } + + .icon-comment { + margin-right: 0.5rem; + } + + a .icon-expand { + color: #838383; + font-size: $small-font-size; + float: right; + } + } + + .comments-wrapper { + position: relative; + + .participation-not-allowed { + padding: 1.25rem 0.5rem; + } + } + + .comment-footer { + position: relative; + + .participation-not-allowed { + font-size: 0.875rem; + height: 50px; + padding: .85rem 0.75rem; + top: -18px; + } + } + + .comment-input { + padding-bottom: 4rem; + margin-bottom: 1rem; + margin-top: 1rem; + border-bottom: 1px solid $border; + + .comment-advice { + border-top: 1px solid #D0D0D0; + border-right: 1px solid #D0D0D0; + border-left: 1px solid #D0D0D0; + width: 100%; + padding: 0.5rem; + display: inline-block; + font-size: $small-font-size; + background: #DFDFDF; + + .icon-proposals { + color: #838383; + } + + a { + color: #87A3B9; + text-decoration: underline; + } + } + + textarea { + border-radius: 0; + box-shadow: none; + border-bottom: 1px solid #D0D0D0; + border-right: 1px solid #D0D0D0; + border-left: 1px solid #D0D0D0; + width: 100%; + height: 200px; + margin-bottom: 0.5rem; + } + + .comment-actions { + .cancel-comment { + color: #87A3B9; + text-decoration: underline; + font-size: $small-font-size; + display: inline-block; + } + } + } + + .comment { + border-bottom: 1px solid $border; + padding-bottom: 0.75rem; + margin-bottom: 1rem; + font-size: $small-font-size; + + .comment-text { + margin-bottom: 0.5rem; + + p { + line-height: 1.5; + font-size: $small-font-size; + + &:last-child { + margin-bottom: 0.5rem; + } + } + } + + .comment-meta { + + .comment-more-info { + display: inline-block; + + .comment-expand { + display: inline-block; + + &::after { + content: "|"; + color: #838383; + } + } + .comment-replies { + display: inline-block; + } + } + + .comment-votes { + color: #838383; + float: right; + display: inline-block; + + .comment-votes-number { + margin-right: 0.25rem; + display: inline-block; + + &::after { + margin-left: 0.25rem; + content: "|"; + } + } + + .icon-like, + .icon-unlike { + cursor: pointer; + color: #C7C7C7; + + &:hover, + &:active, + &:focus { + color: #838383; + } + } + + .icon-like { + margin-right: 0.25rem; + } + } + } + } + } + + .draft-panel { + background: #E5E5E5; + border-left: 1px solid #D4D4D4; + + .panel-title { + display: inline-block; + } + } + + .show-for-medium { + .panel-title { + display: none; + } + } + } + } +} + +// 08. Legislation changes +// ----------------- +.legislation-changes { + ul { + list-style: none; + margin-left: 0; + + li { + margin-bottom: 1rem; + + &::before { + margin-right: 0.25rem; + content: "—" + } + + .changes-link { + display: block; + margin-left: 1rem; + font-size: $small-font-size; + + @include breakpoint(medium) { + display: inline-block; + } + + a { + span { + text-decoration: underline; + } + + .icon-external { + text-decoration: none; + color: #999999; + line-height: 0; + vertical-align: sub; + margin-left: 0.5rem; + } + + &:active, + &:focus, + &:hover { + text-decoration: none; + } + } + } + } + } +} + +// 09. Legislation comments +// ----------------- +.legislation-comments { + + .pull-right { + float: right; + } + + .comment-section { + background: #FAFAFA; + padding: 1rem; + border: 1px solid #DEE0E3; + margin-top: 0.25rem; + margin-bottom: 1rem; + } + + .comment { + margin-bottom: 3rem; + + a { + span { + text-decoration: underline; + } + + .icon-expand, + .icon-comments { + text-decoration: none; + color: #999999; + line-height: 0; + } + + .icon-expand { + margin-left: 0.25rem; + } + + .icon-comments { + margin-right: 0.25rem; + } + + &:active, + &:focus, + &:hover { + text-decoration: none; + } + } + } +} + +// 10. Legislation draft comment +// ----------------- +.legislation-comment { + + .annotation-share-comment { + margin-bottom: 2rem; + } + + .pull-right { + float: right; + } + + .comment-section { + background: #FAFAFA; + padding: 1rem; + border: 1px solid #DEE0E3; + margin-top: 0.25rem; + margin-bottom: 1rem; + } + + .comment { + margin-bottom: 3rem; + + a { + span { + text-decoration: underline; + } + + .icon-expand, + .icon-comments { + text-decoration: none; + color: #999999; + line-height: 0; + } + + .icon-expand { + margin-left: 0.25rem; + } + + .icon-comments { + margin-right: 0.25rem; + } + + &:active, + &:focus, + &:hover { + text-decoration: none; + } + } + } +} diff --git a/app/assets/stylesheets/participation.scss b/app/assets/stylesheets/participation.scss index 0c3ff069a..5658a4e6b 100644 --- a/app/assets/stylesheets/participation.scss +++ b/app/assets/stylesheets/participation.scss @@ -299,7 +299,10 @@ .proposal-show, .investment-project-show, .budget-investment-show, -.polls-show { +.polls-show, +.debate-quiz, +.budget-investment-show, +.draft-panels { p { word-wrap: break-word; @@ -493,7 +496,7 @@ } } -.debate, .proposal, .investment-project, .budget-investment { +.debate, .proposal, .investment-project, .budget-investment, .legislation { margin: $line-height/4 0; .panel { diff --git a/app/controllers/admin/legislation/base_controller.rb b/app/controllers/admin/legislation/base_controller.rb new file mode 100644 index 000000000..b6c62f426 --- /dev/null +++ b/app/controllers/admin/legislation/base_controller.rb @@ -0,0 +1,14 @@ +class Admin::Legislation::BaseController < Admin::BaseController + include FeatureFlags + + feature_flag :legislation + + helper_method :namespace + + private + + def namespace + "admin" + end + +end diff --git a/app/controllers/admin/legislation/draft_versions_controller.rb b/app/controllers/admin/legislation/draft_versions_controller.rb new file mode 100644 index 000000000..0d84addbf --- /dev/null +++ b/app/controllers/admin/legislation/draft_versions_controller.rb @@ -0,0 +1,44 @@ +class Admin::Legislation::DraftVersionsController < Admin::Legislation::BaseController + load_and_authorize_resource :process, class: "Legislation::Process" + load_and_authorize_resource :draft_version, class: "Legislation::DraftVersion", through: :process + + def index + @draft_versions = @process.draft_versions + end + + def create + if @draft_version.save + redirect_to admin_legislation_process_draft_versions_path, notice: t('admin.legislation.draft_versions.create.notice', link: legislation_process_draft_version_path(@process, @draft_version).html_safe) + else + flash.now[:error] = t('admin.legislation.draft_versions.create.error') + render :new + end + end + + def update + if @draft_version.update(draft_version_params) + redirect_to edit_admin_legislation_process_draft_version_path(@process, @draft_version), notice: t('admin.legislation.draft_versions.update.notice', link: legislation_process_draft_version_path(@process, @draft_version).html_safe) + else + flash.now[:error] = t('admin.legislation.draft_versions.update.error') + render :edit + end + end + + def destroy + @draft_version.destroy + redirect_to admin_legislation_process_draft_versions_path, notice: t('admin.legislation.draft_versions.destroy.notice') + end + + private + + def draft_version_params + params.require(:legislation_draft_version).permit( + :title, + :changelog, + :status, + :final_version, + :body, + :body_html + ) + end +end diff --git a/app/controllers/admin/legislation/processes_controller.rb b/app/controllers/admin/legislation/processes_controller.rb new file mode 100644 index 000000000..8cc678151 --- /dev/null +++ b/app/controllers/admin/legislation/processes_controller.rb @@ -0,0 +1,54 @@ +class Admin::Legislation::ProcessesController < Admin::Legislation::BaseController + has_filters %w{open next past all}, only: :index + + load_and_authorize_resource :process, class: "Legislation::Process" + + def index + @processes = ::Legislation::Process.send(@current_filter).order('id DESC').page(params[:page]) + end + + def create + if @process.save + redirect_to edit_admin_legislation_process_path(@process), notice: t('admin.legislation.processes.create.notice', link: legislation_process_path(@process).html_safe) + else + flash.now[:error] = t('admin.legislation.processes.create.error') + render :new + end + end + + def update + if @process.update(process_params) + redirect_to edit_admin_legislation_process_path(@process), notice: t('admin.legislation.processes.update.notice', link: legislation_process_path(@process).html_safe) + else + flash.now[:error] = t('admin.legislation.processes.update.error') + render :edit + end + end + + def destroy + @process.destroy + redirect_to admin_legislation_processes_path, notice: t('admin.legislation.processes.destroy.notice') + end + + private + + def process_params + params.require(:legislation_process).permit( + :title, + :description_summary, + :target_summary, + :description, + :target, + :how_to_participate, + :additional_info, + :start_date, + :end_date, + :debate_start_date, + :debate_end_date, + :draft_publication_date, + :allegations_start_date, + :allegations_end_date, + :final_publication_date + ) + end +end diff --git a/app/controllers/admin/legislation/questions_controller.rb b/app/controllers/admin/legislation/questions_controller.rb new file mode 100644 index 000000000..c8335e6df --- /dev/null +++ b/app/controllers/admin/legislation/questions_controller.rb @@ -0,0 +1,45 @@ +class Admin::Legislation::QuestionsController < Admin::Legislation::BaseController + load_and_authorize_resource :process, class: "Legislation::Process" + load_and_authorize_resource :question, class: "Legislation::Question", through: :process + + def index + @questions = @process.questions + end + + def new + @question.question_options.build + end + + def create + @question.author = current_user + if @question.save + redirect_to admin_legislation_process_questions_path, notice: t('admin.legislation.questions.create.notice', link: legislation_process_question_path(@process, @question).html_safe) + else + flash.now[:error] = t('admin.legislation.questions.create.error') + render :new + end + end + + def update + if @question.update(question_params) + redirect_to edit_admin_legislation_process_question_path(@process, @question), notice: t('admin.legislation.questions.update.notice', link: legislation_process_question_path(@process, @question).html_safe) + else + flash.now[:error] = t('admin.legislation.questions.update.error') + render :edit + end + end + + def destroy + @question.destroy + redirect_to admin_legislation_process_questions_path, notice: t('admin.legislation.questions.destroy.notice') + end + + private + + def question_params + params.require(:legislation_question).permit( + :title, + question_options_attributes: [:id, :value, :_destroy] + ) + end +end diff --git a/app/controllers/annotations_controller.rb b/app/controllers/annotations_controller.rb index d70a2e9be..8b004076c 100644 --- a/app/controllers/annotations_controller.rb +++ b/app/controllers/annotations_controller.rb @@ -24,7 +24,7 @@ class AnnotationsController < ApplicationController end def search - @annotations = Annotation.where(legislation_id: params[:legislation_id]) + @annotations = Annotation.where(legacy_legislation_id: params[:legacy_legislation_id]) annotations_hash = { total: @annotations.size, rows: @annotations } render json: annotations_hash.to_json(methods: :permissions) end @@ -35,6 +35,6 @@ class AnnotationsController < ApplicationController params .require(:annotation) .permit(:quote, :text, ranges: [:start, :startOffset, :end, :endOffset]) - .merge(legislation_id: params[:legislation_id]) + .merge(legacy_legislation_id: params[:legacy_legislation_id]) end end diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 0a0018c9f..fb5e481a0 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -1,6 +1,10 @@ class CommentsController < ApplicationController + include CustomUrlsHelper + before_action :authenticate_user!, only: :create before_action :load_commentable, only: :create + before_action :verify_resident_for_commentable!, only: :create + before_action :verify_comments_open!, only: [:create, :vote] before_action :build_comment, only: :create load_and_authorize_resource @@ -75,6 +79,22 @@ class CommentsController < ApplicationController notifiable = comment.commentable end Notification.add(notifiable.author_id, notifiable) unless comment.author_id == notifiable.author_id - end + end -end \ No newline at end of file + def verify_resident_for_commentable! + return if current_user.administrator? || current_user.moderator? + + if @commentable.respond_to?(:comments_for_verified_residents_only?) && @commentable.comments_for_verified_residents_only? + verify_resident! + end + end + + def verify_comments_open! + return if current_user.administrator? || current_user.moderator? + + if @commentable.respond_to?(:comments_closed?) && @commentable.comments_closed? + redirect_to @commentable, alert: t('comments.comments_closed') + end + end + +end diff --git a/app/controllers/legacy_legislations_controller.rb b/app/controllers/legacy_legislations_controller.rb new file mode 100644 index 000000000..103c81cea --- /dev/null +++ b/app/controllers/legacy_legislations_controller.rb @@ -0,0 +1,8 @@ +class LegacyLegislationsController < ApplicationController + load_and_authorize_resource + + def show + @legacy_legislation = LegacyLegislation.find(params[:id]) + end + +end diff --git a/app/controllers/legislation/annotations_controller.rb b/app/controllers/legislation/annotations_controller.rb new file mode 100644 index 000000000..93de149fa --- /dev/null +++ b/app/controllers/legislation/annotations_controller.rb @@ -0,0 +1,101 @@ +class Legislation::AnnotationsController < ApplicationController + skip_before_action :verify_authenticity_token + + before_action :authenticate_user!, only: [:create, :new_comment] + before_action :convert_ranges_parameters, only: [:create] + + load_and_authorize_resource :process + load_and_authorize_resource :draft_version, through: :process + load_and_authorize_resource + + has_orders %w{most_voted newest oldest}, only: :show + + def index + @annotations = @draft_version.annotations + end + + def show + @commentable = @annotation + @comment_tree = CommentTree.new(@commentable, params[:page], @current_order) + set_comment_flags(@comment_tree.comments) + end + + def create + if !@process.open_phase?(:allegations) || @draft_version.final_version? + render json: {}, status: :not_found and return + end + + existing_annotation = @draft_version.annotations.where( + range_start: annotation_params[:ranges].first[:start], range_start_offset: annotation_params[:ranges].first[:startOffset].to_i, + range_end: annotation_params[:ranges].first[:end], range_end_offset: annotation_params[:ranges].first[:endOffset].to_i).first + + if @annotation = existing_annotation + if comment = @annotation.comments.create(body: annotation_params[:text], user: current_user) + render json: @annotation.to_json + else + render json: comment.errors.full_messages, status: :unprocessable_entity + end + else + @annotation = @draft_version.annotations.new(annotation_params) + @annotation.author = current_user + if @annotation.save + track_event + render json: @annotation.to_json + else + render json: @annotation.errors.full_messages, status: :unprocessable_entity + end + end + end + + def search + @annotations = @draft_version.annotations.order("LENGTH(quote) DESC") + annotations_hash = { total: @annotations.size, rows: @annotations } + render json: annotations_hash.to_json(methods: :weight) + end + + def comments + @annotation = Legislation::Annotation.find(params[:annotation_id]) + @comment = @annotation.comments.new + end + + def new + respond_to do |format| + format.js + end + end + + def new_comment + @draft_version = Legislation::DraftVersion.find(params[:draft_version_id]) + @annotation = @draft_version.annotations.find(params[:annotation_id]) + @comment = @annotation.comments.new(body: params[:comment][:body], user: current_user) + if @comment.save + @comment = @annotation.comments.new + end + + respond_to do |format| + format.js { render :new_comment } + end + end + + private + + def annotation_params + params + .require(:legislation_annotation) + .permit(:quote, :text, ranges: [:start, :startOffset, :end, :endOffset]) + end + + def track_event + ahoy.track "legislation_annotation_created".to_sym, + "legislation_annotation_id": @annotation.id, + "legislation_draft_version_id": @draft_version.id + end + + def convert_ranges_parameters + if params[:legislation_annotation] && params[:legislation_annotation][:ranges] && params[:legislation_annotation][:ranges].is_a?(String) + params[:legislation_annotation][:ranges] = JSON.parse(params[:legislation_annotation][:ranges]) + end + rescue JSON::ParserError + end + +end diff --git a/app/controllers/legislation/answers_controller.rb b/app/controllers/legislation/answers_controller.rb new file mode 100644 index 000000000..372398e41 --- /dev/null +++ b/app/controllers/legislation/answers_controller.rb @@ -0,0 +1,41 @@ +class Legislation::AnswersController < Legislation::BaseController + before_action :authenticate_user! + before_action :verify_resident! + + load_and_authorize_resource :process + load_and_authorize_resource :question, through: :process + load_and_authorize_resource :answer, through: :question + + respond_to :html, :js + + def create + if @process.open_phase?(:debate) + @answer.user = current_user + @answer.save + track_event + respond_to do |format| + format.js + format.html { redirect_to legislation_process_question_path(@process, @question) } + end + else + respond_to do |format| + format.js { render json: {} , status: :not_found } + format.html { redirect_to legislation_process_question_path(@process, @question), alert: t('legislation.questions.participation.phase_not_open') } + end + end + end + + private + def answer_params + params.require(:legislation_answer).permit( + :legislation_question_option_id, + ) + end + + def track_event + ahoy.track "legislation_answer_created".to_sym, + "legislation_answer_id": @answer.id, + "legislation_question_option_id": @answer.legislation_question_option_id, + "legislation_question_id": @answer.legislation_question_id + end +end diff --git a/app/controllers/legislation/base_controller.rb b/app/controllers/legislation/base_controller.rb new file mode 100644 index 000000000..ca609ecba --- /dev/null +++ b/app/controllers/legislation/base_controller.rb @@ -0,0 +1,5 @@ +class Legislation::BaseController < ApplicationController + include FeatureFlags + + feature_flag :legislation +end diff --git a/app/controllers/legislation/draft_versions_controller.rb b/app/controllers/legislation/draft_versions_controller.rb new file mode 100644 index 000000000..cdac2c9d5 --- /dev/null +++ b/app/controllers/legislation/draft_versions_controller.rb @@ -0,0 +1,40 @@ +class Legislation::DraftVersionsController < Legislation::BaseController + load_and_authorize_resource :process + load_and_authorize_resource :draft_version, through: :process + + def index + end + + def show + @draft_versions_list = visible_draft_versions + @draft_version = @draft_versions_list.find(params[:id]) + end + + def changes + @draft_versions_list = visible_draft_versions + @draft_version = @draft_versions_list.find(params[:draft_version_id]) + end + + def go_to_version + version = visible_draft_versions.find(params[:draft_version_id]) + + if params[:redirect_action] == 'changes' + redirect_to legislation_process_draft_version_changes_path(@process, version) + elsif params[:redirect_action] == 'annotations' + redirect_to legislation_process_draft_version_annotations_path(@process, version) + else + redirect_to legislation_process_draft_version_path(@process, version) + end + end + + private + + def visible_draft_versions + if current_user && current_user.administrator? + @process.draft_versions + else + @process.draft_versions.published + end + end + +end diff --git a/app/controllers/legislation/processes_controller.rb b/app/controllers/legislation/processes_controller.rb new file mode 100644 index 000000000..7aff709d0 --- /dev/null +++ b/app/controllers/legislation/processes_controller.rb @@ -0,0 +1,78 @@ +class Legislation::ProcessesController < Legislation::BaseController + has_filters %w{open next past}, only: :index + load_and_authorize_resource + + def index + @current_filter ||= 'open' + @processes = ::Legislation::Process.send(@current_filter).page(params[:page]) + end + + def show + if @process.active_phase?(:allegations) && @process.show_phase?(:allegations) && draft_version = @process.draft_versions.published.last + redirect_to legislation_process_draft_version_path(@process, draft_version) + elsif @process.active_phase?(:debate) + redirect_to legislation_process_debate_path(@process) + else + redirect_to legislation_process_allegations_path(@process) + end + end + + def debate + phase :debate + + if @process.show_phase?(:debate) + render :debate + else + render :phase_not_open + end + end + + def draft_publication + phase :draft_publication + + if @process.show_phase?(@phase) + if draft_version = @process.draft_versions.published.last + redirect_to legislation_process_draft_version_path(@process, draft_version) + else + render :phase_empty + end + else + render :phase_not_open + end + end + + def allegations + phase :allegations + + if @process.show_phase?(@phase) + if draft_version = @process.draft_versions.published.last + redirect_to legislation_process_draft_version_path(@process, draft_version) + else + render :phase_empty + end + else + render :phase_not_open + end + end + + def final_version_publication + phase :final_version_publication + + if @process.show_phase?(@phase) + if final_version = @process.final_draft_version + redirect_to legislation_process_draft_version_path(@process, final_version) + else + render :phase_empty + end + else + render :phase_not_open + end + end + + private + + def phase(phase) + @process = ::Legislation::Process.find(params[:process_id]) + @phase = phase + end +end diff --git a/app/controllers/legislation/questions_controller.rb b/app/controllers/legislation/questions_controller.rb new file mode 100644 index 000000000..d50443281 --- /dev/null +++ b/app/controllers/legislation/questions_controller.rb @@ -0,0 +1,13 @@ +class Legislation::QuestionsController < Legislation::BaseController + load_and_authorize_resource :process + load_and_authorize_resource :question, through: :process + + has_orders %w{most_voted newest oldest}, only: :show + + def show + @commentable = @question + @comment_tree = CommentTree.new(@commentable, params[:page], @current_order) + set_comment_flags(@comment_tree.comments) + @answer = @question.answer_for_user(current_user) || Legislation::Answer.new + end +end diff --git a/app/controllers/legislations_controller.rb b/app/controllers/legislations_controller.rb deleted file mode 100644 index a95f95ef2..000000000 --- a/app/controllers/legislations_controller.rb +++ /dev/null @@ -1,8 +0,0 @@ -class LegislationsController < ApplicationController - load_and_authorize_resource - - def show - @legislation = Legislation.find(params[:id]) - end - -end \ No newline at end of file diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index ab346e802..99f401f7b 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -1,4 +1,6 @@ class NotificationsController < ApplicationController + include CustomUrlsHelper + before_action :authenticate_user! after_action :mark_as_read, only: :show skip_authorization_check diff --git a/app/controllers/sandbox_controller.rb b/app/controllers/sandbox_controller.rb new file mode 100644 index 000000000..12b5dd91e --- /dev/null +++ b/app/controllers/sandbox_controller.rb @@ -0,0 +1,45 @@ +class SandboxController < ApplicationController + skip_authorization_check + + layout :set_layout + + helper_method(:namespace) + + def index + @templates = Dir.glob(Rails.root.join('app/views/sandbox/*.html.erb').to_s).map do |filename| + filename = File.basename(filename, File.extname(filename)) + filename unless filename.starts_with?('_') || filename == 'index.html' + end.compact + end + + def show + if params[:template].index('.') # CVE-2014-0130 + render :action => "index" + elsif lookup_context.exists?("sandbox/#{params[:template]}") + if params[:template] == "index" + render :action => "index" + else + render "sandbox/#{params[:template]}" + end + + elsif lookup_context.exists?("sandbox/#{params[:template]}/index") + render "sandbox/#{params[:template]}/index" + else + render :action => "index" + end + end + + private + + def set_layout + if params[:template] && params[:template].split("_").first == "admin" + "admin" + else + "application" + end + end + + def namespace + "admin" + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 6a040ec69..3cd7580ee 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -18,6 +18,8 @@ module ApplicationHelper end def markdown(text) + return text if text.blank? + # See https://github.com/vmg/redcarpet for options render_options = { filter_html: false, diff --git a/app/helpers/comments_helper.rb b/app/helpers/comments_helper.rb index df6e57b61..3f760f73c 100644 --- a/app/helpers/comments_helper.rb +++ b/app/helpers/comments_helper.rb @@ -1,11 +1,31 @@ module CommentsHelper + def comment_tree_title_text(commentable) + if commentable.class == Legislation::Question + t("legislation.questions.comments.comments_title") + else + t("comments_helper.comments_title") + end + end + + def leave_comment_text(commentable) + if commentable.class == Legislation::Question + t("legislation.questions.comments.form.leave_comment") + else + t("comments.form.leave_comment") + end + end + def comment_link_text(parent_id) parent_id.present? ? t("comments_helper.reply_link") : t("comments_helper.comment_link") end - def comment_button_text(parent_id) - parent_id.present? ? t("comments_helper.reply_button") : t("comments_helper.comment_button") + def comment_button_text(parent_id, commentable) + if commentable.class == Legislation::Question + parent_id.present? ? t("comments_helper.reply_button") : t("legislation.questions.comments.comment_button") + else + parent_id.present? ? t("comments_helper.reply_button") : t("comments_helper.comment_button") + end end def parent_or_commentable_dom_id(parent_id, commentable) @@ -21,12 +41,17 @@ module CommentsHelper end def commentable_path(comment) - if comment.commentable_type == "Budget::Investment" - budget_investment_path(comment.commentable.budget_id, comment.commentable) - elsif comment.commentable_type == "Poll::Question" - question_path(comment.commentable) + commentable = comment.commentable + + case comment.commentable_type + when "Budget::Investment" + budget_investment_path(commentable.budget_id, commentable) + when "Legislation::Question" + legislation_process_question_path(commentable.process, commentable) + when "Legislation::Annotation" + legislation_process_draft_version_annotation_path(commentable.draft_version.process, commentable.draft_version, commentable) else - comment.commentable + commentable end end @@ -50,4 +75,22 @@ module CommentsHelper end end -end \ No newline at end of file + def require_verified_resident_for_commentable?(commentable, current_user) + return false if current_user.administrator? || current_user.moderator? + + commentable.respond_to?(:comments_for_verified_residents_only?) && commentable.comments_for_verified_residents_only? && !current_user.residence_verified? + end + + def comments_closed_for_commentable?(commentable) + commentable.respond_to?(:comments_closed?) && commentable.comments_closed? + end + + def comments_closed_text(commentable) + if commentable.class == Legislation::Question + t("legislation.questions.comments.comments_closed") + else + t("comments.comments_closed") + end + end + +end diff --git a/app/helpers/custom_urls_helper.rb b/app/helpers/custom_urls_helper.rb new file mode 100644 index 000000000..1519e771e --- /dev/null +++ b/app/helpers/custom_urls_helper.rb @@ -0,0 +1,9 @@ +module CustomUrlsHelper + def legislation_question_url(question) + legislation_process_question_url(question.process, question) + end + + def legislation_annotation_url(annotation) + legislation_process_question_url(annotation.draft_version.process, annotation.draft_version, annotation) + end +end diff --git a/app/helpers/legislation_helper.rb b/app/helpers/legislation_helper.rb new file mode 100644 index 000000000..3a993d683 --- /dev/null +++ b/app/helpers/legislation_helper.rb @@ -0,0 +1,9 @@ +module LegislationHelper + def format_date(date) + l(date, format: "%d %b %Y") if date + end + + def format_date_for_calendar_form(date) + l(date, format: "%d/%m/%Y") if date + end +end diff --git a/app/helpers/text_helper.rb b/app/helpers/text_helper.rb new file mode 100644 index 000000000..dda3ba721 --- /dev/null +++ b/app/helpers/text_helper.rb @@ -0,0 +1,9 @@ +module TextHelper + def first_paragraph(text) + if text.blank? + "" + else + text.strip.split("\n").first + end + end +end diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb index 8df52e6eb..f09333ff0 100644 --- a/app/models/abilities/administrator.rb +++ b/app/models/abilities/administrator.rb @@ -32,7 +32,7 @@ module Abilities can :mark_featured, Debate can :unmark_featured, Debate - can :comment_as_administrator, [Debate, Comment, Proposal, Poll::Question, Budget::Investment] + can :comment_as_administrator, [Debate, Comment, Proposal, Poll::Question, Budget::Investment, Legislation::Question, Legislation::Annotation] can [:search, :create, :index, :destroy], ::Moderator can [:search, :create, :index, :summary], ::Valuator @@ -64,6 +64,12 @@ module Abilities can :manage, SiteCustomization::Page can :manage, SiteCustomization::Image can :manage, SiteCustomization::ContentBlock + + can [:manage], ::Legislation::Process + can [:manage], ::Legislation::DraftVersion + can [:manage], ::Legislation::Question + cannot :comment_as_moderator, [::Legislation::Question, Legislation::Annotation] + end end end diff --git a/app/models/abilities/everyone.rb b/app/models/abilities/everyone.rb index c2b6cd6d7..50949671f 100644 --- a/app/models/abilities/everyone.rb +++ b/app/models/abilities/everyone.rb @@ -11,13 +11,18 @@ module Abilities can [:read, :welcome], Budget can :read, Budget::Investment can :read, SpendingProposal - can :read, Legislation + can :read, LegacyLegislation can :read, User can [:search, :read], Annotation can [:read], Budget can [:read], Budget::Group can [:read, :print], Budget::Investment can :new, DirectMessage + can [:read, :debate, :draft_publication, :allegations, :final_version_publication], Legislation::Process + can [:read, :changes, :go_to_version], Legislation::DraftVersion + can [:read], Legislation::Question + can [:create], Legislation::Answer + can [:search, :comments, :read, :create, :new_comment], Legislation::Annotation end end end diff --git a/app/models/abilities/moderator.rb b/app/models/abilities/moderator.rb index 5740e302e..ba8c659ba 100644 --- a/app/models/abilities/moderator.rb +++ b/app/models/abilities/moderator.rb @@ -5,7 +5,7 @@ module Abilities def initialize(user) self.merge Abilities::Moderation.new(user) - can :comment_as_moderator, [Debate, Comment, Proposal, Budget::Investment, Poll::Question] + can :comment_as_moderator, [Debate, Comment, Proposal, Budget::Investment, Poll::Question, Legislation::Question, Legislation::Annotation] end end end diff --git a/app/models/annotation.rb b/app/models/annotation.rb index 66abb2db2..295badd92 100644 --- a/app/models/annotation.rb +++ b/app/models/annotation.rb @@ -1,7 +1,7 @@ class Annotation < ActiveRecord::Base serialize :ranges, Array - belongs_to :legislation + belongs_to :legacy_legislation belongs_to :user def permissions diff --git a/app/models/comment.rb b/app/models/comment.rb index cd84a3578..aa3f27e24 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -11,7 +11,7 @@ class Comment < ActiveRecord::Base validates :body, presence: true validates :user, presence: true - validates_inclusion_of :commentable_type, in: ["Debate", "Proposal", "Budget::Investment", "Poll::Question"] + validates_inclusion_of :commentable_type, in: ["Debate", "Proposal", "Budget::Investment", "Poll::Question", "Legislation::Question", "Legislation::Annotation"] validate :validate_body_length diff --git a/app/models/comment_notifier.rb b/app/models/comment_notifier.rb index 3cceb5c15..68b350e6b 100644 --- a/app/models/comment_notifier.rb +++ b/app/models/comment_notifier.rb @@ -12,7 +12,9 @@ class CommentNotifier private def send_comment_email - Mailer.comment(@comment).deliver_later if email_on_comment? + unless @comment.commentable.is_a?(Legislation::Annotation) + Mailer.comment(@comment).deliver_later if email_on_comment? + end end def send_reply_email diff --git a/app/models/legacy_legislation.rb b/app/models/legacy_legislation.rb new file mode 100644 index 000000000..ddc267e3a --- /dev/null +++ b/app/models/legacy_legislation.rb @@ -0,0 +1,3 @@ +class LegacyLegislation < ActiveRecord::Base + has_many :annotations +end diff --git a/app/models/legislation.rb b/app/models/legislation.rb index d918c0255..5aa217fd0 100644 --- a/app/models/legislation.rb +++ b/app/models/legislation.rb @@ -1,3 +1,5 @@ -class Legislation < ActiveRecord::Base - has_many :annotations +module Legislation + def self.table_name_prefix + 'legislation_' + end end diff --git a/app/models/legislation/annotation.rb b/app/models/legislation/annotation.rb new file mode 100644 index 000000000..92fa9dd86 --- /dev/null +++ b/app/models/legislation/annotation.rb @@ -0,0 +1,58 @@ +class Legislation::Annotation < ActiveRecord::Base + COMMENTS_PAGE_SIZE = 5 + acts_as_paranoid column: :hidden_at + include ActsAsParanoidAliases + + serialize :ranges, Array + + belongs_to :draft_version, class_name: 'Legislation::DraftVersion', foreign_key: 'legislation_draft_version_id' + belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' + has_many :comments, as: :commentable, dependent: :destroy + + validates :text, presence: true + validates :quote, presence: true + validates :draft_version, presence: true + validates :author, presence: true + + before_save :store_range, :store_context + after_create :create_first_comment + + def store_range + self.range_start = ranges.first["start"] + self.range_start_offset = ranges.first["startOffset"] + self.range_end = ranges.first["end"] + self.range_end_offset = ranges.first["endOffset"] + end + + def store_context + begin + html = draft_version.body_html + doc = Nokogiri::HTML(html) + + selector_start = "/html/body/#{range_start}" + el_start = doc.at_xpath(selector_start) + + selector_end = "/html/body/#{range_end}" + el_end = doc.at_xpath(selector_end) + + remainder_el_start = el_start.text[0 .. range_start_offset-1] unless range_start_offset.zero? + remainder_el_end = el_end.text[range_end_offset .. -1] + + self.context = "#{remainder_el_start}#{quote}#{remainder_el_end}" + rescue + "#{quote}" + end + end + + def create_first_comment + comments.create(body: self.text, user: self.author) + end + + def title + text[0..50] + end + + def weight + comments_count + comments.sum(:cached_votes_total) + end +end diff --git a/app/models/legislation/answer.rb b/app/models/legislation/answer.rb new file mode 100644 index 000000000..cd415d0d4 --- /dev/null +++ b/app/models/legislation/answer.rb @@ -0,0 +1,12 @@ +class Legislation::Answer < ActiveRecord::Base + acts_as_paranoid column: :hidden_at + include ActsAsParanoidAliases + + belongs_to :question, class_name: 'Legislation::Question', foreign_key: 'legislation_question_id', inverse_of: :answers, counter_cache: true + belongs_to :question_option, class_name: 'Legislation::QuestionOption', foreign_key: 'legislation_question_option_id', inverse_of: :answers, counter_cache: true + belongs_to :user, dependent: :destroy, inverse_of: :legislation_answers + + validates :question, presence: true, uniqueness: { scope: :user_id} + validates :question_option, presence: true + validates :user, presence: true +end diff --git a/app/models/legislation/draft_version.rb b/app/models/legislation/draft_version.rb new file mode 100644 index 000000000..7b9e6a0aa --- /dev/null +++ b/app/models/legislation/draft_version.rb @@ -0,0 +1,33 @@ +class Legislation::DraftVersion < ActiveRecord::Base + VALID_STATUSES = %w(draft published) + + acts_as_paranoid column: :hidden_at + include ActsAsParanoidAliases + + belongs_to :process, class_name: 'Legislation::Process', foreign_key: 'legislation_process_id' + has_many :annotations, class_name: 'Legislation::Annotation', foreign_key: 'legislation_draft_version_id', dependent: :destroy + + validates :title, presence: true + validates :body, presence: true + validates :status, presence: true, inclusion: { in: VALID_STATUSES } + + scope :published, -> { where(status: 'published').order('id DESC') } + + before_save :render_html + + def render_html + renderer = Redcarpet::Render::HTML.new(with_toc_data: true) + toc_renderer = Redcarpet::Render::HTML_TOC.new(with_toc_data: true) + + self.body_html = Redcarpet::Markdown.new(renderer).render(body) + self.toc_html = Redcarpet::Markdown.new(toc_renderer).render(body) + end + + def display_title + status == 'draft' ? "#{title} *" : title + end + + def total_comments + annotations.sum(:comments_count) + end +end diff --git a/app/models/legislation/process.rb b/app/models/legislation/process.rb new file mode 100644 index 000000000..32468b834 --- /dev/null +++ b/app/models/legislation/process.rb @@ -0,0 +1,90 @@ +class Legislation::Process < ActiveRecord::Base + acts_as_paranoid column: :hidden_at + include ActsAsParanoidAliases + + has_many :draft_versions, -> { order(:id) }, class_name: 'Legislation::DraftVersion', foreign_key: 'legislation_process_id', dependent: :destroy + has_one :final_draft_version, -> { where final_version: true, status: 'published' }, class_name: 'Legislation::DraftVersion', foreign_key: 'legislation_process_id' + has_many :questions, -> { order(:id) }, class_name: 'Legislation::Question', foreign_key: 'legislation_process_id', dependent: :destroy + + validates :title, presence: true + validates :start_date, presence: true + validates :end_date, presence: true + validates :debate_start_date, presence: true, if: :debate_end_date? + validates :debate_end_date, presence: true, if: :debate_start_date? + validates :allegations_start_date, presence: true, if: :allegations_end_date? + validates :allegations_end_date, presence: true, if: :allegations_start_date? + validate :valid_date_ranges + + scope :open, -> { where("start_date <= ? and end_date >= ?", Date.current, Date.current).order('id DESC') } + scope :next, -> { where("start_date > ?", Date.current).order('id DESC') } + scope :past, -> { where("end_date < ?", Date.current).order('id DESC') } + + def open_phase?(phase) + today = Date.current + + case phase + when :debate + active_phase?(:debate) && today >= debate_start_date && today <= debate_end_date + when :draft_publication + active_phase?(:draft_publication) && today >= draft_publication_date + when :allegations + active_phase?(:allegations) && today >= allegations_start_date && today <= allegations_end_date + when :final_version_publication + active_phase?(:final_version_publication) && today >= final_publication_date + end + end + + def show_phase?(phase) + # show past phases even if they're finished + today = Date.current + + case phase + when :debate + active_phase?(:debate) && today >= debate_start_date + when :draft_publication + active_phase?(:draft_publication) && today >= draft_publication_date + when :allegations + active_phase?(:allegations) && today >= allegations_start_date + when :final_version_publication + active_phase?(:final_version_publication) && today >= final_publication_date + end + end + + def active_phase?(phase) + case phase + when :debate + debate_start_date.present? && debate_end_date.present? + when :draft_publication + draft_publication_date.present? + when :allegations + allegations_start_date.present? && allegations_end_date.present? + when :final_version_publication + final_publication_date.present? + end + end + + def total_comments + questions.sum(:comments_count) + draft_versions.map(&:total_comments).sum + end + + def status + today = Date.current + + if today < start_date + :planned + elsif end_date < today + :closed + else + :open + end + end + + private + + def valid_date_ranges + errors.add(:end_date, :invalid_date_range) if end_date && start_date && end_date < start_date + errors.add(:debate_end_date, :invalid_date_range) if debate_end_date && debate_start_date && debate_end_date < debate_start_date + errors.add(:allegations_end_date, :invalid_date_range) if allegations_end_date && allegations_start_date && allegations_end_date < allegations_start_date + end + +end diff --git a/app/models/legislation/question.rb b/app/models/legislation/question.rb new file mode 100644 index 000000000..374c43eb5 --- /dev/null +++ b/app/models/legislation/question.rb @@ -0,0 +1,42 @@ +class Legislation::Question < ActiveRecord::Base + acts_as_paranoid column: :hidden_at + include ActsAsParanoidAliases + + belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' + belongs_to :process, class_name: 'Legislation::Process', foreign_key: 'legislation_process_id' + + has_many :question_options, -> { order(:id) }, class_name: 'Legislation::QuestionOption', foreign_key: 'legislation_question_id', dependent: :destroy, inverse_of: :question + has_many :answers, class_name: 'Legislation::Answer', foreign_key: 'legislation_question_id', dependent: :destroy, inverse_of: :question + has_many :comments, as: :commentable, dependent: :destroy + + accepts_nested_attributes_for :question_options, :reject_if => proc { |attributes| attributes[:value].blank? }, allow_destroy: true + + validates :process, presence: true + validates :title, presence: true + + scope :sorted, -> { order('id ASC') } + + def next_question_id + @next_question_id ||= process.questions.where("id > ?", id).sorted.limit(1).pluck(:id).first + end + + def first_question_id + @first_question_id ||= process.questions.sorted.limit(1).pluck(:id).first + end + + def answer_for_user(user) + answers.where(user: user).first + end + + def comments_for_verified_residents_only? + true + end + + def comments_closed? + !comments_open? + end + + def comments_open? + process.open_phase?(:debate) + end +end diff --git a/app/models/legislation/question_option.rb b/app/models/legislation/question_option.rb new file mode 100644 index 000000000..f7927dd1a --- /dev/null +++ b/app/models/legislation/question_option.rb @@ -0,0 +1,10 @@ +class Legislation::QuestionOption < ActiveRecord::Base + acts_as_paranoid column: :hidden_at + include ActsAsParanoidAliases + + belongs_to :question, class_name: 'Legislation::Question', foreign_key: 'legislation_question_id', inverse_of: :question_options + has_many :answers, class_name: 'Legislation::Answer', foreign_key: 'legislation_question_id', dependent: :destroy, inverse_of: :question + + validates :question, presence: true + validates :value, presence: true, uniqueness: { scope: :legislation_question_id } +end diff --git a/app/models/user.rb b/app/models/user.rb index c3038c88a..89bc4ce87 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -27,6 +27,7 @@ class User < ActiveRecord::Base has_many :notifications has_many :direct_messages_sent, class_name: 'DirectMessage', foreign_key: :sender_id has_many :direct_messages_received, class_name: 'DirectMessage', foreign_key: :receiver_id + has_many :legislation_answers, class_name: 'Legislation::Answer', dependent: :destroy, inverse_of: :user belongs_to :geozone validates :username, presence: true, if: :username_required? diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb index 0b9929f46..642248a17 100644 --- a/app/views/admin/_menu.html.erb +++ b/app/views/admin/_menu.html.erb @@ -117,6 +117,26 @@ + <% if feature?(:legislation) %> +
| <%= t("admin.legislation.draft_versions.table.title") %> | +<%= t("admin.legislation.draft_versions.table.created_at") %> | +<%= t("admin.legislation.draft_versions.table.status") %> | +<%= t("admin.legislation.draft_versions.table.comments") %> | +<%= t("admin.legislation.draft_versions.table.final_version") %> | +
|---|---|---|---|---|
| + <%= link_to draft_version.title, edit_admin_legislation_process_draft_version_path(@process, draft_version) %> + | +<%= draft_version.created_at.to_date %> | +<%= draft_version.status %> <%= link_to "(#{t('.preview')})", legislation_process_draft_version_path(@process, draft_version) if draft_version.status == 'draft' %> | +<%= draft_version.total_comments %> | +<%= draft_version.final_version %> | +
| <%= t("admin.legislation.processes.process.title") %> | +<%= t("admin.legislation.processes.process.status") %> | +<%= t("admin.legislation.processes.process.creation_date") %> | +<%= t("admin.legislation.processes.process.comments") %> | ++ |
|---|---|---|---|---|
| + <%= link_to process.title, edit_admin_legislation_process_path(process) %> + | +<%= t("admin.legislation.processes.process.status_#{process.status}") %> | +<%= I18n.l process.created_at.to_date %> | +<%= process.total_comments %> | ++ <%= link_to t("admin.legislation.processes.index.delete"), admin_legislation_process_path(process), + method: :delete, + class: 'button hollow alert' %> + | +
| <%= t("admin.legislation.questions.table.title") %> | +<%= t("admin.legislation.questions.table.question_options") %> | +<%= t("admin.legislation.questions.table.answers_count") %> | +<%= t("admin.legislation.questions.table.comments_count") %> | +
|---|---|---|---|
| + <%= link_to question.title, edit_admin_legislation_process_question_path(@process, question) %> + | ++ <%= content_tag :ul do %> + <% question.question_options.each do |question_option| %> + <%= content_tag :li do %> + <%= question_option.value %> (<%= question_option.answers_count %>) + <% end %> + <% end %> + <% end %> + | +<%= question.answers_count %> | +<%= link_to question.comments.count, legislation_process_question_path(@process, question, anchor: 'comments') %> | +
- <%= t("debates.show.comments_title") %> + <%= comment_tree_title_text(commentable) %> (<%= commentable.comments_count %>)
<%= render 'shared/wide_order_selector', i18n_namespace: "comments" %> <% if user_signed_in? %> - <%= render 'comments/form', {commentable: commentable, parent_id: nil, toggeable: false} %> + <% if comments_closed_for_commentable?(commentable) %> ++
+
- -
+
+ <%= t("legacy_legislation.help.text", + sign_in: link_to(t("legacy_legislation.help.text_sign_in"), new_user_session_path), + sign_up: link_to(t("legacy_legislation.help.text_sign_up"), new_user_registration_path)).html_safe %> + <%= image_tag ("annotator_help.gif"), alt: t("legacy_legislation.help.alt") %> +
+<%= @legacy_legislation.title %>
+<%= truncate comment.body, length: 250 %>
+<%= t('legislation.annotations.version_chooser.seeing_version') %>
+<%= t('.seeing_version') %>
+<%= t('.seeing_changelog_version') %>
+<%= t('.seeing_version') %>
+<%= t('.empty_questions') %>
+ <% else %> + <%= render process.questions %> + <% end %> +<%= process.title %>
+<%= t('legislation.processes.header_full.description') %>
+ <%= markdown process.description %> + <% end %> +<%= t('legislation.processes.header_full.target') %>
+ <%= markdown process.target %> + <% end %> +<%= t('legislation.processes.header_full.how_to_participate') %>
+ <%= markdown process.how_to_participate %> + <% end %> +<%= t('.title') %>
++ <%= process.title %> +
+<%= t('.description') %>
+ <%= markdown process.description %> + <% end %> +<%= t('.target') %>
+ <%= markdown process.target %> + <% end %> +<%= t('.how_to_participate') %>
+ <%= markdown process.how_to_participate %> + <% end %> +<%= link_to process.title, process %>
+<%= t('legislation.processes.shared.key_dates') %>
+<%= t('legislation.processes.shared.debate_dates') %>
+<%= format_date(process.debate_start_date) %> - <%= format_date(process.debate_end_date) %>
+<%= t('legislation.processes.shared.draft_publication_date') %>
+<%= format_date(process.draft_publication_date) %>
+<%= t('legislation.processes.shared.allegations_dates') %>
+<%= format_date(process.allegations_start_date) %> - <%= format_date(process.allegations_end_date) %>
+<%= t('legislation.processes.shared.final_publication_date') %>
+<%= format_date(process.final_publication_date) %>
+<%= t('.empty_questions') %>
+ <% else %> + <%= render @process.questions %> + <% end %> +<%= t(".empty") %>
+<%= t('.not_open') %>
+<%= link_to question.title, legislation_process_question_path(question.process, question) %>
+<%= t('.title') %>
+<%= link_to @process.title, @process %>
+<%= @question.title %>
+- <%= t("legislation.help.text", - sign_in: link_to(t("legislation.help.text_sign_in"), new_user_session_path), - sign_up: link_to(t("legislation.help.text_sign_up"), new_user_registration_path)).html_safe %> - <%= image_tag ("annotator_help.gif"), alt: t("legislation.help.alt") %> -
-<%= @legislation.title %>
-Licencias urbanísticas, declaraciones responsables y comunicaciones previas
+ ++-
+ Información
+
+ -
+ Debate
+
+ -
+ Texto
+
+
+ +Preguntas asociadas a este proceso
++-
+ No (0)
+
+ -
+ Nunca (0)
+
+
++-
+ No (0)
+
+ -
+ Nunca (0)
+
+
+Licencias urbanísticas, declaraciones responsables y comunicaciones previas
+ ++-
+ Información
+
+ -
+ Debate
+
+ -
+ Texto
+
+
+ +Editar “Esta es mi pregunta”
+Licencias urbanísticas, declaraciones responsables y comunicaciones previas
+ ++-
+ Información
+
+ -
+ Debate
+
+ -
+ Texto
+
+
+ +Versiones del borrador
+Licencias urbanísticas, declaraciones responsables y comunicaciones previas
+ ++-
+ Información
+
+ -
+ Debate
+
+ -
+ Texto
+
+
+ + +Versión 1
+Procesos de legislación colaborativa
++- Filter:
+ - Abiertos
+ - Próximamente
+ - Pasados
+ - Todos
+
+ +Licencias urbanísticas, declaraciones responsables y comunicaciones previas
+ ++-
+ Información
+
+ -
+ Debate
+
+ -
+ Texto
+
+
+ + +Más información
++-
+
+ ¿Cómo funciona este Portal de Gobierno Abierto?
+
+ -
+
+ Utilízalo en tu municipio
+
+ -
+
+ Participación y Transparencia - Próximas novedades
+
+ -
+
+ ¿Cómo funcionan las propuestas ciudadanas?
+
+ -
+
+ ¿Cómo funcionan los presupuestos participativos?
+
+ -
+
+ Participación ciudadana directa en el mundo
+
+ -
+
+ Hechos sobre participación ciudadana y democracia directa
+
+ -
+
+ Soluciones a problemas técnicos (FAQ)
+
+ -
+
+ Hojas de firmas
+
+
++ Descubre todo lo que puedes hacer en esta web +
+ Utilízalo libremente o ayúdanos a mejorarlo, es software libre +
+ Participación Ciudadana, Transparencia y Gobierno Abierto +
+ Crea tus propias propuestas +
+ Envía tus propuestas de gasto +
+ Sistemas de participación ciudadana que ya existen en el mundo +
+ Para perderle el miedo +
+ Preguntas frecuentes sobre problemas técnicos +
+ Hojas de firmas para recoger apoyos +
Welcome to sandbox
+ ++ <% @templates.each do |template| %> +- <%= link_to template, "/sandbox/" + template %>
+ <% end %>
+
++ Licencias urbanísticas, declaraciones responsables y comunicaciones previas +
+Suscrito
+ +En qué consiste
+Se va a modificar la regulación del procedimiento para la autorización de obras y la apertura de locales comerciales o empresariales para simplificar y agilizar trámites.
+A quién va dirigido
++- Ciudadanos con vivienda en propiedad
+ - Profesionales de la construcción y reformas
+ - Empresarios con locales comerciales
+
+Cómo puedes participar
++- Participa en el debate previo para identificar los problemas a solucionar, la necesidad de esta normativa, sus objetivos y posibles soluciones alternativas.
+ - Después del debate el Ayuntamiento presentará un borrador del texto al cual podrás realizar comentarios y alegaciones.
+
+En qué consiste
+El ayuntamiento de Madrid somete...
+Estás viendo la revisión
+TÍTULO PRELIMINAR. Objeto y definiciones
+Artículo 1.
+1.Esta Ordenanza tiene por objeto establecer las condiciones generales que deben cumplir los distintos elementos integrados en el denominado mobiliario urbano, tanto en lo que se refiere a su emplazamiento como a las características propias de dichos elementos.
+2.Para determinar las condiciones relativas a la explotación del mobiliario urbano y a los requisitos que, en su caso, hayan de reunir los respectivos titulares, se estará a la normativa específica establecida para los distintos elementos y a las señaladas en el título que autorice su instalación y funcionamiento.
+3.El mobiliario urbano deberá cumplir los criterios estéticos y de diseño para las distintas zonas de Madrid, aprobados por el titular del Área de Gobierno competente en materia de mobiliario urbano. En aquellos casos en los que la normativa aplicable así lo exija, deberá obtenerse dictamen de la comisión competente en materia de protección del patrimonio histórico y artístico.
+(redacción dada por la Ordenanza de 30 de marzo 2011 de Adaptación al ámbito de la Ciudad de Madrid de las previsiones contenidas en la normativa estatal y autonómica de transposición de la Directiva 2006/123/CE)
+ +Artículo 2.
+1. A los efectos de esta Ordenanza se entiende por mobiliario urbano el conjunto de instalaciones o elementos que ocupan un espacio público, y cuya finalidad sea la de atender una necesidad social o prestar un determinado servicio al vecindario.
+2.En el concepto indicado estarán incluidas tanto las instalaciones y elementos de titularidad pública, explotados directamente o por concesión: bancos, bolardos, marquesinas, papeleras, etcétera, como los colocados por particulares, previa autorización municipal, tales como quioscos o puestos fijos, de temporada u ocasionales.
+(apartado 2 del art. 2, redacción dada por la Ordenanza de 30 de marzo 2011 de Adaptación al ámbito de la Ciudad de Madrid de las previsiones contenidas en la normativa estatal y autonómica de transposición de la Directiva 2006/123/ CE)
+ +Artículo 3.
+Constituirá criterio general para la implantación de mobiliario urbano la armonización de las finalidades asignadas al mismo con las funciones generales de los espacios públicos, la coordinación de los distintos elementos procurando, cuando fuera posible, la polivalencia de cada uno de ellos para evitar la ocupación intensiva de aquellos espacios y la adecuación, tanto por su emplazamiento como por su diseño, al entono urbano en que se localicen.
+TÍTULO I. DE LOS EMPLAZAMIENTOS DE MOBILIARIO URBANO
+ +Artículo 4.
+Cuando se trate de mobiliario urbano de titularidad pública, el número, localización y características de sus emplazamientos estará determinado en el correspondiente acuerdo de implantación o bases de concesión, si fuera municipal, y en el de autorización, si fuese promovido por otras entidades.
+ +Artículo 5.
+ +Los interesados tendrán reconocidos específicamente, además de los establecidos con carácter general en otras normas, los siguientes derechos: +
1. Iniciar inmediatamente el ejercicio de la actividad cuando esté sometida a declaración responsable, o una vez obtenida la licencia de primera ocupación y funcionamiento.
+2. Obtener la licencia de actividad y funcionamiento en el plazo establecido en cada caso, sin perjuicio de los efectos establecidos en la legislación aplicable en el ámbito del silencio administrativo.
+3. No presentar documentación que obre en poder de los servicios municipales
+4. Presentar quejas, reclamaciones y sugerencias sobre el funcionamiento de los servicios municipales y de las entidades colaboradoras urbanísticas.
+ +Artículo 6.
+1. Durante la realización de las obras o la implantación de la actividad y previamente al acto de comprobación, no se considerarán modificación de la licencia o de la declaración responsable las alteraciones que se hayan producido en el edificio, local o sus instalaciones cuando las mismas se ajusten a la normativa que las regula, salvo que afecten a las condiciones de volumen y forma de los edificios, a la posición y ocupación del edificio en la parcela, a la edificabilidad o superficie del local, al número de locales. Si no se ajustasen se aplicarán los mecanismos de restablecimiento de la legalidad urbanística.
+Se considerará modificación el cambio de actividad, salvo que la nueva actividad o la inicial con la incorporación de alguna complementaria de ella, tenga la misma consideración y exigencias urbanísticas, ambientales, de seguridad y salubridad. Las variaciones producidas se relacionarán en el acta de comprobación, sin necesidad de tramitar licencia o declaración aparte, quedando legalizadas con la concesión de la licencia de primera ocupación y funcionamiento o el acto de comprobación posterior. Cuando las variaciones se hayan concretado en obras que requieran proyecto de obras de edificación de acuerdo con el artículo 2.2 de la Ley de Ordenación de la Edificación se incorporará el correspondiente proyecto modificado.
+2. Durante el ejercicio de las actividades con licencia o declaración responsable, y con las mismas salvedades indicadas en el punto 1, no se considerará modificación de la licencia o declaración las variaciones que se hayan producido en la actividad, el local o sus instalaciones cuando no alteren las condiciones de repercusión ambiental, seguridad o salubridad por debajo de las exigencias técnicas establecidas para las mismas por la normativa vigente. Tampoco se considerará modificación el cambio de actividad o la incorporación de alguna complementaria a la misma cuando esta tenga la misma consideración y exigencias urbanísticas, ambientales, de seguridad y salubridad que la primera.
+Las obras que han dado lugar a estas variaciones se legalizarán a través de licencia o declaración responsable, dependiendo de la entidad de las mismas. La licencia que se conceda o la declaración se limitarán a recoger el contenido de la modificación, haciendo referencia a la licencia del establecimiento.
+3. En el caso de actividades de espectáculos públicos y recreativas también se considerará modificación de licenciao declaración responsable el cambio de actividad de las indicadas en el Catálogo, así como el incremento del aforo.
+4. Cuando la modificación sea requerida de oficio, el requerimiento indicará las alteraciones existentes, motivando la necesidad de la modificación de la licencia o de la declaración responsable.
+5. Durante la ejecución de las obras autorizadas no precisarán modificación de licencia las variaciones en el número de plazas de aparcamiento que no supongan disminución de la dotación obligatoria de servicio del edificio, sin perjuicio de su constancia documentada en el expediente.
+Mi comentario va encaminado a que no nos equivoquemos al pensar que esta es una opci...
+Mi comentario va encaminado a que no nos equivoquemos al pensar que esta es una opci...
+Mi comentario va encaminado a que no nos equivoquemos al pensar que esta es una opci...
+Mi comentario va encaminado a que no nos equivoquemos al pensar que esta es una opci...
+Mi comentario va encaminado a que no nos equivoquemos al pensar que esta es una opci...
+Mi comentario va encaminado a que no nos equivoquemos al pensar que esta es una opci...
+En la actualidad, el número de autobuses de la EMT suman 1.900 de los cuales 740 funcionan con gas natural (el 42%), con los 170 que se van a adquirir en 2016, el cómputo sube a 900, lo que significa casi el 50% del total. Con tecnología híbrida existen 23 y se prevé comprar 30 más, lo que supone 53 vehículos, y hay 20 minibuses eléctricos puros. Por último, el 80% de los autobuses de diésel están catalizados a niveles euro4 y euro5 (470), “con lo que podríamos asegurar que el 78% de nuestra flota es verde.
+En la actualidad, el número de autobuses de la EMT suman 1.900 de los cuales 740 funcionan con gas natural (el 42%), con los 170 que se van a adquirir en 2016, el cómputo sube a 900, lo que significa casi el 50% del total. Con tecnología híbrida existen 23 y se prevé comprar 30 más, lo que supone 53 vehículos, y hay 20 minibuses eléctricos puros. Por último, el 80% de los autobuses de diésel están catalizados a niveles euro4 y euro5 (470), “con lo que podríamos asegurar que el 78% de nuestra flota es verde.
+Mi comentario va encaminado a que no nos equivoquemos al pensar que esta es una opci...
+Mi comentario va encaminado a que no nos equivoquemos al pensar que esta es una opci...
+Colabora en la elaboración de la normativa sobre
++ Licencias urbanísticas, declaraciones responsables y comunicaciones previas +
+Suscríbete al proceso
+Recibe notificaciones clave sobre el proceso
+ +En qué consiste
+Se va a modificar la regulación del procedimiento para la autorización de obras y la apertura de locales comerciales o empresariales para simplificar y agilizar trámites.
+A quién va dirigido
++- Ciudadanos con vivienda en propiedad
+ - Profesionales de la construcción y reformas
+ - Empresarios con locales comerciales
+
+Cómo puedes participar
++- Participa en el debate previo para identificar los problemas a solucionar, la necesidad de esta normativa, sus objetivos y posibles soluciones alternativas.
+ - Después del debate el Ayuntamiento presentará un borrador del texto al cual podrás realizar comentarios y alegaciones.
+
+En qué consiste
+1.Esta Ordenanza tiene por objeto establecer las condiciones generales que deben cumplir los distintos elementos integrados en el denominado mobiliario urbano, tanto en lo que se refiere a su emplazamiento como a las características propias de dichos elementos.
+ +2.Para determinar las condiciones relativas a la explotación del mobiliario urbano y a los requisitos que, en su caso, hayan de reunir los respectivos titulares, se estará a la normativa específica establecida para los distintos elementos y a las señaladas en el título que autorice su instalación y funcionamiento.
+ +Preámbulo
+3.El mobiliario urbano deberá cumplir los criterios estéticos y de diseño para las distintas zonas de Madrid, aprobados por el titular del Área de Gobierno competente en materia de mobiliario urbano. En aquellos casos en los que la normativa aplicable así lo exija, deberá obtenerse dictamen de la comisión competente en materia de protección del patrimonio histórico y artístico.
+ +(redacción dada por la Ordenanza de 30 de marzo 2011 de Adaptación al ámbito de la Ciudad de Madrid de las previsiones contenidas en la normativa estatal y autonómica de transposición de la Directiva 2006/123/CE)
+¿Qué medidas podría adoptar el Ayuntamiento de Madrid para facilitar y agilizar la autorización para la realización de obras y la apertura de locales comerciales o empresariales?
+¿En el ámbito de las autorizaciones urbanísticas, es partidario de que se modifique el papel que juegan las empresas privadas acreditadas (ECU) para que también puedan facilitar las gestiones en las obras que realicen los particulares en sus viviendas u otras actuaciones similares?
+¿Qué medidas podría adoptar el Ayuntamiento de Madrid para facilitar y agilizar la autorización para la realización de obras y la apertura de locales comerciales o empresariales?
+¿En el ámbito de las autorizaciones urbanísticas, es partidario de que se modifique el papel que juegan las empresas privadas acreditadas (ECU) para que también puedan facilitar las gestiones en las obras que realicen los particulares en sus viviendas u otras actuaciones similares?
+¿Qué medidas podría adoptar el Ayuntamiento de Madrid para facilitar y agilizar la autorización para la realización de obras y la apertura de locales comerciales o empresariales?
+¿En el ámbito de las autorizaciones urbanísticas, es partidario de que se modifique el papel que juegan las empresas privadas acreditadas (ECU) para que también puedan facilitar las gestiones en las obras que realicen los particulares en sus viviendas u otras actuaciones similares?
+Proceso de legislación colaborativa
+Licencias urbanísticas, declaraciones responsables y comunicaciones previas
+¿Considera necesario realizar una nueva regulación para facilitar y simplificar las obras en viviendas, o la modificación y apertura de locales comerciales o empresariales?
++ +
Colabora en la elaboración de la normativa sobre
++ Licencias urbanísticas, declaraciones responsables y comunicaciones previas +
+Suscríbete al proceso
+Recibe notificaciones clave sobre el proceso
+ +En qué consiste
+Se va a modificar la regulación del procedimiento para la autorización de obras y la apertura de locales comerciales o empresariales para simplificar y agilizar trámites.
+A quién va dirigido
++- Ciudadanos con vivienda en propiedad
+ - Profesionales de la construcción y reformas
+ - Empresarios con locales comerciales
+
+Cómo puedes participar
++- Participa en el debate previo para identificar los problemas a solucionar, la necesidad de esta normativa, sus objetivos y posibles soluciones alternativas.
+ - Después del debate el Ayuntamiento presentará un borrador del texto al cual podrás realizar comentarios y alegaciones.
+
+En qué consiste
+El ayuntamiento de Madrid somete...
+Esta fase del proceso todavía no está abierta
+Suscríbete al proceso para recibir un aviso en el momento en que se abra.
++ Licencias urbanísticas, declaraciones responsables y comunicaciones previas +
+Suscrito
+ +Estás viendo la revisión
+El 12 nov 2016 se publica un nuevo borrador de la normativa sobre Licencias urbanísticas, que incorpora las aportaciones de la ciudadanía realizadas a través de la web de decide.madrid.es. A continuación se detallan los principales cambios que se han introducido en el texto:
+ ++- Se detalla el ámbito de actuación ver apartado
+ - Se reescribe la introducción al Capítulo I para incorporar sugerencias ciudadanas ver apartado
+ - Se detalla el ámbito de actuación ver apartado
+ - Se reescribe la introducción al Capítulo I para incorporar sugerencias ciudadanas ver apartado
+ - Se detalla el ámbito de actuación ver apartado
+ - Se reescribe la introducción al Capítulo I para incorporar sugerencias ciudadanas ver apartado
+
++ Licencias urbanísticas, declaraciones responsables y comunicaciones previas +
+Suscrito
+ +Comentarios de la versión
++ Licencias urbanísticas, declaraciones responsables y comunicaciones previas +
+Suscrito
+ +Comentarios de la versión
++ PROCESOS DESTACADOS +
++
+
+
+
+Procesos activos
Próximamente
Terminados
Licencias urbanísticas, declaraciones responsables y comunicaciones previas
+Se va a modificar la regulación del procedimiento para la autorización de obras y la apertura de locales comerciales o empresariales para simplificar y agilizar trámites
+Fechas clave:
+Debate previo
+15 nov 2016 - 15 dic 2016
+Publicación borrador
+1 dic 2016
+Alegaciones
+1 dic 2016 - 15 dic 2016
+Publicación resultados
+15 feb 2017
+Title 1
+ +Some paragraph.
+ +A list:
+ ++- item 1
+- item 2
+
+ +Subtitle
+ +Another paragraph.
+ +Title 2
+ +Something about this.
+BODY_HTML + end + + def toc_html +<<-TOC_HTML ++-
+Title 1
+
+-
+Title 2
+
+
+TOC_HTML + end +end diff --git a/spec/models/legislation/process_spec.rb b/spec/models/legislation/process_spec.rb new file mode 100644 index 000000000..f89a4bfcf --- /dev/null +++ b/spec/models/legislation/process_spec.rb @@ -0,0 +1,251 @@ +require 'rails_helper' + +RSpec.describe Legislation::Process, type: :model do + let(:process) { create(:legislation_process) } + + it "should be valid" do + expect(process).to be_valid + end + + describe "dates validations" do + it "is invalid if debate_start_date is present but debate_end_date is not" do + process = build(:legislation_process, debate_start_date: Date.current, debate_end_date: "") + expect(process).to be_invalid + expect(process.errors.messages[:debate_end_date]).to include("can't be blank") + end + + it "is invalid if debate_end_date is present but debate_start_date is not" do + process = build(:legislation_process, debate_start_date: nil, debate_end_date: Date.current) + expect(process).to be_invalid + expect(process.errors.messages[:debate_start_date]).to include("can't be blank") + end + + it "is invalid if allegations_start_date is present but debate_end_date is not" do + process = build(:legislation_process, allegations_start_date: Date.current, allegations_end_date: "") + expect(process).to be_invalid + expect(process.errors.messages[:allegations_end_date]).to include("can't be blank") + end + + it "is invalid if debate_end_date is present but allegations_start_date is not" do + process = build(:legislation_process, allegations_start_date: nil, allegations_end_date: Date.current) + expect(process).to be_invalid + expect(process.errors.messages[:allegations_start_date]).to include("can't be blank") + end + end + + describe "date ranges validations" do + it "is invalid if end_date is before start_date" do + process = build(:legislation_process, start_date: Date.current, end_date: Date.current - 1.day) + expect(process).to be_invalid + expect(process.errors.messages[:end_date]).to include("must be on or after the start date") + end + + it "is valid if end_date is the same as start_date" do + process = build(:legislation_process, start_date: Date.current - 1.day, end_date: Date.current - 1.day) + expect(process).to be_valid + end + + it "is invalid if debate_end_date is before debate start_date" do + process = build(:legislation_process, debate_start_date: Date.current, debate_end_date: Date.current - 1.day) + expect(process).to be_invalid + expect(process.errors.messages[:debate_end_date]).to include("must be on or after the debate start date") + end + + it "is valid if debate_end_date is the same as debate_start_date" do + process = build(:legislation_process, debate_start_date: Date.current - 1.day, debate_end_date: Date.current - 1.day) + expect(process).to be_valid + end + + it "is invalid if allegations_end_date is before allegations_start_date" do + process = build(:legislation_process, allegations_start_date: Date.current, allegations_end_date: Date.current - 1.day) + expect(process).to be_invalid + expect(process.errors.messages[:allegations_end_date]).to include("must be on or after the allegations start date") + end + + it "is valid if allegations_end_date is the same as allegations_start_date" do + process = build(:legislation_process, allegations_start_date: Date.current - 1.day, allegations_end_date: Date.current - 1.day) + expect(process).to be_valid + end + end + + describe "filter scopes" do + before(:each) do + @process_1 = create(:legislation_process, start_date: Date.current - 2.days, end_date: Date.current + 1.day) + @process_2 = create(:legislation_process, start_date: Date.current + 1.days, end_date: Date.current + 3.days) + @process_3 = create(:legislation_process, start_date: Date.current - 4.days, end_date: Date.current - 3.days) + end + + it "filters open" do + open_processes = ::Legislation::Process.open + + expect(open_processes).to include(@process_1) + expect(open_processes).to_not include(@process_2) + expect(open_processes).to_not include(@process_3) + end + + it "filters next" do + next_processes = ::Legislation::Process.next + + expect(next_processes).to include(@process_2) + expect(next_processes).to_not include(@process_1) + expect(next_processes).to_not include(@process_3) + end + + it "filters past" do + past_processes = ::Legislation::Process.past + + expect(past_processes).to include(@process_3) + expect(past_processes).to_not include(@process_2) + expect(past_processes).to_not include(@process_1) + end + end + + describe "#open_phase?" do + it "checks debate phase" do + # future + process.update_attributes(debate_start_date: Date.current + 2.days, debate_end_date: Date.current + 3.days) + expect(process.open_phase?(:debate)).to be false + + # started + process.update_attributes(debate_start_date: Date.current - 2.days, debate_end_date: Date.current + 1.day) + expect(process.open_phase?(:debate)).to be true + + # starts today + process.update_attributes(debate_start_date: Date.current, debate_end_date: Date.current + 1.day) + expect(process.open_phase?(:debate)).to be true + + # past + process.update_attributes(debate_start_date: Date.current - 2.days, debate_end_date: Date.current - 1.day) + expect(process.open_phase?(:debate)).to be false + end + + it "checks allegations phase" do + + # future + process.update_attributes(allegations_start_date: Date.current + 2.days, allegations_end_date: Date.current + 3.days) + expect(process.open_phase?(:allegations)).to be false + + # started + process.update_attributes(allegations_start_date: Date.current - 2.days, allegations_end_date: Date.current + 1.day) + expect(process.open_phase?(:allegations)).to be true + + # starts today + process.update_attributes(allegations_start_date: Date.current, allegations_end_date: Date.current + 1.day) + expect(process.open_phase?(:allegations)).to be true + + # past + process.update_attributes(allegations_start_date: Date.current - 2.days, allegations_end_date: Date.current - 1.day) + expect(process.open_phase?(:allegations)).to be false + end + + it "checks draft publication phase" do + # future + process.update_attributes(draft_publication_date: Date.current + 2.days) + expect(process.open_phase?(:draft_publication)).to be false + + # past + process.update_attributes(draft_publication_date: Date.current - 2.days) + expect(process.open_phase?(:draft_publication)).to be true + + # starts today + process.update_attributes(draft_publication_date: Date.current) + expect(process.open_phase?(:draft_publication)).to be true + end + + it "checks final version publication phase" do + # future + process.update_attributes(final_publication_date: Date.current + 2.days) + expect(process.open_phase?(:final_version_publication)).to be false + + # past + process.update_attributes(final_publication_date: Date.current - 2.days) + expect(process.open_phase?(:final_version_publication)).to be true + + # starts today + process.update_attributes(final_publication_date: Date.current) + expect(process.open_phase?(:final_version_publication)).to be true + end + end + + describe "#show_phase?" do + it "checks debate phase" do + # future + process.update_attributes(debate_start_date: Date.current + 2.days, debate_end_date: Date.current + 3.days) + expect(process.show_phase?(:debate)).to be false + + # started + process.update_attributes(debate_start_date: Date.current - 2.days, debate_end_date: Date.current + 1.day) + expect(process.show_phase?(:debate)).to be true + + # starts today + process.update_attributes(debate_start_date: Date.current, debate_end_date: Date.current + 1.day) + expect(process.show_phase?(:debate)).to be true + + # past + process.update_attributes(debate_start_date: Date.current - 2.days, debate_end_date: Date.current - 1.day) + expect(process.show_phase?(:debate)).to be true + end + + it "checks allegations phase" do + # future + process.update_attributes(allegations_start_date: Date.current + 2.days, allegations_end_date: Date.current + 3.days) + expect(process.show_phase?(:allegations)).to be false + + # started + process.update_attributes(allegations_start_date: Date.current - 2.days, allegations_end_date: Date.current + 1.day) + expect(process.show_phase?(:allegations)).to be true + + # starts today + process.update_attributes(allegations_start_date: Date.current, allegations_end_date: Date.current + 1.day) + expect(process.show_phase?(:allegations)).to be true + + # past + process.update_attributes(allegations_start_date: Date.current - 2.days, allegations_end_date: Date.current - 1.day) + expect(process.show_phase?(:allegations)).to be true + end + + it "checks draft publication phase" do + # future + process.update_attributes(draft_publication_date: Date.current + 2.days) + expect(process.show_phase?(:draft_publication)).to be false + + # past + process.update_attributes(draft_publication_date: Date.current - 2.days) + expect(process.show_phase?(:draft_publication)).to be true + + # starts today + process.update_attributes(draft_publication_date: Date.current) + expect(process.show_phase?(:draft_publication)).to be true + end + + it "checks final version publication phase" do + # future + process.update_attributes(final_publication_date: Date.current + 2.days) + expect(process.show_phase?(:final_version_publication)).to be false + + # past + process.update_attributes(final_publication_date: Date.current - 2.days) + expect(process.show_phase?(:final_version_publication)).to be true + + # starts today + process.update_attributes(final_publication_date: Date.current) + expect(process.show_phase?(:final_version_publication)).to be true + end + end + + describe "#status" do + it "should detect planned phase" do + process.update_attributes(start_date: Date.current + 2.days) + expect(process.status).to eq(:planned) + end + + it "should detect closed phase" do + process.update_attributes(end_date: Date.current - 2.days) + expect(process.status).to eq(:closed) + end + + it "should detect open phase" do + expect(process.status).to eq(:open) + end + end +end diff --git a/spec/models/legislation/question_option_spec.rb b/spec/models/legislation/question_option_spec.rb new file mode 100644 index 000000000..37b64c07e --- /dev/null +++ b/spec/models/legislation/question_option_spec.rb @@ -0,0 +1,18 @@ +require 'rails_helper' + +RSpec.describe Legislation::QuestionOption, type: :model do + let(:legislation_question_option) { build(:legislation_question_option) } + + it "should be valid" do + expect(legislation_question_option).to be_valid + end + + it "should be unique per question" do + question = create(:legislation_question) + valid_question_option = create(:legislation_question_option, question: question, value: "uno") + + invalid_question_option = build(:legislation_question_option, question: question, value: "uno") + + expect(invalid_question_option).to_not be_valid + end +end diff --git a/spec/models/legislation/question_spec.rb b/spec/models/legislation/question_spec.rb new file mode 100644 index 000000000..216b9cbfd --- /dev/null +++ b/spec/models/legislation/question_spec.rb @@ -0,0 +1,62 @@ +require 'rails_helper' + +RSpec.describe Legislation::Question, type: :model do + let(:question) { create(:legislation_question) } + + it "should be valid" do + expect(question).to be_valid + end + + context "can be deleted" do + example "when it has no options or answers" do + question = create(:legislation_question) + + expect do + question.destroy + end.to change { Legislation::Question.count }.by(-1) + end + + example "when it has options but no answers" do + create(:legislation_question_option, question: question, value: "Yes") + create(:legislation_question_option, question: question, value: "No") + + expect do + question.destroy + end.to change { Legislation::Question.count }.by(-1) + end + + example "when it has options and answers" do + option_1 = create(:legislation_question_option, question: question, value: "Yes") + option_2 = create(:legislation_question_option, question: question, value: "No") + create(:legislation_answer, question: question, question_option: option_1) + create(:legislation_answer, question: question, question_option: option_2) + + expect do + question.destroy + end.to change { Legislation::Question.count }.by(-1) + end + end + + describe "#next_question_id" do + let!(:question1) { create(:legislation_question) } + let!(:question2) { create(:legislation_question, legislation_process_id: question1.legislation_process_id) } + + it "should return the next question" do + expect(question1.next_question_id).to eq(question2.id) + end + + it "should return nil" do + expect(question2.next_question_id).to be_nil + end + end + + describe "#first_question_id" do + let!(:question1) { create(:legislation_question) } + let!(:question2) { create(:legislation_question, legislation_process_id: question1.legislation_process_id) } + + it "should return the first question" do + expect(question1.first_question_id).to eq(question1.id) + expect(question2.first_question_id).to eq(question1.id) + end + end +end+-
+Subtitle
+
+
+