diff --git a/.codeclimate.yml b/.codeclimate.yml index 097f4ed1a..c026af564 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -32,3 +32,4 @@ exclude_paths: - files/ - public/ - tmp/ +- vendor/ 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/.gitignore b/.gitignore index a6f2826c3..7761dba2a 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ /config/database.yml /config/secrets.yml /config/deploy-secrets.yml +/config/maintenance.yml /coverage diff --git a/.rubocop.yml b/.rubocop.yml index e2cc2bf93..0c56b53fc 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,6 +1,9 @@ inherit_from: .rubocop_todo.yml +require: rubocop-rspec AllCops: + DisplayCopNames: true + DisplayStyleGuide: true Include: - '**/Rakefile' - '**/config.ru' @@ -9,60 +12,177 @@ AllCops: - 'config/**/*' - 'script/**/*' TargetRubyVersion: 2.3 + # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop + # to ignore them, so only the ones explicitly set in this file are enabled. + DisabledByDefault: true Rails: Enabled: true -Documentation: - Enabled: false - Metrics/LineLength: - Max: 140 + Max: 100 Layout/IndentationConsistency: EnforcedStyle: rails -Style/EmptyMethod: +RSpec/AlignLeftLetBrace: Enabled: false -Style/StringLiterals: +RSpec/AlignRightLetBrace: Enabled: false -Style/SymbolProc: +RSpec/AnyInstance: Enabled: false -Style/FrozenStringLiteralComment: +RSpec/AroundBlock: + Enabled: true + +RSpec/BeEql: + Enabled: true + +RSpec/BeforeAfterAll: + Enabled: true + +Capybara/CurrentPathExpectation: + Enabled: true + +Capybara/FeatureMethods: Enabled: false -Style/PercentLiteralDelimiters: +RSpec/ContextWording: Enabled: false -Style/SymbolArray: +RSpec/DescribeClass: + Enabled: true + +RSpec/DescribeMethod: + Enabled: true + +RSpec/DescribeSymbol: + Enabled: true + +RSpec/DescribedClass: + Enabled: true + +RSpec/EmptyExampleGroup: + Enabled: true + +RSpec/EmptyLineAfterSubject: + Enabled: true + +RSpec/ExampleLength: Enabled: false -Layout/EmptyLinesAroundClassBody: +RSpec/ExampleWording: + Enabled: true + +RSpec/ExpectActual: + Enabled: true + +RSpec/ExpectInHook: + Enabled: true + +RSpec/ExpectOutput: + Enabled: true + +RSpec/FilePath: + Enabled: true + +RSpec/Focus: + Enabled: true + +RSpec/HookArgument: + Enabled: true + +RSpec/ImplicitExpect: + Enabled: true + EnforcedStyle: should + +RSpec/InstanceSpy: + Enabled: true + +RSpec/InstanceVariable: Enabled: false -Layout/EmptyLinesAroundBlockBody: +RSpec/InvalidPredicateMatcher: + Enabled: true + +RSpec/ItBehavesLike: + Enabled: true + +RSpec/IteratedExpectation: + Enabled: true + +RSpec/LeadingSubject: + Enabled: true + +RSpec/LetBeforeExamples: + Enabled: true + +RSpec/LetSetup: + Enabled: true + +RSpec/MessageChain: + Enabled: true + +RSpec/MessageExpectation: + Enabled: true + +RSpec/MessageSpies: + Enabled: true + EnforcedStyle: receive + +RSpec/MultipleDescribes: + Enabled: true + +RSpec/MultipleExpectations: Enabled: false -Layout/EmptyLinesAroundModuleBody: +RSpec/MultipleSubjects: + Enabled: true + +RSpec/NamedSubject: Enabled: false -Layout/SpaceBeforeBlockBraces: - Enabled: false +RSpec/NestedGroups: + Enabled: true + Max: 4 -Layout/SpaceInsideBrackets: - Enabled: false +RSpec/NotToNot: + Enabled: true -Layout/SpaceInsideHashLiteralBraces: - Enabled: false +RSpec/OverwritingSetup: + Enabled: true -Layout/SpaceInsideBlockBraces: - Enabled: false +RSpec/PredicateMatcher: + Enabled: true -Layout/TrailingBlankLines: - Enabled: false +RSpec/RepeatedDescription: + Enabled: true -Rails/ApplicationRecord: - Enabled: false +RSpec/RepeatedExample: + Enabled: true + +RSpec/ReturnFromStub: + Enabled: true + +RSpec/ScatteredLet: + Enabled: true + +RSpec/ScatteredSetup: + Enabled: true + +RSpec/SharedContext: + Enabled: true + +RSpec/SingleArgumentMessageChain: + Enabled: true + +RSpec/SubjectStub: + Enabled: true + +RSpec/VerifiedDoubles: + Enabled: true + +RSpec/VoidExpect: + Enabled: true \ No newline at end of file diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 288c964c1..09015e346 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,26 +1,25 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2017-07-07 21:23:30 +0200 using RuboCop version 0.49.1. +# on 2018-01-06 19:20:18 +0100 using RuboCop version 0.52.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. +# Configuration parameters: EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. # SupportedHashRocketStyles: key, separator, table # SupportedColonStyles: key, separator, table # 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: 64 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. +# Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: with_first_parameter, with_fixed_indentation Layout/AlignParameters: Enabled: false @@ -33,9 +32,9 @@ 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. +# Configuration parameters: EnforcedStyle. # SupportedStyles: leading, trailing Layout/DotPosition: Exclude: @@ -64,15 +63,8 @@ Layout/EmptyLineAfterMagicComment: # Offense count: 1 # Cop supports --auto-correct. -Layout/EmptyLines: - Exclude: - - 'app/controllers/admin/budget_investment_milestones_controller.rb' - -# Offense count: 2 -# Cop supports --auto-correct. Layout/EmptyLinesAroundMethodBody: Exclude: - - 'app/models/abilities/administrator.rb' - 'lib/graph_ql/api_types_creator.rb' # Offense count: 2 @@ -85,7 +77,7 @@ Layout/ExtraSpacing: # Offense count: 1 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. +# Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: consistent, special_for_inner_method_call, special_for_inner_method_call_in_parentheses Layout/FirstParameterIndentation: Exclude: @@ -93,14 +85,14 @@ Layout/FirstParameterIndentation: # Offense count: 1 # Cop supports --auto-correct. -# Configuration parameters: SupportedStyles, IndentationWidth. +# Configuration parameters: IndentationWidth. # SupportedStyles: special_inside_parentheses, consistent, align_brackets Layout/IndentArray: EnforcedStyle: consistent # Offense count: 10 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. +# Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: special_inside_parentheses, consistent, align_braces Layout/IndentHash: Exclude: @@ -108,7 +100,7 @@ Layout/IndentHash: # Offense count: 4 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: auto_detection, squiggly, active_support, powerpack, unindent Layout/IndentHeredoc: Exclude: @@ -117,7 +109,7 @@ Layout/IndentHeredoc: # Offense count: 10 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: normal, rails Layout/IndentationConsistency: Exclude: @@ -126,13 +118,13 @@ Layout/IndentationConsistency: - 'spec/models/legislation/draft_version_spec.rb' - 'spec/models/proposal_spec.rb' -# Offense count: 23 +# Offense count: 59 # 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,12 +132,11 @@ 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 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: symmetrical, new_line, same_line Layout/MultilineArrayBraceLayout: Exclude: @@ -155,7 +146,7 @@ Layout/MultilineArrayBraceLayout: # Offense count: 9 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: symmetrical, new_line, same_line Layout/MultilineHashBraceLayout: Exclude: @@ -168,7 +159,7 @@ Layout/MultilineHashBraceLayout: # Offense count: 17 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: symmetrical, new_line, same_line Layout/MultilineMethodCallBraceLayout: Exclude: @@ -181,9 +172,9 @@ 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. +# Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: aligned, indented, indented_relative_to_receiver Layout/MultilineMethodCallIndentation: Exclude: @@ -201,13 +192,12 @@ Layout/MultilineMethodCallIndentation: - 'spec/models/proposal_spec.rb' - 'spec/models/user_spec.rb' -# Offense count: 8 +# Offense count: 5 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. +# Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: aligned, indented Layout/MultilineOperationIndentation: Exclude: - - 'app/controllers/officing/results_controller.rb' - 'app/helpers/users_helper.rb' - 'app/helpers/valuation_helper.rb' - 'app/models/user.rb' @@ -226,13 +216,44 @@ Layout/SpaceBeforeFirstArg: Exclude: - 'spec/factories.rb' +# Offense count: 33 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets. +# SupportedStyles: space, no_space, compact +# SupportedStylesForEmptyBrackets: space, no_space +Layout/SpaceInsideArrayLiteralBrackets: + Exclude: + - 'app/controllers/valuation/budget_investments_controller.rb' + - 'app/controllers/valuation/spending_proposals_controller.rb' + - 'app/helpers/admin_helper.rb' + - 'app/helpers/budgets_helper.rb' + - 'app/helpers/geozones_helper.rb' + - 'app/helpers/proposals_helper.rb' + - 'app/helpers/valuation_helper.rb' + - 'app/models/budget/investment.rb' + - 'app/models/budget/investment/milestone.rb' + - 'app/models/legislation/process.rb' + - 'app/models/legislation/proposal.rb' + - 'app/models/poll/question/answer.rb' + - 'app/models/proposal.rb' + - 'spec/factories.rb' + - 'spec/lib/graphql_spec.rb' + # Offense count: 2 # Cop supports --auto-correct. Layout/SpaceInsideRangeLiteral: Exclude: - 'app/models/legislation/annotation.rb' -# Offense count: 38 +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: space, no_space +Layout/SpaceInsideReferenceBrackets: + Exclude: + - 'app/helpers/settings_helper.rb' + +# Offense count: 39 Lint/AmbiguousBlockAssociation: Exclude: - 'spec/controllers/comments_controller_spec.rb' @@ -244,6 +265,14 @@ Lint/AmbiguousBlockAssociation: - 'spec/models/debate_spec.rb' - 'spec/models/proposal_spec.rb' - 'spec/models/user_spec.rb' + - 'spec/shared/models/map_validations.rb' + +# Offense count: 3 +Lint/DuplicateMethods: + Exclude: + - 'app/models/budget/result.rb' + - 'lib/comment_tree.rb' + - 'lib/email_digest.rb' # Offense count: 2 Lint/HandleExceptions: @@ -253,40 +282,22 @@ Lint/HandleExceptions: # Offense count: 1 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: runtime_error, standard_error Lint/InheritException: Exclude: - 'app/controllers/concerns/feature_flags.rb' # Offense count: 1 -Lint/LiteralInCondition: +Lint/LiteralAsCondition: 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,47 +322,106 @@ Lint/UnusedMethodArgument: - 'app/mailers/mailer.rb' - 'app/models/abilities/everyone.rb' -# Offense count: 278 +# Offense count: 335 Lint/UselessAssignment: Enabled: false -# Offense count: 1 +# Offense count: 2 Lint/Void: Exclude: - 'app/controllers/polls_controller.rb' + - 'app/models/setting.rb' -# Offense count: 74 +# Offense count: 86 Metrics/AbcSize: - Max: 54 + Max: 77 -# Offense count: 454 +# Offense count: 513 # Configuration parameters: CountComments, ExcludedMethods. Metrics/BlockLength: - Max: 1071 + Max: 1242 -# Offense count: 8 +# Offense count: 11 # Configuration parameters: CountComments. Metrics/ClassLength: - Max: 256 + Max: 266 -# Offense count: 10 +# 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: 71 # Configuration parameters: CountComments. Metrics/MethodLength: - Max: 49 + Max: 68 # Offense count: 1 # Configuration parameters: CountComments. Metrics/ModuleLength: - Max: 214 + Max: 273 -# Offense count: 7 +# Offense count: 3 +# Configuration parameters: CountKeywordArgs. +Metrics/ParameterLists: + Max: 7 + +# Offense count: 8 Metrics/PerceivedComplexity: Max: 11 +# Offense count: 10 +Naming/AccessorMethodName: + Exclude: + - 'app/controllers/application_controller.rb' + - 'app/controllers/concerns/commentable_actions.rb' + - 'app/controllers/management/proposals_controller.rb' + - 'app/controllers/management/spending_proposals_controller.rb' + - 'app/controllers/proposals_controller.rb' + - 'lib/merged_comment_tree.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 +Naming/FileName: + Exclude: + - 'Capfile' + - 'Gemfile' + +# Offense count: 10 +# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist, MethodDefinitionMacros. +# NamePrefix: is_, has_, have_ +# NamePrefixBlacklist: is_, has_, have_ +# NameWhitelist: is_a? +# MethodDefinitionMacros: define_method, define_singleton_method +Naming/PredicateName: + Exclude: + - 'spec/**/*' + - 'app/controllers/concerns/has_filters.rb' + - 'app/controllers/concerns/has_orders.rb' + - 'app/helpers/banners_helper.rb' + - 'app/helpers/debates_helper.rb' + - 'app/models/budget/ballot.rb' + - 'app/models/user.rb' + +# Offense count: 95 +# Configuration parameters: EnforcedStyle. +# SupportedStyles: snake_case, normalcase, non_integer +Naming/VariableNumber: + Enabled: false + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: AutoCorrect. +Performance/HashEachMethods: + Exclude: + - 'app/models/ahoy/data_source.rb' + # Offense count: 6 # Cop supports --auto-correct. Performance/RedundantMatch: @@ -360,16 +430,18 @@ Performance/RedundantMatch: - 'app/controllers/valuation/spending_proposals_controller.rb' - 'app/helpers/embed_videos_helper.rb' -# Offense count: 16 +# Offense count: 18 # Cop supports --auto-correct. # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/FindBy: Exclude: - 'app/models/budget/ballot.rb' + - 'app/models/concerns/relationable.rb' - 'app/models/geozone.rb' - 'app/models/legislation/question.rb' - 'app/models/officing/residence.rb' + - 'app/models/poll/booth.rb' - 'app/models/poll/voter.rb' - 'app/models/setting.rb' - 'app/models/signature.rb' @@ -385,6 +457,12 @@ Rails/HasAndBelongsToMany: Exclude: - 'app/models/poll.rb' +# Offense count: 52 +# Configuration parameters: Include. +# Include: app/models/**/*.rb +Rails/HasManyOrHasOneDependent: + Enabled: false + # Offense count: 37 # Cop supports --auto-correct. # Configuration parameters: Include. @@ -402,38 +480,75 @@ Rails/HttpPositionalArguments: - 'spec/controllers/pages_controller_spec.rb' - 'spec/controllers/users/registrations_controller_spec.rb' -# Offense count: 20 +# Offense count: 71 +# Configuration parameters: Include. +# Include: app/models/**/*.rb +Rails/InverseOf: + Enabled: false + +# Offense count: 28 +# Configuration parameters: Include. +# Include: app/controllers/**/*.rb +Rails/LexicallyScopedActionFilter: + Exclude: + - 'app/controllers/admin/banners_controller.rb' + - 'app/controllers/admin/poll/officer_assignments_controller.rb' + - 'app/controllers/admin/poll/polls_controller.rb' + - 'app/controllers/admin/tags_controller.rb' + - 'app/controllers/concerns/search.rb' + - 'app/controllers/debates_controller.rb' + - 'app/controllers/legislation/proposals_controller.rb' + - 'app/controllers/management/proposals_controller.rb' + - 'app/controllers/moderation/comments_controller.rb' + - 'app/controllers/moderation/debates_controller.rb' + - 'app/controllers/moderation/proposals_controller.rb' + - 'app/controllers/proposals_controller.rb' + - 'app/controllers/users/registrations_controller.rb' + - 'app/controllers/valuation/budget_investments_controller.rb' + - 'app/controllers/valuation/spending_proposals_controller.rb' + +# Offense count: 19 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/documents_helper.rb' - 'app/helpers/text_with_links_helper.rb' - - 'app/helpers/users_helper.rb' - 'app/helpers/valuation_helper.rb' -# Offense count: 70 +# Offense count: 7 +# Cop supports --auto-correct. +Rails/Presence: + Exclude: + - 'app/controllers/concerns/commentable_actions.rb' + - 'app/controllers/users/sessions_controller.rb' + - 'app/helpers/welcome_helper.rb' + - 'app/models/legislation/proposal.rb' + - 'app/models/proposal.rb' + - 'app/models/valuator.rb' + +# Offense count: 72 # Configuration parameters: Blacklist. # Blacklist: decrement!, decrement_counter, increment!, increment_counter, toggle!, touch, update_all, update_attribute, update_column, update_columns, update_counters Rails/SkipsModelValidations: Enabled: false -# Offense count: 10 -Style/AccessorMethodName: +# Offense count: 4 +# Configuration parameters: Environments. +# Environments: development, test, production +Rails/UnknownEnv: Exclude: - - 'app/controllers/application_controller.rb' - - 'app/controllers/concerns/commentable_actions.rb' - - 'app/controllers/management/proposals_controller.rb' - - 'app/controllers/management/spending_proposals_controller.rb' - - 'app/controllers/proposals_controller.rb' - - 'lib/merged_comment_tree.rb' + - 'lib/census_api.rb' + - 'lib/sms_api.rb' -# Offense count: 1 +# Offense count: 8 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: braces, no_braces, context_dependent Style/BracesAroundHashParameters: Exclude: @@ -441,8 +556,9 @@ Style/BracesAroundHashParameters: - 'spec/features/budgets/investments_spec.rb' - 'spec/features/proposals_spec.rb' -# Offense count: 119 -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Offense count: 126 +# Cop supports --auto-correct. +# Configuration parameters: AutoCorrect, EnforcedStyle. # SupportedStyles: nested, compact Style/ClassAndModuleChildren: Enabled: false @@ -454,20 +570,13 @@ Style/ClassVars: - 'app/models/organization.rb' - 'app/models/user.rb' -# Offense count: 6 +# Offense count: 8 # Cop supports --auto-correct. -Style/ColonMethodCall: - Exclude: - - 'spec/models/budget/investment_spec.rb' - -# Offense count: 12 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly, IncludeTernaryExpressions. +# Configuration parameters: EnforcedStyle, 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' @@ -475,71 +584,65 @@ Style/ConditionalAssignment: - 'app/controllers/verification/sms_controller.rb' - 'lib/graph_ql/api_types_creator.rb' +# Offense count: 19 +Style/DateTime: + Exclude: + - 'app/models/user.rb' + - 'spec/controllers/admin/api/stats_controller_spec.rb' + - 'spec/factories.rb' + - 'spec/features/admin/banners_spec.rb' + - 'spec/models/ahoy/data_source_spec.rb' + # Offense count: 1 Style/DoubleNegation: Exclude: - 'app/models/flag.rb' -# Offense count: 1 +# Offense count: 6 # Cop supports --auto-correct. -Style/EmptyCaseCondition: +Style/Encoding: Exclude: - - 'app/models/concerns/verification.rb' + - 'spec/features/debates_spec.rb' + - 'spec/features/proposal_ballots_spec.rb' + - 'spec/features/proposals_spec.rb' + - 'spec/mailers/devise_mailer_spec.rb' + - 'spec/models/debate_spec.rb' + - 'spec/models/proposal_spec.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 -Style/FileName: - Exclude: - - 'Capfile' - - 'Gemfile' - -# Offense count: 56 +# Offense count: 61 # Configuration parameters: MinBodyLength. Style/GuardClause: Enabled: false -# Offense count: 12 +# Offense count: 49 # Cop supports --auto-correct. -# Configuration parameters: MaxLineLength. Style/IfUnlessModifier: - Exclude: - - 'app/controllers/annotations_controller.rb' - - 'app/controllers/application_controller.rb' - - 'app/controllers/graphql_controller.rb' - - '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' + Enabled: false # Offense count: 4 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # 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: +# Offense count: 10 +Style/MixinUsage: 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' + - 'lib/census_api.rb' + - 'lib/local_census.rb' + - 'spec/features/comments/budget_investments_spec.rb' + - 'spec/features/comments/debates_spec.rb' + - 'spec/features/comments/legislation_annotations_spec.rb' + - 'spec/features/comments/legislation_questions_spec.rb' + - 'spec/features/comments/polls_spec.rb' + - 'spec/features/comments/proposals_spec.rb' + - 'spec/features/comments/topics_spec.rb' + - 'spec/rails_helper.rb' # Offense count: 1 Style/MultilineBlockChain: @@ -552,17 +655,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,34 +672,15 @@ 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. +# Configuration parameters: AutoCorrect, EnforcedStyle. # SupportedStyles: predicate, comparison Style/NumericPredicate: Exclude: @@ -625,34 +706,9 @@ 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 -# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist. -# NamePrefix: is_, has_, have_ -# NamePrefixBlacklist: is_, has_, have_ -# NameWhitelist: is_a? -Style/PredicateName: - Exclude: - - 'spec/**/*' - - 'app/controllers/concerns/has_filters.rb' - - 'app/controllers/concerns/has_orders.rb' - - 'app/helpers/banners_helper.rb' - - '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. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: compact, exploded Style/RaiseArgs: Exclude: @@ -668,18 +724,15 @@ Style/RedundantBegin: - 'app/controllers/graphql_controller.rb' - 'app/models/legislation/annotation.rb' -# Offense count: 55 +# Offense count: 7 # Cop supports --auto-correct. -Style/RedundantParentheses: - Enabled: false - -# Offense count: 3 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes. +# Configuration parameters: EnforcedStyle, AllowInnerSlashes. # SupportedStyles: slashes, percent_r, mixed Style/RegexpLiteral: Exclude: + - 'app/controllers/related_contents_controller.rb' - 'app/helpers/embed_videos_helper.rb' + - 'app/models/poll/question/answer/video.rb' - 'spec/customization_engine_spec.rb' # Offense count: 6 @@ -691,45 +744,70 @@ Style/RescueModifier: - 'app/controllers/verification/sms_controller.rb' - 'app/models/concerns/measurable.rb' -# Offense count: 1 +# Offense count: 11 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: implicit, explicit +Style/RescueStandardError: + Exclude: + - 'app/controllers/graphql_controller.rb' + - 'app/controllers/related_contents_controller.rb' + - 'app/helpers/locales_helper.rb' + - 'app/models/legislation/annotation.rb' + - 'lib/local_census.rb' + - 'lib/manager_authenticator.rb' + - 'lib/tasks/emails.rake' + - 'spec/shared/features/nested_documentable.rb' + - 'spec/shared/features/nested_imageable.rb' + +# Offense count: 15 # Cop supports --auto-correct. # Configuration parameters: ConvertCodeThatCanStartToReturnNil. Style/SafeNavigation: Exclude: + - 'app/controllers/legislation/draft_versions_controller.rb' + - 'app/controllers/sandbox_controller.rb' + - 'app/controllers/verification/letter_controller.rb' + - 'app/helpers/users_helper.rb' + - 'app/models/concerns/galleryable.rb' + - 'app/models/concerns/imageable.rb' + - 'app/models/legislation/proposal.rb' + - 'app/models/proposal.rb' - '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 -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: snake_case, normalcase, non_integer -Style/VariableNumber: - Enabled: false + - 'app/models/user.rb' # Offense count: 31 # Cop supports --auto-correct. -# Configuration parameters: SupportedStyles, WordRegex. +# Configuration parameters: WordRegex. # SupportedStyles: percent, brackets Style/WordArray: EnforcedStyle: percent - MinSize: 9 + MinSize: 8 + +# Offense count: 10 +RSpec/DescribeClass: + Exclude: + - 'spec/customization_engine_spec.rb' + - 'spec/i18n_spec.rb' + - 'spec/lib/acts_as_paranoid_aliases_spec.rb' + - 'spec/lib/cache_spec.rb' + - 'spec/lib/graphql_spec.rb' + - 'spec/lib/tasks/communities_spec.rb' + - 'spec/lib/tasks/dev_seed_spec.rb' + - 'spec/lib/tasks/settings_spec.rb' + - 'spec/models/abilities/organization_spec.rb' + - 'spec/views/welcome/index.html.erb_spec.rb' + +# Offense count: 2 +# Configuration parameters: SkipBlocks, EnforcedStyle. +# SupportedStyles: described_class, explicit +RSpec/DescribedClass: + Exclude: + - 'spec/controllers/concerns/has_filters_spec.rb' + - 'spec/controllers/concerns/has_orders_spec.rb' + +# Offense count: 1 +# Configuration parameters: IgnoreSymbolicNames. +RSpec/VerifiedDoubles: + Exclude: + - 'spec/models/verification/management/email_spec.rb' \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 8836964a6..b71806141 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,15 +4,17 @@ 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 script: - "bundle exec rake assets:precompile RAILS_ENV=test" - - "bundle exec rake knapsack:rspec" + - "bin/knapsack_pro_rspec" env: global: - - CI_NODE_TOTAL=2 + - KNAPSACK_PRO_FIXED_QUEUE_SPLIT=true + - KNAPSACK_PRO_CI_NODE_TOTAL=2 matrix: - - CI_NODE_INDEX=0 - - CI_NODE_INDEX=1 \ No newline at end of file + - KNAPSACK_PRO_CI_NODE_INDEX=0 + - KNAPSACK_PRO_CI_NODE_INDEX=1 diff --git a/CHANGELOG.md b/CHANGELOG.md index a487eadb4..8701852ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,38 +1,199 @@ -### 0.9 - 2017-06-15 +# Changelog +All notable changes to this project will be documented in this file. -* New features - * Budgets - * Basic polls - * Collaborative legistlation - * Custom pages - * GraphQL API - * Improved admin section -* Enhancements - * Improved admin section - * Rails 4.2.8 - * Ruby 2.3.2 -* Bug fixes - * CKEditor locale compilation fixed - * Fixed bugs in mobile layouts -* Deprecations - * SpendingProposals are deprecated now in favor of Budgets +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -### 0.8 - 2016-07-21 +## [Unreleased](https://github.com/consul/consul/compare/v0.12...consul:master) -* New features - * Support for customization schema, vía specific custom files, assets and folders -* Enhancements - * Rails 2.4.7 - * Ruby 2.3.1 -* Bug fixes - * Fixed bug causing errors on user deletion +### Added +- Added Drafting phase to Budgets https://github.com/consul/consul/pull/2285 +- Allow admins to destroy budgets without investments https://github.com/consul/consul/pull/2283 +- Added rubocop-rspec gem, enabled cops one by one fixing offenses. +- Added CSV download link to budget_investments https://github.com/consul/consul/pull/2147 +- Added Capistrano task to automate maintenance mode https://github.com/consul/consul/pull/1932 -### 0.7 - 2016-04-25 +### Changed +- Updated multiple minor & patch gem versions thanks to [Depfu](https://depfu.com) +- Updated rubocop version and ignored all cops by default +- Removed legislation section arrows and duplicate html tag thanks to [xarlybovi](https://github.com/xarlybovi) https://github.com/consul/consul/issues/1704 +- Display proposal and investment image when sharing in social networks https://github.com/consul/consul/pull/2202 +- Redirect admin to budget lists after edit https://github.com/consul/consul/pull/2284 +- Improve budget investment form https://github.com/consul/consul/pull/2280 +- Prevent edition of investments if budget is in the final phase https://github.com/consul/consul/pull/2223 -* New features - * Debates - * Proposals - * Basic Spending Proposals -* Enhancements - * Rails 2.4.6 - * Ruby 2.2.3 \ No newline at end of file +### Deprecated + +### Removed +- Spending Proposals urls from sitemap, that model is getting entirely deprecated soon. + +### Fixed + +### Security + +## [0.12.0](https://github.com/consul/consul/compare/v0.11...v0.12) - 2018-01-03 + +### Added +- Added Images to Budget Investment's Milestones https://github.com/consul/consul/pull/2186 +- Added Documents to Budget Investment's Milestones https://github.com/consul/consul/pull/2191 +- Added Publication Date Budget Investment's Milestones https://github.com/consul/consul/pull/2188 +- New setting `feature.allow_images` to allow upload and show images for both (proposals and budget investment projects). Set it manually through console with `Setting['feature.allow_images'] = true` +- Related Content feature. Now Debates & Proposals can be related https://github.com/consul/consul/issues/1164 +- Map validations https://github.com/consul/consul/pull/2207 +- Added spec for 'rake db:dev_seed' task https://github.com/consul/consul/pull/2201 +- Adds timestamps to polls https://github.com/consul/consul/pull/2180 (Run `rake polls:initialize_timestamps` to initialize attributes created_at and updated_at with the current time for all existing polls, or manually through console set correct values) + +### Changed +- Some general Design improvements https://github.com/consul/consul/pull/2170 https://github.com/consul/consul/pull/2198 +- Improved Communities design https://github.com/consul/consul/pull/1904 +- Made Milestones description required & hided title usage https://github.com/consul/consul/pull/2195 +- Improved generic error message https://github.com/consul/consul/pull/2217 +- Improved Sitemap for SEO https://github.com/consul/consul/pull/2215 + +### Fixed +- Notifications for hidden resources https://github.com/consul/consul/pull/2172 +- Notifications exceptions https://github.com/consul/consul/pull/2187 +- Fixed map location update https://github.com/consul/consul/pull/2213 + +## [0.11.0](https://github.com/consul/consul/compare/v0.10...v0.11) - 2017-12-05 + +### Added +- Allow social media image meta tags to be overwritten https://github.com/consul/consul/pull/1756 & https://github.com/consul/consul/pull/2153 +- Allow users to verify their account against a local Census https://github.com/consul/consul/pull/1752 +- Make Proposals & Budgets Investments followable by users https://github.com/consul/consul/pull/1727 +- Show user followable activity on public user page https://github.com/consul/consul/pull/1750 +- Add Budget results view & table https://github.com/consul/consul/pull/1748 +- Improved Budget winners calculations https://github.com/consul/consul/pull/1738 +- Allow Documents to be uploaded to Proposals and Budget Investments https://github.com/consul/consul/pull/1809 +- Allow Communities creation on Proposals and Budget Investments (Run rake task 'communities:associate_community') https://github.com/consul/consul/pull/1815 https://github.com/consul/consul/pull/1833 +- Allow user to geolocate Proposals and Budget Investments on a map https://github.com/consul/consul/pull/1864 +- Legislation Process Proposals https://github.com/consul/consul/pull/1906 +- Autocomplete user tags https://github.com/consul/consul/pull/1905 +- GraphQL API docs https://github.com/consul/consul/pull/1763 +- Show recommended proposals and debates to users based in their interests https://github.com/consul/consul/pull/1824 +- Allow images & videos to be added to Poll questions https://github.com/consul/consul/pull/1835 https://github.com/consul/consul/pull/1915 +- Add Poll Shifts, to soon replace Poll OfficerAssignments usage entirely (for now just partially) +- Added dropdown menu for advanced users https://github.com/consul/consul/pull/1761 +- Help text headers and footers https://github.com/consul/consul/pull/1807 +- Added a couple of steps for linux installation guidelines https://github.com/consul/consul/pull/1846 +- Added TotalResult model, to replace Poll::FinalRecount https://github.com/consul/consul/pull/1866 1885 +- Preview Budget Results by admins https://github.com/consul/consul/pull/1923 +- Added comments to Polls https://github.com/consul/consul/pull/1961 +- Added images & videos to Polls https://github.com/consul/consul/pull/1990 https://github.com/consul/consul/pull/1989 +- Poll Answers are orderable now https://github.com/consul/consul/pull/2037 +- Poll Booth Assigment management https://github.com/consul/consul/pull/2087 +- Legislation processes documents https://github.com/consul/consul/pull/2084 +- Poll results https://github.com/consul/consul/pull/2082 +- Poll stats https://github.com/consul/consul/pull/2075 +- Poll stats on admin panel https://github.com/consul/consul/pull/2102 +- Added investment user tags admin interface https://github.com/consul/consul/pull/2068 +- Added Poll comments to GraphQL API https://github.com/consul/consul/pull/2148 +- Added option to unassign Valuator role https://github.com/consul/consul/pull/2110 +- Added search by name/email on several Admin sections https://github.com/consul/consul/pull/2105 +- Added Docker support https://github.com/consul/consul/pull/2127 & documentation https://consul_docs.gitbooks.io/docs/content/en/getting_started/docker.html +- Added population restriction validation on Budget Headings https://github.com/consul/consul/pull/2115 +- Added a `/consul.json` route that returns installation details (current release version and feature flags status) for a future dashboard app https://github.com/consul/consul/pull/2164 + +### Changed +- Gem versions locked & cleanup https://github.com/consul/consul/pull/1730 +- Upgraded many minor versions https://github.com/consul/consul/pull/1747 +- Rails 4.2.10 https://github.com/consul/consul/pull/2128 +- Updated Code of Conduct to use contributor covenant 1.4 https://github.com/consul/consul/pull/1733 +- Improved consistency to all "Go back" buttons https://github.com/consul/consul/pull/1770 +- New CONSUL brand https://github.com/consul/consul/pull/1808 +- Admin panel redesign https://github.com/consul/consul/pull/1875 https://github.com/consul/consul/pull/2060 +- Swapped Poll White/Null/Total Results for Poll Recount https://github.com/consul/consul/pull/1963 +- Improved Poll index view https://github.com/consul/consul/pull/1959 https://github.com/consul/consul/pull/1987 +- Update secrets and deploy secrets example files https://github.com/consul/consul/pull/1966 +- Improved Poll Officer panel features +- Consistency across all admin profiles sections https://github.com/consul/consul/pull/2089 +- Improved dev_seeds with more Poll content https://github.com/consul/consul/pull/2121 +- Comment count now updates live after publishing a new one https://github.com/consul/consul/pull/2090 + +### Removed +- Removed Tolk gem usage, we've moved to Crowdin service https://github.com/consul/consul/pull/1729 +- Removed Polls manual recounts (model Poll::FinalRecount) https://github.com/consul/consul/pull/1764 +- Skipped specs for deprecated Spending Proposal model https://github.com/consul/consul/pull/1773 +- Moved Documentation to https://github.com/consul/docs https://github.com/consul/consul/pull/1861 +- Remove Poll Officer recounts, add Final & Totals votes https://github.com/consul/consul/pull/1919 +- Remove deprecated Poll results models https://github.com/consul/consul/pull/1964 +- Remove deprecated Poll::Question valid_answers attribute & usage https://github.com/consul/consul/pull/2073 https://github.com/consul/consul/pull/2074 + +### Fixed +- Foundation settings stylesheet https://github.com/consul/consul/pull/1766 +- Budget milestone date localization https://github.com/consul/consul/pull/1734 +- Return datetime format for en locale https://github.com/consul/consul/pull/1795 +- Show bottom proposals button only if proposals exists https://github.com/consul/consul/pull/1798 +- Check SMS verification in a more consistent way https://github.com/consul/consul/pull/1832 +- Allow only YouTube/Vimeo URLs on 'video_url' attributes https://github.com/consul/consul/pull/1854 +- Remove empty comments html https://github.com/consul/consul/pull/1862 +- Fixed admin/poll routing errors https://github.com/consul/consul/pull/1863 +- Display datepicker arrows https://github.com/consul/consul/pull/1869 +- Validate presence poll presence on Poll::Question creation https://github.com/consul/consul/pull/1868 +- Switch flag/unflag buttons on use via ajax https://github.com/consul/consul/pull/1883 +- Flaky specs fixed https://github.com/consul/consul/pull/1888 +- Fixed link back from moderation dashboard to root_path https://github.com/consul/consul/pull/2132 +- Fixed Budget random pagination order https://github.com/consul/consul/pull/2131 +- Fixed `direct_messages_max_per_day` set to nil https://github.com/consul/consul/pull/2100 +- Fixed notification link error when someone commented a Topic https://github.com/consul/consul/pull/2094 +- Lots of small UI/UX/SEO/SEM improvements + +## [0.10.0](https://github.com/consul/consul/compare/v0.9...v0.10) - 2017-07-05 +### Added +- Milestones on Budget Investment's +- Feature flag to enable/disable Legislative Processes +- Locale site pages customization +- Incompatible investments + +### Changed +- Localization files reorganization. Check migration instruction at https://github.com/consul/consul/releases/tag/v0.10 +- Rails 4.2.9 + +## [0.9.0](https://github.com/consul/consul/compare/v0.8...v0.9) - 2017-06-15 +### Added +- Budgets +- Basic polls +- Collaborative legistlation +- Custom pages +- GraphQL API +- Improved admin section + +### Changed +- Improved admin section +- Rails 4.2.8 +- Ruby 2.3.2 + +### Deprecated +- SpendingProposals are deprecated now in favor of Budgets + +### Fixed +- CKEditor locale compilation fixed +- Fixed bugs in mobile layouts + +## [0.8.0](https://github.com/consul/consul/compare/v0.7...v0.8)- 2016-07-21 +### Added +- Support for customization schema, vía specific custom files, assets and folders + +### Changed +- Rails 4.2.7 +- Ruby 2.3.1 + +### Fixed +- Fixed bug causing errors on user deletion + +## [0.7.0] - 2016-04-25 +### Added +- Debates +- Proposals +- Basic Spending Proposals + +### Changed +- Rails 4.2.6 +- Ruby 2.2.3 + +[Unreleased]: https://github.com/consul/consul/compare/v0.12...consul:master +[0.12.0]: https://github.com/consul/consul/compare/v0.11...v0.12 +[0.11.0]: https://github.com/consul/consul/compare/v0.10...v0.11 +[0.10.0]: https://github.com/consul/consul/compare/v0.9...v0.10 +[0.9.0]: https://github.com/consul/consul/compare/v0.8...v0.9 +[0.8.0]: https://github.com/consul/consul/compare/v0.7...v0.8 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..ed9d6fe56 100644 --- a/Gemfile +++ b/Gemfile @@ -1,13 +1,13 @@ 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' +gem 'acts-as-taggable-on', '~> 5.0.0' +gem 'acts_as_votable', '~> 0.11.1' gem 'ahoy_matey', '~> 1.6.0' -gem 'ancestry', '~> 2.2.2' -gem 'browser', '~> 2.3.0' -gem 'cancancan', '~> 1.16.0' +gem 'ancestry', '~> 3.0.1' +gem 'browser', '~> 2.5.2' +gem 'cancancan', '~> 2.1.2' gem 'ckeditor', '~> 4.2.3' gem 'cocoon', '~> 1.2.9' gem 'coffee-rails', '~> 4.2.1' @@ -20,70 +20,75 @@ gem 'devise_security_extension', '~> 0.10.0' gem 'foundation-rails', '~> 6.2.4.0' gem 'foundation_rails_helper', '~> 2.0.0' gem 'graphiql-rails', '~> 1.4.1' -gem 'graphql', '~> 1.6.3' +gem 'graphql', '~> 1.7.7' gem 'groupdate', '~> 3.2.0' gem 'initialjs-rails', '~> 0.2.0.5' -gem 'invisible_captcha', '~> 0.9.2' +gem 'invisible_captcha', '~> 0.10.0' gem 'jquery-fileupload-rails' gem 'jquery-rails', '~> 4.3.1' gem 'jquery-ui-rails', '~> 6.0.1' -gem 'kaminari', '~> 1.0.1' +gem 'kaminari', '~> 1.1.1' gem 'newrelic_rpm', '~> 4.1.0.333' -gem 'omniauth', '~> 1.6.1' +gem 'omniauth', '~> 1.8.1' gem 'omniauth-facebook', '~> 4.0.0' gem 'omniauth-google-oauth2', '~> 0.4.0' gem 'omniauth-twitter', '~> 1.4.0' gem 'paperclip', '~> 5.1.0' -gem 'paranoia', '~> 2.3.1' +gem 'paranoia', '~> 2.4.0' 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' -gem 'rollbar', '~> 2.14.1' +gem 'rollbar', '~> 2.15.5' gem 'rubyzip', '~> 1.2.0' gem 'sass-rails', '~> 5.0', '>= 5.0.4' gem 'savon', '~> 2.11.1' -gem 'sitemap_generator', '~> 5.3.1' -gem 'social-share-button', '~> 0.10' +gem 'sitemap_generator', '~> 6.0.0' +gem 'social-share-button', '~> 1.1' gem 'sprockets', '~> 3.7.1' gem 'turbolinks', '~> 2.5.3' gem 'turnout', '~> 2.4.0' -gem 'uglifier', '~> 3.2.0' -gem 'unicorn', '~> 5.3.0' -gem 'whenever', '~> 0.9.7', require: false +gem 'uglifier', '~> 4.1.2' +gem 'unicorn', '~> 5.4.0' +gem 'whenever', '~> 0.10.0', 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 'factory_girl_rails', '~> 4.8.0' - gem "faker", '~> 1.7.3' + gem 'bullet', '~> 5.7.0' + gem 'byebug', '~> 9.1.0' + gem 'factory_bot_rails', '~> 4.8.2' + gem 'faker', '~> 1.8.7' gem 'i18n-tasks', '~> 0.9.15' - gem 'knapsack', '~> 1.13.3' + gem 'knapsack_pro', '~> 0.53.0' 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 group :test do - gem 'capybara', '~> 2.14.0' + gem 'capybara', '~> 2.17.0' gem 'coveralls', '~> 0.8.21', require: false gem 'database_cleaner', '~> 1.6.1' gem 'email_spec', '~> 2.1.0' - gem 'poltergeist', '~> 1.15.0' + gem 'poltergeist', '~> 1.17.0' gem 'rspec-rails', '~> 3.6' end group :development do - gem 'capistrano', '~> 3.8.1', require: false + gem 'capistrano', '~> 3.10.1', require: false gem 'capistrano-bundler', '~> 1.2', require: false - gem "capistrano-rails", '~> 1.2.3', require: false + gem 'capistrano-rails', '~> 1.3.1', require: false gem 'capistrano3-delayed-job', '~> 1.7.3' gem 'mdl', '~> 0.4.0', require: false + gem 'rubocop', '~> 0.52.1', require: false + gem 'rubocop-rspec', '~> 1.21.0', 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..2eb479878 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,45 +2,45 @@ 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) tzinfo (~> 1.1) - acts-as-taggable-on (4.0.0) - activerecord (>= 4.0) - acts_as_votable (0.10.0) - addressable (2.5.1) - public_suffix (~> 2.0, >= 2.0.2) + acts-as-taggable-on (5.0.0) + activerecord (>= 4.2.8) + acts_as_votable (0.11.1) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) ahoy_matey (1.6.0) addressable browser (~> 2.0) @@ -57,8 +57,8 @@ GEM akami (1.3.1) gyoku (>= 0.4.0) nokogiri - ancestry (2.2.2) - activerecord (>= 3.0.0) + ancestry (3.0.1) + activerecord (>= 3.2.0) arel (6.0.4) ast (2.3.0) babel-source (5.8.35) @@ -66,34 +66,34 @@ GEM babel-source (>= 4.0, < 6) execjs (~> 2.0) bcrypt (3.1.11) - browser (2.3.0) + browser (2.5.2) builder (3.2.3) - bullet (5.5.1) + bullet (5.7.1) activesupport (>= 3.0.0) - uniform_notifier (~> 1.10.0) - byebug (9.0.6) - cancancan (1.16.0) - capistrano (3.8.2) + uniform_notifier (~> 1.11.0) + byebug (9.1.0) + cancancan (2.1.2) + capistrano (3.10.1) airbrussh (>= 1.0.0) i18n rake (>= 10.0.0) sshkit (>= 1.9.0) - capistrano-bundler (1.2.0) + capistrano-bundler (1.3.0) capistrano (~> 3.1) sshkit (~> 1.2) - capistrano-rails (1.2.3) + capistrano-rails (1.3.1) capistrano (~> 3.1) capistrano-bundler (~> 1.1) - capistrano3-delayed-job (1.7.3) + capistrano3-delayed-job (1.7.5) capistrano (~> 3.0, >= 3.0.0) daemons (~> 1.2.4) - capybara (2.14.4) + capybara (2.17.0) addressable - mime-types (>= 1.16) + mini_mime (>= 0.1.3) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) - xpath (~> 2.0) + xpath (>= 2.0, < 4.0) chronic (0.10.2) ckeditor (4.2.4) cocaine @@ -102,7 +102,7 @@ GEM cliver (0.3.2) cocaine (0.5.8) climate_control (>= 0.0.3, < 1.0) - cocoon (1.2.10) + cocoon (1.2.11) coffee-rails (4.2.2) coffee-script (>= 2.2.0) railties (>= 4.0.0) @@ -117,9 +117,10 @@ GEM term-ansicolor (~> 1.3) thor (~> 0.19.4) tins (~> 1.6) - daemons (1.2.4) + crass (1.0.3) + daemons (1.2.6) dalli (2.7.6) - database_cleaner (1.6.1) + database_cleaner (1.6.2) debug_inspector (0.0.3) delayed_job (4.1.3) activesupport (>= 3.0, < 5.2) @@ -149,16 +150,16 @@ GEM launchy (~> 2.1) mail (~> 2.6) errbase (0.0.3) - erubi (1.6.1) + erubi (1.7.0) erubis (2.7.0) execjs (2.7.0) - factory_girl (4.8.0) + factory_bot (4.8.2) activesupport (>= 3.0.0) - factory_girl_rails (4.8.0) - factory_girl (~> 4.8.0) + factory_bot_rails (4.8.2) + factory_bot (~> 4.8.2) railties (>= 3.0.0) - faker (1.7.3) - i18n (~> 0.5) + faker (1.8.7) + i18n (>= 0.7) faraday (0.12.1) multipart-post (>= 1.2, < 3) foundation-rails (6.2.4.0) @@ -172,23 +173,24 @@ 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) + graphiql-rails (1.4.8) rails - graphql (1.6.4) + graphql (1.7.7) groupdate (3.2.0) activesupport (>= 3) gyoku (1.3.1) builder (>= 2.1.2) - hashie (3.5.5) - highline (1.7.8) + hashie (3.5.7) + highline (1.7.10) htmlentities (4.3.4) httpi (2.4.2) rack socksify - i18n (0.8.6) - i18n-tasks (0.9.18) + i18n (0.9.1) + concurrent-ruby (~> 1.0) + i18n-tasks (0.9.19) activesupport (>= 4.0.2) ast (>= 2.1.0) easy_translate (>= 0.5.0) @@ -200,7 +202,7 @@ GEM terminal-table (>= 1.5.1) initialjs-rails (0.2.0.5) railties (>= 3.1, < 6.0) - invisible_captcha (0.9.3) + invisible_captcha (0.10.0) rails (>= 3.2.0) jquery-fileupload-rails (0.4.7) actionpack (>= 3.1) @@ -214,22 +216,21 @@ GEM railties (>= 3.2.16) json (2.1.0) jwt (1.5.6) - kaminari (1.0.1) + kaminari (1.1.1) activesupport (>= 4.1.0) - kaminari-actionview (= 1.0.1) - kaminari-activerecord (= 1.0.1) - kaminari-core (= 1.0.1) - kaminari-actionview (1.0.1) + kaminari-actionview (= 1.1.1) + kaminari-activerecord (= 1.1.1) + kaminari-core (= 1.1.1) + kaminari-actionview (1.1.1) actionview - kaminari-core (= 1.0.1) - kaminari-activerecord (1.0.1) + kaminari-core (= 1.1.1) + kaminari-activerecord (1.1.1) activerecord - kaminari-core (= 1.0.1) - kaminari-core (1.0.1) - kgio (2.11.0) - knapsack (1.13.3) + kaminari-core (= 1.1.1) + kaminari-core (1.1.1) + kgio (2.11.1) + knapsack_pro (0.53.0) rake - timecop (>= 0.1.0) kramdown (1.14.0) launchy (2.4.3) addressable (~> 2.3) @@ -239,10 +240,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,19 +253,20 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) mimemagic (0.3.2) - mini_portile2 (2.2.0) - minitest (5.10.3) + mini_mime (1.0.0) + mini_portile2 (2.3.0) + minitest (5.11.1) mixlib-cli (1.7.0) mixlib-config (2.2.4) - multi_json (1.12.1) + multi_json (1.12.2) multi_xml (0.6.0) multipart-post (2.0.0) net-scp (1.2.1) net-ssh (>= 2.6.5) - net-ssh (4.1.0) + net-ssh (4.2.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) @@ -272,7 +275,7 @@ GEM multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 3) - omniauth (1.6.1) + omniauth (1.8.1) hashie (>= 3.4.6, < 3.6.0) rack (>= 1.6.2, < 3) omniauth-facebook (4.0.0) @@ -298,22 +301,22 @@ GEM cocaine (~> 0.5.5) mime-types mimemagic (~> 0.3.0) - parallel (1.11.2) - paranoia (2.3.1) + parallel (1.12.1) + paranoia (2.4.0) activerecord (>= 4.0, < 5.2) - parser (2.4.0.0) - ast (~> 2.2) + parser (2.4.0.2) + ast (~> 2.3) pg (0.21.0) pg_search (2.0.1) activerecord (>= 4.2) activesupport (>= 4.2) arel (>= 6) - poltergeist (1.15.0) + poltergeist (1.17.0) capybara (~> 2.1) cliver (~> 0.3.1) websocket-driver (>= 0.2.0) powerpack (0.1.1) - public_suffix (2.0.5) + public_suffix (3.0.1) quiet_assets (1.1.0) railties (>= 3.1, < 5.0) rack (1.6.8) @@ -323,69 +326,72 @@ 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) - rails-dom-testing (1.0.8) - activesupport (>= 4.2.0.beta, < 5.0) + rails-dom-testing (1.0.9) + activesupport (>= 4.2.0, < 5.0) nokogiri (~> 1.6) 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) + raindrops (0.19.0) + rake (12.3.0) redcarpet (3.4.0) referer-parser (0.3.0) request_store (1.3.2) responders (2.4.0) actionpack (>= 4.2.0, < 5.3) railties (>= 4.2.0, < 5.3) - rinku (2.0.3) - rollbar (2.14.1) + rinku (2.0.4) + rollbar (2.15.5) multi_json - rspec-core (3.6.0) - rspec-support (~> 3.6.0) - rspec-expectations (3.6.0) + rspec-core (3.7.1) + rspec-support (~> 3.7.0) + rspec-expectations (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.6.0) - rspec-mocks (3.6.0) + rspec-support (~> 3.7.0) + rspec-mocks (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.6.0) - rspec-rails (3.6.1) + rspec-support (~> 3.7.0) + rspec-rails (3.7.2) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) - rspec-core (~> 3.6.0) - rspec-expectations (~> 3.6.0) - rspec-mocks (~> 3.6.0) - rspec-support (~> 3.6.0) - rspec-support (3.6.0) - rubocop (0.49.1) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) + rspec-support (~> 3.7.0) + rspec-support (3.7.0) + rubocop (0.52.1) parallel (~> 1.10) - parser (>= 2.3.3.1, < 3.0) + parser (>= 2.4.0.2, < 3.0) powerpack (~> 0.1) - rainbow (>= 1.99.1, < 3.0) + rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) - ruby-progressbar (1.8.1) + rubocop-rspec (1.21.0) + rubocop (>= 0.52.0) + ruby-progressbar (1.9.0) rubyzip (1.2.1) rvm1-capistrano3 (1.4.0) capistrano (~> 3.0) @@ -393,7 +399,7 @@ GEM safely_block (0.2.0) errbase sass (3.4.25) - sass-rails (5.0.6) + sass-rails (5.0.7) railties (>= 4.0.0, < 6) sass (~> 3.1) sprockets (>= 2.8, < 4.0) @@ -415,9 +421,9 @@ GEM json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.1) - sitemap_generator (5.3.1) + sitemap_generator (6.0.0) builder (~> 3.0) - social-share-button (0.10.0) + social-share-button (1.1.0) coffee-rails socksify (1.7.1) spring (2.0.2) @@ -435,7 +441,7 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - sshkit (1.14.0) + sshkit (1.15.1) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) term-ansicolor (1.6.0) @@ -445,8 +451,7 @@ GEM thor (0.19.4) thread (0.2.2) thread_safe (0.3.6) - tilt (2.0.7) - timecop (0.9.1) + tilt (2.0.8) tins (1.15.0) turbolinks (2.5.3) coffee-rails @@ -455,15 +460,15 @@ 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) + uglifier (4.1.3) execjs (>= 0.3.0, < 3) unicode-display_width (1.3.0) - unicorn (5.3.0) + unicorn (5.4.0) kgio (~> 2.6) raindrops (~> 0.7) - uniform_notifier (1.10.0) + uniform_notifier (1.11.0) user_agent_parser (2.3.1) uuidtools (2.1.5) warden (1.2.7) @@ -475,31 +480,31 @@ GEM activemodel (>= 4.2) debug_inspector railties (>= 4.2) - websocket-driver (0.6.5) + websocket-driver (0.7.0) websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.2) - whenever (0.9.7) + websocket-extensions (0.1.3) + whenever (0.10.0) chronic (>= 0.6.3) - xpath (2.1.0) - nokogiri (~> 1.3) + xpath (3.0.0) + nokogiri (~> 1.8) PLATFORMS ruby DEPENDENCIES - acts-as-taggable-on (~> 4.0.0) - acts_as_votable (~> 0.10.0) + acts-as-taggable-on (~> 5.0.0) + acts_as_votable (~> 0.11.1) ahoy_matey (~> 1.6.0) - ancestry (~> 2.2.2) - browser (~> 2.3.0) - bullet (~> 5.5.1) - byebug (~> 9.0.6) - cancancan (~> 1.16.0) - capistrano (~> 3.8.1) + ancestry (~> 3.0.1) + browser (~> 2.5.2) + bullet (~> 5.7.0) + byebug (~> 9.1.0) + cancancan (~> 2.1.2) + capistrano (~> 3.10.1) capistrano-bundler (~> 1.2) - capistrano-rails (~> 1.2.3) + capistrano-rails (~> 1.3.1) capistrano3-delayed-job (~> 1.7.3) - capybara (~> 2.14.0) + capybara (~> 2.17.0) ckeditor (~> 4.2.3) cocoon (~> 1.2.9) coffee-rails (~> 4.2.1) @@ -512,59 +517,61 @@ DEPENDENCIES devise-async (~> 0.10.2) devise_security_extension (~> 0.10.0) email_spec (~> 2.1.0) - factory_girl_rails (~> 4.8.0) - faker (~> 1.7.3) + factory_bot_rails (~> 4.8.2) + faker (~> 1.8.7) foundation-rails (~> 6.2.4.0) foundation_rails_helper (~> 2.0.0) graphiql-rails (~> 1.4.1) - graphql (~> 1.6.3) + graphql (~> 1.7.7) groupdate (~> 3.2.0) i18n-tasks (~> 0.9.15) initialjs-rails (~> 0.2.0.5) - invisible_captcha (~> 0.9.2) + invisible_captcha (~> 0.10.0) jquery-fileupload-rails jquery-rails (~> 4.3.1) jquery-ui-rails (~> 6.0.1) - kaminari (~> 1.0.1) - knapsack (~> 1.13.3) + kaminari (~> 1.1.1) + knapsack_pro (~> 0.53.0) launchy (~> 2.4.3) letter_opener_web (~> 1.3.1) mdl (~> 0.4.0) newrelic_rpm (~> 4.1.0.333) - omniauth (~> 1.6.1) + omniauth (~> 1.8.1) omniauth-facebook (~> 4.0.0) omniauth-google-oauth2 (~> 0.4.0) omniauth-twitter (~> 1.4.0) paperclip (~> 5.1.0) - paranoia (~> 2.3.1) + paranoia (~> 2.4.0) pg (~> 0.21.0) pg_search (~> 2.0.1) - poltergeist (~> 1.15.0) + poltergeist (~> 1.17.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) rinku (~> 2.0.2) - rollbar (~> 2.14.1) + rollbar (~> 2.15.5) rspec-rails (~> 3.6) - rubocop (~> 0.49.1) + rubocop (~> 0.52.1) + rubocop-rspec (~> 1.21.0) rubyzip (~> 1.2.0) rvm1-capistrano3 (~> 1.4.0) sass-rails (~> 5.0, >= 5.0.4) savon (~> 2.11.1) scss_lint (~> 0.54.0) - sitemap_generator (~> 5.3.1) - social-share-button (~> 0.10) + sitemap_generator (~> 6.0.0) + social-share-button (~> 1.1) spring (~> 2.0.1) spring-commands-rspec (~> 1.0.4) sprockets (~> 3.7.1) turbolinks (~> 2.5.3) turnout (~> 2.4.0) - uglifier (~> 3.2.0) - unicorn (~> 5.3.0) + uglifier (~> 4.1.2) + unicorn (~> 5.4.0) web-console (~> 3.3.0) - whenever (~> 0.9.7) + whenever (~> 0.10.0) BUNDLED WITH - 1.15.3 + 1.16.0 diff --git a/README.md b/README.md index 48f6bfcb8..ba9e18d96 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![Logo of CONSUL](https://raw.githubusercontent.com/consul/consul/master/public/consul_logo.png) +![CONSUL logo](https://raw.githubusercontent.com/consul/consul/master/public/consul_logo.png) # CONSUL @@ -21,13 +21,13 @@ This is the opensource code repository of the eParticipation website CONSUL, ori ## Current state -Development started on [2015 July 15th](https://github.com/consul/consul/commit/8db36308379accd44b5de4f680a54c41a0cc6fc6). Code was deployed to production on 2015 september 7th to [decide.madrid.es](https://decide.madrid.es). Since then new features are added often. You can take a look at the current features in [features]( http://www.decide.es/en/) or [docs](https://github.com/consul/consul/tree/master/doc) and future features in the [open issues list](https://github.com/consul/consul/issues). For current status on upcoming features go to [Roadmap](https://github.com/consul/consul/projects/6) +Development started on [2015 July 15th](https://github.com/consul/consul/commit/8db36308379accd44b5de4f680a54c41a0cc6fc6). Code was deployed to production on 2015 september 7th to [decide.madrid.es](https://decide.madrid.es). Since then new features are added often. You can take a look at the current features at the [project's website](http://consulproject.org/) and future features at the [Roadmap](https://github.com/consul/consul/projects/6) and [open issues list](https://github.com/consul/consul/issues). ## Configuration for development and test environments **NOTE**: For more detailed instructions check the [docs](https://github.com/consul/docs/tree/master/en/getting_started/prerequisites) -Prerequisites: install git, Ruby 2.3.2, bundler gem, and PostgreSQL (>=9.4). +Prerequisites: install git, Ruby 2.3.2, `bundler` gem, and PostgreSQL (>=9.4). ```bash git clone https://github.com/consul/consul.git @@ -67,7 +67,7 @@ But for some actions like voting, you will need a verified user, the seeds file ## Documentation -Please check the ongoing documentation at https://consul_docs.gitbooks.io/docs/content/ to learn more about how to start your own CONSUL fork, install it, customize it and learn to use it from an administrator/maintainer perspective. You can contribute to it at https://github.com/consul/docs +Check the ongoing documentation at [https://consul_docs.gitbooks.io/docs/content/](https://consul_docs.gitbooks.io/docs/content/) to learn more about how to start your own CONSUL fork, install it, customize it and learn to use it from an administrator/maintainer perspective. You can contribute to it at [https://github.com/consul/docs](https://github.com/consul/docs) ## License @@ -76,3 +76,7 @@ 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/en/getting_started/docker.html](https://consul_docs.gitbooks.io/docs/content/en/getting_started/docker.html) diff --git a/README_ES.md b/README_ES.md index b22312e29..9e373dea8 100644 --- a/README_ES.md +++ b/README_ES.md @@ -21,7 +21,7 @@ Este es el repositorio de código abierto de la Aplicación de Participación Ci ## Estado del proyecto -El desarrollo de esta aplicación comenzó el [15 de Julio de 2015](https://github.com/consul/consul/commit/8db36308379accd44b5de4f680a54c41a0cc6fc6) y el código fue puesto en producción el día 7 de Septiembre de 2015 en [decide.madrid.es](https://decide.madrid.es). Desde entonces se le añaden mejoras y funcionalidades constantemente. Las funcionalidades actuales se pueden consultar en [características](http://www.decide.es/es/) o en la [documentación](https://github.com/consul/consul/tree/master/doc) y las siguientes funcionaliades en la lista de [tareas por hacer](https://github.com/consul/consul/issues). Para conocer el estado actual de las próximas caracteristicas, vaya a [Roadmap](https://github.com/consul/consul/projects/6) +El desarrollo de esta aplicación comenzó el [15 de Julio de 2015](https://github.com/consul/consul/commit/8db36308379accd44b5de4f680a54c41a0cc6fc6) y el código fue puesto en producción el día 7 de Septiembre de 2015 en [decide.madrid.es](https://decide.madrid.es). Desde entonces se le añaden mejoras y funcionalidades constantemente. Las funcionalidades actuales se pueden consultar en la [la página del projecto](http://consulproject.org/es) y las futuras funcionalidades en el [Roadmap](https://github.com/consul/consul/projects/6) y [el listado de issues](https://github.com/consul/consul/issues). ## Configuración para desarrollo y tests @@ -29,8 +29,7 @@ El desarrollo de esta aplicación comenzó el [15 de Julio de 2015](https://gith Prerequisitos: tener instalado git, Ruby 2.3.2, la gema `bundler` y PostgreSQL (9.4 o superior). -``` - +```bash git clone https://github.com/consul/consul.git cd consul bundle install @@ -68,7 +67,7 @@ Pero para ciertas acciones, como apoyar, necesitarás un usuario verificado, el ## Documentación -Por favor visita la documentación que está siendo completada en https://consul_docs.gitbooks.io/docs/content/ para conocer más sobre este proyecto, como comenzar tu propio fork, instalarlo, customizarlo y usarlo como administrador/mantenedor. Puedes colaborar en ella en https://github.com/consul/docs +Por favor visita la documentación que está siendo completada en [https://consul_docs.gitbooks.io/docs/content/](https://consul_docs.gitbooks.io/docs/content/) para conocer más sobre este proyecto, como comenzar tu propio fork, instalarlo, customizarlo y usarlo como administrador/mantenedor. Puedes colaborar en ella en [https://github.com/consul/docs](https://github.com/consul/docs) ## Licencia @@ -77,3 +76,7 @@ El código de este proyecto está publicado bajo la licencia AFFERO GPL v3 (ver ## Contribuciones Ver fichero [CONTRIBUTING_ES.md](CONTRIBUTING_ES.md) + +## Desarrollo en local con Docker + +Puedes leer la guía en [https://consul_docs.gitbooks.io/docs/content/es/getting_started/docker.html](https://consul_docs.gitbooks.io/docs/content/es/getting_started/docker.html) diff --git a/Rakefile b/Rakefile index ef2c381ff..13a99536b 100644 --- a/Rakefile +++ b/Rakefile @@ -4,4 +4,4 @@ require File.expand_path('../config/application', __FILE__) Rails.application.load_tasks -Knapsack.load_tasks if defined?(Knapsack) \ No newline at end of file +KnapsackPro.load_tasks if defined?(KnapsackPro) 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..611ecc546 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,16 @@ //= 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 +//= require table_sortable var initialize_modules = function() { App.Comments.initialize(); @@ -98,10 +108,17 @@ 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(); + App.TableSortable.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/legislation.js.coffee b/app/assets/javascripts/legislation.js.coffee index 3eb53e6d2..303d4c98e 100644 --- a/app/assets/javascripts/legislation.js.coffee +++ b/app/assets/javascripts/legislation.js.coffee @@ -1,14 +1,9 @@ App.Legislation = initialize: -> - $('#js-toggle-debate').on - click: -> - $('#debate-show').toggle() - $('#js-toggle-small-debate').on click: -> - $('#debate-show').toggle() - $('span').toggleClass('icon-angle-up') + $(this).find($('span')).toggleClass('icon-angle-up') $('form#new_legislation_answer input.button').hide() $('form#new_legislation_answer input[type=radio]').on @@ -19,7 +14,3 @@ App.Legislation = $('form#draft_version_go_to_version select').on change: -> $('form#draft_version_go_to_version').submit() - - $('#js-toggle-legislation-process-header').on - click: -> - $('[data-target="legislation-header-full"]').toggle() diff --git a/app/assets/javascripts/map.js.coffee b/app/assets/javascripts/map.js.coffee new file mode 100644 index 000000000..797232c4f --- /dev/null +++ b/app/assets/javascripts/map.js.coffee @@ -0,0 +1,86 @@ +App.Map = + + initialize: -> + maps = $('*[data-map]') + + if maps.length > 0 + $.each maps, (index, map) -> + App.Map.initializeMap map + + $('.js-toggle-map').on + click: -> + App.Map.toogleMap() + + 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 + + toogleMap: -> + $('.map').toggle() + $('.location-map-remove-marker-button').toggle() \ No newline at end of file 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/table_sortable.js.coffee b/app/assets/javascripts/table_sortable.js.coffee new file mode 100644 index 000000000..331f794a4 --- /dev/null +++ b/app/assets/javascripts/table_sortable.js.coffee @@ -0,0 +1,23 @@ +App.TableSortable = + getCellValue: (row, index) -> + $(row).children('td').eq(index).text() + + comparer: (index) -> + (a, b) -> + valA = App.TableSortable.getCellValue(a, index) + valB = App.TableSortable.getCellValue(b, index) + return if $.isNumeric(valA) and $.isNumeric(valB) then valA - valB else valA.localeCompare(valB) + + initialize: -> + $('table.sortable th').click -> + table = $(this).parents('table').eq(0) + rows = table.find('tr:gt(0)').not('tfoot tr').toArray().sort(App.TableSortable.comparer($(this).index())) + @asc = !@asc + if !@asc + rows = rows.reverse() + i = 0 + while i < rows.length + table.append rows[i] + i++ + return + \ No newline at end of file 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..a2e3e0e7f 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 { @@ -191,10 +270,17 @@ $admin-color: #cf3638; } } +.sortable thead th:hover { + text-decoration: underline; + cursor: pointer; +} + // 02. Sidebar // ----------- .admin-sidebar { + background: $sidebar; + background: linear-gradient(to bottom, #245b80 0%, #488fb5 100%); border-right: 1px solid $border; @include breakpoint(medium) { @@ -208,7 +294,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 +305,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 +338,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 +610,7 @@ table { .callout { height: $line-height * 2; line-height: $line-height * 2; + margin: 0; padding: 0 $line-height / 2; } } @@ -898,3 +984,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 9ecf74442..e47ba0343 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -18,7 +18,10 @@ // 16. Flags // 17. Activity // 18. Banners -// 19. Documents +// 19. Recommended Section Home +// 20. Documents +// 21. Related content +// 22. Images // // 01. Global styles @@ -154,6 +157,10 @@ a { margin-bottom: $line-height; } +.margin-left { + margin-left: $line-height; +} + .margin-right { margin-right: $line-height; } @@ -205,19 +212,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; } } @@ -278,7 +308,11 @@ a { .icon-angle-left { clear: both; color: $text-medium; - float: left; + display: inline-block; +} + +.back:not([class^="icon-"]) { + text-decoration: underline; } .tabs-content { @@ -286,13 +320,16 @@ a { } .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 +357,36 @@ 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; +} + // 02. Header // ---------- @@ -367,7 +430,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; @@ -587,7 +654,7 @@ header { text-align: left; @include breakpoint(medium) { - margin-right: $line-height * 1.5; + margin-right: $line-height; } &:hover { @@ -1083,8 +1150,13 @@ form { color: #ecf00b; font-size: rem-calc(10); position: absolute; - right: 8px; + left: 12px; top: 6px; + + @include breakpoint(medium) { + left: auto; + right: 8px; + } } } @@ -1972,94 +2044,55 @@ table { // ------------ .activity { - margin-bottom: $line-height * 2; - .accordion li { - margin-bottom: $line-height / 2; - - .accordion-title { - border-bottom: 1px solid $border; - background: #f8f9fb; - font-size: $small-font-size; - padding: $line-height / 2; - } - - .accordion-content { - padding: 0; - } - } - - .accordion .title { - display: block; - line-height: $line-height; - } - - .accordion .icon { - font-size: rem-calc(20); - float: left; - margin-right: $line-height / 3; - - &.icon-debates { - margin-top: rem-calc(3); - } - } - - 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; + + span { + background: none; + border: 1px solid #ececec; + } + } +} + +.following { + + .follow-list { + list-style-type: circle; + padding: $line-height / 2; + + li { + margin-bottom: $line-height / 2; + margin-left: $line-height; + } + } + + h3 { + font-size: rem-calc(24); + margin-top: $line-height; + padding-left: rem-calc(30); + position: relative; + + span { + left: 0; + position: absolute; + top: 2px; + } + } + + .interests { + + @include breakpoint(medium) { + border-left: 1px solid #ececec; + padding-left: $line-height; + } } } @@ -2136,68 +2169,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 { @@ -2254,6 +2365,149 @@ 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; +} + +// 21. Related content +// ------------------- + +.related-content { + border-top: 1px solid $border; + + h2 { + font-size: rem-calc(24); + + span { + color: #4f4f4f; + font-weight: normal; + } + } +} + +.add-related-content { + display: block; + + @include breakpoint(medium) { + float: right; + } +} + +.related-content-list { + list-style-type: none; + margin-left: 0; + + li { + border-bottom: 1px solid $border; + margin-bottom: 0 !important; + padding: $line-height / 2; + + &:first-child { + border-top: 1px solid $border; + } + + @include breakpoint(medium) { + + .score-actions { + display: none; + float: right; + } + } + + &:hover { + background: #f9f9f9; + + .score-actions { + display: block; + } + } + } + + h3 { + font-size: $base-font-size; + font-weight: normal; + } + + .related-content-title { + color: #4f4f4f; + font-size: rem-calc(12); + text-transform: uppercase; + } + + .flag { + margin-top: $line-height / 2; + } +} + +.relate-content-score { + display: block; + + @include breakpoint(medium) { + text-align: center; + } + + a { + font-weight: bold; + margin-right: $line-height; + padding-left: rem-calc(20); + position: relative; + text-decoration: none; + + &.score-positive:before, + &.score-negative:before { + font-family: 'icons'; + left: 0; + position: absolute; + } + + &.score-positive { + color: $color-success; + + &:before { + color: $color-success; + content: '\6c'; + } + } + + &.score-negative { + color: $color-alert; + + &:before { + color: $color-alert; + content: '\76'; + } + } + } +} + +// 22. Images +// ----------------- + +.images .button { + margin-top: $line-height / 2; +} diff --git a/app/assets/stylesheets/legislation_process.scss b/app/assets/stylesheets/legislation_process.scss index 2ec9f8ee9..042348f42 100644 --- a/app/assets/stylesheets/legislation_process.scss +++ b/app/assets/stylesheets/legislation_process.scss @@ -39,10 +39,6 @@ $border-dark: darken($border, 10%); } } - .legislation-debate-show { - display: none; - } - .debate-add-info { border-top: 1px solid $border-dark; margin-top: $line-height; @@ -109,16 +105,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, @@ -360,10 +353,6 @@ $border-dark: darken($border, 10%); .legislation-allegation { padding-top: 1rem; - .legislation-debate-show { - margin-top: 2rem; - } - .headline { margin-bottom: 0; @@ -459,11 +448,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 +466,7 @@ $border-dark: darken($border, 10%); } .draft-allegation { + @include breakpoint(medium) { display: flex; padding-left: 0.9375rem; @@ -493,7 +483,6 @@ $border-dark: darken($border, 10%); } } - // Panel calcs for desktop @media screen and (min-width: 40em) { .calc-index { width: calc(35% - 25px); @@ -509,6 +498,7 @@ $border-dark: darken($border, 10%); width: rem-calc(50); .draft-panel { + .panel-title { display: none; } @@ -912,19 +902,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 +922,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 +959,7 @@ $border-dark: darken($border, 10%); // 09. Legislation comments // ----------------- + .legislation-comments { .pull-right { @@ -1020,6 +1008,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/pages.scss b/app/assets/stylesheets/pages.scss index 7114b01d6..d3db7e9b2 100644 --- a/app/assets/stylesheets/pages.scss +++ b/app/assets/stylesheets/pages.scss @@ -44,8 +44,7 @@ // 03. Content // ---------------------- -.more-info-content, -.communities-show { +.more-info-content { h3 { color: $brand; diff --git a/app/assets/stylesheets/participation.scss b/app/assets/stylesheets/participation.scss index 3d90ee128..38eaf7d91 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 @@ -256,7 +257,7 @@ .icon-debates, .icon-proposals, .icon-budget, - .icon-documents { + .icon-image { font-size: rem-calc(50); line-height: $line-height; opacity: 0.5; @@ -267,7 +268,7 @@ } .icon-proposals, - .icon-documents { + .icon-image { color: $proposals; } @@ -300,15 +301,27 @@ } .proposal-form, +.document-form, .topic-form, -.topic-new, -.document-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, +.milestone-new, +.milestone-edit { + @include direct-uploads; +} + // 03. Show participation // ---------------------- @@ -323,14 +336,23 @@ .draft-panels, .debate-questions, .communities-show, -.topic-show { +.topic-show, +.milestone-content { p { 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 { @@ -349,11 +371,6 @@ width: rem-calc(48); } - .edit-debate, - .edit-proposal { - margin-bottom: 0; - } - .debate-info, .proposal-info, .investment-project-info, @@ -465,6 +482,10 @@ .icon-video { color: #cc181e; } + + p { + margin-bottom: 0; + } } .supports { @@ -640,6 +661,42 @@ } } +.budget-investments-list .budget-investment, +.proposals-list .proposal { + + @include breakpoint(medium) { + + .panel { + + &.with-image { + padding: 0 $line-height / 2 0 0; + } + } + + .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, @@ -755,7 +812,7 @@ background: image-url('truncate.png'); background-repeat: repeat-x; bottom: 0; - height: 24px; + height: rem-calc(24); position: absolute; width: 100%; } @@ -769,12 +826,6 @@ display: none; } -.document-form { - max-width: 75rem; - margin-left: auto; - margin-right: auto; -} - .more-info { clear: both; color: $text-medium; @@ -786,7 +837,9 @@ } .debate, -.debate-show { +.debate-show, +.proposal-show, +.legislation-proposals { .votes { @include votes; @@ -801,6 +854,7 @@ } } +.proposal-show .votes, .debate-show .votes { border: 0; padding: $line-height / 2 0; @@ -859,6 +913,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; @@ -1393,33 +1460,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 { @@ -1468,18 +1508,8 @@ // 08. Polls // ---------------------- -.dark-heading { - background: #2d3e50; - color: #fff; - - .title { - color: #92ba48; - } - - .button { - background: #fff; - color: $brand; - } +.polls-show-header { + background: #fafafa; .callout { @@ -1495,28 +1525,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; + } } } @@ -1614,9 +1733,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 { @@ -1637,6 +1760,10 @@ margin-right: $line-height / 4; min-width: rem-calc(168); + @include breakpoint(medium down) { + width: 100%; + } + &.answered { background: #f4f8ec; border: 2px solid #92ba48; @@ -1658,3 +1785,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_investment_milestones_controller.rb b/app/controllers/admin/budget_investment_milestones_controller.rb index fb5f42384..73989939b 100644 --- a/app/controllers/admin/budget_investment_milestones_controller.rb +++ b/app/controllers/admin/budget_investment_milestones_controller.rb @@ -14,7 +14,8 @@ class Admin::BudgetInvestmentMilestonesController < Admin::BaseController @milestone = Budget::Investment::Milestone.new(milestone_params) @milestone.investment = @investment if @milestone.save - redirect_to admin_budget_budget_investment_path(@investment.budget, @investment), notice: t('admin.milestones.create.notice') + redirect_to admin_budget_budget_investment_path(@investment.budget, @investment), + notice: t('admin.milestones.create.notice') else render :new end @@ -25,7 +26,8 @@ class Admin::BudgetInvestmentMilestonesController < Admin::BaseController def update if @milestone.update(milestone_params) - redirect_to admin_budget_budget_investment_path(@investment.budget, @investment), notice: t('admin.milestones.update.notice') + redirect_to admin_budget_budget_investment_path(@investment.budget, @investment), + notice: t('admin.milestones.update.notice') else render :edit end @@ -33,23 +35,25 @@ class Admin::BudgetInvestmentMilestonesController < Admin::BaseController def destroy @milestone.destroy - redirect_to admin_budget_budget_investment_path(@investment.budget, @investment), notice: t('admin.milestones.delete.notice') + redirect_to admin_budget_budget_investment_path(@investment.budget, @investment), + notice: t('admin.milestones.delete.notice') end private def milestone_params params.require(:budget_investment_milestone) - .permit(:title, :description, :budget_investment_id) + .permit(:title, :description, :publication_date, :budget_investment_id, + image_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy], + documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy]) end def load_budget_investment - @investment = Budget::Investment.find params[:budget_investment_id] + @investment = Budget::Investment.find(params[:budget_investment_id]) end def load_budget_investment_milestone - @milestone = Budget::Investment::Milestone.find params[:id] + @milestone = Budget::Investment::Milestone.find(params[:id]) end - end diff --git a/app/controllers/admin/budget_investments_controller.rb b/app/controllers/admin/budget_investments_controller.rb index dd10930fb..0208fccd8 100644 --- a/app/controllers/admin/budget_investments_controller.rb +++ b/app/controllers/admin/budget_investments_controller.rb @@ -1,4 +1,5 @@ class Admin::BudgetInvestmentsController < Admin::BaseController + include FeatureFlags feature_flag :budgets @@ -12,6 +13,13 @@ class Admin::BudgetInvestmentsController < Admin::BaseController before_action :load_investments, only: [:index, :toggle_selection] def index + respond_to do |format| + format.html + format.csv do + send_data Budget::Investment.to_csv(@investments, {headers: true}), + filename: 'budget_investments.csv' + end + end end def show @@ -46,12 +54,12 @@ class Admin::BudgetInvestmentsController < Admin::BaseController def load_investments @investments = Budget::Investment.scoped_filter(params, @current_filter) .order(cached_votes_up: :desc, created_at: :desc) - .page(params[:page]) + @investments = @investments.page(params[:page]) unless request.format.csv? end 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/budgets_controller.rb b/app/controllers/admin/budgets_controller.rb index 7caf4f35c..d2f8706c0 100644 --- a/app/controllers/admin/budgets_controller.rb +++ b/app/controllers/admin/budgets_controller.rb @@ -27,7 +27,7 @@ class Admin::BudgetsController < Admin::BaseController def update if @budget.update(budget_params) - redirect_to admin_budget_path(@budget), notice: t('admin.budgets.update.notice') + redirect_to admin_budgets_path, notice: t('admin.budgets.update.notice') else render :edit end @@ -42,6 +42,15 @@ class Admin::BudgetsController < Admin::BaseController end end + def destroy + if @budget.investments.any? + redirect_to admin_budgets_path, alert: t('admin.budgets.destroy.unable_notice') + else + @budget.destroy + redirect_to admin_budgets_path, notice: t('admin.budgets.destroy.success_notice') + end + end + private def budget_params 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..32e38eb84 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 @@ -17,6 +17,7 @@ class Admin::Poll::PollsController < Admin::Poll::BaseController end def create + @poll = Poll.new(poll_params.merge(author: current_user)) if @poll.save redirect_to [:admin, @poll], notice: t("flash.actions.create.poll") else @@ -47,23 +48,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 +59,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/proposals_controller.rb b/app/controllers/admin/proposals_controller.rb index 2a6dfd718..02cf44aaf 100644 --- a/app/controllers/admin/proposals_controller.rb +++ b/app/controllers/admin/proposals_controller.rb @@ -1,6 +1,10 @@ class Admin::ProposalsController < Admin::BaseController + include FeatureFlags + has_filters %w{without_confirmed_hide all with_confirmed_hide}, only: :index + feature_flag :proposals + before_action :load_proposal, only: [:confirm_hide, :restore] def index @@ -25,4 +29,4 @@ class Admin::ProposalsController < Admin::BaseController @proposal = Proposal.with_hidden.find(params[:id]) end -end \ No newline at end of file +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/stats_controller.rb b/app/controllers/admin/stats_controller.rb index 5a50d03b4..91e266b77 100644 --- a/app/controllers/admin/stats_controller.rb +++ b/app/controllers/admin/stats_controller.rb @@ -36,4 +36,9 @@ class Admin::StatsController < Admin::BaseController @users_who_have_sent_message = DirectMessage.select(:sender_id).distinct.count end -end \ No newline at end of file + def polls + @polls = ::Poll.current + @participants = ::Poll::Voter.where(poll: @polls) + end + +end 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..8522352e3 100644 --- a/app/controllers/budgets/investments_controller.rb +++ b/app/controllers/budgets/investments_controller.rb @@ -28,8 +28,8 @@ module Budgets respond_to :html, :js def index - @investments = @investments.apply_filters_and_search(@budget, params, @current_filter) - .send("sort_by_#{@current_order}").page(params[:page]).per(10).for_render + @investments = investments.page(params[:page]).per(10).for_render + @investment_ids = @investments.pluck(:id) load_investment_votes(@investments) @tag_cloud = tag_cloud @@ -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 @@ -96,18 +94,20 @@ module Budgets def set_random_seed if params[:order] == 'random' || params[:order].blank? - params[:random_seed] ||= rand(99) / 100.0 - seed = Float(params[:random_seed]) rescue 0 - Budget::Investment.connection.execute("select setseed(#{seed})") + seed = rand(-100..100) / 100.0 + params[:random_seed] ||= Float(seed) rescue 0 else params[:random_seed] = nil end 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, :heading_id, :tag_list, + :organization_name, :location, :terms_of_service, :skip_map, + 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 @@ -130,6 +130,16 @@ module Budgets TagCloud.new(Budget::Investment, params[:search]) end + def investments + if @current_order == 'random' + @investments.apply_filters_and_search(@budget, params, @current_filter) + .send("sort_by_#{@current_order}", params[:random_seed]) + else + @investments.apply_filters_and_search(@budget, params, @current_filter) + .send("sort_by_#{@current_order}") + end + end + end end diff --git a/app/controllers/budgets_controller.rb b/app/controllers/budgets_controller.rb index 2a53c410b..4f215c46f 100644 --- a/app/controllers/budgets_controller.rb +++ b/app/controllers/budgets_controller.rb @@ -1,5 +1,6 @@ class BudgetsController < ApplicationController include FeatureFlags + include BudgetsHelper feature_flag :budgets load_and_authorize_resource @@ -9,6 +10,7 @@ class BudgetsController < ApplicationController respond_to :html, :js def show + raise ActionController::RoutingError, 'Not Found' unless budget_published?(@budget) end def index 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..4fec96c63 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 @@ -23,6 +23,7 @@ class DebatesController < ApplicationController def show super + @related_contents = Kaminari.paginate_array(@debate.relationed_contents).page(params[:page]).per(5) redirect_to debate_path(@debate), status: :moved_permanently if request.path != debate_path(@debate) end 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/installation_controller.rb b/app/controllers/installation_controller.rb new file mode 100644 index 000000000..507f3322f --- /dev/null +++ b/app/controllers/installation_controller.rb @@ -0,0 +1,24 @@ +class InstallationController < ApplicationController + + skip_authorization_check + + def details + respond_to do |format| + format.any { render json: consul_installation_details.to_json, content_type: 'application/json' } + end + end + + private + + def consul_installation_details + { + release: 'v0.12' + }.merge(features: settings_feature_flags) + end + + def settings_feature_flags + Setting.where("key LIKE 'feature.%'").each_with_object({}) { |x, n| n[x.key.remove('feature.')] = x.value } + 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/management/proposals_controller.rb b/app/controllers/management/proposals_controller.rb index 54616c05b..2101996af 100644 --- a/app/controllers/management/proposals_controller.rb +++ b/app/controllers/management/proposals_controller.rb @@ -14,6 +14,8 @@ class Management::ProposalsController < Management::BaseController def show super @notifications = @proposal.notifications + @related_contents = Kaminari.paginate_array(@proposal.relationed_contents).page(params[:page]).per(5) + redirect_to management_proposal_path(@proposal), status: :moved_permanently if request.path != management_proposal_path(@proposal) end diff --git a/app/controllers/moderation/proposals_controller.rb b/app/controllers/moderation/proposals_controller.rb index 4883bd378..f15aca85d 100644 --- a/app/controllers/moderation/proposals_controller.rb +++ b/app/controllers/moderation/proposals_controller.rb @@ -1,9 +1,12 @@ class Moderation::ProposalsController < Moderation::BaseController include ModerateActions + include FeatureFlags has_filters %w{pending_flag_review all with_ignored_flag}, only: :index has_orders %w{flags created_at}, only: :index + feature_flag :proposals + before_action :load_resources, only: [:index, :moderate] load_and_authorize_resource 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..400887046 100644 --- a/app/controllers/proposals_controller.rb +++ b/app/controllers/proposals_controller.rb @@ -1,4 +1,5 @@ class ProposalsController < ApplicationController + include FeatureFlags include CommentableActions include FlagActions @@ -7,9 +8,11 @@ class ProposalsController < ApplicationController before_action :load_geozones, only: [:edit, :map, :summary] before_action :authenticate_user!, except: [:index, :show, :map, :summary] + feature_flag :proposals + 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 +22,13 @@ class ProposalsController < ApplicationController def show super @notifications = @proposal.notifications - @document = Document.new(documentable: @proposal) + @related_contents = Kaminari.paginate_array(@proposal.relationed_contents).page(params[:page]).per(5) + 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') @@ -77,8 +80,10 @@ 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]) + :responsible_name, :tag_list, :terms_of_service, :geozone_id, :skip_map, + 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 +118,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/related_contents_controller.rb b/app/controllers/related_contents_controller.rb new file mode 100644 index 000000000..aac64dd99 --- /dev/null +++ b/app/controllers/related_contents_controller.rb @@ -0,0 +1,55 @@ +class RelatedContentsController < ApplicationController + skip_authorization_check + + respond_to :html, :js + + def create + if relationable_object && related_object + RelatedContent.create(parent_relationable: @relationable, child_relationable: @related, author: current_user) + + flash[:success] = t('related_content.success') + else + flash[:error] = t('related_content.error', url: Setting['url']) + end + + redirect_to @relationable + end + + def score_positive + score(:positive) + end + + def score_negative + score(:negative) + end + + private + + def score(action) + @related = RelatedContent.find_by(id: params[:id]) + @related.send("score_#{action}", current_user) + + render template: 'relationable/_refresh_score_actions' + end + + def valid_url? + params[:url].start_with?(Setting['url']) + end + + def relationable_object + @relationable = params[:relationable_klass].singularize.camelize.constantize.find_by(id: params[:relationable_id]) + end + + def related_object + if valid_url? + url = params[:url] + + related_klass = url.match(/\/(#{RelatedContent::RELATIONABLE_MODELS.join("|")})\//)[0].delete("/") + related_id = url.match(/\/[0-9]+/)[0].delete("/") + + @related = related_klass.singularize.camelize.constantize.find_by(id: related_id) + end + rescue + nil + end +end 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/budgets_helper.rb b/app/helpers/budgets_helper.rb index fefda85ab..a14b86538 100644 --- a/app/helpers/budgets_helper.rb +++ b/app/helpers/budgets_helper.rb @@ -1,5 +1,31 @@ module BudgetsHelper + def csv_params + csv_params = params.clone.merge(format: :csv).symbolize_keys + csv_params.delete(:page) + csv_params + end + + def investment_selected_link(investment) + options = investment_selected_link_options(investment) + path = toggle_selection_admin_budget_budget_investment_path(@budget, + investment, filter: params[:filter], page: params[:page]) + link_options = {method: :patch, remote: true, class: options[:link_class]} + link_to options[:text], path, link_options + end + + def investment_selected_link_options(investment) + if investment.selected? + {link_class: "button small expanded", + text: t("admin.budget_investments.index.selected") } + elsif investment.feasible? && investment.valuation_finished? + {link_class: "button small hollow expanded", + text: t("admin.budget_investments.index.select")} + else + {} + end + end + def budget_phases_select_options Budget::PHASES.map { |ph| [ t("budgets.phase.#{ph}"), ph ] } end @@ -42,4 +68,9 @@ module BudgetsHelper def investment_tags_select_options Budget::Investment.tags_on(:valuation).order(:name).select(:name).distinct end + + def budget_published?(budget) + !budget.drafting? || current_user&.administrator? + end + end diff --git a/app/helpers/communities_helper.rb b/app/helpers/communities_helper.rb index 22bc76617..744749a68 100644 --- a/app/helpers/communities_helper.rb +++ b/app/helpers/communities_helper.rb @@ -32,4 +32,7 @@ module CommunitiesHelper community.from_proposal? ? t("community.sidebar.description.proposal") : t("community.sidebar.description.investment") end + def create_topic_link(community) + current_user.present? ? new_community_topic_path(community.id) : new_user_session_path + 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/followables_helper.rb b/app/helpers/followables_helper.rb index e63e2b1a5..f185ab25a 100644 --- a/app/helpers/followables_helper.rb +++ b/app/helpers/followables_helper.rb @@ -13,7 +13,7 @@ module FollowablesHelper def render_follow(follow) followable = follow.followable - partial = followable_class_name(followable) + partial = followable_class_name(followable) + "_follow" locals = {followable_class_name(followable).to_sym => followable} render partial, locals 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/mailer_helper.rb b/app/helpers/mailer_helper.rb index e541eebb6..c3bda48b9 100644 --- a/app/helpers/mailer_helper.rb +++ b/app/helpers/mailer_helper.rb @@ -1,9 +1,11 @@ module MailerHelper def commentable_url(commentable) + return poll_url(commentable) if commentable.is_a?(Poll) return debate_url(commentable) if commentable.is_a?(Debate) return proposal_url(commentable) if commentable.is_a?(Proposal) + return community_topic_url(commentable.community_id, commentable) if commentable.is_a?(Topic) return budget_investment_url(commentable.budget_id, commentable) if commentable.is_a?(Budget::Investment) end -end \ No newline at end of file +end 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/topics_helper.rb b/app/helpers/topics_helper.rb deleted file mode 100644 index 1d5f1964f..000000000 --- a/app/helpers/topics_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -module TopicsHelper - - def disabled_create_topic - "disabled" unless current_user - end - - def disabled_info_title - t("community.show.sidebar.disabled_info_title") unless current_user - end - -end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 111a80267..980dd1e39 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) @@ -68,12 +64,4 @@ module UsersHelper end end - def empty_interests_message_text(user) - if current_user == user - t('account.show.public_interests_my_empty_list') - else - t('account.show.public_interests_user_empty_list') - end - end - end 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.rb b/app/models/budget.rb index 024602730..ce8bc4dfa 100644 --- a/app/models/budget.rb +++ b/app/models/budget.rb @@ -3,7 +3,8 @@ class Budget < ActiveRecord::Base include Measurable include Sluggable - PHASES = %w(accepting reviewing selecting valuating balloting reviewing_ballots finished).freeze + PHASES = %w(drafting accepting reviewing selecting valuating balloting + reviewing_ballots finished).freeze CURRENCY_SYMBOLS = %w(€ $ £ ¥).freeze validates :name, presence: true, uniqueness: true @@ -19,6 +20,7 @@ class Budget < ActiveRecord::Base before_validation :sanitize_descriptions scope :on_hold, -> { where(phase: %w(reviewing valuating reviewing_ballots")) } + scope :drafting, -> { where(phase: "drafting") } scope :accepting, -> { where(phase: "accepting") } scope :reviewing, -> { where(phase: "reviewing") } scope :selecting, -> { where(phase: "selecting") } @@ -41,6 +43,10 @@ class Budget < ActiveRecord::Base 80 end + def drafting? + phase == "drafting" + end + def accepting? phase == "accepting" end diff --git a/app/models/budget/heading.rb b/app/models/budget/heading.rb index 10feaa909..c9719a709 100644 --- a/app/models/budget/heading.rb +++ b/app/models/budget/heading.rb @@ -10,6 +10,7 @@ class Budget validates :name, presence: true, uniqueness: { if: :name_exists_in_budget_headings } validates :price, presence: true validates :slug, presence: true, format: /\A[a-z0-9\-_]+\z/ + validates :population, numericality: { greater_than: 0 }, allow_nil: true delegate :budget, :budget_id, to: :group, allow_nil: true diff --git a/app/models/budget/investment.rb b/app/models/budget/investment.rb index d658e7ab2..4e2e5d0b0 100644 --- a/app/models/budget/investment.rb +++ b/app/models/budget/investment.rb @@ -1,5 +1,6 @@ class Budget class Investment < ActiveRecord::Base + require 'csv' include Measurable include Sanitizable include Taggable @@ -7,15 +8,18 @@ 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 include ActsAsParanoidAliases + include Relationable + include Notifiable belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' belongs_to :heading @@ -42,7 +46,7 @@ class Budget scope :sort_by_confidence_score, -> { reorder(confidence_score: :desc, id: :desc) } scope :sort_by_ballots, -> { reorder(ballot_lines_count: :desc, id: :desc) } scope :sort_by_price, -> { reorder(price: :desc, confidence_score: :desc, id: :desc) } - scope :sort_by_random, -> { reorder("RANDOM()") } + scope :sort_by_random, ->(seed) { reorder("budget_investments.id % #{seed.to_f.nonzero? ? seed.to_f : 1}, budget_investments.id") } scope :valuation_open, -> { where(valuation_finished: false) } scope :without_admin, -> { valuation_open.where(administrator_id: nil) } @@ -259,6 +263,46 @@ class Budget investments end + def self.to_csv(investments, options = {}) + attrs = [I18n.t("admin.budget_investments.index.table_id"), + I18n.t("admin.budget_investments.index.table_title"), + I18n.t("admin.budget_investments.index.table_supports"), + I18n.t("admin.budget_investments.index.table_admin"), + I18n.t("admin.budget_investments.index.table_valuator"), + I18n.t("admin.budget_investments.index.table_geozone"), + I18n.t("admin.budget_investments.index.table_feasibility"), + I18n.t("admin.budget_investments.index.table_valuation_finished"), + I18n.t("admin.budget_investments.index.table_selection")] + csv_string = CSV.generate(options) do |csv| + csv << attrs + investments.each do |investment| + id = investment.id.to_s + title = investment.title + total_votes = investment.total_votes.to_s + admin = if investment.administrator.present? + investment.administrator.name + else + I18n.t("admin.budget_investments.index.no_admin_assigned") + end + vals = if investment.valuators.empty? + I18n.t("admin.budget_investments.index.no_valuators_assigned") + else + investment.valuators.collect(&:description_or_name).join(', ') + end + heading_name = investment.heading.name + price_string = "admin.budget_investments.index.feasibility"\ + ".#{investment.feasibility}" + price = I18n.t(price_string, price: investment.formatted_price) + valuation_finished = investment.valuation_finished? ? + I18n.t('shared.yes') : + I18n.t('shared.no') + csv << [id, title, total_votes, admin, vals, heading_name, price, + valuation_finished] + end + end + csv_string + end + private def set_denormalized_ids diff --git a/app/models/budget/investment/milestone.rb b/app/models/budget/investment/milestone.rb index 9ecbe4dc3..98ea506e9 100644 --- a/app/models/budget/investment/milestone.rb +++ b/app/models/budget/investment/milestone.rb @@ -1,14 +1,23 @@ class Budget class Investment class Milestone < ActiveRecord::Base + include Imageable + include Documentable + documentable max_documents_allowed: 3, + max_file_size: 3.megabytes, + accepted_content_types: [ "application/pdf" ] + belongs_to :investment validates :title, presence: true + validates :description, presence: true validates :investment, presence: true + validates :publication_date, presence: true def self.title_max_length 80 end + end end end diff --git a/app/models/comment.rb b/app/models/comment.rb index 8b68e11ad..fc0fb35f8 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -2,8 +2,9 @@ class Comment < ActiveRecord::Base include Flaggable include HasPublicAuthor include Graphqlable + include Notifiable - COMMENTABLE_TYPES = %w(Debate Proposal Budget::Investment Poll::Question Legislation::Question Legislation::Annotation Topic).freeze + COMMENTABLE_TYPES = %w(Debate Proposal Budget::Investment Poll Topic Legislation::Question Legislation::Annotation Legislation::Proposal).freeze acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases @@ -30,9 +31,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/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..727ff9a6b --- /dev/null +++ b/app/models/concerns/imageable.rb @@ -0,0 +1,12 @@ +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..eb0414fe2 --- /dev/null +++ b/app/models/concerns/mappable.rb @@ -0,0 +1,30 @@ +module Mappable + extend ActiveSupport::Concern + + included do + attr_accessor :skip_map + + has_one :map_location, dependent: :destroy + accepts_nested_attributes_for :map_location, allow_destroy: true + + validate :map_must_be_valid, on: :create, if: :feature_maps? + + def map_must_be_valid + return true if skip_map? + + unless map_location.try(:available?) + errors.add(:skip_map, I18n.t('activerecord.errors.models.map_location.attributes.map.invalid')) + end + end + + def feature_maps? + Setting["feature.map"].present? + end + + def skip_map? + skip_map == "1" + end + + end + +end diff --git a/app/models/concerns/notifiable.rb b/app/models/concerns/notifiable.rb new file mode 100644 index 000000000..b10c57f70 --- /dev/null +++ b/app/models/concerns/notifiable.rb @@ -0,0 +1,35 @@ +module Notifiable + extend ActiveSupport::Concern + + def notifiable_title + case self.class.name + when "ProposalNotification" + proposal.title + when "Comment" + commentable.title + else + title + end + end + + def notifiable_available? + case self.class.name + when "ProposalNotification" + check_availability(proposal) + when "Comment" + check_availability(commentable) + else + check_availability(self) + end + end + + def check_availability(resource) + resource.present? && + resource.try(:hidden_at).nil? && + resource.try(:retired_at).nil? + end + + def linkable_resource + is_a?(ProposalNotification) ? proposal : self + end +end diff --git a/app/models/concerns/relationable.rb b/app/models/concerns/relationable.rb new file mode 100644 index 000000000..4ce6bd3d3 --- /dev/null +++ b/app/models/concerns/relationable.rb @@ -0,0 +1,15 @@ +module Relationable + extend ActiveSupport::Concern + + included do + has_many :related_contents, as: :parent_relationable, dependent: :destroy + end + + def find_related_content(relationable) + RelatedContent.where(parent_relationable: self, child_relationable: relationable).first + end + + def relationed_contents + related_contents.not_hidden.map { |related_content| related_content.child_relationable } + 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..188ab2f93 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -9,6 +9,8 @@ class Debate < ActiveRecord::Base include Filterable include HasPublicAuthor include Graphqlable + include Relationable + include Notifiable acts_as_votable acts_as_paranoid column: :hidden_at @@ -37,14 +39,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 +97,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 +144,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..c4d1456e7 --- /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 diff --git a/app/models/document.rb b/app/models/document.rb index 3556d4c0a..108f29fad 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" - attr_accessor :cached_attachment + has_attached_file :attachment, url: "/system/:class/:prefix/:style/:hash.:extension", + hash_data: ":class/:style/:custom_hash_data", + use_timestamp: false, + hash_secret: Rails.application.secrets.secret_key_base + attr_accessor :cached_attachment, :remove, :original_filename 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 @@ -40,6 +44,10 @@ class Document < ActiveRecord::Base attachment.instance.prefix(attachment, style) end + Paperclip.interpolates :custom_hash_data do |attachment, _style| + attachment.instance.custom_hash_data(attachment) + end + def prefix(attachment, _style) if !attachment.instance.persisted? "cached_attachments/user/#{attachment.instance.user_id}" @@ -48,23 +56,42 @@ class Document < ActiveRecord::Base end end + def custom_hash_data(attachment) + original_filename = if !attachment.instance.persisted? && attachment.instance.remove + attachment.instance.original_filename + elsif !attachment.instance.persisted? + attachment.instance.attachment_file_name + else + attachment.instance.title + end + "#{attachment.instance.user_id}/#{original_filename}" + 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 +101,14 @@ 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, + remove: true, + original_filename: title) + 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..3fe6a9d96 --- /dev/null +++ b/app/models/legislation/proposal.rb @@ -0,0 +1,146 @@ +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 + include Notifiable + + 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 :process, 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/legislation/question.rb b/app/models/legislation/question.rb index 71ce100ea..2ca5fb39a 100644 --- a/app/models/legislation/question.rb +++ b/app/models/legislation/question.rb @@ -1,6 +1,7 @@ class Legislation::Question < ActiveRecord::Base acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases + include Notifiable belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' belongs_to :process, class_name: 'Legislation::Process', foreign_key: 'legislation_process_id' diff --git a/app/models/map_location.rb b/app/models/map_location.rb new file mode 100644 index 000000000..42fb8ab2f --- /dev/null +++ b/app/models/map_location.rb @@ -0,0 +1,10 @@ +class MapLocation < ActiveRecord::Base + + belongs_to :proposal, touch: true + belongs_to :investment, class_name: Budget::Investment, touch: true + + def available? + latitude.present? && longitude.present? && zoom.present? + end + +end diff --git a/app/models/notification.rb b/app/models/notification.rb index 84fc72f1c..de0e7c645 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -1,4 +1,5 @@ class Notification < ActiveRecord::Base + belongs_to :user, counter_cache: true belongs_to :notifiable, polymorphic: true @@ -7,6 +8,9 @@ class Notification < ActiveRecord::Base scope :not_emailed, -> { where(emailed_at: nil) } scope :for_render, -> { includes(:notifiable) } + delegate :notifiable_title, :notifiable_available?, :check_availability, :linkable_resource, + to: :notifiable, allow_nil: true + def timestamp notifiable.created_at end @@ -25,17 +29,6 @@ class Notification < ActiveRecord::Base end end - def notifiable_title - case notifiable.class.name - when "ProposalNotification" - notifiable.proposal.title - when "Comment" - notifiable.commentable.title - else - notifiable.title - end - end - def notifiable_action case notifiable_type when "ProposalNotification" @@ -47,8 +40,4 @@ class Notification < ActiveRecord::Base end end - def linkable_resource - notifiable.is_a?(ProposalNotification) ? notifiable.proposal : notifiable - end - -end +end \ No newline at end of file diff --git a/app/models/poll.rb b/app/models/poll.rb index 3873ac5e0..bf5a5a0a8 100644 --- a/app/models/poll.rb +++ b/app/models/poll.rb @@ -1,38 +1,51 @@ class Poll < ActiveRecord::Base + include Imageable + acts_as_paranoid column: :hidden_at + include ActsAsParanoidAliases + include Notifiable + + 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 +53,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 +78,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..81759ee0f 100644 --- a/app/models/poll/booth_assignment.rb +++ b/app/models/poll/booth_assignment.rb @@ -3,12 +3,26 @@ class Poll belongs_to :booth belongs_to :poll + before_destroy :destroy_poll_shifts, only: :destroy + has_many :officer_assignments, class_name: "Poll::OfficerAssignment", dependent: :destroy 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 + + def shifts? + shifts.empty? ? false : true + end + + private + + def shifts + Poll::Shift.where(booth_id: booth_id, officer_id: officer_assignments.pluck(:officer_id), date: officer_assignments.pluck(:date)) + end + + def destroy_poll_shifts + shifts.destroy_all + end 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..6623e507e 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -10,12 +10,15 @@ class Proposal < ActiveRecord::Base include Graphqlable include Followable include Communitable + include Imageable + include Mappable + include Notifiable 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 + include Relationable acts_as_votable acts_as_paranoid column: :hidden_at @@ -25,8 +28,8 @@ class Proposal < ActiveRecord::Base belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' belongs_to :geozone - has_many :comments, as: :commentable - has_many :proposal_notifications + has_many :comments, as: :commentable, dependent: :destroy + has_many :proposal_notifications, dependent: :destroy validates :title, presence: true validates :question, presence: true @@ -38,7 +41,7 @@ class Proposal < ActiveRecord::Base validates :description, length: { maximum: Proposal.description_max_length } validates :question, length: { in: 10..Proposal.question_max_length } validates :responsible_name, length: { in: 6..Proposal.responsible_name_max_length } - validates :retired_reason, inclusion: {in: RETIRE_OPTIONS, allow_nil: true} + validates :retired_reason, inclusion: { in: RETIRE_OPTIONS, allow_nil: true } validates :terms_of_service, acceptance: { allow_nil: false }, on: :create @@ -48,8 +51,8 @@ class Proposal < ActiveRecord::Base before_save :calculate_hot_score, :calculate_confidence_score - scope :for_render, -> { includes(:tags) } - scope :sort_by_hot_score, -> { reorder(hot_score: :desc) } + 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) } @@ -57,13 +60,29 @@ 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 } + scope :not_supported_by_user, ->(user) { where.not(id: user.find_voted_items(votable_type: "Proposal").compact.map(&:id)) } + + def self.recommendations(user) + tagged_with(user.interests, any: true) + .where("author_id != ?", user.id) + .unsuccessful + .not_followed_by_user(user) + .not_archived + .not_supported_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 @@ -88,7 +107,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 +203,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/proposal_notification.rb b/app/models/proposal_notification.rb index bd4211a85..590f19208 100644 --- a/app/models/proposal_notification.rb +++ b/app/models/proposal_notification.rb @@ -1,6 +1,6 @@ class ProposalNotification < ActiveRecord::Base - include Graphqlable + include Notifiable belongs_to :author, class_name: 'User', foreign_key: 'author_id' belongs_to :proposal @@ -21,4 +21,8 @@ class ProposalNotification < ActiveRecord::Base end end + def notifiable + proposal + end + end diff --git a/app/models/related_content.rb b/app/models/related_content.rb new file mode 100644 index 000000000..1fd1b6b0c --- /dev/null +++ b/app/models/related_content.rb @@ -0,0 +1,63 @@ +class RelatedContent < ActiveRecord::Base + RELATED_CONTENT_SCORE_THRESHOLD = Setting['related_content_score_threshold'].to_f + RELATIONABLE_MODELS = %w{proposals debates}.freeze + + acts_as_paranoid column: :hidden_at + include ActsAsParanoidAliases + + belongs_to :author, class_name: 'User', foreign_key: 'author_id' + belongs_to :parent_relationable, polymorphic: true, touch: true + belongs_to :child_relationable, polymorphic: true, touch: true + has_one :opposite_related_content, class_name: 'RelatedContent', foreign_key: :related_content_id + has_many :related_content_scores + + validates :parent_relationable_id, presence: true + validates :parent_relationable_type, presence: true + validates :child_relationable_id, presence: true + validates :child_relationable_type, presence: true + validates :parent_relationable_id, uniqueness: { scope: [:parent_relationable_type, :child_relationable_id, :child_relationable_type] } + + after_create :create_opposite_related_content, unless: proc { opposite_related_content.present? } + after_create :create_author_score + + scope :not_hidden, -> { where(hidden_at: nil) } + + def score_positive(user) + score(RelatedContentScore::SCORES[:POSITIVE], user) + end + + def score_negative(user) + score(RelatedContentScore::SCORES[:NEGATIVE], user) + end + + def scored_by_user?(user) + related_content_scores.exists?(user: user) + end + + private + + def create_opposite_related_content + related_content = RelatedContent.create!(opposite_related_content: self, parent_relationable: child_relationable, + child_relationable: parent_relationable, author: author) + self.opposite_related_content = related_content + end + + def score(value, user) + score_with_opposite(value, user) + hide_with_opposite if (related_content_scores.sum(:value) / related_content_scores_count) < RELATED_CONTENT_SCORE_THRESHOLD + end + + def hide_with_opposite + hide + opposite_related_content.hide + end + + def create_author_score + score_positive(author) + end + + def score_with_opposite(value, user) + RelatedContentScore.create(user: user, related_content: self, value: value) + RelatedContentScore.create(user: user, related_content: opposite_related_content, value: value) + end +end diff --git a/app/models/related_content_score.rb b/app/models/related_content_score.rb new file mode 100644 index 000000000..da37fc07d --- /dev/null +++ b/app/models/related_content_score.rb @@ -0,0 +1,13 @@ +class RelatedContentScore < ActiveRecord::Base + SCORES = { + POSITIVE: 1, + NEGATIVE: -1 + }.freeze + + belongs_to :related_content, touch: true, counter_cache: :related_content_scores_count + belongs_to :user + + validates :user, presence: true + validates :related_content, presence: true + validates :related_content_id, uniqueness: { scope: [:user_id] } +end diff --git a/app/models/topic.rb b/app/models/topic.rb index e0a73d58b..11c921dd4 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -1,6 +1,7 @@ class Topic < ActiveRecord::Base acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases + include Notifiable belongs_to :community belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' 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/models/vote.rb b/app/models/vote.rb index 14b11a68d..626d71879 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -1,14 +1,2 @@ class Vote < ActsAsVotable::Vote - - include Graphqlable - - scope :public_for_api, -> do - where(%{(votes.votable_type = 'Debate' and votes.votable_id in (?)) or - (votes.votable_type = 'Proposal' and votes.votable_id in (?)) or - (votes.votable_type = 'Comment' and votes.votable_id in (?))}, - Debate.public_for_api.pluck(:id), - Proposal.public_for_api.pluck(:id), - Comment.public_for_api.pluck(:id)) - end - end diff --git a/app/views/account/show.html.erb b/app/views/account/show.html.erb index 8bc09f050..81b25e5e0 100644 --- a/app/views/account/show.html.erb +++ b/app/views/account/show.html.erb @@ -14,7 +14,7 @@ <%= render "shared/errors", resource: @account %>
-
+

<%= t("account.show.personal")%>

@@ -112,10 +112,10 @@
<% end %> - <%= f.submit t("account.show.save_changes_submit"), class: "button" %> + <%= f.submit t("account.show.save_changes_submit"), class: "button margin-top" %>
-
+

<%= t("account.show.user_permission_title") %>

<%= t("account.show.user_permission_info") %>

@@ -143,7 +143,8 @@

<%= t("account.show.user_permission_verify_info") %> -
+

+

<%= t("account.show.user_permission_verify") %>

diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb index 11d58adc8..41500e376 100644 --- a/app/views/admin/_menu.html.erb +++ b/app/views/admin/_menu.html.erb @@ -1,5 +1,5 @@
-