diff --git a/.codeclimate.yml b/.codeclimate.yml index aacf2e734..e25ac86aa 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -3,4 +3,18 @@ plugins: rubocop: enabled: true channel: rubocop-1-56-3 - +exclude_patterns: +- "bin/" +- "config/" +- "db/" +- "dist/" +- "features/" +- "**/node_modules/" +- "script/" +- "**/spec/" +- "**/test/" +- "**/tests/" +- "Tests/" +- "**/vendor/" +- "**/*_test.go" +- "**/*.d.ts" diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml index 4543c0f7d..93b401d76 100644 --- a/.haml-lint_todo.yml +++ b/.haml-lint_todo.yml @@ -46,11 +46,13 @@ linters: - "app/views/admin/events/index.html.haml" - "app/views/admin/tracks/index.html.haml" - "app/views/admin/tracks/show.html.haml" + - "app/views/admin/registrations/index.html.haml" - "app/views/admin/versions/_object_desc_and_link.html.haml" - "app/views/conference_registrations/show.html.haml" - "app/views/layouts/_admin_sidebar.html.haml" - "app/views/proposals/index.html.haml" - "app/views/proposals/show.html.haml" + - "app/views/proposals/_form.html.haml" # Offense count: 28 IdNames: diff --git a/.rubocop.yml b/.rubocop.yml index b44f547da..53babd565 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -3,6 +3,7 @@ require: - rubocop-rails - rubocop-capybara - rubocop-performance + - rubocop-factory_bot inherit_from: .rubocop_todo.yml diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 48a9a2252..be9e93e84 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2024-02-13 04:01:02 UTC using RuboCop version 1.60.2. +# on 2024-03-12 05:45:42 UTC using RuboCop version 1.61.0. # 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 @@ -14,19 +14,66 @@ Bundler/OrderedGems: Exclude: - 'Gemfile' -# Offense count: 1 +# Offense count: 180 +# Configuration parameters: EnforcedStyle. +# SupportedStyles: link_or_button, strict +Capybara/ClickLinkOrButtonStyle: + Enabled: false + +# Offense count: 77 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: have_no, not_to Capybara/NegationMatcher: Exclude: - - 'spec/features/versions_spec.rb' + - 'spec/features/cfp_ability_spec.rb' + - 'spec/features/code_of_conduct_spec.rb' + - 'spec/features/info_desk_ability_spec.rb' + - 'spec/features/organizer_ability_spec.rb' + - 'spec/features/proposals_spec.rb' + - 'spec/features/splashpage_spec.rb' + - 'spec/features/sponsor_spec.rb' + - 'spec/features/track_organizer_ability_spec.rb' + +# Offense count: 10 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: DefaultSelector. +Capybara/RSpec/HaveSelector: + Exclude: + - 'spec/features/cfp_ability_spec.rb' + - 'spec/features/info_desk_ability_spec.rb' + - 'spec/features/organization_admin_ability_spec.rb' + - 'spec/features/organizer_ability_spec.rb' + - 'spec/features/proposals_spec.rb' + - 'spec/features/sponsor_spec.rb' + - 'spec/features/track_organizer_ability_spec.rb' + - 'spec/features/voting_spec.rb' -# Offense count: 98 +# Offense count: 101 # This cop supports safe autocorrection (--autocorrect). Capybara/SpecificFinders: Enabled: false +# Offense count: 8 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle, NonImplicitAssociationMethodNames. +# SupportedStyles: explicit, implicit +FactoryBot/AssociationStyle: + Exclude: + - 'spec/factories/comments.rb' + - 'spec/factories/commercials.rb' + - 'spec/factories/conferences.rb' + - 'spec/factories/surveys.rb' + - 'spec/factories/tracks.rb' + +# Offense count: 4 +# Configuration parameters: Include. +# Include: **/*_spec.rb, **/spec/**/*, **/test/**/*, **/features/support/factories/**/*.rb +FactoryBot/FactoryAssociationWithStrategy: + Exclude: + - 'spec/factories/booths.rb' + - 'spec/factories/users.rb' + # Offense count: 13 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowedMethods, AllowedPatterns. @@ -40,7 +87,7 @@ Lint/AmbiguousBlockAssociation: - 'spec/controllers/schedules_controller_spec.rb' - 'spec/models/user_spec.rb' -# Offense count: 5 +# Offense count: 3 # Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches. Lint/DuplicateBranch: Exclude: @@ -53,28 +100,28 @@ Lint/IneffectiveAccessModifier: - 'app/models/commercial.rb' - 'app/models/conference.rb' -# Offense count: 111 +# Offense count: 110 # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: Max: 72 -# Offense count: 19 +# Offense count: 14 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode. # AllowedMethods: refine Metrics/BlockLength: - Max: 227 + Max: 99 # Offense count: 13 # Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: Max: 270 -# Offense count: 26 +# Offense count: 28 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/CyclomaticComplexity: Max: 15 -# Offense count: 127 +# Offense count: 126 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: Max: 55 @@ -82,9 +129,9 @@ Metrics/MethodLength: # Offense count: 4 # Configuration parameters: CountComments, CountAsOne. Metrics/ModuleLength: - Max: 168 + Max: 158 -# Offense count: 25 +# Offense count: 27 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/PerceivedComplexity: Max: 17 @@ -191,7 +238,7 @@ RSpec/Capybara/FeatureMethods: - 'spec/features/user_spec.rb' - 'spec/features/voting_spec.rb' -# Offense count: 338 +# Offense count: 339 # Configuration parameters: Prefixes, AllowedPatterns. # Prefixes: when, with, without RSpec/ContextWording: @@ -204,7 +251,7 @@ RSpec/ContextWording: RSpec/DescribedClass: Enabled: false -# Offense count: 248 +# Offense count: 251 # Configuration parameters: CountAsOne. RSpec/ExampleLength: Max: 222 @@ -259,7 +306,7 @@ RSpec/IdenticalEqualityAssertion: - 'spec/controllers/admin/conferences_controller_spec.rb' # Offense count: 53 -# Configuration parameters: Max. +# Configuration parameters: Max, AllowedIdentifiers, AllowedPatterns. RSpec/IndexedLet: Exclude: - 'spec/controllers/admin/reports_controller_spec.rb' @@ -319,11 +366,11 @@ RSpec/MultipleDescribes: Exclude: - 'spec/models/conference_spec.rb' -# Offense count: 298 +# Offense count: 300 RSpec/MultipleExpectations: Max: 97 -# Offense count: 272 +# Offense count: 274 # Configuration parameters: AllowSubject. RSpec/MultipleMemoizedHelpers: Max: 32 @@ -396,6 +443,12 @@ RSpec/Rails/InferredSpecType: - 'spec/helpers/users_helper_spec.rb' - 'spec/routing/routing_spec.rb' +# Offense count: 12 +# This cop supports safe autocorrection (--autocorrect). +RSpec/ReceiveMessages: + Exclude: + - 'spec/models/track_spec.rb' + # Offense count: 4 RSpec/RepeatedDescription: Exclude: @@ -466,12 +519,12 @@ Style/CaseLikeIf: Exclude: - 'app/views/admin/events/events.xlsx.axlsx' -# Offense count: 103 +# Offense count: 107 # Configuration parameters: AllowedConstants. Style/Documentation: Enabled: false -# Offense count: 40 +# Offense count: 43 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: always, always_true, never @@ -510,7 +563,7 @@ Style/IdenticalConditionalBranches: - 'app/controllers/admin/booths_controller.rb' - 'app/controllers/admin/events_controller.rb' -# Offense count: 32 +# Offense count: 34 # This cop supports safe autocorrection (--autocorrect). Style/IfUnlessModifier: Enabled: false @@ -587,7 +640,7 @@ Style/OptionalBooleanParameter: - 'app/models/event.rb' # Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). +# This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowedCompactTypes. # SupportedStyles: compact, exploded Style/RaiseArgs: @@ -640,7 +693,7 @@ Style/WordArray: EnforcedStyle: percent MinSize: 5 -# Offense count: 260 +# Offense count: 269 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. # URISchemes: http, https diff --git a/Gemfile b/Gemfile index 508035858..c21525e04 100644 --- a/Gemfile +++ b/Gemfile @@ -8,9 +8,6 @@ source 'https://rubygems.org' ruby ENV.fetch('OSEM_RUBY_VERSION', '3.2.2') -# rails-assets requires >= 1.8.4 -abort 'Bundler version >= 1.8.4 is required' if Gem::Version.new(Bundler::VERSION) < Gem::Version.new('1.8.4') - # as web framework if next? gem 'rails', '~> 7' @@ -89,40 +86,11 @@ gem 'cocoon' # as the JavaScript library # TODO: Consolidate with the rails-assets below or move to webpack... gem 'jquery-rails' -gem 'jquery-ui-rails', '~> 6.0.1' +gem 'jquery-ui-rails', '~> 7.0.0' # for languages validation gem 'iso-639' -# frontend javascripts -source 'https://rails-assets.org' do - # transient dependencies, included here to reduce warnings. - gem 'rails-assets-bootstrap' - gem 'rails-assets-jquery' - # for placeholder images - gem 'rails-assets-holderjs' - # for formating dates - gem 'rails-assets-date.format' - # for or parsing, validating, manipulating, and formatting dates - gem 'rails-assets-momentjs' - # for smooth scrolling - gem 'rails-assets-jquery-smooth-scroll' - # as color picker - gem 'rails-assets-spectrum' - # for color manipulation - gem 'rails-assets-tinycolor' - # for drawing triangle backgrounds - gem 'rails-assets-trianglify' - # for scroll way points - gem 'rails-assets-waypoints' - # for markdown editors - gem 'rails-assets-bootstrap-markdown' - # for select with icon - gem 'rails-assets-bootstrap-select' - gem 'rails-assets-markdown' - gem 'rails-assets-to-markdown', '~> 3' -end - # as date picker gem 'bootstrap3-datetimepicker-rails', '~> 4.17.47' diff --git a/Gemfile.lock b/Gemfile.lock index 99e3dd692..0550a4a21 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,75 +7,51 @@ GIT omniauth (~> 2.0) rack -GEM - remote: https://rails-assets.org/ - specs: - rails-assets-bootstrap (3.4.1) - rails-assets-jquery (>= 1.9.1, < 4) - rails-assets-bootstrap-markdown (2.10.0) - rails-assets-bootstrap (~> 3) - rails-assets-bootstrap-select (1.13.10) - rails-assets-bootstrap (>= 3.0.0) - rails-assets-jquery (>= 1.9.1, < 4) - rails-assets-date.format (1.2.3) - rails-assets-holderjs (2.9.6) - rails-assets-jquery (3.6.0) - rails-assets-jquery-smooth-scroll (2.2.0) - rails-assets-jquery (>= 1.7.0) - rails-assets-markdown (0.5.0) - rails-assets-momentjs (2.29.4) - rails-assets-spectrum (1.8.0) - rails-assets-jquery (>= 1.7.2) - rails-assets-tinycolor (1.6.0) - rails-assets-to-markdown (3.1.1) - rails-assets-trianglify (1.2.0) - rails-assets-waypoints (4.0.1) - GEM remote: https://rubygems.org/ specs: Ascii85 (1.1.0) - actioncable (7.0.8) - actionpack (= 7.0.8) - activesupport (= 7.0.8) + actioncable (7.0.8.4) + actionpack (= 7.0.8.4) + activesupport (= 7.0.8.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8) - actionpack (= 7.0.8) - activejob (= 7.0.8) - activerecord (= 7.0.8) - activestorage (= 7.0.8) - activesupport (= 7.0.8) + actionmailbox (7.0.8.4) + actionpack (= 7.0.8.4) + activejob (= 7.0.8.4) + activerecord (= 7.0.8.4) + activestorage (= 7.0.8.4) + activesupport (= 7.0.8.4) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.8) - actionpack (= 7.0.8) - actionview (= 7.0.8) - activejob (= 7.0.8) - activesupport (= 7.0.8) + actionmailer (7.0.8.4) + actionpack (= 7.0.8.4) + actionview (= 7.0.8.4) + activejob (= 7.0.8.4) + activesupport (= 7.0.8.4) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.8) - actionview (= 7.0.8) - activesupport (= 7.0.8) + actionpack (7.0.8.4) + actionview (= 7.0.8.4) + activesupport (= 7.0.8.4) rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.8) - actionpack (= 7.0.8) - activerecord (= 7.0.8) - activestorage (= 7.0.8) - activesupport (= 7.0.8) + actiontext (7.0.8.4) + actionpack (= 7.0.8.4) + activerecord (= 7.0.8.4) + activestorage (= 7.0.8.4) + activesupport (= 7.0.8.4) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8) - activesupport (= 7.0.8) + actionview (7.0.8.4) + activesupport (= 7.0.8.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -85,22 +61,22 @@ GEM activemodel (>= 4.1, < 7.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (7.0.8) - activesupport (= 7.0.8) + activejob (7.0.8.4) + activesupport (= 7.0.8.4) globalid (>= 0.3.6) - activemodel (7.0.8) - activesupport (= 7.0.8) - activerecord (7.0.8) - activemodel (= 7.0.8) - activesupport (= 7.0.8) - activestorage (7.0.8) - actionpack (= 7.0.8) - activejob (= 7.0.8) - activerecord (= 7.0.8) - activesupport (= 7.0.8) + activemodel (7.0.8.4) + activesupport (= 7.0.8.4) + activerecord (7.0.8.4) + activemodel (= 7.0.8.4) + activesupport (= 7.0.8.4) + activestorage (7.0.8.4) + actionpack (= 7.0.8.4) + activejob (= 7.0.8.4) + activerecord (= 7.0.8.4) + activesupport (= 7.0.8.4) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.8) + activesupport (7.0.8.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -136,7 +112,7 @@ GEM bootstrap-switch-rails (3.3.3) bootstrap3-datetimepicker-rails (4.17.47) momentjs-rails (>= 2.8.1) - builder (3.2.4) + builder (3.3.0) bullet (7.0.2) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) @@ -151,7 +127,7 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - carrierwave (2.2.5) + carrierwave (2.2.6) activemodel (>= 5.0.0) activesupport (>= 5.0.0) addressable (~> 2.6) @@ -182,7 +158,7 @@ GEM rest-client (>= 2.0.0) cocoon (1.2.15) colorize (0.8.1) - concurrent-ruby (1.2.3) + concurrent-ruby (1.3.3) countable-rails (0.0.1) railties (>= 3.1) crack (0.4.5) @@ -259,7 +235,7 @@ GEM dotenv-rails (2.8.1) dotenv (= 2.8.1) railties (>= 3.2) - erubi (1.12.0) + erubi (1.13.0) execjs (2.8.1) factory_bot (6.2.1) activesupport (>= 5.0.0) @@ -314,7 +290,7 @@ GEM httparty (0.21.0) mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) - i18n (1.14.1) + i18n (1.14.5) concurrent-ruby (~> 1.0) i18n_data (0.17.1) simple_po_parser (~> 1.1) @@ -331,7 +307,7 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - jquery-ui-rails (6.0.1) + jquery-ui-rails (7.0.0) railties (>= 3.2.16) json (2.7.1) json-schema (4.0.0) @@ -363,7 +339,7 @@ GEM net-imap net-pop net-smtp - marcel (1.0.2) + marcel (1.0.4) matrix (0.4.2) method_source (1.0.0) mime-types (3.4.1) @@ -374,7 +350,7 @@ GEM rake mini_magick (4.12.0) mini_mime (1.1.5) - minitest (5.22.2) + minitest (5.24.1) momentjs-rails (2.29.4.1) railties (>= 3.1) monetize (1.12.0) @@ -402,11 +378,11 @@ GEM next_rails (1.2.4) colorize (>= 0.8.1) nio4r (2.7.0) - nokogiri (1.16.2-arm64-darwin) + nokogiri (1.16.6-arm64-darwin) racc (~> 1.4) - nokogiri (1.16.2-x86_64-darwin) + nokogiri (1.16.6-x86_64-darwin) racc (~> 1.4) - nokogiri (1.16.2-x86_64-linux) + nokogiri (1.16.6-x86_64-linux) racc (~> 1.4) oauth2 (2.0.9) faraday (>= 0.17.3, < 3.0) @@ -490,8 +466,8 @@ GEM public_suffix (5.0.4) puma (6.4.2) nio4r (~> 2.0) - racc (1.7.3) - rack (2.2.8) + racc (1.8.0) + rack (2.2.9) rack-openid (1.4.2) rack (>= 1.1.0) ruby-openid (>= 2.1.8) @@ -499,20 +475,20 @@ GEM rack rack-test (2.1.0) rack (>= 1.3) - rails (7.0.8) - actioncable (= 7.0.8) - actionmailbox (= 7.0.8) - actionmailer (= 7.0.8) - actionpack (= 7.0.8) - actiontext (= 7.0.8) - actionview (= 7.0.8) - activejob (= 7.0.8) - activemodel (= 7.0.8) - activerecord (= 7.0.8) - activestorage (= 7.0.8) - activesupport (= 7.0.8) + rails (7.0.8.4) + actioncable (= 7.0.8.4) + actionmailbox (= 7.0.8.4) + actionmailer (= 7.0.8.4) + actionpack (= 7.0.8.4) + actiontext (= 7.0.8.4) + actionview (= 7.0.8.4) + activejob (= 7.0.8.4) + activemodel (= 7.0.8.4) + activerecord (= 7.0.8.4) + activestorage (= 7.0.8.4) + activesupport (= 7.0.8.4) bundler (>= 1.15.0) - railties (= 7.0.8) + railties (= 7.0.8.4) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -527,9 +503,9 @@ GEM rails-i18n (7.0.7) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) - railties (7.0.8) - actionpack (= 7.0.8) - activesupport (= 7.0.8) + railties (7.0.8.4) + actionpack (= 7.0.8.4) + activesupport (= 7.0.8.4) method_source rake (>= 12.2) thor (~> 1.0) @@ -551,7 +527,8 @@ GEM http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - rexml (3.2.6) + rexml (3.3.2) + strscan rolify (6.0.1) rqrcode (2.2.0) chunky_png (~> 1.0) @@ -578,7 +555,7 @@ GEM rspec-mocks (~> 3.12) rspec-support (~> 3.12) rspec-support (3.12.0) - rubocop (1.60.2) + rubocop (1.61.0) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) @@ -589,9 +566,11 @@ GEM rubocop-ast (>= 1.30.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.30.0) - parser (>= 3.2.1.0) - rubocop-capybara (2.18.0) + rubocop-ast (1.31.1) + parser (>= 3.3.0.4) + rubocop-capybara (2.20.0) + rubocop (~> 1.41) + rubocop-factory_bot (2.25.1) rubocop (~> 1.41) rubocop-performance (1.17.1) rubocop (>= 1.7.0, < 2.0) @@ -600,14 +579,15 @@ GEM activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) - rubocop-rspec (2.20.0) + rubocop-rspec (2.23.0) rubocop (~> 1.33) rubocop-capybara (~> 2.17) + rubocop-factory_bot (~> 2.22) ruby-oembed (0.16.1) ruby-openid (2.9.2) ruby-progressbar (1.13.0) ruby-rc4 (0.1.5) - ruby-vips (2.2.0) + ruby-vips (2.2.1) ffi (~> 1.12) ruby2_keywords (0.0.5) rubyzip (2.3.2) @@ -667,13 +647,14 @@ GEM dante (>= 0.2.0) multi_json (~> 1.0) stripe (> 5, < 6) + strscan (3.1.0) sys-uname (1.2.2) ffi (~> 1.1) sysexits (1.2.0) temple (0.10.1) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) - thor (1.3.0) + thor (1.3.1) tilt (2.1.0) timecop (0.9.6) timeout (0.4.1) @@ -724,6 +705,7 @@ PLATFORMS arm64-darwin-20 arm64-darwin-23 x86_64-darwin-21 + x86_64-darwin-23 x86_64-linux DEPENDENCIES @@ -774,7 +756,7 @@ DEPENDENCIES iso-639 jquery-datatables jquery-rails - jquery-ui-rails (~> 6.0.1) + jquery-ui-rails (~> 7.0.0) json-schema leaflet-rails letter_opener @@ -803,20 +785,6 @@ DEPENDENCIES pronto-rubocop puma rails (~> 7.0) - rails-assets-bootstrap! - rails-assets-bootstrap-markdown! - rails-assets-bootstrap-select! - rails-assets-date.format! - rails-assets-holderjs! - rails-assets-jquery! - rails-assets-jquery-smooth-scroll! - rails-assets-markdown! - rails-assets-momentjs! - rails-assets-spectrum! - rails-assets-tinycolor! - rails-assets-to-markdown (~> 3)! - rails-assets-trianglify! - rails-assets-waypoints! rails-controller-testing rails-i18n recaptcha diff --git a/app/assets/javascripts/osem-schedule.js b/app/assets/javascripts/osem-schedule.js index c233c877d..b38ae2cc8 100644 --- a/app/assets/javascripts/osem-schedule.js +++ b/app/assets/javascripts/osem-schedule.js @@ -15,10 +15,10 @@ var Schedule = { schedule_id = schedule_id_param; }, remove: function(element) { - var e = $("#" + element); - var event_schedule_id = e.attr("event_schedule_id"); - if(event_schedule_id != null){ - var my_url = url + '/' + event_schedule_id; + var e = $("#" + element); + var event_schedule_id = parseInt(e.attr("event_schedule_id")); + if(event_schedule_id > 0) { + var my_url = `${url}/${event_schedule_id}`; var success_callback = function(data) { e.attr("event_schedule_id", null); e.appendTo($(".unscheduled-events")); @@ -41,18 +41,18 @@ var Schedule = { }, add: function (previous_parent, new_parent, event) { event.appendTo(new_parent); - var event_schedule_id = event.attr("event_schedule_id"); + // Event Schedule Id could be an empty string. + var event_schedule_id = parseInt(event.attr("event_schedule_id")); var my_url = url; var type = 'POST'; var params = { event_schedule: { room_id: new_parent.attr("room_id"), start_time: (new_parent.attr("date") + ' ' + new_parent.attr("hour")) }}; - if(event_schedule_id != null){ + if (event_schedule_id > 0) { type = 'PUT'; - my_url += ('/' + event_schedule_id); - } - else{ + my_url += `/${event_schedule_id}`; + } else { params['event_schedule']['event_id'] = event.attr("event_id"); params['event_schedule']['schedule_id'] = schedule_id; } @@ -82,30 +82,27 @@ $(document).ready( function() { $('#current-event-btn').on('click', function() { var now = new Date(); - var closestEventId = null; + var closestEvent = null; var smallestDiff = Infinity; - var i = 0; $('.event-item').each(function() { + let $event = $(this), eventTimeStr = $event.data('time'); - var eventTimeStr = $(this).data('time'); - - if (eventTimeStr) { - var eventTime = new Date(eventTimeStr); - var diff = Math.abs(eventTime - now); - - if (diff < smallestDiff) { - smallestDiff = diff; - closestEventId = $(this).attr('class').split(' ')[1]; - } - } + if (!eventTimeStr) { return; } + + var eventTime = new Date(eventTimeStr); + var diff = Math.abs(eventTime - now); + if (diff < smallestDiff) { + smallestDiff = diff; + closestEvent = $event; + } }); - if (closestEventId) { - //Instead of relying on hash it's probably better to scroll using javascript - //Since the users and click button->scroll->click again, which won't re-scroll + if (closestEvent) { + // Instead of relying on hash it's probably better to scroll using javascript + // Since the users and click button->scroll->click again, which won't re-scroll $('.highlighted').removeClass('highlighted'); - $('.' + closestEventId).addClass('highlighted').get(0).scrollIntoView({ behavior: 'smooth', block: 'start' }); + $(closestEvent).addClass('highlighted').get(0).scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); diff --git a/app/assets/javascripts/osem-switch.js b/app/assets/javascripts/osem-switch.js index 5e8c47921..b4517c1cb 100644 --- a/app/assets/javascripts/osem-switch.js +++ b/app/assets/javascripts/osem-switch.js @@ -1,7 +1,5 @@ function checkboxSwitch(selector){ - $(selector).bootstrapSwitch( - - ); + $(selector).bootstrapSwitch(); $(selector).on('switchChange.bootstrapSwitch', function(event, state) { var url = $(this).attr('url') + state; diff --git a/app/assets/javascripts/osem.js b/app/assets/javascripts/osem.js index 9749a2fa6..3185a984c 100644 --- a/app/assets/javascripts/osem.js +++ b/app/assets/javascripts/osem.js @@ -49,7 +49,7 @@ $(function () { $('.' + id).collapse('hide'); $(`#event_type_${$(this).val()}-help.${id}`).collapse('show'); - $(`#event_type_${$(this).val()}-instructions.${id}`).collapse('show');w + $(`#event_type_${$(this).val()}-instructions.${id}`).collapse('show'); }); $('.dropdown-toggle').dropdown(); @@ -132,6 +132,7 @@ function get_color() { } function word_count(text, divId, maxcount) { + if (!text) { return; } var area = document.getElementById(text.id) Countable.once(area, function(counter) { @@ -162,58 +163,6 @@ function replace_defaut_submission_text(input_selector, new_text, valid_defaults }); } -/* Wait for the DOM to be ready before attaching events to the elements */ -$( document ).ready(function() { - /* Set the minimum and maximum proposal abstract word length */ - function updateEventTypeRequirements() { - var $selected = $("#event_event_type_id option:selected") - var max = $selected.data("max-words"); - var min = $selected.data("min-words"); - - // We replace the default text only if the current field is empty, - // or is set to the default text of another event type. - replace_defaut_submission_text( - '#event_submission_text', - $selected.data("instructions"), - $("#event_event_type_id option").toArray().map(e => $(e).data('instructions')) - ); - - $("#abstract-maximum-word-count").text(max); - $("#submission-maximum-word-count").text(max); - $("#abstract-minimum-word-count").text(min); - $("#submission-minimum-word-count").text(min); - word_count($('#event_abstract').get(0), 'abstract-count', max); - } - $("#event_event_type_id").change(updateEventTypeRequirements); - updateEventTypeRequirements(); - - /* Count the proposal abstract length */ - $("#event_abstract").on('input', function() { - var $selected = $("#event_event_type_id option:selected") - var max = $selected.data("max-words"); - word_count(this, 'abstract-count', max); - } ); - - /* Count the submission text length */ - $("#event_submission_text").bind('change keyup paste input', function() { - var $selected = $("event_event_type_id option:selected") - var max = $selected.data("max-words"); - word_count(this, 'submission-count', max); - }); - - /* Listen for reset template button, wait for confirm, and reset. */ - $('.js-resetSubmissionText').click((e) => { - let $selected = $("#event_event_type_id option:selected"); - let $this = $(e.target); - let affirm = confirm($this.data('confirm')); - if (affirm) { - let sub_text = $('#event_submission_text'); - sub_text.val($selected.data('instructions')); - sub_text.trigger('change'); - } - }); -}); - /* Commodity function for modal windows */ window.build_dialog = function(selector, content) { diff --git a/app/controllers/admin/commercials_controller.rb b/app/controllers/admin/commercials_controller.rb index 093ea3914..0b7c8af3f 100644 --- a/app/controllers/admin/commercials_controller.rb +++ b/app/controllers/admin/commercials_controller.rb @@ -90,9 +90,7 @@ def aggregate_errors(errors) end def commercial_params - params.require(:commercial).permit(:title, :url).tap do |params| - params[:url] = Commercial.generate_snap_embed(params[:url]) if params[:url] - end + params.require(:commercial).permit(:title, :url) end end end diff --git a/app/controllers/admin/venue_commercials_controller.rb b/app/controllers/admin/venue_commercials_controller.rb index 11a87d199..753817fd0 100644 --- a/app/controllers/admin/venue_commercials_controller.rb +++ b/app/controllers/admin/venue_commercials_controller.rb @@ -38,7 +38,7 @@ def destroy end def render_commercial - result = Commercial.render_from_url(params[:url]) + result = Commercial.render_from_url(params[:url], params[:title]) if result[:error] render plain: result[:error], status: :bad_request else diff --git a/app/controllers/conferences_controller.rb b/app/controllers/conferences_controller.rb index 1e60e6466..af8b286b5 100644 --- a/app/controllers/conferences_controller.rb +++ b/app/controllers/conferences_controller.rb @@ -9,7 +9,7 @@ class ConferencesController < ApplicationController def index @current = Conference.upcoming.reorder(start_date: :asc) - @antiquated = Conference.past.select { |conf| conf.splashpage&.public? } + @antiquated = Conference.past.joins(:splashpage).where(splashpage: { public: true }).includes(:venue, :program, :splashpage, :registration_period) render :new_install if @antiquated.empty? && @current.empty? && User.empty? end diff --git a/app/controllers/payments_controller.rb b/app/controllers/payments_controller.rb index e86f9ffb7..17a4b94b2 100644 --- a/app/controllers/payments_controller.rb +++ b/app/controllers/payments_controller.rb @@ -28,6 +28,9 @@ def new def create @payment = Payment.new payment_params + session[:selected_currency] = params[:currency] if params[:currency].present? + selected_currency = session[:selected_currency] || @conference.tickets.first.price_currency + from_currency = @conference.tickets.first.price_currency if @payment.purchase && @payment.save update_purchased_ticket_purchases @@ -47,7 +50,8 @@ def create notice: 'Thanks! Your ticket is booked successfully.' end else - @total_amount_to_pay = convert_currency(@conference, Ticket.total_price(@conference, current_user, paid: false), from_currency, selected_currency) + # TODO-SNAPCON: This case is not tested at all + @total_amount_to_pay = CurrencyConversion.convert_currency(@conference, Ticket.total_price(@conference, current_user, paid: false), from_currency, selected_currency) @unpaid_ticket_purchases = current_user.ticket_purchases.unpaid.by_conference(@conference) flash.now[:error] = @payment.errors.full_messages.to_sentence + ' Please try again with correct credentials.' render :new diff --git a/app/controllers/proposals_controller.rb b/app/controllers/proposals_controller.rb index 23087362d..2c3281e9a 100644 --- a/app/controllers/proposals_controller.rb +++ b/app/controllers/proposals_controller.rb @@ -41,6 +41,7 @@ def edit def create @url = conference_program_proposals_path(@conference.short_title) + @superevents = @program.events.where(superevent: true) # We allow proposal submission and sign up on same page. # If user is not signed in then first create new user and then sign them in @@ -80,6 +81,7 @@ def create def update @url = conference_program_proposal_path(@conference.short_title, params[:id]) + @superevents = @program.events.where(superevent: true) track = Track.find_by(id: params[:event][:track_id]) if track && !track.cfp_active @@ -216,6 +218,6 @@ def event_params end def user_params - params.require(:user).permit(:email, :password, :password_confirmation, :username, :is_admin) + params.require(:user).permit(:email, :password, :password_confirmation, :username) end end diff --git a/app/datatables/registration_datatable.rb b/app/datatables/registration_datatable.rb index 1639ef422..d8969fff8 100644 --- a/app/datatables/registration_datatable.rb +++ b/app/datatables/registration_datatable.rb @@ -5,6 +5,8 @@ class RegistrationDatatable < AjaxDatatablesRails::ActiveRecord def_delegator :@view, :dom_id def_delegator :@view, :edit_admin_conference_registration_path + # def_delegator :@view, :delete_admin_conference_registration_path + def_delegator :@view, :admin_conference_registration_toggle_attendance_path def initialize(params, opts = {}) @view = opts[:view_context] @@ -17,6 +19,8 @@ def view_columns name: { source: 'User.name' }, email: { source: 'User.email' }, accepted_code_of_conduct: { source: 'Registration.accepted_code_of_conduct', searchable: false }, + attended: { source: 'Registration.attended', searchable: false }, + ticket_price: { source: 'TicketPurchase.amount_paid' }, ticket_type: { source: 'Ticket.title' }, actions: { source: 'Registration.id', searchable: false, orderable: false } } @@ -34,6 +38,14 @@ def conference_role_titles(record) end.compact end + def registration_ticket(record) + record.user.tickets.for_registration(conference) + end + + def registration_ticket_price(record) + record.user.ticket_purchases.where(ticket: registration_ticket(record)).first.amount_paid + end + def data records.map do |record| { @@ -42,7 +54,9 @@ def data roles: conference_role_titles(record.user), email: record.email, accepted_code_of_conduct: !!record.accepted_code_of_conduct, # rubocop:disable Style/DoubleNegation - ticket_type: record.user.tickets.where(conference: conference).pluck(:title), + ticket_type: registration_ticket(record).title, + ticket_price: registration_ticket_price(record), + attended: record.attended?, edit_url: edit_admin_conference_registration_path(conference, record), DT_RowId: dom_id(record) } @@ -50,7 +64,7 @@ def data end def get_raw_records # rubocop:disable Naming/AccessorMethodName - conference.registrations.includes(user: %i[roles tickets]).references(:users, :roles).distinct + conference.registrations.includes(user: %i[roles tickets ticket_purchases]).references(:users, :roles).distinct end # override upstream santitation, which converts everything to strings diff --git a/app/helpers/conference_helper.rb b/app/helpers/conference_helper.rb index d393b6099..3a2c3a136 100644 --- a/app/helpers/conference_helper.rb +++ b/app/helpers/conference_helper.rb @@ -55,8 +55,9 @@ def icalendar_proposals(calendar, proposals, conference) location += "#{v.country_name}, " if v.country_name e.location = location end - e.categories = conference.title, "Difficulty: #{proposal.difficulty_level.title}", - "Track: #{proposal.track.name}" + e.categories = conference.title + e.categories << "Difficulty: #{proposal.difficulty_level.title}" if proposal.difficulty_level.present? + e.categories << "Track: #{proposal.track.name}" if proposal.track.present? end end calendar diff --git a/app/mailers/mailbot.rb b/app/mailers/mailbot.rb index 32dfb7c2a..58a9f6a9b 100644 --- a/app/mailers/mailbot.rb +++ b/app/mailers/mailbot.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -YTLF_TICKET_ID = Rails.configuration.mailbot[:ytlf_ticket_id] - class Mailbot < ApplicationMailer helper ApplicationHelper helper ConferenceHelper @@ -32,28 +30,19 @@ def ticket_confirmation_mail(ticket_purchase) attachments["ticket_for_#{@conference.short_title}_#{physical_ticket.id}.pdf"] = pdf.render end - if @ticket_purchase.ticket_id == YTLF_TICKET_ID - template_name = 'young_thinkers_ticket_confirmation_template' - mail(subject: "#{@conference.title} | Ticket Confirmation and PDF!", template_name: template_name) + email_subject = "#{@conference.title} | Ticket Confirmation and PDF!" + email_template = 'ticket_confirmation_template' + + if @ticket_purchase.ticket.email_subject.present? + email_subject = @ticket_purchase.render_email_data(@ticket_purchase.ticket.email_subject) end - # if email subject is empty, use custom template - if @ticket_purchase.ticket.email_subject.empty? && !@ticket_purchase.ticket.email_body.empty? - @ticket_purchase.ticket.email_body = @ticket_purchase.generate_confirmation_mail(@ticket_purchase.ticket.email_body) - mail(subject: "#{@conference.title} | Ticket Confirmation and PDF!", template_name: 'custom_ticket_confirmation_template') - # if email body is empty, use default template with subject - elsif !@ticket_purchase.ticket.email_subject.empty? && @ticket_purchase.ticket.email_body.empty? - @ticket_purchase.ticket.email_subject = @ticket_purchase.generate_confirmation_mail(@ticket_purchase.ticket.email_subject) - mail(subject: @ticket_purchase.ticket.email_subject, template_name: 'ticket_confirmation_template') - # if both exist, use custom - elsif !@ticket_purchase.ticket.email_subject.empty? && !@ticket_purchase.ticket.email_body.empty? - @ticket_purchase.ticket.email_body = @ticket_purchase.generate_confirmation_mail(@ticket_purchase.ticket.email_body) - @ticket_purchase.ticket.email_subject = @ticket_purchase.generate_confirmation_mail(@ticket_purchase.ticket.email_subject) - mail(subject: @ticket_purchase.ticket.email_subject, template_name: 'custom_ticket_confirmation_template') - # if both empty, use default - else - mail(subject: "#{@conference.title} | Ticket Confirmation and PDF!", template_name: 'ticket_confirmation_template') + if @ticket_purchase.ticket.email_body.present? + @rendered_email_body = @ticket_purchase.render_email_data(@ticket_purchase.ticket.email_body) + email_template = 'custom_ticket_confirmation_template' end + + mail(subject: email_subject, template_name: email_template) end def acceptance_mail(event) diff --git a/app/models/commercial.rb b/app/models/commercial.rb index 42d4ccadf..b01da1aff 100644 --- a/app/models/commercial.rb +++ b/app/models/commercial.rb @@ -28,51 +28,30 @@ class Commercial < ApplicationRecord validate :valid_url - def self.render_from_url(url) + def self.render_from_url(url, title = nil) register_provider - url = generate_snap_embed(url) begin resource = OEmbed::Providers.get(url, maxwidth: 560, maxheight: 315) { html: resource.html.html_safe } rescue StandardError - { html: iframe_fallback(url) } + { html: EmbeddableURL.new(url, title).render_embed.html_safe } # { error: exception.message } end end - def self.generate_snap_embed(url) - return url unless url - - uri = URI.parse(url) - if uri.host == 'snap.berkeley.edu' && uri.path == '/project' - args = URI.decode_www_form(uri.query).to_h - else - return url - end - if args.key?('username') && args.key?('projectname') - new_query = URI.encode_www_form({ - 'projectname' => args['projectname'], - 'username' => args['username'], - 'showTitle' => 'true', - 'showAuthor' => 'true', - 'editButton' => 'true', - 'pauseButton' => 'true' - }) - new_uri = URI::HTTPS.build( - host: uri.host, - path: '/embed', - query: new_query - ) - return new_uri.to_s - end - url - end - - def self.iframe_fallback(url) - "".html_safe + # TODO: Is this necessary? + def self.iframe_fallback(url, title) + iframe = <<~HTML + + HTML + iframe.html_safe end def self.read_file(file) + require 'csv' errors = {} errors[:no_event] = [] errors[:validation_errors] = [] @@ -96,7 +75,8 @@ def self.read_file(file) commercial = event.commercials.new(title: title, url: url) unless commercial.save - errors[:validation_errors] << ("Could not create materials for event with ID #{event.id} (" + commercial.errors.full_messages.to_sentence + ')') + errors[:validation_errors] << + "Could not create materials for event with ID #{event.id} (#{commercial.errors.full_messages.to_sentence})" end end errors @@ -105,7 +85,9 @@ def self.read_file(file) private def valid_url - result = Commercial.render_from_url(url) + return unless url + + result = Commercial.render_from_url(url, title) errors.add(:base, result[:error]) if result[:error] end diff --git a/app/models/conference.rb b/app/models/conference.rb index dcf525754..21717f7ef 100644 --- a/app/models/conference.rb +++ b/app/models/conference.rb @@ -44,9 +44,9 @@ class Conference < ApplicationRecord # Dependent destroy will fail as roles#destroy will be cancelled,hence delete_all resourcify :roles, dependent: :delete_all - default_scope { order('start_date DESC') } - scope :upcoming, -> { where('end_date >= ?', Date.current) } - scope :past, -> { where('end_date < ?', Date.current) } + default_scope { order('conferences.start_date DESC') } + scope :upcoming, -> { where('conferences.end_date >= ?', Date.current) } + scope :past, -> { where('conferences.end_date < ?', Date.current) } belongs_to :organization delegate :code_of_conduct, to: :organization diff --git a/app/models/ticket_purchase.rb b/app/models/ticket_purchase.rb index 5eced20ec..2577269e8 100644 --- a/app/models/ticket_purchase.rb +++ b/app/models/ticket_purchase.rb @@ -125,7 +125,7 @@ def registration_ticket_already_purchased end end - def generate_confirmation_mail(event_template) + def render_email_data(event_template) parser = EmailTemplateParser.new(conference, user) values = parser.retrieve_values(nil, nil, quantity, ticket) EmailTemplateParser.parse_template(event_template, values) diff --git a/app/services/embeddable_url.rb b/app/services/embeddable_url.rb new file mode 100644 index 000000000..289a69208 --- /dev/null +++ b/app/services/embeddable_url.rb @@ -0,0 +1,99 @@ +# Transform a URL to a version that allows iframes + +class EmbeddableURL + attr_accessor :url, :title + + DEFAULT_FRAME_ATTRS = 'width=560 height=315 frameborder=0 allowfullscreen'.freeze + + TRANSFORMATIONS = { + /snap\.berkeley\.edu/ => :snap, + /docs\.google\.com/ => :google_docs, + /dropbox\.com/ => :dropbox + }.freeze + + def initialize(url, title) + # Do some normalizing so that URIs parse correctly. + if url + self.url = url.strip + end + self.title = title + end + + def render_embed + return render_dropbox if url.include?('dropbox.com') + + # TODO-A11Y: Set an iframe title + "" + end + + def iframe_url + TRANSFORMATIONS.each do |regex, fn| + return send(fn, url) if url.match?(regex) + end + url + end + + # TODO: Consider adjusting the id / loading if > 1 dropbox embed per page. + def render_dropbox + <<~HTML +
+ + +
+ HTML + end + + private + + def optional_params + return '' unless url.include?('snap.berkeley') + + 'allow="geolocation;microphone;camera"' + end + + def google_docs(url) + # replace /edit, /share, /comment with /embed and remove the querystring + url.gsub(%r{(/edit|/share|/comment).*}, '/embed') + end + + def dropbox(url) + # debugger + uri = URI.parse(url) + params = URI.decode_www_form(uri.query)&.to_h + params.delete('raw') + params['dl'] = '0' + # params['rlkey'] = params['rlkey'] + uri.query = params.to_query + uri.to_s + end + + def snap(url) + uri = URI.parse(url) + return url if uri.query.blank? + + args = URI.decode_www_form(uri.query).to_h + username = args['username'] || args['user'] + projectname = args['projectname'] || args['project'] + + return url if username.blank? || projectname.blank? + + query = URI.encode_www_form({ + 'projectname' => projectname, + 'username' => username, + 'showTitle' => 'true', + 'showAuthor' => 'true', + 'editButton' => 'true', + 'pauseButton' => 'true' + }) + URI::HTTPS.build(host: uri.host, path: '/embed', query: query).to_s + end + + def iframe_title + if title + "title='#{title} Embedded Media'" + else + 'title="Embedded Media for Presentation"' + end + end +end diff --git a/app/views/admin/events/_form.html.haml b/app/views/admin/events/_form.html.haml index 538d842a6..ca8b6f547 100644 --- a/app/views/admin/events/_form.html.haml +++ b/app/views/admin/events/_form.html.haml @@ -28,7 +28,7 @@ = form_for(@event.commercials.new, url: conference_program_proposal_commercials_path(conference_id: @conference.short_title, proposal_id: @event)) do |f| = render 'proposals/commercial_form_fields', f: f, commercial: @event.commercials.build %hr - - @event.commercials.each_slice(3) do |slice| + - @event.commercials.includes([:versions]).each_slice(3) do |slice| .row - slice.each do |commercial| - if commercial.persisted? diff --git a/app/views/admin/events/index.html.haml b/app/views/admin/events/index.html.haml index 6f68faee1..2d1ac6372 100644 --- a/app/views/admin/events/index.html.haml +++ b/app/views/admin/events/index.html.haml @@ -66,6 +66,7 @@ %th Rating %th Submitter %th Speakers + %th Volunteers - if @program.languages.present? %th Language %th Requires Registration diff --git a/app/views/admin/registrations/index.html.haml b/app/views/admin/registrations/index.html.haml index 45f66b232..c756e8dd2 100644 --- a/app/views/admin/registrations/index.html.haml +++ b/app/views/admin/registrations/index.html.haml @@ -28,6 +28,8 @@ %th{ width: '25%' } Name %th{ width: '0' } E-Mail %th{ width: '0' } Ticket Type + %th{ width: '0' } Price + %th{ width: '0' } Attended %th{ width: '0' } %abbr{ title: 'Code of Conduct' } CoC %th{ width: '0' } Actions @@ -55,12 +57,12 @@ { "data": "name", "className": "truncate", - "render": function(data, type, row) { - var content = '' + data + '
'; - $.each(row.roles, function(index, role){ - content += ' ' + role + '' - }); - return content; + "render": (data, type, row) => { + return ` + ${data} +
+ ${row.roles.map(role => ` ${role}`)} + `; } }, { @@ -69,20 +71,42 @@ { "data": "ticket_type" }, + { + "data": "ticket_price", + "render": data => `$${data}` + }, + { + "data": "attended", + "search": data => data, + "render": (data, _type, row) => { + let js_url = "#{toggle_attendance_admin_conference_registration_path(@conference.short_title, id: 'ROW_ID')}"; + return ` + ${data} + + `; + } + }, { "data": "accepted_code_of_conduct", "className": "code-of-conduct text-center", "searchable": false, + "visible": codeOfConductPresent }, { "data": "actions", "className": "actions", "searchable": false, "sortable": false, - "render": function (data, type, row, meta) { - return '
'+ - 'Edit'+ - '
'; + "render": (data, type, row) => { + return `
+ Edit +
`; } } ] diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 03f5684f0..d479390e7 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -15,9 +15,13 @@ %tr %th{ width: '0' } ID %th{ width: '0' } Confirmed? - %th{ width: '0' } Email - %th{ width: '30%' } Name - %th{ width: '20%' } Conferences Attended + %th{ width: '0' } Name + %th{ width: '0' } EMail + %th{ width: '0' } Username + %th{ width: '15%' } + Conferences + %br + Attended %th{ width: '50%' } Roles %th{ width: '0' } Actions %th{ style: 'display: none' } Confirmed? diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index f73e125a4..64374c3f4 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -23,6 +23,7 @@ - if @conference && @conference.custom_css %style{type: 'text/css'} = @conference.custom_css.html_safe + = yield(:script_head) %body{ class: ("conference-#{@conference.short_title}" if @conference) } = render 'layouts/navigation', conference: @conference diff --git a/app/views/mailbot/custom_ticket_confirmation_template.html.erb b/app/views/mailbot/custom_ticket_confirmation_template.html.erb index d5fa91b3c..d58c7d910 100644 --- a/app/views/mailbot/custom_ticket_confirmation_template.html.erb +++ b/app/views/mailbot/custom_ticket_confirmation_template.html.erb @@ -1,7 +1,7 @@ <%= render partial: "layouts/mailbot_header" %>
- <%= @ticket_purchase.ticket.email_body %> + <%= @rendered_email_body %>
<%= render partial: "layouts/mailbot_footer" %> diff --git a/app/views/mailbot/custom_ticket_confirmation_template.text.erb b/app/views/mailbot/custom_ticket_confirmation_template.text.erb index 426630251..50239f3bd 100644 --- a/app/views/mailbot/custom_ticket_confirmation_template.text.erb +++ b/app/views/mailbot/custom_ticket_confirmation_template.text.erb @@ -1 +1 @@ -<%= @ticket_purchase.ticket.email_body %> \ No newline at end of file +<%= @rendered_email_body %> diff --git a/app/views/mailbot/young_thinkers_ticket_confirmation_template.html.erb b/app/views/mailbot/young_thinkers_ticket_confirmation_template.html.erb deleted file mode 100644 index d35d8a1f9..000000000 --- a/app/views/mailbot/young_thinkers_ticket_confirmation_template.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -<%= render partial: "layouts/mailbot_header" %> -
- - Dear <%= @user.name %>, - - Thanks! You have successfully booked <%= @ticket_purchase.quantity %> <%= @ticket_purchase.ticket.title %> ticket(s) for the event <%= @conference.title %>. Your transaction id is <%= @ticket_purchase.id %>. - - Please, find the ticket(s) pdf attached. - - The SAP Young Thinkers team will reach out to you with information on how to participate in the event soon. In the meantime, you can check the event page (https://events.sap.com/yt-learning-festival-at-snapcon-2020/en/home) or send an email with your questions to youngthinkers@sap.com. - - Best wishes, - <%= @conference.title %> Team - -
-<%= render partial: "layouts/mailbot_footer" %> \ No newline at end of file diff --git a/app/views/proposals/_form.html.haml b/app/views/proposals/_form.html.haml index 150083d4c..6d5badcb2 100644 --- a/app/views/proposals/_form.html.haml +++ b/app/views/proposals/_form.html.haml @@ -96,3 +96,55 @@ %p.text-right = f.submit @event.persisted? ? 'Update Proposal' : 'Create Proposal', class: 'btn btn-success' + +- content_for :script_head do + :javascript + /* Wait for the DOM to be ready before attaching events to the elements */ + $( document ).ready(function() { + /* Set the minimum and maximum proposal abstract word length */ + function updateEventTypeRequirements() { + var $selected = $("#event_event_type_id option:selected") + var max = $selected.data("max-words"); + var min = $selected.data("min-words"); + + // We replace the default text only if the current field is empty, + // or is set to the default text of another event type. + replace_defaut_submission_text( + '#event_submission_text', + $selected.data("instructions"), + $("#event_event_type_id option").toArray().map(e => $(e).data('instructions')) + ); + + $("#abstract-maximum-word-count").text(max); + $("#abstract-minimum-word-count").text(min); + word_count($('#event_abstract').get(0), 'abstract-count', max); + } + $("#event_event_type_id").change(updateEventTypeRequirements); + updateEventTypeRequirements(); + + /* Count the proposal abstract length */ + $("#event_abstract").on('input', function() { + var $selected = $("#event_event_type_id option:selected") + var max = $selected.data("max-words"); + word_count(this, 'abstract-count', max); + } ); + + /* Count the submission text length */ + $("#event_submission_text").bind('change keyup paste input', function() { + var $selected = $("event_event_type_id option:selected") + var max = $selected.data("max-words"); + word_count(this, 'submission-count', max); + }); + + $('.js-resetSubmissionText').click((e) => { + console.log('CLICKED') + let $selected = $("#event_event_type_id option:selected"); + let $this = $(e.target); + let affirm = confirm($this.data('confirm')); + if (affirm) { + let sub_text = $('#event_submission_text'); + sub_text.val($selected.data('instructions')); + sub_text.trigger('change'); + } + }); + }); diff --git a/app/views/proposals/_submission_type_content_form.haml b/app/views/proposals/_submission_type_content_form.haml index c7b947e25..76b915047 100644 --- a/app/views/proposals/_submission_type_content_form.haml +++ b/app/views/proposals/_submission_type_content_form.haml @@ -3,10 +3,8 @@ .form-group = f.label :event_type_id, "Type" - - if current_user&.is_admin - = f.select :event_type_id, event_type_select_options(@conference.program.event_types), { include_blank: false }, { class: 'select-help-toggle form-control' } - - else - = f.select :event_type_id, event_type_select_options(@conference.program.event_types.available_for_public), { include_blank: false }, { class: 'select-help-toggle form-control' } + - visible_event_types = current_user&.is_admin ? @conference.program.event_types : @conference.program.event_types.available_for_public + = f.select :event_type_id, event_type_select_options(visible_event_types), { include_blank: false }, { class: 'select-help-toggle form-control' } - program.event_types.each do |event_type| .help-block.event_event_type_id.collapse{ id: "#{dom_id(event_type)}-help" } diff --git a/app/views/schedules/_date_event_types.haml b/app/views/schedules/_date_event_types.haml index 2ba3cd4d2..c129e782e 100644 --- a/app/views/schedules/_date_event_types.haml +++ b/app/views/schedules/_date_event_types.haml @@ -5,6 +5,8 @@ The conference timezone is #{timezone_mapping(conference.timezone)}. - if current_user Visit your #{link_to('user profile page', edit_user_path(current_user.id))} to set your timezone. + - else + (#{link_to('Log in', new_user_session_path)} to view the schedule in your preferred timezone.) - if current_user %p.text-center Click the star next to each event to add or remove it from diff --git a/app/views/schedules/_event.html.haml b/app/views/schedules/_event.html.haml index 122d3e1e1..e3438bde0 100644 --- a/app/views/schedules/_event.html.haml +++ b/app/views/schedules/_event.html.haml @@ -14,7 +14,7 @@ %h3.event-panel-title = link_to conference_program_proposal_path(@conference.short_title, event.id), - style: color_style do + style: "#{color_style}; line-height: 1.7" do = event.title - if event.subtitle.present? %br diff --git a/app/views/schedules/_event_mini.html.haml b/app/views/schedules/_event_mini.html.haml index b0e29509c..27acb4a77 100644 --- a/app/views/schedules/_event_mini.html.haml +++ b/app/views/schedules/_event_mini.html.haml @@ -25,8 +25,8 @@ %span= " at #{new_start_time.strftime('%l:%M %P')}" = join_event_link(event, event_schedule, current_user, small: true) %p - = truncate(markdown(event.abstract), length: 250, escape: false) - - if event.subevents.present? + = markdown(truncate(event.abstract, length: 256)) + - if event.program_subevents.present? %ul - event.program_subevents.each do |subevent| %li= link_to(subevent.title, conference_program_proposal_path(@conference.short_title, subevent.id)) diff --git a/app/views/schedules/events.html.haml b/app/views/schedules/events.html.haml index 14a1669ca..a8079c9f2 100644 --- a/app/views/schedules/events.html.haml +++ b/app/views/schedules/events.html.haml @@ -40,38 +40,33 @@ .row / scheduled events :ruby - date = nil - time = nil - tz_object = current_user&.timezone ? current_user : @conference + prev_date, prev_time = nil + display_tz = current_user&.timezone.presence || @conference.timezone / TODO-SNAPCON: Explore caching this. - - @events_schedules.each do |event_schedule| - - next if event_schedule.event.parent_event.present? - - new_start_time = convert_timezone(event_schedule.start_time, @conference.timezone, tz_object.timezone) - - start_ymd = new_start_time.strftime('%Y-%m-%d') - - unless start_ymd.eql?(date) - .col-xs-12.col-md-12 - .date-content - %span.date-title - = inyourtz(event_schedule.start_time, @conference.timezone) do - = date = start_ymd - %a.pull-right{ title: "Go up", href: "#program" } - %i.fa-solid.fa-angles-up.fa-lg{ 'aria-hidden': true } - .col-xs-12.col-md-1 - - if !event_schedule.start_time.strftime('%H:%M').eql?(time) || !start_ymd.eql?(date) - - time = new_start_time.strftime('%H:%M') - = inyourtz(event_schedule.start_time, @conference.timezone) do - = time + ' ' + timezone_text(tz_object) - .col-xs-12.col-md-11 - - cache [@program, event_schedule, event_schedule.event, current_user, event_schedule.happening_now?, '#scheduled#full#panel'] do - .event-item{ data: { time: event_schedule.start_time.iso8601 }, class: "event-#{event_schedule.event.id}" } - = render 'event', event: event_schedule.event, event_schedule: event_schedule + - @events_schedules.select { |es| es.event.parent_event.nil? }.each do |event_schedule| + - event = event_schedule.event + - start_time = convert_timezone(event_schedule.start_time, @conference.timezone, display_tz) + + - unless start_time.to_date.eql?(prev_date) + .date-content + %h2.date-title{ style: "margin: 0;" } + = prev_date = start_time.to_date + %a.pull-right{ title: "Go up", href: "#program" } + %i.fa-solid.fa-angles-up.fa-lg{ 'aria-hidden': true } + - if !start_time.eql?(prev_time) + - prev_time = start_time + %h3= start_time.strftime('%H:%M %p %Z') + .col-12 + -# - cache [@program, event_schedule, event, current_user, event_schedule.happening_now?, '#scheduled#full#panel'] do + .event-item{ data: { time: event_schedule.start_time.iso8601 }, id: dom_id(event) } + = render 'event', event: event, event_schedule: event_schedule / confirmed events that are not scheduled - if @unscheduled_events.any? - .col-xs-12.col-md-12 + .col-12 .date-content - %span.date-title#unscheduled + %h2.date-title#unscheduled{ style: "margin: 0;" } Unscheduled events %a.pull-right{ title: "Go up", href: "#program" } %i.fa-solid.fa-angles-up.fa-lg{ 'aria-hidden': true } diff --git a/app/views/shared/_media_item.html.haml b/app/views/shared/_media_item.html.haml index 367d04a2f..8e36eb4ad 100644 --- a/app/views/shared/_media_item.html.haml +++ b/app/views/shared/_media_item.html.haml @@ -15,7 +15,7 @@ - elsif commercial.commercial_type == 'YouTube' %iframe{width: '560', height: '315', frameborder: '0', allowfullscreen: 'true', src: "https://www.youtube.com/embed/#{commercial.commercial_id}?rel=0"} - elsif commercial.url - = Commercial.render_from_url(commercial.url)[:html] + = Commercial.render_from_url(commercial.url, commercial.title)[:html] - if commercial.url = link_to('Open in a new tab', commercial.url, target: '_blank', class: 'btn btn-info btn-xs') %br diff --git a/config/environments/development.rb b/config/environments/development.rb index 1512c020b..275bd6525 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -66,8 +66,7 @@ # Test mailbot settings config.mailbot = { - ytlf_ticket_id: 50, - bcc_address: 'test@test.com' + bcc_address: 'test@test.com' } # Use omniauth mock credentials diff --git a/config/environments/production.rb b/config/environments/production.rb index 59431a74d..c139b2caa 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -144,8 +144,7 @@ # Mailbot settings config.mailbot = { - ytlf_ticket_id: ENV.fetch('YTLF_TICKET_ID', 50), - bcc_address: ENV.fetch('OSEM_MESSAGE_BCC_ADDRESS', nil) + bcc_address: ENV.fetch('OSEM_MESSAGE_BCC_ADDRESS', nil) } config.after_initialize do diff --git a/config/environments/test.rb b/config/environments/test.rb index a7723d22d..279e313c9 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -59,7 +59,6 @@ # Test mailbot settings config.mailbot = { - ytlf_ticket_id: 50, - bcc_address: 'test@test.com' + bcc_address: 'test@test.com' } end diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index ac033bf9d..90ac51ca2 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -11,6 +11,6 @@ # end # These inflection rules are supported but not enabled by default: -# ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.acronym 'RESTful' -# end +ActiveSupport::Inflector.inflections(:en) do |inflect| + inflect.acronym 'URL' +end diff --git a/config/puma.rb b/config/puma.rb index 35d73834e..c401d6fc2 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -25,12 +25,13 @@ # processes). # workers ENV.fetch('WEB_CONCURRENCY') { 2 } +# Set a 10 minute timeout in development for debugging. +worker_timeout 60 * 60 * 10 if ENV.fetch('RAILS_ENV') == 'development' # Use the `preload_app!` method when specifying a `workers` number. # This directive tells Puma to first boot the application and load code # before forking the application. This takes advantage of Copy On Write # process behavior so workers use less memory. -# preload_app! lowlevel_error_handler do |ex, env| diff --git a/db/migrate/20230418211400_add_currency_to_payments.rb b/db/migrate/20230418211400_add_currency_to_payments.rb new file mode 100644 index 000000000..467a04f43 --- /dev/null +++ b/db/migrate/20230418211400_add_currency_to_payments.rb @@ -0,0 +1,5 @@ +class AddCurrencyToPayments < ActiveRecord::Migration[7.0] + def change + add_column :payments, :currency, :string + end +end diff --git a/db/migrate/20230418211410_add_currency_to_ticket_purchases.rb b/db/migrate/20230418211410_add_currency_to_ticket_purchases.rb new file mode 100644 index 000000000..70e4f9d96 --- /dev/null +++ b/db/migrate/20230418211410_add_currency_to_ticket_purchases.rb @@ -0,0 +1,5 @@ +class AddCurrencyToTicketPurchases < ActiveRecord::Migration[7.0] + def change + add_column :ticket_purchases, :currency, :string + end +end diff --git a/db/migrate/20230418211400_create_currency_conversions.rb b/db/migrate/20230418211420_create_currency_conversions.rb similarity index 79% rename from db/migrate/20230418211400_create_currency_conversions.rb rename to db/migrate/20230418211420_create_currency_conversions.rb index 9c5100085..3403e779c 100644 --- a/db/migrate/20230418211400_create_currency_conversions.rb +++ b/db/migrate/20230418211420_create_currency_conversions.rb @@ -1,6 +1,6 @@ class CreateCurrencyConversions < ActiveRecord::Migration[7.0] def change - create_table :currency_conversions do |t| + create_table :currency_conversions, if_not_exists: true do |t| t.decimal :rate t.string :from_currency t.string :to_currency diff --git a/db/migrate/20240422200831_add_amount_paid_cents_to_ticket_purchases.rb b/db/migrate/20240422200831_add_amount_paid_cents_to_ticket_purchases.rb index 349471109..b8176309f 100644 --- a/db/migrate/20240422200831_add_amount_paid_cents_to_ticket_purchases.rb +++ b/db/migrate/20240422200831_add_amount_paid_cents_to_ticket_purchases.rb @@ -3,17 +3,6 @@ def up add_column :ticket_purchases, :amount_paid_cents, :integer, default: 0 TicketPurchase.reset_column_information - - TicketPurchase.find_each do |purchase| - converted_amount = CurrencyConversion.convert_currency( - purchase.conference, - purchase.price, - purchase.price_currency, - purchase.currency - ) - - purchase.update_column(:amount_paid_cents, converted_amount.fractional) - end end def down diff --git a/features/conferences/all_events.feature b/features/conferences/all_events.feature index 4df1fe39b..409c91da1 100644 --- a/features/conferences/all_events.feature +++ b/features/conferences/all_events.feature @@ -9,7 +9,7 @@ Scenario: Display events using the conference's timezone And I should see "All events are currently displayed in PDT (UTC -7)." And I should see the following data: 8:00 am - 8:30 am PDT, 8:30 am - 9:00 am PDT And I should see the following data in order: Dates, 2014-05-03, 2014-05-04, 2014-05-05, 2014-05-06, 2014-05-07, Unscheduled - And I should see the following data in order: 2014-05-03, 08:00 PDT, first_scheduled_event, 8:00 am, first_scheduled_subevent, 08:30 PDT, multiple_speaker_event + And I should see the following data in order: 2014-05-03, 08:00 AM PDT, first_scheduled_event, 8:00 am, first_scheduled_subevent, 08:30 AM PDT, multiple_speaker_event Scenario: Display events using the user's timezone Given I sign in with username "admin" and password "password123" @@ -18,4 +18,4 @@ Scenario: Display events using the user's timezone And I should see "All events are currently displayed in AEST (UTC 10)." And I should see the following data: 1:00 am - 1:30 am AEST, 1:30 am - 2:00 am AEST And I should see the following data in order: Dates, 2014-05-04, 2014-05-05, 2014-05-06, 2014-05-07, 2014-05-08, Unscheduled - And I should see the following data in order: 2014-05-04, 01:00 AEST, first_scheduled_event, 1:00 am, first_scheduled_subevent, 01:30 AEST, multiple_speaker_event + And I should see the following data in order: 2014-05-04, 01:00 AM AEST, first_scheduled_event, 1:00 am, first_scheduled_subevent, 01:30 AM AEST, multiple_speaker_event diff --git a/lib/tasks/migrate_config.rake b/lib/tasks/migrate_config.rake index 99081611d..7ee42cdc8 100644 --- a/lib/tasks/migrate_config.rake +++ b/lib/tasks/migrate_config.rake @@ -40,5 +40,18 @@ namespace :data do puts "Migrated config/config.yml to .env.#{Rails.env}" end + + task :update_ticket_purchase_currency do + TicketPurchase.find_each do |purchase| + converted_amount = CurrencyConversion.convert_currency( + purchase.conference, + purchase.ticket.price_cents, + purchase.price_currency, + purchase.currency + ) + + purchase.update_column(:amount_paid_cents, converted_amount.fractional) + end + end end end diff --git a/lib/tasks/migrate_currency.rake b/lib/tasks/migrate_currency.rake new file mode 100644 index 000000000..b05304131 --- /dev/null +++ b/lib/tasks/migrate_currency.rake @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +namespace :data do + namespace :migrate do + desc 'Update Currency of past ticket purchases' + task :update_ticket_purchase_currency do + TicketPurchase.where(currency: nil).update_all(currency: 'USD') + TicketPurchase.find_each do |purchase| + converted_amount = CurrencyConversion.convert_currency( + purchase.conference, + purchase.ticket.price_cents, + purchase.price_currency, + purchase.currency + ) + + purchase.update_column(:amount_paid_cents, converted_amount) + end + end + end +end diff --git a/package-lock.json b/package-lock.json index fedc26385..e5e7a779a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "snapcon2", + "name": "snapcon", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/spec/features/currency_conversions_spec.rb b/spec/features/currency_conversions_spec.rb index 38c42a4bb..229a7771d 100644 --- a/spec/features/currency_conversions_spec.rb +++ b/spec/features/currency_conversions_spec.rb @@ -6,7 +6,7 @@ let!(:conference) { create(:conference, title: 'ExampleCon') } let!(:admin) { create(:admin) } - context 'as an admin', js: true do + context 'as an admin', :js do before do sign_in admin end diff --git a/spec/features/event_schedules_spec.rb b/spec/features/event_schedules_spec.rb index cc01677a5..bbb3eaf20 100644 --- a/spec/features/event_schedules_spec.rb +++ b/spec/features/event_schedules_spec.rb @@ -41,6 +41,6 @@ it 'jumps to the closest event' do find('#current-event-btn').click highlighted_element = page.find('.highlighted', visible: true, wait: 1) - expect(highlighted_element[:class]).to include("event-#{scheduled_event_mid.id}") + expect(highlighted_element[:id]).to include("event_#{scheduled_event_mid.id}") end end diff --git a/spec/features/ticket_purchases_spec.rb b/spec/features/ticket_purchases_spec.rb index c3b4f9b87..32bb9fd1f 100644 --- a/spec/features/ticket_purchases_spec.rb +++ b/spec/features/ticket_purchases_spec.rb @@ -198,7 +198,6 @@ def make_failed_stripe_purchase context 'currency conversion' do before do - ENV['SHOW_CURRENCY_SELECTOR'] = 'true' conference.currency_conversions << create(:currency_conversion, from_currency: 'USD', to_currency: 'EUR', rate: 0.89) conference.currency_conversions << create(:currency_conversion, from_currency: 'USD', to_currency: 'GBP', rate: 0.75) visit root_path diff --git a/spec/features/user_spec.rb b/spec/features/user_spec.rb index 0bc07888d..a6bb12161 100644 --- a/spec/features/user_spec.rb +++ b/spec/features/user_spec.rb @@ -9,6 +9,7 @@ shared_examples 'admin ability' do scenario 'edits a user', feature: true, js: true do visit admin_users_path + wait_for_ajax within "tr#user_#{user.id}" do click_on 'Edit' end diff --git a/spec/models/commercial_spec.rb b/spec/models/commercial_spec.rb index 2adc17c97..a09077f8e 100644 --- a/spec/models/commercial_spec.rb +++ b/spec/models/commercial_spec.rb @@ -29,11 +29,4 @@ commercial = build(:conference_commercial) expect(commercial.valid?).to be true end - - it 'parses snap url' do - url = 'https://snap.berkeley.edu/project?username=avi_shor&projectname=stamps' - transformed_url = Commercial.generate_snap_embed(url) - expected_url = 'https://snap.berkeley.edu/embed?projectname=stamps&username=avi_shor&showTitle=true&showAuthor=true&editButton=true&pauseButton=true' - expect(transformed_url).to eq expected_url - end end diff --git a/spec/models/track_spec.rb b/spec/models/track_spec.rb index 7c8df641a..a054de04b 100644 --- a/spec/models/track_spec.rb +++ b/spec/models/track_spec.rb @@ -61,8 +61,6 @@ withdrawn]) } - it { is_expected.to validate_inclusion_of(:cfp_active).in_array([true, false]) } - context 'when self_organized_and_accepted_or_confirmed? returns true' do before do allow(subject).to receive(:self_organized_and_accepted_or_confirmed?).and_return(true) diff --git a/spec/services/embeddable_url_spec.rb b/spec/services/embeddable_url_spec.rb new file mode 100644 index 000000000..b88a94a37 --- /dev/null +++ b/spec/services/embeddable_url_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe EmbeddableURL do + describe '#iframe_url' do + it 'returns the original url if no transformations apply' do + url = 'https://example.com' + expect(EmbeddableURL.new(url, 'title').iframe_url).to eq url + end + + it 'transforms a Google Drive URL' do + url = EmbeddableURL.new('https://docs.google.com/presentation/d/1eGbEQtcOPW2N2P5rKfBVfSo2zn4C307Sh6C7vpJsruE/edit#slide=id.g1088c029399_0_47', 'title').iframe_url + expect(url).to include '/embed' + expect(url).not_to include('/edit') + end + + it 'transforms a Dropbox URL' do + url = EmbeddableURL.new('https://www.dropbox.com/scl/fi/49gkp6ghfnxgqex64zvzd/Guzdial-SnapCon23.pdf?rlkey=ecwvmcmfscqtwfq21l3kzqcul&dl=1', 'title').iframe_url + expect(url).to include('dl=0') + expect(url).not_to include('raw=') + end + + it 'transforms a Snap! Project URL' do + url = EmbeddableURL.new('https://snap.berkeley.edu/project?username=jedi_force&projectname=Autograder%2dlite', 'title').iframe_url + expect(url).to include('/embed') + end + end + + # it 'parses snap url' do + # url = 'https://snap.berkeley.edu/project?username=avi_shor&projectname=stamps' + # transformed_url = Commercial.generate_snap_embed(url) + # expected_url = 'https://snap.berkeley.edu/embed?projectname=stamps&username=avi_shor&showTitle=true&showAuthor=true&editButton=true&pauseButton=true' + # expect(transformed_url).to eq expected_url + # end + # TODO: Test ifram generation, snap-embedding +end diff --git a/spec/support/external_request.rb b/spec/support/external_request.rb index 45f0a5e03..6fd7af57f 100644 --- a/spec/support/external_request.rb +++ b/spec/support/external_request.rb @@ -5,9 +5,10 @@ driver_urls = Webdrivers::Common.subclasses.map do |driver| Addressable::URI.parse(driver.base_url).host end -# Local chromedriver pings GitHub. :( +# Local chromedriver pings many sites. :( driver_urls << 'googlechromelabs.github.io' driver_urls << 'edgedl.me.gvt1.com' # The fuck, Google? +driver_urls << 'storage.googleapis.com' WebMock.disable_net_connect!(allow_localhost: true, allow: [*driver_urls, /stripe.com/]) RSpec.configure do |config| diff --git a/spec/support/wait_for_ajax..rb b/spec/support/wait_for_ajax..rb new file mode 100644 index 000000000..5a524e156 --- /dev/null +++ b/spec/support/wait_for_ajax..rb @@ -0,0 +1,16 @@ +module WaitForAjax + def wait_for_ajax + Timeout.timeout(Capybara.default_max_wait_time) do + puts '...waiting...' + loop until finished_all_ajax_requests? + end + end + + def finished_all_ajax_requests? + page.evaluate_script('jQuery.active').zero? + end +end + +RSpec.configure do |config| + config.include WaitForAjax, type: :feature +end diff --git a/vendor/assets/images/holderjs/test/image.jpg b/vendor/assets/images/holderjs/test/image.jpg new file mode 100644 index 000000000..4d7fa14c9 Binary files /dev/null and b/vendor/assets/images/holderjs/test/image.jpg differ diff --git a/vendor/assets/javascripts/bootstrap-markdown.js b/vendor/assets/javascripts/bootstrap-markdown.js new file mode 100644 index 000000000..eb380aeef --- /dev/null +++ b/vendor/assets/javascripts/bootstrap-markdown.js @@ -0,0 +1 @@ +//= require bootstrap-markdown/bootstrap-markdown.js diff --git a/vendor/assets/javascripts/bootstrap-markdown/bootstrap-markdown.js b/vendor/assets/javascripts/bootstrap-markdown/bootstrap-markdown.js new file mode 100644 index 000000000..21e8c60ea --- /dev/null +++ b/vendor/assets/javascripts/bootstrap-markdown/bootstrap-markdown.js @@ -0,0 +1,1390 @@ +/* =================================================== + * bootstrap-markdown.js v2.10.0 + * http://github.com/toopay/bootstrap-markdown + * =================================================== + * Copyright 2013-2016 Taufan Aditya + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + +(function(factory){ + if (typeof define === "function" && define.amd) { + //RequireJS + define(["jquery"], factory); + } else if (typeof exports === 'object') { + //Backbone.js + factory(require('jquery')); + } else { + //Jquery plugin + factory(jQuery); + } +}(function($){ + "use strict"; // jshint ;_; + + /* MARKDOWN CLASS DEFINITION + * ========================== */ + + var Markdown = function (element, options) { + // @TODO : remove this BC on next major release + // @see : https://github.com/toopay/bootstrap-markdown/issues/109 + var opts = ['autofocus', 'savable', 'hideable', 'width', + 'height', 'resize', 'iconlibrary', 'language', + 'footer', 'fullscreen', 'hiddenButtons', 'disabledButtons']; + $.each(opts,function(_, opt){ + if (typeof $(element).data(opt) !== 'undefined') { + options = typeof options == 'object' ? options : {} + options[opt] = $(element).data(opt) + } + }); + // End BC + + // Class Properties + this.$ns = 'bootstrap-markdown'; + this.$element = $(element); + this.$editable = {el:null, type:null,attrKeys:[], attrValues:[], content:null}; + this.$options = $.extend(true, {}, $.fn.markdown.defaults, options, this.$element.data('options')); + this.$oldContent = null; + this.$isPreview = false; + this.$isFullscreen = false; + this.$editor = null; + this.$textarea = null; + this.$handler = []; + this.$callback = []; + this.$nextTab = []; + + this.showEditor(); + }; + + Markdown.prototype = { + + constructor: Markdown + + , __alterButtons: function(name,alter) { + var handler = this.$handler, isAll = (name == 'all'),that = this; + + $.each(handler,function(k,v) { + var halt = true; + if (isAll) { + halt = false; + } else { + halt = v.indexOf(name) < 0; + } + + if (halt === false) { + alter(that.$editor.find('button[data-handler="'+v+'"]')); + } + }); + } + + , __buildButtons: function(buttonsArray, container) { + var i, + ns = this.$ns, + handler = this.$handler, + callback = this.$callback; + + for (i=0;i', { + 'class': 'btn-group' + }); + + for (z=0;z'); + buttonContainer.text(' ' + this.__localize(btnText)).addClass('btn-default btn-sm').addClass(btnClass); + if(btnClass.match(/btn\-(primary|success|info|warning|danger|link)/)){ + buttonContainer.removeClass('btn-default'); + } + buttonContainer.attr({ + 'type': 'button', + 'title': this.__localize(button.title) + hotkeyCaption, + 'tabindex': tabIndex, + 'data-provider': ns, + 'data-handler': buttonHandler, + 'data-hotkey': hotkey + }); + if (button.toggle === true){ + buttonContainer.attr('data-toggle', 'button'); + } + buttonIconContainer = $(''); + buttonIconContainer.addClass(buttonIcon); + buttonIconContainer.prependTo(buttonContainer); + + // Attach the button object + btnGroupContainer.append(buttonContainer); + + // Register handler and callback + handler.push(buttonHandler); + callback.push(button.callback); + } + + // Attach the button group into container dom + container.append(btnGroupContainer); + } + } + + return container; + } + , __setListener: function() { + // Set size and resizable Properties + var hasRows = typeof this.$textarea.attr('rows') !== 'undefined', + maxRows = this.$textarea.val().split("\n").length > 5 ? this.$textarea.val().split("\n").length : '5', + rowsVal = hasRows ? this.$textarea.attr('rows') : maxRows; + + this.$textarea.attr('rows',rowsVal); + if (this.$options.resize) { + this.$textarea.css('resize',this.$options.resize); + } + + this.$textarea.on({ + 'focus' : $.proxy(this.focus, this), + 'keyup' : $.proxy(this.keyup, this), + 'change' : $.proxy(this.change, this), + 'select' : $.proxy(this.select, this) + }); + + if (this.eventSupported('keydown')) { + this.$textarea.on('keydown', $.proxy(this.keydown, this)); + } + + if (this.eventSupported('keypress')) { + this.$textarea.on('keypress', $.proxy(this.keypress, this)) + } + + // Re-attach markdown data + this.$textarea.data('markdown',this); + } + + , __handle: function(e) { + var target = $(e.currentTarget), + handler = this.$handler, + callback = this.$callback, + handlerName = target.attr('data-handler'), + callbackIndex = handler.indexOf(handlerName), + callbackHandler = callback[callbackIndex]; + + // Trigger the focusin + $(e.currentTarget).focus(); + + callbackHandler(this); + + // Trigger onChange for each button handle + this.change(this); + + // Unless it was the save handler, + // focusin the textarea + if (handlerName.indexOf('cmdSave') < 0) { + this.$textarea.focus(); + } + + e.preventDefault(); + } + + , __localize: function(string) { + var messages = $.fn.markdown.messages, + language = this.$options.language; + if ( + typeof messages !== 'undefined' && + typeof messages[language] !== 'undefined' && + typeof messages[language][string] !== 'undefined' + ) { + return messages[language][string]; + } + return string; + } + + , __getIcon: function(src) { + return typeof src == 'object' ? src[this.$options.iconlibrary] : src; + } + + , setFullscreen: function(mode) { + var $editor = this.$editor, + $textarea = this.$textarea; + + if (mode === true) { + $editor.addClass('md-fullscreen-mode'); + $('body').addClass('md-nooverflow'); + this.$options.onFullscreen(this); + } else { + $editor.removeClass('md-fullscreen-mode'); + $('body').removeClass('md-nooverflow'); + + if (this.$isPreview == true) this.hidePreview().showPreview() + } + + this.$isFullscreen = mode; + $textarea.focus(); + } + + , showEditor: function() { + var instance = this, + textarea, + ns = this.$ns, + container = this.$element, + originalHeigth = container.css('height'), + originalWidth = container.css('width'), + editable = this.$editable, + handler = this.$handler, + callback = this.$callback, + options = this.$options, + editor = $( '
', { + 'class': 'md-editor', + click: function() { + instance.focus(); + } + }); + + // Prepare the editor + if (this.$editor === null) { + // Create the panel + var editorHeader = $('
', { + 'class': 'md-header btn-toolbar' + }); + + // Merge the main & additional button groups together + var allBtnGroups = []; + if (options.buttons.length > 0) allBtnGroups = allBtnGroups.concat(options.buttons[0]); + if (options.additionalButtons.length > 0) { + // iterate the additional button groups + $.each(options.additionalButtons[0], function(idx, buttonGroup){ + + // see if the group name of the addional group matches an existing group + var matchingGroups = $.grep(allBtnGroups, function(allButtonGroup, allIdx){ + return allButtonGroup.name === buttonGroup.name; + }); + + // if it matches add the addional buttons to that group, if not just add it to the all buttons group + if(matchingGroups.length > 0) { + matchingGroups[0].data = matchingGroups[0].data.concat(buttonGroup.data); + } else { + allBtnGroups.push(options.additionalButtons[0][idx]); + } + + }); + } + + // Reduce and/or reorder the button groups + if (options.reorderButtonGroups.length > 0) { + allBtnGroups = allBtnGroups + .filter(function(btnGroup) { + return options.reorderButtonGroups.indexOf(btnGroup.name) > -1; + }) + .sort(function(a, b) { + if (options.reorderButtonGroups.indexOf(a.name) < options.reorderButtonGroups.indexOf(b.name)) return -1; + if (options.reorderButtonGroups.indexOf(a.name) > options.reorderButtonGroups.indexOf(b.name)) return 1; + return 0; + }); + } + + // Build the buttons + if (allBtnGroups.length > 0) { + editorHeader = this.__buildButtons([allBtnGroups], editorHeader); + } + + if (options.fullscreen.enable) { + editorHeader.append('
').on('click', '.md-control-fullscreen', function(e) { + e.preventDefault(); + instance.setFullscreen(true); + }); + } + + editor.append(editorHeader); + + // Wrap the textarea + if (container.is('textarea')) { + container.before(editor); + textarea = container; + textarea.addClass('md-input'); + editor.append(textarea); + } else { + var rawContent = (typeof toMarkdown == 'function') ? toMarkdown(container.html()) : container.html(), + currentContent = $.trim(rawContent); + + // This is some arbitrary content that could be edited + textarea = $('