diff --git a/.editorconfig b/.editorconfig index 0e3ab2a51..619c27623 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,3 +11,6 @@ insert_final_newline = true [*.md] trim_trailing_whitespace = false + +[*.yml] +insert_final_newline = false diff --git a/.rubocop.yml b/.rubocop.yml index e2cc2bf93..49de3550b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,6 +1,8 @@ inherit_from: .rubocop_todo.yml AllCops: + DisplayCopNames: true + DisplayStyleGuide: true Include: - '**/Rakefile' - '**/config.ru' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 288c964c1..373bb213c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,12 +1,12 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2017-07-07 21:23:30 +0200 using RuboCop version 0.49.1. +# on 2017-10-17 22:05:23 +0200 using RuboCop version 0.49.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 45 +# Offense count: 40 # Cop supports --auto-correct. # Configuration parameters: EnforcedHashRocketStyle, SupportedHashRocketStyles, EnforcedColonStyle, SupportedColonStyles, EnforcedLastArgumentHashStyle, SupportedLastArgumentHashStyles. # SupportedHashRocketStyles: key, separator, table @@ -14,11 +14,10 @@ # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit Layout/AlignHash: Exclude: - - 'app/controllers/officing/results_controller.rb' - 'spec/controllers/legislation/annotations_controller_spec.rb' - 'spec/features/admin/banners_spec.rb' -# Offense count: 50 +# Offense count: 52 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. # SupportedStyles: with_first_parameter, with_fixed_indentation @@ -33,7 +32,7 @@ Layout/ClosingParenthesisIndentation: - 'spec/models/legislation/annotation_spec.rb' - 'spec/rails_helper.rb' -# Offense count: 51 +# Offense count: 37 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: leading, trailing @@ -68,11 +67,10 @@ Layout/EmptyLines: Exclude: - 'app/controllers/admin/budget_investment_milestones_controller.rb' -# Offense count: 2 +# Offense count: 1 # Cop supports --auto-correct. Layout/EmptyLinesAroundMethodBody: Exclude: - - 'app/models/abilities/administrator.rb' - 'lib/graph_ql/api_types_creator.rb' # Offense count: 2 @@ -126,13 +124,13 @@ Layout/IndentationConsistency: - 'spec/models/legislation/draft_version_spec.rb' - 'spec/models/proposal_spec.rb' -# Offense count: 23 +# Offense count: 48 # Cop supports --auto-correct. # Configuration parameters: Width, IgnoredPatterns. Layout/IndentationWidth: Enabled: false -# Offense count: 7 +# Offense count: 6 # Cop supports --auto-correct. Layout/LeadingCommentSpace: Exclude: @@ -140,7 +138,6 @@ Layout/LeadingCommentSpace: - 'app/controllers/budgets/ballot/lines_controller.rb' - 'spec/features/budgets/ballots_spec.rb' - 'spec/features/comments/poll_questions_spec.rb' - - 'spec/features/officing/voters_spec.rb' - 'spec/support/common_actions.rb' # Offense count: 3 @@ -181,7 +178,7 @@ Layout/MultilineMethodCallBraceLayout: - 'spec/models/legislation/annotation_spec.rb' - 'spec/rails_helper.rb' -# Offense count: 71 +# Offense count: 59 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. # SupportedStyles: aligned, indented, indented_relative_to_receiver @@ -201,7 +198,7 @@ Layout/MultilineMethodCallIndentation: - 'spec/models/proposal_spec.rb' - 'spec/models/user_spec.rb' -# Offense count: 8 +# Offense count: 7 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. # SupportedStyles: aligned, indented @@ -264,29 +261,11 @@ Lint/LiteralInCondition: Exclude: - 'app/models/budget/investment.rb' -# Offense count: 51 -Lint/ParenthesesAsGroupedExpression: - Exclude: - - 'spec/factories.rb' - - 'spec/features/admin/organizations_spec.rb' - - 'spec/features/budgets/investments_spec.rb' - - 'spec/features/campaigns_spec.rb' - - 'spec/features/debates_spec.rb' - - 'spec/features/management/managed_users_spec.rb' - - 'spec/features/management/proposals_spec.rb' - - 'spec/features/management/spending_proposals_spec.rb' - - 'spec/features/management/users_spec.rb' - - 'spec/features/proposals_spec.rb' - - 'spec/models/debate_spec.rb' - -# Offense count: 13 +# Offense count: 3 # Cop supports --auto-correct. Lint/StringConversionInInterpolation: Exclude: - - 'app/models/poll/null_result.rb' - 'app/models/poll/partial_result.rb' - - 'app/models/poll/white_result.rb' - - 'app/models/poll/total_result.rb' # Offense count: 15 # Cop supports --auto-correct. @@ -311,7 +290,7 @@ Lint/UnusedMethodArgument: - 'app/mailers/mailer.rb' - 'app/models/abilities/everyone.rb' -# Offense count: 278 +# Offense count: 325 Lint/UselessAssignment: Enabled: false @@ -320,35 +299,46 @@ Lint/Void: Exclude: - 'app/controllers/polls_controller.rb' -# Offense count: 74 +# Offense count: 86 Metrics/AbcSize: - Max: 54 + Max: 64 -# Offense count: 454 +# Offense count: 487 # Configuration parameters: CountComments, ExcludedMethods. Metrics/BlockLength: - Max: 1071 - -# Offense count: 8 -# Configuration parameters: CountComments. -Metrics/ClassLength: - Max: 256 + Max: 1227 # Offense count: 10 +# Configuration parameters: CountComments. +Metrics/ClassLength: + Max: 262 + +# Offense count: 13 Metrics/CyclomaticComplexity: Max: 10 -# Offense count: 53 +# Offense count: 25 +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. +# URISchemes: http, https +Metrics/LineLength: + Max: 248 + +# Offense count: 67 # Configuration parameters: CountComments. Metrics/MethodLength: - Max: 49 + Max: 56 # Offense count: 1 # Configuration parameters: CountComments. Metrics/ModuleLength: - Max: 214 + Max: 242 -# Offense count: 7 +# Offense count: 3 +# Configuration parameters: CountKeywordArgs. +Metrics/ParameterLists: + Max: 7 + +# Offense count: 8 Metrics/PerceivedComplexity: Max: 11 @@ -402,20 +392,20 @@ Rails/HttpPositionalArguments: - 'spec/controllers/pages_controller_spec.rb' - 'spec/controllers/users/registrations_controller_spec.rb' -# Offense count: 20 +# Offense count: 18 Rails/OutputSafety: Exclude: - 'app/controllers/admin/legislation/draft_versions_controller.rb' - 'app/controllers/admin/legislation/processes_controller.rb' - 'app/controllers/admin/legislation/questions_controller.rb' - 'app/controllers/budgets/investments_controller.rb' + - 'app/controllers/direct_uploads_controller.rb' - 'app/controllers/spending_proposals_controller.rb' - 'app/helpers/application_helper.rb' - 'app/helpers/text_with_links_helper.rb' - - 'app/helpers/users_helper.rb' - 'app/helpers/valuation_helper.rb' -# Offense count: 70 +# Offense count: 71 # Configuration parameters: Blacklist. # Blacklist: decrement!, decrement_counter, increment!, increment_counter, toggle!, touch, update_all, update_attribute, update_column, update_columns, update_counters Rails/SkipsModelValidations: @@ -431,7 +421,7 @@ Style/AccessorMethodName: - 'app/controllers/proposals_controller.rb' - 'lib/merged_comment_tree.rb' -# Offense count: 1 +# Offense count: 8 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: braces, no_braces, context_dependent @@ -441,7 +431,7 @@ Style/BracesAroundHashParameters: - 'spec/features/budgets/investments_spec.rb' - 'spec/features/proposals_spec.rb' -# Offense count: 119 +# Offense count: 123 # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: nested, compact Style/ClassAndModuleChildren: @@ -454,20 +444,13 @@ Style/ClassVars: - 'app/models/organization.rb' - 'app/models/user.rb' -# Offense count: 6 -# Cop supports --auto-correct. -Style/ColonMethodCall: - Exclude: - - 'spec/models/budget/investment_spec.rb' - -# Offense count: 12 +# Offense count: 8 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly, IncludeTernaryExpressions. # SupportedStyles: assign_to_condition, assign_inside_condition Style/ConditionalAssignment: Exclude: - 'app/controllers/admin/poll/booth_assignments_controller.rb' - - 'app/controllers/admin/poll/officer_assignments_controller.rb' - 'app/controllers/admin/poll/questions_controller.rb' - 'app/controllers/comments_controller.rb' - 'app/controllers/management/spending_proposals_controller.rb' @@ -480,12 +463,6 @@ Style/DoubleNegation: Exclude: - 'app/models/flag.rb' -# Offense count: 1 -# Cop supports --auto-correct. -Style/EmptyCaseCondition: - Exclude: - - 'app/models/concerns/verification.rb' - # Offense count: 2 # Configuration parameters: ExpectMatchingDefinition, Regex, IgnoreExecutableScripts, AllowedAcronyms. # AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS @@ -499,7 +476,7 @@ Style/FileName: Style/GuardClause: Enabled: false -# Offense count: 12 +# Offense count: 11 # Cop supports --auto-correct. # Configuration parameters: MaxLineLength. Style/IfUnlessModifier: @@ -510,37 +487,23 @@ Style/IfUnlessModifier: - 'app/controllers/legislation/annotations_controller.rb' - 'app/controllers/valuation/budget_investments_controller.rb' - 'app/controllers/verification/letter_controller.rb' - - 'app/controllers/welcome_controller.rb' - 'app/helpers/embed_videos_helper.rb' - 'app/mailers/mailer.rb' - 'app/models/proposal.rb' - 'app/models/spending_proposal.rb' -# Offense count: 4 +# Offense count: 5 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: line_count_dependent, lambda, literal Style/Lambda: Exclude: - 'app/models/comment.rb' + - 'app/models/concerns/followable.rb' - 'app/models/direct_message.rb' - 'app/models/vote.rb' - 'lib/graph_ql/api_types_creator.rb' -# Offense count: 1 -# Cop supports --auto-correct. -Style/MethodCallWithoutArgsParentheses: - Exclude: - - 'app/controllers/management/document_verifications_controller.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: require_parentheses, require_no_parentheses, require_no_parentheses_except_multiline -Style/MethodDefParentheses: - Exclude: - - 'spec/helpers/comments_helper_spec.rb' - # Offense count: 1 Style/MultilineBlockChain: Exclude: @@ -552,17 +515,14 @@ Style/MultilineIfThen: Exclude: - 'app/controllers/management/users_controller.rb' -# Offense count: 15 +# Offense count: 13 # Cop supports --auto-correct. Style/MutableConstant: Exclude: - 'app/models/activity.rb' - 'app/models/budget/reclassified_vote.rb' - 'app/models/legislation/draft_version.rb' - - 'app/models/poll/null_result.rb' - 'app/models/poll/partial_result.rb' - - 'app/models/poll/white_result.rb' - - 'app/models/poll/total_result.rb' - 'app/models/proposal.rb' - 'app/models/signature_sheet.rb' - 'app/models/site_customization/content_block.rb' @@ -572,32 +532,13 @@ Style/MutableConstant: - 'lib/tag_sanitizer.rb' - 'lib/wysiwyg_sanitizer.rb' -# Offense count: 29 -# Cop supports --auto-correct. -Style/NestedParenthesizedCalls: - Exclude: - - 'spec/features/debates_spec.rb' - - 'spec/features/emails_spec.rb' - - 'spec/features/valuation/budget_investments_spec.rb' - - 'spec/features/valuation/spending_proposals_spec.rb' - - 'spec/helpers/settings_helper_spec.rb' - - 'spec/helpers/verification_helper_spec.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles. -# SupportedStyles: skip_modifier_ifs, always -Style/Next: - Exclude: - - 'app/controllers/officing/results_controller.rb' - # Offense count: 54 # Cop supports --auto-correct. # Configuration parameters: Strict. Style/NumericLiterals: MinDigits: 9 -# Offense count: 19 +# Offense count: 20 # Cop supports --auto-correct. # Configuration parameters: AutoCorrect, EnforcedStyle, SupportedStyles. # SupportedStyles: predicate, comparison @@ -625,16 +566,7 @@ Style/ParallelAssignment: - 'lib/active_model/dates.rb' - 'spec/support/common_actions.rb' -# Offense count: 3 -# Cop supports --auto-correct. -# Configuration parameters: AllowSafeAssignment. -Style/ParenthesesAroundCondition: - Exclude: - - 'app/controllers/proposals_controller.rb' - - 'app/models/debate.rb' - - 'app/models/proposal.rb' - -# Offense count: 11 +# Offense count: 10 # Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist. # NamePrefix: is_, has_, have_ # NamePrefixBlacklist: is_, has_, have_ @@ -648,7 +580,6 @@ Style/PredicateName: - 'app/helpers/debates_helper.rb' - 'app/models/budget/ballot.rb' - 'app/models/user.rb' - - 'lib/census_api.rb' # Offense count: 4 # Cop supports --auto-correct. @@ -668,18 +599,14 @@ Style/RedundantBegin: - 'app/controllers/graphql_controller.rb' - 'app/models/legislation/annotation.rb' -# Offense count: 55 -# Cop supports --auto-correct. -Style/RedundantParentheses: - Enabled: false - -# Offense count: 3 +# Offense count: 5 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes. # SupportedStyles: slashes, percent_r, mixed Style/RegexpLiteral: Exclude: - 'app/helpers/embed_videos_helper.rb' + - 'app/models/poll/question/answer/video.rb' - 'spec/customization_engine_spec.rb' # Offense count: 6 @@ -698,38 +625,16 @@ Style/SafeNavigation: Exclude: - 'app/models/signature.rb' -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: single_quotes, double_quotes -Style/StringLiteralsInInterpolation: - Exclude: - - 'spec/features/budgets/investments_spec.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyleForMultiline, SupportedStylesForMultiline. -# SupportedStylesForMultiline: comma, consistent_comma, no_comma -Style/TrailingCommaInArguments: - Exclude: - - 'app/controllers/legislation/answers_controller.rb' - -# Offense count: 9 -# Configuration parameters: SupportedStyles. -# SupportedStyles: snake_case, camelCase -Style/VariableName: - EnforcedStyle: snake_case - -# Offense count: 107 +# Offense count: 93 # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: snake_case, normalcase, non_integer Style/VariableNumber: Enabled: false -# Offense count: 31 +# Offense count: 34 # Cop supports --auto-correct. # Configuration parameters: SupportedStyles, WordRegex. # SupportedStyles: percent, brackets Style/WordArray: EnforcedStyle: percent - MinSize: 9 + MinSize: 8 diff --git a/.travis.yml b/.travis.yml index 8836964a6..4eb86bbf6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ addons: rvm: - "2.3.2" cache: bundler +bundler_args: --without development before_script: - "for i in config/*.example; do cp \"$i\" \"${i/.example}\"; done" - bundle exec rake db:setup @@ -15,4 +16,4 @@ env: - CI_NODE_TOTAL=2 matrix: - CI_NODE_INDEX=0 - - CI_NODE_INDEX=1 \ No newline at end of file + - CI_NODE_INDEX=1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fabf10a0a..076512ea3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,8 +5,9 @@ * Raimond García [github](https://github.com/voodoorai2000) | [twitter](https://twitter.com/voodoorai2000) * Juanjo Bazán [github](https://github.com/xuanxu) | [twitter](https://twitter.com/xuanxu) * Enrique García Cota [github](https://github.com/kikito) | [twitter](https://twitter.com/otikik) -* Alberto García Cabeza [github](https://github.com/decabeza) | [twitter](https://twitter.com/decabeza) +* Alberto García Cabeza [github](https://github.com/decabeza) * Alberto Calderón [github](https://github.com/bertocq) | [twitter](https://twitter.com/bertocq) +* Maria Checa [github](https://github.com/MariaCheca) ## Code of conduct diff --git a/CONTRIBUTING_ES.md b/CONTRIBUTING_ES.md index 6b77c0f42..c05556fb1 100644 --- a/CONTRIBUTING_ES.md +++ b/CONTRIBUTING_ES.md @@ -5,8 +5,9 @@ * Raimond García [github](https://github.com/voodoorai2000) | [twitter](https://twitter.com/voodoorai2000) * Juanjo Bazán [github](https://github.com/xuanxu) | [twitter](https://twitter.com/xuanxu) * Enrique García Cota [github](https://github.com/kikito) | [twitter](https://twitter.com/otikik) -* Alberto García Cabeza [github](https://github.com/decabeza) | [twitter](https://twitter.com/decabeza) +* Alberto García Cabeza [github](https://github.com/decabeza) * Alberto Calderón [github](https://github.com/bertocq) | [twitter](https://twitter.com/bertocq) +* Maria Checa [github](https://github.com/MariaCheca) ## Código de conducta diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..af49d3a30 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +# # Select ubuntu as the base image +FROM coreapps/ruby2.3 + +# Install essential Linux packages +RUN apt-get update -qq && apt-get install -y build-essential libpq-dev postgresql-client nodejs + +# Define where our application will live inside the image +ENV RAILS_ROOT /var/www/consul + +# Create application home. App server will need the pids dir so just create everything in one shot +RUN mkdir -p $RAILS_ROOT/tmp/pids + +# Set our working directory inside the image +WORKDIR $RAILS_ROOT + +# Use the Gemfiles as Docker cache markers. Always bundle before copying app src. +# (the src likely changed and we don't want to invalidate Docker's cache too early) +# http://ilikestuffblog.com/2014/01/06/how-to-skip-bundle-install-when-deploying-a-rails-app-to-docker/ +COPY Gemfile Gemfile + +COPY Gemfile.lock Gemfile.lock + +COPY Gemfile_custom Gemfile_custom + +# Prevent bundler warnings; ensure that the bundler version executed is >= that which created Gemfile.lock +RUN gem install bundler + +# Finish establishing our Ruby enviornment +RUN bundle install --full-index + +# Copy the Rails application into place +COPY . . + +# Define the script we want run once the container boots +# Use the "exec" form of CMD so our script shuts down gracefully on SIGTERM (i.e. `docker stop`) +#CMD [ "config/containers/app_cmd.sh" ] +CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"] diff --git a/Gemfile b/Gemfile index a6c1dc71e..b0c7f2e9a 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -gem 'rails', '4.2.9' +gem 'rails', '4.2.10' gem 'acts-as-taggable-on', '~> 4.0.0' gem 'acts_as_votable', '~> 0.10.0' @@ -37,7 +37,6 @@ gem 'paperclip', '~> 5.1.0' gem 'paranoia', '~> 2.3.1' gem 'pg', '~> 0.21.0' gem 'pg_search', '~> 2.0.1' -gem 'rails-assets-markdown-it', '~> 8.2.1', source: 'https://rails-assets.org' gem 'redcarpet', '~> 3.4.0' gem 'responders', '~> 2.4.0' gem 'rinku', '~> 2.0.2', require: 'rails_rinku' @@ -54,17 +53,21 @@ gem 'uglifier', '~> 3.2.0' gem 'unicorn', '~> 5.3.0' gem 'whenever', '~> 0.9.7', require: false +source 'https://rails-assets.org' do + gem 'rails-assets-leaflet' + gem 'rails-assets-markdown-it', '~> 8.2.1' +end + group :development, :test do - gem "bullet", '~> 5.5.1' - gem 'byebug', '~> 9.0.6' + gem 'bullet', '~> 5.5.1' + gem 'byebug', '~> 9.1.0' gem 'factory_girl_rails', '~> 4.8.0' - gem "faker", '~> 1.7.3' + gem 'faker', '~> 1.7.3' gem 'i18n-tasks', '~> 0.9.15' gem 'knapsack', '~> 1.13.3' gem 'launchy', '~> 2.4.3' gem 'letter_opener_web', '~> 1.3.1' gem 'quiet_assets', '~> 1.1.0' - gem 'rubocop', '~> 0.49.1', require: false gem 'spring', '~> 2.0.1' gem 'spring-commands-rspec', '~> 1.0.4' end @@ -81,9 +84,10 @@ end group :development do gem 'capistrano', '~> 3.8.1', require: false gem 'capistrano-bundler', '~> 1.2', require: false - gem "capistrano-rails", '~> 1.2.3', require: false + gem 'capistrano-rails', '~> 1.2.3', require: false gem 'capistrano3-delayed-job', '~> 1.7.3' gem 'mdl', '~> 0.4.0', require: false + gem 'rubocop', '~> 0.49.1', require: false gem 'rvm1-capistrano3', '~> 1.4.0', require: false gem 'scss_lint', '~> 0.54.0', require: false gem 'web-console', '~> 3.3.0' diff --git a/Gemfile.lock b/Gemfile.lock index f15f0c6e9..8bfbbb27c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,36 +2,36 @@ GEM remote: https://rubygems.org/ remote: https://rails-assets.org/ specs: - actionmailer (4.2.9) - actionpack (= 4.2.9) - actionview (= 4.2.9) - activejob (= 4.2.9) + actionmailer (4.2.10) + actionpack (= 4.2.10) + actionview (= 4.2.10) + activejob (= 4.2.10) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.9) - actionview (= 4.2.9) - activesupport (= 4.2.9) + actionpack (4.2.10) + actionview (= 4.2.10) + activesupport (= 4.2.10) rack (~> 1.6) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.9) - activesupport (= 4.2.9) + actionview (4.2.10) + activesupport (= 4.2.10) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (4.2.9) - activesupport (= 4.2.9) + activejob (4.2.10) + activesupport (= 4.2.10) globalid (>= 0.3.0) - activemodel (4.2.9) - activesupport (= 4.2.9) + activemodel (4.2.10) + activesupport (= 4.2.10) builder (~> 3.1) - activerecord (4.2.9) - activemodel (= 4.2.9) - activesupport (= 4.2.9) + activerecord (4.2.10) + activemodel (= 4.2.10) + activesupport (= 4.2.10) arel (~> 6.0) - activesupport (4.2.9) + activesupport (4.2.10) i18n (~> 0.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) @@ -71,7 +71,7 @@ GEM bullet (5.5.1) activesupport (>= 3.0.0) uniform_notifier (~> 1.10.0) - byebug (9.0.6) + byebug (9.1.0) cancancan (1.16.0) capistrano (3.8.2) airbrussh (>= 1.0.0) @@ -117,6 +117,7 @@ GEM term-ansicolor (~> 1.3) thor (~> 0.19.4) tins (~> 1.6) + crass (1.0.3) daemons (1.2.4) dalli (2.7.6) database_cleaner (1.6.1) @@ -172,7 +173,7 @@ GEM railties (>= 4.1) tzinfo (~> 1.2, >= 1.2.2) geocoder (1.4.4) - globalid (0.4.0) + globalid (0.4.1) activesupport (>= 4.2.0) graphiql-rails (1.4.2) rails @@ -187,7 +188,8 @@ GEM httpi (2.4.2) rack socksify - i18n (0.8.6) + i18n (0.9.1) + concurrent-ruby (~> 1.0) i18n-tasks (0.9.18) activesupport (>= 4.0.2) ast (>= 2.1.0) @@ -239,10 +241,11 @@ GEM actionmailer (>= 3.2) letter_opener (~> 1.0) railties (>= 3.2) - loofah (2.0.3) + loofah (2.1.1) + crass (~> 1.0.2) nokogiri (>= 1.5.9) - mail (2.6.6) - mime-types (>= 1.16, < 4) + mail (2.7.0) + mini_mime (>= 0.1.1) mdl (0.4.0) kramdown (~> 1.12, >= 1.12.0) mixlib-cli (~> 1.7, >= 1.7.0) @@ -251,7 +254,8 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) mimemagic (0.3.2) - mini_portile2 (2.2.0) + mini_mime (1.0.0) + mini_portile2 (2.3.0) minitest (5.10.3) mixlib-cli (1.7.0) mixlib-config (2.2.4) @@ -262,8 +266,8 @@ GEM net-ssh (>= 2.6.5) net-ssh (4.1.0) newrelic_rpm (4.1.0.333) - nokogiri (1.8.0) - mini_portile2 (~> 2.2.0) + nokogiri (1.8.1) + mini_portile2 (~> 2.3.0) nori (2.6.0) oauth (0.5.3) oauth2 (1.4.0) @@ -323,17 +327,18 @@ GEM rack rack-test (0.6.3) rack (>= 1.0) - rails (4.2.9) - actionmailer (= 4.2.9) - actionpack (= 4.2.9) - actionview (= 4.2.9) - activejob (= 4.2.9) - activemodel (= 4.2.9) - activerecord (= 4.2.9) - activesupport (= 4.2.9) + rails (4.2.10) + actionmailer (= 4.2.10) + actionpack (= 4.2.10) + actionview (= 4.2.10) + activejob (= 4.2.10) + activemodel (= 4.2.10) + activerecord (= 4.2.10) + activesupport (= 4.2.10) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.9) + railties (= 4.2.10) sprockets-rails + rails-assets-leaflet (1.1.0) rails-assets-markdown-it (8.2.2) rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) @@ -343,15 +348,15 @@ GEM rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.3) loofah (~> 2.0) - railties (4.2.9) - actionpack (= 4.2.9) - activesupport (= 4.2.9) + railties (4.2.10) + actionpack (= 4.2.10) + activesupport (= 4.2.10) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.2.2) rake raindrops (0.18.0) - rake (12.0.0) + rake (12.2.1) redcarpet (3.4.0) referer-parser (0.3.0) request_store (1.3.2) @@ -455,7 +460,7 @@ GEM rack (>= 1.3, < 3) rack-accept (~> 0.4) tilt (>= 1.4, < 3) - tzinfo (1.2.3) + tzinfo (1.2.4) thread_safe (~> 0.1) uglifier (3.2.0) execjs (>= 0.3.0, < 3) @@ -493,7 +498,7 @@ DEPENDENCIES ancestry (~> 2.2.2) browser (~> 2.3.0) bullet (~> 5.5.1) - byebug (~> 9.0.6) + byebug (~> 9.1.0) cancancan (~> 1.16.0) capistrano (~> 3.8.1) capistrano-bundler (~> 1.2) @@ -541,7 +546,8 @@ DEPENDENCIES pg_search (~> 2.0.1) poltergeist (~> 1.15.0) quiet_assets (~> 1.1.0) - rails (= 4.2.9) + rails (= 4.2.10) + rails-assets-leaflet! rails-assets-markdown-it (~> 8.2.1)! redcarpet (~> 3.4.0) responders (~> 2.4.0) @@ -567,4 +573,4 @@ DEPENDENCIES whenever (~> 0.9.7) BUNDLED WITH - 1.15.3 + 1.15.4 diff --git a/README.md b/README.md index 48f6bfcb8..3e160b438 100644 --- a/README.md +++ b/README.md @@ -76,3 +76,8 @@ Code published under AFFERO GPL v3 (see [LICENSE-AGPLv3.txt](LICENSE-AGPLv3.txt) ## Contributions See [CONTRIBUTING.md](CONTRIBUTING.md) + + +## Local development with Docker + +Please check the documentation at https://consul_docs.gitbooks.io/docs/content diff --git a/app/assets/fonts/icons.eot b/app/assets/fonts/icons.eot index c1afd24fd..53439b281 100644 Binary files a/app/assets/fonts/icons.eot and b/app/assets/fonts/icons.eot differ diff --git a/app/assets/fonts/icons.svg b/app/assets/fonts/icons.svg index 68d878942..a0c854397 100644 --- a/app/assets/fonts/icons.svg +++ b/app/assets/fonts/icons.svg @@ -62,4 +62,12 @@ + + + + + + + + diff --git a/app/assets/fonts/icons.ttf b/app/assets/fonts/icons.ttf index a9443499b..3bd8e9dec 100644 Binary files a/app/assets/fonts/icons.ttf and b/app/assets/fonts/icons.ttf differ diff --git a/app/assets/fonts/icons.woff b/app/assets/fonts/icons.woff index 09a6f6dc5..0ac7f8f19 100644 Binary files a/app/assets/fonts/icons.woff and b/app/assets/fonts/icons.woff differ diff --git a/app/assets/images/ballot.gif b/app/assets/images/ballot.gif deleted file mode 100644 index 18cf0717f..000000000 Binary files a/app/assets/images/ballot.gif and /dev/null differ diff --git a/app/assets/images/ballot_tiny.gif b/app/assets/images/ballot_tiny.gif deleted file mode 100644 index 976d37591..000000000 Binary files a/app/assets/images/ballot_tiny.gif and /dev/null differ diff --git a/app/assets/images/help/help_icon_budgets.png b/app/assets/images/help/help_icon_budgets.png index fc5e3022f..f986a939c 100644 Binary files a/app/assets/images/help/help_icon_budgets.png and b/app/assets/images/help/help_icon_budgets.png differ diff --git a/app/assets/images/help/help_icon_polls.png b/app/assets/images/help/help_icon_polls.png index b7f7cf479..f13363934 100644 Binary files a/app/assets/images/help/help_icon_polls.png and b/app/assets/images/help/help_icon_polls.png differ diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index cf579627b..51de9c678 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -14,6 +14,8 @@ //= require jquery_ujs //= require jquery-ui/widgets/datepicker //= require jquery-ui/i18n/datepicker-es +//= require jquery-ui/widgets/autocomplete +//= require jquery-ui/widgets/sortable //= require jquery-fileupload/basic //= require foundation //= require turbolinks @@ -62,8 +64,15 @@ //= require followable //= require flaggable //= require documentable +//= require imageable //= require tree_navigator //= require custom +//= require tag_autocomplete +//= require polls_admin +//= require leaflet +//= require map +//= require polls +//= require sortable var initialize_modules = function() { App.Comments.initialize(); @@ -98,10 +107,16 @@ var initialize_modules = function() { App.WatchFormChanges.initialize(); App.TreeNavigator.initialize(); App.Documentable.initialize(); + App.Imageable.initialize(); + App.TagAutocomplete.initialize(); + App.PollsAdmin.initialize(); + App.Map.initialize(); + App.Polls.initialize(); + App.Sortable.initialize(); }; $(function(){ - Turbolinks.enableProgressBar() + Turbolinks.enableProgressBar(); $(document).ready(initialize_modules); $(document).on('page:load', initialize_modules); diff --git a/app/assets/javascripts/documentable.js.coffee b/app/assets/javascripts/documentable.js.coffee index 4c354e5a6..7cc71f39d 100644 --- a/app/assets/javascripts/documentable.js.coffee +++ b/app/assets/javascripts/documentable.js.coffee @@ -1,101 +1,156 @@ App.Documentable = initialize: -> - @initializeDirectUploads() - @initializeInterface() - initializeDirectUploads: -> + inputFiles = $('.js-document-attachment') + $.each inputFiles, (index, input) -> + App.Documentable.initializeDirectUploadInput(input) - $('input.js-document-attachment[type=file]').fileupload + $('#nested-documents').on 'cocoon:after-remove', (e, insertedItem) -> + App.Documentable.unlockUploads() - paramName: "document[attachment]" + $('#nested-documents').on 'cocoon:after-insert', (e, nested_document) -> + input = $(nested_document).find('.js-document-attachment') + App.Documentable.initializeDirectUploadInput(input) + + if $(nested_document).closest('#nested-documents').find('.document:visible').length >= $('#nested-documents').data('max-documents-allowed') + App.Documentable.lockUploads() + + initializeDirectUploadInput: (input) -> + + inputData = @buildData([], input) + + @initializeRemoveCachedDocumentLink(input, inputData) + + $(input).fileupload + + paramName: "attachment" formData: null add: (e, data) -> - wrapper = $(e.target).closest('.document') - index = $(e.target).data('index') - is_nested_document = $(e.target).data('nested-document') - $(wrapper).find('.progress-bar-placeholder').empty() - data.progressBar = $(wrapper).find('.progress-bar-placeholder').html('
') - $(wrapper).find('.progress-bar-placeholder').css('display','block') - data.formData = { - "document[title]": $(wrapper).find('input.document-title').val() || data.files[0].name - "index": index, - "nested_document": is_nested_document - } + data = App.Documentable.buildFileUploadData(e, data) + App.Documentable.clearProgressBar(data) + App.Documentable.setProgressBar(data, 'uploading') data.submit() change: (e, data) -> - wrapper = $(e.target).parent() - $.each(data.files, (index, file)-> - $(wrapper).find('.file-name').text(file.name) - ) + $.each data.files, (index, file) -> + App.Documentable.setFilename(inputData, file.name) + + fail: (e, data) -> + $(data.cachedAttachmentField).val("") + App.Documentable.clearFilename(data) + App.Documentable.setProgressBar(data, 'errors') + App.Documentable.clearInputErrors(data) + App.Documentable.setInputErrors(data) + $(data.destroyAttachmentLinkContainer).find("a.delete:not(.remove-nested)").remove() + $(data.addAttachmentLabel).addClass('error') + $(data.addAttachmentLabel).show() + + done: (e, data) -> + $(data.cachedAttachmentField).val(data.result.cached_attachment) + App.Documentable.setTitleFromFile(data, data.result.filename) + App.Documentable.setProgressBar(data, 'complete') + App.Documentable.setFilename(data, data.result.filename) + App.Documentable.clearInputErrors(data) + $(data.addAttachmentLabel).hide() + $(data.wrapper).find(".attachment-actions").removeClass('small-12').addClass('small-6 float-right') + $(data.wrapper).find(".attachment-actions .action-remove").removeClass('small-3').addClass('small-12') + + destroyAttachmentLink = $(data.result.destroy_link) + $(data.destroyAttachmentLinkContainer).html(destroyAttachmentLink) + $(destroyAttachmentLink).on 'click', (e) -> + e.preventDefault() + e.stopPropagation() + App.Documentable.doDeleteCachedAttachmentRequest(this.href, data) progress: (e, data) -> progress = parseInt(data.loaded / data.total * 100, 10) $(data.progressBar).find('.loading-bar').css 'width', progress + '%' return - initializeInterface: -> - input_files = $('input.js-document-attachment[type=file]') + buildFileUploadData: (e, data) -> + data = @buildData(data, e.target) + return data - $.each input_files, (index, file) -> - wrapper = $(file).parent() - App.Documentable.watchRemoveDocumentbutton(wrapper) + buildData: (data, input) -> + wrapper = $(input).closest('.direct-upload') + data.input = input + data.wrapper = wrapper + data.progressBar = $(wrapper).find('.progress-bar-placeholder') + data.errorContainer = $(wrapper).find('.attachment-errors') + data.fileNameContainer = $(wrapper).find('p.file-name') + data.destroyAttachmentLinkContainer = $(wrapper).find('.action-remove') + data.addAttachmentLabel = $(wrapper).find('.action-add label') + data.cachedAttachmentField = $(wrapper).find("input[name$='[cached_attachment]']") + data.titleField = $(wrapper).find("input[name$='[title]']") + $(wrapper).find('.progress-bar-placeholder').css('display', 'block') + return data - watchRemoveDocumentbutton: (wrapper) -> - remove_document_button = $(wrapper).find('.remove-document') - $(remove_document_button).on 'click', (e) -> + clearFilename: (data) -> + $(data.fileNameContainer).text('') + $(data.fileNameContainer).hide() + + clearInputErrors: (data) -> + $(data.errorContainer).find('small.error').remove() + + clearProgressBar: (data) -> + $(data.progressBar).find('.loading-bar').removeClass('complete errors uploading').css('width', "0px") + + setFilename: (data, file_name) -> + $(data.fileNameContainer).text(file_name) + $(data.fileNameContainer).show() + + setProgressBar: (data, klass) -> + $(data.progressBar).find('.loading-bar').addClass(klass) + + setTitleFromFile: (data, title) -> + if $(data.titleField).val() == "" + $(data.titleField).val(title) + + setInputErrors: (data) -> + errors = '' + data.jqXHR.responseJSON.errors + '' + $(data.errorContainer).append(errors) + + lockUploads: -> + $('#max-documents-notice').removeClass('hide') + $('#new_document_link').addClass('hide') + + unlockUploads: -> + $('#max-documents-notice').addClass('hide') + $('#new_document_link').removeClass('hide') + + doDeleteCachedAttachmentRequest: (url, data) -> + $.ajax + type: "POST" + url: url + dataType: "json" + data: { "_method": "delete" } + complete: -> + $(data.cachedAttachmentField).val("") + $(data.addAttachmentLabel).show() + + App.Documentable.clearFilename(data) + App.Documentable.clearInputErrors(data) + App.Documentable.clearProgressBar(data) + + App.Documentable.unlockUploads() + $(data.wrapper).find(".attachment-actions").addClass('small-12').removeClass('small-6 float-right') + $(data.wrapper).find(".attachment-actions .action-remove").addClass('small-3').removeClass('small-12') + + if $(data.input).data('nested-document') == true + $(data.wrapper).remove() + else + $(data.wrapper).find('a.remove-cached-attachment').remove() + + initializeRemoveCachedDocumentLink: (input, data) -> + wrapper = $(input).closest(".direct-upload") + remove_document_link = $(wrapper).find('a.remove-cached-attachment') + $(remove_document_link).on 'click', (e) -> e.preventDefault() - $(wrapper).remove() - $('#new_document_link').show() - $('.max-documents-notice').hide() + e.stopPropagation() + App.Documentable.doDeleteCachedAttachmentRequest(this.href, data) - uploadNestedDocument: (id, nested_document, result) -> - $('#' + id).replaceWith(nested_document) - @updateLoadingBar(id, result) - @initialize() - - uploadPlainDocument: (id, nested_document, result) -> - $('#' + id).replaceWith(nested_document) - @updateLoadingBar(id, result) - @initialize() - - updateLoadingBar: (id, result) -> - if result - $('#' + id).find('.loading-bar').addClass 'complete' - else - $('#' + id).find('.loading-bar').addClass 'errors' - $('#' + id).find('.progress-bar-placeholder').css('display','block') - - new: (nested_fields) -> - $(".documents-list").append(nested_fields) - @initialize() - - destroyNestedDocument: (id, notice) -> + removeDocument: (id) -> $('#' + id).remove() - @updateNotice(notice) - - replacePlainDocument: (id, notice, plain_document) -> - $('#' + id).replaceWith(plain_document) - @updateNotice(notice) - @initialize() - - updateNotice: (notice) -> - if $('[data-alert]').length > 0 - $('[data-alert]').replaceWith(notice) - else - $("body").append(notice) - - updateNewDocumentButton: (link) -> - if $('.document').length >= $('.documents').data('max-documents') - $('#new_document_link').hide() - $('.max-documents-notice').removeClass('hide') - $('.max-documents-notice').show() - else if $('#new_document_link').length > 0 - $('#new_document_link').replaceWith(link) - $('.max-documents-notice').hide() - else - $('.max-documents-notice').hide() - $(link).insertBefore('.documents hr:last') diff --git a/app/assets/javascripts/imageable.js.coffee b/app/assets/javascripts/imageable.js.coffee new file mode 100644 index 000000000..e029dff52 --- /dev/null +++ b/app/assets/javascripts/imageable.js.coffee @@ -0,0 +1,166 @@ +App.Imageable = + + initialize: -> + inputFiles = $('.js-image-attachment') + $.each inputFiles, (index, input) -> + App.Imageable.initializeDirectUploadInput(input) + + $('#nested-image').on 'cocoon:after-remove', (e, item) -> + $("#new_image_link").removeClass('hide') + + $('#nested-image').on 'cocoon:before-insert', (e, nested_image) -> + if $(".js-image-attachment").length > 0 + $(".js-image-attachment").closest('.image').remove() + + $('#nested-image').on 'cocoon:after-insert', (e, nested_image) -> + $("#new_image_link").addClass('hide') + input = $(nested_image).find('.js-image-attachment') + App.Imageable.initializeDirectUploadInput(input) + + initializeDirectUploadInput: (input) -> + + inputData = @buildData([], input) + + @initializeRemoveCachedImageLink(input, inputData) + + $(input).fileupload + + paramName: "attachment" + + formData: null + + add: (e, data) -> + data = App.Imageable.buildFileUploadData(e, data) + App.Imageable.clearProgressBar(data) + App.Imageable.setProgressBar(data, 'uploading') + data.submit() + + change: (e, data) -> + $.each data.files, (index, file) -> + App.Imageable.setFilename(inputData, file.name) + + fail: (e, data) -> + $(data.cachedAttachmentField).val("") + App.Imageable.clearFilename(data) + App.Imageable.setProgressBar(data, 'errors') + App.Imageable.clearInputErrors(data) + App.Imageable.setInputErrors(data) + App.Imageable.clearPreview(data) + $(data.destroyAttachmentLinkContainer).find("a.delete:not(.remove-nested)").remove() + $(data.addAttachmentLabel).addClass('error') + $(data.addAttachmentLabel).show() + + done: (e, data) -> + $(data.cachedAttachmentField).val(data.result.cached_attachment) + App.Imageable.setTitleFromFile(data, data.result.filename) + App.Imageable.setProgressBar(data, 'complete') + App.Imageable.setFilename(data, data.result.filename) + App.Imageable.clearInputErrors(data) + $(data.addAttachmentLabel).hide() + $(data.wrapper).find(".attachment-actions").removeClass('small-12').addClass('small-6 float-right') + $(data.wrapper).find(".attachment-actions .action-remove").removeClass('small-3').addClass('small-12') + App.Imageable.setPreview(data) + + destroyAttachmentLink = $(data.result.destroy_link) + $(data.destroyAttachmentLinkContainer).html(destroyAttachmentLink) + $(destroyAttachmentLink).on 'click', (e) -> + e.preventDefault() + e.stopPropagation() + App.Imageable.doDeleteCachedAttachmentRequest(this.href, data) + + progress: (e, data) -> + progress = parseInt(data.loaded / data.total * 100, 10) + $(data.progressBar).find('.loading-bar').css 'width', progress + '%' + return + + buildFileUploadData: (e, data) -> + data = @buildData(data, e.target) + return data + + buildData: (data, input) -> + wrapper = $(input).closest('.direct-upload') + data.input = input + data.wrapper = wrapper + data.progressBar = $(wrapper).find('.progress-bar-placeholder') + data.preview = $(wrapper).find('.image-preview') + data.errorContainer = $(wrapper).find('.attachment-errors') + data.fileNameContainer = $(wrapper).find('p.file-name') + data.destroyAttachmentLinkContainer = $(wrapper).find('.action-remove') + data.addAttachmentLabel = $(wrapper).find('.action-add label') + data.cachedAttachmentField = $(wrapper).find("input[name$='[cached_attachment]']") + data.titleField = $(wrapper).find("input[name$='[title]']") + $(wrapper).find('.progress-bar-placeholder').css('display', 'block') + return data + + clearFilename: (data) -> + $(data.fileNameContainer).text('') + $(data.fileNameContainer).hide() + + clearInputErrors: (data) -> + $(data.errorContainer).find('small.error').remove() + + clearProgressBar: (data) -> + $(data.progressBar).find('.loading-bar').removeClass('complete errors uploading').css('width', "0px") + + clearPreview: (data) -> + $(data.wrapper).find('.image-preview').remove() + + setFilename: (data, file_name) -> + $(data.fileNameContainer).text(file_name) + $(data.fileNameContainer).show() + + setProgressBar: (data, klass) -> + $(data.progressBar).find('.loading-bar').addClass(klass) + + setTitleFromFile: (data, title) -> + if $(data.titleField).val() == "" + $(data.titleField).val(title) + + setInputErrors: (data) -> + errors = '' + data.jqXHR.responseJSON.errors + '' + $(data.errorContainer).append(errors) + + setPreview: (data) -> + image_preview = '
' + if $(data.preview).length > 0 + $(data.preview).replaceWith(image_preview) + else + $(image_preview).insertBefore($(data.wrapper).find(".attachment-actions")) + data.preview = $(data.wrapper).find('.image-preview') + + doDeleteCachedAttachmentRequest: (url, data) -> + $.ajax + type: "POST" + url: url + dataType: "json" + data: { "_method": "delete" } + complete: -> + $(data.cachedAttachmentField).val("") + $(data.addAttachmentLabel).show() + + App.Imageable.clearFilename(data) + App.Imageable.clearInputErrors(data) + App.Imageable.clearProgressBar(data) + App.Imageable.clearPreview(data) + + $('#new_image_link').removeClass('hide') + + $(data.wrapper).find(".attachment-actions").addClass('small-12').removeClass('small-6 float-right') + $(data.wrapper).find(".attachment-actions .action-remove").addClass('small-3').removeClass('small-12') + + if $(data.input).data('nested-image') == true + $(data.wrapper).remove() + else + $(data.wrapper).find('a.remove-cached-attachment').remove() + + initializeRemoveCachedImageLink: (input, data) -> + wrapper = $(input).closest(".direct-upload") + remove_image_link = $(wrapper).find('a.remove-cached-attachment') + $(remove_image_link).on 'click', (e) -> + e.preventDefault() + e.stopPropagation() + App.Imageable.doDeleteCachedAttachmentRequest(this.href, data) + + removeImage: (id) -> + $('#' + id).remove() + $("#new_image_link").removeClass('hide') diff --git a/app/assets/javascripts/map.js.coffee b/app/assets/javascripts/map.js.coffee new file mode 100644 index 000000000..3f28024a7 --- /dev/null +++ b/app/assets/javascripts/map.js.coffee @@ -0,0 +1,78 @@ +App.Map = + + initialize: -> + maps = $('*[data-map]') + + if maps.length > 0 + $.each maps, (index, map) -> + App.Map.initializeMap map + + initializeMap: (element) -> + + mapCenterLatitude = $(element).data('map-center-latitude') + mapCenterLongitude = $(element).data('map-center-longitude') + markerLatitude = $(element).data('marker-latitude') + markerLongitude = $(element).data('marker-longitude') + zoom = $(element).data('map-zoom') + mapTilesProvider = $(element).data('map-tiles-provider') + mapAttribution = $(element).data('map-tiles-provider-attribution') + latitudeInputSelector = $(element).data('latitude-input-selector') + longitudeInputSelector = $(element).data('longitude-input-selector') + zoomInputSelector = $(element).data('zoom-input-selector') + removeMarkerSelector = $(element).data('marker-remove-selector') + editable = $(element).data('marker-editable') + marker = null; + markerIcon = L.divIcon( + className: 'map-marker' + iconSize: [30, 30] + iconAnchor: [15, 40] + html: '
') + + createMarker = (latitude, longitude) -> + markerLatLng = new (L.LatLng)(latitude, longitude) + marker = L.marker(markerLatLng, { icon: markerIcon, draggable: editable }) + if editable + marker.on 'dragend', updateFormfields + marker.addTo(map) + return marker + + removeMarker = (e) -> + e.preventDefault() + if marker + map.removeLayer(marker) + marker = null; + clearFormfields() + return + + moveOrPlaceMarker = (e) -> + if marker + marker.setLatLng(e.latlng) + else + marker = createMarker(e.latlng.lat, e.latlng.lng) + + updateFormfields() + return + + updateFormfields = -> + $(latitudeInputSelector).val marker.getLatLng().lat + $(longitudeInputSelector).val marker.getLatLng().lng + $(zoomInputSelector).val map.getZoom() + return + + clearFormfields = -> + $(latitudeInputSelector).val '' + $(longitudeInputSelector).val '' + $(zoomInputSelector).val '' + return + + mapCenterLatLng = new (L.LatLng)(mapCenterLatitude, mapCenterLongitude) + map = L.map(element.id).setView(mapCenterLatLng, zoom) + L.tileLayer(mapTilesProvider, attribution: mapAttribution).addTo map + + if markerLatitude && markerLongitude + marker = createMarker(markerLatitude, markerLongitude) + + if editable + $(removeMarkerSelector).on 'click', removeMarker + map.on 'zoomend', updateFormfields + map.on 'click', moveOrPlaceMarker diff --git a/app/assets/javascripts/polls.js.coffee b/app/assets/javascripts/polls.js.coffee new file mode 100644 index 000000000..ac2c759ba --- /dev/null +++ b/app/assets/javascripts/polls.js.coffee @@ -0,0 +1,44 @@ +App.Polls = + generateToken: -> + token = '' + rand = '' + for n in [0..5] + rand = Math.random().toString(36).substr(2) # remove `0.` + token = token + rand; + + token = token.substring(0, 64) + return token + + replaceToken: -> + for link in $('.js-question-answer') + token_param = link.search.slice(-6) + if token_param == "token=" + link.href = link.href + @token + + initialize: -> + @token = App.Polls.generateToken() + App.Polls.replaceToken() + + $(".js-question-answer").on + click: => + token_message = $(".js-token-message") + if !token_message.is(':visible') + token_message.html(token_message.html() + "
" + @token + ""); + token_message.show() + false + + $(".zoom-link").on "click", (event) -> + element = event.target + answer = $(element).closest('div.answer') + + if $(answer).hasClass('medium-6') + $(answer).removeClass("medium-6"); + $(answer).addClass("answer-divider"); + unless $(answer).hasClass('first') + $(answer).insertBefore($(answer).prev('div.answer')); + else + $(answer).addClass("medium-6"); + $(answer).removeClass("answer-divider"); + unless $(answer).hasClass('first') + $(answer).insertAfter($(answer).next('div.answer')); + diff --git a/app/assets/javascripts/polls_admin.js.coffee b/app/assets/javascripts/polls_admin.js.coffee new file mode 100644 index 000000000..ef1dd44f1 --- /dev/null +++ b/app/assets/javascripts/polls_admin.js.coffee @@ -0,0 +1,12 @@ +App.PollsAdmin = + + initialize: -> + $("select[class='js-poll-shifts']").on + change: -> + switch ($(this).val()) + when 'vote_collection' + $("select[class='js-shift-vote-collection-dates']").show(); + $("select[class='js-shift-recount-scrutiny-dates']").hide(); + when 'recount_scrutiny' + $("select[class='js-shift-recount-scrutiny-dates']").show(); + $("select[class='js-shift-vote-collection-dates']").hide(); diff --git a/app/assets/javascripts/prevent_double_submission.js.coffee b/app/assets/javascripts/prevent_double_submission.js.coffee index 2a4ef9c5e..56a099730 100644 --- a/app/assets/javascripts/prevent_double_submission.js.coffee +++ b/app/assets/javascripts/prevent_double_submission.js.coffee @@ -22,11 +22,13 @@ App.PreventDoubleSubmission = initialize: -> $('form').on('submit', (event) -> - buttons = $(this).find(':button, :submit') - App.PreventDoubleSubmission.disable_buttons(buttons) - ).on('ajax:success', -> - buttons = $(this).find(':button, :submit') - App.PreventDoubleSubmission.reset_buttons(buttons) + unless event.target.id == "new_officing_voter" + buttons = $(this).find(':button, :submit') + App.PreventDoubleSubmission.disable_buttons(buttons) + ).on('ajax:success', (event) -> + unless event.target.id == "new_officing_voter" + buttons = $(this).find(':button, :submit') + App.PreventDoubleSubmission.reset_buttons(buttons) ) false diff --git a/app/assets/javascripts/sortable.js.coffee b/app/assets/javascripts/sortable.js.coffee new file mode 100644 index 000000000..1af543f6a --- /dev/null +++ b/app/assets/javascripts/sortable.js.coffee @@ -0,0 +1,9 @@ +App.Sortable = + initialize: -> + $(".sortable").sortable + update: (event, ui) -> + new_order = $(this).sortable('toArray', {attribute: 'data-answer-id'}); + $.ajax + url: $('.sortable').data('js-url'), + data: {ordered_list: new_order}, + type: 'POST' diff --git a/app/assets/javascripts/tag_autocomplete.js.coffee b/app/assets/javascripts/tag_autocomplete.js.coffee new file mode 100644 index 000000000..be27cd81c --- /dev/null +++ b/app/assets/javascripts/tag_autocomplete.js.coffee @@ -0,0 +1,34 @@ +App.TagAutocomplete = + + split: ( val ) -> + return (val.split( /,\s*/ )) + + extractLast: ( term ) -> + return (App.TagAutocomplete.split( term ).pop()) + + init_autocomplete: -> + $('.tag-autocomplete').autocomplete + source: (request, response) -> + $.ajax + url: $('.tag-autocomplete').data('js-url'), + data: {search: App.TagAutocomplete.extractLast( request.term )}, + type: 'GET', + dataType: 'json' + success: ( data ) -> + response( data ); + + minLength: 0, + search: -> + App.TagAutocomplete.extractLast( this.value ); + focus: -> + return false; + select: ( event, ui ) -> ( + terms = App.TagAutocomplete.split( this.value ); + terms.pop(); + terms.push( ui.item.value ); + terms.push( "" ); + this.value = terms.join( ", " ); + return false;); + + initialize: -> + App.TagAutocomplete.init_autocomplete(); \ No newline at end of file diff --git a/app/assets/stylesheets/_consul_settings.scss b/app/assets/stylesheets/_consul_settings.scss index 0844f861f..1f03c9be7 100644 --- a/app/assets/stylesheets/_consul_settings.scss +++ b/app/assets/stylesheets/_consul_settings.scss @@ -75,3 +75,5 @@ $accordion-content-color: foreground($accordion-background, $text); $tab-item-font-size: $base-font-size; $tab-item-padding: $line-height / 2 0; $tab-content-border: $border; + +$orbit-bullet-diameter: 0.8rem; diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss index 6ac728e2c..85997aecd 100644 --- a/app/assets/stylesheets/admin.scss +++ b/app/assets/stylesheets/admin.scss @@ -8,38 +8,105 @@ // 06. Polls // 07. Legislation // 08. CMS +// 09. Map // // 01. Global styles // ----------------- -$admin-color: #cf3638; +$admin-color: #245b80; +$sidebar: #245b80; +$sidebar-hover: #25597c; +$sidebar-active: #f4fcd0; .admin { + h2 { + font-weight: 100; + margin-bottom: $line-height; + + &.title { + text-transform: uppercase; + } + } + + .back { + float: none; + } + .header { border: 0; } .top-links { - background: darken($admin-color, 15%); - } - - .back-web { - padding-top: $line-height / 4; - text-decoration: underline; + background: #000; } .top-bar { - background: $admin-color !important; + background: #fff !important; + border-bottom: 1px solid #eee; + box-shadow: 0 2px 2px #eee; + color: #000; height: auto; + + [class^="icon-"]:not(.icon-circle) { + font-size: $base-font-size; + } } .top-bar-title { h1 { margin-bottom: 0; + margin-top: $line-height / 2; + + @include breakpoint(medium) { + margin-left: $line-height / 2; + } + + small { + color: #000; + text-transform: uppercase; + } } + + a { + color: #000 !important; + line-height: $line-height !important; + + @include breakpoint(medium) { + line-height: $line-height !important; + } + } + } + + .top-bar .menu > li { + + @include breakpoint(medium) { + height: auto !important; + padding-top: $line-height / 2; + } + + a { + color: #000 !important; + } + } + + .menu-icon.dark { + + &::after, + &:hover::after { + background: #000 !important; + box-shadow: 0 7px 0 #000, 0 14px 0 #000 !important; + } + } + + .notifications .icon-circle { + color: $admin-color; + } + + .dropdown.menu > .is-dropdown-submenu-parent > a::after { + border-color: #000 transparent transparent; } .fieldset { @@ -54,7 +121,8 @@ $admin-color: #cf3638; } } - th, td { + th, + td { text-align: left; &.text-center { @@ -81,6 +149,7 @@ $admin-color: #cf3638; } table { + .break { word-break: break-word; } @@ -107,9 +176,19 @@ $admin-color: #cf3638; max-width: none; } - .menu.simple .active { - border-bottom: 2px solid $admin-color; - color: $admin-color; + .menu.simple { + margin-bottom: $line-height / 2; + + h2 { + font-weight: bold; + margin-bottom: $line-height / 3; + } + + .active { + border-bottom: 2px solid $admin-color; + color: $admin-color; + font-weight: bold; + } } .tabs-panel { @@ -195,6 +274,8 @@ $admin-color: #cf3638; // ----------- .admin-sidebar { + background: $sidebar; + background: linear-gradient(to bottom, #245b80 0%, #488fb5 100%); border-right: 1px solid $border; @include breakpoint(medium) { @@ -208,7 +289,7 @@ $admin-color: #cf3638; padding: 0; [class^="icon-"] { - color: $admin-color; + color: #fff; display: inline-block; font-size: rem-calc(24); line-height: $line-height; @@ -219,39 +300,32 @@ $admin-color: #cf3638; } li { - background: #fff; margin: 0; outline: 0; ul { margin-left: $line-height / 1.5; - border-left: 1px solid $border; + border-left: 1px solid $sidebar-hover; padding-left: $line-height / 2; } - &.section-title { - border-bottom: 1px solid $border; - } - &.active a { - background: #f3f6f7; - border-radius: rem-calc(6); - color: $admin-color; + background: $sidebar-hover; + border-left: 2px solid $sidebar-active; font-weight: bold; } } li a { - color: $text; + color: #fff; display: block; line-height: rem-calc(48); padding-left: rem-calc(12); vertical-align: top; &:hover { - background: #f3f6f7; - border-radius: rem-calc(6); - color: $admin-color; + background: $sidebar-hover; + color: #fff; text-decoration: none; } } @@ -259,7 +333,13 @@ $admin-color: #cf3638; .is-accordion-submenu-parent { > a::after { - border-color: $admin-color transparent transparent; + border: 0; + content: '\61' !important; + font-family: "icons" !important; + height: auto; + position: absolute !important; + right: 30px; + top: 6px !important; } } @@ -525,6 +605,7 @@ table { .callout { height: $line-height * 2; line-height: $line-height * 2; + margin: 0; padding: 0 $line-height / 2; } } @@ -898,3 +979,51 @@ table { border: 0; } } + +// 09. Map +// -------------- + +.map { + width: 100%; + height: 350px; + + .map-marker { + visibility: visible; + position: absolute; + left: 50%; + top: 50%; + margin-top: -5px; + + .map-icon { + width: 30px; + height: 30px; + border-radius: 50% 50% 50% 0; + background: #00cae9; + transform: rotate(-45deg); + } + + .map-icon::after { + content: ''; + width: 14px; + height: 14px; + margin: 8px 0 0 8px; + background: #fff; + position: absolute; + border-radius: 50%; + } + } + + .map-attributtion { + visibility: visible; + height: auto; + } +} + +.map-marker { + visibility: hidden; +} + +.map-attributtion { + visibility: hidden; + height: 0; +} diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 19c73de32..3f93ffa6b 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -16,4 +16,7 @@ @import 'annotator_overrides'; @import 'jquery-ui/datepicker'; @import 'datepicker_overrides'; -@import 'documentable'; +@import 'jquery-ui/autocomplete'; +@import 'autocomplete_overrides'; +@import 'jquery-ui/sortable'; +@import 'leaflet'; diff --git a/app/assets/stylesheets/autocomplete_overrides.scss b/app/assets/stylesheets/autocomplete_overrides.scss new file mode 100644 index 000000000..dd2b939ad --- /dev/null +++ b/app/assets/stylesheets/autocomplete_overrides.scss @@ -0,0 +1,40 @@ +// Overrides styles of jquery-ui/autocomplete +// + +/* Autocomplete +----------------------------------*/ +.ui-autocomplete { + position: absolute; + cursor: default; +} + +/* workarounds */ +* html .ui-autocomplete { + width: 1px; +} /* without this, the menu expands to 100% in IE6 */ + +/* Menu +----------------------------------*/ +.ui-menu { + list-style: none; + padding: $line-height / 4 $line-height / 3; + display: block; + background: #fff; + border: 1px solid $border; + font-size: $small-font-size; + + .ui-menu-item { + + .ui-menu-item-wrapper { + padding: $line-height / 4 $line-height / 3; + position: relative; + } + + .ui-state-hover, + .ui-state-active { + background: #ececec; + border-radius: rem-calc(6); + } + } + +} diff --git a/app/assets/stylesheets/documentable.scss b/app/assets/stylesheets/documentable.scss deleted file mode 100644 index 70629bb27..000000000 --- a/app/assets/stylesheets/documentable.scss +++ /dev/null @@ -1,63 +0,0 @@ -.progress-bar-placeholder { - display: none; -} - -.document-form { - - .document .file-name { - margin-top: 0; - } - - .progress-bar-placeholder { - margin-bottom: 15px; - } - - .document .loading-bar.errors { - margin-top: $line-height * 2; - } -} - -.document { - - .button { - font-weight: normal; - } - - .progress-bar { - width: 100%; - background-color: $light-gray; - } - - .js-document-attachment { - display: none; - } - - .file-name { - margin-top: $line-height / 2; - } - - .loading-bar { - height: 5px; - width: 0; - transition: width 500ms ease-out; - - &.uploading { - background-color: $dark-gray; - } - - &.complete { - background-color: $success-color; - width: 100%; - } - - &.errors { - background-color: $alert-color; - width: 100%; - margin-top: $line-height / 2; - } - } - - .loading-bar.no-transition { - transition: none; - } -} diff --git a/app/assets/stylesheets/foundation_and_overrides.scss b/app/assets/stylesheets/foundation_and_overrides.scss index 0bead10ae..66640f3f2 100644 --- a/app/assets/stylesheets/foundation_and_overrides.scss +++ b/app/assets/stylesheets/foundation_and_overrides.scss @@ -4,6 +4,7 @@ @import 'consul_settings'; @import 'custom_settings'; @import 'foundation'; +@import 'motion-ui/motion-ui'; @include foundation-global-styles; @include foundation-grid; @@ -37,3 +38,7 @@ @include foundation-title-bar; @include foundation-top-bar; @include foundation-menu-icon; +@include foundation-orbit; + +@include motion-ui-transitions; +@include motion-ui-animations; diff --git a/app/assets/stylesheets/icons.scss b/app/assets/stylesheets/icons.scss index 5e92ee460..b9f2735b4 100644 --- a/app/assets/stylesheets/icons.scss +++ b/app/assets/stylesheets/icons.scss @@ -97,10 +97,6 @@ content: '\72'; } -.icon-documents::before { - content: '\68'; -} - .icon-proposals::before { content: '\68'; } @@ -260,3 +256,35 @@ .icon-instagram::before { content: '\32'; } + +.icon-image::before { + content: '\33'; +} + +.icon-search-plus::before { + content: '\34'; +} + +.icon-search-minus::before { + content: '\35'; +} + +.icon-calculator::before { + content: '\36'; +} + +.icon-map-marker::before { + content: '\37'; +} + +.icon-user-plus::before { + content: '\38'; +} + +.icon-file-text-o::before { + content: '\39'; +} + +.icon-file-text::before { + content: '\21'; +} diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index ffcd1e7f9..8dfe6dc14 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -18,7 +18,8 @@ // 16. Flags // 17. Activity // 18. Banners -// 19. Documents +// 19. Recommended Section Home +// 20. Documents // // 01. Global styles @@ -154,6 +155,10 @@ a { margin-bottom: $line-height; } +.margin-left { + margin-left: $line-height; +} + .margin-right { margin-right: $line-height; } @@ -205,19 +210,42 @@ a { .menu.simple { border-bottom: 1px solid $border; - margin-bottom: $line-height; + clear: both; + margin-bottom: $line-height / 2; li { - padding-bottom: rem-calc(7); + font-size: $base-font-size; + margin-bottom: 0; + margin-right: $line-height / 2; + + @include breakpoint(medium) { + margin-right: $line-height * 1.5; + } a { - color: $text-medium; + color: $text; + display: inline-block; + font-weight: bold; + position: relative; + text-align: left; + + &:hover { + color: $link; + } } &.active { border-bottom: 2px solid $brand; color: $brand; } + + &:not(.active) { + margin-bottom: $line-height / 3; + } + } + + h2 { + font-size: $base-font-size; } } @@ -281,18 +309,25 @@ a { display: inline-block; } +.back:not([class^="icon-"]) { + text-decoration: underline; +} + .tabs-content { border: 0; } .tabs { - border: { - left: 0; - right: 0; - top: 0; - }; + border-left: 0; + border-right: 0; + border-top: 0; margin-bottom: $line-height; + .tabs-title { + font-size: $base-font-size; + margin-bottom: 0; + } + .tabs-title > a { color: $text-medium; margin-bottom: rem-calc(-1); @@ -320,10 +355,32 @@ a { background: $brand; } +.truncate-horizontal-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .align-top { vertical-align: top; } +.aling-middle { + vertical-align: middle; +} + +.table { + display: table; +} + +.table-cell { + display: table-cell; +} + +.off-canvas-content { + box-shadow: none; +} + .uppercase { text-transform: uppercase; } @@ -371,7 +428,11 @@ header { .top-bar-title a { @include logo; - line-height: rem-calc(80) !important; + line-height: rem-calc(80); + + @include breakpoint(medium) { + line-height: rem-calc(80); + } &:hover { text-decoration: none; @@ -591,7 +652,7 @@ header { text-align: left; @include breakpoint(medium) { - margin-right: $line-height * 1.5; + margin-right: $line-height; } &:hover { @@ -1087,8 +1148,13 @@ form { color: #ecf00b; font-size: rem-calc(10); position: absolute; - right: 8px; + left: 12px; top: 6px; + + @include breakpoint(medium) { + left: auto; + right: 8px; + } } } @@ -1976,7 +2042,6 @@ table { // ------------ .activity { - margin-bottom: $line-height * 2; .accordion li { margin-bottom: $line-height / 2; @@ -2008,62 +2073,15 @@ table { } } - table { - border: 0; - margin-bottom: 0; - } - - td { - position: relative; - - &:first-child { - padding-left: $line-height * 1.5; - width: 75%; - } - - &::before { - color: $brand; - font-family: "icons" !important; - font-size: rem-calc(24); - left: 4px; - position: absolute; - } - } - - .activity-comments td:first-child::before { - content: 'e'; - top: 18px; - } - - .activity-debates td:first-child::before { - content: 'i'; - top: 14px; - } - - .activity-proposals { - - td:first-child::before { - content: 'h'; - top: 18px; - } - - .retired { - text-decoration: line-through; - } - } - - .activity-investment-projects td:first-child::before, - .activity-ballot td:first-child::before { - content: '\53'; - top: 10px; + .retired { + text-decoration: line-through; } } .public-interests { - margin-top: $line-height; - .column { - padding-left: 0; + li { + margin-right: $line-height / 4; } } @@ -2140,68 +2158,146 @@ table { } } -// 19. Documents -.document-form form { +// 19. Recommended Section Home +// ---------------------------- - .radio-buttons { - label { - margin-right: $line-height; - } +.home-page { + + .push { + display: none; + } +} + +.section-recommended { + padding: $line-height * 2 0; + + h2 { + margin-bottom: $line-height * 2; } - .source-option-link { - input { - padding-bottom: 0; + .debates, + .proposals, + .budget-investments { + + @include breakpoint(medium) { + margin-bottom: 0; } - .error { + @include breakpoint(small) { margin-bottom: $line-height; } - label { - &.error { - margin-bottom: 0; - } + .button.hollow { + margin-top: rem-calc(15); } } - .source-option-file { - .file-name { - label { + .card { - @include breakpoint(small medium) { - float: none; - } - - @include breakpoint(large) { - float: left; - } - } + .card-section { + padding: $line-height 0; + max-width: rem-calc(300); + margin: 0 auto; p { + font-size: rem-calc(15); + text-align: left; + } + } - @include breakpoint(small medium) { - float: none; - margin-top: 0; - margin-left: 0; - margin-bottom: 0; - } + .orbit { + height: rem-calc(300); - @include breakpoint(large) { - float: left; - margin-bottom: 0; - margin-top: $line-height / 2; - margin-left: $line-height; - } + .orbit-wrapper { + max-height: rem-calc(250); + overflow: hidden; + position: relative; + } + + .orbit-bullets { + @include orbit-bullets; + width: 100%; } } } - .attachment-errors { - margin-bottom: $line-height; + .card .orbit .orbit-wrapper .truncate { + background: image-url('truncate.png'); + background-repeat: repeat-x; + bottom: 0; + height: rem-calc(20); + position: absolute; + width: 100%; + } + + .debates-inner { + border-top: 4px solid $debates; + } + + .proposals-inner { + border-top: 4px solid $proposals; + } + + .budget-investments-inner { + border-top: 4px solid $budget; + } + + .debates-inner, + .proposals-inner, + .budget-investments-inner { + background: #fff; + max-height: rem-calc(350); + + @include breakpoint(small) { + max-height: rem-calc(400); + } + + h4 { + margin-top: $line-height; + margin-bottom: 0; + font-size: rem-calc(18); + min-height: rem-calc(50); + } + + h5 { + font-size: $small-font-size; + text-align: left; + } + } + + .carousel-image { + + .card .orbit { + height: rem-calc(480); + + .orbit-wrapper { + max-height: rem-calc(450); + } + } + + .debates-inner, + .proposals-inner, + .budget-investments-inner { + max-height: rem-calc(500); + + @include breakpoint(small) { + max-height: rem-calc(600); + } + } + } + + .carousel-image .orbit-wrapper img { + display: block; + + @include breakpoint(small) { + margin: 0 auto; + } } } +// 20. Documents +// ------------- + .documents-list { table { @@ -2258,6 +2354,31 @@ table { } } - } } + +.additional-document-link { + background: $highlight-soft; + border: 1px solid $highlight; + display: block; + margin: $line-height / 2 0; + padding: $line-height / 2; + position: relative; + + a { + word-wrap: break-word; + } + + .icon-document { + color: #007bb7; + display: inline-block; + font-size: rem-calc(24); + line-height: $line-height; + vertical-align: middle; + } +} + +.document-divider { + background: #fafafa; + border-bottom: 1px solid #eee; +} diff --git a/app/assets/stylesheets/legislation_process.scss b/app/assets/stylesheets/legislation_process.scss index 2ec9f8ee9..e536bb280 100644 --- a/app/assets/stylesheets/legislation_process.scss +++ b/app/assets/stylesheets/legislation_process.scss @@ -109,16 +109,13 @@ $border-dark: darken($border, 10%); li { cursor: pointer; display: inline-block; - margin: 0 1rem 1rem 0; + margin-bottom: $line-height; + margin-right: $line-height; transition: all 0.4s; border-bottom: 2px solid transparent; @include breakpoint(medium) { - margin-left: $line-height * 2; - } - - &:first-of-type { - margin-left: 0; + margin-bottom: 0; } &:hover, @@ -459,11 +456,11 @@ $border-dark: darken($border, 10%); span { vertical-align: inherit; font-style: normal; + } - a { - text-decoration: underline; - color: $text-medium; - } + .see-changes { + color: $text-medium; + text-decoration: underline; } } @@ -477,6 +474,7 @@ $border-dark: darken($border, 10%); } .draft-allegation { + @include breakpoint(medium) { display: flex; padding-left: 0.9375rem; @@ -493,7 +491,6 @@ $border-dark: darken($border, 10%); } } - // Panel calcs for desktop @media screen and (min-width: 40em) { .calc-index { width: calc(35% - 25px); @@ -509,6 +506,7 @@ $border-dark: darken($border, 10%); width: rem-calc(50); .draft-panel { + .panel-title { display: none; } @@ -912,19 +910,15 @@ $border-dark: darken($border, 10%); display: inline-block; } } - - .show-for-medium { - .panel-title { - display: none; - } - } } } } // 08. Legislation changes // ----------------- + .legislation-changes { + ul { list-style: none; margin-left: 0; @@ -936,35 +930,36 @@ $border-dark: darken($border, 10%); margin-right: 0.25rem; content: '—'; } + } + } - .changes-link { - display: block; - margin-left: 1rem; - font-size: $small-font-size; + .changes-link { + display: block; + margin-left: 1rem; + font-size: $small-font-size; - @include breakpoint(medium) { - display: inline-block; - } + @include breakpoint(medium) { + display: inline-block; + } - a { - span { - text-decoration: underline; - } + a { - .icon-external { - text-decoration: none; - color: #999; - line-height: 0; - vertical-align: sub; - margin-left: 0.5rem; - } + span { + text-decoration: underline; + } - &:active, - &:focus, - &:hover { - text-decoration: none; - } - } + .icon-external { + text-decoration: none; + color: #999; + line-height: 0; + vertical-align: sub; + margin-left: 0.5rem; + } + + &:active, + &:focus, + &:hover { + text-decoration: none; } } } @@ -972,6 +967,7 @@ $border-dark: darken($border, 10%); // 09. Legislation comments // ----------------- + .legislation-comments { .pull-right { @@ -1020,6 +1016,7 @@ $border-dark: darken($border, 10%); // 10. Legislation draft comment // ----------------- + .legislation-comment { .annotation-share-comment { diff --git a/app/assets/stylesheets/mixins.scss b/app/assets/stylesheets/mixins.scss index 401a2ec81..ac3c0a923 100644 --- a/app/assets/stylesheets/mixins.scss +++ b/app/assets/stylesheets/mixins.scss @@ -1,7 +1,9 @@ // Table of Contents // // 01. Logo -// +// 02. Orbit bullets +// 03. Direct uploads +// ------------------ // 01. Logo // -------- @@ -30,3 +32,108 @@ } } } + +// 02. Orbit bullet +// ---------------- + +@mixin orbit-bullets { + @include disable-mouse-outline; + position: relative; + margin-top: $orbit-bullet-margin-top; + margin-bottom: $orbit-bullet-margin-bottom; + text-align: center; + + button { + width: $orbit-bullet-diameter; + height: $orbit-bullet-diameter; + margin: $orbit-bullet-margin; + + border-radius: 50%; + background-color: $orbit-bullet-background; + + &:hover { + background-color: $orbit-bullet-background-active; + } + + &.is-active { + background-color: $orbit-bullet-background-active; + } + } +} + +// 03. Direct uploads +// ------------------ + +@mixin direct-uploads { + + .cached-image { + max-width: rem-calc(150); + max-height: rem-calc(150); + } + + .progress-bar-placeholder { + display: none; + margin-bottom: $line-height; + } + + .document, + .image { + + .document-attachment, + .image-attachment { + padding-left: 0; + + p { + margin-bottom: 0; + } + } + + .attachment-errors { + + > .js-image-attachment, + > .js-document-attachment { + display: none; + + ~ .error { + display: inline-block; + } + } + } + } + + .button { + font-weight: normal; + } + + .progress-bar { + width: 100%; + background-color: $light-gray; + } + + .file-name { + margin-top: 0; + } + + .loading-bar { + height: 5px; + width: 0; + transition: width 500ms ease-out; + + &.uploading { + background-color: $dark-gray; + } + + &.complete { + background-color: $success-color; + } + + &.errors { + background-color: $alert-color; + margin-top: $line-height / 2; + } + } + + .loading-bar.no-transition { + transition: none; + } +} diff --git a/app/assets/stylesheets/participation.scss b/app/assets/stylesheets/participation.scss index 8f85040fd..1384ed4d2 100644 --- a/app/assets/stylesheets/participation.scss +++ b/app/assets/stylesheets/participation.scss @@ -8,6 +8,7 @@ // 06. Budget // 07. Proposals successful // 08. Polls +// 09. Polls results and stats // // 01. Votes and supports @@ -249,12 +250,14 @@ .proposal-form, .budget-investment-form, .spending-proposal-form, -.document-form { +.document-form, +.topic-new, +.topic-form { .icon-debates, .icon-proposals, .icon-budget, - .icon-documents { + .icon-image { font-size: rem-calc(50); line-height: $line-height; opacity: 0.5; @@ -265,7 +268,7 @@ } .icon-proposals, - .icon-documents { + .icon-image { color: $proposals; } @@ -298,13 +301,25 @@ } .proposal-form, -.document-form { +.document-form, +.topic-form, +.topic-new { .recommendations li::before { color: $proposals; } } +.budget-investment-new, +.proposal-form, +.proposal-edit, +.polls-form, +.poll-question-form, +.legislation-process-new, +.legislation-process-edit { + @include direct-uploads; +} + // 03. Show participation // ---------------------- @@ -325,8 +340,16 @@ word-wrap: break-word; } - .callout.proposal-retired { - font-size: $base-font-size; + .callout { + &.token-message { + background-color: #fff; + border-color: $info-border; + color: $color-info; + } + + &.proposal-retired { + font-size: $base-font-size; + } } .social-share-full .social-share-button { @@ -345,11 +368,6 @@ width: rem-calc(48); } - .edit-debate, - .edit-proposal { - margin-bottom: 0; - } - .debate-info, .proposal-info, .investment-project-info, @@ -636,6 +654,71 @@ } } +.budget-investments-list .budget-investment, +.proposals-list .proposal { + + .no-image { + background: $brand; + } +} + +.budget-investments-list .budget-investment, +.proposals-list .proposal { + + @include breakpoint(small) { + + .no-image { + width: 100%; + max-width: rem-calc(300); + margin: 0 auto; + + &::before { + content: ''; + display: block; + padding-top: 100%; + } + } + } + + @include breakpoint(medium) { + + .panel { + + &.with-image { + padding: 0 $line-height / 2 0 0; + } + + .no-image { + height: 100%; + min-height: rem-calc(245); + width: rem-calc(140); + } + } + + .column:first-child { + overflow: hidden; + } + + .column:nth-child(2) { + float: left; + } + + .column:last-child:not(:first-child) { + padding-top: $line-height / 2; + } + + img { + max-width: 12rem; + } + + .budget-investment-content { + ul { + margin-bottom: 0; + } + } + } +} + .debate, .proposal, .investment-project, @@ -751,7 +834,7 @@ background: image-url('truncate.png'); background-repeat: repeat-x; bottom: 0; - height: 24px; + height: rem-calc(24); position: absolute; width: 100%; } @@ -765,12 +848,6 @@ display: none; } -.document-form { - max-width: 75rem; - margin-left: auto; - margin-right: auto; -} - .more-info { clear: both; color: $text-medium; @@ -782,7 +859,9 @@ } .debate, -.debate-show { +.debate-show, +.proposal-show, +.legislation-proposals { .votes { @include votes; @@ -797,6 +876,7 @@ } } +.proposal-show .votes, .debate-show .votes { border: 0; padding: $line-height / 2 0; @@ -855,6 +935,19 @@ } } +.budget-investment-show { + + figure { + margin: rem-calc(10) 0 0; + display: inline-block; + + figcaption { + font-size: $small-font-size; + margin-top: rem-calc(10); + } + } +} + .investment-project-show .supports, .budget-investment-show .supports { border: 0; @@ -1389,33 +1482,6 @@ } } -.featured-proposals-ballot-banner, -.sucessfull-proposals-banner { - background: #2d3e50 image-url('ballot_tiny.gif') no-repeat; - background-position: 75% 0; - position: relative; - - @include breakpoint(medium) { - margin-left: 0 !important; - margin-right: 0 !important; - } - - @include breakpoint(large) { - background: #2d3e50 image-url('ballot.gif') no-repeat; - background-position: 90% 0; - } - - h2, - a:hover h2 { - color: #ffd200 !important; - } - - p { - color: #fff; - } -} - -.sucessfull-proposals-banner, .successful .panel { .icon-successful { @@ -1464,18 +1530,8 @@ // 08. Polls // ---------------------- -.dark-heading { - background: #2d3e50; - color: #fff; - - .title { - color: #92ba48; - } - - .button { - background: #fff; - color: $brand; - } +.polls-show-header { + background: #fafafa; .callout { @@ -1491,28 +1547,117 @@ color: $color-alert; } } +} - .info { - background: #314253; - padding: $line-height; +.poll-more-info, +.poll-more-info-answers { + border-top: 1px solid #eee; +} - @include breakpoint(medium) { - border-top: rem-calc(6) solid #92ba48; +.poll-more-info-answers { + background: #fafafa; + border-bottom: 1px solid #eee; + + .column:nth-child(odd) { + border-right: 2px solid $text; + } + + .answer-divider { + border-bottom: 2px solid $text; + border-right: 0 !important; + margin-bottom: $line-height; + padding-bottom: $line-height; + } + + .answer-description { + height: 100%; + + &.short { + height: rem-calc(300); + overflow: hidden; } } +} - a:not(.button) { - color: #fff; - text-decoration: underline; +.orbit-bullets button { + background-color: #ccc; + height: $line-height / 2; + width: $line-height / 2; + + &.is-active { + background-color: $brand; } +} - .back, - .icon-angle-left { - color: #fff; +.orbit-container { + height: 100% !important; + max-height: none !important; + + li { + margin-bottom: 0 !important; } +} - &.polls-show-header { - min-height: $line-height * 8; +.orbit-slide { + max-height: none !important; + + img { + image-rendering: pixelated; + } +} + +.orbit-caption { + background: #eee; + color: $text; +} + +.orbit-next, +.orbit-previous { + background: rgba(34, 34, 34, 0.25); +} + +.zoom-link { + background: #fff; + border-radius: rem-calc(48); + color: #000; + font-size: rem-calc(24); + font-weight: bold; + height: rem-calc(48); + line-height: rem-calc(48); + right: 12px; + padding-top: rem-calc(4); + position: absolute; + text-align: center; + top: 24px; + width: rem-calc(48); + z-index: 9; + + &:hover { + background: $dark; + color: #fff; + text-decoration: none; + } +} + +.image-container { + background: #fafafa; + overflow: hidden; + position: relative; +} + +.poll { + + &.with-image { + + @include breakpoint(medium) { + padding: 0 $line-height / 2 0 0; + } + + img { + height: 100%; + max-width: none; + position: absolute; + } } } @@ -1610,9 +1755,13 @@ } .section-title-divider { - border-bottom: 2px solid $brand; - color: $brand; - margin-bottom: $line-height; + border-bottom: 1px solid #eee; + color: #000; + margin: $line-height 0; + + span { + border-bottom: 1px solid #000; + } } .poll-question { @@ -1633,6 +1782,10 @@ margin-right: $line-height / 4; min-width: rem-calc(168); + @include breakpoint(medium down) { + width: 100%; + } + &.answered { background: #f4f8ec; border: 2px solid #92ba48; @@ -1654,3 +1807,59 @@ } } } + +// 09. Polls results and stats +// --------------------------- + +.polls-results-stats { + + .sidebar { + border-bottom: 1px solid $border; + margin-bottom: $line-height; + + @include breakpoint(medium) { + border-bottom: 0; + border-right: 1px solid $border; + } + + .menu { + padding: 0; + + li a { + color: $link; + line-height: $line-height; + } + } + } + + table { + table-layout: fixed; + + caption { + padding: $line-height / 2 0; + text-align: left; + } + + th { + text-align: left; + + &.win { + background: #009fde; + } + } + + td { + + &.win { + background: #ccedf8; + font-weight: bold; + } + } + } + + .number { + font-size: rem-calc(60); + font-weight: bold; + line-height: rem-calc(60); + } +} diff --git a/app/assets/stylesheets/print.css b/app/assets/stylesheets/print.css index 6b3e5d6f6..58d2bab5e 100644 --- a/app/assets/stylesheets/print.css +++ b/app/assets/stylesheets/print.css @@ -12,7 +12,7 @@ #print_link { display: none !important; } -#responsive-menu { display: none !important; } +#responsive_menu { display: none !important; } .admin-sidebar { display: none !important; } diff --git a/app/controllers/admin/administrators_controller.rb b/app/controllers/admin/administrators_controller.rb index 938a7570d..3467ee0c0 100644 --- a/app/controllers/admin/administrators_controller.rb +++ b/app/controllers/admin/administrators_controller.rb @@ -6,16 +6,10 @@ class Admin::AdministratorsController < Admin::BaseController end def search - @user = User.find_by(email: params[:email]) - - respond_to do |format| - if @user - @administrator = Administrator.find_or_initialize_by(user: @user) - format.js - else - format.js { render "user_not_found" } - end - end + @users = User.search(params[:name_or_email]) + .includes(:administrator) + .page(params[:page]) + .for_render end def create diff --git a/app/controllers/admin/budget_investments_controller.rb b/app/controllers/admin/budget_investments_controller.rb index dd10930fb..4b6bd6b3a 100644 --- a/app/controllers/admin/budget_investments_controller.rb +++ b/app/controllers/admin/budget_investments_controller.rb @@ -51,7 +51,7 @@ class Admin::BudgetInvestmentsController < Admin::BaseController def budget_investment_params params.require(:budget_investment) - .permit(:title, :description, :external_url, :heading_id, :administrator_id, :valuation_tag_list, :incompatible, + .permit(:title, :description, :external_url, :heading_id, :administrator_id, :tag_list, :valuation_tag_list, :incompatible, :selected, valuator_ids: []) end diff --git a/app/controllers/admin/legislation/processes_controller.rb b/app/controllers/admin/legislation/processes_controller.rb index 5d8d828cd..37f547883 100644 --- a/app/controllers/admin/legislation/processes_controller.rb +++ b/app/controllers/admin/legislation/processes_controller.rb @@ -19,8 +19,10 @@ class Admin::Legislation::ProcessesController < Admin::Legislation::BaseControll def update if @process.update(process_params) + set_tag_list + link = legislation_process_path(@process).html_safe - redirect_to edit_admin_legislation_process_path(@process), notice: t('admin.legislation.processes.update.notice', link: link) + redirect_to :back, notice: t('admin.legislation.processes.update.notice', link: link) else flash.now[:error] = t('admin.legislation.processes.update.error') render :edit @@ -47,12 +49,23 @@ class Admin::Legislation::ProcessesController < Admin::Legislation::BaseControll :draft_publication_date, :allegations_start_date, :allegations_end_date, + :proposals_phase_start_date, + :proposals_phase_end_date, :result_publication_date, :debate_phase_enabled, :allegations_phase_enabled, + :proposals_phase_enabled, :draft_publication_enabled, :result_publication_enabled, - :published + :published, + :proposals_description, + :custom_list, + documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy] ) end + + def set_tag_list + @process.set_tag_list_on(:customs, process_params[:custom_list]) + @process.save + end end diff --git a/app/controllers/admin/legislation/proposals_controller.rb b/app/controllers/admin/legislation/proposals_controller.rb new file mode 100644 index 000000000..7f4441bce --- /dev/null +++ b/app/controllers/admin/legislation/proposals_controller.rb @@ -0,0 +1,7 @@ +class Admin::Legislation::ProposalsController < Admin::Legislation::BaseController + load_and_authorize_resource :process, class: "Legislation::Process" + load_and_authorize_resource :proposal, class: "Legislation::Proposal", through: :process + + def index + end +end diff --git a/app/controllers/admin/managers_controller.rb b/app/controllers/admin/managers_controller.rb index 5023a1380..865ead3be 100644 --- a/app/controllers/admin/managers_controller.rb +++ b/app/controllers/admin/managers_controller.rb @@ -6,16 +6,10 @@ class Admin::ManagersController < Admin::BaseController end def search - @user = User.find_by(email: params[:email]) - - respond_to do |format| - if @user - @manager = Manager.find_or_initialize_by(user: @user) - format.js - else - format.js { render "user_not_found" } - end - end + @users = User.search(params[:name_or_email]) + .includes(:manager) + .page(params[:page]) + .for_render end def create diff --git a/app/controllers/admin/moderators_controller.rb b/app/controllers/admin/moderators_controller.rb index 2566c0c02..cd5072aa8 100644 --- a/app/controllers/admin/moderators_controller.rb +++ b/app/controllers/admin/moderators_controller.rb @@ -6,16 +6,10 @@ class Admin::ModeratorsController < Admin::BaseController end def search - @user = User.find_by(email: params[:email]) - - respond_to do |format| - if @user - @moderator = Moderator.find_or_initialize_by(user: @user) - format.js - else - format.js { render "user_not_found" } - end - end + @users = User.search(params[:name_or_email]) + .includes(:moderator) + .page(params[:page]) + .for_render end def create diff --git a/app/controllers/admin/poll/booth_assignments_controller.rb b/app/controllers/admin/poll/booth_assignments_controller.rb index 06eb233bd..5b9ea9d7b 100644 --- a/app/controllers/admin/poll/booth_assignments_controller.rb +++ b/app/controllers/admin/poll/booth_assignments_controller.rb @@ -15,24 +15,32 @@ class Admin::Poll::BoothAssignmentsController < Admin::Poll::BaseController end def show - @booth_assignment = @poll.booth_assignments.includes(:total_results, :voters, + @booth_assignment = @poll.booth_assignments.includes(:recounts, :voters, officer_assignments: [officer: [:user]]).find(params[:id]) @voters_by_date = @booth_assignment.voters.group_by {|v| v.created_at.to_date} + @partial_results = @booth_assignment.partial_results + @recounts = @booth_assignment.recounts end def create - @booth_assignment = ::Poll::BoothAssignment.new(poll_id: booth_assignment_params[:poll_id], - booth_id: booth_assignment_params[:booth_id]) + @poll = Poll.find(booth_assignment_params[:poll_id]) + @booth = Poll::Booth.find(booth_assignment_params[:booth_id]) + @booth_assignment = ::Poll::BoothAssignment.new(poll: @poll, + booth: @booth) if @booth_assignment.save notice = t("admin.poll_booth_assignments.flash.create") else notice = t("admin.poll_booth_assignments.flash.error_create") end - redirect_to admin_poll_booth_assignments_path(@booth_assignment.poll_id), notice: notice + respond_to do |format| + format.js { render layout: false } + end end def destroy + @poll = Poll.find(booth_assignment_params[:poll_id]) + @booth = Poll::Booth.find(booth_assignment_params[:booth_id]) @booth_assignment = ::Poll::BoothAssignment.find(params[:id]) if @booth_assignment.destroy @@ -40,7 +48,14 @@ class Admin::Poll::BoothAssignmentsController < Admin::Poll::BaseController else notice = t("admin.poll_booth_assignments.flash.error_destroy") end - redirect_to admin_poll_booth_assignments_path(@booth_assignment.poll_id), notice: notice + respond_to do |format| + format.js { render layout: false } + end + end + + def manage + @booths = ::Poll::Booth.all + @poll = Poll.find(params[:poll_id]) end private diff --git a/app/controllers/admin/poll/officer_assignments_controller.rb b/app/controllers/admin/poll/officer_assignments_controller.rb index 7fa120aa6..7532362a7 100644 --- a/app/controllers/admin/poll/officer_assignments_controller.rb +++ b/app/controllers/admin/poll/officer_assignments_controller.rb @@ -18,14 +18,16 @@ class Admin::Poll::OfficerAssignmentsController < Admin::Poll::BaseController @officer = ::Poll::Officer.includes(:user).find(officer_assignment_params[:officer_id]) @officer_assignments = ::Poll::OfficerAssignment. joins(:booth_assignment). - includes(:total_results, booth_assignment: :booth). + includes(:recounts, booth_assignment: :booth). where("officer_id = ? AND poll_booth_assignments.poll_id = ?", @officer.id, @poll.id). order(:date) end def search_officers load_search - @officers = User.joins(:poll_officer).search(@search).order(username: :asc) + + poll_officers = User.where(id: @poll.officers.pluck(:user_id)) + @officers = poll_officers.search(@search).order(username: :asc) respond_to do |format| format.js diff --git a/app/controllers/admin/poll/polls_controller.rb b/app/controllers/admin/poll/polls_controller.rb index c95c8ed1f..90a31192c 100644 --- a/app/controllers/admin/poll/polls_controller.rb +++ b/app/controllers/admin/poll/polls_controller.rb @@ -1,7 +1,7 @@ class Admin::Poll::PollsController < Admin::Poll::BaseController load_and_authorize_resource - before_action :load_search, only: [:search_booths, :search_questions, :search_officers] + before_action :load_search, only: [:search_booths, :search_officers] before_action :load_geozones, only: [:new, :create, :edit, :update] def index @@ -47,23 +47,8 @@ class Admin::Poll::PollsController < Admin::Poll::BaseController redirect_to admin_poll_path(@poll), notice: notice end - def remove_question - question = ::Poll::Question.find(params[:question_id]) - - if @poll.questions.include? question - @poll.questions.delete(question) - notice = t("admin.polls.flash.question_removed") - else - notice = t("admin.polls.flash.error_on_question_removed") - end - redirect_to admin_poll_path(@poll), notice: notice - end - - def search_questions - @questions = ::Poll::Question.where("poll_id IS ? OR poll_id != ?", nil, @poll.id).search(search: @search).order(title: :asc) - respond_to do |format| - format.js - end + def booth_assignments + @polls = Poll.current_or_incoming end private @@ -73,7 +58,10 @@ class Admin::Poll::PollsController < Admin::Poll::BaseController end def poll_params - params.require(:poll).permit(:name, :starts_at, :ends_at, :geozone_restricted, geozone_ids: []) + params.require(:poll).permit(:name, :starts_at, :ends_at, :geozone_restricted, + :summary, :description, :results_enabled, :stats_enabled, + geozone_ids: [], + image_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy]) end def search_params diff --git a/app/controllers/admin/poll/questions/answers/images_controller.rb b/app/controllers/admin/poll/questions/answers/images_controller.rb new file mode 100644 index 000000000..030e177a1 --- /dev/null +++ b/app/controllers/admin/poll/questions/answers/images_controller.rb @@ -0,0 +1,33 @@ +class Admin::Poll::Questions::Answers::ImagesController < Admin::Poll::BaseController + before_action :load_answer + + def index + end + + def new + @answer = ::Poll::Question::Answer.find(params[:answer_id]) + end + + def create + @answer = ::Poll::Question::Answer.find(params[:answer_id]) + @answer.attributes = images_params + + if @answer.save + redirect_to admin_answer_images_path(@answer), + notice: "Image uploaded successfully" + else + render :new + end + end + + private + + def images_params + params.require(:poll_question_answer).permit(:answer_id, + images_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy]) + end + + def load_answer + @answer = ::Poll::Question::Answer.find(params[:answer_id]) + end +end diff --git a/app/controllers/admin/poll/questions/answers/videos_controller.rb b/app/controllers/admin/poll/questions/answers/videos_controller.rb new file mode 100644 index 000000000..e9262b6dd --- /dev/null +++ b/app/controllers/admin/poll/questions/answers/videos_controller.rb @@ -0,0 +1,57 @@ +class Admin::Poll::Questions::Answers::VideosController < Admin::Poll::BaseController + before_action :load_answer, only: [:index, :new, :create] + before_action :load_video, only: [:edit, :update, :destroy] + + def index + end + + def new + @video = ::Poll::Question::Answer::Video.new + end + + def create + @video = ::Poll::Question::Answer::Video.new(video_params) + + if @video.save + redirect_to admin_answer_videos_path(@answer), + notice: t("flash.actions.create.poll_question_answer_video") + else + render :new + end + end + + def edit + end + + def update + if @video.update(video_params) + redirect_to admin_answer_videos_path(@video.answer_id), + notice: t("flash.actions.save_changes.notice") + else + render :edit + end + end + + def destroy + notice = if @video.destroy + t("flash.actions.destroy.poll_question_answer_video") + else + t("flash.actions.destroy.error") + end + redirect_to :back, notice: notice + end + + private + + def video_params + params.require(:poll_question_answer_video).permit(:title, :url, :answer_id) + end + + def load_answer + @answer = ::Poll::Question::Answer.find(params[:answer_id]) + end + + def load_video + @video = ::Poll::Question::Answer::Video.find(params[:id]) + end +end diff --git a/app/controllers/admin/poll/questions/answers_controller.rb b/app/controllers/admin/poll/questions/answers_controller.rb new file mode 100644 index 000000000..bef176a22 --- /dev/null +++ b/app/controllers/admin/poll/questions/answers_controller.rb @@ -0,0 +1,57 @@ +class Admin::Poll::Questions::AnswersController < Admin::Poll::BaseController + before_action :load_answer, only: [:show, :edit, :update, :documents] + + load_and_authorize_resource :question, class: "::Poll::Question" + + def new + @answer = ::Poll::Question::Answer.new + end + + def create + @answer = ::Poll::Question::Answer.new(answer_params) + + if @answer.save + redirect_to admin_question_path(@answer.question), + notice: t("flash.actions.create.poll_question_answer") + else + render :new + end + end + + def show + end + + def edit + end + + def update + if @answer.update(answer_params) + redirect_to admin_question_path(@answer.question), + notice: t("flash.actions.save_changes.notice") + else + redirect_to :back + end + end + + def documents + @documents = @answer.documents + + render 'admin/poll/questions/answers/documents' + end + + def order_answers + ::Poll::Question::Answer.order_answers(params[:ordered_list]) + render nothing: true + end + + private + + def answer_params + params.require(:poll_question_answer).permit(:title, :description, :question_id, documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy]) + end + + def load_answer + @answer = ::Poll::Question::Answer.find(params[:id] || params[:answer_id]) + end + +end diff --git a/app/controllers/admin/poll/questions_controller.rb b/app/controllers/admin/poll/questions_controller.rb index 8f6c18841..64ecf4009 100644 --- a/app/controllers/admin/poll/questions_controller.rb +++ b/app/controllers/admin/poll/questions_controller.rb @@ -15,14 +15,12 @@ class Admin::Poll::QuestionsController < Admin::Poll::BaseController def new @polls = Poll.all - @question.valid_answers = I18n.t('poll_questions.default_valid_answers') proposal = Proposal.find(params[:proposal_id]) if params[:proposal_id].present? @question.copy_attributes_from_proposal(proposal) end def create @question.author = @question.proposal.try(:author) || current_user - recover_documents_from_cache(@question) if @question.save redirect_to admin_question_path(@question) @@ -32,7 +30,6 @@ class Admin::Poll::QuestionsController < Admin::Poll::BaseController end def show - @document = Document.new(documentable: @question) end def edit @@ -58,8 +55,7 @@ class Admin::Poll::QuestionsController < Admin::Poll::BaseController private def question_params - params.require(:poll_question).permit(:poll_id, :title, :question, :description, :proposal_id, :valid_answers, :video_url, - documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id]) + params.require(:poll_question).permit(:poll_id, :title, :question, :proposal_id, :video_url) end def search_params diff --git a/app/controllers/admin/poll/recounts_controller.rb b/app/controllers/admin/poll/recounts_controller.rb index 6d4a9a442..32c533eec 100644 --- a/app/controllers/admin/poll/recounts_controller.rb +++ b/app/controllers/admin/poll/recounts_controller.rb @@ -3,7 +3,7 @@ class Admin::Poll::RecountsController < Admin::Poll::BaseController def index @booth_assignments = @poll.booth_assignments. - includes(:booth, :total_results, :voters). + includes(:booth, :recounts, :voters). order("poll_booths.name"). page(params[:page]).per(50) end diff --git a/app/controllers/admin/poll/shifts_controller.rb b/app/controllers/admin/poll/shifts_controller.rb index 48f540c07..1261f2951 100644 --- a/app/controllers/admin/poll/shifts_controller.rb +++ b/app/controllers/admin/poll/shifts_controller.rb @@ -1,12 +1,13 @@ class Admin::Poll::ShiftsController < Admin::Poll::BaseController before_action :load_booth - before_action :load_polls before_action :load_officer def new load_shifts @shift = ::Poll::Shift.new + @voting_polls = @booth.polls.current_or_incoming + @recount_polls = @booth.polls.current_or_recounting_or_incoming end def create @@ -14,10 +15,10 @@ class Admin::Poll::ShiftsController < Admin::Poll::BaseController @officer = @shift.officer if @shift.save - notice = t("admin.poll_shifts.flash.create") - redirect_to new_admin_booth_shift_path(@shift.booth), notice: notice + redirect_to new_admin_booth_shift_path(@shift.booth), notice: t("admin.poll_shifts.flash.create") else load_shifts + flash[:error] = t("admin.poll_shifts.flash.date_missing") render :new end end @@ -30,7 +31,7 @@ class Admin::Poll::ShiftsController < Admin::Poll::BaseController end def search_officers - @officers = User.search(params[:search]).order(username: :asc) + @officers = User.search(params[:search]).order(username: :asc).select { |o| o.poll_officer? == true } end private @@ -39,10 +40,6 @@ class Admin::Poll::ShiftsController < Admin::Poll::BaseController @booth = ::Poll::Booth.find(params[:booth_id]) end - def load_polls - @polls = ::Poll.current_or_incoming - end - def load_shifts @shifts = @booth.shifts end @@ -54,7 +51,7 @@ class Admin::Poll::ShiftsController < Admin::Poll::BaseController end def shift_params - params.require(:shift).permit(:booth_id, :officer_id, :date) + shift_params = params.require(:shift).permit(:booth_id, :officer_id, :task, date: [:vote_collection_date, :recount_scrutiny_date]) + shift_params.merge(date: shift_params[:date]["#{shift_params[:task]}_date".to_sym]) end - end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index 63ec8cf52..14dfc6658 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -1,7 +1,7 @@ class Admin::SettingsController < Admin::BaseController def index - all_settings = (Setting.all).group_by { |s| s.type } + all_settings = Setting.all.group_by { |s| s.type } @settings = all_settings['common'] @feature_flags = all_settings['feature'] @banner_styles = all_settings['banner-style'] @@ -14,6 +14,13 @@ class Admin::SettingsController < Admin::BaseController redirect_to admin_settings_path, notice: t("admin.settings.flash.updated") end + def update_map + Setting["map_latitude"] = params[:latitude].to_f + Setting["map_longitude"] = params[:longitude].to_f + Setting["map_zoom"] = params[:zoom].to_i + redirect_to admin_settings_path, notice: t("admin.settings.index.map.flash.update") + end + private def settings_params diff --git a/app/controllers/admin/valuators_controller.rb b/app/controllers/admin/valuators_controller.rb index 4b52a753e..8b454a1a2 100644 --- a/app/controllers/admin/valuators_controller.rb +++ b/app/controllers/admin/valuators_controller.rb @@ -6,16 +6,10 @@ class Admin::ValuatorsController < Admin::BaseController end def search - @user = User.find_by(email: params[:email]) - - respond_to do |format| - if @user - @valuator = Valuator.find_or_initialize_by(user: @user) - format.js - else - format.js { render "user_not_found" } - end - end + @users = User.search(params[:name_or_email]) + .includes(:valuator) + .page(params[:page]) + .for_render end def create @@ -25,6 +19,11 @@ class Admin::ValuatorsController < Admin::BaseController redirect_to admin_valuators_path end + def destroy + @valuator.destroy + redirect_to admin_valuators_path + end + def summary @valuators = Valuator.order(spending_proposals_count: :desc) end @@ -36,4 +35,4 @@ class Admin::ValuatorsController < Admin::BaseController params.require(:valuator).permit(:user_id, :description) end -end \ No newline at end of file +end diff --git a/app/controllers/budgets/investments_controller.rb b/app/controllers/budgets/investments_controller.rb index 799d1f76d..bb00f24af 100644 --- a/app/controllers/budgets/investments_controller.rb +++ b/app/controllers/budgets/investments_controller.rb @@ -44,12 +44,10 @@ module Budgets set_comment_flags(@comment_tree.comments) load_investment_votes(@investment) @investment_ids = [@investment.id] - @document = Document.new(documentable: @investment) end def create @investment.author = current_user - recover_documents_from_cache(@investment) if @investment.save Mailer.budget_investment_created(@investment).deliver_later @@ -105,9 +103,12 @@ module Budgets end def investment_params - params.require(:budget_investment).permit(:title, :description, :external_url, :heading_id, :tag_list, - :organization_name, :location, :terms_of_service, - documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id]) + params.require(:budget_investment) + .permit(:title, :description, :external_url, :heading_id, :tag_list, + :organization_name, :location, :terms_of_service, + image_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy], + documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy], + map_location_attributes: [:latitude, :longitude, :zoom]) end def load_ballot diff --git a/app/controllers/concerns/commentable_actions.rb b/app/controllers/concerns/commentable_actions.rb index 1c47b0c06..826d90da9 100644 --- a/app/controllers/concerns/commentable_actions.rb +++ b/app/controllers/concerns/commentable_actions.rb @@ -4,17 +4,22 @@ module CommentableActions include Search def index - @resources = @search_terms.present? ? resource_model.search(@search_terms) : resource_model.all - @resources = @advanced_search_terms.present? ? @resources.filter(@advanced_search_terms) : @resources + @resources = resource_model.all + @resources = @current_order == "recommendations" && current_user.present? ? @resources.recommendations(current_user) : @resources.for_render + @resources = @resources.search(@search_terms) if @search_terms.present? + @resources = @advanced_search_terms.present? ? @resources.filter(@advanced_search_terms) : @resources @resources = @resources.tagged_with(@tag_filter) if @tag_filter - @resources = @resources.page(params[:page]).for_render.send("sort_by_#{@current_order}") + + @resources = @resources.page(params[:page]).send("sort_by_#{@current_order}") + index_customization if index_customization.present? @tag_cloud = tag_cloud @banners = Banner.with_active set_resource_votes(@resources) + set_resources_instance end @@ -57,10 +62,7 @@ module CommentableActions end def update - resource.assign_attributes(strong_params) - recover_documents_from_cache(resource) - - if resource.save + if resource.update(strong_params) redirect_to resource, notice: t("flash.actions.update.#{resource_name.underscore}") else load_categories @@ -112,11 +114,4 @@ module CommentableActions nil end - def recover_documents_from_cache(resource) - return false unless resource.try(:documents) - resource.documents = resource.documents.each do |document| - document.set_attachment_from_cached_attachment if document.cached_attachment.present? - end - end - end diff --git a/app/controllers/debates_controller.rb b/app/controllers/debates_controller.rb index b8d6bdf10..cd113d486 100644 --- a/app/controllers/debates_controller.rb +++ b/app/controllers/debates_controller.rb @@ -10,7 +10,7 @@ class DebatesController < ApplicationController invisible_captcha only: [:create, :update], honeypot: :subtitle - has_orders %w{hot_score confidence_score created_at relevance}, only: :index + has_orders ->(c) { Debate.debates_orders(c.current_user) }, only: :index has_orders %w{most_voted newest oldest}, only: :show load_and_authorize_resource diff --git a/app/controllers/direct_uploads_controller.rb b/app/controllers/direct_uploads_controller.rb new file mode 100644 index 000000000..a067a5d04 --- /dev/null +++ b/app/controllers/direct_uploads_controller.rb @@ -0,0 +1,48 @@ +class DirectUploadsController < ApplicationController + include DirectUploadsHelper + include ActionView::Helpers::UrlHelper + before_action :authenticate_user! + + load_and_authorize_resource except: :create + skip_authorization_check only: :create + + helper_method :render_destroy_upload_link + + def create + @direct_upload = DirectUpload.new(direct_upload_params.merge(user: current_user, attachment: params[:attachment])) + + if @direct_upload.valid? + @direct_upload.save_attachment + @direct_upload.relation.set_cached_attachment_from_attachment + + render json: { cached_attachment: @direct_upload.relation.cached_attachment, + filename: @direct_upload.relation.attachment.original_filename, + destroy_link: render_destroy_upload_link(@direct_upload).html_safe, + attachment_url: @direct_upload.relation.attachment.url} + else + @direct_upload.destroy_attachment + render json: { errors: @direct_upload.errors[:attachment].join(", ") }, + status: 422 + end + end + + def destroy + @direct_upload = DirectUpload.new(direct_upload_params.merge(user: current_user)) + @direct_upload.relation.set_attachment_from_cached_attachment + + if @direct_upload.destroy_attachment + render json: :ok + else + render json: :error + end + end + + private + + def direct_upload_params + params.require(:direct_upload) + .permit(:resource, :resource_type, :resource_id, :resource_relation, + :attachment, :cached_attachment, attachment_attributes: []) + end + +end \ No newline at end of file diff --git a/app/controllers/documents_controller.rb b/app/controllers/documents_controller.rb index 8f085e27f..001d23446 100644 --- a/app/controllers/documents_controller.rb +++ b/app/controllers/documents_controller.rb @@ -1,29 +1,7 @@ class DocumentsController < ApplicationController before_action :authenticate_user! - before_action :find_documentable, except: :destroy - before_action :prepare_new_document, only: [:new, :new_nested] - before_action :prepare_document_for_creation, only: :create - load_and_authorize_resource except: :upload - skip_authorization_check only: :upload - - def new - end - - def new_nested - end - - def create - recover_attachments_from_cache - - if @document.save - flash[:notice] = t "documents.actions.create.notice" - redirect_to params[:from] - else - flash[:alert] = t "documents.actions.create.alert" - render :new - end - end + load_and_authorize_resource def destroy respond_to do |format| @@ -45,57 +23,4 @@ class DocumentsController < ApplicationController end end - def destroy_upload - @document = Document.new(cached_attachment: params[:path]) - @document.set_attachment_from_cached_attachment - @document.cached_attachment = nil - @document.documentable = @documentable - - if @document.attachment.destroy - flash.now[:notice] = t "documents.actions.destroy.notice" - else - flash.now[:alert] = t "documents.actions.destroy.alert" - end - render :destroy - end - - def upload - @document = Document.new(document_params.merge(user: current_user)) - @document.documentable = @documentable - - if @document.valid? - @document.attachment.save - @document.set_cached_attachment_from_attachment(URI(request.url)) - else - @document.attachment.destroy - end - end - - private - - def document_params - params.require(:document).permit(:title, :documentable_type, :documentable_id, - :attachment, :cached_attachment, :user_id) - end - - def find_documentable - @documentable = params[:documentable_type].constantize.find_or_initialize_by(id: params[:documentable_id]) - end - - def prepare_new_document - @document = Document.new(documentable: @documentable, user_id: current_user.id) - end - - def prepare_document_for_creation - @document = Document.new(document_params) - @document.documentable = @documentable - @document.user = current_user - end - - def recover_attachments_from_cache - if @document.attachment.blank? && @document.cached_attachment.present? - @document.set_attachment_from_cached_attachment - end - end - end diff --git a/app/controllers/images_controller.rb b/app/controllers/images_controller.rb new file mode 100644 index 000000000..e273234f2 --- /dev/null +++ b/app/controllers/images_controller.rb @@ -0,0 +1,26 @@ +class ImagesController < ApplicationController + before_action :authenticate_user! + + load_and_authorize_resource + + def destroy + respond_to do |format| + format.html do + if @image.destroy + flash[:notice] = t "images.actions.destroy.notice" + else + flash[:alert] = t "images.actions.destroy.alert" + end + redirect_to params[:from] + end + format.js do + if @image.destroy + flash.now[:notice] = t "images.actions.destroy.notice" + else + flash.now[:alert] = t "images.actions.destroy.alert" + end + end + end + end + +end diff --git a/app/controllers/legislation/annotations_controller.rb b/app/controllers/legislation/annotations_controller.rb index 352fce266..ebbe05b12 100644 --- a/app/controllers/legislation/annotations_controller.rb +++ b/app/controllers/legislation/annotations_controller.rb @@ -30,7 +30,7 @@ class Legislation::AnnotationsController < ApplicationController def create if !@process.allegations_phase.open? || @draft_version.final_version? - render(json: {}, status: :not_found) && (return) + render(json: {}, status: :not_found) && return end existing_annotation = @draft_version.annotations.where( diff --git a/app/controllers/legislation/answers_controller.rb b/app/controllers/legislation/answers_controller.rb index 2e7e058e7..ecaa0037a 100644 --- a/app/controllers/legislation/answers_controller.rb +++ b/app/controllers/legislation/answers_controller.rb @@ -30,7 +30,7 @@ class Legislation::AnswersController < Legislation::BaseController def answer_params params.require(:legislation_answer).permit( - :legislation_question_option_id, + :legislation_question_option_id ) end diff --git a/app/controllers/legislation/base_controller.rb b/app/controllers/legislation/base_controller.rb index ca609ecba..b1d01a676 100644 --- a/app/controllers/legislation/base_controller.rb +++ b/app/controllers/legislation/base_controller.rb @@ -2,4 +2,8 @@ class Legislation::BaseController < ApplicationController include FeatureFlags feature_flag :legislation + + def legislation_proposal_votes(proposals) + @legislation_proposal_votes = current_user ? current_user.legislation_proposal_votes(proposals) : {} + end end diff --git a/app/controllers/legislation/processes_controller.rb b/app/controllers/legislation/processes_controller.rb index 588ffdf9e..7cde7b6d2 100644 --- a/app/controllers/legislation/processes_controller.rb +++ b/app/controllers/legislation/processes_controller.rb @@ -14,6 +14,8 @@ class Legislation::ProcessesController < Legislation::BaseController redirect_to legislation_process_draft_version_path(@process, draft_version) elsif @process.debate_phase.enabled? redirect_to debate_legislation_process_path(@process) + elsif @process.proposals_phase.enabled? + redirect_to proposals_legislation_process_path(@process) else redirect_to allegations_legislation_process_path(@process) end @@ -81,6 +83,18 @@ class Legislation::ProcessesController < Legislation::BaseController end end + def proposals + set_process + @phase = :proposals_phase + + if @process.proposals_phase.started? + legislation_proposal_votes(@process.proposals) + render :proposals + else + render :phase_not_open + end + end + private def member_method? diff --git a/app/controllers/legislation/proposals_controller.rb b/app/controllers/legislation/proposals_controller.rb new file mode 100644 index 000000000..6b5e1c973 --- /dev/null +++ b/app/controllers/legislation/proposals_controller.rb @@ -0,0 +1,72 @@ +class Legislation::ProposalsController < Legislation::BaseController + include CommentableActions + include FlagActions + + load_and_authorize_resource :process, class: "Legislation::Process" + load_and_authorize_resource :proposal, class: "Legislation::Proposal", through: :process + + before_action :parse_tag_filter, only: :index + before_action :load_categories, only: [:index, :new, :create, :edit, :map, :summary] + before_action :load_geozones, only: [:edit, :map, :summary] + before_action :authenticate_user!, except: [:index, :show, :map, :summary] + + invisible_captcha only: [:create, :update], honeypot: :subtitle + + has_orders %w{confidence_score created_at}, only: :index + has_orders %w{most_voted newest oldest}, only: :show + + helper_method :resource_model, :resource_name + respond_to :html, :js + + def show + super + legislation_proposal_votes(@process.proposals) + @document = Document.new(documentable: @proposal) + if request.path != legislation_process_proposal_path(params[:process_id], @proposal) + redirect_to legislation_process_proposal_path(params[:process_id], @proposal), + status: :moved_permanently + end + end + + def create + @proposal = Legislation::Proposal.new(proposal_params.merge(author: current_user)) + + if @proposal.save + redirect_to legislation_process_proposal_path(params[:process_id], @proposal), notice: I18n.t('flash.actions.create.proposal') + else + render :new + end + end + + def index_customization + load_successful_proposals + load_featured unless @proposal_successful_exists + end + + def vote + @proposal.register_vote(current_user, params[:value]) + legislation_proposal_votes(@proposal) + end + + private + + def proposal_params + params.require(:legislation_proposal).permit(:legislation_process_id, :title, + :question, :summary, :description, :video_url, :tag_list, + :terms_of_service, :geozone_id, + documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id]) + end + + def resource_model + Legislation::Proposal + end + + def resource_name + 'proposal' + end + + def load_successful_proposals + @proposal_successful_exists = Legislation::Proposal.successful.exists? + end + +end diff --git a/app/controllers/management/document_verifications_controller.rb b/app/controllers/management/document_verifications_controller.rb index 687d8ee52..f59c00c8f 100644 --- a/app/controllers/management/document_verifications_controller.rb +++ b/app/controllers/management/document_verifications_controller.rb @@ -4,7 +4,7 @@ class Management::DocumentVerificationsController < Management::BaseController before_action :set_document, only: :check def index - @document_verification = Verification::Management::Document.new() + @document_verification = Verification::Management::Document.new end def check diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 99f401f7b..7d833f6a1 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -11,7 +11,7 @@ class NotificationsController < ApplicationController def show @notification = current_user.notifications.find(params[:id]) - redirect_to url_for(@notification.linkable_resource) + redirect_to linkable_resource_path(@notification) end def mark_all_as_read @@ -25,4 +25,15 @@ class NotificationsController < ApplicationController @notification.mark_as_read end + def linkable_resource_path(notification) + case notification.linkable_resource.class.name + when "Budget::Investment" + budget_investment_path @notification.linkable_resource.budget, @notification.linkable_resource + when "Topic" + community_topic_path @notification.linkable_resource.community, @notification.linkable_resource + else + url_for @notification.linkable_resource + end + end + end diff --git a/app/controllers/officing/base_controller.rb b/app/controllers/officing/base_controller.rb index 97ef23d30..07cf4cfa5 100644 --- a/app/controllers/officing/base_controller.rb +++ b/app/controllers/officing/base_controller.rb @@ -7,6 +7,6 @@ class Officing::BaseController < ApplicationController skip_authorization_check def verify_officer - raise CanCan::AccessDenied unless current_user.try(:poll_officer?) || current_user.try(:administrator?) + raise CanCan::AccessDenied unless current_user.try(:poll_officer?) end end \ No newline at end of file diff --git a/app/controllers/officing/polls_controller.rb b/app/controllers/officing/polls_controller.rb index 46bcf9f37..69b65ad23 100644 --- a/app/controllers/officing/polls_controller.rb +++ b/app/controllers/officing/polls_controller.rb @@ -7,7 +7,9 @@ class Officing::PollsController < Officing::BaseController def final @polls = if current_user.poll_officer? - current_user.poll_officer.final_days_assigned_polls.select {|poll| poll.ends_at > 2.week.ago && poll.expired?} + current_user.poll_officer.final_days_assigned_polls.select do |poll| + poll.ends_at > 2.weeks.ago && poll.expired? || poll.ends_at.today? + end else [] end diff --git a/app/controllers/officing/results_controller.rb b/app/controllers/officing/results_controller.rb index 65a6deac5..39adcc7ed 100644 --- a/app/controllers/officing/results_controller.rb +++ b/app/controllers/officing/results_controller.rb @@ -5,7 +5,7 @@ class Officing::ResultsController < Officing::BaseController before_action :load_partial_results, only: :new before_action :load_officer_assignment, only: :create - before_action :check_booth_and_date, only: :create + before_action :check_officer_assignment, only: :create before_action :build_results, only: :create def new @@ -26,21 +26,15 @@ class Officing::ResultsController < Officing::BaseController @partial_results = ::Poll::PartialResult.includes(:question). where(booth_assignment_id: index_params[:booth_assignment_id]). where(date: index_params[:date]) - @whites = ::Poll::WhiteResult.where(booth_assignment_id: @booth_assignment.id, date: index_params[:date]).sum(:amount) - @nulls = ::Poll::NullResult.where(booth_assignment_id: @booth_assignment.id, date: index_params[:date]).sum(:amount) - @total = ::Poll::TotalResult.where(booth_assignment_id: @booth_assignment.id, date: index_params[:date]).sum(:amount) + @recounts = ::Poll::Recount.where(booth_assignment_id: @booth_assignment.id, date: index_params[:date]) end end private - def check_booth_and_date + def check_officer_assignment if @officer_assignment.blank? go_back_to_new(t("officing.results.flash.error_wrong_booth")) - elsif results_params[:date].blank? || - Date.parse(results_params[:date]) < @poll.starts_at.to_date || - Date.parse(results_params[:date]) > @poll.ends_at.to_date - go_back_to_new(t("officing.results.flash.error_wrong_date")) end end @@ -52,66 +46,41 @@ class Officing::ResultsController < Officing::BaseController go_back_to_new if question.blank? results.each_pair do |answer_index, count| - if count.present? - answer = question.valid_answers[answer_index.to_i] - go_back_to_new if question.blank? + next if count.blank? + answer = question.question_answers.where(given_order: answer_index.to_i + 1).first.title + go_back_to_new if question.blank? - partial_result = ::Poll::PartialResult.find_or_initialize_by(booth_assignment_id: @officer_assignment.booth_assignment_id, - date: results_params[:date], - question_id: question_id, - answer: answer) - partial_result.officer_assignment_id = @officer_assignment.id - partial_result.amount = count.to_i - partial_result.author = current_user - partial_result.origin = 'booth' - @results << partial_result - end + partial_result = ::Poll::PartialResult.find_or_initialize_by(booth_assignment_id: @officer_assignment.booth_assignment_id, + date: Date.current, + question_id: question_id, + answer: answer) + partial_result.officer_assignment_id = @officer_assignment.id + partial_result.amount = count.to_i + partial_result.author = current_user + partial_result.origin = 'booth' + @results << partial_result end end - build_white_results - build_null_results - build_total_results + build_recounts end - def build_white_results - if results_params[:whites].present? - white_result = ::Poll::WhiteResult.find_or_initialize_by(booth_assignment_id: @officer_assignment.booth_assignment_id, - date: results_params[:date]) - white_result.officer_assignment_id = @officer_assignment.id - white_result.amount = results_params[:whites].to_i - white_result.author = current_user - white_result.origin = 'booth' - @results << white_result - end - end - - def build_null_results - if results_params[:nulls].present? - null_result = ::Poll::NullResult.find_or_initialize_by(booth_assignment_id: @officer_assignment.booth_assignment_id, - date: results_params[:date]) - null_result.officer_assignment_id = @officer_assignment.id - null_result.amount = results_params[:nulls].to_i - null_result.author = current_user - null_result.origin = 'booth' - @results << null_result - end - end - - def build_total_results - if results_params[:total].present? - total_result = ::Poll::TotalResult.find_or_initialize_by(booth_assignment_id: @officer_assignment.booth_assignment_id, - date: results_params[:date]) - total_result.officer_assignment_id = @officer_assignment.id - total_result.amount = results_params[:total].to_i - total_result.author = current_user - total_result.origin = 'booth' - @results << total_result + def build_recounts + recount = ::Poll::Recount.find_or_initialize_by(booth_assignment_id: @officer_assignment.booth_assignment_id, + date: Date.current) + recount.officer_assignment_id = @officer_assignment.id + recount.author = current_user + recount.origin = 'booth' + [:whites, :nulls, :total].each do |recount_type| + if results_params[recount_type].present? + recount["#{recount_type.to_s.singularize}_amount"] = results_params[recount_type].to_i + end end + @results << recount end def go_back_to_new(alert = nil) - params[:d] = results_params[:date] + params[:d] = Date.current params[:oa] = results_params[:officer_assignment_id] flash.now[:alert] = (alert || t("officing.results.flash.error_create")) load_officer_assignments @@ -120,7 +89,7 @@ class Officing::ResultsController < Officing::BaseController end def load_poll - @poll = ::Poll.expired.includes(:questions).find(params[:poll_id]) + @poll = ::Poll.includes(:questions).find(params[:poll_id]) end def load_officer_assignment @@ -135,7 +104,7 @@ class Officing::ResultsController < Officing::BaseController final. where(id: current_user.poll_officer.officer_assignment_ids). where("poll_booth_assignments.poll_id = ?", @poll.id). - order(date: :asc) + where(date: Date.current) end def load_partial_results @@ -146,7 +115,7 @@ class Officing::ResultsController < Officing::BaseController end def results_params - params.permit(:officer_assignment_id, :date, :questions, :whites, :nulls, :total) + params.permit(:officer_assignment_id, :questions, :whites, :nulls, :total) end def index_params diff --git a/app/controllers/officing/voters_controller.rb b/app/controllers/officing/voters_controller.rb index dee1e00bd..2b7ed329f 100644 --- a/app/controllers/officing/voters_controller.rb +++ b/app/controllers/officing/voters_controller.rb @@ -3,7 +3,8 @@ class Officing::VotersController < Officing::BaseController def new @user = User.find(params[:id]) - @polls = Poll.answerable_by(@user) + booths = current_user.poll_officer.shifts.current.vote_collection.pluck(:booth_id).uniq + @polls = Poll.answerable_by(@user).where(id: Poll::BoothAssignment.where(booth: booths).pluck(:poll_id).uniq) end def create @@ -12,7 +13,9 @@ class Officing::VotersController < Officing::BaseController @voter = Poll::Voter.new(document_type: @user.document_type, document_number: @user.document_number, user: @user, - poll: @poll) + poll: @poll, + origin: "booth", + officer: current_user.poll_officer) @voter.save! end diff --git a/app/controllers/polls/questions_controller.rb b/app/controllers/polls/questions_controller.rb index bb1560f54..73e7fa160 100644 --- a/app/controllers/polls/questions_controller.rb +++ b/app/controllers/polls/questions_controller.rb @@ -5,25 +5,19 @@ class Polls::QuestionsController < ApplicationController has_orders %w{most_voted newest oldest}, only: :show - def show - @commentable = @question.proposal.present? ? @question.proposal : @question - @comment_tree = CommentTree.new(@commentable, params[:page], @current_order) - set_comment_flags(@comment_tree.comments) - - @document = Document.new(documentable: @question) - - question_answer = @question.answers.where(author_id: current_user.try(:id)).first - @answers_by_question_id = {@question.id => question_answer.try(:answer)} - end - def answer answer = @question.answers.find_or_initialize_by(author: current_user) + token = params[:token] answer.answer = params[:answer] + answer.touch if answer.persisted? answer.save! - answer.record_voter_participation + answer.record_voter_participation(token) + @question.question_answers.where(question_id: @question).each do |question_answer| + question_answer.set_most_voted + end - @answers_by_question_id = {@question.id => params[:answer]} + @answers_by_question_id = { @question.id => params[:answer] } end end diff --git a/app/controllers/polls_controller.rb b/app/controllers/polls_controller.rb index 41a038b46..708f68abb 100644 --- a/app/controllers/polls_controller.rb +++ b/app/controllers/polls_controller.rb @@ -1,8 +1,10 @@ class PollsController < ApplicationController + include PollsHelper load_and_authorize_resource has_filters %w{current expired incoming} + has_orders %w{most_voted newest oldest}, only: :show ::Poll::Answer # trigger autoload @@ -12,12 +14,24 @@ class PollsController < ApplicationController def show @questions = @poll.questions.for_render.sort_for_list + @token = poll_voter_token(@poll, current_user) + @poll_questions_answers = Poll::Question::Answer.where(question: @poll.questions).where.not(description: "").order(:given_order) @answers_by_question_id = {} poll_answers = ::Poll::Answer.by_question(@poll.question_ids).by_author(current_user.try(:id)) poll_answers.each do |answer| @answers_by_question_id[answer.question_id] = answer.answer end + + @commentable = @poll + @comment_tree = CommentTree.new(@commentable, params[:page], @current_order) + end + + def stats + @stats = Poll::Stats.new(@poll).generate + end + + def results end end diff --git a/app/controllers/proposals_controller.rb b/app/controllers/proposals_controller.rb index 06e170ee2..e86caf922 100644 --- a/app/controllers/proposals_controller.rb +++ b/app/controllers/proposals_controller.rb @@ -9,7 +9,7 @@ class ProposalsController < ApplicationController invisible_captcha only: [:create, :update], honeypot: :subtitle - has_orders %w{hot_score confidence_score created_at relevance archival_date}, only: :index + has_orders ->(c) { Proposal.proposals_orders(c.current_user) }, only: :index has_orders %w{most_voted newest oldest}, only: :show load_and_authorize_resource @@ -19,13 +19,11 @@ class ProposalsController < ApplicationController def show super @notifications = @proposal.notifications - @document = Document.new(documentable: @proposal) redirect_to proposal_path(@proposal), status: :moved_permanently if request.path != proposal_path(@proposal) end def create @proposal = Proposal.new(proposal_params.merge(author: current_user)) - recover_documents_from_cache(@proposal) if @proposal.save redirect_to share_proposal_path(@proposal), notice: I18n.t('flash.actions.create.proposal') @@ -78,7 +76,9 @@ class ProposalsController < ApplicationController def proposal_params params.require(:proposal).permit(:title, :question, :summary, :description, :external_url, :video_url, :responsible_name, :tag_list, :terms_of_service, :geozone_id, - documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id]) + image_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy], + documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy], + map_location_attributes: [:latitude, :longitude, :zoom]) end def retired_params @@ -113,7 +113,7 @@ class ProposalsController < ApplicationController end def load_featured - return unless !@advanced_search_terms && @search_terms.blank? && @tag_filter.blank? && params[:retired].blank? + return unless !@advanced_search_terms && @search_terms.blank? && @tag_filter.blank? && params[:retired].blank? && @current_order != "recommendations" @featured_proposals = Proposal.not_archived.sort_by_confidence_score.limit(3) if @featured_proposals.present? set_featured_proposal_votes(@featured_proposals) diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb new file mode 100644 index 000000000..c58067048 --- /dev/null +++ b/app/controllers/tags_controller.rb @@ -0,0 +1,11 @@ +class TagsController < ApplicationController + + load_and_authorize_resource class: ActsAsTaggableOn::Tag + respond_to :json + + def suggest + @tags = ActsAsTaggableOn::Tag.search(params[:search]).map(&:name) + respond_with @tags + end + +end diff --git a/app/controllers/welcome_controller.rb b/app/controllers/welcome_controller.rb index d0c650fa3..73b2b3aba 100644 --- a/app/controllers/welcome_controller.rb +++ b/app/controllers/welcome_controller.rb @@ -1,12 +1,10 @@ class WelcomeController < ApplicationController skip_authorization_check + before_action :set_user_recommendations, only: :index, if: :current_user layout "devise", only: [:welcome, :verification] def index - if current_user - redirect_to :proposals - end end def welcome @@ -16,4 +14,11 @@ class WelcomeController < ApplicationController redirect_to verification_path if signed_in? end + private + + def set_user_recommendations + @recommended_debates = Debate.recommendations(current_user).sort_by_recommendations.limit(3) + @recommended_proposals = Proposal.recommendations(current_user).sort_by_recommendations.limit(3) + end + end diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb index f9c2e8c69..40c5f8b84 100644 --- a/app/helpers/admin_helper.rb +++ b/app/helpers/admin_helper.rb @@ -13,33 +13,33 @@ module AdminHelper end def menu_tags? - ["tags"].include? controller_name + ["tags"].include?(controller_name) end def menu_moderated_content? - ["proposals", "debates", "comments", "hidden_users"].include? controller_name + ["proposals", "debates", "comments", "hidden_users"].include?(controller_name) && controller.class.parent != Admin::Legislation end def menu_budget? - ["spending_proposals"].include? controller_name + ["spending_proposals"].include?(controller_name) end def menu_polls? - ["polls", "questions", "officers", "booths", "officer_assignments", "booth_assignments", "recounts", "results", "shifts"].include? controller_name + %w[polls questions officers booths officer_assignments booth_assignments recounts results shifts questions answers].include?(controller_name) end def menu_profiles? - ["administrators", "organizations", "officials", "moderators", "valuators", "managers", "users"].include? controller_name + %w[administrators organizations officials moderators valuators managers users activity].include?(controller_name) end def menu_banners? - ["banners"].include? controller_name + ["banners"].include?(controller_name) end def menu_customization? - ["pages", "images", "content_blocks"].include? controller_name + ["pages", "images", "content_blocks"].include?(controller_name) end - + def official_level_options options = [["", 0]] (1..5).each do |i| diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 369cabe84..3191a2ab9 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -60,4 +60,8 @@ module ApplicationHelper def format_price(number) number_to_currency(number, precision: 0, locale: I18n.default_locale) end + + def kaminari_path(url) + "#{root_url.chomp("\/")}#{url}" + end end diff --git a/app/helpers/debates_helper.rb b/app/helpers/debates_helper.rb index 8db989f61..e880f0831 100644 --- a/app/helpers/debates_helper.rb +++ b/app/helpers/debates_helper.rb @@ -4,4 +4,12 @@ module DebatesHelper Debate.all.featured.count > 0 end -end \ No newline at end of file + def empty_recommended_debates_message_text(user) + if user.interests.any? + t('debates.index.recommendations.without_results') + else + t('debates.index.recommendations.without_interests') + end + end + +end diff --git a/app/helpers/direct_uploads_helper.rb b/app/helpers/direct_uploads_helper.rb new file mode 100644 index 000000000..87ed630d7 --- /dev/null +++ b/app/helpers/direct_uploads_helper.rb @@ -0,0 +1,16 @@ +module DirectUploadsHelper + + def render_destroy_upload_link(direct_upload) + label = direct_upload.resource_relation == "image" ? "images" : "documents" + link_to t("#{label}.form.delete_button"), + direct_upload_destroy_url("direct_upload[resource_type]": direct_upload.resource_type, + "direct_upload[resource_id]": direct_upload.resource_id, + "direct_upload[resource_relation]": direct_upload.resource_relation, + "direct_upload[cached_attachment]": direct_upload.relation.cached_attachment, + format: :json), + method: :delete, + remote: true, + class: "delete remove-cached-attachment" + end + +end \ No newline at end of file diff --git a/app/helpers/documentables_helper.rb b/app/helpers/documentables_helper.rb index edbf88131..5c3181c95 100644 --- a/app/helpers/documentables_helper.rb +++ b/app/helpers/documentables_helper.rb @@ -8,12 +8,12 @@ module DocumentablesHelper documentable.class.max_documents_allowed end - def max_file_size(documentable) - bytes_to_mega(documentable.class.max_file_size) + def max_file_size(documentable_class) + bytes_to_mega(documentable_class.max_file_size) end - def accepted_content_types(documentable) - documentable.class.accepted_content_types + def accepted_content_types(documentable_class) + documentable_class.accepted_content_types end def accepted_content_types_extensions(documentable_class) @@ -22,20 +22,19 @@ module DocumentablesHelper .join(",") end - def humanized_accepted_content_types(documentable) - documentable.class.accepted_content_types - .collect{ |content_type| content_type.split("/").last } - .join(", ") + def documentable_humanized_accepted_content_types(documentable_class) + documentable_class.accepted_content_types + .collect{ |content_type| content_type.split("/").last } + .join(", ") end def documentables_note(documentable) t "documents.form.note", max_documents_allowed: max_documents_allowed(documentable), - accepted_content_types: humanized_accepted_content_types(documentable), - max_file_size: max_file_size(documentable) + accepted_content_types: documentable_humanized_accepted_content_types(documentable.class), + max_file_size: max_file_size(documentable.class) end def max_documents_allowed?(documentable) documentable.documents.count >= documentable.class.max_documents_allowed end - end diff --git a/app/helpers/documents_helper.rb b/app/helpers/documents_helper.rb index cc39f7857..cbc94c9bb 100644 --- a/app/helpers/documents_helper.rb +++ b/app/helpers/documents_helper.rb @@ -4,7 +4,7 @@ module DocumentsHelper document.attachment_file_name end - def errors_on_attachment(document) + def document_errors_on_attachment(document) document.errors[:attachment].join(', ') if document.errors.key?(:attachment) end @@ -12,78 +12,53 @@ module DocumentsHelper bytes / Numeric::MEGABYTE end - def document_nested_field_name(document, index, field) - parent = document.documentable_type.parameterize.underscore - "#{parent.parameterize}[documents_attributes][#{index}][#{field}]" - end - - def document_nested_field_id(document, index, field) - parent = document.documentable_type.parameterize.underscore - "#{parent.parameterize}_documents_attributes_#{index}_#{field}" - end - def document_nested_field_wrapper_id(index) "document_#{index}" end - def render_destroy_document_link(document, index) - if document.persisted? + def render_destroy_document_link(builder, document) + if !document.persisted? && document.cached_attachment.present? link_to t('documents.form.delete_button'), - document_path(document, index: index, nested_document: true), - method: :delete, - remote: true, - data: { confirm: t('documents.actions.destroy.confirm') }, - class: "delete float-right" - elsif !document.persisted? && document.cached_attachment.present? - link_to t('documents.form.delete_button'), - destroy_upload_documents_path(path: document.cached_attachment, - nested_document: true, - index: index, - documentable_type: document.documentable_type, - documentable_id: document.documentable_id), - method: :delete, - remote: true, - class: "delete float-right" + direct_upload_destroy_url("direct_upload[resource_type]": document.documentable_type, + "direct_upload[resource_id]": document.documentable_id, + "direct_upload[resource_relation]": "documents", + "direct_upload[cached_attachment]": document.cached_attachment), + method: :delete, + remote: true, + class: "delete remove-cached-attachment" else - link_to t('documents.form.delete_button'), - "#", - class: "delete float-right remove-document" + link_to_remove_association t('documents.form.delete_button'), builder, class: "delete remove-document" end end - def render_attachment(document, index) - html = file_field_tag :attachment, - accept: accepted_content_types_extensions(document.documentable_type.constantize), - class: 'js-document-attachment', - data: { - url: document_direct_upload_url(document), - cached_attachment_input_field: document_nested_field_id(document, index, :cached_attachment), - multiple: false, - index: index, - nested_document: true - }, - name: document_nested_field_name(document, index, :attachment), - id: document_nested_field_id(document, index, :attachment) - if document.attachment.blank? && document.cached_attachment.blank? - klass = document.errors[:attachment].any? ? "error" : "" - html += label_tag document_nested_field_id(document, index, :attachment), - t("documents.form.attachment_label"), - class: "button hollow #{klass}" - if document.errors[:attachment].any? - html += content_tag :small, class: "error" do - errors_on_attachment(document) - end - end - end + def render_attachment(builder, document) + klass = document.errors[:attachment].any? ? "error" : "" + klass = document.persisted? || document.cached_attachment.present? ? " hide" : "" + html = builder.label :attachment, + t("documents.form.attachment_label"), + class: "button hollow #{klass}" + html += builder.file_field :attachment, + label: false, + accept: accepted_content_types_extensions(document.documentable_type.constantize), + class: 'js-document-attachment', + data: { + url: document_direct_upload_url(document), + nested_document: true + } html end def document_direct_upload_url(document) - upload_documents_url( - documentable_type: document.documentable_type, - documentable_id: document.documentable_id, - format: :js - ) + direct_uploads_url("direct_upload[resource_type]": document.documentable_type, + "direct_upload[resource_id]": document.documentable_id, + "direct_upload[resource_relation]": "documents") end + def document_item_link(document) + link_to "#{document.title} (#{document.humanized_content_type} | \ + #{number_to_human_size(document.attachment_file_size)})".html_safe, + document.attachment.url, + target: "_blank", + title: t("shared.target_blank_html") + end end diff --git a/app/helpers/flags_helper.rb b/app/helpers/flags_helper.rb index b5ba67f41..9983b34ae 100644 --- a/app/helpers/flags_helper.rb +++ b/app/helpers/flags_helper.rb @@ -12,7 +12,7 @@ module FlagsHelper def flagged?(flaggable) if flaggable.is_a? Comment - @comment_flags[flaggable.id] + @comment_flags[flaggable.id] unless flaggable.commentable_type == "Poll" else Flag.flagged?(current_user, flaggable) end diff --git a/app/helpers/imageables_helper.rb b/app/helpers/imageables_helper.rb new file mode 100644 index 000000000..8cae1b989 --- /dev/null +++ b/app/helpers/imageables_helper.rb @@ -0,0 +1,40 @@ +module ImageablesHelper + + def can_destroy_image?(imageable) + imageable.image.present? && can?(:destroy, imageable.image) + end + + def imageable_class(imageable) + imageable.class.name.parameterize('_') + end + + def imageable_max_file_size + bytes_to_megabytes(Image::MAX_IMAGE_SIZE) + end + + def bytes_to_megabytes(bytes) + bytes / Numeric::MEGABYTE + end + + def imageable_accepted_content_types + Image::ACCEPTED_CONTENT_TYPE + end + + def imageable_accepted_content_types_extensions + Image::ACCEPTED_CONTENT_TYPE + .collect{ |content_type| ".#{content_type.split('/').last}" } + .join(",") + end + + def imageable_humanized_accepted_content_types + Image::ACCEPTED_CONTENT_TYPE + .collect{ |content_type| content_type.split("/").last } + .join(", ") + end + + def imageables_note(_imageable) + t "images.form.note", accepted_content_types: imageable_humanized_accepted_content_types, + max_file_size: imageable_max_file_size + end + +end diff --git a/app/helpers/images_helper.rb b/app/helpers/images_helper.rb new file mode 100644 index 000000000..208ccf5ab --- /dev/null +++ b/app/helpers/images_helper.rb @@ -0,0 +1,79 @@ +module ImagesHelper + + def image_absolute_url(image, version) + return "" unless image + if Paperclip::Attachment.default_options[:storage] == :filesystem + URI(request.url) + image.attachment.url(version) + else + investment.image_url(version) + end + end + + def image_first_recommendation(image) + t "images.#{image.imageable.class.name.parameterize.underscore}.recommendation_one_html", + title: image.imageable.title + end + + def image_attachment_file_name(image) + image.attachment_file_name + end + + def image_errors_on_attachment(image) + image.errors[:attachment].join(', ') if image.errors.key?(:attachment) + end + + def image_bytes_to_megabytes(bytes) + bytes / Numeric::MEGABYTE + end + + def image_class(image) + image.persisted? ? "persisted-image" : "cached-image" + end + + def render_destroy_image_link(builder, image) + if !image.persisted? && image.cached_attachment.present? + link_to t('images.form.delete_button'), + direct_upload_destroy_url("direct_upload[resource_type]": image.imageable_type, + "direct_upload[resource_id]": image.imageable_id, + "direct_upload[resource_relation]": "image", + "direct_upload[cached_attachment]": image.cached_attachment), + method: :delete, + remote: true, + class: "delete remove-cached-attachment" + else + link_to_remove_association t('images.form.delete_button'), builder, class: "delete remove-image" + end + end + + def render_image_attachment(builder, imageable, image) + klass = image.errors[:attachment].any? ? "error" : "" + klass = image.persisted? || image.cached_attachment.present? ? " hide" : "" + html = builder.label :attachment, + t("images.form.attachment_label"), + class: "button hollow #{klass}" + html += builder.file_field :attachment, + label: false, + accept: imageable_accepted_content_types_extensions, + class: 'js-image-attachment', + data: { + url: image_direct_upload_url(imageable), + nested_image: true + } + + html + end + + def render_image(image, version, show_caption = true) + version = image.persisted? ? version : :original + render partial: "images/image", locals: { image: image, + version: version, + show_caption: show_caption } + end + + def image_direct_upload_url(imageable) + direct_uploads_url("direct_upload[resource_type]": imageable.class.name, + "direct_upload[resource_id]": imageable.id, + "direct_upload[resource_relation]": "image") + end + +end diff --git a/app/helpers/legislation_helper.rb b/app/helpers/legislation_helper.rb index 3a993d683..a93c2fa47 100644 --- a/app/helpers/legislation_helper.rb +++ b/app/helpers/legislation_helper.rb @@ -1,6 +1,6 @@ module LegislationHelper def format_date(date) - l(date, format: "%d %b %Y") if date + l(date, format: "%d %h %Y") if date end def format_date_for_calendar_form(date) diff --git a/app/helpers/map_locations_helper.rb b/app/helpers/map_locations_helper.rb new file mode 100644 index 000000000..ae45e7c1d --- /dev/null +++ b/app/helpers/map_locations_helper.rb @@ -0,0 +1,67 @@ +module MapLocationsHelper + + def map_location_available?(map_location) + map_location.present? && map_location.available? + end + + def map_location_latitude(map_location) + map_location.present? && map_location.latitude.present? ? map_location.latitude : Setting["map_latitude"] + end + + def map_location_longitude(map_location) + map_location.present? && map_location.longitude.present? ? map_location.longitude : Setting["map_longitude"] + end + + def map_location_zoom(map_location) + map_location.present? && map_location.zoom.present? ? map_location.zoom : Setting["map_zoom"] + end + + def map_location_input_id(prefix, attribute) + "#{prefix}_map_location_attributes_#{attribute}" + end + + def map_location_remove_marker_link_id(map_location) + "remove-marker-link-#{dom_id(map_location)}" + end + + def render_map(map_location, parent_class, editable, remove_marker_label) + map = content_tag_for :div, + map_location, + class: "map", + data: prepare_map_settings(map_location, editable, parent_class) + map += map_location_remove_marker(map_location, remove_marker_label) if editable + map + end + + def map_location_remove_marker(map_location, text) + content_tag :div, class: "text-right" do + content_tag :a, + id: map_location_remove_marker_link_id(map_location), + href: "#", + class: "location-map-remove-marker-button delete" do + text + end + end + end + + private + + def prepare_map_settings(map_location, editable, parent_class) + options = { + map: "", + map_center_latitude: map_location_latitude(map_location), + map_center_longitude: map_location_longitude(map_location), + map_zoom: map_location_zoom(map_location), + map_tiles_provider: Rails.application.secrets.map_tiles_provider, + map_tiles_provider_attribution: Rails.application.secrets.map_tiles_provider_attribution, + marker_editable: editable, + marker_latitude: map_location.latitude, + marker_longitude: map_location.longitude, + marker_remove_selector: "##{map_location_remove_marker_link_id(map_location)}", + latitude_input_selector: "##{map_location_input_id(parent_class, 'latitude')}", + longitude_input_selector: "##{map_location_input_id(parent_class, 'longitude')}", + zoom_input_selector: "##{map_location_input_id(parent_class, 'zoom')}" + } + end + +end diff --git a/app/helpers/poll_recounts_helper.rb b/app/helpers/poll_recounts_helper.rb index 716b71d85..45eefd696 100644 --- a/app/helpers/poll_recounts_helper.rb +++ b/app/helpers/poll_recounts_helper.rb @@ -1,7 +1,7 @@ module PollRecountsHelper def total_recounts_by_booth(booth_assignment) - booth_assignment.total_results.any? ? booth_assignment.total_results.to_a.sum(&:amount) : nil + booth_assignment.recounts.any? ? booth_assignment.recounts.to_a.sum(&:total_amount) : nil end end diff --git a/app/helpers/polls_helper.rb b/app/helpers/polls_helper.rb index 27d33ea04..0d5b0a605 100644 --- a/app/helpers/polls_helper.rb +++ b/app/helpers/polls_helper.rb @@ -41,4 +41,12 @@ module PollsHelper booth.name + location end + def poll_voter_token(poll, user) + Poll::Voter.where(poll: poll, user: user, origin: "web").first&.token || '' + end + + def voted_before_sign_in(question) + question.answers.where(author: current_user).any? { |vote| current_user.current_sign_in_at >= vote.updated_at } + end + end diff --git a/app/helpers/proposals_helper.rb b/app/helpers/proposals_helper.rb index d40f7950c..9d5ac40cc 100644 --- a/app/helpers/proposals_helper.rb +++ b/app/helpers/proposals_helper.rb @@ -12,8 +12,8 @@ module ProposalsHelper percentage = (proposal.total_votes.to_f * 100 / Proposal.votes_needed_for_success) case percentage when 0 then "0%" - when 0..(0.1) then "0.1%" - when (0.1)..100 then number_to_percentage(percentage, strip_insignificant_zeros: true, precision: 1) + when 0..0.1 then "0.1%" + when 0.1..100 then number_to_percentage(percentage, strip_insignificant_zeros: true, precision: 1) else "100%" end end @@ -32,8 +32,12 @@ module ProposalsHelper Proposal::RETIRE_OPTIONS.collect { |option| [ t("proposals.retire_options.#{option}"), option ] } end - def can_create_document?(document, proposal) - can?(:create, document) && proposal.documents.size < Proposal.max_documents_allowed + def empty_recommended_proposals_message_text(user) + if user.interests.any? + t('proposals.index.recommendations.without_results') + else + t('proposals.index.recommendations.without_interests') + end end def author_of_proposal?(proposal) @@ -44,4 +48,4 @@ module ProposalsHelper current_user && proposal.editable_by?(current_user) end -end \ No newline at end of file +end diff --git a/app/helpers/shifts_helper.rb b/app/helpers/shifts_helper.rb index 37f22a3e2..7ea17baf2 100644 --- a/app/helpers/shifts_helper.rb +++ b/app/helpers/shifts_helper.rb @@ -1,15 +1,30 @@ module ShiftsHelper - def shift_dates_select_options(polls) - options = [] - (start_date(polls)..end_date(polls)).each do |date| - options << [l(date, format: :long), l(date)] + def shift_vote_collection_dates(booth, polls) + return [] if polls.blank? + date_options((start_date(polls)..end_date(polls)), Poll::Shift.tasks[:vote_collection], booth) + end + + def shift_recount_scrutiny_dates(booth, polls) + return [] if polls.blank? + dates = polls.map(&:ends_at).map(&:to_date).sort.inject([]) do |total, date| + initial_date = date < Date.current ? Date.current : date + total << (initial_date..date + Poll::RECOUNT_DURATION).to_a end - options_for_select(options, params[:date]) + date_options(dates.flatten.uniq, Poll::Shift.tasks[:recount_scrutiny], booth) + end + + def date_options(dates, task_id, booth) + valid_dates(dates, task_id, booth).map { |date| [l(date, format: :long), l(date)] } + end + + def valid_dates(dates, task_id, booth) + dates.reject { |date| officer_shifts(task_id, booth).include?(date) } end def start_date(polls) - polls.map(&:starts_at).min.to_date + start_date = polls.map(&:starts_at).min.to_date + start_date < Date.current ? Date.current : start_date end def end_date(polls) @@ -20,4 +35,9 @@ module ShiftsHelper officers.collect { |officer| [officer.name, officer.id] } end + private + + def officer_shifts(task_id, booth) + @officer.shifts.where(task: task_id, booth: booth).map(&:date) + end end diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb index 4cabae278..b31f3167f 100644 --- a/app/helpers/tags_helper.rb +++ b/app/helpers/tags_helper.rb @@ -8,6 +8,8 @@ module TagsHelper proposals_path(search: tag_name) when 'budget/investment' budget_investments_path(@budget, search: tag_name) + when 'legislation/proposal' + legislation_process_proposals_path(@process, search: tag_name) else '#' end @@ -22,6 +24,8 @@ module TagsHelper proposal_path(taggable) when 'budget/investment' budget_investment_path(taggable.budget_id, taggable) + when 'legislation/proposal' + legislation_process_proposal_path(@process, taggable) else '#' end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 111a80267..da340bb46 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -52,12 +52,8 @@ module UsersHelper current_user && current_user.manager? end - def current_poll_officer? - current_user && current_user.poll_officer? - end - def show_admin_menu? - current_administrator? || current_moderator? || current_valuator? || current_manager? || current_poll_officer? + current_administrator? || current_moderator? || current_valuator? || current_manager? end def interests_title_text(user) diff --git a/app/helpers/welcome_helper.rb b/app/helpers/welcome_helper.rb new file mode 100644 index 000000000..0b483468f --- /dev/null +++ b/app/helpers/welcome_helper.rb @@ -0,0 +1,58 @@ +module WelcomeHelper + + def active_class(index) + "is-active is-in" if index.zero? + end + + def slide_display(index) + "display: none;" if index.positive? + end + + def recommended_path(recommended) + case recommended.class.name + when "Debate" + debate_path(recommended) + when "Proposal" + proposal_path(recommended) + else + '#' + end + end + + def render_recommendation_image(recommended, image_default) + image_path = calculate_image_path(recommended, image_default) + image_tag(image_path) if image_path.present? + end + + def calculate_image_path(recommended, image_default) + if recommended.try(:image) && recommended.image.present? && recommended.image.attachment.exists? + recommended.image.attachment.send("url", :medium) + elsif image_default.present? + image_default + end + end + + def calculate_carousel_size(debates, proposals, apply_offset) + offset = calculate_offset(debates, proposals, apply_offset) + centered = calculate_centered(debates, proposals) + "#{offset if offset} #{centered if centered}" + end + + def calculate_centered(debates, proposals) + if (debates.blank? && proposals.any?) || + (debates.any? && proposals.blank?) + centered = "medium-centered large-centered" + end + end + + def calculate_offset(debates, proposals, apply_offset) + if debates.any? && proposals.any? + offset = if apply_offset + "medium-offset-2 large-offset-2" + else + "end" + end + end + end + +end diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb index 773dabf34..3f136f913 100644 --- a/app/models/abilities/administrator.rb +++ b/app/models/abilities/administrator.rb @@ -14,6 +14,9 @@ module Abilities can :restore, Proposal cannot :restore, Proposal, hidden_at: nil + can :restore, Legislation::Proposal + cannot :restore, Legislation::Proposal, hidden_at: nil + can :restore, User cannot :restore, User, hidden_at: nil @@ -26,6 +29,9 @@ module Abilities can :confirm_hide, Proposal cannot :confirm_hide, Proposal, hidden_at: nil + can :confirm_hide, Legislation::Proposal + cannot :confirm_hide, Legislation::Proposal, hidden_at: nil + can :confirm_hide, User cannot :confirm_hide, User, hidden_at: nil @@ -33,11 +39,11 @@ module Abilities can :unmark_featured, Debate can :comment_as_administrator, [Debate, Comment, Proposal, Poll::Question, Budget::Investment, - Legislation::Question, Legislation::Annotation, Topic] + Legislation::Question, Legislation::Proposal, Legislation::Annotation, Topic] can [:search, :create, :index, :destroy], ::Administrator can [:search, :create, :index, :destroy], ::Moderator - can [:search, :create, :index, :summary], ::Valuator + can [:search, :create, :index, :destroy, :summary], ::Valuator can [:search, :create, :index, :destroy], ::Manager can [:search, :index], ::User @@ -45,7 +51,7 @@ module Abilities can [:read, :update, :valuate, :destroy, :summary], SpendingProposal - can [:index, :read, :new, :create, :update, :destroy, :calculate_winners], Budget + can [:index, :read, :new, :create, :update, :destroy, :calculate_winners, :read_results], Budget can [:read, :create, :update, :destroy], Budget::Group can [:read, :create, :update, :destroy], Budget::Heading can [:hide, :update, :toggle_selection], Budget::Investment @@ -56,10 +62,10 @@ module Abilities can [:index, :create, :edit, :update, :destroy], Geozone - can [:read, :create, :update, :destroy, :add_question, :remove_question, :search_booths, :search_questions, :search_officers], Poll + can [:read, :create, :update, :destroy, :add_question, :search_booths, :search_officers, :booth_assignments, :results, :stats], Poll can [:read, :create, :update, :destroy, :available], Poll::Booth can [:search, :create, :index, :destroy], ::Poll::Officer - can [:create, :destroy], ::Poll::BoothAssignment + can [:create, :destroy, :manage], ::Poll::BoothAssignment can [:create, :destroy], ::Poll::OfficerAssignment can [:read, :create, :update], Poll::Question can :destroy, Poll::Question # , comments_count: 0, votes_up: 0 @@ -71,9 +77,12 @@ module Abilities can [:manage], ::Legislation::Process can [:manage], ::Legislation::DraftVersion can [:manage], ::Legislation::Question - cannot :comment_as_moderator, [::Legislation::Question, Legislation::Annotation] + can [:manage], ::Legislation::Proposal + cannot :comment_as_moderator, [::Legislation::Question, Legislation::Annotation, ::Legislation::Proposal] can [:create, :destroy], Document + can [:destroy], Image + can [:create, :destroy], DirectUpload end end end diff --git a/app/models/abilities/common.rb b/app/models/abilities/common.rb index 620cfb212..8f289f986 100644 --- a/app/models/abilities/common.rb +++ b/app/models/abilities/common.rb @@ -18,12 +18,21 @@ module Abilities end can [:retire_form, :retire], Proposal, author_id: user.id + can :read, Legislation::Proposal + cannot [:edit, :update], Legislation::Proposal do |proposal| + proposal.editable_by?(user) + end + can [:retire_form, :retire], Legislation::Proposal, author_id: user.id + can :create, Comment can :create, Debate can :create, Proposal + can :create, Legislation::Proposal can :suggest, Debate can :suggest, Proposal + can :suggest, Legislation::Proposal + can :suggest, ActsAsTaggableOn::Tag can [:flag, :unflag], Comment cannot [:flag, :unflag], Comment, user_id: user.id @@ -34,10 +43,16 @@ module Abilities can [:flag, :unflag], Proposal cannot [:flag, :unflag], Proposal, author_id: user.id + can [:flag, :unflag], Legislation::Proposal + cannot [:flag, :unflag], Legislation::Proposal, author_id: user.id + can [:create, :destroy], Follow - can [:create, :destroy, :new], Document, documentable: { author_id: user.id } - can [:new_nested, :upload, :destroy_upload], Document + can [:destroy], Document, documentable: { author_id: user.id } + + can [:destroy], Image, imageable: { author_id: user.id } + + can [:create, :destroy], DirectUpload unless user.organization? can :vote, Debate @@ -50,10 +65,15 @@ module Abilities can :vote, SpendingProposal can :create, SpendingProposal + can :vote, Legislation::Proposal + can :vote_featured, Legislation::Proposal + can :create, Legislation::Answer + can :create, Budget::Investment, budget: { phase: "accepting" } can :suggest, Budget::Investment, budget: { phase: "accepting" } can :destroy, Budget::Investment, budget: { phase: ["accepting", "reviewing"] }, author_id: user.id can :vote, Budget::Investment, budget: { phase: "selecting" } + can [:show, :create], Budget::Ballot, budget: { phase: "balloting" } can [:create, :destroy], Budget::Ballot::Line, budget: { phase: "balloting" } diff --git a/app/models/abilities/everyone.rb b/app/models/abilities/everyone.rb index 3004ca8ad..a3720803e 100644 --- a/app/models/abilities/everyone.rb +++ b/app/models/abilities/everyone.rb @@ -7,6 +7,12 @@ module Abilities can [:read, :map, :summary, :share], Proposal can :read, Comment can :read, Poll + can :results, Poll do |poll| + poll.expired? && poll.results_enabled? + end + can :stats, Poll do |poll| + poll.expired? && poll.stats_enabled? + end can :read, Poll::Question can [:read, :welcome], Budget can :read, SpendingProposal @@ -18,10 +24,10 @@ module Abilities can [:read, :print], Budget::Investment can :read_results, Budget, phase: "finished" can :new, DirectMessage - can [:read, :debate, :draft_publication, :allegations, :result_publication], Legislation::Process, published: true + can [:read, :debate, :draft_publication, :allegations, :result_publication, :proposals], Legislation::Process, published: true can [:read, :changes, :go_to_version], Legislation::DraftVersion can [:read], Legislation::Question - can [:create], Legislation::Answer + can [:read, :map, :share], Legislation::Proposal can [:search, :comments, :read, :create, :new_comment], Legislation::Annotation end end diff --git a/app/models/abilities/moderation.rb b/app/models/abilities/moderation.rb index e9a1da2ac..f0f823de1 100644 --- a/app/models/abilities/moderation.rb +++ b/app/models/abilities/moderation.rb @@ -38,6 +38,15 @@ module Abilities can :moderate, Proposal cannot :moderate, Proposal, author_id: user.id + can :hide, Legislation::Proposal, hidden_at: nil + cannot :hide, Legislation::Proposal, author_id: user.id + + can :ignore_flag, Legislation::Proposal, ignored_flag_at: nil, hidden_at: nil + cannot :ignore_flag, Legislation::Proposal, author_id: user.id + + can :moderate, Legislation::Proposal + cannot :moderate, Legislation::Proposal, author_id: user.id + can :hide, User cannot :hide, User, id: user.id diff --git a/app/models/abilities/moderator.rb b/app/models/abilities/moderator.rb index 4e1427c12..eac434f22 100644 --- a/app/models/abilities/moderator.rb +++ b/app/models/abilities/moderator.rb @@ -6,7 +6,7 @@ module Abilities merge Abilities::Moderation.new(user) can :comment_as_moderator, [Debate, Comment, Proposal, Budget::Investment, Poll::Question, - Legislation::Question, Legislation::Annotation, Topic] + Legislation::Question, Legislation::Annotation, Legislation::Proposal, Topic] end end end diff --git a/app/models/budget/investment.rb b/app/models/budget/investment.rb index d658e7ab2..ddf6e819c 100644 --- a/app/models/budget/investment.rb +++ b/app/models/budget/investment.rb @@ -7,11 +7,12 @@ class Budget include Reclassification include Followable include Communitable + include Imageable + include Mappable include Documentable documentable max_documents_allowed: 3, max_file_size: 3.megabytes, accepted_content_types: [ "application/pdf" ] - accepts_nested_attributes_for :documents, allow_destroy: true acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/app/models/comment.rb b/app/models/comment.rb index 8b68e11ad..ccfc04602 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -3,7 +3,7 @@ class Comment < ActiveRecord::Base include HasPublicAuthor include Graphqlable - COMMENTABLE_TYPES = %w(Debate Proposal Budget::Investment Poll::Question Legislation::Question Legislation::Annotation Topic).freeze + COMMENTABLE_TYPES = %w(Debate Proposal Budget::Investment Poll::Question Legislation::Question Legislation::Annotation Topic Legislation::Proposal Poll).freeze acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases @@ -30,9 +30,11 @@ class Comment < ActiveRecord::Base scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) } scope :public_for_api, -> do where(%{(comments.commentable_type = 'Debate' and comments.commentable_id in (?)) or - (comments.commentable_type = 'Proposal' and comments.commentable_id in (?))}, + (comments.commentable_type = 'Proposal' and comments.commentable_id in (?)) or + (comments.commentable_type = 'Poll' and comments.commentable_id in (?))}, Debate.public_for_api.pluck(:id), - Proposal.public_for_api.pluck(:id)) + Proposal.public_for_api.pluck(:id), + Poll.public_for_api.pluck(:id)) end scope :sort_by_most_voted, -> { order(confidence_score: :desc, created_at: :desc) } diff --git a/app/models/comment_notifier.rb b/app/models/comment_notifier.rb index 68b350e6b..600009a23 100644 --- a/app/models/comment_notifier.rb +++ b/app/models/comment_notifier.rb @@ -22,6 +22,7 @@ class CommentNotifier end def email_on_comment? + return false if @comment.commentable.is_a?(Poll) commentable_author = @comment.commentable.author commentable_author != @author && commentable_author.email_on_comment? end diff --git a/app/models/concerns/documentable.rb b/app/models/concerns/documentable.rb index 729a0b0f8..36e91ece7 100644 --- a/app/models/concerns/documentable.rb +++ b/app/models/concerns/documentable.rb @@ -3,6 +3,7 @@ module Documentable included do has_many :documents, as: :documentable, dependent: :destroy + accepts_nested_attributes_for :documents, allow_destroy: true end module ClassMethods @@ -15,6 +16,7 @@ module Documentable @max_file_size = options[:max_file_size] @accepted_content_types = options[:accepted_content_types] end + end end diff --git a/app/models/concerns/followable.rb b/app/models/concerns/followable.rb index 84853b8ca..d635cadda 100644 --- a/app/models/concerns/followable.rb +++ b/app/models/concerns/followable.rb @@ -4,6 +4,10 @@ module Followable included do has_many :follows, as: :followable, dependent: :destroy has_many :followers, through: :follows, source: :user + + scope :followed_by_user, ->(user){ + joins(:follows).where("follows.user_id = ?", user.id) + } end def followed_by?(user) diff --git a/app/models/concerns/galleryable.rb b/app/models/concerns/galleryable.rb new file mode 100644 index 000000000..1063c5571 --- /dev/null +++ b/app/models/concerns/galleryable.rb @@ -0,0 +1,12 @@ +module Galleryable + extend ActiveSupport::Concern + + included do + has_many :images, as: :imageable, dependent: :destroy + accepts_nested_attributes_for :images, allow_destroy: true, update_only: true + + def image_url(style) + image.attachment.url(style) if image && image.attachment.exists? + end + end +end \ No newline at end of file diff --git a/app/models/concerns/graphqlable.rb b/app/models/concerns/graphqlable.rb index 228810579..c8b47cece 100644 --- a/app/models/concerns/graphqlable.rb +++ b/app/models/concerns/graphqlable.rb @@ -24,7 +24,7 @@ module Graphqlable end def graphql_type_description - (model_name.human).to_s + model_name.human.to_s end end diff --git a/app/models/concerns/imageable.rb b/app/models/concerns/imageable.rb new file mode 100644 index 000000000..a2a2f537d --- /dev/null +++ b/app/models/concerns/imageable.rb @@ -0,0 +1,14 @@ +# can [:update, :destroy ], Image, :imageable_id => user.id, :imageable_type => 'User' +# and add a feature like forbidden/without_role_images_spec.rb to test it +module Imageable + extend ActiveSupport::Concern + + included do + has_one :image, as: :imageable, dependent: :destroy + accepts_nested_attributes_for :image, allow_destroy: true, update_only: true + + def image_url(style) + image.attachment.url(style) if image && image.attachment.exists? + end + end +end diff --git a/app/models/concerns/mappable.rb b/app/models/concerns/mappable.rb new file mode 100644 index 000000000..c108bdca7 --- /dev/null +++ b/app/models/concerns/mappable.rb @@ -0,0 +1,9 @@ +module Mappable + extend ActiveSupport::Concern + + included do + has_one :map_location, dependent: :destroy + accepts_nested_attributes_for :map_location, allow_destroy: true + end + +end diff --git a/app/models/concerns/verification.rb b/app/models/concerns/verification.rb index a4770117e..b8d23f594 100644 --- a/app/models/concerns/verification.rb +++ b/app/models/concerns/verification.rb @@ -55,10 +55,9 @@ module Verification end def user_type - case - when level_three_verified? + if level_three_verified? :level_3_user - when level_two_verified? + elsif level_two_verified? :level_2_user else :level_1_user diff --git a/app/models/debate.rb b/app/models/debate.rb index 9d74597b8..65c06345d 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -37,14 +37,21 @@ class Debate < ActiveRecord::Base scope :sort_by_random, -> { reorder("RANDOM()") } scope :sort_by_relevance, -> { all } scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) } + scope :sort_by_recommendations, -> { order(cached_votes_total: :desc) } scope :last_week, -> { where("created_at >= ?", 7.days.ago)} scope :featured, -> { where("featured_at is not null")} scope :public_for_api, -> { all } + # Ahoy setup visitable # Ahoy will automatically assign visit_id on create attr_accessor :link_required + def self.recommendations(user) + tagged_with(user.interests, any: true) + .where("author_id != ?", user.id) + end + def searchable_values { title => 'A', author.username => 'B', @@ -88,7 +95,7 @@ class Debate < ActiveRecord::Base def register_vote(user, vote_value) if votable_by?(user) - Debate.increment_counter(:cached_anonymous_votes_total, id) if (user.unverified? && !user.voted_for?(self)) + Debate.increment_counter(:cached_anonymous_votes_total, id) if user.unverified? && !user.voted_for?(self) vote_by(voter: user, vote: vote_value) end end @@ -135,4 +142,9 @@ class Debate < ActiveRecord::Base featured_at.present? end + def self.debates_orders(user) + orders = %w{hot_score confidence_score created_at relevance} + orders << "recommendations" if user.present? + orders + end end diff --git a/app/models/direct_message.rb b/app/models/direct_message.rb index ae14e652d..7c652c83e 100644 --- a/app/models/direct_message.rb +++ b/app/models/direct_message.rb @@ -13,6 +13,7 @@ class DirectMessage < ActiveRecord::Base def max_per_day return if errors.any? max = Setting[:direct_message_max_per_day] + return unless max if sender.direct_messages_sent.today.count >= max.to_i errors.add(:title, I18n.t('activerecord.errors.models.direct_message.attributes.max_per_day.invalid')) diff --git a/app/models/direct_upload.rb b/app/models/direct_upload.rb new file mode 100644 index 000000000..6681aa7be --- /dev/null +++ b/app/models/direct_upload.rb @@ -0,0 +1,66 @@ +class DirectUpload + include ActiveModel::Validations + include ActiveModel::Conversion + extend ActiveModel::Naming + + attr_accessor :resource, :resource_type, :resource_id, + :relation, :resource_relation, + :attachment, :cached_attachment, :user + + validates :attachment, :resource_type, :resource_relation, :user, presence: true + validate :parent_resource_attachment_validations, + if: -> { attachment.present? && resource_type.present? && resource_relation.present? && user.present? } + + def initialize(attributes = {}) + attributes.each do |name, value| + send("#{name}=", value) + end + + if @resource_type.present? && @resource_relation.present? && (@attachment.present? || @cached_attachment.present?) + @resource = @resource_type.constantize.find_or_initialize_by(id: @resource_id) + + # Refactor + @relation = if @resource.respond_to?(:images) && + ((@attachment.present? && !@attachment.content_type.match(/pdf/)) || @cached_attachment.present?) + @resource.images.send("build", relation_attributtes) + elsif @resource.class.reflections[@resource_relation].macro == :has_one + @resource.send("build_#{resource_relation}", relation_attributtes) + else + @resource.send(@resource_relation).build(relation_attributtes) + end + + @relation.user = user + end + end + + def save_attachment + @relation.attachment.save + end + + def destroy_attachment + @relation.attachment.destroy + end + + def persisted? + false + end + + private + + def parent_resource_attachment_validations + @relation.valid? + + if @relation.errors.key? :attachment + errors[:attachment] = @relation.errors[:attachment] + end + end + + def relation_attributtes + { + attachment: @attachment, + cached_attachment: @cached_attachment, + user: @user + } + end + +end \ No newline at end of file diff --git a/app/models/document.rb b/app/models/document.rb index 3556d4c0a..3771a4e69 100644 --- a/app/models/document.rb +++ b/app/models/document.rb @@ -1,14 +1,17 @@ class Document < ActiveRecord::Base include DocumentsHelper include DocumentablesHelper - has_attached_file :attachment, path: ":rails_root/public/system/:class/:prefix/:style/:filename" + has_attached_file :attachment, url: "/system/:class/:prefix/:style/:hash.:extension", + hash_data: ":class/:style", + use_timestamp: false, + hash_secret: Rails.application.secrets.secret_key_base attr_accessor :cached_attachment belongs_to :user belongs_to :documentable, polymorphic: true # Disable paperclip security validation due to polymorphic configuration - # Paperclip do not allow to user Procs on valiations definition + # Paperclip do not allow to use Procs on valiations definition do_not_validate_attachment_file_type :attachment validate :attachment_presence validate :validate_attachment_content_type, if: -> { attachment.present? } @@ -18,13 +21,14 @@ class Document < ActiveRecord::Base validates :documentable_id, presence: true, if: -> { persisted? } validates :documentable_type, presence: true, if: -> { persisted? } - after_save :remove_cached_document, if: -> { valid? && persisted? && cached_attachment.present? } + before_save :set_attachment_from_cached_attachment, if: -> { cached_attachment.present? } + after_save :remove_cached_attachment, if: -> { cached_attachment.present? } - def set_cached_attachment_from_attachment(prefix) + def set_cached_attachment_from_attachment self.cached_attachment = if Paperclip::Attachment.default_options[:storage] == :filesystem attachment.path else - prefix + attachment.url + attachment.url end end @@ -48,23 +52,31 @@ class Document < ActiveRecord::Base end end + def humanized_content_type + attachment_content_type.split("/").last.upcase + end + private + def documentable_class + documentable_type.constantize if documentable_type.present? + end + def validate_attachment_size - if documentable.present? && - attachment_file_size > documentable.class.max_file_size + if documentable_class.present? && + attachment_file_size > documentable_class.max_file_size errors[:attachment] = I18n.t("documents.errors.messages.in_between", min: "0 Bytes", - max: "#{max_file_size(documentable)} MB") + max: "#{max_file_size(documentable_class)} MB") end end def validate_attachment_content_type - if documentable.present? && - !accepted_content_types(documentable).include?(attachment_content_type) + if documentable_class && + !accepted_content_types(documentable_class).include?(attachment_content_type) errors[:attachment] = I18n.t("documents.errors.messages.wrong_content_type", content_type: attachment_content_type, - accepted_content_types: humanized_accepted_content_types(documentable)) + accepted_content_types: documentable_humanized_accepted_content_types(documentable_class)) end end @@ -74,8 +86,12 @@ class Document < ActiveRecord::Base end end - def remove_cached_document - File.delete(cached_attachment) if File.exist?(cached_attachment) + def remove_cached_attachment + document = Document.new(documentable: documentable, + cached_attachment: cached_attachment, + user: user) + document.set_attachment_from_cached_attachment + document.attachment.destroy end end diff --git a/app/models/image.rb b/app/models/image.rb new file mode 100644 index 000000000..99061f6f1 --- /dev/null +++ b/app/models/image.rb @@ -0,0 +1,112 @@ +class Image < ActiveRecord::Base + include ImagesHelper + include ImageablesHelper + + TITLE_LEGHT_RANGE = 4..80 + MIN_SIZE = 475 + MAX_IMAGE_SIZE = 1.megabyte + ACCEPTED_CONTENT_TYPE = %w(image/jpeg image/jpg).freeze + + has_attached_file :attachment, styles: { large: "x#{MIN_SIZE}", medium: "300x300#", thumb: "140x245#" }, + url: "/system/:class/:prefix/:style/:hash.:extension", + hash_data: ":class/:style", + use_timestamp: false, + hash_secret: Rails.application.secrets.secret_key_base + attr_accessor :cached_attachment + + belongs_to :user + belongs_to :imageable, polymorphic: true + + # Disable paperclip security validation due to polymorphic configuration + # Paperclip do not allow to use Procs on valiations definition + do_not_validate_attachment_file_type :attachment + validate :attachment_presence + validate :validate_attachment_content_type, if: -> { attachment.present? } + validate :validate_attachment_size, if: -> { attachment.present? } + validates :title, presence: true, length: { in: TITLE_LEGHT_RANGE } + validates :user_id, presence: true + validates :imageable_id, presence: true, if: -> { persisted? } + validates :imageable_type, presence: true, if: -> { persisted? } + validate :validate_image_dimensions, if: -> { attachment.present? && attachment.dirty? } + + before_save :set_attachment_from_cached_attachment, if: -> { cached_attachment.present? } + after_save :remove_cached_attachment, if: -> { cached_attachment.present? } + + def set_cached_attachment_from_attachment + self.cached_attachment = if Paperclip::Attachment.default_options[:storage] == :filesystem + attachment.path + else + attachment.url + end + end + + def set_attachment_from_cached_attachment + self.attachment = if Paperclip::Attachment.default_options[:storage] == :filesystem + File.open(cached_attachment) + else + URI.parse(cached_attachment) + end + end + + Paperclip.interpolates :prefix do |attachment, style| + attachment.instance.prefix(attachment, style) + end + + def prefix(attachment, _style) + if !attachment.instance.persisted? + "cached_attachments/user/#{attachment.instance.user_id}" + else + ":attachment/:id_partition" + end + end + + private + + def imageable_class + imageable_type.constantize if imageable_type.present? + end + + def validate_image_dimensions + if attachment_of_valid_content_type? + dimensions = Paperclip::Geometry.from_file(attachment.queued_for_write[:original].path) + errors.add(:attachment, :min_image_width, required_min_width: MIN_SIZE) if dimensions.width < MIN_SIZE + errors.add(:attachment, :min_image_height, required_min_height: MIN_SIZE) if dimensions.height < MIN_SIZE + end + end + + def validate_attachment_size + if imageable_class && + attachment_file_size > 1.megabytes + errors[:attachment] = I18n.t("images.errors.messages.in_between", + min: "0 Bytes", + max: "#{imageable_max_file_size} MB") + end + end + + def validate_attachment_content_type + if imageable_class && !attachment_of_valid_content_type? + errors[:attachment] = I18n.t("images.errors.messages.wrong_content_type", + content_type: attachment_content_type, + accepted_content_types: imageable_humanized_accepted_content_types) + end + end + + def attachment_presence + if attachment.blank? && cached_attachment.blank? + errors[:attachment] = I18n.t("errors.messages.blank") + end + end + + def attachment_of_valid_content_type? + attachment.present? && imageable_accepted_content_types.include?(attachment_content_type) + end + + def remove_cached_attachment + image = Image.new(imageable: imageable, + cached_attachment: cached_attachment, + user: user) + image.set_attachment_from_cached_attachment + image.attachment.destroy + end + +end diff --git a/app/models/legislation/process.rb b/app/models/legislation/process.rb index b6043df57..ff683aa00 100644 --- a/app/models/legislation/process.rb +++ b/app/models/legislation/process.rb @@ -1,14 +1,22 @@ class Legislation::Process < ActiveRecord::Base - acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases + include Taggable + include Documentable + documentable max_documents_allowed: 3, + max_file_size: 3.megabytes, + accepted_content_types: [ "application/pdf" ] - PHASES_AND_PUBLICATIONS = %i(debate_phase allegations_phase draft_publication result_publication).freeze + acts_as_paranoid column: :hidden_at + acts_as_taggable_on :customs + + PHASES_AND_PUBLICATIONS = %i(debate_phase allegations_phase proposals_phase draft_publication result_publication).freeze 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 + has_many :proposals, -> { order(:id) }, class_name: 'Legislation::Proposal', foreign_key: 'legislation_process_id', dependent: :destroy validates :title, presence: true validates :start_date, presence: true @@ -17,6 +25,7 @@ class Legislation::Process < ActiveRecord::Base 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? + validates :proposals_phase_end_date, presence: true, if: :proposals_phase_start_date? validate :valid_date_ranges scope :open, -> { where("start_date <= ? and end_date >= ?", Date.current, Date.current).order('id DESC') } @@ -33,6 +42,10 @@ class Legislation::Process < ActiveRecord::Base Legislation::Process::Phase.new(allegations_start_date, allegations_end_date, allegations_phase_enabled) end + def proposals_phase + Legislation::Process::Phase.new(proposals_phase_start_date, proposals_phase_end_date, proposals_phase_enabled) + end + def draft_publication Legislation::Process::Publication.new(draft_publication_date, draft_publication_enabled) end diff --git a/app/models/legislation/proposal.rb b/app/models/legislation/proposal.rb new file mode 100644 index 000000000..56eaac24b --- /dev/null +++ b/app/models/legislation/proposal.rb @@ -0,0 +1,144 @@ +class Legislation::Proposal < ActiveRecord::Base + include ActsAsParanoidAliases + include Flaggable + include Taggable + include Conflictable + include Measurable + include Sanitizable + include Searchable + include Filterable + include Followable + include Communitable + include Documentable + + documentable max_documents_allowed: 3, + max_file_size: 3.megabytes, + accepted_content_types: [ "application/pdf" ] + accepts_nested_attributes_for :documents, allow_destroy: true + + acts_as_votable + acts_as_paranoid column: :hidden_at + + belongs_to :process, class_name: 'Legislation::Process', foreign_key: 'legislation_process_id' + belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' + belongs_to :geozone + has_many :comments, as: :commentable + + validates :title, presence: true + validates :summary, presence: true + validates :author, presence: true + + validates :title, length: { in: 4..Legislation::Proposal.title_max_length } + validates :description, length: { maximum: Legislation::Proposal.description_max_length } + + validates :terms_of_service, acceptance: { allow_nil: false }, on: :create + + before_validation :set_responsible_name + + before_save :calculate_hot_score, :calculate_confidence_score + + scope :for_render, -> { includes(:tags) } + scope :sort_by_hot_score, -> { reorder(hot_score: :desc) } + scope :sort_by_confidence_score, -> { reorder(confidence_score: :desc) } + scope :sort_by_created_at, -> { reorder(created_at: :desc) } + scope :sort_by_most_commented, -> { reorder(comments_count: :desc) } + scope :sort_by_random, -> { reorder("RANDOM()") } + scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) } + scope :last_week, -> { where("proposals.created_at >= ?", 7.days.ago)} + + def to_param + "#{id}-#{title}".parameterize + end + + def searchable_values + { title => 'A', + question => 'B', + author.username => 'B', + tag_list.join(' ') => 'B', + geozone.try(:name) => 'B', + summary => 'C', + description => 'D'} + end + + def self.search(terms) + by_code = search_by_code(terms.strip) + by_code.present? ? by_code : pg_search(terms) + end + + def self.search_by_code(terms) + matched_code = match_code(terms) + results = where(id: matched_code[1]) if matched_code + return results if results.present? && results.first.code == terms + end + + def self.match_code(terms) + /\A#{Setting["proposal_code_prefix"]}-\d\d\d\d-\d\d-(\d*)\z/.match(terms) + end + + def likes + cached_votes_up + end + + def dislikes + cached_votes_down + end + + def total_votes + cached_votes_total + end + + def voters + User.active.where(id: votes_for.voters) + end + + def editable? + total_votes <= Setting["max_votes_for_proposal_edit"].to_i + end + + def editable_by?(user) + author_id == user.id && editable? + end + + def votable_by?(user) + user && user.level_two_or_three_verified? + end + + def register_vote(user, vote_value) + vote_by(voter: user, vote: vote_value) if votable_by?(user) + end + + def code + "#{Setting['proposal_code_prefix']}-#{created_at.strftime('%Y-%m')}-#{id}" + end + + def after_commented + save # updates the hot_score because there is a before_save + end + + def calculate_hot_score + self.hot_score = ScoreCalculator.hot_score(created_at, + total_votes, + total_votes, + comments_count) + end + + def calculate_confidence_score + self.confidence_score = ScoreCalculator.confidence_score(total_votes, total_votes) + end + + def after_hide + tags.each{ |t| t.decrement_custom_counter_for('LegislationProposal') } + end + + def after_restore + tags.each{ |t| t.increment_custom_counter_for('LegislationProposal') } + end + + protected + + def set_responsible_name + if author && author.document_number? + self.responsible_name = author.document_number + end + end +end diff --git a/app/models/map_location.rb b/app/models/map_location.rb new file mode 100644 index 000000000..14e91d92c --- /dev/null +++ b/app/models/map_location.rb @@ -0,0 +1,10 @@ +class MapLocation < ActiveRecord::Base + + belongs_to :proposal + belongs_to :investment, class_name: Budget::Investment + + def available? + latitude.present? && longitude.present? && zoom.present? + end + +end diff --git a/app/models/poll.rb b/app/models/poll.rb index 3873ac5e0..ffb4e36f4 100644 --- a/app/models/poll.rb +++ b/app/models/poll.rb @@ -1,38 +1,50 @@ class Poll < ActiveRecord::Base + include Imageable + acts_as_paranoid column: :hidden_at + include ActsAsParanoidAliases + + RECOUNT_DURATION = 1.week + has_many :booth_assignments, class_name: "Poll::BoothAssignment" has_many :booths, through: :booth_assignments has_many :partial_results, through: :booth_assignments - has_many :white_results, through: :booth_assignments - has_many :null_results, through: :booth_assignments - has_many :total_results, through: :booth_assignments + has_many :recounts, through: :booth_assignments has_many :voters has_many :officer_assignments, through: :booth_assignments has_many :officers, through: :officer_assignments has_many :questions + has_many :comments, as: :commentable has_and_belongs_to_many :geozones + belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' validates :name, presence: true validate :date_range - scope :current, -> { where('starts_at <= ? and ? <= ends_at', Time.current, Time.current) } - scope :incoming, -> { where('? < starts_at', Time.current) } - scope :expired, -> { where('ends_at < ?', Time.current) } + scope :current, -> { where('starts_at <= ? and ? <= ends_at', Date.current.beginning_of_day, Date.current.beginning_of_day) } + scope :incoming, -> { where('? < starts_at', Date.current.beginning_of_day) } + scope :expired, -> { where('ends_at < ?', Date.current.beginning_of_day) } + scope :recounting, -> { Poll.where(ends_at: (Date.current.beginning_of_day - RECOUNT_DURATION)..Date.current.beginning_of_day) } scope :published, -> { where('published = ?', true) } scope :by_geozone_id, ->(geozone_id) { where(geozones: {id: geozone_id}.joins(:geozones)) } + scope :public_for_api, -> { all } scope :sort_for_list, -> { order(:geozone_restricted, :starts_at, :name) } - def current?(timestamp = DateTime.current) + def title + name + end + + def current?(timestamp = Date.current.beginning_of_day) starts_at <= timestamp && timestamp <= ends_at end - def incoming?(timestamp = DateTime.current) + def incoming?(timestamp = Date.current.beginning_of_day) timestamp < starts_at end - def expired?(timestamp = DateTime.current) + def expired?(timestamp = Date.current.beginning_of_day) ends_at < timestamp end @@ -40,6 +52,10 @@ class Poll < ActiveRecord::Base current + incoming end + def self.current_or_recounting_or_incoming + current + recounting + incoming + end + def answerable_by?(user) user.present? && user.level_two_or_three_verified? && @@ -61,6 +77,14 @@ class Poll < ActiveRecord::Base voters.where(document_number: document_number, document_type: document_type).exists? end + def voted_in_booth?(user) + Poll::Voter.where(poll: self, user: user, origin: "booth").exists? + end + + def voted_in_web?(user) + Poll::Voter.where(poll: self, user: user, origin: "web").exists? + end + def date_range unless starts_at.present? && ends_at.present? && starts_at <= ends_at errors.add(:starts_at, I18n.t('errors.messages.invalid_date_range')) diff --git a/app/models/poll/answer.rb b/app/models/poll/answer.rb index a1ff30f0a..78122188a 100644 --- a/app/models/poll/answer.rb +++ b/app/models/poll/answer.rb @@ -8,12 +8,14 @@ class Poll::Answer < ActiveRecord::Base validates :question, presence: true validates :author, presence: true validates :answer, presence: true - validates :answer, inclusion: {in: ->(a) { a.question.valid_answers }} + + validates :answer, inclusion: { in: ->(a) { a.question.question_answers.pluck(:title) }}, + unless: ->(a) { a.question.blank? } scope :by_author, ->(author_id) { where(author_id: author_id) } scope :by_question, ->(question_id) { where(question_id: question_id) } - def record_voter_participation - Poll::Voter.create!(user: author, poll: poll) + def record_voter_participation(token) + Poll::Voter.find_or_create_by(user: author, poll: poll, origin: "web", token: token) end -end \ No newline at end of file +end diff --git a/app/models/poll/booth.rb b/app/models/poll/booth.rb index 04f9d3dea..b9cba45b1 100644 --- a/app/models/poll/booth.rb +++ b/app/models/poll/booth.rb @@ -12,8 +12,11 @@ class Poll end def self.available - where(polls: { id: Poll.current_or_incoming }).includes(:polls) + where(polls: { id: Poll.current_or_recounting_or_incoming }).includes(:polls) end + def assignment_on_poll(poll) + booth_assignments.where(poll: poll).first + end end -end \ No newline at end of file +end diff --git a/app/models/poll/booth_assignment.rb b/app/models/poll/booth_assignment.rb index 8b4b655ef..8489c3cf0 100644 --- a/app/models/poll/booth_assignment.rb +++ b/app/models/poll/booth_assignment.rb @@ -7,8 +7,6 @@ class Poll has_many :officers, through: :officer_assignments has_many :voters has_many :partial_results - has_many :white_results - has_many :null_results - has_many :total_results + has_many :recounts end end diff --git a/app/models/poll/null_result.rb b/app/models/poll/null_result.rb deleted file mode 100644 index d10fe3cb0..000000000 --- a/app/models/poll/null_result.rb +++ /dev/null @@ -1,23 +0,0 @@ -class Poll::NullResult < ActiveRecord::Base - - VALID_ORIGINS = %w{web booth} - - belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' - belongs_to :booth_assignment - belongs_to :officer_assignment - - validates :author, presence: true - validates :origin, inclusion: {in: VALID_ORIGINS} - - scope :by_author, ->(author_id) { where(author_id: author_id) } - - before_save :update_logs - - def update_logs - if amount_changed? && amount_was.present? - self.amount_log += ":#{amount_was.to_s}" - self.officer_assignment_id_log += ":#{officer_assignment_id_was.to_s}" - self.author_id_log += ":#{author_id_was.to_s}" - end - end -end diff --git a/app/models/poll/officer.rb b/app/models/poll/officer.rb index bf4c73c36..384202455 100644 --- a/app/models/poll/officer.rb +++ b/app/models/poll/officer.rb @@ -2,6 +2,7 @@ class Poll class Officer < ActiveRecord::Base belongs_to :user has_many :officer_assignments, class_name: "Poll::OfficerAssignment" + has_many :shifts, class_name: "Poll::Shift" has_many :failed_census_calls, foreign_key: :poll_officer_id validates :user_id, presence: true, uniqueness: true diff --git a/app/models/poll/officer_assignment.rb b/app/models/poll/officer_assignment.rb index 8e86d309c..417e6a150 100644 --- a/app/models/poll/officer_assignment.rb +++ b/app/models/poll/officer_assignment.rb @@ -3,14 +3,12 @@ class Poll belongs_to :officer belongs_to :booth_assignment has_many :partial_results - has_many :white_results - has_many :null_results - has_many :total_results + has_many :recounts has_many :voters validates :officer_id, presence: true validates :booth_assignment_id, presence: true - validates :date, presence: true, uniqueness: { scope: [:officer_id, :booth_assignment_id] } + validates :date, presence: true delegate :poll_id, :booth_id, to: :booth_assignment diff --git a/app/models/poll/partial_result.rb b/app/models/poll/partial_result.rb index 12b16aa3a..0550650a5 100644 --- a/app/models/poll/partial_result.rb +++ b/app/models/poll/partial_result.rb @@ -10,8 +10,9 @@ class Poll::PartialResult < ActiveRecord::Base validates :question, presence: true validates :author, presence: true validates :answer, presence: true - validates :answer, inclusion: {in: ->(a) { a.question.valid_answers }} - validates :origin, inclusion: {in: VALID_ORIGINS} + validates :answer, inclusion: { in: ->(a) { a.question.question_answers.pluck(:title) }}, + unless: ->(a) { a.question.blank? } + validates :origin, inclusion: { in: VALID_ORIGINS } scope :by_author, ->(author_id) { where(author_id: author_id) } scope :by_question, ->(question_id) { where(question_id: question_id) } diff --git a/app/models/poll/question.rb b/app/models/poll/question.rb index a46f28a8c..6be729757 100644 --- a/app/models/poll/question.rb +++ b/app/models/poll/question.rb @@ -1,11 +1,6 @@ class Poll::Question < ActiveRecord::Base include Measurable include Searchable - include Documentable - documentable max_documents_allowed: 1, - max_file_size: 3.megabytes, - accepted_content_types: [ "application/pdf" ] - accepts_nested_attributes_for :documents, allow_destroy: true acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases @@ -14,7 +9,8 @@ class Poll::Question < ActiveRecord::Base belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' has_many :comments, as: :commentable - has_many :answers + has_many :answers, class_name: 'Poll::Answer' + has_many :question_answers, -> { order 'given_order asc' }, class_name: 'Poll::Question::Answer' has_many :partial_results belongs_to :proposal @@ -23,7 +19,6 @@ class Poll::Question < ActiveRecord::Base validates :poll_id, presence: true validates :title, length: { minimum: 4 } - validates :description, length: { maximum: Poll::Question.description_max_length } scope :by_poll_id, ->(poll_id) { where(poll_id: poll_id) } @@ -40,27 +35,16 @@ class Poll::Question < ActiveRecord::Base def searchable_values { title => 'A', proposal.try(:title) => 'A', - description => 'B', author.username => 'C', author_visible_name => 'C' } end - def description - super.try :html_safe - end - - def valid_answers - (super.try(:split, ',').compact || []).map(&:strip) - end - def copy_attributes_from_proposal(proposal) if proposal.present? self.author = proposal.author self.author_visible_name = proposal.author.name self.proposal_id = proposal.id self.title = proposal.title - self.description = proposal.description - self.valid_answers = I18n.t('poll_questions.default_valid_answers') end end @@ -71,4 +55,8 @@ class Poll::Question < ActiveRecord::Base where(poll_id: Poll.answerable_by(user).pluck(:id)) end + def answers_total_votes + question_answers.map { |a| Poll::Answer.where(question_id: self, answer: a.title).count }.sum + end + end diff --git a/app/models/poll/question/answer.rb b/app/models/poll/question/answer.rb new file mode 100644 index 000000000..1c10e2b5c --- /dev/null +++ b/app/models/poll/question/answer.rb @@ -0,0 +1,54 @@ +class Poll::Question::Answer < ActiveRecord::Base + include Galleryable + include Documentable + documentable max_documents_allowed: 3, + max_file_size: 3.megabytes, + accepted_content_types: [ "application/pdf" ] + accepts_nested_attributes_for :documents, allow_destroy: true + + belongs_to :question, class_name: 'Poll::Question', foreign_key: 'question_id' + has_many :videos, class_name: 'Poll::Question::Answer::Video' + + validates :title, presence: true + validates :given_order, presence: true, uniqueness: { scope: :question_id } + + before_validation :set_order, on: :create + + def description + super.try :html_safe + end + + def self.order_answers(ordered_array) + ordered_array.each_with_index do |answer_id, order| + find(answer_id).update_attribute(:given_order, (order + 1)) + end + end + + def set_order + self.given_order = self.class.last_position(question_id) + 1 + end + + def self.last_position(question_id) + where(question_id: question_id).maximum('given_order') || 0 + end + + def total_votes + Poll::Answer.where(question_id: question, answer: title).count + end + + def most_voted? + most_voted + end + + def total_votes_percentage + question.answers_total_votes.zero? ? 0 : (total_votes * 100) / question.answers_total_votes + end + + def set_most_voted + answers = question.question_answers + .map { |a| Poll::Answer.where(question_id: a.question, answer: a.title).count } + is_most_voted = answers.none?{ |a| a > total_votes } + + update(most_voted: is_most_voted) + end +end diff --git a/app/models/poll/question/answer/video.rb b/app/models/poll/question/answer/video.rb new file mode 100644 index 000000000..3d214af96 --- /dev/null +++ b/app/models/poll/question/answer/video.rb @@ -0,0 +1,16 @@ +class Poll::Question::Answer::Video < ActiveRecord::Base + belongs_to :answer, class_name: 'Poll::Question::Answer', foreign_key: 'answer_id' + + VIMEO_REGEX = /vimeo.*(staffpicks\/|channels\/|videos\/|video\/|\/)([^#\&\?]*).*/ + YOUTUBE_REGEX = /youtu.*(be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/ + + validates :title, presence: true + validate :valid_url? + + def valid_url? + return if url.blank? + return if url.match(VIMEO_REGEX) + return if url.match(YOUTUBE_REGEX) + errors.add(:url, :invalid) + end +end diff --git a/app/models/poll/recount.rb b/app/models/poll/recount.rb new file mode 100644 index 000000000..74a6fe937 --- /dev/null +++ b/app/models/poll/recount.rb @@ -0,0 +1,36 @@ +class Poll::Recount < ActiveRecord::Base + + VALID_ORIGINS = %w{web booth letter}.freeze + + belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' + belongs_to :booth_assignment + belongs_to :officer_assignment + + validates :author, presence: true + validates :origin, inclusion: {in: VALID_ORIGINS} + + scope :web, -> { where(origin: 'web') } + scope :booth, -> { where(origin: 'booth') } + scope :letter, -> { where(origin: 'letter') } + + scope :by_author, ->(author_id) { where(author_id: author_id) } + + before_save :update_logs + + def update_logs + amounts_changed = false + + [:white, :null, :total].each do |amount| + next unless send("#{amount}_amount_changed?") && send("#{amount}_amount_was").present? + self["#{amount}_amount_log"] += ":#{send("#{amount}_amount_was")}" + amounts_changed = true + end + + update_officer_author if amounts_changed + end + + def update_officer_author + self.officer_assignment_id_log += ":#{officer_assignment_id_was}" + self.author_id_log += ":#{author_id_was}" + end +end diff --git a/app/models/poll/shift.rb b/app/models/poll/shift.rb index d894df5a4..e3cfd2280 100644 --- a/app/models/poll/shift.rb +++ b/app/models/poll/shift.rb @@ -5,25 +5,41 @@ class Poll validates :booth_id, presence: true validates :officer_id, presence: true - validates :date, presence: true - validates :date, uniqueness: { scope: [:officer_id, :booth_id] } + validates :date, presence: true, uniqueness: { scope: [:officer_id, :booth_id, :task] } + validates :task, presence: true + + enum task: { vote_collection: 0, recount_scrutiny: 1 } + + scope :vote_collection, -> { where(task: 'vote_collection') } + scope :recount_scrutiny, -> { where(task: 'recount_scrutiny') } + scope :current, -> { where(date: Date.current) } before_create :persist_data after_create :create_officer_assignments - - def create_officer_assignments - booth.booth_assignments.each do |booth_assignment| - attrs = { officer_id: officer_id, - date: date, - booth_assignment_id: booth_assignment.id } - Poll::OfficerAssignment.create!(attrs) - end - end + before_destroy :destroy_officer_assignments def persist_data self.officer_name = officer.name self.officer_email = officer.email end + def create_officer_assignments + booth.booth_assignments.each do |booth_assignment| + attrs = { + officer_id: officer_id, + date: date, + booth_assignment_id: booth_assignment.id, + final: recount_scrutiny? + } + Poll::OfficerAssignment.create!(attrs) + end + end + + def destroy_officer_assignments + Poll::OfficerAssignment.where(booth_assignment: booth.booth_assignments, + officer: officer, + date: date, + final: recount_scrutiny?).destroy_all + end end end diff --git a/app/models/poll/stats.rb b/app/models/poll/stats.rb new file mode 100644 index 000000000..f07795a73 --- /dev/null +++ b/app/models/poll/stats.rb @@ -0,0 +1,143 @@ +class Poll + class Stats + + def initialize(poll) + @poll = poll + end + + def generate + stats = %w[total_participants total_participants_web total_web_valid total_web_white total_web_null + total_participants_booth total_booth_valid total_booth_white total_booth_null + total_valid_votes total_white_votes total_null_votes valid_percentage_web valid_percentage_booth + total_valid_percentage white_percentage_web white_percentage_booth total_white_percentage + null_percentage_web null_percentage_booth total_null_percentage total_participants_web_percentage + total_participants_booth_percentage] + stats.map { |stat_name| [stat_name.to_sym, send(stat_name)] }.to_h + end + + private + + def total_participants + stats_cache('total_participants') { total_participants_web + total_participants_booth } + end + + def total_participants_web + stats_cache('total_participants_web') { total_web_valid + total_web_white + total_web_null } + end + + def total_participants_web_percentage + stats_cache('total_participants_web_percentage') do + total_participants.zero? ? 0 : total_participants_web * 100 / total_participants + end + end + + def total_participants_booth + stats_cache('total_participants_booth') { voters.where(origin: 'booth').count } + end + + def total_participants_booth_percentage + stats_cache('total_participants_booth_percentage') do + total_participants.zero? ? 0 : total_participants_booth * 100 / total_participants.to_f + end + end + + def total_web_valid + stats_cache('total_web_valid') { voters.where(origin: 'web').count } + end + + def valid_percentage_web + stats_cache('valid_percentage_web') do + total_valid_votes.zero? ? 0 : total_web_valid * 100 / total_valid_votes.to_f + end + end + + def total_web_white + stats_cache('total_web_white') { 0 } + end + + def white_percentage_web + stats_cache('white_percentage_web') { 0 } + end + + def total_web_null + stats_cache('total_web_null') { 0 } + end + + def null_percentage_web + stats_cache('null_percentage_web') { 0 } + end + + def total_booth_valid + stats_cache('total_booth_valid') { recounts.sum(:total_amount) } + end + + def valid_percentage_booth + stats_cache('valid_percentage_booth') do + total_valid_votes.zero? ? 0 : total_booth_valid * 100 / total_valid_votes.to_f + end + end + + def total_booth_white + stats_cache('total_booth_white') { recounts.sum(:white_amount) } + end + + def white_percentage_booth + stats_cache('white_percentage_booth') do + total_white_votes.zero? ? 0 : total_booth_white * 100 / total_white_votes.to_f + end + end + + def total_booth_null + stats_cache('total_booth_null') { recounts.sum(:null_amount) } + end + + def null_percentage_booth + stats_cache('null_percentage_booth') do + total_null_votes.zero? ? 0 : total_booth_null * 100 / total_null_votes.to_f + end + end + + def total_valid_votes + stats_cache('total_valid_votes') { total_web_valid + total_booth_valid } + end + + def total_valid_percentage + stats_cache('total_valid_percentage') do + total_participants.zero? ? 0 : total_valid_votes * 100 / total_participants.to_f + end + end + + def total_white_votes + stats_cache('total_white_votes') { total_web_white + total_booth_white } + end + + def total_white_percentage + stats_cache('total_white_percentage') do + total_participants.zero? ? 0 : total_white_votes * 100 / total_participants.to_f + end + end + + def total_null_votes + stats_cache('total_null_votes') { total_web_null + total_booth_null } + end + + def total_null_percentage + stats_cache('total_null_percentage') do + total_participants.zero? ? 0 : total_null_votes * 100 / total_participants.to_f + end + end + + def voters + stats_cache('voters') { @poll.voters } + end + + def recounts + stats_cache('recounts') { @poll.recounts } + end + + def stats_cache(key, &block) + Rails.cache.fetch("polls_stats/#{@poll.id}/#{key}", &block) + end + + end +end diff --git a/app/models/poll/total_result.rb b/app/models/poll/total_result.rb deleted file mode 100644 index 2df01929e..000000000 --- a/app/models/poll/total_result.rb +++ /dev/null @@ -1,23 +0,0 @@ -class Poll::TotalResult < ActiveRecord::Base - - VALID_ORIGINS = %w{web booth} - - belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' - belongs_to :booth_assignment - belongs_to :officer_assignment - - validates :author, presence: true - validates :origin, inclusion: {in: VALID_ORIGINS} - - scope :by_author, ->(author_id) { where(author_id: author_id) } - - before_save :update_logs - - def update_logs - if amount_changed? && amount_was.present? - self.amount_log += ":#{amount_was.to_s}" - self.officer_assignment_id_log += ":#{officer_assignment_id_was.to_s}" - self.author_id_log += ":#{author_id_was.to_s}" - end - end -end diff --git a/app/models/poll/voter.rb b/app/models/poll/voter.rb index 760096206..778a5d88a 100644 --- a/app/models/poll/voter.rb +++ b/app/models/poll/voter.rb @@ -1,18 +1,26 @@ class Poll class Voter < ActiveRecord::Base + + VALID_ORIGINS = %w{web booth}.freeze + belongs_to :poll belongs_to :user belongs_to :geozone belongs_to :booth_assignment belongs_to :officer_assignment + belongs_to :officer validates :poll_id, presence: true validates :user_id, presence: true validates :document_number, presence: true, uniqueness: { scope: [:poll_id, :document_type], message: :has_voted } + validates :origin, inclusion: { in: VALID_ORIGINS } before_validation :set_demographic_info, :set_document_info + scope :web, -> { where(origin: 'web') } + scope :booth, -> { where(origin: 'booth') } + def set_demographic_info return if user.blank? diff --git a/app/models/poll/white_result.rb b/app/models/poll/white_result.rb deleted file mode 100644 index a4a4e5a4d..000000000 --- a/app/models/poll/white_result.rb +++ /dev/null @@ -1,23 +0,0 @@ -class Poll::WhiteResult < ActiveRecord::Base - - VALID_ORIGINS = %w{web booth} - - belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' - belongs_to :booth_assignment - belongs_to :officer_assignment - - validates :author, presence: true - validates :origin, inclusion: {in: VALID_ORIGINS} - - scope :by_author, ->(author_id) { where(author_id: author_id) } - - before_save :update_logs - - def update_logs - if amount_changed? && amount_was.present? - self.amount_log += ":#{amount_was.to_s}" - self.officer_assignment_id_log += ":#{officer_assignment_id_was.to_s}" - self.author_id_log += ":#{author_id_was.to_s}" - end - end -end diff --git a/app/models/proposal.rb b/app/models/proposal.rb index f9eeecc6f..3b0696d26 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -10,11 +10,12 @@ class Proposal < ActiveRecord::Base include Graphqlable include Followable include Communitable + include Imageable + include Mappable include Documentable documentable max_documents_allowed: 3, max_file_size: 3.megabytes, accepted_content_types: [ "application/pdf" ] - accepts_nested_attributes_for :documents, allow_destroy: true include EmbedVideosHelper acts_as_votable @@ -57,14 +58,27 @@ class Proposal < ActiveRecord::Base scope :sort_by_relevance, -> { all } scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) } scope :sort_by_archival_date, -> { archived.sort_by_confidence_score } + scope :sort_by_recommendations, -> { order(cached_votes_up: :desc) } scope :archived, -> { where("proposals.created_at <= ?", Setting["months_to_archive_proposals"].to_i.months.ago) } scope :not_archived, -> { where("proposals.created_at > ?", Setting["months_to_archive_proposals"].to_i.months.ago) } scope :last_week, -> { where("proposals.created_at >= ?", 7.days.ago)} scope :retired, -> { where.not(retired_at: nil) } scope :not_retired, -> { where(retired_at: nil) } scope :successful, -> { where("cached_votes_up >= ?", Proposal.votes_needed_for_success) } + scope :unsuccessful, -> { where("cached_votes_up < ?", Proposal.votes_needed_for_success) } scope :public_for_api, -> { all } + def self.recommendations(user) + tagged_with(user.interests, any: true) + .where("author_id != ?", user.id) + .unsuccessful + .not_followed_by_user(user) + end + + def self.not_followed_by_user(user) + where.not(id: followed_by_user(user).pluck(:id)) + end + def to_param "#{id}-#{title}".parameterize end @@ -88,7 +102,7 @@ class Proposal < ActiveRecord::Base def self.search_by_code(terms) matched_code = match_code(terms) results = where(id: matched_code[1]) if matched_code - return results if (results.present? && results.first.code == terms) + return results if results.present? && results.first.code == terms end def self.match_code(terms) @@ -184,6 +198,12 @@ class Proposal < ActiveRecord::Base (voters + followers).uniq end + def self.proposals_orders(user) + orders = %w{hot_score confidence_score created_at relevance archival_date} + orders << "recommendations" if user.present? + orders + end + protected def set_responsible_name diff --git a/app/models/user.rb b/app/models/user.rb index 006d1a09e..527ae3abc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -88,12 +88,17 @@ class User < ActiveRecord::Base end def debate_votes(debates) - voted = votes.for_debates(debates) + voted = votes.for_debates(Array(debates).map(&:id)) voted.each_with_object({}) { |v, h| h[v.votable_id] = v.value } end def proposal_votes(proposals) - voted = votes.for_proposals(proposals) + voted = votes.for_proposals(Array(proposals).map(&:id)) + voted.each_with_object({}) { |v, h| h[v.votable_id] = v.value } + end + + def legislation_proposal_votes(proposals) + voted = votes.for_legislation_proposals(proposals) voted.each_with_object({}) { |v, h| h[v.votable_id] = v.value } end diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb index 11d58adc8..bae999fe4 100644 --- a/app/views/admin/_menu.html.erb +++ b/app/views/admin/_menu.html.erb @@ -1,5 +1,5 @@
-