diff --git a/app/controllers/api/v1/api_controller.rb b/app/controllers/api/v1/api_controller.rb index 424b216..79e13a9 100644 --- a/app/controllers/api/v1/api_controller.rb +++ b/app/controllers/api/v1/api_controller.rb @@ -1,66 +1,80 @@ module Api::V1 - class ApiController < ActionController::API + class ApiController < ActionController::API ##### Doorkeeper - include Doorkeeper::Rails::Helpers + include Doorkeeper::Rails::Helpers ##### Errors - rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity_response - rescue_from ActiveRecord::RecordNotDestroyed, with: :render_unprocessable_entity_response - rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_response - rescue_from ActiveRecord::RecordNotSaved, with: :render_unprocessable_entity_response - rescue_from ActiveRecord::RecordNotUnique, with: :render_unprocessable_entity_response - rescue_from Api::V1::SameFavoriteUserError, with: :render_unprocessable_entity_response - rescue_from Api::V1::FavoriteAlreadyExistsError, with: :render_unprocessable_entity_response - rescue_from Api::V1::UnauthorizedError, with: :render_unauthorized_response - rescue_from ActionController::ParameterMissing, with: :render_unprocessable_entity_response + rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity_response + rescue_from ActiveRecord::RecordNotDestroyed, with: :render_unprocessable_entity_response + rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_response + rescue_from ActiveRecord::RecordNotSaved, with: :render_unprocessable_entity_response + rescue_from ActiveRecord::RecordNotUnique, with: :render_unprocessable_entity_response + rescue_from Api::V1::SameFavoriteUserError, with: :render_unprocessable_entity_response + rescue_from Api::V1::FavoriteAlreadyExistsError, with: :render_unprocessable_entity_response + rescue_from Api::V1::NoJobProvidedError, with: :render_unprocessable_entity_response + rescue_from Api::V1::TooManySkillsOfTypeError, with: :render_unprocessable_entity_response + rescue_from Api::V1::UnauthorizedError, with: :render_unauthorized_response + rescue_from ActionController::ParameterMissing, with: :render_unprocessable_entity_response + + rescue_from GranblueError do |e| + render_error(e) + end ##### Hooks - before_action :current_user - before_action :set_default_content_type + before_action :current_user + before_action :set_default_content_type ##### Responders - respond_to :json + respond_to :json ##### Methods - # Assign the current user if the Doorkeeper token isn't nil, then - # update the current user's last seen datetime and last IP address - # before returning - def current_user - @current_user ||= User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token + # Assign the current user if the Doorkeeper token isn't nil, then + # update the current user's last seen datetime and last IP address + # before returning + def current_user + @current_user ||= User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token - return @current_user - end - - # Set the response content-type - def set_content_type(content_type) - response.headers["Content-Type"] = content_type - end - - # Set the default response content-type to application/javascript - # with a UTF-8 charset - def set_default_content_type - set_content_type("application/javascript; charset=utf-8") - end - - ### Error response methods - def render_unprocessable_entity_response(exception) - @exception = exception - render action: 'errors', status: :unprocessable_entity - end - - def render_not_found_response - response = { errors: [{ message: "Record could not be found.", code: "not_found" }]} - render 'not_found', status: :not_found - end - - def render_unauthorized_response - render action: 'errors', status: :unauthorized - end - - private - - def restrict_access - raise UnauthorizedError unless current_user - end + return @current_user end -end \ No newline at end of file + + # Set the response content-type + def set_content_type(content_type) + response.headers["Content-Type"] = content_type + end + + # Set the default response content-type to application/javascript + # with a UTF-8 charset + def set_default_content_type + set_content_type("application/javascript; charset=utf-8") + end + + ### Error response methods + def render_error(error) + if error + render action: 'errors', json: error.to_hash, status: error.http_status + else + render action: 'errors' + end + end + + def render_unprocessable_entity_response(exception) + @exception = exception + render action: 'errors', status: :unprocessable_entity + end + + def render_not_found_response + response = { errors: [{ message: "Record could not be found.", code: "not_found" }] } + render 'not_found', status: :not_found + end + + def render_unauthorized_response + render action: 'errors', status: :unauthorized + end + + private + + def restrict_access + raise UnauthorizedError unless current_user + end + end +end diff --git a/app/controllers/api/v1/job_skills_controller.rb b/app/controllers/api/v1/job_skills_controller.rb new file mode 100644 index 0000000..24d519f --- /dev/null +++ b/app/controllers/api/v1/job_skills_controller.rb @@ -0,0 +1,13 @@ +class Api::V1::JobSkillsController < Api::V1::ApiController + def all + @skills = JobSkill.all() + render :all, status: :ok + end + + def job + job = Job.find(params[:id]) + + @skills = JobSkill.where(job: job).or(JobSkill.where(sub: true)) + render :all, status: :ok + end +end diff --git a/app/controllers/api/v1/jobs_controller.rb b/app/controllers/api/v1/jobs_controller.rb index f79a40d..74d5a9a 100644 --- a/app/controllers/api/v1/jobs_controller.rb +++ b/app/controllers/api/v1/jobs_controller.rb @@ -1,6 +1,160 @@ class Api::V1::JobsController < Api::V1::ApiController - def all - @jobs = Job.all() - render :all, status: :ok + before_action :set, only: %w[update_job update_job_skills] + + def all + @jobs = Job.all() + render :all, status: :ok + end + + def update_job + raise NoJobProvidedError unless job_params[:job_id].present? + + # Extract job and find its main skills + job = Job.find(job_params[:job_id]) + main_skills = JobSkill.where(job: job.id, main: true) + + # Update the party + @party.job = job + main_skills.each_with_index do |skill, index| + @party["skill#{index}_id"] = skill.id end -end \ No newline at end of file + + # Check for incompatible Base and EMP skills + %w[skill1_id skill2_id skill3_id].each do |key| + @party[key] = nil if @party[key] && mismatched_skill(@party.job, JobSkill.find(@party[key])) + end + + render :update, status: :ok if @party.save! + end + + def update_job_skills + throw NoJobSkillProvidedError unless job_params[:skill1_id] || job_params[:skill2_id] || job_params[:skill3_id] + + # Determine which incoming keys contain new skills + skill_keys = %w[skill1_id skill2_id skill3_id] + new_skill_keys = job_params.keys.select { |key| skill_keys.include?(key) } + + # If there are new skills, merge them with the existing skills + unless new_skill_keys.empty? + existing_skills = { + 1 => @party.skill1, + 2 => @party.skill2, + 3 => @party.skill3 + } + + new_skill_ids = new_skill_keys.map { |key| job_params[key] } + new_skill_ids.map do |id| + skill = JobSkill.find(id) + raise Api::V1::IncompatibleSkillError.new(job: @party.job, skill: skill) if mismatched_skill(@party.job, skill) + end + + positions = extract_positions_from_keys(new_skill_keys) + new_skills = merge_skills_with_existing_skills(existing_skills, new_skill_ids, positions) + + new_skill_ids = new_skills.each_with_object({}) do |(index, skill), memo| + memo["skill#{index}_id"] = skill.id if skill + end + + @party.attributes = new_skill_ids + end + + render :update, status: :ok if @party.save! + end + + private + + def merge_skills_with_existing_skills( + existing_skills, + new_skill_ids, + positions + ) + new_skills = new_skill_ids.map { |id| JobSkill.find(id) } + + new_skills.each_with_index do |skill, index| + existing_skills = place_skill_in_existing_skills(existing_skills, skill, positions[index]) + end + + existing_skills + end + + def place_skill_in_existing_skills(existing_skills, skill, position) + # Test if skill will exceed allowances of skill types + skill_type = skill.sub ? 'sub' : 'emp' + + unless can_add_skill_of_type(existing_skills, position, skill_type) + raise Api::V1::TooManySkillsOfTypeError.new(skill_type: skill_type) + end + + if !existing_skills[position] + existing_skills[position] = skill + else + value = existing_skills.compact.detect { |_, value| value && value.id == skill.id } + old_position = existing_skills.key(value[1]) if value + + if old_position + existing_skills = swap_skills_at_position(existing_skills, skill, position, old_position) + else + existing_skills[position] = skill + end + end + + existing_skills + end + + def swap_skills_at_position(skills, new_skill, position1, position2) + # Check desired position for a skill + displaced_skill = skills[position1] if skills[position1].present? + + # Put skill in new position + skills[position1] = new_skill + skills[position2] = displaced_skill + + skills + end + + def extract_positions_from_keys(keys) + # Subtract by 1 because we won't operate on the 0th skill, so we don't pass it + keys.map { |key| key['skill'.length].to_i } + end + + def can_add_skill_of_type(skills, position, type) + if skills.values.compact.length.positive? + max_skill_of_type = 2 + skills_to_check = skills.compact.reject { |key, _| key == position } + + sum = skills_to_check.values.count { |value| value.send(type) } + + sum + 1 <= max_skill_of_type + else + true + end + end + + def mismatched_skill(job, skill) + mismatched_main = (skill.job.id != job.id) && skill.main && !skill.sub + mismatched_emp = (skill.job.id != job.id) && skill.emp + mismatched_base = skill.job.base_job && (job.row != 'ex2' || skill.job.base_job.id != job.base_job.id) && skill.base + + if %w[4 5 ex2].include?(job.row) + true if mismatched_emp || mismatched_base || mismatched_main + elsif mismatched_emp || mismatched_main + true + else + false + end + end + + def set + @party = Party.where('id = ?', params[:id]).first + end + + def job_params + params.require(:party).permit( + :job_id, + :skill0_id, + :skill1_id, + :skill2_id, + :skill3_id + ) + end +end diff --git a/app/controllers/api/v1/parties_controller.rb b/app/controllers/api/v1/parties_controller.rb index c25c709..bf3361e 100644 --- a/app/controllers/api/v1/parties_controller.rb +++ b/app/controllers/api/v1/parties_controller.rb @@ -1,123 +1,160 @@ class Api::V1::PartiesController < Api::V1::ApiController - before_action :set_from_slug, except: ['create', 'destroy', 'update', 'index', 'favorites'] - before_action :set, only: ['update', 'destroy'] + before_action :set_from_slug, + except: %w[create destroy update index favorites] + before_action :set, only: %w[update destroy] - def create - @party = Party.new(shortcode: random_string) - @party.extra = party_params['extra'] - - if current_user - @party.user = current_user - end + def create + @party = Party.new(shortcode: random_string) + @party.extra = party_params['extra'] - render :show, status: :created if @party.save! + job = Job.find(party_params['job_id']) if party_params['job_id'].present? + job_skills = JobSkill.where(job: job.id, main: true) + job_skills.each_with_index do |skill, index| + @party["skill#{index}_id"] = skill.id end - def show - render_not_found_response if @party.nil? + @party.user = current_user if current_user + + render :show, status: :created if @party.save! + end + + def show + render_not_found_response if @party.nil? + end + + def update + if @party.user != current_user + render_unauthorized_response + else + @party.attributes = party_params.except(:skill1_id, :skill2_id, :skill3_id) end - def index - @per_page = 15 + render :update, status: :ok if @party.save! + end - now = DateTime.current - start_time = (now - request.params['recency'].to_i.seconds).to_datetime.beginning_of_day unless request.params['recency'].blank? + def index + @per_page = 15 - conditions = {} - conditions[:element] = request.params['element'] unless request.params['element'].blank? - conditions[:raid] = request.params['raid'] unless request.params['raid'].blank? - conditions[:created_at] = start_time..now unless request.params['recency'].blank? - conditions[:weapons_count] = 5..13 + now = DateTime.current + start_time = + ( + now - request.params["recency"].to_i.seconds + ).to_datetime.beginning_of_day unless request.params["recency"].blank? - @parties = Party - .where(conditions) - .order(created_at: :desc) - .paginate(page: request.params[:page], per_page: @per_page) - .each { |party| - party.favorited = (current_user) ? party.is_favorited(current_user) : false - } - @count = Party.where(conditions).count + conditions = {} + conditions[:element] = request.params["element"] unless request.params[ + "element" + ].blank? + conditions[:raid] = request.params["raid"] unless request.params[ + "raid" + ].blank? + conditions[:created_at] = start_time..now unless request.params[ + "recency" + ].blank? + conditions[:weapons_count] = 5..13 - render :all, status: :ok + @parties = + Party + .where(conditions) + .order(created_at: :desc) + .paginate(page: request.params[:page], per_page: @per_page) + .each do |party| + party.favorited = + current_user ? party.is_favorited(current_user) : false + end + @count = Party.where(conditions).count + + render :all, status: :ok + end + + def favorites + raise Api::V1::UnauthorizedError unless current_user + + @per_page = 15 + + now = DateTime.current + start_time = + ( + now - params["recency"].to_i.seconds + ).to_datetime.beginning_of_day unless request.params["recency"].blank? + + conditions = {} + conditions[:element] = request.params["element"] unless request.params[ + "element" + ].blank? + conditions[:raid] = request.params["raid"] unless request.params[ + "raid" + ].blank? + conditions[:created_at] = start_time..now unless request.params[ + "recency" + ].blank? + conditions[:favorites] = { user_id: current_user.id } + + @parties = + Party + .joins(:favorites) + .where(conditions) + .order("favorites.created_at DESC") + .paginate(page: request.params[:page], per_page: @per_page) + .each { |party| party.favorited = party.is_favorited(current_user) } + @count = Party.joins(:favorites).where(conditions).count + + render :all, status: :ok + end + + def destroy + if @party.user != current_user + render_unauthorized_response + elsif @party.destroy + render :destroyed, status: :ok end + end - def favorites - raise Api::V1::UnauthorizedError unless current_user - - @per_page = 15 + def weapons + render_not_found_response if @party.nil? + render :weapons, status: :ok + end - now = DateTime.current - start_time = (now - params['recency'].to_i.seconds).to_datetime.beginning_of_day unless request.params['recency'].blank? + def summons + render_not_found_response if @party.nil? + render :summons, status: :ok + end - conditions = {} - conditions[:element] = request.params['element'] unless request.params['element'].blank? - conditions[:raid] = request.params['raid'] unless request.params['raid'].blank? - conditions[:created_at] = start_time..now unless request.params['recency'].blank? - conditions[:favorites] = { user_id: current_user.id } + def characters + render_not_found_response if @party.nil? + render :characters, status: :ok + end - @parties = Party - .joins(:favorites) - .where(conditions) - .order('favorites.created_at DESC') - .paginate(page: request.params[:page], per_page: @per_page) - .each { |party| - party.favorited = party.is_favorited(current_user) - } - @count = Party.joins(:favorites).where(conditions).count + private - render :all, status: :ok - end + def random_string + numChars = 6 + o = [("a".."z"), ("A".."Z"), (0..9)].map(&:to_a).flatten + return (0...numChars).map { o[rand(o.length)] }.join + end - def update - if @party.user != current_user - render_unauthorized_response - else - @party.attributes = party_params - render :update, status: :ok if @party.save! - end - end + def set_from_slug + @party = Party.where("shortcode = ?", params[:id]).first + @party.favorited = + current_user && @party ? @party.is_favorited(current_user) : false + end - def destroy - if @party.user != current_user - render_unauthorized_response - else - render :destroyed, status: :ok if @party.destroy - end - end + def set + @party = Party.where("id = ?", params[:id]).first + end - def weapons - render_not_found_response if @party.nil? - render :weapons, status: :ok - end - - def summons - render_not_found_response if @party.nil? - render :summons, status: :ok - end - - def characters - render_not_found_response if @party.nil? - render :characters, status: :ok - end - - private - - def random_string - numChars = 6 - o = [('a'..'z'), ('A'..'Z'), (0..9)].map(&:to_a).flatten - return (0...numChars).map { o[rand(o.length)] }.join - end - - def set_from_slug - @party = Party.where("shortcode = ?", params[:id]).first - @party.favorited = (current_user && @party) ? @party.is_favorited(current_user) : false - end - - def set - @party = Party.where("id = ?", params[:id]).first - end - - def party_params - params.require(:party).permit(:user_id, :extra, :name, :description, :raid_id, :job_id) - end -end \ No newline at end of file + def party_params + params.require(:party).permit( + :user_id, + :extra, + :name, + :description, + :raid_id, + :job_id, + :skill0_id, + :skill1_id, + :skill2_id, + :skill3_id + ) + end +end diff --git a/app/controllers/api/v1/search_controller.rb b/app/controllers/api/v1/search_controller.rb index f3230a7..9f42fc6 100644 --- a/app/controllers/api/v1/search_controller.rb +++ b/app/controllers/api/v1/search_controller.rb @@ -1,85 +1,139 @@ class Api::V1::SearchController < Api::V1::ApiController - def characters - filters = search_params[:filters] - locale = search_params[:locale] || 'en' - conditions = {} + def characters + filters = search_params[:filters] + locale = search_params[:locale] || 'en' + conditions = {} - if filters - conditions[:rarity] = filters['rarity'] unless filters['rarity'].blank? || filters['rarity'].empty? - conditions[:element] = filters['element'] unless filters['element'].blank? || filters['element'].empty? - conditions[:proficiency1] = filters['proficiency1'] unless filters['proficiency1'].blank? || filters['proficiency1'].empty? - conditions[:proficiency2] = filters['proficiency2'] unless filters['proficiency2'].blank? || filters['proficiency2'].empty? - # conditions[:series] = filters['series'] unless filters['series'].blank? || filters['series'].empty? - end - - if search_params[:query].present? && search_params[:query].length >= 2 - if locale == 'ja' - @characters = Character.jp_search(search_params[:query]).where(conditions) - else - @characters = Character.en_search(search_params[:query]).where(conditions) - end - else - @characters = Character.where(conditions) - end - - @count = @characters.length - @characters = @characters.paginate(page: search_params[:page], per_page: 10) + if filters + conditions[:rarity] = filters['rarity'] unless filters['rarity'].blank? || filters['rarity'].empty? + conditions[:element] = filters['element'] unless filters['element'].blank? || filters['element'].empty? + conditions[:proficiency1] = filters['proficiency1'] unless filters['proficiency1'].blank? || filters['proficiency1'].empty? + conditions[:proficiency2] = filters['proficiency2'] unless filters['proficiency2'].blank? || filters['proficiency2'].empty? + # conditions[:series] = filters['series'] unless filters['series'].blank? || filters['series'].empty? end - def weapons - filters = search_params[:filters] - locale = search_params[:locale] || 'en' - conditions = {} + @characters = if search_params[:query].present? && search_params[:query].length >= 2 + if locale == 'ja' + Character.jp_search(search_params[:query]).where(conditions) + else + Character.en_search(search_params[:query]).where(conditions) + end + else + Character.where(conditions) + end - if filters - conditions[:rarity] = filters['rarity'] unless filters['rarity'].blank? || filters['rarity'].empty? - conditions[:element] = filters['element'] unless filters['element'].blank? || filters['element'].empty? - conditions[:proficiency] = filters['proficiency1'] unless filters['proficiency1'].blank? || filters['proficiency1'].empty? - conditions[:series] = filters['series'] unless filters['series'].blank? || filters['series'].empty? - end + @count = @characters.length + @characters = @characters.paginate(page: search_params[:page], per_page: 10) + end - if search_params[:query].present? && search_params[:query].length >= 2 - if locale == 'ja' - @weapons = Weapon.jp_search(search_params[:query]).where(conditions) - else - @weapons = Weapon.en_search(search_params[:query]).where(conditions) - end - else - @weapons = Weapon.where(conditions) - end + def weapons + filters = search_params[:filters] + locale = search_params[:locale] || 'en' + conditions = {} - @count = @weapons.length - @weapons = @weapons.paginate(page: search_params[:page], per_page: 10) + if filters + conditions[:rarity] = filters['rarity'] unless filters['rarity'].blank? || filters['rarity'].empty? + conditions[:element] = filters['element'] unless filters['element'].blank? || filters['element'].empty? + conditions[:proficiency] = filters['proficiency1'] unless filters['proficiency1'].blank? || filters['proficiency1'].empty? + conditions[:series] = filters['series'] unless filters['series'].blank? || filters['series'].empty? end - def summons - filters = search_params[:filters] - locale = search_params[:locale] || 'en' - conditions = {} + @weapons = if search_params[:query].present? && search_params[:query].length >= 2 + if locale == 'ja' + Weapon.jp_search(search_params[:query]).where(conditions) + else + Weapon.en_search(search_params[:query]).where(conditions) + end + else + Weapon.where(conditions) + end - if filters - conditions[:rarity] = filters['rarity'] unless filters['rarity'].blank? || filters['rarity'].empty? - conditions[:element] = filters['element'] unless filters['element'].blank? || filters['element'].empty? - end + @count = @weapons.length + @weapons = @weapons.paginate(page: search_params[:page], per_page: 10) + end - if search_params[:query].present? && search_params[:query].length >= 2 - if locale == 'ja' - @summons = Summon.jp_search(search_params[:query]).where(conditions) - else - @summons = Summon.en_search(search_params[:query]).where(conditions) - end - else - @summons = Summon.where(conditions) - end + def summons + filters = search_params[:filters] + locale = search_params[:locale] || 'en' + conditions = {} - @count = @summons.length - @summons = @summons.paginate(page: search_params[:page], per_page: 10) + if filters + conditions[:rarity] = filters['rarity'] unless filters['rarity'].blank? || filters['rarity'].empty? + conditions[:element] = filters['element'] unless filters['element'].blank? || filters['element'].empty? end - private + @summons = if search_params[:query].present? && search_params[:query].length >= 2 + if locale == 'ja' + Summon.jp_search(search_params[:query]).where(conditions) + else + Summon.en_search(search_params[:query]).where(conditions) + end + else + Summon.where(conditions) + end - # Specify whitelisted properties that can be modified. - def search_params - params.require(:search).permit! + @count = @summons.length + @summons = @summons.paginate(page: search_params[:page], per_page: 10) + end + + def job_skills + raise Api::V1::NoJobProvidedError unless search_params[:job].present? + + # Set up basic parameters we'll use + job = Job.find(search_params[:job]) + locale = search_params[:locale] || 'en' + + # Set the conditions based on the group requested + conditions = {} + if search_params[:filters].present? && search_params[:filters]['group'].present? + group = search_params[:filters]['group'].to_i + + if group >= 0 && group < 4 + conditions[:color] = group + conditions[:emp] = false + conditions[:base] = false + elsif group == 4 + conditions[:emp] = true + elsif group == 5 + conditions[:base] = true + end end -end \ No newline at end of file + + # Perform the query + @skills = if search_params[:query].present? && search_params[:query].length >= 2 + JobSkill.method("#{locale}_search").call(search_params[:query]) + .where(conditions) + .where(job: job.id, main: false) + .or( + JobSkill.method("#{locale}_search").call(search_params[:query]) + .where(conditions) + .where(sub: true) + ) + else + JobSkill.all + .where(conditions) + .where(job: job.id, main: false) + .or( + JobSkill.all + .where(conditions) + .where(sub: true) + ) + .or( + JobSkill.all + .where(conditions) + .where(job: job.base_job.id, base: true) + .where.not(job: job.id) + ) + end + + @count = @skills.length + @skills = @skills.paginate(page: search_params[:page], per_page: 10) + end + + private + + # Specify whitelisted properties that can be modified. + def search_params + params.require(:search).permit! + end +end diff --git a/app/errors/api/v1/FavoriteAlreadyExistsError.rb b/app/errors/api/v1/FavoriteAlreadyExistsError.rb index 8c7ca73..e0ac7b7 100644 --- a/app/errors/api/v1/FavoriteAlreadyExistsError.rb +++ b/app/errors/api/v1/FavoriteAlreadyExistsError.rb @@ -1,22 +1,22 @@ module Api::V1 - class FavoriteAlreadyExistsError < StandardError - def http_status - 422 - end - - def code - "favorite_already_exists" - end - - def message - "This user has favorited this party already" - end - - def to_hash - { - message: message, - code: code - } - end + class FavoriteAlreadyExistsError < GranblueError + def http_status + 422 end + + def code + "favorite_already_exists" + end + + def message + "This user has favorited this party already" + end + + def to_hash + { + message: message, + code: code + } + end + end end diff --git a/app/errors/api/v1/GranblueError.rb b/app/errors/api/v1/GranblueError.rb new file mode 100644 index 0000000..dec43a1 --- /dev/null +++ b/app/errors/api/v1/GranblueError.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Api + module V1 + # This is the base error that we inherit from for application errors + class GranblueError < StandardError + def initialize(data) + @data = data + end + + def http_status + 422 + end + + def code + 'granblue_error' + end + + def message + 'Something went wrong' + end + + def to_hash + { + message: message, + code: code + } + end + end + end +end diff --git a/app/errors/api/v1/IncompatibleSkillError.rb b/app/errors/api/v1/IncompatibleSkillError.rb new file mode 100644 index 0000000..bd8c808 --- /dev/null +++ b/app/errors/api/v1/IncompatibleSkillError.rb @@ -0,0 +1,29 @@ +module Api::V1 + class IncompatibleSkillError < GranblueError + def initialize(data) + @data = data + end + + def http_status + 422 + end + + def code + 'incompatible_skill' + end + + def message + 'The selected skill cannot be added to the current job' + end + + def to_hash + ap @data + { + message: message, + code: code, + job: @data[:job], + skill: @data[:skill] + } + end + end +end diff --git a/app/errors/api/v1/NoJobProvidedError.rb b/app/errors/api/v1/NoJobProvidedError.rb new file mode 100644 index 0000000..c27f48a --- /dev/null +++ b/app/errors/api/v1/NoJobProvidedError.rb @@ -0,0 +1,22 @@ +module Api::V1 + class NoJobProvidedError < GranblueError + def http_status + 422 + end + + def code + "no_job_provided" + end + + def message + "A job ID must be provided" + end + + def to_hash + { + message: message, + code: code + } + end + end +end diff --git a/app/errors/api/v1/NoJobSkillProvidedError.rb b/app/errors/api/v1/NoJobSkillProvidedError.rb new file mode 100644 index 0000000..60ad888 --- /dev/null +++ b/app/errors/api/v1/NoJobSkillProvidedError.rb @@ -0,0 +1,22 @@ +module Api::V1 + class NoJobSkillProvidedError < GranblueError + def http_status + 422 + end + + def code + "no_job_skill_provided" + end + + def message + "A job skill ID must be provided" + end + + def to_hash + { + message: message, + code: code + } + end + end +end diff --git a/app/errors/api/v1/SameFavoriteUserError.rb b/app/errors/api/v1/SameFavoriteUserError.rb index b948c1a..6fbcc79 100644 --- a/app/errors/api/v1/SameFavoriteUserError.rb +++ b/app/errors/api/v1/SameFavoriteUserError.rb @@ -1,22 +1,20 @@ -module Api::V1 - class SameFavoriteUserError < StandardError - def http_status - 422 - end +module Api + module V1 + class SameFavoriteUserError < GranblueError + def code + 'same_favorite_user' + end - def code - "same_favorite_user" - end + def message + 'Users cannot favorite their own parties' + end - def message - "Users cannot favorite their own parties" - end - - def to_hash - { - message: message, - code: code - } - end + def to_hash + { + message: message, + code: code + } + end end + end end diff --git a/app/errors/api/v1/TooManySkillsOfTypeError.rb b/app/errors/api/v1/TooManySkillsOfTypeError.rb new file mode 100644 index 0000000..3d0b4ab --- /dev/null +++ b/app/errors/api/v1/TooManySkillsOfTypeError.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Api + module V1 + class TooManySkillsOfTypeError < GranblueError + def code + 'too_many_skills_of_type' + end + + def message + 'You can only have up to 2 skills of type' + end + + def to_hash + { + message: message, + code: code, + skill_type: @data[:skill_type] + } + end + end + end +end diff --git a/app/models/job.rb b/app/models/job.rb index 1263a02..bded82e 100644 --- a/app/models/job.rb +++ b/app/models/job.rb @@ -1,7 +1,12 @@ class Job < ApplicationRecord - belongs_to :party + belongs_to :party - def display_resource(job) - job.name_en - end + belongs_to :base_job, + foreign_key: 'base_job_id', + class_name: 'Job', + optional: true + + def display_resource(job) + job.name_en + end end diff --git a/app/models/job_skill.rb b/app/models/job_skill.rb new file mode 100644 index 0000000..3ab1d84 --- /dev/null +++ b/app/models/job_skill.rb @@ -0,0 +1,33 @@ +class JobSkill < ApplicationRecord + alias eql? == + + include PgSearch::Model + + belongs_to :job + + pg_search_scope :en_search, + against: :name_en, + using: { + tsearch: { + prefix: true, + dictionary: "simple", + }, + } + + pg_search_scope :jp_search, + against: :name_jp, + using: { + tsearch: { + prefix: true, + dictionary: "simple", + }, + } + + def display_resource(skill) + skill.name_en + end + + def ==(o) + self.class == o.class && id == o.id + end +end diff --git a/app/models/party.rb b/app/models/party.rb index 62db4e8..8c6ef3d 100644 --- a/app/models/party.rb +++ b/app/models/party.rb @@ -1,28 +1,72 @@ class Party < ApplicationRecord -##### ActiveRecord Associations - belongs_to :user, optional: true - belongs_to :raid, optional: true - belongs_to :job, optional: true - - has_many :characters, - foreign_key: "party_id", - class_name: "GridCharacter", - dependent: :destroy + ##### ActiveRecord Associations + belongs_to :user, optional: true + belongs_to :raid, optional: true + belongs_to :job, optional: true - has_many :weapons, - foreign_key: "party_id", - class_name: "GridWeapon", - dependent: :destroy + belongs_to :skill0, + foreign_key: "skill0_id", + class_name: "JobSkill", + optional: true - has_many :summons, - foreign_key: "party_id", - class_name: "GridSummon", - dependent: :destroy - has_many :favorites - - attr_accessor :favorited + belongs_to :skill1, + foreign_key: "skill1_id", + class_name: "JobSkill", + optional: true - def is_favorited(user) - user.favorite_parties.include? self + belongs_to :skill2, + foreign_key: "skill2_id", + class_name: "JobSkill", + optional: true + + belongs_to :skill3, + foreign_key: "skill3_id", + class_name: "JobSkill", + optional: true + + has_many :characters, + foreign_key: "party_id", + class_name: "GridCharacter", + dependent: :destroy + + has_many :weapons, + foreign_key: "party_id", + class_name: "GridWeapon", + dependent: :destroy + + has_many :summons, + foreign_key: "party_id", + class_name: "GridSummon", + dependent: :destroy + + has_many :favorites + + ##### ActiveRecord Validations + validate :skills_are_unique + + attr_accessor :favorited + + def is_favorited(user) + user.favorite_parties.include? self + end + + private + + def skills_are_unique + skills = [skill0, skill1, skill2, skill3].compact + + if skills.uniq.length != skills.length + errors.add(:skill1, "must be unique") if skill0 == skill1 + + if skill0 == skill2 || skill1 == skill2 + errors.add(:skill2, "must be unique") + end + + if skill0 == skill3 || skill1 == skill3 || skill2 == skill3 + errors.add(:skill3, "must be unique") + end + + errors.add(:job_skills, "must be unique") end -end \ No newline at end of file + end +end diff --git a/app/views/api/v1/job_skills/all.json.rabl b/app/views/api/v1/job_skills/all.json.rabl new file mode 100644 index 0000000..ba30a1c --- /dev/null +++ b/app/views/api/v1/job_skills/all.json.rabl @@ -0,0 +1,3 @@ +collection @skills, object_root: false + +extends 'job_skills/base' diff --git a/app/views/api/v1/job_skills/base.json.rabl b/app/views/api/v1/job_skills/base.json.rabl new file mode 100644 index 0000000..03c6f1e --- /dev/null +++ b/app/views/api/v1/job_skills/base.json.rabl @@ -0,0 +1,10 @@ +object :job_skill + +attributes :id, :job, :slug, :color, :main, :base, :sub, :emp, :order + +node :name do |w| + { + :en => w.name_en, + :ja => w.name_jp + } +end diff --git a/app/views/api/v1/jobs/update.json.rabl b/app/views/api/v1/jobs/update.json.rabl new file mode 100644 index 0000000..6cb851f --- /dev/null +++ b/app/views/api/v1/jobs/update.json.rabl @@ -0,0 +1,20 @@ +object @party + +attributes :id, :user_id, :shortcode + +node :is_extra do |p| + p.extra +end + +node :job do |p| + partial("jobs/base", object: p.job) +end + +node :job_skills do |p| + { + "0" => partial("job_skills/base", object: p.skill0), + "1" => partial("job_skills/base", object: p.skill1), + "2" => partial("job_skills/base", object: p.skill2), + "3" => partial("job_skills/base", object: p.skill3), + } +end diff --git a/app/views/api/v1/parties/base.json.rabl b/app/views/api/v1/parties/base.json.rabl index 42dfc00..364c711 100644 --- a/app/views/api/v1/parties/base.json.rabl +++ b/app/views/api/v1/parties/base.json.rabl @@ -1,31 +1,47 @@ object :party -attributes :id, :name, :description, :element, :favorited, :shortcode, :created_at, :updated_at +attributes :id, + :name, + :description, + :element, + :favorited, + :shortcode, + :created_at, + :updated_at node :extra do |p| - p.extra + p.extra end node :user do |p| - partial('users/base', :object => p.user) + partial("users/base", object: p.user) end node :raid do |p| - partial('raids/base', :object => p.raid) + partial("raids/base", object: p.raid) end node :job do |p| - partial('jobs/base', :object => p.job) + partial("jobs/base", object: p.job) +end + +node :job_skills do |p| + { + "0" => partial("job_skills/base", object: p.skill0), + "1" => partial("job_skills/base", object: p.skill1), + "2" => partial("job_skills/base", object: p.skill2), + "3" => partial("job_skills/base", object: p.skill3), + } end node :characters do |p| - partial('grid_characters/base', :object => p.characters) + partial("grid_characters/base", object: p.characters) end node :weapons do |p| - partial('grid_weapons/base', :object => p.weapons) + partial("grid_weapons/base", object: p.weapons) end node :summons do |p| - partial('grid_summons/base', :object => p.summons) + partial("grid_summons/base", object: p.summons) end diff --git a/app/views/api/v1/parties/update.json.rabl b/app/views/api/v1/parties/update.json.rabl index fafd7e0..6cb851f 100644 --- a/app/views/api/v1/parties/update.json.rabl +++ b/app/views/api/v1/parties/update.json.rabl @@ -3,5 +3,18 @@ object @party attributes :id, :user_id, :shortcode node :is_extra do |p| - p.extra -end \ No newline at end of file + p.extra +end + +node :job do |p| + partial("jobs/base", object: p.job) +end + +node :job_skills do |p| + { + "0" => partial("job_skills/base", object: p.skill0), + "1" => partial("job_skills/base", object: p.skill1), + "2" => partial("job_skills/base", object: p.skill2), + "3" => partial("job_skills/base", object: p.skill3), + } +end diff --git a/app/views/api/v1/search/job_skills.json.rabl b/app/views/api/v1/search/job_skills.json.rabl new file mode 100644 index 0000000..100cca8 --- /dev/null +++ b/app/views/api/v1/search/job_skills.json.rabl @@ -0,0 +1,11 @@ +node :count do + @count +end + +node :total_pages do + (@count.to_f / 10 > 1) ? (@count.to_f / 10).ceil() : 1 +end + +node(:results) { + partial('job_skills/base', object: @skills) +} unless @skills.empty? diff --git a/config/routes.rb b/config/routes.rb index cf1c984..f7f94e2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,49 +1,57 @@ Rails.application.routes.draw do - use_doorkeeper do - controllers :tokens => 'tokens' - skip_controllers :applications, :authorized_applications - end - - namespace :api, defaults: { format: :json } do - namespace :v1 do - resources :parties, only: [:index, :create, :update, :destroy] - resources :users, only: [:create, :update, :show] - resources :grid_weapons, only: [:update] - resources :favorites, only: [:create] - - get 'users/info/:id', to: 'users#info' - - get 'parties/favorites', to: 'parties#favorites' - get 'parties/:id', to: 'parties#show' - get 'parties/:id/weapons', to: 'parties#weapons' - get 'parties/:id/summons', to: 'parties#summons' - get 'parties/:id/characters', to: 'parties#characters' - - post 'check/email', to: 'users#check_email' - post 'check/username', to: 'users#check_username' - - post 'search/characters', to: 'search#characters' - post 'search/weapons', to: 'search#weapons' - post 'search/summons', to: 'search#summons' - - get 'jobs', to: 'jobs#all' - get 'raids', to: 'raids#all' - get 'weapon_keys', to: 'weapon_keys#all' - - post 'characters', to: 'grid_characters#create' - post 'characters/resolve', to: 'grid_characters#resolve' - post 'characters/update_uncap', to: 'grid_characters#update_uncap_level' - delete 'characters', to: 'grid_characters#destroy' - - post 'weapons', to: 'grid_weapons#create' - post 'weapons/update_uncap', to: 'grid_weapons#update_uncap_level' - delete 'weapons', to: 'grid_weapons#destroy' - - post 'summons', to: 'grid_summons#create' - post 'summons/update_uncap', to: 'grid_summons#update_uncap_level' - delete 'summons', to: 'grid_summons#destroy' - - delete 'favorites', to: 'favorites#destroy' - end + use_doorkeeper do + controllers :tokens => 'tokens' + skip_controllers :applications, :authorized_applications + end + + namespace :api, defaults: { format: :json } do + namespace :v1 do + resources :parties, only: [:index, :create, :update, :destroy] + resources :users, only: [:create, :update, :show] + resources :grid_weapons, only: [:update] + resources :favorites, only: [:create] + + get 'users/info/:id', to: 'users#info' + + get 'parties/favorites', to: 'parties#favorites' + get 'parties/:id', to: 'parties#show' + get 'parties/:id/weapons', to: 'parties#weapons' + get 'parties/:id/summons', to: 'parties#summons' + get 'parties/:id/characters', to: 'parties#characters' + + put 'parties/:id/jobs', to: 'jobs#update_job' + put 'parties/:id/job_skills', to: 'jobs#update_job_skills' + + post 'check/email', to: 'users#check_email' + post 'check/username', to: 'users#check_username' + + post 'search/characters', to: 'search#characters' + post 'search/weapons', to: 'search#weapons' + post 'search/summons', to: 'search#summons' + post 'search/job_skills', to: 'search#job_skills' + + get 'jobs', to: 'jobs#all' + + get 'jobs/skills', to: 'job_skills#all' + get 'jobs/:id/skills', to: 'job_skills#job' + + get 'raids', to: 'raids#all' + get 'weapon_keys', to: 'weapon_keys#all' + + post 'characters', to: 'grid_characters#create' + post 'characters/resolve', to: 'grid_characters#resolve' + post 'characters/update_uncap', to: 'grid_characters#update_uncap_level' + delete 'characters', to: 'grid_characters#destroy' + + post 'weapons', to: 'grid_weapons#create' + post 'weapons/update_uncap', to: 'grid_weapons#update_uncap_level' + delete 'weapons', to: 'grid_weapons#destroy' + + post 'summons', to: 'grid_summons#create' + post 'summons/update_uncap', to: 'grid_summons#update_uncap_level' + delete 'summons', to: 'grid_summons#destroy' + + delete 'favorites', to: 'favorites#destroy' end + end end diff --git a/db/migrate/20221120055510_add_job_skills_table.rb b/db/migrate/20221120055510_add_job_skills_table.rb new file mode 100644 index 0000000..b21a873 --- /dev/null +++ b/db/migrate/20221120055510_add_job_skills_table.rb @@ -0,0 +1,14 @@ +class AddJobSkillsTable < ActiveRecord::Migration[6.1] + def change + create_table :job_skills, id: :uuid, default: -> { "gen_random_uuid()" } do |t| + t.references :job, type: :uuid + t.string :name_en, null: false, unique: true + t.string :name_jp, null: false, unique: true + t.string :slug, null: false, unique: true + t.integer :color, null: false + t.boolean :main, default: false + t.boolean :sub, default: false + t.boolean :emp, default: false + end + end +end diff --git a/db/migrate/20221120065331_remove_null_constraint_from_job_skills.rb b/db/migrate/20221120065331_remove_null_constraint_from_job_skills.rb new file mode 100644 index 0000000..8aa1a7f --- /dev/null +++ b/db/migrate/20221120065331_remove_null_constraint_from_job_skills.rb @@ -0,0 +1,6 @@ +class RemoveNullConstraintFromJobSkills < ActiveRecord::Migration[6.1] + def change + change_column :job_skills, :name_en, :string, unique: false + change_column :job_skills, :name_jp, :string, unique: false + end +end diff --git a/db/migrate/20221120075133_add_order_to_job_skills.rb b/db/migrate/20221120075133_add_order_to_job_skills.rb new file mode 100644 index 0000000..c244627 --- /dev/null +++ b/db/migrate/20221120075133_add_order_to_job_skills.rb @@ -0,0 +1,5 @@ +class AddOrderToJobSkills < ActiveRecord::Migration[6.1] + def change + add_column :job_skills, :order, :integer + end +end diff --git a/db/migrate/20221120135403_add_base_and_group_to_job_skills.rb b/db/migrate/20221120135403_add_base_and_group_to_job_skills.rb new file mode 100644 index 0000000..1644821 --- /dev/null +++ b/db/migrate/20221120135403_add_base_and_group_to_job_skills.rb @@ -0,0 +1,6 @@ +class AddBaseAndGroupToJobSkills < ActiveRecord::Migration[6.1] + def change + add_column :job_skills, :base, :boolean, default: false + add_column :job_skills, :group, :integer + end +end diff --git a/db/migrate/20221120145204_remove_group_from_job_skills.rb b/db/migrate/20221120145204_remove_group_from_job_skills.rb new file mode 100644 index 0000000..6f82de9 --- /dev/null +++ b/db/migrate/20221120145204_remove_group_from_job_skills.rb @@ -0,0 +1,5 @@ +class RemoveGroupFromJobSkills < ActiveRecord::Migration[6.1] + def change + remove_column :job_skills, :group, :integer + end +end diff --git a/db/migrate/20221130155225_add_job_skills_to_party.rb b/db/migrate/20221130155225_add_job_skills_to_party.rb new file mode 100644 index 0000000..d993378 --- /dev/null +++ b/db/migrate/20221130155225_add_job_skills_to_party.rb @@ -0,0 +1,9 @@ +class AddJobSkillsToParty < ActiveRecord::Migration[6.1] + def change + change_table(:parties) do |t| + t.references :skill1, type: :uuid, foreign_key: { to_table: 'job_skills' } + t.references :skill2, type: :uuid, foreign_key: { to_table: 'job_skills' } + t.references :skill3, type: :uuid, foreign_key: { to_table: 'job_skills' } + end + end +end diff --git a/db/migrate/20221201123645_add_skill0_to_party.rb b/db/migrate/20221201123645_add_skill0_to_party.rb new file mode 100644 index 0000000..f5e66d4 --- /dev/null +++ b/db/migrate/20221201123645_add_skill0_to_party.rb @@ -0,0 +1,7 @@ +class AddSkill0ToParty < ActiveRecord::Migration[6.1] + def change + change_table(:parties) do |t| + t.references :skill0, type: :uuid, foreign_key: { to_table: "job_skills" } + end + end +end diff --git a/db/migrate/20221203112452_add_base_job_to_jobs.rb b/db/migrate/20221203112452_add_base_job_to_jobs.rb new file mode 100644 index 0000000..e689758 --- /dev/null +++ b/db/migrate/20221203112452_add_base_job_to_jobs.rb @@ -0,0 +1,7 @@ +class AddBaseJobToJobs < ActiveRecord::Migration[6.1] + def change + change_table(:jobs) do |t| + t.references :base_job, type: :uuid, foreign_key: { to_table: 'jobs' } + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 9d4fe9d..52d1af4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_11_17_070255) do +ActiveRecord::Schema.define(version: 2022_12_03_112452) do # These are extensions that must be enabled in order to support this database enable_extension "btree_gin" @@ -102,6 +102,20 @@ ActiveRecord::Schema.define(version: 2022_11_17_070255) do t.index ["weapon_id"], name: "index_grid_weapons_on_weapon_id" end + create_table "job_skills", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.uuid "job_id" + t.string "name_en", null: false + t.string "name_jp", null: false + t.string "slug", null: false + t.integer "color", null: false + t.boolean "main", default: false + t.boolean "sub", default: false + t.boolean "emp", default: false + t.integer "order" + t.boolean "base", default: false + t.index ["job_id"], name: "index_job_skills_on_job_id" + end + create_table "jobs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.string "name_en" t.string "name_jp" @@ -110,6 +124,8 @@ ActiveRecord::Schema.define(version: 2022_11_17_070255) do t.string "row" t.boolean "ml", default: false t.integer "order" + t.uuid "base_job_id" + t.index ["base_job_id"], name: "index_jobs_on_base_job_id" end create_table "oauth_access_grants", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| @@ -163,7 +179,15 @@ ActiveRecord::Schema.define(version: 2022_11_17_070255) do t.integer "weapons_count" t.uuid "job_id" t.integer "ml" + t.uuid "skill1_id" + t.uuid "skill2_id" + t.uuid "skill3_id" + t.uuid "skill0_id" t.index ["job_id"], name: "index_parties_on_job_id" + t.index ["skill0_id"], name: "index_parties_on_skill0_id" + t.index ["skill1_id"], name: "index_parties_on_skill1_id" + t.index ["skill2_id"], name: "index_parties_on_skill2_id" + t.index ["skill3_id"], name: "index_parties_on_skill3_id" t.index ["user_id"], name: "index_parties_on_user_id" end @@ -257,8 +281,13 @@ ActiveRecord::Schema.define(version: 2022_11_17_070255) do add_foreign_key "grid_weapons", "parties" add_foreign_key "grid_weapons", "weapon_keys", column: "weapon_key3_id" add_foreign_key "grid_weapons", "weapons" + add_foreign_key "jobs", "jobs", column: "base_job_id" add_foreign_key "oauth_access_grants", "oauth_applications", column: "application_id" add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id" + add_foreign_key "parties", "job_skills", column: "skill0_id" + add_foreign_key "parties", "job_skills", column: "skill1_id" + add_foreign_key "parties", "job_skills", column: "skill2_id" + add_foreign_key "parties", "job_skills", column: "skill3_id" add_foreign_key "parties", "jobs" add_foreign_key "parties", "raids" add_foreign_key "parties", "users"