diff --git a/Gemfile b/Gemfile index 18979c093..84e1f6268 100644 --- a/Gemfile +++ b/Gemfile @@ -74,8 +74,11 @@ gem 'barnes' # Forum gem 'thredded', git: 'https://github.com/indentlabs/thredded.git', branch: 'feature/report-posts' +# gem 'thredded', git: 'https://github.com/thredded/thredded', branch: 'master' +# gem 'thredded', git: 'https://github.com/sudara/thredded', branch: 'master' gem 'rails-ujs' gem 'language_filter' +gem 'discordrb' # Smarts gem 'word_count_analyzer' diff --git a/Gemfile.lock b/Gemfile.lock index ff4e7ef56..e84183867 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,13 +13,13 @@ GIT GIT remote: https://github.com/indentlabs/thredded.git - revision: b9a3976e310bb4eeb3f9a4b30d6c176d15a28ce1 + revision: 6ed40273e4696fc4f32013e4f2a101ff8658b960 branch: feature/report-posts specs: thredded (0.16.16) active_record_union (>= 1.3.0) autoprefixer-rails - db_text_search (~> 0.3.0) + db_text_search (~> 0.3.2) friendly_id html-pipeline htmlentities @@ -28,10 +28,10 @@ GIT kramdown (>= 2.0.0) kramdown-parser-gfm nokogiri - onebox (~> 1.8, >= 1.8.99) + onebox (>= 1.8.99) pundit (>= 1.1.0) rails (>= 4.2.10, != 6.0.0.rc2) - rb-gravatar + rails_gravatar rinku sanitize sassc-rails (>= 2.0.0) @@ -41,38 +41,40 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (6.0.3.4) - actionpack (= 6.0.3.4) + actioncable (6.1.4.1) + actionpack (= 6.1.4.1) + activesupport (= 6.1.4.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.0.3.4) - actionpack (= 6.0.3.4) - activejob (= 6.0.3.4) - activerecord (= 6.0.3.4) - activestorage (= 6.0.3.4) - activesupport (= 6.0.3.4) + actionmailbox (6.1.4.1) + actionpack (= 6.1.4.1) + activejob (= 6.1.4.1) + activerecord (= 6.1.4.1) + activestorage (= 6.1.4.1) + activesupport (= 6.1.4.1) mail (>= 2.7.1) - actionmailer (6.0.3.4) - actionpack (= 6.0.3.4) - actionview (= 6.0.3.4) - activejob (= 6.0.3.4) + actionmailer (6.1.4.1) + actionpack (= 6.1.4.1) + actionview (= 6.1.4.1) + activejob (= 6.1.4.1) + activesupport (= 6.1.4.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.0.3.4) - actionview (= 6.0.3.4) - activesupport (= 6.0.3.4) - rack (~> 2.0, >= 2.0.8) + actionpack (6.1.4.1) + actionview (= 6.1.4.1) + activesupport (= 6.1.4.1) + rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.0.3.4) - actionpack (= 6.0.3.4) - activerecord (= 6.0.3.4) - activestorage (= 6.0.3.4) - activesupport (= 6.0.3.4) + actiontext (6.1.4.1) + actionpack (= 6.1.4.1) + activerecord (= 6.1.4.1) + activestorage (= 6.1.4.1) + activesupport (= 6.1.4.1) nokogiri (>= 1.8.5) - actionview (6.0.3.4) - activesupport (= 6.0.3.4) + actionview (6.1.4.1) + activesupport (= 6.1.4.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -81,39 +83,41 @@ GEM activerecord (>= 4.0) active_storage_validations (0.9.5) rails (>= 5.2.0) - activejob (6.0.3.4) - activesupport (= 6.0.3.4) + activejob (6.1.4.1) + activesupport (= 6.1.4.1) globalid (>= 0.3.6) - activemodel (6.0.3.4) - activesupport (= 6.0.3.4) + activemodel (6.1.4.1) + activesupport (= 6.1.4.1) activemodel-serializers-xml (1.0.2) activemodel (> 5.x) activesupport (> 5.x) builder (~> 3.1) - activerecord (6.0.3.4) - activemodel (= 6.0.3.4) - activesupport (= 6.0.3.4) - activestorage (6.0.3.4) - actionpack (= 6.0.3.4) - activejob (= 6.0.3.4) - activerecord (= 6.0.3.4) - marcel (~> 0.3.1) - activesupport (6.0.3.4) + activerecord (6.1.4.1) + activemodel (= 6.1.4.1) + activesupport (= 6.1.4.1) + activestorage (6.1.4.1) + actionpack (= 6.1.4.1) + activejob (= 6.1.4.1) + activerecord (= 6.1.4.1) + activesupport (= 6.1.4.1) + marcel (~> 1.0.0) + mini_mime (>= 1.1.0) + activesupport (6.1.4.1) concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - zeitwerk (~> 2.2, >= 2.2.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) acts_as_list (1.0.4) activerecord (>= 4.2) - addressable (2.7.0) + addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) authority (3.3.0) activesupport (>= 3.0.0) - autoprefixer-rails (9.8.4) - execjs + autoprefixer-rails (10.3.3.0) + execjs (~> 2) aws-eventstream (1.2.0) - aws-partitions (1.503.0) + aws-partitions (1.543.0) aws-sdk (3.1.0) aws-sdk-resources (~> 3) aws-sdk-accessanalyzer (1.23.0) @@ -311,9 +315,9 @@ GEM aws-sdk-connectparticipant (1.14.0) aws-sdk-core (~> 3, >= 3.120.0) aws-sigv4 (~> 1.1) - aws-sdk-core (3.121.0) + aws-sdk-core (3.125.0) aws-eventstream (~> 1, >= 1.0.2) - aws-partitions (~> 1, >= 1.239.0) + aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) aws-sdk-costandusagereportservice (1.34.0) @@ -574,8 +578,8 @@ GEM aws-sdk-kinesisvideosignalingchannels (1.13.0) aws-sdk-core (~> 3, >= 3.120.0) aws-sigv4 (~> 1.1) - aws-sdk-kms (1.48.0) - aws-sdk-core (~> 3, >= 3.120.0) + aws-sdk-kms (1.53.0) + aws-sdk-core (~> 3, >= 3.125.0) aws-sigv4 (~> 1.1) aws-sdk-lakeformation (1.17.0) aws-sdk-core (~> 3, >= 3.120.0) @@ -1082,8 +1086,8 @@ GEM aws-sdk-route53resolver (1.30.0) aws-sdk-core (~> 3, >= 3.120.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.103.0) - aws-sdk-core (~> 3, >= 3.120.0) + aws-sdk-s3 (1.110.0) + aws-sdk-core (~> 3, >= 3.125.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) aws-sdk-s3control (1.40.0) @@ -1261,7 +1265,7 @@ GEM bundler (>= 1.2.0, < 3) thor (~> 1.0) chartkick (4.0.5) - childprocess (3.0.0) + childprocess (4.1.0) climate_control (0.2.0) cocoon (1.2.15) codeclimate-test-reporter (1.0.9) @@ -1277,34 +1281,43 @@ GEM concurrent-ruby (1.1.9) connection_pool (2.2.5) crass (1.0.6) - csv (3.2.0) + csv (3.2.2) d3-rails (5.9.2) railties (>= 3.1) database_cleaner (2.0.1) database_cleaner-active_record (~> 2.0.0) - database_cleaner-active_record (2.0.0) + database_cleaner-active_record (2.0.1) activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) dateslices (0.0.4) rails (> 4) - db_text_search (0.3.1) + db_text_search (0.3.2) activerecord (>= 4.1.15, < 7.0) - debug_inspector (1.0.0) + debug_inspector (1.1.0) devise (4.8.0) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) + discordrb (3.4.0) + discordrb-webhooks (~> 3.3.0) + ffi (>= 1.9.24) + opus-ruby + rest-client (>= 2.0.0) + websocket-client-simple (>= 0.3.0) + discordrb-webhooks (3.3.0) + rest-client (>= 2.1.0.rc1) docile (1.1.5) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) engtagger (0.2.1) erubi (1.10.0) + event_emitter (0.2.6) eventmachine (1.2.7) - execjs (2.7.0) - faraday (0.17.3) + execjs (2.8.1) + faraday (0.17.4) multipart-post (>= 1.2, < 3) faraday_middleware (0.14.0) faraday (>= 0.7.4, < 1.0) @@ -1319,7 +1332,7 @@ GEM flamegraph (0.9.5) font-awesome-rails (4.7.0.7) railties (>= 3.2, < 7) - friendly_id (5.3.0) + friendly_id (5.4.2) activerecord (>= 4.0.0) globalid (0.5.2) activesupport (>= 5.0) @@ -1335,6 +1348,7 @@ GEM http-cookie (~> 1.0) http-form_data (~> 2.2) http-parser (~> 1.2.0) + http-accept (1.7.0) http-cookie (1.0.4) domain_name (~> 0.5) http-form_data (2.3.0) @@ -1356,7 +1370,7 @@ GEM image_processing (1.12.1) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) - inline_svg (1.7.1) + inline_svg (1.7.2) activesupport (>= 3.0) nokogiri (>= 1.6) jmespath (1.4.0) @@ -1366,7 +1380,7 @@ GEM thor (>= 0.14, < 2.0) jquery-ui-rails (6.0.1) railties (>= 3.2.16) - json (2.3.0) + json (2.5.1) jwt (2.2.3) kaminari (1.2.1) activesupport (>= 4.1.0) @@ -1394,8 +1408,7 @@ GEM nokogiri (>= 1.5.9) mail (2.7.1) mini_mime (>= 0.1.1) - marcel (0.3.3) - mimemagic (~> 0.3.2) + marcel (1.0.2) material_icons (2.2.1) railties (>= 3.2) medium-editor-rails (2.3.1) @@ -1406,12 +1419,12 @@ GEM method_source (1.0.0) mime-types (3.3.1) mime-types-data (~> 3.2015) - mime-types-data (3.2020.0512) + mime-types-data (3.2021.0704) mimemagic (0.3.10) nokogiri (~> 1) rake mini_magick (4.11.0) - mini_mime (1.1.0) + mini_mime (1.1.2) mini_portile2 (2.6.1) mini_racer (0.4.0) libv8-node (~> 15.14.0.0) @@ -1420,20 +1433,21 @@ GEM multipart-post (2.1.1) mustache (1.1.1) nested_form (0.3.2) + netrc (0.11.0) newrelic_rpm (8.0.0) nio4r (2.5.8) nokogiri (1.12.5) mini_portile2 (~> 2.6.1) racc (~> 1.4) - nokogumbo (2.0.2) - nokogiri (~> 1.8, >= 1.8.4) - onebox (1.9.29) - addressable (~> 2.7.0) + onebox (2.2.19) + addressable (~> 2.8.0) htmlentities (~> 4.3) multi_json (~> 1.11) mustache nokogiri (~> 1.7) sanitize + opus-ruby (1.0.1) + ffi orm_adapter (0.5.0) paperclip (6.1.0) activemodel (>= 4.2.0) @@ -1455,13 +1469,13 @@ GEM coderay (~> 1.1) method_source (~> 1.0) public_suffix (4.0.6) - puma (5.5.0) + puma (5.5.1) nio4r (~> 2.0) puma-heroku (2.0.0) puma (>= 5.0, < 6.0) - pundit (2.1.0) + pundit (2.1.1) activesupport (>= 3.0.0) - racc (1.5.2) + racc (1.6.0) rack (2.2.3) rack-mini-profiler (2.3.3) rack (>= 1.2.0) @@ -1472,20 +1486,20 @@ GEM rack rack-test (1.1.0) rack (>= 1.0, < 3) - rails (6.0.3.4) - actioncable (= 6.0.3.4) - actionmailbox (= 6.0.3.4) - actionmailer (= 6.0.3.4) - actionpack (= 6.0.3.4) - actiontext (= 6.0.3.4) - actionview (= 6.0.3.4) - activejob (= 6.0.3.4) - activemodel (= 6.0.3.4) - activerecord (= 6.0.3.4) - activestorage (= 6.0.3.4) - activesupport (= 6.0.3.4) - bundler (>= 1.3.0) - railties (= 6.0.3.4) + rails (6.1.4.1) + actioncable (= 6.1.4.1) + actionmailbox (= 6.1.4.1) + actionmailer (= 6.1.4.1) + actionpack (= 6.1.4.1) + actiontext (= 6.1.4.1) + actionview (= 6.1.4.1) + activejob (= 6.1.4.1) + activemodel (= 6.1.4.1) + activerecord (= 6.1.4.1) + activestorage (= 6.1.4.1) + activesupport (= 6.1.4.1) + bundler (>= 1.15.0) + railties (= 6.1.4.1) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) @@ -1508,15 +1522,16 @@ GEM rails (>= 5.0, < 7) remotipart (~> 1.3) sassc-rails (>= 1.3, < 3) - railties (6.0.3.4) - actionpack (= 6.0.3.4) - activesupport (= 6.0.3.4) + rails_gravatar (1.0.4) + actionview + railties (6.1.4.1) + actionpack (= 6.1.4.1) + activesupport (= 6.1.4.1) method_source - rake (>= 0.8.7) - thor (>= 0.20.3, < 2.0) + rake (>= 0.13) + thor (~> 1.0) rake (13.0.6) rb-fsevent (0.11.0) - rb-gravatar (1.0.5) rb-inotify (0.10.1) ffi (~> 1.0) react-rails (2.6.1) @@ -1526,21 +1541,25 @@ GEM railties (>= 3.2) tilt redcarpet (3.5.1) - redis (4.4.0) + redis (4.5.1) remotipart (1.4.4) responders (3.0.1) actionpack (>= 5.0) railties (>= 5.0) + rest-client (2.1.0) + http-accept (>= 1.7.0, < 2.0) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 4.0) + netrc (~> 0.8) rexml (3.2.5) rinku (2.0.6) - rmagick (4.2.2) + rmagick (4.2.4) ruby-vips (2.0.17) ffi (~> 1.9) - rubyzip (2.0.0) - sanitize (5.2.1) + rubyzip (2.3.2) + sanitize (6.0.0) crass (~> 1.0.2) - nokogiri (>= 1.8.0) - nokogumbo (~> 2.0) + nokogiri (>= 1.12.0) sass-rails (6.0.0) sassc-rails (~> 2.1, >= 2.1.1) sassc (2.4.0) @@ -1551,8 +1570,9 @@ GEM sprockets (> 3.0) sprockets-rails tilt - selenium-webdriver (3.142.7) - childprocess (>= 0.5, < 4.0) + selenium-webdriver (4.1.0) + childprocess (>= 0.5, < 5.0) + rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2) semantic_range (3.0.0) sidekiq (6.2.2) @@ -1579,7 +1599,7 @@ GEM sqlite3 (1.4.2) stackprof (0.2.17) statsd-ruby (1.5.0) - stripe (5.38.0) + stripe (5.42.0) stripe_event (2.3.1) activesupport (>= 3.1) stripe (>= 2.8, < 6) @@ -1587,15 +1607,14 @@ GEM terrapin (0.6.0) climate_control (>= 0.0.3, < 1.0) text-hyphen (1.4.1) - textstat (0.1.6) + textstat (0.1.7) text-hyphen (~> 1.4, >= 1.4.1) thor (1.1.0) - thread_safe (0.3.6) tilt (2.0.10) timeago_js (3.0.2.2) tribute (3.6.0.0) - tzinfo (1.2.9) - thread_safe (~> 0.1) + tzinfo (2.0.4) + concurrent-ruby (~> 1.0) uglifier (4.2.0) execjs (>= 0.3.0, < 3) unf (0.1.4) @@ -1614,12 +1633,16 @@ GEM rack-proxy (>= 0.6.1) railties (>= 5.2) semantic_range (>= 2.3.0) + websocket (1.2.9) + websocket-client-simple (0.3.0) + event_emitter + websocket websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) word_count_analyzer (1.0.1) engtagger - zeitwerk (2.4.2) + zeitwerk (2.5.1) PLATFORMS ruby @@ -1644,6 +1667,7 @@ DEPENDENCIES database_cleaner dateslices devise + discordrb engtagger filesize flamegraph diff --git a/app/assets/javascripts/content.js b/app/assets/javascripts/content.js index 6eac7528a..8de06125f 100644 --- a/app/assets/javascripts/content.js +++ b/app/assets/javascripts/content.js @@ -104,13 +104,35 @@ $(document).ready(function () { // Replace this element's content with the name of the page var tag = $(this); - $.get( - '/api/internal/' + tag.data('klass') + '/' + tag.data('id') + '/name' - ).done(function (response) { - tag.find('.name-container').text(response); - }).fail(function() { - tag.find('.name-conainer').text("Unknown " + tag.data('klass')); - }); + // Instantiate a cache for all page lookup queries (if not already created) + window.load_page_name_cache = window.load_page_name_cache || {}; + var page_name_key = tag.data('klass') + '/' + tag.data('id'); + + if (page_name_key in window.load_page_name_cache) { + // If we've already made a request for this klass+id, we can just insta-load the + // cached result instead of requesting it again. + tag.find('.name-container').text(window.load_page_name_cache[page_name_key]); + + } else { + // If we haven't made a request for this klass+id, look it up and cache it + $.get( + '/api/internal/' + page_name_key + '/name' + ).done(function (response) { + tag.find('.name-container').text(response); + window.load_page_name_cache[page_name_key] = response; + + // Go ahead and pre-fill all tags on the page for this klass+id, too + $('.js-load-page-name[data-klass=' + tag.data('klass') + '][data-id=' + tag.data('id') + ']') + .find('.name-container') + .text(response); + + }).fail(function() { + tag.find('.name-container').text("Unknown " + tag.data('klass')); + }); + } + + + }); }); diff --git a/app/assets/javascripts/page_tags.coffee b/app/assets/javascripts/page_tags.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/page_tags.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/authorizers/timeline_authorizer.rb b/app/authorizers/timeline_authorizer.rb new file mode 100644 index 000000000..1a4e3a0d0 --- /dev/null +++ b/app/authorizers/timeline_authorizer.rb @@ -0,0 +1,31 @@ +class TimelineAuthorizer < ContentAuthorizer + def self.creatable_by?(user) + return false unless user.present? + return false if ENV.key?('CONTENT_BLACKLIST') && ENV['CONTENT_BLACKLIST'].split(',').include?(user.email) + + return true if user.on_premium_plan? + end + + def readable_by?(user) + return true if resource.privacy == 'public' + return true if user && resource.user_id == user.id + return true if resource.universe.present? && resource.universe.privacy == 'public' + return true if user && resource.universe.present? && resource.universe.user_id == user.id + return true if user && resource.universe.present? && resource.universe.contributors.pluck(:user_id).include?(user.id) + return true if user && user.site_administrator? + + return false + end + + def updatable_by?(user) + [ + user && resource.user_id == user.id + ].any? + end + + def deletable_by?(user) + [ + user && resource.user_id == user.id + ].any? + end +end diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index e46e52682..7b6e7f4a2 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -59,6 +59,13 @@ def reported_shares def churn end + def notifications + @clicked_notifications = Notification.where.not(viewed_at: nil) + @notifications = Notification.all.order(:reference_code) + + @codes = Notification.distinct.order('reference_code').pluck(:reference_code) + end + def hate @posts = Thredded::PrivatePost.order('id DESC').limit(params.fetch(:limit, 500)).includes(:postable) @list = params[:matchlist] diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6e5b9a9f5..efb66824c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -16,6 +16,12 @@ def content_type_from_controller(content_controller_name) private + def require_premium_plan + unless user_signed_in? && current_user.on_premium_plan? + return redirect_back(fallback_location: root_path, notice: "Doing that requires Premium access.") + end + end + def set_metadata @page_title ||= '' @page_keywords ||= %w[writing author nanowrimo novel character fiction fantasy universe creative dnd roleplay game design] @@ -94,6 +100,13 @@ def cache_current_user_content universe_id: @universe_scope.try(:id) ) + # Due to the way we loop over @current_user_content (page, list) later, we want to make sure that we + # at least have an empty list for all activated content types -- otherwise we may skip over the contributor + # content injection for a type that a user doesn't have ANY pages for. + @activated_content_types.each do |content_type| + @current_user_content[content_type] ||= [] + end + # Likewise, we should also always cache Timelines & Documents if @universe_scope @current_user_content['Timeline'] = current_user.timelines.where(universe_id: @universe_scope.try(:id)).to_a @@ -159,7 +172,7 @@ def cache_contributable_universe_ids cache_current_user_content @contributable_universe_ids ||= if user_signed_in? - current_user.contributable_universe_ids + @current_user_content.fetch('Universe', []).map(&:id) + current_user.contributable_universe_ids else [] end @@ -196,7 +209,7 @@ def cache_linkable_content_for_each_content_type # If we're scoped to a universe, also scope contributor content pulled to that # universe. If we're not, leave it as all contributor content. - if @universe_scope && pages_to_add.klass.respond_to?(:universe) + if @universe_scope && pages_to_add.klass.name != 'Universe' pages_to_add = pages_to_add.where(universe: @universe_scope) end diff --git a/app/controllers/content_controller.rb b/app/controllers/content_controller.rb index 9b99761cb..ec99a8d97 100644 --- a/app/controllers/content_controller.rb +++ b/app/controllers/content_controller.rb @@ -55,6 +55,11 @@ def index @questioned_content = @content.sample @attribute_field_to_question = SerendipitousService.question_for(@questioned_content) + @random_image_including_private_pool_cache = ImageUpload.where( + content_type: @content_type_class.name, + content_id: @content.pluck(:id) + ).group_by { |image| [image.content_type, image.content_id] } + # Uh, do we ever actually make JSON requests to logged-in user pages? respond_to do |format| format.html { render 'content/index' } @@ -147,6 +152,10 @@ def edit return redirect_to @content, notice: t(:no_do_permission) end + @random_image_including_private_pool_cache = ImageUpload.where( + user_id: current_user.id, + ).group_by { |image| [image.content_type, image.content_id] } + respond_to do |format| format.html { render 'content/edit', locals: { content: @content } } format.json { render json: @content } diff --git a/app/controllers/contributors_controller.rb b/app/controllers/contributors_controller.rb index a7f8fe568..6d2207543 100644 --- a/app/controllers/contributors_controller.rb +++ b/app/controllers/contributors_controller.rb @@ -13,7 +13,8 @@ def destroy icon: Universe.icon, icon_color: Universe.color, happened_at: DateTime.current, - passthrough_link: Rails.application.routes.url_helpers.universe_path(relevant_universe) + passthrough_link: Rails.application.routes.url_helpers.universe_path(relevant_universe), + reference_code: 'contributor-removed' ) if user.present? # Create a notification letting the universe owner know @@ -22,7 +23,8 @@ def destroy icon: Universe.icon, icon_color: Universe.color, happened_at: DateTime.current, - passthrough_link: Rails.application.routes.url_helpers.universe_path(relevant_universe) + passthrough_link: Rails.application.routes.url_helpers.universe_path(relevant_universe), + reference_code: 'contributor-left' ) if user.present? #todo send "you've been removed as a contributor" email diff --git a/app/controllers/data_controller.rb b/app/controllers/data_controller.rb index f42eef7e6..be56236aa 100644 --- a/app/controllers/data_controller.rb +++ b/app/controllers/data_controller.rb @@ -18,12 +18,18 @@ def review_year @created_content = {} @total_created_non_universe_content = 0 + @words_written = 0 Rails.application.config.content_types[:all].each do |klass| @created_content[klass.name] = klass.where(user_id: current_user.id) .where('created_at > ?', comparable_year.beginning_of_year) .where('created_at < ?', comparable_year.end_of_year) .order('created_at ASC') + @words_written += WordCountUpdate.where( + entity_type: klass.name, + entity_id: @created_content[klass.name].map(&:id) + ).sum(:word_count) + if klass.name != 'Universe' @total_created_non_universe_content += @created_content[klass.name].count end @@ -34,6 +40,8 @@ def review_year .where('created_at < ?', comparable_year.end_of_year) .order('created_at ASC') + @words_written += @created_content['Document'].sum(:cached_word_count) + earliest_page_date = DateTime.current @earliest_page = nil @created_content.each do |content_type, list| @@ -80,7 +88,6 @@ def review_year @published_collections = PageCollection.where(id: @created_content['PageCollectionSubmission'].pluck(:page_collection_id) - @created_content['PageCollection'].pluck(:id)) @publish_rate = @created_content['PageCollectionSubmission'].select { |s| s.accepted_at.present? && !@created_content['PageCollection'].pluck(:id).include?(s.page_collection_id) }.count.to_f / @created_content['PageCollectionSubmission'].count - end def archive @@ -115,6 +122,10 @@ def usage @content = current_user.content end + def tags + @tags = current_user.page_tags + end + def discussions @topics = Thredded::Topic.where(user_id: current_user.id) @posts = Thredded::Post.where(user_id: current_user.id) diff --git a/app/controllers/page_collection_submissions_controller.rb b/app/controllers/page_collection_submissions_controller.rb index 81f324d03..311955fa9 100644 --- a/app/controllers/page_collection_submissions_controller.rb +++ b/app/controllers/page_collection_submissions_controller.rb @@ -68,7 +68,8 @@ def approve icon: PageCollection.icon, icon_color: PageCollection.color, happened_at: DateTime.current, - passthrough_link: Rails.application.routes.url_helpers.page_collection_path(@page_collection_submission.page_collection) + passthrough_link: Rails.application.routes.url_helpers.page_collection_path(@page_collection_submission.page_collection), + reference_code: 'approved-in-collection' ) redirect_to(page_collection_pending_submissions_path(@page_collection_submission.page_collection), notice: "Submission approved!") diff --git a/app/controllers/page_tags_controller.rb b/app/controllers/page_tags_controller.rb new file mode 100644 index 000000000..ee41cc8a9 --- /dev/null +++ b/app/controllers/page_tags_controller.rb @@ -0,0 +1,38 @@ +class PageTagsController < ApplicationController + def update + raise "placeholder" + end + + def rename + old_tag_name = params[:tag].to_s + new_tag_name = params.dig(old_tag_name, :label) + + if new_tag_name.blank? + new_tag_name = 'Untitled Tag' + end + + current_user.page_tags.where(tag: old_tag_name).update_all(tag: new_tag_name) + end + + # Remove a tag and all of its links to a page + def remove + # Params + # {"page_type"=>"Location", "slug"=>"mountains", "controller"=>"page_tags", "action"=>"remove" + return unless params.key?(:page_type) && params.key?(:slug) + + PageTag.where( + page_type: params[:page_type], + slug: params[:slug], + user_id: current_user.id + ).destroy_all + + return redirect_back fallback_location: root_path, notice: 'Tag(s) deleted successfully.' + end + + # Destroy a specific tag by ID + def destroy + PageTag.find_by(id: params[:id], user_id: current_user.id).destroy! + + return redirect_back fallback_location: root_path, notice: 'Tag(s) deleted successfully.' + end +end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 32321e417..894d1e49a 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -88,7 +88,8 @@ def add_account icon: Universe.icon, icon_color: Universe.color, happened_at: DateTime.current, - passthrough_link: Rails.application.routes.url_helpers.universe_path(contributorship.universe) + passthrough_link: Rails.application.routes.url_helpers.universe_path(contributorship.universe), + reference_code: 'contributor-added' ) end end diff --git a/app/controllers/subscriptions_controller.rb b/app/controllers/subscriptions_controller.rb index 26823c172..5887c081f 100644 --- a/app/controllers/subscriptions_controller.rb +++ b/app/controllers/subscriptions_controller.rb @@ -224,7 +224,8 @@ def redeem_code icon: 'star', icon_color: 'text-darken-3 yellow', happened_at: DateTime.current, - passthrough_link: Rails.application.routes.url_helpers.customization_content_types_path + passthrough_link: Rails.application.routes.url_helpers.customization_content_types_path, + reference_code: 'premium-activation' ) redirect_back(fallback_location: subscription_path, notice: "Promo code successfully activated!") diff --git a/app/controllers/timelines_controller.rb b/app/controllers/timelines_controller.rb index 3e243cce3..2c67807de 100644 --- a/app/controllers/timelines_controller.rb +++ b/app/controllers/timelines_controller.rb @@ -5,15 +5,28 @@ class TimelinesController < ApplicationController before_action :set_navbar_color before_action :set_sidenav_expansion + before_action :require_premium_plan, only: [:new, :create] + before_action :require_timeline_read_permission, only: [:show] + before_action :require_timeline_edit_permission, only: [:edit, :update] + # GET /timelines def index cache_linkable_content_for_each_content_type + # TODO: We SHOULD be just doing the below, but since it returns ContentPage stand-ins instead + # of actual Timeline models, it's a bit wonky to get all the Timeline-specific logic in place + # without reworking most of the views. For now, we're just grabbing timelines and contributable + # timelines manually. + # @timelines = @linkables_raw.fetch('Timeline', []) @timelines = current_user.timelines + @page_title = "My timelines" if @universe_scope - @timelines = @timelines.where(universe: @universe_scope) + @timelines = Timeline.where(universe: @universe_scope) + else + # Add in all timelines from shared universes also + @timelines += Timeline.where(universe_id: current_user.contributable_universe_ids) end @page_tags = PageTag.where( @@ -26,17 +39,12 @@ def index end # if params.key?(:favorite_only) - # @content.select!(&:favorite?) + # @timelines.select!(&:favorite?) # end - end def show @page_title = @timeline.name - - unless @timeline.privacy == 'public' || (user_signed_in? && current_user == @timeline.user) - return redirect_back(fallback_location: root_path, notice: "You don't have permission to view that timeline!") - end end # GET /timelines/new @@ -47,11 +55,8 @@ def new # GET /timelines/1/edit def edit - @page_title = "Editing " + @timeline.name - + @page_title = "Editing #{@timeline.name}" @suggested_page_tags = [] - - raise "No Access" unless user_signed_in? && current_user == @timeline.user end # POST /timelines @@ -70,8 +75,6 @@ def create # PATCH/PUT /timelines/1 def update - return unless user_signed_in? && current_user == @timeline.user - if @timeline.update(timeline_params) update_page_tags @@ -89,6 +92,14 @@ def destroy private + def require_timeline_read_permission + return user_signed_in? && @timeline.readable_by?(current_user) + end + + def require_timeline_edit_permission + return user_signed_in? && @timeline.updatable_by?(current_user) + end + # TODO: move this (and the copy in ContentController) into the has_page_tags concern? def update_page_tags tag_list = page_tag_params.split(PageTag::SUBMISSION_DELIMITER) diff --git a/app/controllers/user_followings_controller.rb b/app/controllers/user_followings_controller.rb index e2ac95a6b..bf7fddfcc 100644 --- a/app/controllers/user_followings_controller.rb +++ b/app/controllers/user_followings_controller.rb @@ -34,7 +34,8 @@ def create icon: User.icon, icon_color: User.color, happened_at: DateTime.current, - passthrough_link: Rails.application.routes.url_helpers.user_path(@user_following.user) + passthrough_link: Rails.application.routes.url_helpers.user_path(@user_following.user), + reference_code: 'followed-by-user' ) redirect_to @user_following.followed_user, notice: "You're now following #{@user_following.followed_user.name.presence || 'this user'}" diff --git a/app/helpers/page_tags_helper.rb b/app/helpers/page_tags_helper.rb new file mode 100644 index 000000000..96c4808b4 --- /dev/null +++ b/app/helpers/page_tags_helper.rb @@ -0,0 +1,2 @@ +module PageTagsHelper +end diff --git a/app/jobs/cache_attribute_word_count_job.rb b/app/jobs/cache_attribute_word_count_job.rb new file mode 100644 index 000000000..fd1238dc2 --- /dev/null +++ b/app/jobs/cache_attribute_word_count_job.rb @@ -0,0 +1,30 @@ +class CacheAttributeWordCountJob < ApplicationJob + queue_as :cache + + def perform(*args) + attribute_id = args.shift + attribute = Attribute.find_by(id: attribute_id) + + return if attribute.nil? + return if attribute.value.nil? || attribute.value.blank? + + word_count = WordCountAnalyzer::Counter.new( + ellipsis: 'no_special_treatment', + hyperlink: 'count_as_one', + contraction: 'count_as_one', + hyphenated_word: 'count_as_one', + date: 'no_special_treatment', + number: 'count', + numbered_list: 'ignore', + xhtml: 'remove', + forward_slash: 'count_as_multiple_except_dates', + backslash: 'count_as_one', + dotted_line: 'ignore', + dashed_line: 'ignore', + underscore: 'ignore', + stray_punctuation: 'ignore' + ).count(attribute.value) + + attribute.update!(word_count_cache: word_count) + end +end diff --git a/app/jobs/cache_sum_attribute_word_count_job.rb b/app/jobs/cache_sum_attribute_word_count_job.rb new file mode 100644 index 000000000..63dad69aa --- /dev/null +++ b/app/jobs/cache_sum_attribute_word_count_job.rb @@ -0,0 +1,20 @@ +class CacheSumAttributeWordCountJob < ApplicationJob + queue_as :cache + + def perform(*args) + entity_type = args.shift + entity_id = args.shift + + entity = entity_type.constantize.find_by(id: entity_id) + return if entity.nil? + + sum_attribute_word_count = Attribute.where(entity_type: entity_type, entity_id: entity_id).sum(:word_count_cache) + update = entity.word_count_updates.find_or_initialize_by( + for_date: DateTime.current, + ) + update.word_count = sum_attribute_word_count + update.user_id ||= entity.user_id + + update.save! + end +end diff --git a/app/jobs/content_page_share_notification_job.rb b/app/jobs/content_page_share_notification_job.rb index 7d5cc5299..1efa46812 100644 --- a/app/jobs/content_page_share_notification_job.rb +++ b/app/jobs/content_page_share_notification_job.rb @@ -20,7 +20,8 @@ def perform(*args) passthrough_link: Rails.application.routes.url_helpers.user_content_page_share_path( id: comment.content_page_share.id, user_id: comment.content_page_share.user_id - ) + ), + reference_code: 'comment-on-shared-page' ) end end diff --git a/app/jobs/document_analysis_job.rb b/app/jobs/document_analysis_job.rb index d454bcf5c..02d2978eb 100644 --- a/app/jobs/document_analysis_job.rb +++ b/app/jobs/document_analysis_job.rb @@ -42,7 +42,8 @@ def perform(*args) icon: Document.icon, icon_color: Document.color, happened_at: DateTime.current, - passthrough_link: Rails.application.routes.url_helpers.analysis_document_url(analysis.document) + passthrough_link: Rails.application.routes.url_helpers.analysis_document_url(analysis.document), + reference_code: 'analysis-ready' ) end end diff --git a/app/jobs/notify_discord_of_thread_job.rb b/app/jobs/notify_discord_of_thread_job.rb new file mode 100644 index 000000000..e454f147b --- /dev/null +++ b/app/jobs/notify_discord_of_thread_job.rb @@ -0,0 +1,26 @@ +class NotifyDiscordOfThreadJob < ApplicationJob + require 'discordrb/webhooks' + + queue_as :low_priority + + def perform(*args) + thread_id = args.shift + thread = Thredded::Topic.find_by(id: thread_id) + raise "No thread found for new ID #{thread.id.inspect}" unless thread + + webhook_url = ENV.fetch('DISCORD_FORUMS_WEBHOOK', '').freeze + + client = Discordrb::Webhooks::Client.new(url: webhook_url) + client.execute do |builder| + builder.content = "New thread in **#{thread.messageboard.name}** by #{thread.user.display_name}" + builder.add_embed do |embed| + embed.title = thread.title + embed.description = thread.first_post.content.truncate(140) + embed.timestamp = Time.now + embed.url = "https://www.notebook.ai/forum/#{thread.messageboard.slug}/#{thread.slug}" + embed.colour = 2201331 + end + end + + end +end diff --git a/app/jobs/save_document_revision_job.rb b/app/jobs/save_document_revision_job.rb index d52d737fc..95fc41016 100644 --- a/app/jobs/save_document_revision_job.rb +++ b/app/jobs/save_document_revision_job.rb @@ -4,13 +4,21 @@ class SaveDocumentRevisionJob < ApplicationJob def perform(*args) document_id = args.shift - document = Document.find(document_id) - return unless document.present? + document = Document.find_by(id: document_id) + return unless document # Update cached word count for the document regardless of how often this is called new_word_count = document.computed_word_count document.update(cached_word_count: new_word_count) + # Save a WordCountUpdate for this document for today + update = document.word_count_updates.find_or_initialize_by( + for_date: DateTime.current, + ) + update.word_count = new_word_count + update.user_id ||= document.user_id + update.save! + # Make sure we're only storing revisions at least every 5 min latest_revision = document.document_revisions.order('created_at DESC').limit(1).first if latest_revision.present? && latest_revision.created_at > 5.minutes.ago diff --git a/app/models/concerns/has_page_tags.rb b/app/models/concerns/has_page_tags.rb index b1ada5cb0..cfcdb1eb9 100644 --- a/app/models/concerns/has_page_tags.rb +++ b/app/models/concerns/has_page_tags.rb @@ -4,6 +4,6 @@ module HasPageTags extend ActiveSupport::Concern included do - has_many :page_tags, as: :page + has_many :page_tags, as: :page, dependent: :destroy end end diff --git a/app/models/concerns/is_content_page.rb b/app/models/concerns/is_content_page.rb index ad0b69fd3..718797a40 100644 --- a/app/models/concerns/is_content_page.rb +++ b/app/models/concerns/is_content_page.rb @@ -18,6 +18,11 @@ module IsContentPage has_many :timeline_events, through: :timeline_event_entities has_many :timelines, -> { distinct }, through: :timeline_events + has_many :word_count_updates, as: :entity, dependent: :destroy + def latest_word_count_cache + word_count_updates.order('for_date DESC').limit(1).first.try(:word_count) || 0 + end + scope :unarchived, -> { where(archived_at: nil) } def archive! update!(archived_at: DateTime.now) diff --git a/app/models/documents/document.rb b/app/models/documents/document.rb index a28e3eb89..cbbd10279 100644 --- a/app/models/documents/document.rb +++ b/app/models/documents/document.rb @@ -26,6 +26,12 @@ class Document < ApplicationRecord attr_accessor :tagged_text + # Duplicated from is_content_page since we don't include that here yet + has_many :word_count_updates, as: :entity, dependent: :destroy + def latest_word_count_cache + word_count_updates.order('for_date DESC').limit(1).first.try(:word_count) || 0 + end + KEYS_TO_TRIGGER_REVISION_ON_CHANGE = %w(title body synopsis notes_text) def self.color diff --git a/app/models/page_collections/page_collection_submission.rb b/app/models/page_collections/page_collection_submission.rb index 0feac127e..766a02a92 100644 --- a/app/models/page_collections/page_collection_submission.rb +++ b/app/models/page_collections/page_collection_submission.rb @@ -35,7 +35,8 @@ def accept! icon: PageCollection.icon, icon_color: PageCollection.color, happened_at: DateTime.current, - passthrough_link: Rails.application.routes.url_helpers.page_collection_path(page_collection) + passthrough_link: Rails.application.routes.url_helpers.page_collection_path(page_collection), + reference_code: 'collection-submission-accepted' ) end @@ -79,7 +80,8 @@ def create_submission_notification icon: PageCollection.icon, icon_color: PageCollection.color, happened_at: DateTime.current, - passthrough_link: Rails.application.routes.url_helpers.page_collection_pending_submissions_path(page_collection) + passthrough_link: Rails.application.routes.url_helpers.page_collection_pending_submissions_path(page_collection), + reference_code: 'collection-has-submission' ) end end diff --git a/app/models/page_data/attribute.rb b/app/models/page_data/attribute.rb index cdd17cb15..a08f3cc22 100644 --- a/app/models/page_data/attribute.rb +++ b/app/models/page_data/attribute.rb @@ -19,6 +19,16 @@ class Attribute < ApplicationRecord end end + after_commit do + if saved_changes.key?('value') + # Cache the updated word count on this attribute + CacheAttributeWordCountJob.perform_later(self.id) + + # Cache the updated word count on the page this attribute belongs to + CacheSumAttributeWordCountJob.perform_later(self.entity_type, self.entity_id) + end + end + after_save do entity.touch end diff --git a/app/models/serializers/content_serializer.rb b/app/models/serializers/content_serializer.rb index bddf3a068..8dfbb4287 100644 --- a/app/models/serializers/content_serializer.rb +++ b/app/models/serializers/content_serializer.rb @@ -113,13 +113,16 @@ def initialize(content) def value_for(attribute_field, content) case attribute_field.field_type when 'link' - page_links = attribute_field.attribute_values.find_by(entity_type: content.class.name, entity_id: content.id) + page_links = self.attribute_values.detect do |value| + value.entity_type == content.class.name && value.entity_id == content.id && value.attribute_field_id == attribute_field.id + end if page_links.nil? # Fall back on old relation value # We're technically doing a double lookup here (by converting response # to link code, then looking up again later) but since this is just stopgap # code to standardize links in views this should be fine for now. if attribute_field.old_column_source.present? + # raise "wee" self.raw_model.send(attribute_field.old_column_source).map { |page| "#{page.page_type}-#{page.id}" } else [] diff --git a/app/models/timelines/timeline.rb b/app/models/timelines/timeline.rb index 8efa143fd..e12d64495 100644 --- a/app/models/timelines/timeline.rb +++ b/app/models/timelines/timeline.rb @@ -8,7 +8,7 @@ class Timeline < ApplicationRecord include BelongsToUniverse include Authority::Abilities - self.authorizer_name = 'ExtendedContentAuthorizer' + self.authorizer_name = 'TimelineAuthorizer' validates :user_id, presence: true belongs_to :user diff --git a/app/models/users/user.rb b/app/models/users/user.rb index 9c7583af6..3c5528d18 100644 --- a/app/models/users/user.rb +++ b/app/models/users/user.rb @@ -122,6 +122,9 @@ def contributable_universes def contributable_universe_ids # TODO: email confirmation needs to happen for data safety / privacy (only verified emails) @contributable_universe_ids ||= Contributor.where('email = ? OR user_id = ?', self.email, self.id).pluck(:universe_id) + @contributable_universe_ids += Contributor.where(universe_id: my_universe_ids).pluck(:universe_id) + + @contributable_universe_ids.uniq end # TODO: rename this to #{content_type}_shared_with_me @@ -290,7 +293,7 @@ def update_without_password(params, *options) params.delete(:username) end - result = update_attributes(params, *options) + result = update(params, *options) clean_up_passwords result end diff --git a/app/models/word_count_update.rb b/app/models/word_count_update.rb new file mode 100644 index 000000000..1f26dccce --- /dev/null +++ b/app/models/word_count_update.rb @@ -0,0 +1,4 @@ +class WordCountUpdate < ApplicationRecord + belongs_to :user + belongs_to :entity, polymorphic: true +end diff --git a/app/services/contributor_service.rb b/app/services/contributor_service.rb index 509ffc60c..97007a1fb 100644 --- a/app/services/contributor_service.rb +++ b/app/services/contributor_service.rb @@ -25,7 +25,8 @@ def self.invite_contributor_to_universe universe:, email: icon: Universe.icon, icon_color: Universe.color, happened_at: DateTime.current, - passthrough_link: Rails.application.routes.url_helpers.universe_path(universe) + passthrough_link: Rails.application.routes.url_helpers.universe_path(universe), + reference_code: 'invitation-to-contribute' ) if related_user.present? end end diff --git a/app/services/subscription_service.rb b/app/services/subscription_service.rb index 9d4154faf..23f1336a2 100644 --- a/app/services/subscription_service.rb +++ b/app/services/subscription_service.rb @@ -46,7 +46,8 @@ def self.add_subscription(user, plan_id) icon: 'star', icon_color: 'text-darken-3 yellow', happened_at: DateTime.current, - passthrough_link: Rails.application.routes.url_helpers.customization_content_types_path + passthrough_link: Rails.application.routes.url_helpers.customization_content_types_path, + reference_code: 'premium-activation' ) if user.reload.on_premium_plan? report_subscription_change_to_slack(user, plan_id) diff --git a/app/views/admin/notifications.html.erb b/app/views/admin/notifications.html.erb new file mode 100644 index 000000000..7f91e1b18 --- /dev/null +++ b/app/views/admin/notifications.html.erb @@ -0,0 +1,28 @@ +
We're extremely excited to be a part of a super cool project put together by our friends at Infostack.io: a gigantic bundle of high-quality writing tools for a low, super-accessible price. If you've already purchased the stack for yourself (or a friend—they make great gifts!), @@ -15,57 +15,12 @@
"I built Notebook.ai because I want to see powerful writing tools in the hands of more people. I'm excited to include 6 months of free Premium alongside a ton of other hugely-discounted writing resources just for you." --
- Andrew Brown, Notebook.ai creator +
+ - Andrew Brown, Notebook.ai creator
- If you haven't bought the Writer's Craft Super Stack yet, here's a quick overview of some of my personal favorites inside: -
-- Now that you're signed into a Notebook.ai account, you can paste the code you received from the Writer's Craft Super Stack into the box below. + Now that you're signed into a Notebook.ai account, you can paste the code you received from the Writer's Craft 3.0 into the box below. Your account will be immediately upgraded to Premium (normally $9/month) for 6 months.
- Once you've signed into a Notebook.ai account, you can paste the code you received from the Writer's Craft Super Stack into the box below. + Once you've signed into a Notebook.ai account, you can paste the code you received from the Writer's Craft 3.0 into the box below. Your account will be immediately upgraded to Premium (normally $9/month) for 6 months.