diff --git a/.env b/.env.sample similarity index 93% rename from .env rename to .env.sample index fd56f06f4..c804ae841 100644 --- a/.env +++ b/.env.sample @@ -68,3 +68,10 @@ # export DANBOORU_VERSION= # export DANBOORU_HOSTNAME= # export DANBOORU_IQDBS_SERVER= + +# +# Development Only +# + +# Start the integrated solargraph service from the compose file. Requires a rebuild when changed. +# export COMPOSE_PROFILES=solargraph diff --git a/.gitignore b/.gitignore index 57b2a6256..70f6a6529 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -.env.* +.env .bundle .yardoc config/database.yml diff --git a/.rubocop.yml b/.rubocop.yml index a1d56794b..bd46bbec6 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,8 +4,8 @@ require: AllCops: NewCops: enable Exclude: - - "bin/*" - - "node_modules/**/*" + - bin/* + - node_modules/**/* Bundler/OrderedGems: Enabled: false @@ -13,23 +13,30 @@ Bundler/OrderedGems: Layout/EmptyLineAfterGuardClause: Enabled: false -Layout/EmptyLineBetweenDefs: - Enabled: false +Layout/FirstArrayElementIndentation: + EnforcedStyle: consistent + +Layout/FirstHashElementIndentation: + EnforcedStyle: consistent Layout/LineLength: Enabled: false -Lint/InheritException: - Enabled: false - -Lint/RescueException: - Enabled: false +Lint/SymbolConversion: + EnforcedStyle: consistent Metrics/AbcSize: Enabled: false Metrics/BlockLength: - Enabled: false + AllowedMethods: + - class_methods + - concerning + - context + - create_table + - should + Exclude: + - config/routes.rb Metrics/ClassLength: Enabled: false @@ -49,15 +56,28 @@ Metrics/PerceivedComplexity: Naming/PredicateName: Enabled: false +Rails/BulkChangeTable: + Enabled: false + Rails/HasManyOrHasOneDependent: Enabled: false Rails/HttpStatus: EnforcedStyle: numeric +Rails/I18nLocaleTexts: + Enabled: false + Rails/InverseOf: Enabled: false +Rails/Output: + Exclude: + - db/*.rb + +Rails/ReversibleMigration: + Enabled: false + Rails/SkipsModelValidations: Enabled: false @@ -85,14 +105,17 @@ Style/FloatDivision: Style/FrozenStringLiteralComment: Enabled: false +Style/GuardClause: + Enabled: false + +Style/HashSyntax: + EnforcedShorthandSyntax: never + Style/IfUnlessModifier: Enabled: false -Style/MutableConstant: - Enabled: false - Style/NumericPredicate: - Enabled: false + EnforcedStyle: comparison Style/PerlBackrefs: Enabled: false @@ -100,17 +123,14 @@ Style/PerlBackrefs: Style/QuotedSymbols: Enabled: false -Style/RescueStandardError: - Enabled: false - Style/StringLiterals: - Enabled: false + EnforcedStyle: double_quotes -Style/StringLiteralsInInterpolation: - Enabled: false +Style/TrailingCommaInArguments: + EnforcedStyleForMultiline: comma -Style/SymbolArray: - Enabled: false +Style/TrailingCommaInArrayLiteral: + EnforcedStyleForMultiline: consistent_comma -Style/WordArray: - Enabled: false +Style/TrailingCommaInHashLiteral: + EnforcedStyleForMultiline: consistent_comma diff --git a/.solargraph.yml b/.solargraph.yml index 145bc915d..64b155321 100644 --- a/.solargraph.yml +++ b/.solargraph.yml @@ -1,31 +1,12 @@ ---- -include: -- "**/*.rb" -exclude: -- spec/**/* -- test/**/* -- vendor/**/* -- ".bundle/**/*" require: -- actioncable -- actionmailer -- actionpack -- actionview -- activejob -- activemodel -- activerecord -- activestorage -- activesupport -domains: [] + - actioncable + - actionmailer + - actionpack + - actionview + - activejob + - activemodel + - activerecord + - activestorage + - activesupport reporters: -- rubocop -- require_not_found -formatter: - rubocop: - cops: safe - except: [] - only: [] - extra_args: [] -require_paths: [] -plugins: [] -max_files: 5000 + - rubocop diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..8117eaea2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "solargraph.checkGemVersion": false, + "solargraph.diagnostics": true, + "solargraph.externalServer": { + "host": "localhost", + "port": 7658 + }, + "solargraph.formatting": true, + "solargraph.transport": "external", +} diff --git a/Dockerfile b/Dockerfile index 3a757a24c..b84ed677b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,5 +26,15 @@ RUN gem install bundler:2.3.12 && \ RUN wget -O /usr/bin/shoreman https://github.com/chrismytton/shoreman/raw/master/shoreman.sh \ && chmod +x /usr/bin/shoreman + +# Only setup solargraph stuff when the profile is selected +ARG COMPOSE_PROFILES +RUN if [[ $COMPOSE_PROFILES == *"solargraph"* ]]; then \ + solargraph download-core && bundle exec yard gems && solargraph bundle; \ +fi + +# Stop bin/rails console from offering autocomplete +RUN echo "IRB.conf[:USE_AUTOCOMPLETE] = false" > ~/.irbrc + WORKDIR /app CMD [ "shoreman" ] diff --git a/Gemfile b/Gemfile index 1273cbef3..674e85fe3 100644 --- a/Gemfile +++ b/Gemfile @@ -53,6 +53,12 @@ group :development, :test do gem 'puma' end +group :docker do + gem "rubocop", require: false + gem "rubocop-rails", require: false + gem "solargraph", require: false +end + group :test do gem "shoulda-context" gem "shoulda-matchers" diff --git a/Gemfile.lock b/Gemfile.lock index 02348c4f9..ba15723c4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -85,7 +85,10 @@ GEM tzinfo (~> 2.0) addressable (2.8.1) public_suffix (>= 2.0.2, < 6.0) + ast (2.4.2) + backport (1.2.0) bcrypt (3.1.18) + benchmark (0.2.0) bootsnap (1.13.0) msgpack (~> 1.2) brpoplpush-redis_script (0.1.2) @@ -118,6 +121,7 @@ GEM activesupport (>= 5.0) request_store (>= 1.0) ruby2_keywords + e2mmap (0.1.0) elasticsearch (7.17.1) elasticsearch-api (= 7.17.1) elasticsearch-transport (= 7.17.1) @@ -174,9 +178,14 @@ GEM multi_xml (>= 0.5.2) i18n (1.12.0) concurrent-ruby (~> 1.0) + jaro_winkler (1.5.4) json (2.6.2) jsonapi-renderer (0.2.2) kgio (2.11.4) + kramdown (2.4.0) + rexml + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) listen (3.7.1) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) @@ -217,6 +226,9 @@ GEM nokogiri (1.13.8) mini_portile2 (~> 2.8.0) racc (~> 1.4) + parallel (1.22.1) + parser (3.1.2.1) + ast (~> 2.4.1) pg (1.4.3) pry (0.14.1) coderay (~> 1.1) @@ -259,6 +271,7 @@ GEM rake (>= 12.2) thor (~> 1.0) zeitwerk (~> 2.5) + rainbow (3.1.1) raindrops (0.20.0) rake (13.0.6) rb-fsevent (0.11.2) @@ -267,6 +280,7 @@ GEM recaptcha (5.12.3) json redis (4.8.0) + regexp_parser (2.6.0) request_store (1.5.1) rack (>= 1.4) resolv (0.2.1) @@ -285,7 +299,26 @@ GEM mime-types (>= 1.16, < 4.0) netrc (~> 0.8) retriable (3.1.2) + reverse_markdown (2.1.1) + nokogiri rexml (3.2.5) + rubocop (1.37.0) + json (~> 2.3) + parallel (~> 1.10) + parser (>= 3.1.2.1) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.22.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 1.4.0, < 3.0) + rubocop-ast (1.23.0) + parser (>= 3.1.1.0) + rubocop-rails (2.17.0) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.33.0, < 2.0) + ruby-progressbar (1.11.0) ruby-vips (2.1.4) ffi (~> 1.12) ruby2_keywords (0.0.5) @@ -308,9 +341,25 @@ GEM simple_form (5.1.0) actionpack (>= 5.2) activemodel (>= 5.2) + solargraph (0.47.2) + backport (~> 1.2) + benchmark + bundler (>= 1.17.2) + diff-lcs (~> 1.4) + e2mmap + jaro_winkler (~> 1.5) + kramdown (~> 2.3) + kramdown-parser-gfm (~> 1.1) + parser (~> 3.0) + reverse_markdown (>= 1.0.5, < 3) + rubocop (>= 0.52) + thor (~> 1.0) + tilt (~> 2.0) + yard (~> 0.9, >= 0.9.24) streamio-ffmpeg (3.0.2) multi_json (~> 1.8) thor (1.2.1) + tilt (2.0.11) timecop (0.9.5) timeout (0.3.0) tzinfo (2.0.5) @@ -318,6 +367,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.8.2) + unicode-display_width (2.3.0) unicorn (6.1.0) kgio (~> 2.6) raindrops (~> 0.7) @@ -333,11 +383,14 @@ GEM rack-proxy (>= 0.6.1) railties (>= 5.2) semantic_range (>= 2.3.0) + webrick (1.7.0) websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) whenever (1.0.0) chronic (>= 0.6.3) + yard (0.9.28) + webrick (~> 1.7.0) zeitwerk (2.6.1) PLATFORMS @@ -377,6 +430,8 @@ DEPENDENCIES resolv responders retriable + rubocop + rubocop-rails ruby-vips sanitize shoulda-context @@ -384,6 +439,7 @@ DEPENDENCIES sidekiq sidekiq-unique-jobs simple_form + solargraph streamio-ffmpeg timecop unicorn diff --git a/README.md b/README.md index 92e7fff29..faa386d66 100644 --- a/README.md +++ b/README.md @@ -15,22 +15,30 @@ To mitigate this you can install a WSL distribution and clone the project inside #### Installation 1. Download and install the prerequisites. -2. Clone the repo with `git clone https://github.com/zwagoth/e621ng.git`. -3. `cd` into the repo. -4. Run the following commands: +1. Clone the repo with `git clone https://github.com/zwagoth/e621ng.git`. +1. `cd` into the repo. +1. Copy the sample environment file with `cp .env.sample .env`. +1. Uncomment the `COMPOSE_PROFILES` variable if you wish to use solargraph. Doesn't work on Windows without WSL. +1. Run the following commands: ``` docker-compose run -e DANBOORU_DISABLE_THROTTLES=true -e SEED_POST_COUNT=100 e621 /app/bin/setup docker-compose up ``` After running the commands once only `docker-compose up` is needed to bring up the containers. -5. This would be a good time to rewatch your favorite TV series installment, cook & have breakfast/lunch/dinner, walk the dog, clean your room, etc.
+1. This would be a good time to rewatch your favorite TV series installment, cook & have breakfast/lunch/dinner, walk the dog, clean your room, etc.
By the time you get back the install will surely have completed.1 -6. To confirm the installation worked, open the web browser of your choice and enter `http://localhost:3000` into the address bar and see if the website loads correctly. An admin account has been created automatically, the username and password are `admin` and `e621test` respectively. +1. To confirm the installation worked, open the web browser of your choice and enter `http://localhost:3000` into the address bar and see if the website loads correctly. An admin account has been created automatically, the username and password are `admin` and `e621test` respectively. Note: When gems or js packages were updated you need to execute `docker-compose build` to reflect them in the container. 1 If the install did not finish by the time an activity is complete please select another activity to avoid crippling boredom. +#### Useful docker services + +`docker-compose run --rm tests` to execute the test suite. + +`docker-compose run --rm rubocop` to run the linter. Run it against changed files only, there are too many existing violations at the moment. + #### Development Database The postgres server accepts outside connections which you can use to connect with a local client. Use `localhost:34517` to connect to a database named `danbooru2` with the user `danbooru`. Leave the password blank, anything will work. @@ -66,9 +74,3 @@ debug your Nginx configuration file. ### IQDB Service IQDB integration is delegated to the [IQDBS service](https://github.com/zwagoth/iqdbs). - -### Cropped Thumbnails - -There's optional support for cropped thumbnails. This relies on installing -`libvips-8.6` or higher and setting `Danbooru.config.enable_image_cropping?` -to true. diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index a746fc3c8..d6b120413 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -28,6 +28,8 @@ ORDER BY u1.id DESC, u2.last_logged_in_at DESC;") def update @user = User.find(params[:id]) + @user.validate_email_format = true + @user.skip_email_blank_check = true @user.update!(user_params) if @user.saved_change_to_profile_about || @user.saved_change_to_profile_artinfo ModAction.log(:user_text_change, { user_id: @user.id }) @@ -37,8 +39,6 @@ ORDER BY u1.id DESC, u2.last_logged_in_at DESC;") end @user.mark_verified! if params[:user][:verified] == 'true' @user.mark_unverified! if params[:user][:verified] == 'false' - params[:user][:is_upgrade] = true - params[:user][:skip_dmail] = true @user.promote_to!(params[:user][:level], params[:user]) old_username = @user.name diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 3827b04fe..680d1cb8f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -32,12 +32,6 @@ class ApplicationController < ActionController::Base protected - def self.rescue_with(*klasses, status: 500) - rescue_from *klasses do |exception| - render_error_page(status, exception) - end - end - def api_check if !CurrentUser.is_anonymous? && !request.get? && !request.head? throttled = CurrentUser.user.token_bucket.throttled? @@ -55,6 +49,9 @@ class ApplicationController < ActionController::Base def rescue_exception(exception) @exception = exception + # If InvalidAuthenticityToken was raised, CurrentUser isn't set so we have to do it here manually. + CurrentUser.user ||= User.anonymous + if Rails.env.test? && ENV["DEBUG"] puts "---" STDERR.puts("#{exception.class} exception thrown: #{exception.message}") @@ -120,9 +117,8 @@ class ApplicationController < ActionController::Base def render_expected_error(status, message, format: request.format.symbol) format = :html unless format.in?(%i[html json atom]) - layout = CurrentUser.user.present? ? "default" : "blank" @message = message - render "static/error", layout: layout, status: status, formats: format + render "static/error", status: status, formats: format end def render_error_page(status, exception, message: exception.message, format: request.format.symbol) @@ -132,17 +128,14 @@ class ApplicationController < ActionController::Base @backtrace = Rails.backtrace_cleaner.clean(@exception.backtrace) format = :html unless format.in?(%i[html json atom]) - # if InvalidAuthenticityToken was raised, CurrentUser isn't set so we have to use the blank layout. - layout = CurrentUser.user.present? ? "default" : "blank" - - if !CurrentUser.user&.try(:is_janitor?) && message == exception.message + if !CurrentUser.user.is_janitor? && message == exception.message @message = "An unexpected error occurred." end DanbooruLogger.log(@exception, expected: @expected) log = ExceptionLog.add(exception, CurrentUser.id, request) if !@expected @log_code = log&.code - render "static/error", layout: layout, status: status, formats: format + render "static/error", status: status, formats: format end def access_denied(exception = nil) diff --git a/app/controllers/comment_votes_controller.rb b/app/controllers/comment_votes_controller.rb index 3a0c595dc..42f86b7e2 100644 --- a/app/controllers/comment_votes_controller.rb +++ b/app/controllers/comment_votes_controller.rb @@ -8,19 +8,19 @@ class CommentVotesController < ApplicationController def create @comment = Comment.find(params[:comment_id]) @comment_vote = VoteManager.comment_vote!(comment: @comment, user: CurrentUser.user, score: params[:score]) - if @comment_vote == :need_unvote + if @comment_vote == :need_unvote && params[:no_unvote] != "true" VoteManager.comment_unvote!(comment: @comment, user: CurrentUser.user) end @comment.reload render json: {score: @comment.score, our_score: @comment_vote != :need_unvote ? @comment_vote.score : 0} - rescue CommentVote::Error, ActiveRecord::RecordInvalid => x + rescue UserVote::Error, ActiveRecord::RecordInvalid => x render_expected_error(422, x) end def destroy @comment = Comment.find(params[:comment_id]) VoteManager.comment_unvote!(comment: @comment, user: CurrentUser.user) - rescue CommentVote::Error => x + rescue UserVote::Error => x render_expected_error(422, x) end diff --git a/app/controllers/mascots_controller.rb b/app/controllers/mascots_controller.rb new file mode 100644 index 000000000..cf6691261 --- /dev/null +++ b/app/controllers/mascots_controller.rb @@ -0,0 +1,43 @@ +class MascotsController < ApplicationController + respond_to :html, :json + before_action :admin_only, except: [:index] + + def index + @mascots = Mascot.search(search_params).paginate(params[:page], limit: 75) + respond_with(@mascots) + end + + def new + @mascot = Mascot.new + end + + def create + @mascot = Mascot.create(mascot_params.merge(creator: CurrentUser.user)) + ModAction.log(:mascot_create, { id: @mascot.id }) if @mascot.valid? + respond_with(@mascot, location: mascots_path) + end + + def edit + @mascot = Mascot.find(params[:id]) + end + + def update + @mascot = Mascot.find(params[:id]) + @mascot.update(mascot_params) + ModAction.log(:mascot_update, { id: @mascot.id }) if @mascot.valid? + respond_with(@mascot, location: mascots_path) + end + + def destroy + @mascot = Mascot.find(params[:id]) + @mascot.destroy + ModAction.log(:mascot_delete, { id: @mascot.id }) + respond_with(@mascot) + end + + private + + def mascot_params + params.fetch(:mascot, {}).permit(%i[mascot_file display_name background_color artist_url artist_name safe_mode_only active]) + end +end diff --git a/app/controllers/post_versions_controller.rb b/app/controllers/post_versions_controller.rb index e80381a35..68fe5b37e 100644 --- a/app/controllers/post_versions_controller.rb +++ b/app/controllers/post_versions_controller.rb @@ -4,7 +4,7 @@ class PostVersionsController < ApplicationController respond_to :js, only: [:undo] def index - @post_versions = PostVersion.__elasticsearch__.search(PostVersion.build_query(search_params)).paginate(params[:page], :limit => params[:limit], :search_count => params[:search], includes: [:updater, post: [:versions]]) + @post_versions = PostVersion.__elasticsearch__.search(PostVersion.build_query(search_params)).paginate(params[:page], limit: params[:limit], max_count: 10_000, search_count: params[:search], includes: [:updater, post: [:versions]]) respond_with(@post_versions) end diff --git a/app/controllers/post_votes_controller.rb b/app/controllers/post_votes_controller.rb index 1804fc8cf..8e66ed511 100644 --- a/app/controllers/post_votes_controller.rb +++ b/app/controllers/post_votes_controller.rb @@ -10,14 +10,14 @@ class PostVotesController < ApplicationController VoteManager.unvote!(post: @post, user: CurrentUser.user) end render json: {score: @post.score, up: @post.up_score, down: @post.down_score, our_score: @post_vote != :need_unvote ? @post_vote.score : 0} - rescue PostVote::Error, ActiveRecord::RecordInvalid => x + rescue UserVote::Error, ActiveRecord::RecordInvalid => x render_expected_error(422, x) end def destroy @post = Post.find(params[:post_id]) VoteManager.unvote!(post: @post, user: CurrentUser.user) - rescue PostVote::Error => x + rescue UserVote::Error => x render_expected_error(422, x) end diff --git a/app/controllers/takedowns_controller.rb b/app/controllers/takedowns_controller.rb index a6a4a6267..f25a33a8b 100644 --- a/app/controllers/takedowns_controller.rb +++ b/app/controllers/takedowns_controller.rb @@ -10,6 +10,7 @@ class TakedownsController < ApplicationController def destroy @takedown = Takedown.find(params[:id]) @takedown.destroy + ModAction.log(:takedown_delete, { takedown_id: @takedown.id }) respond_with(@takedown) end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 1f325326d..891444dff 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -19,12 +19,7 @@ class UsersController < ApplicationController def index if params[:name].present? - @user = User.find_by_name(params[:name]) - if @user.nil? - raise "No user found with name: #{params[:name]}" - else - redirect_to user_path(@user) - end + redirect_to user_path(id: params[:name]) else @users = User.search(search_params).includes(:user_status).paginate(params[:page], limit: params[:limit], search_count: params[:search]) respond_with(@users) do |format| @@ -63,6 +58,7 @@ class UsersController < ApplicationController raise User::PrivilegeError.new("Signups are disabled") unless Danbooru.config.enable_signups? User.transaction do @user = User.new(user_params(:create).merge({last_ip_addr: request.remote_ip})) + @user.validate_email_format = true @user.email_verification_key = '1' if Danbooru.config.enable_email_verification? if !Danbooru.config.enable_recaptcha? || verify_recaptcha(model: @user) @user.save @@ -92,6 +88,7 @@ class UsersController < ApplicationController def update @user = User.find(CurrentUser.id) + @user.validate_email_format = true check_privilege(@user) @user.update(user_params(:update)) if @user.errors.any? diff --git a/app/decorators/mod_action_decorator.rb b/app/decorators/mod_action_decorator.rb index 4b3a9bea6..d4a128920 100644 --- a/app/decorators/mod_action_decorator.rb +++ b/app/decorators/mod_action_decorator.rb @@ -23,6 +23,8 @@ class ModActionDecorator < ApplicationDecorator ### Takedowns ### when "takedown_process" "Completed takedown ##{vals['takedown_id']}" + when "takedown_delete" + "Deleted takedown ##{vals['takedown_id']}" ### IP Ban ### when "ip_ban_create" @@ -292,6 +294,14 @@ class ModActionDecorator < ApplicationDecorator when "wiki_page_rename" "Renamed wiki page ([[#{vals['old_title']}]] → [[#{vals['new_title']}]])" + ### Mascots ### + when "mascot_create" + "Created mascot ##{vals['id']}" + when "mascot_update" + "Updated mascot ##{vals['id']}" + when "mascot_delete" + "Deleted mascot ##{vals['id']}" + when "bulk_revert" "Processed bulk revert for #{vals['constraints']} by #{user}" diff --git a/app/decorators/paginated_decorator.rb b/app/decorators/paginated_decorator.rb index c3f562531..6ce7d8e73 100644 --- a/app/decorators/paginated_decorator.rb +++ b/app/decorators/paginated_decorator.rb @@ -1,3 +1,3 @@ class PaginatedDecorator < Draper::CollectionDecorator - delegate :current_page, :total_pages, :is_first_page?, :is_last_page?, :sequential_paginator_mode, :records, :total_count + delegate :current_page, :total_pages, :is_first_page?, :is_last_page?, :sequential_paginator_mode, :max_numbered_pages, :records, :total_count end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 4fab55081..7f53b3599 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -116,14 +116,6 @@ module ApplicationHelper content_tag(:time, content || datetime, :datetime => datetime, :title => time.to_formatted_s) end - def humanized_duration(from, to) - duration = distance_of_time_in_words(from, to) - datetime = from.iso8601 + "/" + to.iso8601 - title = "#{from.strftime("%Y-%m-%d %H:%M")} to #{to.strftime("%Y-%m-%d %H:%M")}" - - raw content_tag(:time, duration, datetime: datetime, title: title) - end - def time_ago_in_words_tagged(time, compact: false) if time.past? text = time_ago_in_words(time) + " ago" diff --git a/app/helpers/pagination_helper.rb b/app/helpers/pagination_helper.rb index 0b587903d..4c7053ee0 100644 --- a/app/helpers/pagination_helper.rb +++ b/app/helpers/pagination_helper.rb @@ -3,7 +3,7 @@ module PaginationHelper html = '
' if records.respond_to?(:any?) && records.any? - if params[:page] =~ /[ab]/ && !records.is_first_page? + if !records.is_first_page? html << '
  • ' + link_to("< Previous", nav_params_for("a#{records[0].id}"), rel: "prev", id: "paginator-prev", "data-shortcut": "a left") + '
  • ' end @@ -17,7 +17,7 @@ module PaginationHelper end def use_sequential_paginator?(records) - params[:page] =~ /\A[ab]\d+\z/ || records.current_page >= Danbooru.config.max_numbered_pages + params[:page] =~ /\A[ab]\d+\z/ || records.current_page >= records.max_numbered_pages end def numbered_paginator(records, switch_to_sequential = true) @@ -36,35 +36,35 @@ module PaginationHelper if records.total_pages <= (window * 2) + 5 1.upto(records.total_pages) do |page| - html << numbered_paginator_item(page, records.current_page) + html << numbered_paginator_item(page, records) end elsif records.current_page <= window + 2 1.upto(records.current_page + window) do |page| - html << numbered_paginator_item(page, records.current_page) + html << numbered_paginator_item(page, records) end - html << numbered_paginator_item("...", records.current_page) - html << numbered_paginator_final_item(records.total_pages, records.current_page) + html << numbered_paginator_item("...", records) + html << numbered_paginator_final_item(records) elsif records.current_page >= records.total_pages - (window + 1) - html << numbered_paginator_item(1, records.current_page) - html << numbered_paginator_item("...", records.current_page) + html << numbered_paginator_item(1, records) + html << numbered_paginator_item("...", records) (records.current_page - window).upto(records.total_pages) do |page| - html << numbered_paginator_item(page, records.current_page) + html << numbered_paginator_item(page, records) end else - html << numbered_paginator_item(1, records.current_page) - html << numbered_paginator_item("...", records.current_page) + html << numbered_paginator_item(1, records) + html << numbered_paginator_item("...", records) if records.size > 0 right_window = records.current_page + window else right_window = records.current_page end (records.current_page - window).upto(right_window) do |page| - html << numbered_paginator_item(page, records.current_page) + html << numbered_paginator_item(page, records) end if records.size > 0 - html << numbered_paginator_item("...", records.current_page) - html << numbered_paginator_final_item(records.total_pages, records.current_page) + html << numbered_paginator_item("...", records) + html << numbered_paginator_final_item(records) end end @@ -78,23 +78,23 @@ module PaginationHelper html.html_safe end - def numbered_paginator_final_item(total_pages, current_page) - if total_pages <= Danbooru.config.max_numbered_pages - numbered_paginator_item(total_pages, current_page) + def numbered_paginator_final_item(records) + if records.total_pages <= records.max_numbered_pages + numbered_paginator_item(records.total_pages, records) else - numbered_paginator_item(Danbooru.config.max_numbered_pages, current_page) + numbered_paginator_item(records.max_numbered_pages, records) end end - def numbered_paginator_item(page, current_page) - return "" if page.to_i > Danbooru.config.max_numbered_pages + def numbered_paginator_item(page, records) + return "" if page.to_i > records.max_numbered_pages html = [] if page == "..." html << "
  • " html << content_tag(:i, nil, class: "fas fa-ellipsis-h") html << "
  • " - elsif page == current_page + elsif page == records.current_page html << "
  • " html << '' + page.to_s + '' html << "
  • " diff --git a/app/javascript/src/javascripts/autocompletable_input.vue b/app/javascript/src/javascripts/autocompletable_input.vue index 358bed741..994b72e05 100644 --- a/app/javascript/src/javascripts/autocompletable_input.vue +++ b/app/javascript/src/javascripts/autocompletable_input.vue @@ -22,7 +22,7 @@ export default { watch: { addToList(value) { const maxEntries = 50; - const entries = new Set([value, ...this.currentEntries()]); + const entries = new Set([value.trim(), ...this.currentEntries()]); LS.putObject(`autocomplete-${this.listId}`, [...entries].slice(0, maxEntries)); } }, diff --git a/app/javascript/src/javascripts/mascots.js b/app/javascript/src/javascripts/mascots.js index e6bbfe5b2..b56dcee10 100644 --- a/app/javascript/src/javascripts/mascots.js +++ b/app/javascript/src/javascripts/mascots.js @@ -4,28 +4,24 @@ const Mascots = { current: 0 }; -function showMascot(cur) { - const mascots = window.mascots; +function showMascot(mascot) { + $('body').css("background-image", "url(" + mascot.background_url + ")"); + $('body').css("background-color", mascot.background_color); + $('.mascotbox').css("background-image", "url(" + mascot.background_url + ")"); + $('.mascotbox').css("background-color", mascot.background_color); - const blurred = mascots[cur][0].substr(0, mascots[cur][0].lastIndexOf(".")) + "_blur" + mascots[cur][0].slice(mascots[cur][0].lastIndexOf(".")); - - $('body').css("background-image", "url(" + mascots[cur][0] + ")"); - $('body').css("background-color", mascots[cur][1]); - $('.mascotbox').css("background-image", "url(" + blurred + ")"); - $('.mascotbox').css("background-color", mascots[cur][1]); - - if (mascots[cur][2]) - $('#mascot_artist').html("Mascot by " + mascots[cur][2]); - else - $('#mascot_artist').html(" "); + const artistLink = $("").text("Mascot by ").append($("").text(mascot.artist_name).attr("href", mascot.artist_url)); + $("#mascot_artist").empty().append(artistLink); } function changeMascot() { const mascots = window.mascots; - Mascots.current += 1; - Mascots.current = Mascots.current % mascots.length; - showMascot(Mascots.current); + const availableMascotIds = Object.keys(mascots); + const currentMascotIndex = availableMascotIds.indexOf(Mascots.current); + + Mascots.current = availableMascotIds[(currentMascotIndex + 1) % availableMascotIds.length]; + showMascot(mascots[Mascots.current]); LS.put('mascot', Mascots.current); } @@ -33,10 +29,13 @@ function changeMascot() { function initMascots() { $('#change-mascot').on('click', changeMascot); const mascots = window.mascots; - Mascots.current = parseInt(LS.get("mascot")); - if (isNaN(Mascots.current) || Mascots.current < 0 || Mascots.current >= mascots.length) - Mascots.current = Math.floor(Math.random() * mascots.length); - showMascot(Mascots.current); + Mascots.current = LS.get("mascot"); + if (!mascots[Mascots.current]) { + const availableMascotIds = Object.keys(mascots); + const mascotIndex = Math.floor(Math.random() * availableMascotIds.length); + Mascots.current = availableMascotIds[mascotIndex]; + } + showMascot(mascots[Mascots.current]); } $(function () { diff --git a/app/javascript/src/styles/specific/posts.scss b/app/javascript/src/styles/specific/posts.scss index 58406c16a..e99b85cd3 100644 --- a/app/javascript/src/styles/specific/posts.scss +++ b/app/javascript/src/styles/specific/posts.scss @@ -3,28 +3,6 @@ width: 15em; // Match width to that of the } -@keyframes heartbeat { - 0% { - transform:scale(1); - } - 50% { - transform:scale(1.3); - } - 100% { - transform:scale(1); - } -} - -@keyframes sharpen { - from { - filter: blur(8px); - } - - to { - filter: none; - } -} - article.post-preview { box-sizing: border-box; height: 154px; diff --git a/app/logical/danbooru/paginator/active_record_extension.rb b/app/logical/danbooru/paginator/active_record_extension.rb index 5f4ea0829..fefd97fbc 100644 --- a/app/logical/danbooru/paginator/active_record_extension.rb +++ b/app/logical/danbooru/paginator/active_record_extension.rb @@ -45,9 +45,7 @@ module Danbooru end # taken from kaminari (https://github.com/amatsuda/kaminari) - def total_count - return option_for(:count) if option_for(:count) - + def real_count c = except(:offset, :limit, :order) c = c.reorder(nil) c = c.count diff --git a/app/logical/danbooru/paginator/base_extension.rb b/app/logical/danbooru/paginator/base_extension.rb index 0de4ae158..f6837e6c5 100644 --- a/app/logical/danbooru/paginator/base_extension.rb +++ b/app/logical/danbooru/paginator/base_extension.rb @@ -1,7 +1,7 @@ module Danbooru module Paginator module BaseExtension - def paginate_base(page, options) + def paginate_base(page, options = {}) @paginator_options = options if use_numbered_paginator?(page) @@ -16,8 +16,16 @@ module Danbooru end def validate_numbered_page(page) - return if page.to_i <= Danbooru.config.max_numbered_pages - raise Danbooru::Paginator::PaginationError, "You cannot go beyond page #{Danbooru.config.max_numbered_pages}. Please narrow your search terms." + return if page.to_i <= max_numbered_pages + raise Danbooru::Paginator::PaginationError, "You cannot go beyond page #{max_numbered_pages}. Please narrow your search terms." + end + + def max_numbered_pages + if @paginator_options[:max_count] + [Danbooru.config.max_numbered_pages, @paginator_options[:max_count] / records_per_page].min + else + Danbooru.config.max_numbered_pages + end end def use_numbered_paginator?(page) @@ -39,7 +47,8 @@ module Danbooru end def records_per_page - option_for(:limit).to_i + limit = @paginator_options.try(:[], :limit) || Danbooru.config.posts_per_page + [limit.to_i, 320].min end # When paginating large tables, we want to avoid doing an expensive count query @@ -48,24 +57,11 @@ module Danbooru # exist, then assume we're doing a search and don't override the default count # behavior. Otherwise, just return some large number so the paginator skips the # count. - def option_for(key) - case key - when :limit - limit = @paginator_options.try(:[], :limit) || Danbooru.config.posts_per_page - if limit.to_i > 320 - limit = 320 - end - limit + def total_count + return 1_000_000 if @paginator_options.key?(:search_count) && @paginator_options[:search_count].blank? + return @paginator_options[:exact_count] if @paginator_options[:exact_count] - when :count - if @paginator_options.key?(:search_count) && @paginator_options[:search_count].blank? - 1_000_000 - elsif @paginator_options[:count] - @paginator_options[:count] - else - nil - end - end + real_count end end end diff --git a/app/logical/danbooru/paginator/elasticsearch_extensions.rb b/app/logical/danbooru/paginator/elasticsearch_extensions.rb index 0ed73e184..2f145ffcf 100644 --- a/app/logical/danbooru/paginator/elasticsearch_extensions.rb +++ b/app/logical/danbooru/paginator/elasticsearch_extensions.rb @@ -8,6 +8,7 @@ module Danbooru @_current_page = options[:current_page] @_records_per_page = options[:per_page] @_total_count = options[:total] + @_max_numbered_pages = options[:max_numbered_pages] || Danbooru.config.max_numbered_pages real_array = orig_array || [] @_orig_size = real_array.size if options[:mode] == :sequential @@ -41,6 +42,10 @@ module Danbooru end end + def max_numbered_pages + @_max_numbered_pages + end + def total_pages if records_per_page > 0 (total_count.to_f / records_per_page).ceil @@ -55,10 +60,10 @@ module Danbooru attr_reader :current_page, :sequential_paginator_mode - def paginate(page, options = {}) + def paginate(page, options) paginated, mode = paginate_base(page, options) - new_opts = {mode: mode, seq_mode: sequential_paginator_mode, + new_opts = {mode: mode, seq_mode: sequential_paginator_mode, max_numbered_pages: max_numbered_pages, per_page: records_per_page, total: total_count, current_page: current_page} if options[:results] == :results PaginatedArray.new(paginated.results, new_opts) @@ -94,7 +99,7 @@ module Danbooru end def paginate_numbered(page) - search.definition.update(size: records_per_page, from: (page - 1) * records_per_page, track_total_hits: Danbooru.config.max_numbered_pages * records_per_page + 1) + search.definition.update(size: records_per_page, from: (page - 1) * records_per_page, track_total_hits: (max_numbered_pages * records_per_page) + 1) @current_page = page self @@ -105,13 +110,7 @@ module Danbooru self end - def total_count - return option_for(:count) if option_for(:count) - - response_hits_total - end - - def response_hits_total + def real_count if response['hits']['total'].respond_to?(:keys) response['hits']['total']['value'] else @@ -122,13 +121,13 @@ module Danbooru def exists? search.definition[:body]&.delete(:sort) search.definition.update(from: 0, size: 1, terminate_after: 1, sort: '_doc', _source: false, track_total_hits: false) - response_hits_total > 0 + real_count > 0 end def count_only search.definition[:body]&.delete(:sort) search.definition.update(from: 0, size: 0, sort: '_doc', _source: false, track_total_hits: true) - response_hits_total + real_count end end end diff --git a/app/logical/dummy_ticket.rb b/app/logical/dummy_ticket.rb new file mode 100644 index 000000000..d87a59100 --- /dev/null +++ b/app/logical/dummy_ticket.rb @@ -0,0 +1,18 @@ +class DummyTicket + def initialize(accused, post_id) + @ticket = Ticket.new( + id: 0, + created_at: Time.now, + updated_at: Time.now, + creator_id: User.system.id, + disp_id: 0, + status: "pending", + qtype: "user", + reason: "User ##{accused.id} (#{accused.name}) tried to reupload destroyed post ##{post_id}", + ) + end + + def notify + @ticket.push_pubsub("create") + end +end diff --git a/app/logical/file_methods.rb b/app/logical/file_methods.rb index c7fe8db58..38627b4b4 100644 --- a/app/logical/file_methods.rb +++ b/app/logical/file_methods.rb @@ -45,6 +45,26 @@ module FileMethods end end + def is_ai_generated?(file_path) + return false if !is_image? + + image = Vips::Image.new_from_file(file_path) + fetch = lambda do |key| + value = image.get(key) + value.encode("ASCII", invalid: :replace, undef: :replace).gsub("\u0000", "") + rescue Vips::Error + "" + end + + return true if fetch.call("png-comment-0-parameters").present? + return true if fetch.call("png-comment-0-Dream").present? + return true if fetch.call("exif-ifd0-Software").include?("NovelAI") || fetch.call("png-comment-2-Software").include?("NovelAI") + return true if ["exif-ifd0-ImageDescription", "exif-ifd2-UserComment", "png-comment-4-Comment"].any? { |field| fetch.call(field).include?('"sampler": "') } + exif_data = fetch.call("exif-data") + return true if ["Model hash", "OpenAI", "NovelAI"].any? { |marker| exif_data.include?(marker) } + false + end + def file_header_to_file_ext(file_path) File.open file_path do |bin| mime_type = Marcel::MimeType.for(bin) diff --git a/app/logical/file_validator.rb b/app/logical/file_validator.rb index 27292ab80..d4e134f74 100644 --- a/app/logical/file_validator.rb +++ b/app/logical/file_validator.rb @@ -6,13 +6,13 @@ class FileValidator @file_path = file_path end - def validate - validate_file_ext - validate_file_size + def validate(max_file_sizes: Danbooru.config.max_file_sizes, max_width: Danbooru.config.max_image_width, max_height: Danbooru.config.max_image_height) + validate_file_ext(max_file_sizes) + validate_file_size(max_file_sizes) validate_file_integrity validate_video_container_format validate_video_duration - validate_resolution + validate_resolution(max_width, max_height) end def validate_file_integrity @@ -21,35 +21,35 @@ class FileValidator end end - def validate_file_ext - if Danbooru.config.max_file_sizes.keys.exclude? record.file_ext - record.errors.add(:file_ext, "#{record.file_ext} is invalid (only JPEG, PNG, GIF, and WebM files are allowed") + def validate_file_ext(max_file_sizes) + if max_file_sizes.keys.exclude? record.file_ext + record.errors.add(:file_ext, "#{record.file_ext} is invalid (only #{max_file_sizes.keys.to_sentence} files are allowed") throw :abort end end - def validate_file_size + def validate_file_size(max_file_sizes) if record.file_size <= 16 record.errors.add(:file_size, "is too small") end - max_size = Danbooru.config.max_file_sizes.fetch(record.file_ext, 0) + max_size = max_file_sizes.fetch(record.file_ext, 0) if record.file_size > max_size - record.errors.add(:file_size, "is too large. Maximum allowed for this file type is #{max_size / (1024 * 1024)} MiB") + record.errors.add(:file_size, "is too large. Maximum allowed for this file type is #{ApplicationController.helpers.number_to_human_size(max_size)}") end if record.is_animated_png?(file_path) && record.file_size > Danbooru.config.max_apng_file_size - record.errors.add(:file_size, "is too large. Maximum allowed for this file type is #{Danbooru.config.max_apng_file_size / (1024*1024)} MiB") + record.errors.add(:file_size, "is too large. Maximum allowed for this file type is #{ApplicationController.helpers.number_to_human_size(Danbooru.config.max_apng_file_size)}") end end - def validate_resolution + def validate_resolution(max_width, max_height) resolution = record.image_width.to_i * record.image_height.to_i if resolution > Danbooru.config.max_image_resolution record.errors.add(:base, "image resolution is too large (resolution: #{(resolution / 1_000_000.0).round(1)} megapixels (#{record.image_width}x#{record.image_height}); max: #{Danbooru.config.max_image_resolution / 1_000_000} megapixels)") - elsif record.image_width > Danbooru.config.max_image_width - record.errors.add(:image_width, "is too large (width: #{record.image_width}; max width: #{Danbooru.config.max_image_width})") - elsif record.image_height > Danbooru.config.max_image_height - record.errors.add(:image_height, "is too large (height: #{record.image_height}; max height: #{Danbooru.config.max_image_height})") + elsif record.image_width > max_width + record.errors.add(:image_width, "is too large (width: #{record.image_width}; max width: #{max_width})") + elsif record.image_height > max_height + record.errors.add(:image_height, "is too large (height: #{record.image_height}; max height: #{max_height})") end end diff --git a/app/logical/moderator/ip_addr_search.rb b/app/logical/moderator/ip_addr_search.rb index e02fd69f5..7da4e6a28 100644 --- a/app/logical/moderator/ip_addr_search.rb +++ b/app/logical/moderator/ip_addr_search.rb @@ -13,7 +13,12 @@ module Moderator elsif params[:user_name].present? search_by_user_name(params[:user_name].split(/,/).map(&:strip), with_history) elsif params[:ip_addr].present? - search_by_ip_addr(params[:ip_addr].split(/,/).map(&:strip), with_history) + ip_addrs = params[:ip_addr].split(/,/).map(&:strip) + if params[:add_ip_mask].to_s.truthy? && ip_addrs.count == 1 && ip_addrs[0].exclude?("/") + mask = IPAddr.new(ip_addrs[0]).ipv4? ? 24 : 64 + ip_addrs[0] = "#{ip_addrs[0]}/#{mask}" + end + search_by_ip_addr(ip_addrs, with_history) else [] end diff --git a/app/logical/post_sets/base.rb b/app/logical/post_sets/base.rb index 173609868..39b9d9a17 100644 --- a/app/logical/post_sets/base.rb +++ b/app/logical/post_sets/base.rb @@ -12,14 +12,6 @@ module PostSets nil end - def unknown_post_count? - false - end - - def use_sequential_paginator? - false - end - def fill_tag_types(posts) tag_array = [] posts.each do |p| diff --git a/app/logical/post_sets/favorites.rb b/app/logical/post_sets/favorites.rb index f2138aeda..485801e99 100644 --- a/app/logical/post_sets/favorites.rb +++ b/app/logical/post_sets/favorites.rb @@ -20,7 +20,7 @@ module PostSets def posts @post_count ||= ::Post.tag_match("fav:#{@user.name} status:any").count_only @posts ||= begin - favs = ::Favorite.for_user(@user.id).includes(:post).order(created_at: :desc).paginate(page, count: @post_count, limit: @limit) + favs = ::Favorite.for_user(@user.id).includes(:post).order(created_at: :desc).paginate(page, exact_count: @post_count, limit: @limit) new_opts = {mode: :numbered, per_page: favs.records_per_page, total: @post_count, current_page: current_page} ::Danbooru::Paginator::PaginatedArray.new(favs.map {|f| f.post}, new_opts diff --git a/app/logical/post_sets/pool.rb b/app/logical/post_sets/pool.rb index 981412229..1b361f6cd 100644 --- a/app/logical/post_sets/pool.rb +++ b/app/logical/post_sets/pool.rb @@ -1,9 +1,7 @@ +require_relative "../danbooru/paginator/elasticsearch_extensions" + module PostSets class Pool < PostSets::Base - module ActiveRecordExtension - attr_accessor :total_pages, :current_page - end - attr_reader :pool, :page def initialize(pool, page = 1) @@ -25,11 +23,9 @@ module PostSets def posts @posts ||= begin - x = pool.posts(:offset => offset, :limit => limit) - x.extend(ActiveRecordExtension) - x.total_pages = total_pages - x.current_page = current_page - x + posts = pool.posts(offset: offset, limit: limit) + options = { mode: :numbered, per_page: limit, total: pool.post_count, current_page: current_page } + Danbooru::Paginator::PaginatedArray.new(posts, options) end end @@ -45,14 +41,6 @@ module PostSets @presenter ||= PostSetPresenters::Pool.new(self) end - def total_pages - (pool.post_count.to_f / limit).ceil - end - - def size - posts.size - end - def current_page [page.to_i, 1].max end diff --git a/app/logical/post_sets/pool_gallery.rb b/app/logical/post_sets/pool_gallery.rb index 8bccd853d..ea97a26b8 100644 --- a/app/logical/post_sets/pool_gallery.rb +++ b/app/logical/post_sets/pool_gallery.rb @@ -1,16 +1,9 @@ module PostSets class PoolGallery < PostSets::Base - attr_reader :page, :per_page, :pools + attr_reader :pools - def initialize(pools, page = 1, per_page = nil) + def initialize(pools) @pools = pools - @page = page - @per_page = (per_page || CurrentUser.per_page).to_i - @per_page = 200 if @per_page > 200 - end - - def current_page - [page.to_i, 1].max end def presenter diff --git a/app/logical/post_sets/post.rb b/app/logical/post_sets/post.rb index 6208585d4..7e1e1b172 100644 --- a/app/logical/post_sets/post.rb +++ b/app/logical/post_sets/post.rb @@ -66,10 +66,6 @@ module PostSets random || (Tag.has_metatag?(tag_array, :order) == "random" && !Tag.has_metatag?(tag_array, :randseed)) end - def use_sequential_paginator? - unknown_post_count? && !CurrentUser.is_privileged? - end - def posts @posts ||= begin temp = ::Post.tag_match(tag_string).paginate(page, limit: per_page, includes: [:uploader]) @@ -86,10 +82,6 @@ module PostSets _posts end - def unknown_post_count? - post_count == Danbooru.config.blank_tag_search_fast_count - end - def hide_from_crawler? !is_empty_tag? end diff --git a/app/logical/session_loader.rb b/app/logical/session_loader.rb index 0af1d599a..1b375e735 100644 --- a/app/logical/session_loader.rb +++ b/app/logical/session_loader.rb @@ -96,11 +96,9 @@ private end def authenticate_api_key(name, api_key) - CurrentUser.user = User.authenticate_api_key(name, api_key) - - if CurrentUser.user.nil? - raise AuthenticationFailure.new - end + user = User.authenticate_api_key(name, api_key) + raise AuthenticationFailure if user.nil? + CurrentUser.user = user end def load_session_user diff --git a/app/logical/storage_manager.rb b/app/logical/storage_manager.rb index 0b146b871..0b0ee375b 100644 --- a/app/logical/storage_manager.rb +++ b/app/logical/storage_manager.rb @@ -3,6 +3,7 @@ class StorageManager DEFAULT_BASE_DIR = "#{Rails.root}/public/data" IMAGE_TYPES = %i[preview large crop original] + MASCOT_PREFIX = "mascots" attr_reader :base_url, :base_dir, :hierarchical, :large_image_prefix, :protected_prefix, :base_path, :replacement_prefix @@ -185,6 +186,24 @@ class StorageManager "#{base_dir}/#{replacement_prefix}/#{subdir}#{file}" end + def store_mascot(io, mascot) + store(io, mascot_path(mascot.md5, mascot.file_ext)) + end + + def mascot_path(md5, file_ext) + file = "#{md5}.#{file_ext}" + "#{base_dir}/#{MASCOT_PREFIX}/#{file}" + end + + def mascot_url(mascot) + file = "#{mascot.md5}.#{mascot.file_ext}" + "#{base_url}#{base_path}/#{MASCOT_PREFIX}/#{file}" + end + + def delete_mascot(md5, file_ext) + delete(mascot_path(md5, file_ext)) + end + def subdir_for(md5) hierarchical ? "#{md5[0..1]}/#{md5[2..3]}/" : "" end diff --git a/app/logical/upload_service/utils.rb b/app/logical/upload_service/utils.rb index c816b9d9b..e56b4016c 100644 --- a/app/logical/upload_service/utils.rb +++ b/app/logical/upload_service/utils.rb @@ -71,6 +71,7 @@ class UploadService tags = [] tags += ["animated_gif", "animated"] if upload.is_animated_gif?(file.path) tags += ["animated_png", "animated"] if upload.is_animated_png?(file.path) + tags += ["ai_generated"] if upload.is_ai_generated?(file.path) tags.join(" ") end diff --git a/app/logical/user_deletion.rb b/app/logical/user_deletion.rb index 1171f692e..6f32cbd50 100644 --- a/app/logical/user_deletion.rb +++ b/app/logical/user_deletion.rb @@ -30,16 +30,19 @@ class UserDeletion end def clear_user_settings - user.update_columns(recent_tags: '', - favorite_tags: '', - blacklisted_tags: '', - time_zone: "Eastern Time (US & Canada)", - email: '', - email_verification_key: '1', - avatar_id: nil, - profile_about: '', - profile_artinfo: '', - custom_style: '') + user.update_columns( + recent_tags: '', + favorite_tags: '', + blacklisted_tags: '', + time_zone: "Eastern Time (US & Canada)", + email: '', + email_verification_key: '1', + avatar_id: nil, + profile_about: '', + profile_artinfo: '', + custom_style: '', + level: User::Levels::MEMBER, + ) end def reset_password diff --git a/app/logical/user_email_change.rb b/app/logical/user_email_change.rb index 1c98becc5..00fcb5a3e 100644 --- a/app/logical/user_email_change.rb +++ b/app/logical/user_email_change.rb @@ -20,6 +20,7 @@ class UserEmailChange if User.authenticate(user.name, password).nil? user.errors.add(:base, "Password was incorrect") else + user.validate_email_format = true user.email = new_email user.email_verification_key = '1' if Danbooru.config.enable_email_verification? user.save diff --git a/app/logical/user_name_validator.rb b/app/logical/user_name_validator.rb index a14633314..88130d0e4 100644 --- a/app/logical/user_name_validator.rb +++ b/app/logical/user_name_validator.rb @@ -1,6 +1,6 @@ class UserNameValidator < ActiveModel::EachValidator def validate_each(rec, attr, value) - name = value + name = value rec.errors.add(attr, "already exists") if User.find_by_name(name).present? rec.errors.add(attr, "must be 2 to 20 characters long") if !name.length.between?(2, 20) @@ -8,6 +8,7 @@ class UserNameValidator < ActiveModel::EachValidator rec.errors.add(attr, "must not begin with a special character") if name =~ /\A[_\-~']/ rec.errors.add(attr, "must not contain consecutive special characters") if name =~ /_{2}|-{2}|~{2}|'{2}/ rec.errors.add(attr, "cannot begin or end with an underscore") if name =~ /\A_|_\z/ + rec.errors.add(attr, "cannot consist of numbers only") if name =~ /\A[0-9]+\z/ rec.errors.add(attr, "cannot be the string 'me'") if name.downcase == 'me' end end diff --git a/app/logical/vote_manager.rb b/app/logical/vote_manager.rb index c882a1be2..ebb6bab77 100644 --- a/app/logical/vote_manager.rb +++ b/app/logical/vote_manager.rb @@ -4,8 +4,8 @@ class VoteManager retries = 5 score = score.to_i begin - raise PostVote::Error.new("Invalid vote") unless [1, -1].include?(score) - raise PostVote::Error.new("You do not have permission to vote") unless user.is_voter? + raise UserVote::Error.new("Invalid vote") unless [1, -1].include?(score) + raise UserVote::Error.new("You do not have permission to vote") unless user.is_voter? target_isolation = !Rails.env.test? ? { isolation: :serializable } : {} PostVote.transaction(**target_isolation) do PostVote.uncached do @@ -13,7 +13,7 @@ class VoteManager score_modifier = score old_vote = PostVote.where(user_id: user.id, post_id: post.id).first if old_vote - raise PostVote::Error.new("Vote is locked") if old_vote.score == 0 + raise UserVote::Error.new("Vote is locked") if old_vote.score == 0 if old_vote.score == score return :need_unvote else @@ -38,9 +38,9 @@ class VoteManager rescue ActiveRecord::SerializationFailure retries -= 1 retry if retries > 0 - raise PostVote::Error.new("Failed to vote, please try again later") + raise UserVote::Error.new("Failed to vote, please try again later") rescue ActiveRecord::RecordNotUnique - raise PostVote::Error.new("You have already voted for this post") + raise UserVote::Error.new("You have already voted for this post") end post.update_index @vote @@ -55,7 +55,7 @@ class VoteManager post.with_lock do vote = PostVote.where(user_id: user.id, post_id: post.id).first return unless vote - raise PostVote::Error.new "You can't remove locked votes" if vote.score == 0 && !force + raise UserVote::Error.new "You can't remove locked votes" if vote.score == 0 && !force post.votes.where(user: user).delete_all subtract_vote(post, vote) post.reload @@ -65,7 +65,7 @@ class VoteManager rescue ActiveRecord::SerializationFailure retries -= 1 retry if retries > 0 - raise PostVote::Error.new("Failed to unvote, please try again later") + raise UserVote::Error.new("Failed to unvote, please try again later") end post.update_index end @@ -93,8 +93,8 @@ class VoteManager @vote = nil score = score.to_i begin - raise CommentVote::Error.new("Invalid vote") unless [1, -1].include?(score) - raise CommentVote::Error.new("You do not have permission to vote") unless user.is_voter? + raise UserVote::Error.new("Invalid vote") unless [1, -1].include?(score) + raise UserVote::Error.new("You do not have permission to vote") unless user.is_voter? target_isolation = !Rails.env.test? ? { isolation: :serializable } : {} CommentVote.transaction(**target_isolation) do CommentVote.uncached do @@ -102,7 +102,7 @@ class VoteManager score_modifier = score old_vote = CommentVote.where(user_id: user.id, comment_id: comment.id).first if old_vote - raise CommentVote::Error.new("Vote is locked") if old_vote.score == 0 + raise UserVote::Error.new("Vote is locked") if old_vote.score == 0 if old_vote.score == score return :need_unvote else @@ -118,9 +118,9 @@ class VoteManager rescue ActiveRecord::SerializationFailure retries -= 1 retry if retries > 0 - raise PostVote::Error.new("Failed to vote, please try again later.") + raise UserVote::Error.new("Failed to vote, please try again later.") rescue ActiveRecord::RecordNotUnique - raise CommentVote::Error.new("You have already voted for this comment") + raise UserVote::Error.new("You have already voted for this comment") end @vote end @@ -132,7 +132,7 @@ class VoteManager comment.with_lock do vote = CommentVote.where(user_id: user.id, comment_id: comment.id).first return unless vote - raise CommentVote::Error.new("You can't remove locked votes") if vote.score == 0 && !force + raise UserVote::Error.new("You can't remove locked votes") if vote.score == 0 && !force CommentVote.where(user_id: user.id, comment_id: comment.id).delete_all Comment.where(id: comment.id).update_all("score = score - #{vote.score}") end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 57ca014c2..ddce2bc77 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -25,12 +25,6 @@ class ApplicationRecord < ActiveRecord::Base where.not("lower(#{qualified_column_for(attr)}) LIKE ? ESCAPE E'\\\\'", value.downcase.to_escaped_for_sql_like) end - # https://www.postgresql.org/docs/current/static/functions-matching.html#FUNCTIONS-POSIX-REGEXP - # "(?e)" means force use of ERE syntax; see sections 9.7.3.1 and 9.7.3.4. - def where_regex(attr, value) - where("#{qualified_column_for(attr)} ~ ?", "(?e)" + value) - end - def attribute_exact_matches(attribute, value, **options) return all unless value.present? @@ -312,7 +306,7 @@ class ApplicationRecord < ActiveRecord::Base define_method "#{name}=" do |value| if value.respond_to?(:to_str) - super value.to_str.scan(parse).map(&cast) + super value.to_str.scan(parse).flatten.map(&cast) elsif value.respond_to?(:to_a) super value.to_a else diff --git a/app/models/artist.rb b/app/models/artist.rb index 325230b33..026ccc304 100644 --- a/app/models/artist.rb +++ b/app/models/artist.rb @@ -182,7 +182,7 @@ class Artist < ApplicationRecord while artists.empty? && url.length > 10 u = url.sub(/\/+$/, "") + "/" u = u.to_escaped_for_sql_like.gsub(/\*/, '%') + '%' - artists += Artist.joins(:urls).where(["artists.is_active = TRUE AND artist_urls.normalized_url LIKE ? ESCAPE E'\\\\'", u]).limit(10).order("artists.name").all + artists += Artist.joins(:urls).where(["artists.is_active = TRUE AND artist_urls.normalized_url ILIKE ? ESCAPE E'\\\\'", u]).limit(10).order("artists.name").all url = File.dirname(url) + "/" break if url =~ SITE_BLACKLIST_REGEXP @@ -423,24 +423,16 @@ class Artist < ApplicationRecord end def any_name_matches(query) - if query =~ %r!\A/(.*)/\z! - where_regex(:name, $1).or(any_other_name_matches($1)).or(where_regex(:group_name, $1)) - else - normalized_name = normalize_name(query) - normalized_name = "*#{normalized_name}*" unless normalized_name.include?("*") - where_like(:name, normalized_name).or(any_other_name_like(normalized_name)).or(where_like(:group_name, normalized_name)) - end + normalized_name = normalize_name(query) + normalized_name = "*#{normalized_name}*" unless normalized_name.include?("*") + where_like(:name, normalized_name).or(any_other_name_like(normalized_name)).or(where_like(:group_name, normalized_name)) end def url_matches(query) - if query =~ %r!\A/(.*)/\z! - where(id: ArtistUrl.where_regex(:url, $1).select(:artist_id)) - elsif query.include?("*") - where(id: ArtistUrl.where_like(:url, query).select(:artist_id)) - elsif query =~ %r!\Ahttps?://!i + if query =~ %r!\Ahttps?://!i find_artists(query) else - where(id: ArtistUrl.where_like(:url, "*#{query}*").select(:artist_id)) + where(id: ArtistUrl.search(url_matches: query).select(:artist_id)) end end diff --git a/app/models/artist_url.rb b/app/models/artist_url.rb index 89c5395ae..e9db77688 100644 --- a/app/models/artist_url.rb +++ b/app/models/artist_url.rb @@ -65,12 +65,9 @@ class ArtistUrl < ApplicationRecord def self.url_attribute_matches(attr, url) if url.blank? all - elsif url =~ %r!\A/(.*)/\z! - where_regex(attr, $1) - elsif url.include?("*") - where_ilike(attr, url) else - where(attr => normalize(url)) + url = "*#{url}*" if url.exclude?("*") + where_ilike(attr, url) end end diff --git a/app/models/comment_vote.rb b/app/models/comment_vote.rb index 1468ce5b0..c2f6fcb61 100644 --- a/app/models/comment_vote.rb +++ b/app/models/comment_vote.rb @@ -1,24 +1,16 @@ -class CommentVote < ApplicationRecord - class Error < Exception; - end - - belongs_to :comment - belongs_to :user - before_validation :initialize_user, :on => :create - validates :user_id, :comment_id, :score, presence: true - # validates :user_id, uniqueness: { :scope => :comment_id, :message => "have already voted for this comment" } +class CommentVote < UserVote validate :validate_user_can_vote validate :validate_comment_can_be_voted - validates :score, inclusion: { :in => [-1, 0, 1], :message => "must be 1 or -1" } - - scope :for_user, ->(uid) {where("user_id = ?", uid)} - def self.for_comments_and_user(comment_ids, user_id) return {} unless user_id CommentVote.where(comment_id: comment_ids, user_id: user_id).index_by(&:comment_id) end + def self.model_creator_column + :creator + end + def validate_user_can_vote allowed = user.can_comment_vote_with_reason if allowed != true @@ -36,83 +28,4 @@ class CommentVote < ApplicationRecord errors.add :base, "You cannot vote on sticky comments" end end - - def is_positive? - score == 1 - end - - def is_negative? - score == -1 - end - - def is_locked? - score == 0 - end - - def initialize_user - self.user_id ||= CurrentUser.user.id - self.user_ip_addr ||= CurrentUser.ip_addr - end - - module SearchMethods - def search(params) - q = super - - if params[:comment_id].present? - q = q.where("comment_id = ?", params[:comment_id].to_i) - end - - if params[:user_name].present? - user_id = User.name_to_id(params[:user_name]) - if user_id - q = q.where('user_id = ?', user_id) - else - q = q.none - end - end - - if params[:user_id].present? - q = q.where('user_id = ?', params[:user_id].to_i) - end - - allow_complex_parameters = (params.keys & %w[comment_id user_name user_id]).any? - - if allow_complex_parameters - if params[:timeframe].present? - q = q.where("comment_votes.updated_at >= ?", params[:timeframe].to_i.days.ago) - end - - if params[:user_ip_addr].present? - q = q.where("user_ip_addr <<= ?", params[:user_ip_addr]) - end - - if params[:score].present? - q = q.where("comment_votes.score = ?", params[:score]) - end - - if params[:comment_creator_name].present? - comment_creator_id = User.name_to_id(params[:comment_creator_name]) - if comment_creator_id - q = q.joins(:comment).where("comments.creator_id = ?", comment_creator_id) - else - q = q.none - end - end - - if params[:duplicates_only] == "1" - subselect = CommentVote.search(params.except("duplicates_only")).select(:user_ip_addr).group(:user_ip_addr).having("count(user_ip_addr) > 1").reorder("") - q = q.where(user_ip_addr: subselect) - end - end - - if params[:order] == "ip_addr" && allow_complex_parameters - q = q.order(:user_ip_addr) - else - q = q.apply_default_order(params) - end - q - end - end - - extend SearchMethods end diff --git a/app/models/mascot.rb b/app/models/mascot.rb new file mode 100644 index 000000000..73b716431 --- /dev/null +++ b/app/models/mascot.rb @@ -0,0 +1,89 @@ +class Mascot < ApplicationRecord + belongs_to_creator + + attr_accessor :mascot_file + + validates :display_name, :background_color, :artist_url, :artist_name, presence: true + validates :artist_url, format: { with: %r{\Ahttps?://}, message: "must start with http:// or https://" } + validates :mascot_file, presence: true, on: :create + validate :set_file_properties + validates :md5, uniqueness: true + validate if: :mascot_file do |mascot| + max_file_sizes = { "jpg" => 500.kilobytes, "png" => 500.kilobytes } + FileValidator.new(mascot, mascot_file.path).validate(max_file_sizes: max_file_sizes, max_width: 1_000, max_height: 1_000) + end + + after_commit :invalidate_cache + after_save_commit :write_storage_file + after_destroy_commit :remove_storage_file + + def set_file_properties + return if mascot_file.blank? + + self.file_ext = file_header_to_file_ext(mascot_file.path) + self.md5 = Digest::MD5.file(mascot_file.path).hexdigest + end + + def write_storage_file + return if mascot_file.blank? + + Danbooru.config.storage_manager.delete_mascot(md5_previously_was, file_ext_previously_was) + Danbooru.config.storage_manager.store_mascot(mascot_file, self) + end + + def self.active_for_browser + Cache.get("active_mascots", 1.day) do + query = Mascot.where(active: true) + query = query.where(safe_mode_only: false) if !Danbooru.config.safe_mode? + mascots = query.map do |mascot| + mascot.slice(:id, :background_color, :artist_url, :artist_name).merge(background_url: mascot.url_path) + end + mascots.index_by { |mascot| mascot["id"] } + end + end + + def invalidate_cache + Cache.delete("active_mascots") + end + + def remove_storage_file + Danbooru.config.storage_manager.delete_mascot(md5, file_ext) + end + + def url_path + Danbooru.config.storage_manager.mascot_url(self) + end + + def file_path + Danbooru.config.storage_manager.mascot_path(self) + end + + concerning :ValidationMethods do + def dimensions + @dimensions ||= calculate_dimensions(mascot_file.path) + end + + def image_width + dimensions[0] + end + + def image_height + dimensions[1] + end + + def file_size + @file_size ||= Danbooru.config.storage_manager.open(mascot_file.path).size + end + end + + def self.search(params) + q = super + q.order("lower(artist_name)") + end + + def method_attributes + super + [:url_path] + end + + include FileMethods +end diff --git a/app/models/mod_action.rb b/app/models/mod_action.rb index 22189f181..6cac11c26 100644 --- a/app/models/mod_action.rb +++ b/app/models/mod_action.rb @@ -39,6 +39,9 @@ class ModAction < ApplicationRecord :help_update, :ip_ban_create, :ip_ban_delete, + :mascot_create, + :mascot_update, + :mascot_delete, :pool_delete, :report_reason_create, :report_reason_delete, @@ -76,7 +79,8 @@ class ModAction < ApplicationRecord :mass_update, :nuke_tag, - :takedown_process + :takedown_delete, + :takedown_process, ] def self.search(params) diff --git a/app/models/news_update.rb b/app/models/news_update.rb index 6912b7eba..869566806 100644 --- a/app/models/news_update.rb +++ b/app/models/news_update.rb @@ -6,12 +6,12 @@ class NewsUpdate < ApplicationRecord after_destroy :invalidate_cache def self.recent - @recent_news ||= Cache.get('recent_news', 1.day) do - self.order('id desc').first(1) + Cache.get("recent_news_v2", 1.day) do + order("id desc").first end end def invalidate_cache - Cache.delete('recent_news') + Cache.delete("recent_news_v2") end end diff --git a/app/models/pool.rb b/app/models/pool.rb index cc25887c2..20b5bf42c 100644 --- a/app/models/pool.rb +++ b/app/models/pool.rb @@ -2,7 +2,7 @@ class Pool < ApplicationRecord class RevertError < Exception; end - array_attribute :post_ids, parse: /\d+/, cast: :to_i + array_attribute :post_ids, parse: %r{(?:https://(?:e621|e926)\.net/posts/)?(\d+)}i, cast: :to_i belongs_to_creator validates :name, uniqueness: { case_sensitive: false, if: :name_changed? } diff --git a/app/models/post_replacement.rb b/app/models/post_replacement.rb index 5aabfb79b..81c82e356 100644 --- a/app/models/post_replacement.rb +++ b/app/models/post_replacement.rb @@ -42,6 +42,13 @@ class PostReplacement < ApplicationRecord def no_pending_duplicates return true if is_backup + + if (destroyed_post = DestroyedPost.find_by(md5: md5)) + errors.add(:base, "An unexpected errror occured") + DummyTicket.new(creator, destroyed_post.post_id).notify + return + end + post = Post.where(md5: md5).first if post self.errors.add(:md5, "duplicate of existing post ##{post.id}") diff --git a/app/models/post_set.rb b/app/models/post_set.rb index f6197cd48..aed384a1f 100644 --- a/app/models/post_set.rb +++ b/app/models/post_set.rb @@ -1,6 +1,5 @@ -# -*- encoding : utf-8 -*- class PostSet < ApplicationRecord - array_attribute :post_ids, parse: /\d+/, cast: :to_i + array_attribute :post_ids, parse: %r{(?:https://(?:e621|e926)\.net/posts/)?(\d+)}i, cast: :to_i has_many :post_set_maintainers, dependent: :destroy do def in_cooldown(user) diff --git a/app/models/post_vote.rb b/app/models/post_vote.rb index 8ba3059f6..4c00c207f 100644 --- a/app/models/post_vote.rb +++ b/app/models/post_vote.rb @@ -1,19 +1,8 @@ -class PostVote < ApplicationRecord - class Error < Exception ; end - - belongs_to :post - belongs_to :user - - after_initialize :initialize_attributes, if: :new_record? +class PostVote < UserVote validate :validate_user_can_vote - validates :post_id, :user_id, :score, presence: true - validates :score, inclusion: { :in => [1, 0, -1] } - scope :for_user, ->(uid) {where("user_id = ?", uid)} - - def initialize_attributes - self.user_id ||= CurrentUser.user.id - self.user_ip_addr ||= CurrentUser.ip_addr + def self.model_creator_column + :uploader end def validate_user_can_vote @@ -28,57 +17,4 @@ class PostVote < ApplicationRecord end true end - - module SearchMethods - def search(params) - q = super - - if params[:post_id].present? - q = q.where('post_id = ?', params[:post_id]) - end - - if params[:user_name].present? - user_id = User.name_to_id(params[:user_name]) - if user_id - q = q.where('user_id = ?', user_id) - else - q = q.none - end - end - - if params[:user_id].present? - q = q.where('user_id = ?', params[:user_id].to_i) - end - - allow_complex_parameters = (params.keys & %w[post_id user_name user_id]).any? - - if allow_complex_parameters - if params[:timeframe].present? - q = q.where("updated_at >= ?", params[:timeframe].to_i.days.ago) - end - - if params[:user_ip_addr].present? - q = q.where("user_ip_addr <<= ?", params[:user_ip_addr]) - end - - if params[:score].present? - q = q.where("score = ?", params[:score]) - end - - if params[:duplicates_only] == "1" - subselect = PostVote.search(params.except("duplicates_only")).select(:user_ip_addr).group(:user_ip_addr).having("count(user_ip_addr) > 1").reorder("") - q = q.where(user_ip_addr: subselect) - end - end - - if params[:order] == "ip_addr" && allow_complex_parameters - q = q.order(:user_ip_addr) - else - q = q.apply_default_order(params) - end - q - end - end - - extend SearchMethods end diff --git a/app/models/takedown.rb b/app/models/takedown.rb index 505b0964e..8f6976754 100644 --- a/app/models/takedown.rb +++ b/app/models/takedown.rb @@ -88,8 +88,7 @@ class Takedown < ApplicationRecord def add_posts_by_ids!(ids) added_ids = [] with_lock do - ids = ids.gsub(/(https?:\/\/)?(e621|e926)\.net\/posts\/(\d+)/i, '\3') - self.post_ids = (post_array + ids.scan(/\d+/).map(&:to_i)).uniq.join(' ') + self.post_ids = (post_array + matching_post_ids(ids)).uniq.join(' ') added_ids = self.post_array - self.post_array_was save! end @@ -107,10 +106,14 @@ class Takedown < ApplicationRecord def remove_posts_by_ids!(ids) with_lock do - self.post_ids = (post_array - ids.scan(/\d+/).map(&:to_i)).uniq.join(' ') + self.post_ids = (post_array - matching_post_ids(ids)).uniq.join(' ') save! end end + + def matching_post_ids(input) + input.scan(%r{(?:https://(?:e621|e926)\.net/posts/)?(\d+)}i).flatten.map(&:to_i).uniq + end end module PostMethods @@ -119,12 +122,12 @@ class Takedown < ApplicationRecord end def normalize_post_ids - self.post_ids = post_ids.scan(/\d+/).uniq.join(' ') + self.post_ids = matching_post_ids(post_ids).join(' ') end def normalize_deleted_post_ids - posts = post_ids.scan(/\d+/).uniq - del_posts = del_post_ids.scan(/\d+/).uniq + posts = matching_post_ids(post_ids) + del_posts = matching_post_ids(del_post_ids) del_posts = del_posts & posts # ensure that all deleted posts are also posts self.del_post_ids = del_posts.join(' ') end @@ -135,7 +138,7 @@ class Takedown < ApplicationRecord end def del_post_array - del_post_ids.scan(/\d+/).map(&:to_i) + matching_post_ids(del_post_ids) end def actual_deleted_posts @@ -143,11 +146,11 @@ class Takedown < ApplicationRecord end def post_array - post_ids.scan(/\d+/).map(&:to_i) + matching_post_ids(post_ids) end def post_array_was - post_ids_was.scan(/\d+/).map(&:to_i) + matching_post_ids(post_ids_was) end def actual_posts diff --git a/app/models/ticket.rb b/app/models/ticket.rb index e31ada179..9a7406e33 100644 --- a/app/models/ticket.rb +++ b/app/models/ticket.rb @@ -62,7 +62,7 @@ class Ticket < ApplicationRecord def can_see_details?(user) if content - content.visible?(user) + content.visible?(user) || (user.id == creator_id) else true end diff --git a/app/models/upload.rb b/app/models/upload.rb index 94b39e2c1..ba9f941ba 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -179,6 +179,12 @@ class Upload < ApplicationRecord return end + if (destroyed_post = DestroyedPost.find_by(md5: md5)) + errors.add(:base, "An unexpected errror occured") + DummyTicket.new(uploader, destroyed_post.post_id).notify + return + end + replacements = PostReplacement.pending.where(md5: md5) replacements = replacements.where.not(id: replacement_id) if replacement_id diff --git a/app/models/user.rb b/app/models/user.rb index 91c296932..07d240208 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -67,25 +67,16 @@ class User < ApplicationRecord include Danbooru::HasBitFlags has_bit_flags BOOLEAN_ATTRIBUTES, :field => "bit_prefs" - attr_accessor :password, :old_password + attr_accessor :password, :old_password, :validate_email_format, :skip_email_blank_check after_initialize :initialize_attributes, if: :new_record? - before_validation :normalize_email - if Danbooru.config.enable_email_verification? - validates :email, presence: { on: :create } - validates :email, presence: { on: :update, if: ->(rec) { rec.email_changed? } } - validates :email, uniqueness: { case_sensitive: false, on: :update, if: ->(rec) { rec.email.present? && rec.saved_change_to_email? } } - validates :email, uniqueness: { case_sensitive: false, on: :update, if: ->(rec) { rec.email.present? && rec.email_changed? } } - validates :email, uniqueness: { case_sensitive: false, on: :create } - validates :email, format: { with: /\A.+@[^ ,;@]+\.[^ ,;@]+\z/, on: :create } - validates :email, format: { with: /\A.+@[^ ,;@]+\.[^ ,;@]+\z/, on: :update, if: ->(rec) { rec.email_changed? } } - else - validates :email, uniqueness: { case_sensitive: false, on: :create, if: ->(rec) { rec.email.present?} } - end + validates :email, presence: { if: :enable_email_verification?, unless: :skip_email_blank_check } + validates :email, uniqueness: { case_sensitive: false, if: :enable_email_verification? } + validates :email, uniqueness: { case_sensitive: false, on: :create, if: ->(rec) { rec.email.present? && !Danbooru.config.enable_email_verification? } } + validates :email, format: { with: /\A.+@[^ ,;@]+\.[^ ,;@]+\z/, if: :enable_email_verification?, unless: ->(rec) { rec.email.blank? && !rec.email_changed? && rec.skip_email_blank_check } } validate :validate_email_address_allowed, on: [:create, :update], if: ->(rec) { (rec.new_record? && rec.email.present?) || (rec.email.present? && rec.email_changed?) } - validates :name, user_name: true, on: :create validates :default_image_size, inclusion: { :in => %w(large fit fitv original) } validates :per_page, inclusion: { :in => 1..320 } @@ -402,8 +393,8 @@ class User < ApplicationRecord update_attribute(:email_verification_key, nil) end - def normalize_email - self.email = nil if email.blank? + def enable_email_verification? + Danbooru.config.enable_email_verification? && validate_email_format end def validate_email_address_allowed diff --git a/app/models/user_vote.rb b/app/models/user_vote.rb new file mode 100644 index 000000000..b7fb9c871 --- /dev/null +++ b/app/models/user_vote.rb @@ -0,0 +1,101 @@ +class UserVote < ApplicationRecord + class Error < Exception; end + + self.abstract_class = true + + belongs_to :user + validates :score, inclusion: { in: [-1, 0, 1], message: "must be 1 or -1" } + after_initialize :initialize_attributes, if: :new_record? + scope :for_user, ->(uid) { where("user_id = ?", uid) } + + def self.inherited(child_class) + super + child_class.class_eval do + belongs_to model_type + end + end + + # PostVote => :post + def self.model_type + model_name.singular.delete_suffix("_vote").to_sym + end + + def initialize_attributes + self.user_id ||= CurrentUser.user.id + self.user_ip_addr ||= CurrentUser.ip_addr + end + + def is_positive? + score == 1 + end + + def is_negative? + score == -1 + end + + def is_locked? + score == 0 + end + + module SearchMethods + def search(params) + q = super + + if params["#{model_type}_id"].present? + q = q.where("#{model_type}_id = ?", params["#{model_type}_id"]) + end + + if params[:user_name].present? + user_id = User.name_to_id(params[:user_name]) + if user_id + q = q.where("user_id = ?", user_id) + else + q = q.none + end + end + + if params[:user_id].present? + q = q.where("user_id = ?", params[:user_id].to_i) + end + + allow_complex_params = (params.keys & ["#{model_type}_id", "user_name", "user_id"]).any? + + if allow_complex_params + if params[:"#{model_type}_creator_name"].present? + creator_id = User.name_to_id(params[:"#{model_type}_creator_name"]) + if creator_id + q = q.joins(model_type).where(model_type => { "#{model_creator_column}_id": creator_id }) + else + q = q.none + end + end + + if params[:timeframe].present? + q = q.where("#{table_name}.updated_at >= ?", params[:timeframe].to_i.days.ago) + end + + if params[:user_ip_addr].present? + q = q.where("user_ip_addr <<= ?", params[:user_ip_addr]) + end + + if params[:score].present? + q = q.where("#{table_name}.score = ?", params[:score]) + end + + if params[:duplicates_only] == "1" + subselect = search(params.except("duplicates_only")).select(:user_ip_addr).group(:user_ip_addr).having("count(user_ip_addr) > 1").reorder("") + q = q.where(user_ip_addr: subselect) + end + end + + if params[:order] == "ip_addr" && allow_complex_params + q = q.order(:user_ip_addr) + else + q = q.apply_default_order(params) + end + q + end + end + + extend SearchMethods +end diff --git a/app/views/comment_votes/index.html.erb b/app/views/comment_votes/index.html.erb index 456a81974..e17823136 100644 --- a/app/views/comment_votes/index.html.erb +++ b/app/views/comment_votes/index.html.erb @@ -1,74 +1 @@ -
    -
    - <%# path is a string here because of duplicate routes %> - <%= form_search(path: "comment_votes") do |f| %> - <%= f.input :user_name, label: "Voter Username", autocomplete: "user" %> - <%= f.input :comment_id, label: "Comment ID" %> -
    - <%= f.input :comment_creator_name, label: "Comment Creator Username", autocomplete: "user" %> - <%= f.input :timeframe, label: "Timeframe", include_blank: true, collection: [["Last Week", "7"], ["Last Month", "30"], ["Last Three Months", "90"], ["Last Year", "360"]] %> - <%= f.input :score, label: "Type", include_blank: true, collection: [["Upvote", "1"], ["Locked", "0"], ["Downvote", "-1"]] %> - <%= f.input :user_ip_addr, label: "IP Address" %> - <%= f.input :duplicates_only, label: "Duplicates Only", as: :boolean %> - <%= f.input :order, collection: [["Created", "id"], ["IP Address", "ip_addr"]] %> - <%= f.submit "Search" %> - <% end %> - - - - - - - - - - - - - - - - - - <% @comment_votes.each do |vote| %> - - - - - - - - - - - <% end %> - -
    IDCommentComment CreatorVoterEmailSigned UpVoteCreatedUpdatedIP
    <%= vote.id %><%= link_to vote.comment_id, comment_path(vote.comment) %><%= mod_link_to_user vote.comment.creator, :negative %><%= mod_link_to_user vote.user, :negative %><%= vote.user.email %> - "><%= time_ago_in_words(vote.user.created_at) %> ago - - <% if vote.score == 1 %>Up - <% elsif vote.score == 0 %>Locked - <% elsif vote.score == nil %>Unrecorded - <% else %>Down - <% end %>"><%= time_ago_in_words(vote.created_at) %> ago - "><%= time_ago_in_words(vote.updated_at) %> ago - <%= link_to_ip vote.user_ip_addr %>
    -
    - <%= tag.button "Select All", id: "select-all-votes" %>
    - <%= tag.button "Lock Votes", id: "lock-votes" %> Set the votes to 0, preventing the user - from voting on the image again
    - <%= tag.button "Delete Votes", id: "delete-votes" %> Remove the votes - - <%= javascript_tag nonce: true do -%> - new Danbooru.VoteManager('comment'); - <% end -%> - -
    - <%= numbered_paginator(@comment_votes) %> -
    -
    -
    - -<% content_for(:page_title) do %> - Comment Votes -<% end %> +<%= render "user_votes/common_index", type: CommentVote, votes: @comment_votes %> diff --git a/app/views/comments/_index_by_comment.html.erb b/app/views/comments/_index_by_comment.html.erb index a4e3cdae0..a3b5c8873 100644 --- a/app/views/comments/_index_by_comment.html.erb +++ b/app/views/comments/_index_by_comment.html.erb @@ -3,13 +3,7 @@ <% if comment.post.present? && (CurrentUser.is_moderator? || !comment.is_hidden?) %>
    - <%= content_tag(:div, { id: "post_#{comment.post.id}", class: ["post", *PostPresenter.preview_class(comment.post)].join(" ") }.merge(PostPresenter.data_attributes(comment.post))) do %> -
    - <% if comment.post.visible? %> - <%= link_to(image_tag(comment.post.preview_file_url), post_path(comment.post)) %> - <% end %> -
    - <% end %> + <%= PostPresenter.preview(comment.post, inline: true, show_deleted: true) %>
    <%= render "comments/partials/show/comment", comment: comment, post: nil %> diff --git a/app/views/comments/_index_by_post.html.erb b/app/views/comments/_index_by_post.html.erb index c70fdecfa..0ff0d751b 100644 --- a/app/views/comments/_index_by_post.html.erb +++ b/app/views/comments/_index_by_post.html.erb @@ -13,13 +13,7 @@ <% if CurrentUser.is_moderator? || post.comments.undeleted.exists? %>
    - <%= content_tag(:div, { id: "post_#{post.id}", class: ["post", *PostPresenter.preview_class(post)].join(" ") }.merge(PostPresenter.data_attributes(post))) do %> -
    - <% if post.visible? %> - <%= link_to(image_tag(post.preview_file_url), post_path(post)) %> - <% end %> -
    - <% end %> + <%= PostPresenter.preview(post, inline: true, show_deleted: true) %> diff --git a/app/views/comments/show.html.erb b/app/views/comments/show.html.erb index d1059c375..7aec74315 100644 --- a/app/views/comments/show.html.erb +++ b/app/views/comments/show.html.erb @@ -1,14 +1,6 @@
    - <% if @comment.post %> - <%= content_tag(:div, { id: "post_#{@comment.post_id}", class: ["post", *PostPresenter.preview_class(@comment.post)].join(" ") }.merge(PostPresenter.data_attributes(@comment.post))) do %> -
    - <% if @comment.post.visible? %> - <%= link_to(image_tag(@comment.post.preview_file_url), post_path(@comment.post)) %> - <% end %> -
    - <% end %> - <% end %> + <%= PostPresenter.preview(@comment.post, inline: true, show_deleted: true) %>
    <%= render "comments/partials/show/comment", comment: @comment, post: nil %> diff --git a/app/views/layouts/_head.html.erb b/app/views/layouts/_head.html.erb new file mode 100644 index 000000000..f94104291 --- /dev/null +++ b/app/views/layouts/_head.html.erb @@ -0,0 +1,91 @@ +<%= get_title %> + + + + + + + + + +<%= csrf_meta_tag %> +<% unless disable_mobile_mode? %> + +<% end %> + + + + + +<% if CurrentUser.user.blacklisted_tags.present? %> + + +<% end %> + + + + +<% if CurrentUser.user.custom_style.present? %> + <%= stylesheet_link_tag custom_style_users_path(md5: Digest::MD5.hexdigest(CurrentUser.user.custom_style)), media: "screen", nonce: true %> +<% end %> + +<% if flash[:notice] =~ /error/i %> + +<% end %> +<%= auto_discovery_link_tag :atom, posts_path(:format => "atom", :tags => params[:tags]) %> + +<%= javascript_include_tag "/vendor/jquery-3.5.0.min.js", nonce: true, integrity: "sha256-xNzN2a4ltkB44Mc/Jz3pT4iU1cmeR0FkXs4pru/JxaQ=", crossorigin: "anonymous" %> +<%= stylesheet_link_tag "/vendor/fontawesome/css/all.min.css", nonce: true, integrity: "sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf", crossorigin: "anonymous" %> +<%= stylesheet_pack_tag "application", nonce: true %> +<%= javascript_pack_tag "application", nonce: true, defer: false %> + +<% if Danbooru.config.twitter_site %> + +<% end %> + + + + + + +<%= raw Danbooru.config.custom_html_header_content %> +<%= yield :html_header %> diff --git a/app/views/layouts/blank.html.erb b/app/views/layouts/blank.html.erb index 23696acf8..0e0940345 100644 --- a/app/views/layouts/blank.html.erb +++ b/app/views/layouts/blank.html.erb @@ -1,25 +1,12 @@ - <%= get_title %> - - - <%= csrf_meta_tag %> - <% unless disable_mobile_mode? %> - - <% end %> - <%= auto_discovery_link_tag :atom, posts_path(:format => "atom", :tags => params[:tags]) %> - <%= raw Danbooru.config.custom_html_header_content %> - - - <%= stylesheet_pack_tag "application", nonce: true %> - <%= javascript_pack_tag "application", nonce: true %> - <%= yield :html_header %> + <%= render "layouts/head" %> - +<%= tag.body **body_attributes(CurrentUser.user) do %> <%= render "layouts/theme_include" %>
    <%= yield :layout %>
    - +<% end %> diff --git a/app/views/layouts/default.html.erb b/app/views/layouts/default.html.erb index 6d1a12f69..fc87a73df 100644 --- a/app/views/layouts/default.html.erb +++ b/app/views/layouts/default.html.erb @@ -1,91 +1,7 @@ - <%= get_title %> - - - - - - - - - - <%= csrf_meta_tag %> - <% unless disable_mobile_mode? %> - - <% end %> - - - - - <% if CurrentUser.user.blacklisted_tags.present? %> - - - <% end %> - <% if flash[:notice] =~ /error/i %> - - <% end %> - - - - - <%= auto_discovery_link_tag :atom, posts_path(:format => "atom", :tags => params[:tags]) %> - <%= javascript_include_tag "/vendor/jquery-3.5.0.min.js", nonce: true, integrity: "sha256-xNzN2a4ltkB44Mc/Jz3pT4iU1cmeR0FkXs4pru/JxaQ=", crossorigin: "anonymous" %> - <%= stylesheet_link_tag "/vendor/fontawesome/css/all.min.css", nonce: true, integrity: "sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf", crossorigin: "anonymous" %> - <%= stylesheet_pack_tag "application", nonce: true %> - <%= javascript_pack_tag "application", nonce: true, defer: false %> - <% if CurrentUser.user.custom_style.present? %> - <%= stylesheet_link_tag custom_style_users_path(md5: Digest::MD5.hexdigest(CurrentUser.user.custom_style)), media: "screen", nonce: true %> - <% end %> - <% if Danbooru.config.twitter_site %> - - <% end %> - - - <%= yield :html_header %> - <%= raw Danbooru.config.custom_html_header_content %> - - + <%= render "layouts/head" %> <%= tag.body **body_attributes(CurrentUser.user) do %> <%= render "layouts/theme_include" %> @@ -120,7 +36,7 @@
    <% end %> - <%= render "news_updates/listing" %> + <%= render "news_updates/notice", news_update: NewsUpdate.recent %> <% if CurrentUser.user.is_banned? %> <%= render "users/ban_notice" %> diff --git a/app/views/mascots/_form.html.erb b/app/views/mascots/_form.html.erb new file mode 100644 index 000000000..a83ac8c8a --- /dev/null +++ b/app/views/mascots/_form.html.erb @@ -0,0 +1,11 @@ +<%= custom_form_for(mascot) do |f| %> + <%= error_messages_for "mascot" %> + <%= f.input :mascot_file, as: :file, input_html: { accept: "image/png,image/jpeg,.png,.jpg,.jpeg" } %> + <%= f.input :display_name %> + <%= f.input :background_color %> + <%= f.input :artist_url %> + <%= f.input :artist_name %> + <%= f.input :safe_mode_only, label: "E9 Only" %> + <%= f.input :active %> + <%= f.submit %> +<% end %> diff --git a/app/views/mascots/_secondary_links.html.erb b/app/views/mascots/_secondary_links.html.erb new file mode 100644 index 000000000..f2cdb7186 --- /dev/null +++ b/app/views/mascots/_secondary_links.html.erb @@ -0,0 +1,8 @@ +<% content_for(:secondary_links) do %> + + <%= subnav_link_to "Listing", mascots_path %> + <% if CurrentUser.user.is_admin? %> + <%= subnav_link_to "New", new_mascot_path %> + <% end %> + +<% end %> diff --git a/app/views/mascots/edit.html.erb b/app/views/mascots/edit.html.erb new file mode 100644 index 000000000..9929ffe34 --- /dev/null +++ b/app/views/mascots/edit.html.erb @@ -0,0 +1,12 @@ +
    +
    +

    Edit Mascot

    + <%= render "form", mascot: @mascot %> +
    +
    + +<%= render "secondary_links" %> + +<% content_for(:page_title) do %> + Edit Mascot +<% end %> diff --git a/app/views/mascots/index.html.erb b/app/views/mascots/index.html.erb new file mode 100644 index 000000000..0cbbc701b --- /dev/null +++ b/app/views/mascots/index.html.erb @@ -0,0 +1,49 @@ +
    +
    +

    Mascots

    + + + + + + + + + + + + <% if CurrentUser.user.is_admin? %> + + <% end %> + + + + <% @mascots.each do |mascot| %> + + + + + + + + + <% if CurrentUser.user.is_admin? %> + + <% end %> + + <% end %> + +
    NameBackground ColorArtist NameArtist URLActiveE9 OnlyCreated
    <%= link_to mascot.display_name, mascot.url_path %><%= mascot.background_color %><%= mascot.artist_name %><%= mascot.artist_url %><%= mascot.active %><%= mascot.safe_mode_only %><%= compact_time mascot.created_at %> + <%= link_to "Edit", edit_mascot_path(mascot) %> + | <%= link_to "Delete", mascot_path(mascot), method: :delete, data: { confirm: "Are you sure you want to delete this mascot?" } %> +
    + + <%= numbered_paginator(@mascots) %> +
    +
    + +<%= render "secondary_links" %> + +<% content_for(:page_title) do %> + Mascots +<% end %> diff --git a/app/views/mascots/new.html.erb b/app/views/mascots/new.html.erb new file mode 100644 index 000000000..f0594aece --- /dev/null +++ b/app/views/mascots/new.html.erb @@ -0,0 +1,12 @@ +
    +
    +

    New Mascot

    + <%= render "form", mascot: @mascot %> +
    +
    + +<%= render "secondary_links" %> + +<% content_for(:page_title) do %> + New Mascot +<% end %> diff --git a/app/views/moderator/ip_addrs/_search.html.erb b/app/views/moderator/ip_addrs/_search.html.erb index 9782245d5..b01900d22 100644 --- a/app/views/moderator/ip_addrs/_search.html.erb +++ b/app/views/moderator/ip_addrs/_search.html.erb @@ -5,6 +5,9 @@ <%= f.input :user_id, label: "User IDs" %> <%= f.input :user_name, label: "User Names", autocomplete: "user" %> <%= f.input :ip_addr, label: "IP Addresses" %> - <%= f.input :with_history, label: "With History", as: :boolean %> + <% if (params.dig(:search, :ip_addr)&.split(",")&.count || 0) == 1 %> + <%= f.input :add_ip_mask, label: "Add IP Mask?", as: :boolean, hint: "/24 for IPv4, /64 for IPv6" %> + <% end %> + <%= f.input :with_history, label: "With History?", as: :boolean %> <%= f.submit "Search" %> <% end %> diff --git a/app/views/news_updates/_listing.html.erb b/app/views/news_updates/_listing.html.erb deleted file mode 100644 index 2b83a661f..000000000 --- a/app/views/news_updates/_listing.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -<% if NewsUpdate.recent.present? %> - -<% end %> diff --git a/app/views/news_updates/_listing_home.html.erb b/app/views/news_updates/_listing_home.html.erb deleted file mode 100644 index 64400e5fd..000000000 --- a/app/views/news_updates/_listing_home.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -<% if NewsUpdate.recent.present? %> -
    <%= format_text(NewsUpdate.recent[0].message) %>
    -<% end %> diff --git a/app/views/news_updates/_notice.erb b/app/views/news_updates/_notice.erb new file mode 100644 index 000000000..f46357c11 --- /dev/null +++ b/app/views/news_updates/_notice.erb @@ -0,0 +1,10 @@ +<% if news_update.present? %> + +<% end %> diff --git a/app/views/pools/show.html.erb b/app/views/pools/show.html.erb index 4bbd816c6..d14c37c83 100644 --- a/app/views/pools/show.html.erb +++ b/app/views/pools/show.html.erb @@ -16,7 +16,7 @@ <%= @post_set.presenter.post_previews_html(self) %>
    - <%= numbered_paginator(@post_set) %> + <%= numbered_paginator(@post_set.posts) %>
    diff --git a/app/views/post_versions/_listing.html.erb b/app/views/post_versions/_listing.html.erb index 634ba2e70..8be89fdeb 100644 --- a/app/views/post_versions/_listing.html.erb +++ b/app/views/post_versions/_listing.html.erb @@ -47,7 +47,7 @@
    <% elsif post_version.description_changed && post_version.version != 1%> - Cleared + Cleared <% end %>
    diff --git a/app/views/post_votes/index.html.erb b/app/views/post_votes/index.html.erb index a6de9cb47..04e4f22a9 100644 --- a/app/views/post_votes/index.html.erb +++ b/app/views/post_votes/index.html.erb @@ -1,70 +1 @@ -
    -
    - <%# path is a string here because of duplicate routes %> - <%= form_search(path: "post_votes") do |f| %> - <%= f.input :user_name, label: "Username", autocomplete: "user" %> - <%= f.input :post_id, label: "Post ID" %> -
    - <%= f.input :timeframe, label: "Timeframe", include_blank: true, collection: [["Last Week", "7"], ["Last Month", "30"], ["Last Three Months", "90"], ["Last Year", "360"]] %> - <%= f.input :score, label: "Type", include_blank: true, collection: [["Upvote", "1"], ["Locked", "0"], ["Downvote", "-1"]] %> - <%= f.input :user_ip_addr, label: "IP Address" %> - <%= f.input :duplicates_only, label: "Duplicates Only", as: :boolean %> - <%= f.input :order, collection: [["Created", "id"], ["IP Address", "ip_addr"]] %> - <%= f.submit "Search" %> - <% end %> - - - - - - - - - - - - - - - - - <% @post_votes.each do |vote| %> - - - - - - - - - - - <% end %> -
    IDPostVoterEmailSigned UpVoteCreatedUpdatedIP
    <%= vote.id %><%= link_to vote.post_id, post_path(id: vote.post_id) %><%= mod_link_to_user vote.user, :negative %><%= vote.user.email %> - "><%= time_ago_in_words(vote.user.created_at) %> ago - - <% if vote.score == 1 %>Up - <% elsif vote.score == 0 %>Locked - <% else %>Down - <% end %>"><%= time_ago_in_words(vote.created_at) %> ago - "><%= time_ago_in_words(vote.updated_at) %> ago - <%= link_to_ip vote.user_ip_addr %>
    -
    - <%= tag.button "Select All", id: "select-all-votes" %>
    - <%= tag.button "Lock Votes", id: "lock-votes" %> Set the votes to 0, preventing the user from - voting on the image again
    - <%= tag.button "Delete Votes", id: "delete-votes" %> Remove the votes - - <%= javascript_tag nonce: true do -%> - new Danbooru.VoteManager('post'); - <% end -%> - -
    - <%= numbered_paginator(@post_votes) %> -
    -
    -
    - -<% content_for(:page_title) do %> - Post Votes -<% end %> +<%= render "user_votes/common_index", type: PostVote, votes: @post_votes %> diff --git a/app/views/static/home.html.erb b/app/views/static/home.html.erb index 820620134..0dc39dce6 100644 --- a/app/views/static/home.html.erb +++ b/app/views/static/home.html.erb @@ -32,6 +32,10 @@ } .mascotbox { + z-index: 1; + overflow: hidden; + position: relative; + background-repeat:no-repeat; background-attachment:fixed; background-position:50% 0; @@ -44,6 +48,23 @@ text-shadow: 0 0 2px black, 0 0 6px black; } + .mascotbox:before { + z-index: -1; + content: ""; + position: absolute; + width: 200vw; + height: 200vh; + top: -100px; + left: -100px; + + background-image: inherit; + background-color: inherit; + background-repeat: inherit; + background-attachment: inherit; + background-position: inherit; + filter: blur(8px); + } + #searchbox { padding-bottom:5px; } @@ -66,7 +87,7 @@
    <%= javascript_tag nonce: true do -%> - var mascots = <%= Danbooru.config.mascots.to_json.html_safe %>; + var mascots = <%= Mascot.active_for_browser.to_json.html_safe %>; <% end -%> @@ -104,9 +125,9 @@ <% end %>
    - <% if NewsUpdate.recent.present? %> + <% if news_update = NewsUpdate.recent %>
    -
    <%= format_text(NewsUpdate.recent[0].message.lines.first, inline: true) %>
    +
    <%= format_text(news_update.message.lines.first, inline: true) %>
    <% end %> diff --git a/app/views/static/site_map.html.erb b/app/views/static/site_map.html.erb index 23ba89f67..85b927261 100644 --- a/app/views/static/site_map.html.erb +++ b/app/views/static/site_map.html.erb @@ -30,6 +30,7 @@
    • Tools

    • <%= link_to("News Updates", news_updates_path) %>
    • +
    • <%= link_to("Mascots", mascots_path) %>
    • <%= link_to("Source Code", Danbooru.config.source_code_url) %>
    • <%= link_to("Keyboard Shortcuts", keyboard_shortcuts_path) %>
    • <%= link_to("API Documentation", help_page_path(id: "api")) %>
    • diff --git a/app/views/user_votes/_common_index.html.erb b/app/views/user_votes/_common_index.html.erb new file mode 100644 index 000000000..60427d7f5 --- /dev/null +++ b/app/views/user_votes/_common_index.html.erb @@ -0,0 +1,70 @@ +
      +
      + <%# path is a string here because of duplicate routes %> + <%= form_search(path: type.model_name.route_key) do |f| %> + <%= f.input :user_name, label: "Voter Username", autocomplete: "user" %> + <%= f.input :"#{type.model_type}_id", label: "#{type.model_type.capitalize} ID" %> +
      + <%= f.input :"#{type.model_type}_creator_name", label: "#{type.model_type.capitalize} Creator Username", autocomplete: "user" %> + <%= f.input :timeframe, label: "Timeframe", include_blank: true, collection: [["Last Week", "7"], ["Last Month", "30"], ["Last Three Months", "90"], ["Last Year", "360"]] %> + <%= f.input :score, label: "Type", include_blank: true, collection: [["Upvote", "1"], ["Locked", "0"], ["Downvote", "-1"]] %> + <%= f.input :user_ip_addr, label: "IP Address" %> + <%= f.input :duplicates_only, label: "Duplicates Only", as: :boolean %> + <%= f.input :order, collection: [["Created", "id"], ["IP Address", "ip_addr"]] %> + <%= f.submit "Search" %> + <% end %> + + + + + + + + + + + + + + + + + <% votes.each do |vote| %> + + + + + + + + + + <% end %> + +
      ID<%= type.model_type.capitalize %><%= type.model_type.capitalize %> CreatorVoterEmailSigned UpVoteCreatedIP
      <%= vote.id %><%= link_to vote.send("#{type.model_type}_id"), vote.send(type.model_type) %><%= mod_link_to_user vote.send(type.model_type).send(type.model_creator_column), :negative %><%= mod_link_to_user vote.user, :negative %><%= vote.user.email %> + "><%= time_ago_in_words(vote.user.created_at) %> ago + + <% if vote.is_positive? %>Up + <% elsif vote.is_locked? %>Locked + <% else %>Down + <% end %>"><%= time_ago_in_words(vote.created_at) %> ago + <%= link_to_ip vote.user_ip_addr %>
      +
      + <%= tag.button "Select All", id: "select-all-votes" %>
      + <%= tag.button "Lock Votes", id: "lock-votes" %> Set the votes to 0, preventing the user + from voting on the <%= type.model_type %> again
      + <%= tag.button "Delete Votes", id: "delete-votes" %> Remove the votes + + <%= javascript_tag nonce: true do -%> + new Danbooru.VoteManager('<%= type.model_type %>'); + <% end -%> + +
      + <%= numbered_paginator(votes) %> +
      +
      +
      + +<% content_for(:page_title) do %> + <%= type.model_name.plural.titleize %> +<% end %> diff --git a/config/danbooru_default_config.rb b/config/danbooru_default_config.rb index 40b708d30..de75bf82a 100644 --- a/config/danbooru_default_config.rb +++ b/config/danbooru_default_config.rb @@ -352,8 +352,8 @@ fart' def max_file_sizes { 'jpg' => 100.megabytes, - 'gif' => 20.megabytes, 'png' => 100.megabytes, + 'gif' => 20.megabytes, 'webm' => 100.megabytes } end @@ -600,6 +600,8 @@ fart' "Previously deleted (post #%PARENT_ID%)", "Excessive same base image set", "Colored base", + "Advertisment", + "Underage artist", "", "Does not meet minimum quality standards (Artistic)", "Does not meet minimum quality standards (Resolution)", @@ -614,6 +616,7 @@ fart' "Irrelevant to site (Human only)", "Irrelevant to site (Screencap)", "Irrelevant to site (Zero pictured)", + "Irrelevant to site (AI assisted/generated)", "Irrelevant to site (%OTHER_ID%)", "", "Paysite/commercial content", @@ -783,19 +786,6 @@ fart' {zone: nil, revive_id: nil, checksum: nil} end - def mascots - [ - ["https://static1.e621.net/data/mascot_bg/esix1.jpg", "#012e56", "
      Keishinkae"], - ["https://static1.e621.net/data/mascot_bg/esix2.jpg", "#012e56", "Keishinkae"], - ["https://static1.e621.net/data/mascot_bg/raptor1.jpg", "#012e56", "darkdoomer"], - ["https://static1.e621.net/data/mascot_bg/hexerade.jpg", "#002d55", "chizi"], - ["https://static1.e621.net/data/mascot_bg/wiredhooves.jpg", "#012e56", "wiredhooves"], - ["https://static1.e621.net/data/mascot_bg/ecmajor.jpg", "#012e57", "ECMajor"], - ["https://static1.e621.net/data/mascot_bg/evalionfix.jpg", "#012e57", "evalion"], - ["https://static1.e621.net/data/mascot_bg/peacock.png", "#012e57", "Ratte"] - ] - end - # Additional video samples will be generated in these dimensions if it makes sense to do so # They will be available as additional scale options on applicable posts in the order they appear here def video_rescales diff --git a/config/routes.rb b/config/routes.rb index 079f76498..13615bf17 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -341,6 +341,7 @@ Rails.application.routes.draw do get :resend_confirmation end end + resources :mascots, only: [:index, :new, :create, :edit, :update, :destroy] options "*all", to: "application#enable_cors" diff --git a/db/migrate/20221014085948_add_mascot_table.rb b/db/migrate/20221014085948_add_mascot_table.rb new file mode 100644 index 000000000..d92b90571 --- /dev/null +++ b/db/migrate/20221014085948_add_mascot_table.rb @@ -0,0 +1,16 @@ +class AddMascotTable < ActiveRecord::Migration[6.1] + def change + create_table :mascots do |t| + t.references :creator, foreign_key: { to_table: :users }, null: false + t.string :display_name, null: false + t.string :md5, index: { unique: true }, null: false + t.string :file_ext, null: false + t.string :background_color, null: false + t.string :artist_url, null: false + t.string :artist_name, null: false + t.boolean :safe_mode_only, default: false, null: false + t.boolean :active, default: true, null: false + t.timestamps + end + end +end diff --git a/db/seeds.rb b/db/seeds.rb index fcf8e7b2f..5aefcc9a6 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -13,7 +13,7 @@ end puts "== Seeding database with sample content ==\n" # Uncomment to see detailed logs -#ActiveRecord::Base.logger = ActiveSupport::Logger.new($stdout) +# ActiveRecord::Base.logger = ActiveSupport::Logger.new($stdout) admin = User.find_or_create_by!(name: "admin") do |user| user.created_at = 2.weeks.ago @@ -41,41 +41,57 @@ ForumCategory.find_or_create_by!(id: Danbooru.config.alias_implication_forum_cat category.can_view = 0 end -unless Rails.env.test? - CurrentUser.user = admin - CurrentUser.ip_addr = "127.0.0.1" - - resources = YAML.load_file Rails.root.join("db", "seeds.yml") - url = "https://e621.net/posts.json?limit=#{ENV.fetch("SEED_POST_COUNT", 100)}&tags=id:#{resources["post_ids"].join(",")}" - response = HTTParty.get(url, { - headers: {"User-Agent" => "e621ng/seeding"} +def api_request(path) + response = HTTParty.get("https://e621.net#{path}", { + headers: { "User-Agent" => "e621ng/seeding" }, }) - json = JSON.parse(response.body) + JSON.parse(response.body) +end + +def import_posts + resources = YAML.load_file Rails.root.join("db/seeds.yml") + json = api_request("/posts.json?limit=#{ENV.fetch('SEED_POST_COUNT', 100)}&tags=id:#{resources['post_ids'].join(',')}") json["posts"].each do |post| puts post["file"]["url"] - data = Net::HTTP.get(URI(post["file"]["url"])) - file = Tempfile.new.binmode - file.write data - post["tags"].each do |category, tags| - Tag.find_or_create_by_name_list(tags.map {|tag| category + ":" + tag}) + Tag.find_or_create_by_name_list(tags.map { |tag| "#{category}:#{tag}" }) end - md5 = Digest::MD5.hexdigest(data) service = UploadService.new({ - uploader_id: CurrentUser.id, - uploader_ip_addr: CurrentUser.ip_addr, - file: file, - tag_string: post["tags"].values.flatten.join(" "), - source: post["sources"].join("\n"), - description: post["description"], - rating: post["rating"], - md5: md5, - md5_confirmation: md5 - }) + uploader: CurrentUser.user, + uploader_ip_addr: CurrentUser.ip_addr, + direct_url: post["file"]["url"], + tag_string: post["tags"].values.flatten.join(" "), + source: post["sources"].join("\n"), + description: post["description"], + rating: post["rating"], + }) service.start! end end + +def import_mascots + api_request("/mascots.json").each do |mascot| + puts mascot["url_path"] + Mascot.create!( + creator: CurrentUser.user, + mascot_file: Downloads::File.new(mascot["url_path"]).download!, + display_name: mascot["display_name"], + background_color: mascot["background_color"], + artist_url: mascot["artist_url"], + artist_name: mascot["artist_name"], + safe_mode_only: mascot["safe_mode_only"], + active: mascot["active"], + ) + end +end + +unless Rails.env.test? + CurrentUser.user = admin + CurrentUser.ip_addr = "127.0.0.1" + import_posts + import_mascots +end diff --git a/db/structure.sql b/db/structure.sql index 4b9324599..54a7a9d86 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1034,6 +1034,45 @@ CREATE SEQUENCE public.janitor_trials_id_seq ALTER SEQUENCE public.janitor_trials_id_seq OWNED BY public.janitor_trials.id; +-- +-- Name: mascots; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.mascots ( + id bigint NOT NULL, + creator_id bigint NOT NULL, + display_name character varying NOT NULL, + md5 character varying NOT NULL, + file_ext character varying NOT NULL, + background_color character varying NOT NULL, + artist_url character varying NOT NULL, + artist_name character varying NOT NULL, + safe_mode_only boolean DEFAULT false NOT NULL, + active boolean DEFAULT true NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: mascots_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.mascots_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: mascots_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.mascots_id_seq OWNED BY public.mascots.id; + + -- -- Name: mod_actions_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- @@ -2623,6 +2662,13 @@ ALTER TABLE ONLY public.ip_bans ALTER COLUMN id SET DEFAULT nextval('public.ip_b ALTER TABLE ONLY public.janitor_trials ALTER COLUMN id SET DEFAULT nextval('public.janitor_trials_id_seq'::regclass); +-- +-- Name: mascots id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.mascots ALTER COLUMN id SET DEFAULT nextval('public.mascots_id_seq'::regclass); + + -- -- Name: news_updates id; Type: DEFAULT; Schema: public; Owner: - -- @@ -3076,6 +3122,14 @@ ALTER TABLE ONLY public.janitor_trials ADD CONSTRAINT janitor_trials_pkey PRIMARY KEY (id); +-- +-- Name: mascots mascots_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.mascots + ADD CONSTRAINT mascots_pkey PRIMARY KEY (id); + + -- -- Name: mod_actions mod_actions_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -3765,6 +3819,20 @@ CREATE UNIQUE INDEX index_ip_bans_on_ip_addr ON public.ip_bans USING btree (ip_a CREATE INDEX index_janitor_trials_on_user_id ON public.janitor_trials USING btree (user_id); +-- +-- Name: index_mascots_on_creator_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_mascots_on_creator_id ON public.mascots USING btree (creator_id); + + +-- +-- Name: index_mascots_on_md5; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_mascots_on_md5 ON public.mascots USING btree (md5); + + -- -- Name: index_mod_actions_on_action; Type: INDEX; Schema: public; Owner: - -- @@ -4452,6 +4520,14 @@ ALTER TABLE ONLY public.staff_audit_logs ADD CONSTRAINT fk_rails_02329e5ef9 FOREIGN KEY (user_id) REFERENCES public.users(id); +-- +-- Name: mascots fk_rails_9901e810fa; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.mascots + ADD CONSTRAINT fk_rails_9901e810fa FOREIGN KEY (creator_id) REFERENCES public.users(id); + + -- -- Name: favorites fk_rails_a7668ef613; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4734,6 +4810,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20220316162257'), ('20220516103329'), ('20220710133556'), -('20220810131625'); +('20220810131625'), +('20221014085948'); diff --git a/docker-compose.yml b/docker-compose.yml index d9baae18a..f3f100337 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,10 @@ x-environment: &common-env services: e621: - build: ./ + build: + context: ./ + args: + COMPOSE_PROFILES: ${COMPOSE_PROFILES-} image: e621 volumes: - .:/app @@ -31,24 +34,6 @@ services: - iqdb tty: true - tests: - image: e621 - environment: - <<: *common-env - # Hide annoying output from libvips on corrupt files - VIPS_WARNING: "0" - volumes: - - .:/app - - node_modules:/app/node_modules - depends_on: - - postgres - - redis - - memcached - - elastic - entrypoint: ["docker/test_runner.sh"] - profiles: - - test - nginx: image: nginx:stable-alpine volumes: @@ -111,6 +96,45 @@ services: - post_data:/data - iqdb_data:/home/vagrant/iqdbs + # Useful for development + + tests: + image: e621 + environment: + <<: *common-env + # Hide annoying output from libvips on corrupt files + VIPS_WARNING: "0" + volumes: + - .:/app + - node_modules:/app/node_modules + depends_on: + - postgres + - redis + - memcached + - elastic + entrypoint: ["docker/test_runner.sh"] + profiles: + - tests + + rubocop: + image: e621 + volumes: + - .:/app + entrypoint: bundle exec rubocop + profiles: + - rubocop + + solargraph: + image: e621 + entrypoint: solargraph socket -h 0.0.0.0 + working_dir: $PWD + volumes: + - .:$PWD + ports: + - 7658:7658 + profiles: + - solargraph + volumes: post_data: iqdb_data: diff --git a/test/functional/admin/users_controller_test.rb b/test/functional/admin/users_controller_test.rb index 34c71b0f6..b166e9538 100644 --- a/test/functional/admin/users_controller_test.rb +++ b/test/functional/admin/users_controller_test.rb @@ -42,6 +42,26 @@ class Admin::UsersControllerTest < ActionDispatch::IntegrationTest assert_equal(50, @admin.level) end end + + context "on an user with a blank email" do + setup do + @user = create(:user, email: "") + Danbooru.config.stubs(:enable_email_verification?).returns(true) + end + + should "succeed" do + put_auth admin_user_path(@user), @mod, params: { user: { level: "20", email: "" } } + assert_redirected_to(user_path(@user)) + @user.reload + assert_equal(20, @user.level) + end + + should "prevent invalid emails" do + put_auth admin_user_path(@user), @mod, params: { user: { level: "10", email: "invalid" } } + @user.reload + assert_equal("", @user.email) + end + end end end end diff --git a/test/functional/maintenance/user/email_changes_controller_test.rb b/test/functional/maintenance/user/email_changes_controller_test.rb index c16994b23..2a2cbbd0b 100644 --- a/test/functional/maintenance/user/email_changes_controller_test.rb +++ b/test/functional/maintenance/user/email_changes_controller_test.rb @@ -5,6 +5,7 @@ module Maintenance class EmailChangesControllerTest < ActionDispatch::IntegrationTest context "in all cases" do setup do + Danbooru.config.stubs(:enable_email_verification?).returns(true) @user = create(:user, email: "bob@ogres.net") end @@ -32,6 +33,20 @@ module Maintenance assert_equal("bob@ogres.net", @user.email) end end + + should "not work with an invalid email" do + post_auth maintenance_user_email_change_path, @user, params: { email_change: { password: "password", email: "" } } + @user.reload + assert_not_equal("", @user.email) + assert_match(/Email can't be blank/, flash[:notice]) + end + + should "work with a valid email when the users current email is invalid" do + @user = create(:user, email: "") + post_auth maintenance_user_email_change_path, @user, params: { email_change: { password: "password", email: "abc@ogres.net" } } + @user.reload + assert_equal("abc@ogres.net", @user.email) + end end end end diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb index b78f29f55..1f2d198e5 100644 --- a/test/functional/users_controller_test.rb +++ b/test/functional/users_controller_test.rb @@ -12,14 +12,9 @@ class UsersControllerTest < ActionDispatch::IntegrationTest assert_response :success end - should "list all users for /users?name=" do - get users_path, params: { name: @user.name } - assert_redirected_to(@user) - end - - should "raise error for /users?name=" do - get users_path, params: { name: "nobody" } - assert_response :error + should "redirect for /users?name=" do + get users_path, params: { name: "some_username" } + assert_redirected_to(user_path(id: "some_username")) end should "list all users (with search)" do @@ -96,6 +91,43 @@ class UsersControllerTest < ActionDispatch::IntegrationTest end end end + + context "with a duplicate username" do + setup do + create(:user, name: "test123") + end + + should "prevent creation" do + assert_no_difference(-> { User.count }) do + post users_path, params: { user: { name: "TEst123", password: "xxxxx1", password_confirmation: "xxxxx1" } } + assert_match(/Name already exists/, flash[:notice]) + end + end + end + + context "with email validation" do + setup do + Danbooru.config.stubs(:enable_email_verification?).returns(true) + end + + should "reject invalid emails" do + assert_no_difference(-> { User.count }) do + post users_path, params: { user: { name: "test", password: "xxxxxx", password_confirmation: "xxxxxx" } } + assert_match(/Email can't be blank/, flash[:notice]) + post users_path, params: { user: { name: "test", password: "xxxxxx", password_confirmation: "xxxxxx", email: "invalid" } } + assert_match(/Email is invalid/, flash[:notice]) + end + end + + should "reject duplicate emails" do + create(:user, email: "valid@e621.net") + + assert_no_difference(-> { User.count }) do + post users_path, params: { user: { name: "test2", password: "xxxxxx", password_confirmation: "xxxxxx", email: "VaLid@E621.net" } } + assert_match(/Email has already been taken/, flash[:notice]) + end + end + end end context "edit action" do @@ -131,6 +163,18 @@ class UsersControllerTest < ActionDispatch::IntegrationTest assert_equal(20, @user.level) end end + + context "for an user with blank email" do + setup do + @user = create(:user, email: "") + Danbooru.config.stubs(:enable_email_verification?).returns(true) + end + + should "force them to update their email" do + put_auth user_path(@user), @user, params: { user: { comment_threshold: "-100" } } + assert_match(/Email can't be blank/, flash[:notice]) + end + end end end end diff --git a/test/unit/artist_test.rb b/test/unit/artist_test.rb index bad6ce5b0..03df87d3d 100644 --- a/test/unit/artist_test.rb +++ b/test/unit/artist_test.rb @@ -197,33 +197,33 @@ class ArtistTest < ActiveSupport::TestCase end should "search on its name should return results" do - artist = FactoryBot.create(:artist, :name => "artist") + FactoryBot.create(:artist, name: "artist") - assert_not_nil(Artist.search(:name => "artist").first) - assert_not_nil(Artist.search(:name_like => "artist").first) - assert_not_nil(Artist.search(:any_name_matches => "artist").first) - assert_not_nil(Artist.search(:any_name_matches => "/art/").first) + assert_not_nil(Artist.search(name: "artist").first) + assert_not_nil(Artist.search(name_like: "artist").first) + assert_not_nil(Artist.search(any_name_matches: "artist").first) + assert_not_nil(Artist.search(any_name_matches: "*art*").first) end should "search on other names should return matches" do - artist = FactoryBot.create(:artist, :name => "artist", :other_names_string => "aaa ccc_ddd") + FactoryBot.create(:artist, name: "artist", other_names_string: "aaa ccc_ddd") assert_nil(Artist.search(any_other_name_like: "*artist*").first) assert_not_nil(Artist.search(any_other_name_like: "*aaa*").first) assert_not_nil(Artist.search(any_other_name_like: "*ccc_ddd*").first) assert_not_nil(Artist.search(name: "artist").first) - assert_not_nil(Artist.search(:any_name_matches => "aaa").first) - assert_not_nil(Artist.search(:any_name_matches => "/a/").first) + assert_not_nil(Artist.search(any_name_matches: "aaa").first) + assert_not_nil(Artist.search(any_name_matches: "*a*").first) end should "search on group name and return matches" do - cat_or_fish = FactoryBot.create(:artist, :name => "cat_or_fish") - yuu = FactoryBot.create(:artist, :name => "yuu", :group_name => "cat_or_fish") + cat_or_fish = FactoryBot.create(:artist, name: "cat_or_fish") + FactoryBot.create(:artist, name: "yuu", group_name: "cat_or_fish") assert_equal("yuu", cat_or_fish.member_names) - assert_not_nil(Artist.search(:group_name => "cat_or_fish").first) - assert_not_nil(Artist.search(:any_name_matches => "cat_or_fish").first) - assert_not_nil(Artist.search(:any_name_matches => "/cat/").first) + assert_not_nil(Artist.search(group_name: "cat_or_fish").first) + assert_not_nil(Artist.search(any_name_matches: "cat_or_fish").first) + assert_not_nil(Artist.search(any_name_matches: "*cat*").first) end should "search on url and return matches" do @@ -231,7 +231,7 @@ class ArtistTest < ActiveSupport::TestCase assert_equal([bkub.id], Artist.search(url_matches: "bkub").map(&:id)) assert_equal([bkub.id], Artist.search(url_matches: "*bkub*").map(&:id)) - assert_equal([bkub.id], Artist.search(url_matches: "/rifyu|bkub/").map(&:id)) + assert_equal([], Artist.search(url_matches: "*rifyu*").map(&:id)) assert_equal([bkub.id], Artist.search(url_matches: "http://bkub.com/test.jpg").map(&:id)) end diff --git a/test/unit/artist_url_test.rb b/test/unit/artist_url_test.rb index f697c4257..83569a624 100644 --- a/test/unit/artist_url_test.rb +++ b/test/unit/artist_url_test.rb @@ -93,11 +93,9 @@ class ArtistUrlTest < ActiveSupport::TestCase assert_search_equals([@bkub_url], artist: { name: "bkub" }) assert_search_equals([@bkub_url], url_matches: "*bkub*") - assert_search_equals([@bkub_url], url_matches: "/^https?://bkub\.com$/") assert_search_equals([@bkub_url], normalized_url_matches: "*bkub*") - assert_search_equals([@bkub_url], normalized_url_matches: "/^https?://bkub\.com/$/") - assert_search_equals([@bkub_url], normalized_url_matches: "https://bkub.com") + assert_search_equals([@bkub_url], normalized_url_matches: "http://bkub.com") assert_search_equals([@bkub_url], url: "https://bkub.com") assert_search_equals([@bkub_url], url_eq: "https://bkub.com") diff --git a/test/unit/post_sets/pool_test.rb b/test/unit/post_sets/pool_test.rb index b77865640..8b9ff9fe1 100644 --- a/test/unit/post_sets/pool_test.rb +++ b/test/unit/post_sets/pool_test.rb @@ -34,11 +34,11 @@ module PostSets end should "know the total number of pages" do - assert_equal(3, @set.total_pages) + assert_equal(3, @set.posts.total_pages) end should "know the current page" do - assert_equal(2, @set.current_page) + assert_equal(2, @set.posts.current_page) end end diff --git a/test/unit/post_test.rb b/test/unit/post_test.rb index 280edb57a..da95c52b8 100644 --- a/test/unit/post_test.rb +++ b/test/unit/post_test.rb @@ -1988,15 +1988,6 @@ class PostTest < ActiveSupport::TestCase end context "Voting:" do - # TODO: What the heck is this about? - # should "not allow members to vote" do - # @user = FactoryBot.create(:user) - # @post = FactoryBot.create(:post) - # as_user do - # assert_raises(PostVote::Error) { VoteManager.vote!(user: @user, post: @post, score: 1) } - # end - # end - should "not allow duplicate votes" do user = FactoryBot.create(:privileged_user) post = FactoryBot.create(:post) diff --git a/test/unit/post_vote_test.rb b/test/unit/post_vote_test.rb index f2cf9b9e2..86ea4d9df 100644 --- a/test/unit/post_vote_test.rb +++ b/test/unit/post_vote_test.rb @@ -23,7 +23,7 @@ class PostVoteTest < ActiveSupport::TestCase end should "not accept any other scores" do - error = assert_raises(PostVote::Error) { VoteManager.vote!(user: @user, post: @post, score: 'xxx') } + error = assert_raises(UserVote::Error) { VoteManager.vote!(user: @user, post: @post, score: 'xxx') } assert_equal("Invalid vote", error.message) end diff --git a/test/unit/user_deletion_test.rb b/test/unit/user_deletion_test.rb index c87e3255d..ec8e1478d 100644 --- a/test/unit/user_deletion_test.rb +++ b/test/unit/user_deletion_test.rb @@ -2,11 +2,11 @@ require 'test_helper' class UserDeletionTest < ActiveSupport::TestCase setup do - Sidekiq::Testing::inline! + Sidekiq::Testing.inline! end teardown do - Sidekiq::Testing::fake! + Sidekiq::Testing.fake! end context "an invalid user deletion" do @@ -43,7 +43,7 @@ class UserDeletionTest < ActiveSupport::TestCase context "a valid user deletion" do setup do - @user = FactoryBot.create(:user, created_at: 2.weeks.ago) + @user = FactoryBot.create(:privileged_user, created_at: 2.weeks.ago) CurrentUser.user = @user CurrentUser.ip_addr = "127.0.0.1" @@ -69,6 +69,10 @@ class UserDeletionTest < ActiveSupport::TestCase assert_nil(User.authenticate(@user.name, "password")) end + should "reset the level" do + assert_equal(User::Levels::MEMBER, @user.level) + end + should "remove any favorites" do @post.reload assert_equal(0, Favorite.count)